//! Pure domain types shared by the arr crates. //! //! This crate must never depend on `axum`, `sqlx` or `reqwest`. use std::{collections::BTreeMap, fmt, path::PathBuf, time::SystemTime}; pub mod grabbing; pub mod lang; pub mod layout; pub mod matching; pub mod policy; pub mod score; pub mod status; pub mod tracking; pub use arr_parse::NameClaims as ParsedRelease; pub use grabbing::{season_grab_mode, SeasonGrabFacts, SeasonGrabMode}; pub use matching::{ match_episode, match_movie, EpisodeMatch, MatchKind, MatchShape, MovieMatch, ReleaseIds, WantedEpisode, WantedMovie, }; pub use score::{Score, ScoreWeights}; pub use status::{derive_series_status, SeriesStatus}; macro_rules! id_type { ($name:ident) => { #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct $name(pub i64); }; } id_type!(RootId); id_type!(PolicyId); id_type!(MovieId); id_type!(SeriesId); id_type!(SeasonId); id_type!(EpisodeId); id_type!(MediaFileId); id_type!(ReleaseId); id_type!(GrabId); id_type!(OwnerId); #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Audience { Main, Kids, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct Root { pub id: RootId, pub audience: Audience, pub path: PathBuf, pub policy_id: PolicyId, } #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum Language { PortuguesePortugal, PortugueseBrazil, PortugueseUnverified, Other(String), } impl fmt::Display for Language { /// The tag the policy columns and §7.4 filenames spell it as: /// `pt-PT`, `pt-BR`, `por-unverified`, anything else verbatim. fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::PortuguesePortugal => formatter.write_str("pt-PT"), Self::PortugueseBrazil => formatter.write_str("pt-BR"), Self::PortugueseUnverified => formatter.write_str("por-unverified"), Self::Other(tag) => formatter.write_str(tag), } } } #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum Resolution { R2160p, R1080p, R720p, Other(u16), } impl fmt::Display for Resolution { /// The spelling `resolution_pref` and §7.4 filename tags use. fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::R2160p => formatter.write_str("2160p"), Self::R1080p => formatter.write_str("1080p"), Self::R720p => formatter.write_str("720p"), Self::Other(height) => write!(formatter, "{height}p"), } } } #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum Source { Remux, BluRay, WebDl, WebRip, Hdtv, Dvd, Telecine, Telesync, Cam, Screener, Other, } impl fmt::Display for Source { /// The spelling `source_weights` and §7.4 filename tags use. fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(match self { Self::Remux => "Remux", Self::BluRay => "BluRay", Self::WebDl => "WEB-DL", Self::WebRip => "WEBRip", Self::Hdtv => "HDTV", Self::Dvd => "DVD", Self::Telecine => "Telecine", Self::Telesync => "Telesync", Self::Cam => "CAM", Self::Screener => "Screener", Self::Other => "Unknown", }) } } impl From for Resolution { fn from(value: arr_parse::Resolution) -> Self { match value { arr_parse::Resolution::P480 => Self::Other(480), arr_parse::Resolution::P576 => Self::Other(576), arr_parse::Resolution::P720 => Self::R720p, arr_parse::Resolution::P1080 => Self::R1080p, arr_parse::Resolution::P2160 => Self::R2160p, } } } impl From for Source { fn from(value: arr_parse::Source) -> Self { match value { arr_parse::Source::Cam => Self::Cam, arr_parse::Source::Telesync => Self::Telesync, arr_parse::Source::Telecine => Self::Telecine, arr_parse::Source::Screener => Self::Screener, arr_parse::Source::Dvd => Self::Dvd, arr_parse::Source::Hdtv => Self::Hdtv, arr_parse::Source::WebRip => Self::WebRip, arr_parse::Source::WebDl => Self::WebDl, arr_parse::Source::BluRay => Self::BluRay, arr_parse::Source::Remux => Self::Remux, } } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct DolbyVisionProfile { pub profile: u8, pub compatibility_id: Option, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum HdrFormat { Sdr, Hdr10, Hdr10Plus, Hlg, DolbyVision(DolbyVisionProfile), } impl fmt::Display for HdrFormat { /// The §7.4 filename tag: `HDR10`, `DV8.1`, and so on. Dolby Vision /// carries its profile because that is the whole point of probing it /// (§5.3) — `ls` shows which files are DV and which profile. fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Sdr => formatter.write_str("SDR"), Self::Hdr10 => formatter.write_str("HDR10"), Self::Hdr10Plus => formatter.write_str("HDR10+"), Self::Hlg => formatter.write_str("HLG"), Self::DolbyVision(dv) => match dv.compatibility_id { Some(compatibility_id) => write!(formatter, "DV{}.{compatibility_id}", dv.profile), None => write!(formatter, "DV{}", dv.profile), }, } } } #[derive(Clone, Debug, Eq, PartialEq)] pub enum RequiredAudio { OriginalLanguage, AnyOf(Vec), } #[derive(Clone, Debug, Eq, PartialEq)] pub struct HdrRules { pub rejected_dolby_vision_profiles: Vec, } /// One resolution's size band (`DESIGN.md` §5.5). Below the floor is a hard /// filter; the target is where the score peaks; above it the penalty grows /// with every gibibyte. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct SizeBand { pub floor_bytes: u64, pub target_bytes: u64, /// Points lost per gibibyte above `target_bytes`, on the scale set by /// [`ScoreWeights::size_at_target`] — 1000 points at target means one /// point is a tenth of a percent, fine enough to tune by hand without a /// fractional type. Applied per byte over, so a partial gibibyte costs /// its fraction. pub penalty_points_per_gib_over: i32, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct Policy { pub id: PolicyId, pub name: String, pub required_audio: RequiredAudio, pub dub_blacklist: Vec, pub hdr_rules: HdrRules, pub size_bands: BTreeMap, pub resolution_preference: Vec, pub source_weights: BTreeMap, pub score_weights: ScoreWeights, } #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct TitleOverrides { pub only_4k: bool, pub allow_english_audio: bool, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum MediaState { Missing, Downloading, Available, /// #108: `wanted` was cleared after the grab vanished from Transmission. /// Distinct from `Missing` so it does not read as an open gap. Parked, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct Movie { pub id: MovieId, pub tmdb_id: u64, pub title: String, pub year: u16, pub original_language: Language, pub root_id: RootId, pub wanted: bool, pub overrides: TitleOverrides, pub state: MediaState, pub blocked: bool, pub search_attempts: u32, pub last_searched_at: Option, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct Series { pub id: SeriesId, pub tmdb_id: u64, pub title: String, pub year: u16, pub original_language: Language, pub root_id: RootId, pub auto_track: bool, pub overrides: TitleOverrides, pub upstream_ended: bool, pub blocked: bool, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct Season { pub id: SeasonId, pub series_id: SeriesId, pub number: u16, pub tracked: bool, } #[derive(Clone, Debug, Eq, PartialEq)] 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, pub wanted: bool, pub state: MediaState, pub search_attempts: u32, pub last_searched_at: Option, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct AudioTrack { pub language: Language, pub title: Option, pub handler_name: Option, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct SubtitleTrack { pub language: Language, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct ProbedMedia { pub resolution: Resolution, pub source: Option, pub hdr: HdrFormat, pub audio_tracks: Vec, pub subtitle_tracks: Vec, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum Rule { RequiredAudio, DubBlacklist(Language), /// A Portuguese track whose pt-PT / pt-BR flavour no signal resolved. PortugueseUnverified, DolbyVisionProfile(DolbyVisionProfile), Resolution(Resolution), Source(Source), Size, Other(String), } impl Rule { /// The stable name shared by `releases.rejected_rule`, blacklist reasons /// and `media_files.waiver`, so one rule reads the same everywhere. #[must_use] pub fn name(&self) -> String { match self { Self::RequiredAudio => "required_audio".into(), Self::DubBlacklist(_) => "dub_blacklist".into(), Self::PortugueseUnverified => "portuguese_unverified".into(), Self::DolbyVisionProfile(_) => "dolby_vision_profile".into(), Self::Resolution(_) => "resolution".into(), Self::Source(_) => "source".into(), Self::Size => "size".into(), Self::Other(name) => name.clone(), } } } #[derive(Clone, Debug, Eq, PartialEq)] pub enum Verdict { Eligible, Waived(Rule), Rejected(Rule), } #[derive(Clone, Debug, Eq, PartialEq)] pub struct MediaFile { pub id: MediaFileId, pub movie_id: MovieId, pub path: PathBuf, pub size: u64, pub probed: ProbedMedia, pub waiver: Option, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct Release { pub id: ReleaseId, pub indexer_id: i64, pub guid: String, pub name: String, pub size: u64, pub seeders: u32, pub publish_date: SystemTime, pub download_url: String, pub parsed: ParsedRelease, pub score: i64, pub verdict: Verdict, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum GrabState { Downloading, Downloaded, Imported, Failed, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct Grab { pub id: GrabId, pub release_id: ReleaseId, pub movie_id: MovieId, pub infohash: String, pub state: GrabState, pub grabbed_at: SystemTime, pub imported_at: Option, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct BlacklistEntry { pub infohash: String, pub normalised_name: String, pub reason: String, pub created_at: SystemTime, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct Owner { pub id: OwnerId, pub name: String, pub ntfy_topic: String, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct MovieOwner { pub movie_id: MovieId, pub owner_id: OwnerId, } #[cfg(test)] mod tests { use super::{DolbyVisionProfile, HdrFormat, Language, Rule, Verdict}; #[test] fn portuguese_variants_are_distinct() { assert_ne!(Language::PortuguesePortugal, Language::PortugueseBrazil); assert_ne!(Language::PortugueseBrazil, Language::PortugueseUnverified); } #[test] fn dolby_vision_profile_is_preserved() { assert_ne!( HdrFormat::DolbyVision(DolbyVisionProfile { profile: 8, compatibility_id: Some(1), }), HdrFormat::DolbyVision(DolbyVisionProfile { profile: 8, compatibility_id: Some(2), }) ); } #[test] fn non_eligible_verdicts_name_the_rule() { let rule = Rule::DolbyVisionProfile(DolbyVisionProfile { profile: 5, compatibility_id: None, }); assert_eq!(Verdict::Rejected(rule.clone()), Verdict::Rejected(rule)); } }