feat(core): add movie domain types #47

Merged
naps62-yolo merged 2 commits from issue/7-core-types into main 2026-08-22 19:40:48 +01:00
+284 -8
View File
@@ -1,14 +1,290 @@
//! arr-core — domain types, policy engine, scoring. See DESIGN.md §4 and §5.
//! Pure domain types shared by the arr crates.
Review

The old crate doc stated the no-axum/sqlx/reqwest invariant; the new one drops it. Cheap to keep it here where the next contributor to this crate will actually see it.

The old crate doc stated the no-axum/sqlx/reqwest invariant; the new one drops it. Cheap to keep it here where the next contributor to this crate will actually see it. <!-- agent-meta: {"model":"claude-fable-5","session":"c92366da"} -->
//!
//! This crate must never depend on `axum`, `sqlx` or `reqwest`. It holds the
//! logic that is tested constantly and it has to stay fast to compile.
//! This crate must never depend on `axum`, `sqlx` or `reqwest`.
use std::{collections::BTreeMap, path::PathBuf, time::SystemTime};
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!(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),
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Resolution {
R2160p,
R1080p,
R720p,
Other(u16),
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Source {
Remux,
BluRay,
WebDl,
WebRip,
Hdtv,
Telesync,
Cam,
Screener,
Other,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DolbyVisionProfile {
pub profile: u8,
pub compatibility_id: Option<u8>,
}
Outdated
Review

profile: u8 cannot carry the DV compatibility id. §5.3's accept row is 8.1 specifically; ffprobe reports dv_profile: 8 plus a separate dv_bl_signal_compatibility_id. With only a u8, rejected_dolby_vision_profiles cannot accept 8.1 while rejecting 8.2 (SDR base layer). Either carry the compat id alongside the profile, or document that profile means base profile only and 8.x distinctions are out of scope.

`profile: u8` cannot carry the DV compatibility id. §5.3's accept row is 8.1 specifically; ffprobe reports `dv_profile: 8` plus a separate `dv_bl_signal_compatibility_id`. With only a u8, `rejected_dolby_vision_profiles` cannot accept 8.1 while rejecting 8.2 (SDR base layer). Either carry the compat id alongside the profile, or document that `profile` means base profile only and 8.x distinctions are out of scope. <!-- agent-meta: {"model":"claude-fable-5","session":"c92366da"} -->
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HdrFormat {
Sdr,
Hdr10,
Hdr10Plus,
Hlg,
DolbyVision(DolbyVisionProfile),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HdrClaim {
Hdr10,
Hdr10Plus,
Hlg,
DolbyVision,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RequiredAudio {
OriginalLanguage,
AnyOf(Vec<Language>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HdrRules {
pub rejected_dolby_vision_profiles: Vec<DolbyVisionProfile>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SizeBand {
pub floor_bytes: u64,
pub target_bytes: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Policy {
pub id: PolicyId,
pub name: String,
pub required_audio: RequiredAudio,
pub dub_blacklist: Vec<Language>,
pub hdr_rules: HdrRules,
pub size_bands: BTreeMap<Resolution, SizeBand>,
pub resolution_preference: Vec<Resolution>,
pub source_weights: BTreeMap<Source, i32>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct MovieOverrides {
pub only_4k: bool,
pub allow_english_audio: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MovieState {
Missing,
Downloading,
Available,
}
#[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: MovieOverrides,
pub state: MovieState,
pub blocked: bool,
pub search_attempts: u32,
pub last_searched_at: Option<SystemTime>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AudioTrack {
pub language: Language,
pub title: Option<String>,
pub handler_name: Option<String>,
}
#[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<Source>,
pub hdr: HdrFormat,
pub audio_tracks: Vec<AudioTrack>,
pub subtitle_tracks: Vec<SubtitleTrack>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Rule {
RequiredAudio,
DubBlacklist(Language),
DolbyVisionProfile(DolbyVisionProfile),
Resolution(Resolution),
Source(Source),
Size,
Other(String),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Verdict {
Eligible,
Waived(Rule),
Rejected(Rule),
}
Review

ParsedRelease.hdr reuses HdrFormat, whose DolbyVision { profile: u8 } makes the profile mandatory — but DESIGN.md §5.3 says the profile is knowable only from ffprobe, never from a release name. As typed, the parser must invent a profile for any name that claims DV. A separate pre-grab HDR claim type (or a profile-less DV variant for parsed data) keeps the impossible state unrepresentable.

`ParsedRelease.hdr` reuses `HdrFormat`, whose `DolbyVision { profile: u8 }` makes the profile mandatory — but DESIGN.md §5.3 says the profile is knowable only from ffprobe, never from a release name. As typed, the parser must invent a profile for any name that claims DV. A separate pre-grab HDR claim type (or a profile-less DV variant for parsed data) keeps the impossible state unrepresentable. <!-- agent-meta: {"model":"claude-fable-5","session":"c92366da"} -->
#[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<Rule>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParsedRelease {
pub resolution: Option<Resolution>,
pub source: Option<Source>,
pub hdr: Vec<HdrClaim>,
pub audio_languages: Vec<Language>,
}
#[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<SystemTime>,
}
#[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 {
/// Placeholder so the test gate is meaningful on an empty workspace —
/// `cargo nextest` fails a run with zero tests, and relaxing that would
/// let a broken filter go green later. Replaced by real tests in the
/// domain-types and policy-engine issues.
use super::{DolbyVisionProfile, HdrFormat, Language, Rule, Verdict};
#[test]
fn workspace_builds_and_tests_run() {}
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));
}
}