diff --git a/crates/arr-core/src/lib.rs b/crates/arr-core/src/lib.rs index 3449d2d..11f2517 100644 --- a/crates/arr-core/src/lib.rs +++ b/crates/arr-core/src/lib.rs @@ -7,6 +7,7 @@ use std::{collections::BTreeMap, path::PathBuf, time::SystemTime}; pub mod lang; pub mod policy; pub mod score; +pub mod tracking; pub use arr_parse::NameClaims as ParsedRelease; pub use score::{Score, ScoreWeights}; diff --git a/crates/arr-core/src/tracking.rs b/crates/arr-core/src/tracking.rs new file mode 100644 index 0000000..8aa56ab --- /dev/null +++ b/crates/arr-core/src/tracking.rs @@ -0,0 +1,86 @@ +use crate::{Episode, Series}; + +/// Applies a series' tracking rule to episodes discovered in a new season. +/// +/// Callers pass only episodes from a newly revealed season. Existing seasons +/// keep their leaf-level intent unchanged. +pub fn apply_new_season_auto_track(series: &Series, episodes: &mut [Episode]) { + if series.auto_track { + for episode in episodes { + episode.wanted = true; + } + } +} + +#[cfg(test)] +mod tests { + use std::time::SystemTime; + + use super::apply_new_season_auto_track; + use crate::{ + Episode, EpisodeId, Language, MediaState, RootId, SeasonId, Series, SeriesId, + TitleOverrides, + }; + + fn series(auto_track: bool) -> Series { + Series { + id: SeriesId(1), + tmdb_id: 82_728, + title: "Bluey".into(), + year: 2018, + original_language: Language::Other("en".into()), + root_id: RootId(1), + auto_track, + overrides: TitleOverrides::default(), + upstream_ended: false, + blocked: false, + } + } + + fn episode(season: i64, number: u16, wanted: bool) -> Episode { + Episode { + id: EpisodeId(season * 100 + i64::from(number)), + season_id: SeasonId(season), + number, + title: format!("Episode {number}"), + air_date: Some(SystemTime::UNIX_EPOCH), + wanted, + state: MediaState::Missing, + search_attempts: 0, + last_searched_at: None, + } + } + + #[test] + fn auto_track_marks_new_season_episodes_wanted() { + let mut new_season = vec![episode(3, 1, false), episode(3, 2, false)]; + + apply_new_season_auto_track(&series(true), &mut new_season); + + assert!(new_season.iter().all(|episode| episode.wanted)); + } + + #[test] + fn untracked_series_keeps_leaf_intent_only() { + let manually_wanted_season = [ + episode(2, 1, true), + episode(2, 2, true), + episode(2, 3, true), + ]; + let mut new_season = vec![episode(3, 1, false), episode(3, 2, false)]; + + apply_new_season_auto_track(&series(false), &mut new_season); + + assert!(manually_wanted_season.iter().all(|episode| episode.wanted)); + assert!(new_season.iter().all(|episode| !episode.wanted)); + } + + #[test] + fn auto_track_does_not_clear_explicit_leaf_intent() { + let mut new_season = vec![episode(3, 1, true), episode(3, 2, false)]; + + apply_new_season_auto_track(&series(true), &mut new_season); + + assert!(new_season.iter().all(|episode| episode.wanted)); + } +}