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
+44 -5
View File
@@ -13,8 +13,14 @@ pub struct RefreshedSeason {
/// Existing seasons keep their tracking rule and leaf-level intent unchanged.
/// Season 0 is never tracked by the rule (`DESIGN.md` §4.1); an operator may
/// still track it by hand.
pub fn apply_auto_track(series: &Series, seasons: &mut [RefreshedSeason]) {
if !series.auto_track {
///
/// `ever_refreshed` says whether the series has had a metadata refresh yet
/// (`metadata_refreshed_at IS NOT NULL`). The caller supplies the fact because
/// `arr-core` has no IO; seasons revealed by a series' first refresh are its
/// back catalogue as it stood at add time, and §4.1 never tracks those — the
/// rule applies from the second refresh onward.
pub fn apply_auto_track(series: &Series, ever_refreshed: bool, seasons: &mut [RefreshedSeason]) {
if !series.auto_track || !ever_refreshed {
return;
}
@@ -114,7 +120,7 @@ mod tests {
refreshed_season(3, true, &[false, false]),
];
apply_auto_track(&series(true), &mut refresh);
apply_auto_track(&series(true), true, &mut refresh);
assert!(!refresh[0].season.tracked);
assert!(refresh[0].episodes.iter().all(|episode| !episode.wanted));
@@ -122,6 +128,39 @@ mod tests {
assert!(refresh[1].episodes.iter().all(|episode| episode.wanted));
}
/// #160. The first refresh of a newly added series reveals the whole back
/// catalogue at once; §4.1 never tracks those.
#[test]
fn the_seeding_refresh_tracks_nothing() {
let mut refresh = [
refreshed_season(1, true, &[false, false]),
refreshed_season(2, true, &[false]),
];
apply_auto_track(&series(true), false, &mut refresh);
assert!(refresh.iter().all(|refreshed| !refreshed.season.tracked));
assert!(refresh
.iter()
.all(|refreshed| refreshed.episodes.iter().all(|episode| !episode.wanted)));
}
/// #160. From the second refresh onward a season that genuinely did not
/// exist when the series was added is tracked, and its episodes arrive
/// wanted — while the seasons already on record stay as they were.
#[test]
fn a_later_refresh_tracks_a_genuinely_new_season() {
let mut refresh = [
refreshed_season(1, false, &[true, true]),
refreshed_season(3, true, &[false]),
];
apply_auto_track(&series(true), true, &mut refresh);
assert!(refresh[1].season.tracked);
assert!(refresh[1].episodes.iter().all(|episode| episode.wanted));
}
#[test]
fn untracked_series_keeps_manual_leaf_intent_only() {
let mut refresh = [
@@ -129,7 +168,7 @@ mod tests {
refreshed_season(3, true, &[false, false]),
];
apply_auto_track(&series(false), &mut refresh);
apply_auto_track(&series(false), true, &mut refresh);
assert!(refresh[0].episodes.iter().all(|episode| episode.wanted));
assert!(!refresh[0].season.tracked);
@@ -141,7 +180,7 @@ mod tests {
fn auto_track_skips_season_zero() {
let mut refresh = [refreshed_season(0, true, &[false])];
apply_auto_track(&series(true), &mut refresh);
apply_auto_track(&series(true), true, &mut refresh);
assert!(!refresh[0].season.tracked);
assert!(refresh[0].episodes.iter().all(|episode| !episode.wanted));