fix(core): auto-track skips the seeding refresh
A series' first metadata refresh reveals its whole back catalogue, and apply_auto_track flagged every season as new — adding Rick and Morty tracked S01-S09 and wanted 91 episodes (#160). Per DESIGN.md 4.1 the rule applies from the second refresh onward. The caller passes whether metadata_refreshed_at is set; arr-core stays IO-free and decides. Wired through the daemon's daily refresh and the API's create-season endpoint.
This commit is contained in:
@@ -208,6 +208,9 @@ struct SeriesRow {
|
||||
poster_path: Option<String>,
|
||||
/// TMDB's rating, out of 10; `null` when TMDB has no votes for it.
|
||||
vote_average: Option<f64>,
|
||||
/// NULL until a metadata refresh has stamped it (#160): the fact the
|
||||
/// auto-track rule needs to tell a seeding refresh from a later one.
|
||||
metadata_refreshed_at: Option<String>,
|
||||
}
|
||||
|
||||
struct EpisodeRow {
|
||||
@@ -353,7 +356,7 @@ async fn tv_by_series(state: &AppState) -> Result<HashMap<i64, Vec<arr_core::Epi
|
||||
}
|
||||
|
||||
async fn load_series_row(state: &AppState, id: i64) -> Result<SeriesRow, ApiError> {
|
||||
sqlx::query_as!(SeriesRow, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", tvdb_id, title AS "title!: String", year, original_language, root_id AS "root_id!: i64", auto_track AS "auto_track!: bool", overrides AS "overrides!: serde_json::Value", upstream_ended AS "upstream_ended!: bool", blocked AS "blocked!: bool", poster_path, vote_average FROM series WHERE id = ?"#, id)
|
||||
sqlx::query_as!(SeriesRow, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", tvdb_id, title AS "title!: String", year, original_language, root_id AS "root_id!: i64", auto_track AS "auto_track!: bool", overrides AS "overrides!: serde_json::Value", upstream_ended AS "upstream_ended!: bool", blocked AS "blocked!: bool", poster_path, vote_average, metadata_refreshed_at FROM series WHERE id = ?"#, id)
|
||||
.fetch_optional(pool(state)?)
|
||||
.await?
|
||||
.ok_or(ApiError::SeriesNotFound)
|
||||
@@ -407,11 +410,11 @@ pub async fn list(
|
||||
Query(query): Query<ListSeriesQuery>,
|
||||
) -> Result<Json<Vec<Series>>, ApiError> {
|
||||
let rows = if let Some(owner_id) = query.owner_id {
|
||||
sqlx::query_as!(SeriesRow, r#"SELECT s.id AS "id!: i64", s.tmdb_id AS "tmdb_id!: i64", s.tvdb_id, s.title AS "title!: String", s.year, s.original_language, s.root_id AS "root_id!: i64", s.auto_track AS "auto_track!: bool", s.overrides AS "overrides!: serde_json::Value", s.upstream_ended AS "upstream_ended!: bool", s.blocked AS "blocked!: bool", poster_path, vote_average FROM series s JOIN title_owners t ON t.title_kind = 'series' AND t.title_id = s.id WHERE t.owner_id = ? ORDER BY s.title, s.year, s.id"#, owner_id)
|
||||
sqlx::query_as!(SeriesRow, r#"SELECT s.id AS "id!: i64", s.tmdb_id AS "tmdb_id!: i64", s.tvdb_id, s.title AS "title!: String", s.year, s.original_language, s.root_id AS "root_id!: i64", s.auto_track AS "auto_track!: bool", s.overrides AS "overrides!: serde_json::Value", s.upstream_ended AS "upstream_ended!: bool", s.blocked AS "blocked!: bool", poster_path, vote_average, s.metadata_refreshed_at FROM series s JOIN title_owners t ON t.title_kind = 'series' AND t.title_id = s.id WHERE t.owner_id = ? ORDER BY s.title, s.year, s.id"#, owner_id)
|
||||
.fetch_all(pool(&state)?)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as!(SeriesRow, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", tvdb_id, title AS "title!: String", year, original_language, root_id AS "root_id!: i64", auto_track AS "auto_track!: bool", overrides AS "overrides!: serde_json::Value", upstream_ended AS "upstream_ended!: bool", blocked AS "blocked!: bool", poster_path, vote_average FROM series ORDER BY title, year, id"#)
|
||||
sqlx::query_as!(SeriesRow, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", tvdb_id, title AS "title!: String", year, original_language, root_id AS "root_id!: i64", auto_track AS "auto_track!: bool", overrides AS "overrides!: serde_json::Value", upstream_ended AS "upstream_ended!: bool", blocked AS "blocked!: bool", poster_path, vote_average, metadata_refreshed_at FROM series ORDER BY title, year, id"#)
|
||||
.fetch_all(pool(&state)?)
|
||||
.await?
|
||||
};
|
||||
@@ -765,6 +768,9 @@ pub async fn create_season(
|
||||
|
||||
// §4.1. The request does not say whether the episodes are wanted; the
|
||||
// series' auto_track rule does, through the one function that owns it.
|
||||
// #160: whether a refresh has happened yet rides in from the row — a
|
||||
// hand-added season on a never-refreshed series is still seeding, and
|
||||
// §4.1 does not track what was there at add time.
|
||||
let mut revealed = [RefreshedSeason {
|
||||
season: arr_core::Season {
|
||||
id: SeasonId(0),
|
||||
@@ -790,7 +796,11 @@ pub async fn create_season(
|
||||
.collect(),
|
||||
is_new: true,
|
||||
}];
|
||||
apply_auto_track(&core_series(&series), &mut revealed);
|
||||
apply_auto_track(
|
||||
&core_series(&series),
|
||||
series.metadata_refreshed_at.is_some(),
|
||||
&mut revealed,
|
||||
);
|
||||
let [revealed] = revealed;
|
||||
|
||||
// One transaction: a rejected episode must not leave the season behind,
|
||||
@@ -1457,15 +1467,21 @@ mod tests {
|
||||
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
/// #160. A season added before the series' first metadata refresh is part
|
||||
/// of the back catalogue at add time, so the rule does not track it. Once
|
||||
/// a refresh has happened, a hand-revealed season is genuinely new and
|
||||
/// arrives tracked with its episodes wanted.
|
||||
#[tokio::test]
|
||||
async fn auto_track_decides_whether_a_new_season_arrives_wanted() {
|
||||
let (_dir, state, base) = application().await;
|
||||
let root_id = tv_root(&state, "kids").await;
|
||||
|
||||
let tracked = add_series(&base, root_id, true).await;
|
||||
let tracked_id = tracked["id"].as_i64().expect("id");
|
||||
// Seeding: no refresh has ever stamped this series.
|
||||
let season = add_season(
|
||||
&base,
|
||||
tracked["id"].as_i64().expect("id"),
|
||||
tracked_id,
|
||||
1,
|
||||
serde_json::json!([
|
||||
{"number": 1, "title": "The Magic Xylophone", "air_date": "2018-10-01"},
|
||||
@@ -1473,6 +1489,33 @@ mod tests {
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(season["tracked"], false);
|
||||
assert!(
|
||||
season["episodes"]
|
||||
.as_array()
|
||||
.expect("episodes")
|
||||
.iter()
|
||||
.all(|episode| episode["wanted"] == false),
|
||||
"§4.1: the back catalogue at add time is never auto-tracked"
|
||||
);
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE series SET metadata_refreshed_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
||||
WHERE id = ?",
|
||||
)
|
||||
.bind(tracked_id)
|
||||
.execute(state.database().expect("database").pool())
|
||||
.await
|
||||
.expect("stamp refreshed");
|
||||
let season = add_season(
|
||||
&base,
|
||||
tracked_id,
|
||||
2,
|
||||
serde_json::json!([
|
||||
{"number": 1, "title": "Dance Mode", "air_date": "2019-04-01"}
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(season["tracked"], true);
|
||||
assert!(
|
||||
season["episodes"]
|
||||
@@ -1480,7 +1523,7 @@ mod tests {
|
||||
.expect("episodes")
|
||||
.iter()
|
||||
.all(|episode| episode["wanted"] == true),
|
||||
"§4.1: auto_track marks the episodes of a revealed season wanted"
|
||||
"§4.1: from the second refresh onward the rule tracks what is new"
|
||||
);
|
||||
|
||||
let untracked_id = sqlx::query_scalar::<_, i64>(
|
||||
|
||||
Reference in New Issue
Block a user