feat: season-pack vs per-episode grab selection (#94)
This commit was merged in pull request #94.
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
//! Season-pack versus per-episode grab selection (`DESIGN.md` §13 phase 6).
|
||||
//!
|
||||
//! The operator's rule: season packs only when the season is fully released;
|
||||
//! while a season is airing, grab per episode. A completed season with no
|
||||
//! episodes on disk prefers the pack — one torrent, better seeded, consistent
|
||||
//! encode. A pack that hard-failed must not cost the whole season, so the
|
||||
//! season falls back to per-episode instead of being blacklisted outright.
|
||||
//!
|
||||
//! Re-grabbing a pack once an airing season completes is deliberately not
|
||||
//! done (§14): a season with any episode already on disk grabs per episode.
|
||||
|
||||
use std::time::SystemTime;
|
||||
|
||||
/// How a season's missing wanted episodes should be grabbed next.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum SeasonGrabMode {
|
||||
/// One season-pack torrent for the whole season.
|
||||
SeasonPack,
|
||||
/// One grab per aired wanted episode.
|
||||
PerEpisode,
|
||||
}
|
||||
|
||||
/// Everything the season-pack decision depends on.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct SeasonGrabFacts<'a> {
|
||||
/// One entry per episode the season is known to hold, aired or not.
|
||||
/// `None` is an announced episode with no date yet.
|
||||
pub air_dates: &'a [Option<SystemTime>],
|
||||
pub now: SystemTime,
|
||||
/// Whether any episode of the season already has a file (§14: nothing
|
||||
/// re-grabs a pack over episodes on disk, and a pack must not re-import
|
||||
/// what exists).
|
||||
pub any_episode_on_disk: bool,
|
||||
/// Whether a season-pack grab for this season already hard-failed.
|
||||
pub pack_hard_failed: bool,
|
||||
}
|
||||
|
||||
/// Picks the grab mode for one season.
|
||||
///
|
||||
/// A season is fully released only when every known episode has an air date
|
||||
/// in the past. An episode with no date could still be unaired, and grabbing
|
||||
/// a "complete" pack of a season that is not complete costs a whole torrent
|
||||
/// of the wrong thing — so an undated episode keeps the season per-episode.
|
||||
#[must_use]
|
||||
pub fn season_grab_mode(facts: &SeasonGrabFacts<'_>) -> SeasonGrabMode {
|
||||
let fully_released = !facts.air_dates.is_empty()
|
||||
&& facts
|
||||
.air_dates
|
||||
.iter()
|
||||
.all(|date| date.is_some_and(|date| date <= facts.now));
|
||||
|
||||
if fully_released && !facts.any_episode_on_disk && !facts.pack_hard_failed {
|
||||
SeasonGrabMode::SeasonPack
|
||||
} else {
|
||||
SeasonGrabMode::PerEpisode
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
|
||||
const DAY: Duration = Duration::from_hours(24);
|
||||
|
||||
fn facts(air_dates: &[Option<SystemTime>]) -> SeasonGrabFacts<'_> {
|
||||
SeasonGrabFacts {
|
||||
air_dates,
|
||||
now: SystemTime::UNIX_EPOCH + 100 * DAY,
|
||||
any_episode_on_disk: false,
|
||||
pack_hard_failed: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fully_released_season_with_nothing_on_disk_takes_the_pack() {
|
||||
let aired = [
|
||||
Some(SystemTime::UNIX_EPOCH + 10 * DAY),
|
||||
Some(SystemTime::UNIX_EPOCH + 17 * DAY),
|
||||
];
|
||||
assert_eq!(season_grab_mode(&facts(&aired)), SeasonGrabMode::SeasonPack);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_airing_season_grabs_per_episode() {
|
||||
let airing = [
|
||||
Some(SystemTime::UNIX_EPOCH + 10 * DAY),
|
||||
Some(SystemTime::UNIX_EPOCH + 110 * DAY),
|
||||
];
|
||||
assert_eq!(
|
||||
season_grab_mode(&facts(&airing)),
|
||||
SeasonGrabMode::PerEpisode
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_undated_episode_keeps_the_season_per_episode() {
|
||||
let undated = [Some(SystemTime::UNIX_EPOCH + 10 * DAY), None];
|
||||
assert_eq!(
|
||||
season_grab_mode(&facts(&undated)),
|
||||
SeasonGrabMode::PerEpisode
|
||||
);
|
||||
assert_eq!(season_grab_mode(&facts(&[])), SeasonGrabMode::PerEpisode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hard_failed_pack_falls_back_to_per_episode() {
|
||||
let aired = [Some(SystemTime::UNIX_EPOCH + 10 * DAY)];
|
||||
let mut facts = facts(&aired);
|
||||
facts.pack_hard_failed = true;
|
||||
assert_eq!(season_grab_mode(&facts), SeasonGrabMode::PerEpisode);
|
||||
}
|
||||
|
||||
/// §14: episodes already on disk are never re-grabbed as part of a pack.
|
||||
#[test]
|
||||
fn a_season_with_an_episode_on_disk_grabs_per_episode() {
|
||||
let aired = [Some(SystemTime::UNIX_EPOCH + 10 * DAY)];
|
||||
let mut facts = facts(&aired);
|
||||
facts.any_episode_on_disk = true;
|
||||
assert_eq!(season_grab_mode(&facts), SeasonGrabMode::PerEpisode);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user