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:
Miguel Palhas
2026-08-24 11:40:44 +01:00
parent 2495895717
commit 8f7373d73d
6 changed files with 176 additions and 23 deletions
+59 -6
View File
@@ -168,7 +168,13 @@ impl SeriesRefreshAction {
});
}
}
apply_auto_track(&core_series(stale), &mut revealed);
// #160: the fact of a previous refresh rides in from the row, so the
// seeding refresh never tracks the back catalogue it reveals.
apply_auto_track(
&core_series(stale),
stale.metadata_refreshed_at.is_some(),
&mut revealed,
);
for refreshed in revealed {
self.insert_revealed_season(&mut transaction, stale.id, refreshed)
.await?;
@@ -779,8 +785,11 @@ mod tests {
assert_eq!(vote, Some(8.417));
}
/// #160. A series' first refresh reveals its back catalogue, but §4.1
/// never tracks what was already there at add time: nothing is tracked,
/// nothing arrives wanted.
#[tokio::test]
async fn first_refresh_reveals_seasons_and_applies_auto_track() {
async fn the_seeding_refresh_reveals_but_tracks_nothing() {
let (_dir, database) = seeded_series(true).await;
let server = tmdb("Returning Series", season_one_body(&two_episodes())).await;
@@ -792,14 +801,14 @@ mod tests {
.fetch_all(database.pool())
.await
.unwrap();
assert_eq!(rows, vec![(1, true), (2, true)]);
assert_eq!(rows, vec![(1, false), (2, false)]);
let wanted: i64 = sqlx::query_scalar(
"SELECT count(*) FROM episodes WHERE wanted = 1 AND state = 'missing'",
)
.fetch_one(database.pool())
.await
.unwrap();
assert_eq!(wanted, 3);
assert_eq!(wanted, 0);
// #120 backfill: the series was added before it had an id on file.
let tvdb_id: Option<i64> =
sqlx::query_scalar("SELECT tvdb_id FROM series WHERE tmdb_id = 82728")
@@ -816,6 +825,50 @@ mod tests {
assert_eq!(stamped, 1);
}
/// #160. Once the series has been refreshed before, a season TMDB reveals
/// that the library has never seen genuinely appeared after add time, so
/// it is tracked and its episodes arrive wanted.
#[tokio::test]
async fn a_later_refresh_tracks_a_genuinely_new_season() {
let (_dir, database) = seeded_series(true).await;
let server = MockServer::start().await;
mount_with(&server, "Returning Series", &[1]).await;
action(&server).tick(&database).await.unwrap();
let tracked: i64 = sqlx::query_scalar("SELECT count(*) FROM seasons WHERE tracked = 1")
.fetch_one(database.pool())
.await
.unwrap();
assert_eq!(tracked, 0, "the seeding refresh tracks nothing");
expire_refresh(&database).await;
server.reset().await;
mount(
&server,
"Returning Series",
season_one_body(&two_episodes()),
)
.await;
let outcomes = action(&server).tick(&database).await.unwrap();
assert_eq!(outcomes.len(), 1);
let rows: Vec<(i64, bool)> =
sqlx::query_as("SELECT number, tracked FROM seasons ORDER BY number")
.fetch_all(database.pool())
.await
.unwrap();
assert_eq!(rows, vec![(1, false), (2, true)]);
let wanted: Vec<i64> = sqlx::query_scalar(
"SELECT e.number FROM episodes e JOIN seasons s ON s.id = e.season_id
WHERE s.number = 2 AND e.wanted = 1",
)
.fetch_all(database.pool())
.await
.unwrap();
assert_eq!(wanted, vec![1]);
}
/// `auto_track` is a rule about reveals, not intent: without it nothing
/// arrives wanted, even though everything is revealed.
#[tokio::test]
@@ -877,13 +930,13 @@ mod tests {
.fetch_all(database.pool())
.await
.unwrap();
assert_eq!(rows, vec![(1, true), (2, true)]);
assert_eq!(rows, vec![(1, false), (2, false)]);
let counts: (i64, i64) =
sqlx::query_as("SELECT count(*), coalesce(sum(wanted), 0) FROM episodes")
.fetch_one(database.pool())
.await
.unwrap();
assert_eq!(counts, (3, 3));
assert_eq!(counts, (3, 0));
}
/// Episodes revealed into an existing season follow that season's rule: