core: carry season number on Episode
derive_series_status identified specials by looking the episode's season up in a parallel seasons slice, so a caller passing an incomplete slice silently reverted to pre-#118 behaviour. The season number now rides on each episode and the slice is gone.
This commit is contained in:
@@ -288,6 +288,9 @@ pub struct Season {
|
||||
pub struct Episode {
|
||||
pub id: EpisodeId,
|
||||
pub season_id: SeasonId,
|
||||
/// The owning season's number, carried so status derivation never needs
|
||||
/// a second slice a caller can forget (#131).
|
||||
pub season_number: u16,
|
||||
pub number: u16,
|
||||
pub title: String,
|
||||
pub air_date: Option<SystemTime>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use crate::{Episode, MediaState, Season, Series};
|
||||
use crate::{Episode, MediaState, Series};
|
||||
|
||||
const AIRING_WINDOW: Duration = Duration::from_hours(14 * 24);
|
||||
|
||||
@@ -16,26 +16,19 @@ pub enum SeriesStatus {
|
||||
|
||||
/// Derives a series' status at `now` without persisting lifecycle intent.
|
||||
///
|
||||
/// Season 0 is invisible to all of it (`DESIGN.md` §4.2): episodes in a
|
||||
/// season 0 listed here are ignored, so a manually wanted special cannot pin
|
||||
/// the series at `incomplete` or hold back `ended`. The seasons must be the
|
||||
/// series' own; an episode whose season is not listed counts like any other.
|
||||
/// Season 0 is invisible to all of it (`DESIGN.md` §4.2): episodes with
|
||||
/// `season_number` 0 are ignored, so a manually wanted special cannot pin
|
||||
/// the series at `incomplete` or hold back `ended`. The season number rides
|
||||
/// on each episode, so there is no second slice a caller can forget (#131).
|
||||
#[must_use]
|
||||
pub fn derive_series_status(
|
||||
series: &Series,
|
||||
seasons: &[Season],
|
||||
episodes: &[Episode],
|
||||
now: SystemTime,
|
||||
) -> SeriesStatus {
|
||||
// A series has at most one season 0, so this is a comparison rather than
|
||||
// a set: no allocation on a function the list view calls per series.
|
||||
let specials = seasons
|
||||
.iter()
|
||||
.find(|season| season.number == 0)
|
||||
.map(|season| season.id);
|
||||
let wanted = episodes
|
||||
.iter()
|
||||
.filter(|episode| episode.wanted && specials != Some(episode.season_id));
|
||||
.filter(|episode| episode.wanted && episode.season_number != 0);
|
||||
let complete = wanted
|
||||
.clone()
|
||||
.all(|episode| episode.state == MediaState::Available);
|
||||
@@ -107,6 +100,7 @@ mod tests {
|
||||
Episode {
|
||||
id: EpisodeId(i64::from(number)),
|
||||
season_id: SeasonId(1),
|
||||
season_number: 1,
|
||||
number,
|
||||
title: format!("Episode {number}"),
|
||||
air_date,
|
||||
@@ -117,23 +111,13 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The season every default-test episode sits in.
|
||||
fn seasons() -> [Season; 1] {
|
||||
[Season {
|
||||
id: SeasonId(1),
|
||||
series_id: SeriesId(1),
|
||||
number: 1,
|
||||
tracked: true,
|
||||
}]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn airing_when_a_wanted_episode_is_in_the_window() {
|
||||
let now = SystemTime::UNIX_EPOCH + 100 * DAY;
|
||||
for air_date in [now - 14 * DAY, now + 14 * DAY] {
|
||||
let episodes = [episode(1, Some(air_date), MediaState::Missing)];
|
||||
assert_eq!(
|
||||
derive_series_status(&series(true, false), &seasons(), &episodes, now),
|
||||
derive_series_status(&series(true, false), &episodes, now),
|
||||
SeriesStatus::Airing
|
||||
);
|
||||
}
|
||||
@@ -145,7 +129,7 @@ mod tests {
|
||||
MediaState::Missing,
|
||||
)];
|
||||
assert_eq!(
|
||||
derive_series_status(&series(true, false), &seasons(), &episodes, now),
|
||||
derive_series_status(&series(true, false), &episodes, now),
|
||||
SeriesStatus::Waiting
|
||||
);
|
||||
}
|
||||
@@ -156,7 +140,7 @@ mod tests {
|
||||
let episodes = [episode(1, Some(now - 20 * DAY), MediaState::Missing)];
|
||||
|
||||
assert_eq!(
|
||||
derive_series_status(&series(true, false), &seasons(), &episodes, now),
|
||||
derive_series_status(&series(true, false), &episodes, now),
|
||||
SeriesStatus::Incomplete
|
||||
);
|
||||
}
|
||||
@@ -167,7 +151,7 @@ mod tests {
|
||||
let episodes = [episode(1, Some(now + 30 * DAY), MediaState::Missing)];
|
||||
|
||||
assert_eq!(
|
||||
derive_series_status(&series(true, false), &seasons(), &episodes, now),
|
||||
derive_series_status(&series(true, false), &episodes, now),
|
||||
SeriesStatus::Waiting
|
||||
);
|
||||
}
|
||||
@@ -178,7 +162,7 @@ mod tests {
|
||||
let episodes = [episode(1, Some(now), MediaState::Available)];
|
||||
|
||||
assert_eq!(
|
||||
derive_series_status(&series(false, false), &seasons(), &episodes, now),
|
||||
derive_series_status(&series(false, false), &episodes, now),
|
||||
SeriesStatus::Complete
|
||||
);
|
||||
}
|
||||
@@ -189,7 +173,7 @@ mod tests {
|
||||
let episodes = [episode(1, Some(now), MediaState::Available)];
|
||||
|
||||
assert_eq!(
|
||||
derive_series_status(&series(true, true), &seasons(), &episodes, now),
|
||||
derive_series_status(&series(true, true), &episodes, now),
|
||||
SeriesStatus::Ended
|
||||
);
|
||||
}
|
||||
@@ -199,14 +183,14 @@ mod tests {
|
||||
let now = SystemTime::UNIX_EPOCH + 100 * DAY;
|
||||
let mut episodes = [episode(1, Some(now - 20 * DAY), MediaState::Missing)];
|
||||
assert_eq!(
|
||||
derive_series_status(&series(false, false), &seasons(), &episodes, now),
|
||||
derive_series_status(&series(false, false), &episodes, now),
|
||||
SeriesStatus::Incomplete
|
||||
);
|
||||
|
||||
episodes[0].state = MediaState::Available;
|
||||
|
||||
assert_eq!(
|
||||
derive_series_status(&series(false, false), &seasons(), &episodes, now),
|
||||
derive_series_status(&series(false, false), &episodes, now),
|
||||
SeriesStatus::Complete
|
||||
);
|
||||
}
|
||||
@@ -216,14 +200,14 @@ mod tests {
|
||||
let now = SystemTime::UNIX_EPOCH + 100 * DAY;
|
||||
let mut episodes = vec![episode(1, Some(now - 30 * DAY), MediaState::Available)];
|
||||
assert_eq!(
|
||||
derive_series_status(&series(true, false), &seasons(), &episodes, now),
|
||||
derive_series_status(&series(true, false), &episodes, now),
|
||||
SeriesStatus::Waiting
|
||||
);
|
||||
|
||||
episodes.push(episode(2, Some(now + 7 * DAY), MediaState::Missing));
|
||||
|
||||
assert_eq!(
|
||||
derive_series_status(&series(true, false), &seasons(), &episodes, now),
|
||||
derive_series_status(&series(true, false), &episodes, now),
|
||||
SeriesStatus::Airing
|
||||
);
|
||||
}
|
||||
@@ -231,34 +215,30 @@ mod tests {
|
||||
#[test]
|
||||
fn missing_specials_do_not_hold_back_complete_or_ended() {
|
||||
let now = SystemTime::UNIX_EPOCH + 100 * DAY;
|
||||
let specials = [Season {
|
||||
id: SeasonId(9),
|
||||
series_id: SeriesId(1),
|
||||
number: 0,
|
||||
tracked: false,
|
||||
}];
|
||||
let mut episodes = vec![episode(1, Some(now - 30 * DAY), MediaState::Available)];
|
||||
let mut special = episode(1, Some(now - 90 * DAY), MediaState::Missing);
|
||||
special.season_id = SeasonId(9);
|
||||
special.season_number = 0;
|
||||
|
||||
assert_eq!(
|
||||
derive_series_status(&series(true, false), &specials, &episodes, now),
|
||||
derive_series_status(&series(true, false), &episodes, now),
|
||||
SeriesStatus::Waiting
|
||||
);
|
||||
|
||||
episodes.push(special);
|
||||
assert_eq!(
|
||||
derive_series_status(&series(true, false), &[], &episodes, now),
|
||||
SeriesStatus::Incomplete,
|
||||
"without knowing the seasons, the special pins the series"
|
||||
derive_series_status(&series(true, false), &episodes, now),
|
||||
SeriesStatus::Waiting,
|
||||
"#131: the season number rides on the episode, so the special is \
|
||||
invisible however the caller builds the slice"
|
||||
);
|
||||
assert_eq!(
|
||||
derive_series_status(&series(false, false), &specials, &episodes, now),
|
||||
derive_series_status(&series(false, false), &episodes, now),
|
||||
SeriesStatus::Complete,
|
||||
"§4.2: the only gap is a special"
|
||||
);
|
||||
assert_eq!(
|
||||
derive_series_status(&series(false, true), &specials, &episodes, now),
|
||||
derive_series_status(&series(false, true), &episodes, now),
|
||||
SeriesStatus::Ended
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ mod tests {
|
||||
Episode {
|
||||
id: EpisodeId(season * 100 + i64::from(number)),
|
||||
season_id: SeasonId(season),
|
||||
season_number: u16::try_from(season).unwrap_or_default(),
|
||||
number,
|
||||
title: format!("Episode {number}"),
|
||||
air_date: Some(SystemTime::UNIX_EPOCH),
|
||||
|
||||
Reference in New Issue
Block a user