feat: let a size rejection be waived

A release below §5.5's floor was rejected with no way through, so a
policy wrong about one title left three Rick and Morty S09 packs
visible and none grabbable.

`allow_below_floor` relaxes the floor for one title into a soft fail,
never a pass: the release is waived, so automatic grabbing still skips
it and the import records a §5.7 waiver. The deck offers the one click
on a rejected row where the rule has an override, which is exactly what
§9.3's override is for.

Stored verdicts are re-derived when a title's overrides change — the
deck and the daemon's grab gate both read that column, so without it
the row the operator just acted on would keep reading `rejected`.

Closes #210

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Miguel Palhas
2026-08-24 22:15:01 +01:00
parent 405358bf24
commit 9fee07f080
16 changed files with 1098 additions and 20 deletions
+73
View File
@@ -193,6 +193,7 @@ impl Db {
overrides: TitleOverrides {
only_4k: overrides.only_4k,
allow_english_audio: overrides.allow_english_audio,
allow_below_floor: overrides.allow_below_floor,
},
root_id: row.root_id,
root_kind: row.root_kind,
@@ -268,6 +269,75 @@ impl Db {
overrides: TitleOverrides {
only_4k: overrides.only_4k,
allow_english_audio: overrides.allow_english_audio,
allow_below_floor: overrides.allow_below_floor,
},
root_id: row.root_id,
root_kind: row.root_kind,
root_audience: row.root_audience,
root_path: row.root_path,
}))
}
/// The policy attached to one series' root, with the series' own
/// overrides (§5.1).
///
/// `None` when the series does not exist.
///
/// # Errors
///
/// If the query fails, or a policy column does not hold the JSON its
/// migration promises.
pub async fn series_policy(&self, series_id: i64) -> Result<Option<TitlePolicy>, PolicyError> {
let row = sqlx::query!(
r#"
SELECT s.overrides AS "overrides!: String",
r.id AS "root_id!: i64",
r.kind AS "root_kind!: String",
r.audience AS "root_audience!: String",
r.path AS "root_path!: String",
p.id AS "policy_id!: i64",
p.name AS "policy_name!: String",
p.required_audio AS "required_audio!: String",
p.dub_blacklist AS "dub_blacklist!: String",
p.hdr_rules AS "hdr_rules!: String",
p.size_bands AS "size_bands!: String",
p.resolution_pref AS "resolution_pref!: String",
p.source_weights AS "source_weights!: String",
p.score_weights AS "score_weights!: String"
FROM series s
JOIN roots r ON r.id = s.root_id
JOIN policies p ON p.id = r.policy_id
WHERE s.id = ?
"#,
series_id
)
.fetch_optional(self.pool())
.await?;
let Some(row) = row else {
return Ok(None);
};
let overrides: OverridesJson = json("overrides", &row.overrides)?;
let policy = PolicyColumns {
id: row.policy_id,
name: row.policy_name,
required_audio: row.required_audio,
dub_blacklist: row.dub_blacklist,
hdr_rules: row.hdr_rules,
size_bands: row.size_bands,
resolution_pref: row.resolution_pref,
source_weights: row.source_weights,
score_weights: row.score_weights,
}
.to_policy()?;
Ok(Some(TitlePolicy {
policy,
overrides: TitleOverrides {
only_4k: overrides.only_4k,
allow_english_audio: overrides.allow_english_audio,
allow_below_floor: overrides.allow_below_floor,
},
root_id: row.root_id,
root_kind: row.root_kind,
@@ -337,6 +407,7 @@ impl Db {
overrides: TitleOverrides {
only_4k: overrides.only_4k,
allow_english_audio: overrides.allow_english_audio,
allow_below_floor: overrides.allow_below_floor,
},
root_id: row.root_id,
root_kind: row.root_kind,
@@ -428,6 +499,8 @@ struct OverridesJson {
only_4k: bool,
#[serde(default)]
allow_english_audio: bool,
#[serde(default)]
allow_below_floor: bool,
}
fn gib(value: u64) -> u64 {