diff --git a/crates/arr-core/src/policy.rs b/crates/arr-core/src/policy.rs index b4edfd3..223b248 100644 --- a/crates/arr-core/src/policy.rs +++ b/crates/arr-core/src/policy.rs @@ -2,8 +2,8 @@ use arr_parse::{LanguageMarker, NameClaims}; use crate::{ lang::{is_resolved_portuguese, language_of_marker}, - Language, MovieOverrides, Policy, ProbedMedia, RequiredAudio, Resolution, Rule, Source, - Verdict, + HdrFormat, Language, MovieOverrides, Policy, ProbedMedia, RequiredAudio, Resolution, Rule, + Source, Verdict, }; /// The evidence available while evaluating a candidate. @@ -105,9 +105,10 @@ pub fn evaluate( original_language, candidate, }; - let rules: [&dyn PolicyRule; 4] = [ + let rules: [&dyn PolicyRule; 5] = [ &ResolutionRule, &SourceRule, + &HdrRule, &DubBlacklistRule, &RequiredAudioRule, ]; @@ -200,6 +201,38 @@ impl PolicyRule for SourceRule { } } +/// Applies Dolby Vision profile policy using post-download probe evidence. +#[derive(Clone, Copy, Debug, Default)] +pub struct HdrRule; + +impl PolicyRule for HdrRule { + fn evaluate(&self, context: &EvaluationContext<'_>) -> RuleEvaluation { + let Candidate::PostDownload(media) = context.candidate else { + return RuleEvaluation::Unknown(RuleKind::DolbyVisionProfile); + }; + let HdrFormat::DolbyVision(profile) = media.hdr else { + return RuleEvaluation::Pass(RuleKind::DolbyVisionProfile); + }; + + let rejected = context + .policy + .hdr_rules + .rejected_dolby_vision_profiles + .iter() + .any(|blocked| { + blocked.profile == profile.profile + && blocked.compatibility_id.is_none_or(|compatibility_id| { + profile.compatibility_id == Some(compatibility_id) + }) + }); + if rejected { + RuleEvaluation::HardFail(Rule::DolbyVisionProfile(profile)) + } else { + RuleEvaluation::Pass(RuleKind::DolbyVisionProfile) + } + } +} + /// The one language expression from `DESIGN.md` ยง5.2: /// /// ```text @@ -363,7 +396,9 @@ mod tests { use arr_parse::{NameClaims, Resolution as ClaimedResolution, Source as ClaimedSource}; use super::*; - use crate::{AudioTrack, HdrFormat, HdrRules, PolicyId, RequiredAudio, SubtitleTrack}; + use crate::{ + AudioTrack, DolbyVisionProfile, HdrFormat, HdrRules, PolicyId, RequiredAudio, SubtitleTrack, + }; fn en() -> Language { Language::Other("en".to_owned()) @@ -378,7 +413,16 @@ mod tests { required_audio: RequiredAudio::OriginalLanguage, dub_blacklist: vec![Language::PortugueseBrazil], hdr_rules: HdrRules { - rejected_dolby_vision_profiles: Vec::new(), + rejected_dolby_vision_profiles: vec![ + DolbyVisionProfile { + profile: 5, + compatibility_id: None, + }, + DolbyVisionProfile { + profile: 7, + compatibility_id: None, + }, + ], }, size_bands: BTreeMap::new(), resolution_preference: vec![Resolution::R2160p, Resolution::R1080p], @@ -470,6 +514,7 @@ mod tests { vec![ RuleEvaluation::Unknown(RuleKind::Resolution), RuleEvaluation::Unknown(RuleKind::Source), + RuleEvaluation::Unknown(RuleKind::DolbyVisionProfile), RuleEvaluation::Unknown(RuleKind::DubBlacklist), RuleEvaluation::Unknown(RuleKind::RequiredAudio), ] @@ -648,6 +693,81 @@ mod tests { } } + #[test] + fn dolby_vision_profile_5_is_rejected() { + assert_dolby_vision_verdict(&policy(), 5, None, true); + } + + #[test] + fn dolby_vision_profile_7_is_rejected() { + assert_dolby_vision_verdict(&policy(), 7, Some(6), true); + } + + #[test] + fn dolby_vision_profile_8_1_is_accepted() { + assert_dolby_vision_verdict(&policy(), 8, Some(1), false); + } + + #[test] + fn compatibility_specific_rule_matches_only_that_id() { + let mut policy = policy(); + policy + .hdr_rules + .rejected_dolby_vision_profiles + .push(DolbyVisionProfile { + profile: 8, + compatibility_id: Some(2), + }); + + assert_dolby_vision_verdict(&policy, 8, Some(2), true); + assert_dolby_vision_verdict(&policy, 8, Some(1), false); + } + + fn assert_dolby_vision_verdict( + policy: &Policy, + profile: u8, + compatibility_id: Option, + rejected: bool, + ) { + let mut media = probed(Resolution::R2160p, Some(Source::WebDl)); + let profile = DolbyVisionProfile { + profile, + compatibility_id, + }; + media.hdr = HdrFormat::DolbyVision(profile); + + let report = evaluate( + policy, + &MovieOverrides::default(), + &en(), + Candidate::PostDownload(&media), + ); + let expected = if rejected { + Verdict::Rejected(Rule::DolbyVisionProfile(profile)) + } else { + Verdict::Eligible + }; + + assert_eq!(report.verdict, expected); + } + + #[test] + fn pre_grab_dolby_vision_claim_stays_unknown() { + let claims = arr_parse::parse("Movie.2024.2160p.WEB-DL.DV-GROUP"); + let report = evaluate( + &policy(), + &MovieOverrides::default(), + &en(), + Candidate::PreGrab(&claims), + ); + + assert_eq!(report.verdict, Verdict::Eligible); + assert_eq!( + report.rules[2], + RuleEvaluation::Unknown(RuleKind::DolbyVisionProfile) + ); + } + #[derive(Debug)] struct FixedRule { evaluation: RuleEvaluation, @@ -929,7 +1049,7 @@ mod tests { ); assert_eq!(report.verdict, Verdict::Eligible); assert_eq!( - report.rules[3], + report.rules[4], RuleEvaluation::Unknown(RuleKind::RequiredAudio) ); }