Hard/soft fail handling and release blacklist (#87)
ci / web (push) Successful in 26s
ci / rust (push) Successful in 1m6s
e2e / e2e (push) Successful in 52s

This commit was merged in pull request #87.
This commit is contained in:
2026-08-22 23:42:36 +01:00
parent 38bd4102bb
commit 01af397a40
14 changed files with 674 additions and 43 deletions
+60 -10
View File
@@ -203,7 +203,7 @@ impl ImportAction {
let Some(content) = self.transmission.torrent_content(&pending.infohash).await? else {
// Gone from Transmission. Whether that is a failure or a manual
// removal is issue #24's call; leave the grab alone.
// removal is issue #86's call; leave the grab alone.
tracing::warn!(
grab_id = pending.grab_id,
infohash = pending.infohash,
@@ -312,16 +312,12 @@ impl ImportAction {
pending: &PendingImport,
reason: &str,
) -> Result<Outcome, ImportError> {
let normalised = arr_parse::normalise(&pending.release_name);
sqlx::query!(
"INSERT INTO blacklist (infohash, normalised_name, reason)
VALUES (?, ?, ?)
ON CONFLICT (infohash) DO NOTHING",
pending.infohash,
normalised,
reason
arr_db::blacklist::add(
database.pool(),
Some(&pending.infohash),
&pending.release_name,
reason,
)
.execute(database.pool())
.await?;
sqlx::query!(
"UPDATE grabs SET state = 'failed' WHERE id = ?",
@@ -770,6 +766,60 @@ mod tests {
);
}
/// §5.7 soft fail: watchable but not what was asked. It imports, and the
/// row carries the relaxed rule — the file must never read as a clean
/// match. The torrent is untouched either way (§7.3).
#[tokio::test]
async fn an_english_only_kids_import_carries_a_waiver() {
let h = harness(HDR10_PROBE).await;
// The kids policy requires Portuguese audio; `allow_english_audio`
// turns that hard fail into a waiver (§5.2, §5.7).
let kids_library = h.library.join("kids");
std::fs::create_dir_all(&kids_library).unwrap();
sqlx::query("UPDATE roots SET path = ? WHERE kind = 'movie' AND audience = 'kids'")
.bind(kids_library.to_string_lossy().into_owned())
.execute(h.database.pool())
.await
.unwrap();
sqlx::query(
r#"UPDATE movies
SET root_id = (SELECT id FROM roots
WHERE kind = 'movie' AND audience = 'kids'),
overrides = '{"allow_english_audio":true}'
WHERE id = 1"#,
)
.execute(h.database.pool())
.await
.unwrap();
let outcomes = h.action.tick(&h.database).await.unwrap();
assert_eq!(outcomes.len(), 1);
let waiver: Option<String> = sqlx::query_scalar(
"SELECT json_extract(waiver, '$.rule') FROM media_files
WHERE owner_kind = 'movie' AND owner_id = 1",
)
.fetch_one(h.database.pool())
.await
.unwrap();
assert_eq!(waiver.as_deref(), Some("required_audio"));
let blacklisted: i64 = sqlx::query_scalar("SELECT count(*) FROM blacklist")
.fetch_one(h.database.pool())
.await
.unwrap();
assert_eq!(blacklisted, 0, "a soft fail blacklists nothing");
let grab_state: String = sqlx::query_scalar("SELECT state FROM grabs")
.fetch_one(h.database.pool())
.await
.unwrap();
assert_eq!(grab_state, "imported");
assert!(
h.downloads.join("Dune/Dune.mkv").is_file(),
"§7.3: neither failure mode deletes the torrent"
);
}
/// §8: killed between the hardlink and the bookkeeping, a restart
/// converges instead of failing on the existing destination.
#[tokio::test]