refactor(core): share title policy types
ci / rust (pull_request) Successful in 1m47s
ci / web (pull_request) Successful in 34s
e2e / e2e (pull_request) Successful in 1m26s

This commit is contained in:
Miguel Palhas
2026-08-22 21:44:59 +01:00
parent b4c6c872bb
commit f7a62958fd
6 changed files with 41 additions and 48 deletions
+4 -4
View File
@@ -3,8 +3,8 @@ use std::time::UNIX_EPOCH;
use arr_core::policy::{evaluate, Candidate}; use arr_core::policy::{evaluate, Candidate};
use arr_core::{ use arr_core::{
DolbyVisionProfile, HdrRules, Language, MovieOverrides, Policy, PolicyId, RequiredAudio, DolbyVisionProfile, HdrRules, Language, Policy, PolicyId, RequiredAudio, Resolution, Rule,
Resolution, Rule, ScoreWeights, SizeBand, Source, Verdict, ScoreWeights, SizeBand, Source, TitleOverrides, Verdict,
}; };
use arr_indexer::{ProwlarrClient, SearchRelease, SearchRequest}; use arr_indexer::{ProwlarrClient, SearchRelease, SearchRequest};
use axum::extract::{Query, State}; use axum::extract::{Query, State};
@@ -293,7 +293,7 @@ pub async fn releases(
})?; })?;
let overrides: OverridesJson = serde_json::from_value(movie.overrides) let overrides: OverridesJson = serde_json::from_value(movie.overrides)
.map_err(|error| ApiError::Database(error.to_string()))?; .map_err(|error| ApiError::Database(error.to_string()))?;
let overrides = MovieOverrides { let overrides = TitleOverrides {
only_4k: overrides.only_4k, only_4k: overrides.only_4k,
allow_english_audio: overrides.allow_english_audio, allow_english_audio: overrides.allow_english_audio,
}; };
@@ -460,7 +460,7 @@ fn policy_from_row(row: PolicyRow) -> Result<Policy, ApiError> {
fn classify( fn classify(
release: SearchRelease, release: SearchRelease,
policy: &Policy, policy: &Policy,
overrides: &MovieOverrides, overrides: &TitleOverrides,
original_language: &Language, original_language: &Language,
) -> Result<ClassifiedRelease, ApiError> { ) -> Result<ClassifiedRelease, ApiError> {
let parsed = arr_parse::parse(&release.name); let parsed = arr_parse::parse(&release.name);
+4 -13
View File
@@ -163,10 +163,8 @@ pub struct TitleOverrides {
pub allow_english_audio: bool, pub allow_english_audio: bool,
} }
pub type MovieOverrides = TitleOverrides;
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MovieState { pub enum MediaState {
Missing, Missing,
Downloading, Downloading,
Available, Available,
@@ -181,8 +179,8 @@ pub struct Movie {
pub original_language: Language, pub original_language: Language,
pub root_id: RootId, pub root_id: RootId,
pub wanted: bool, pub wanted: bool,
pub overrides: MovieOverrides, pub overrides: TitleOverrides,
pub state: MovieState, pub state: MediaState,
pub blocked: bool, pub blocked: bool,
pub search_attempts: u32, pub search_attempts: u32,
pub last_searched_at: Option<SystemTime>, pub last_searched_at: Option<SystemTime>,
@@ -210,13 +208,6 @@ pub struct Season {
pub tracked: bool, pub tracked: bool,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EpisodeState {
Missing,
Downloading,
Available,
}
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct Episode { pub struct Episode {
pub id: EpisodeId, pub id: EpisodeId,
@@ -225,7 +216,7 @@ pub struct Episode {
pub title: String, pub title: String,
pub air_date: Option<SystemTime>, pub air_date: Option<SystemTime>,
pub wanted: bool, pub wanted: bool,
pub state: EpisodeState, pub state: MediaState,
pub search_attempts: u32, pub search_attempts: u32,
pub last_searched_at: Option<SystemTime>, pub last_searched_at: Option<SystemTime>,
} }
+26 -26
View File
@@ -2,8 +2,8 @@ use arr_parse::{LanguageMarker, NameClaims};
use crate::{ use crate::{
lang::{is_resolved_portuguese, language_of_marker}, lang::{is_resolved_portuguese, language_of_marker},
HdrFormat, Language, MovieOverrides, Policy, ProbedMedia, RequiredAudio, Resolution, Rule, HdrFormat, Language, Policy, ProbedMedia, RequiredAudio, Resolution, Rule, Source,
Source, Verdict, TitleOverrides, Verdict,
}; };
/// The evidence available while evaluating a candidate. /// The evidence available while evaluating a candidate.
@@ -55,7 +55,7 @@ pub enum EvaluationPhase {
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct EvaluationContext<'a> { pub struct EvaluationContext<'a> {
pub policy: &'a Policy, pub policy: &'a Policy,
pub overrides: &'a MovieOverrides, pub overrides: &'a TitleOverrides,
/// The title's original language from TMDB (`DESIGN.md` §5.2) — a /// The title's original language from TMDB (`DESIGN.md` §5.2) — a
/// distinct input from any track's language. /// distinct input from any track's language.
pub original_language: &'a Language, pub original_language: &'a Language,
@@ -103,7 +103,7 @@ pub struct Evaluation {
#[must_use] #[must_use]
pub fn evaluate( pub fn evaluate(
policy: &Policy, policy: &Policy,
overrides: &MovieOverrides, overrides: &TitleOverrides,
original_language: &Language, original_language: &Language,
candidate: Candidate<'_>, candidate: Candidate<'_>,
size_bytes: Option<u64>, size_bytes: Option<u64>,
@@ -514,7 +514,7 @@ mod tests {
} }
} }
fn verdict(policy: &Policy, overrides: &MovieOverrides, candidate: Candidate<'_>) -> Verdict { fn verdict(policy: &Policy, overrides: &TitleOverrides, candidate: Candidate<'_>) -> Verdict {
evaluate(policy, overrides, &en(), candidate, None).verdict evaluate(policy, overrides, &en(), candidate, None).verdict
} }
@@ -525,7 +525,7 @@ mod tests {
) -> Verdict { ) -> Verdict {
evaluate( evaluate(
policy, policy,
&MovieOverrides::default(), &TitleOverrides::default(),
original_language, original_language,
candidate, candidate,
None, None,
@@ -539,7 +539,7 @@ mod tests {
let claims = claims(None, None); let claims = claims(None, None);
let report = evaluate( let report = evaluate(
&policy, &policy,
&MovieOverrides::default(), &TitleOverrides::default(),
&en(), &en(),
Candidate::PreGrab(&claims), Candidate::PreGrab(&claims),
None, None,
@@ -562,7 +562,7 @@ mod tests {
#[test] #[test]
fn preferred_resolutions_pass_in_both_phases() { fn preferred_resolutions_pass_in_both_phases() {
let policy = policy(); let policy = policy();
let overrides = MovieOverrides::default(); let overrides = TitleOverrides::default();
for claimed in [ClaimedResolution::P2160, ClaimedResolution::P1080] { for claimed in [ClaimedResolution::P2160, ClaimedResolution::P1080] {
let claims = claims(Some(claimed), Some(ClaimedSource::WebDl)); let claims = claims(Some(claimed), Some(ClaimedSource::WebDl));
@@ -583,7 +583,7 @@ mod tests {
#[test] #[test]
fn resolution_outside_root_policy_hard_fails_in_both_phases() { fn resolution_outside_root_policy_hard_fails_in_both_phases() {
let policy = policy(); let policy = policy();
let overrides = MovieOverrides::default(); let overrides = TitleOverrides::default();
let cases = [ let cases = [
(ClaimedResolution::P480, Resolution::Other(480)), (ClaimedResolution::P480, Resolution::Other(480)),
(ClaimedResolution::P576, Resolution::Other(576)), (ClaimedResolution::P576, Resolution::Other(576)),
@@ -607,9 +607,9 @@ mod tests {
#[test] #[test]
fn only_4k_tightens_the_root_policy_in_both_phases() { fn only_4k_tightens_the_root_policy_in_both_phases() {
let policy = policy(); let policy = policy();
let overrides = MovieOverrides { let overrides = TitleOverrides {
only_4k: true, only_4k: true,
..MovieOverrides::default() ..TitleOverrides::default()
}; };
let claims_1080 = claims(Some(ClaimedResolution::P1080), Some(ClaimedSource::WebDl)); let claims_1080 = claims(Some(ClaimedResolution::P1080), Some(ClaimedSource::WebDl));
let media = probed(Resolution::R1080p, Some(Source::WebDl)); let media = probed(Resolution::R1080p, Some(Source::WebDl));
@@ -634,9 +634,9 @@ mod tests {
fn only_4k_never_expands_the_root_policy() { fn only_4k_never_expands_the_root_policy() {
let mut policy = policy(); let mut policy = policy();
policy.resolution_preference = vec![Resolution::R1080p]; policy.resolution_preference = vec![Resolution::R1080p];
let overrides = MovieOverrides { let overrides = TitleOverrides {
only_4k: true, only_4k: true,
..MovieOverrides::default() ..TitleOverrides::default()
}; };
let claims = claims(Some(ClaimedResolution::P2160), Some(ClaimedSource::WebDl)); let claims = claims(Some(ClaimedResolution::P2160), Some(ClaimedSource::WebDl));
@@ -649,7 +649,7 @@ mod tests {
#[test] #[test]
fn every_unsafe_source_hard_fails_in_both_phases() { fn every_unsafe_source_hard_fails_in_both_phases() {
let policy = policy(); let policy = policy();
let overrides = MovieOverrides::default(); let overrides = TitleOverrides::default();
let cases = [ let cases = [
(ClaimedSource::Cam, Source::Cam), (ClaimedSource::Cam, Source::Cam),
(ClaimedSource::Telesync, Source::Telesync), (ClaimedSource::Telesync, Source::Telesync),
@@ -679,7 +679,7 @@ mod tests {
assert_eq!( assert_eq!(
verdict( verdict(
&policy, &policy,
&MovieOverrides::default(), &TitleOverrides::default(),
Candidate::PreGrab(&claims), Candidate::PreGrab(&claims),
), ),
Verdict::Rejected(Rule::Source(Source::Telesync)) Verdict::Rejected(Rule::Source(Source::Telesync))
@@ -689,7 +689,7 @@ mod tests {
#[test] #[test]
fn every_safe_source_passes_in_both_phases() { fn every_safe_source_passes_in_both_phases() {
let policy = policy(); let policy = policy();
let overrides = MovieOverrides::default(); let overrides = TitleOverrides::default();
let cases = [ let cases = [
(ClaimedSource::Dvd, Source::Dvd), (ClaimedSource::Dvd, Source::Dvd),
(ClaimedSource::Telecine, Source::Telecine), (ClaimedSource::Telecine, Source::Telecine),
@@ -721,7 +721,7 @@ mod tests {
let media = probed(Resolution::R2160p, source); let media = probed(Resolution::R2160p, source);
let report = evaluate( let report = evaluate(
&policy, &policy,
&MovieOverrides::default(), &TitleOverrides::default(),
&en(), &en(),
Candidate::PostDownload(&media), Candidate::PostDownload(&media),
None, None,
@@ -777,7 +777,7 @@ mod tests {
let report = evaluate( let report = evaluate(
policy, policy,
&MovieOverrides::default(), &TitleOverrides::default(),
&en(), &en(),
Candidate::PostDownload(&media), Candidate::PostDownload(&media),
None, None,
@@ -796,7 +796,7 @@ mod tests {
let claims = arr_parse::parse("Movie.2024.2160p.WEB-DL.DV-GROUP"); let claims = arr_parse::parse("Movie.2024.2160p.WEB-DL.DV-GROUP");
let report = evaluate( let report = evaluate(
&policy(), &policy(),
&MovieOverrides::default(), &TitleOverrides::default(),
&en(), &en(),
Candidate::PreGrab(&claims), Candidate::PreGrab(&claims),
None, None,
@@ -823,7 +823,7 @@ mod tests {
#[test] #[test]
fn hard_failure_wins_over_an_earlier_soft_failure() { fn hard_failure_wins_over_an_earlier_soft_failure() {
let policy = policy(); let policy = policy();
let overrides = MovieOverrides::default(); let overrides = TitleOverrides::default();
let original = en(); let original = en();
let claims = claims(None, None); let claims = claims(None, None);
let context = EvaluationContext { let context = EvaluationContext {
@@ -851,7 +851,7 @@ mod tests {
#[test] #[test]
fn first_failure_of_the_deciding_kind_is_stable() { fn first_failure_of_the_deciding_kind_is_stable() {
let policy = policy(); let policy = policy();
let overrides = MovieOverrides::default(); let overrides = TitleOverrides::default();
let original = en(); let original = en();
let claims = claims(None, None); let claims = claims(None, None);
let context = EvaluationContext { let context = EvaluationContext {
@@ -984,9 +984,9 @@ mod tests {
#[test] #[test]
fn kids_allow_english_audio_grabs_and_imports_with_a_waiver() { fn kids_allow_english_audio_grabs_and_imports_with_a_waiver() {
let policy = kids_policy(); let policy = kids_policy();
let overrides = MovieOverrides { let overrides = TitleOverrides {
allow_english_audio: true, allow_english_audio: true,
..MovieOverrides::default() ..TitleOverrides::default()
}; };
let claims = name_claims(&[LanguageMarker::English]); let claims = name_claims(&[LanguageMarker::English]);
assert_eq!( assert_eq!(
@@ -1078,7 +1078,7 @@ mod tests {
// kids: could be the required pt-PT — must not reject as a dub. // kids: could be the required pt-PT — must not reject as a dub.
let report = evaluate( let report = evaluate(
&kids_policy(), &kids_policy(),
&MovieOverrides::default(), &TitleOverrides::default(),
&en(), &en(),
Candidate::PostDownload(&media), Candidate::PostDownload(&media),
None, None,
@@ -1089,7 +1089,7 @@ mod tests {
// as verified either. // as verified either.
let report = evaluate( let report = evaluate(
&policy(), &policy(),
&MovieOverrides::default(), &TitleOverrides::default(),
&Language::PortugueseBrazil, &Language::PortugueseBrazil,
Candidate::PostDownload(&media), Candidate::PostDownload(&media),
None, None,
@@ -1102,7 +1102,7 @@ mod tests {
let claims = name_claims(&[LanguageMarker::Portuguese]); let claims = name_claims(&[LanguageMarker::Portuguese]);
let report = evaluate( let report = evaluate(
&kids_policy(), &kids_policy(),
&MovieOverrides::default(), &TitleOverrides::default(),
&en(), &en(),
Candidate::PreGrab(&claims), Candidate::PreGrab(&claims),
None, None,
+3 -3
View File
@@ -156,7 +156,7 @@ mod tests {
use super::*; use super::*;
use crate::{ use crate::{
policy::{evaluate, EvaluationContext, PolicyRule, RuleEvaluation, SizeRule}, policy::{evaluate, EvaluationContext, PolicyRule, RuleEvaluation, SizeRule},
HdrRules, Language, MovieOverrides, PolicyId, RequiredAudio, Rule, Verdict, HdrRules, Language, PolicyId, RequiredAudio, Rule, TitleOverrides, Verdict,
}; };
const GIB: u64 = 1 << 30; const GIB: u64 = 1 << 30;
@@ -221,7 +221,7 @@ mod tests {
fn size_rule(size_bytes: u64) -> RuleEvaluation { fn size_rule(size_bytes: u64) -> RuleEvaluation {
let policy = policy(); let policy = policy();
let overrides = MovieOverrides::default(); let overrides = TitleOverrides::default();
let language = Language::Other("en".to_owned()); let language = Language::Other("en".to_owned());
let claims = claims(ClaimedSource::WebDl); let claims = claims(ClaimedSource::WebDl);
SizeRule.evaluate(&EvaluationContext { SizeRule.evaluate(&EvaluationContext {
@@ -256,7 +256,7 @@ mod tests {
let claims = claims(ClaimedSource::Remux); let claims = claims(ClaimedSource::Remux);
let evaluation = evaluate( let evaluation = evaluate(
&policy, &policy,
&MovieOverrides::default(), &TitleOverrides::default(),
&Language::Other("en".to_owned()), &Language::Other("en".to_owned()),
Candidate::PreGrab(&claims), Candidate::PreGrab(&claims),
Some(gib(60)), Some(gib(60)),
+3 -1
View File
@@ -65,7 +65,8 @@ CREATE TABLE episodes (
title TEXT NOT NULL, title TEXT NOT NULL,
air_date TEXT, air_date TEXT,
wanted INTEGER NOT NULL DEFAULT 0 CHECK (wanted IN (0, 1)), wanted INTEGER NOT NULL DEFAULT 0 CHECK (wanted IN (0, 1)),
-- Mirrors arr-core's canonical EpisodeState vocabulary. -- Mirrors arr-core's canonical MediaState vocabulary. The older movies
-- table uses legacy names; its additive correction is tracked in #73.
state TEXT NOT NULL DEFAULT 'missing' state TEXT NOT NULL DEFAULT 'missing'
CHECK (state IN ('missing', 'downloading', 'available')), CHECK (state IN ('missing', 'downloading', 'available')),
search_attempts INTEGER NOT NULL DEFAULT 0, search_attempts INTEGER NOT NULL DEFAULT 0,
@@ -91,6 +92,7 @@ INSERT INTO policies (
json('{"dv_profile_allow":["8.1"],"dv_profile_reject":["5","7"],"allow_hdr10":true,"allow_hdr10plus":true,"allow_sdr":true}'), json('{"dv_profile_allow":["8.1"],"dv_profile_reject":["5","7"],"allow_hdr10":true,"allow_hdr10plus":true,"allow_sdr":true}'),
json('{"2160p":{"floor_gib":8,"target_gib":22,"penalty_points_per_gib_over":60},"1080p":{"floor_gib":3,"target_gib":8,"penalty_points_per_gib_over":60}}'), json('{"2160p":{"floor_gib":8,"target_gib":22,"penalty_points_per_gib_over":60},"1080p":{"floor_gib":3,"target_gib":8,"penalty_points_per_gib_over":60}}'),
json('["2160p","1080p"]'), json('["2160p","1080p"]'),
-- §5.5 deliberately keeps source tier as Remux > BluRay > WEB-DL.
json('{"Remux":4,"BluRay":3,"WEB-DL":2,"WEBRip":1,"HDTV":0}'), json('{"Remux":4,"BluRay":3,"WEB-DL":2,"WEBRip":1,"HDTV":0}'),
json('{"size_at_target":1000,"source_tier":25,"seeder_doubling":8}') json('{"size_at_target":1000,"source_tier":25,"seeder_doubling":8}')
); );
+1 -1
View File
@@ -360,7 +360,7 @@ mod tests {
.bind(season_id) .bind(season_id)
.execute(db.pool()) .execute(db.pool())
.await .await
.expect_err("database mirrors EpisodeState"); .expect_err("database mirrors MediaState");
sqlx::query("UPDATE series SET upstream_ended = 2 WHERE id = ?") sqlx::query("UPDATE series SET upstream_ended = 2 WHERE id = ?")
.bind(series_id) .bind(series_id)
.execute(db.pool()) .execute(db.pool())