Import pipeline: probe, hardlink, rename, layout (#82)
ci / rust (push) Failing after 1m43s
ci / web (push) Successful in 58s
e2e / e2e (push) Successful in 1m25s

This commit was merged in pull request #82.
This commit is contained in:
2026-08-22 23:21:30 +01:00
parent 62aba6315c
commit ffb8485939
18 changed files with 1653 additions and 20 deletions
+49 -17
View File
@@ -13,12 +13,12 @@
//! - the `grabs` row is written from that response, so a crash between the add
//! and the insert heals on the next tick instead of leaving an orphan.
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use arr_core::policy::{evaluate, Candidate};
use arr_core::{score::score, Language, Policy, Rule, TitleOverrides, Verdict};
use arr_core::{score::score, Language, Policy, TitleOverrides, Verdict};
use arr_db::{Db, MoviePolicy};
use arr_dl::{AddTorrent, TorrentSource, TransmissionClient};
use arr_indexer::{ProwlarrClient, SearchRelease, SearchRequest};
@@ -317,7 +317,14 @@ impl GrabAction {
let candidates = self
.search(database, movie, indexers, &loaded, &original_language)
.await?;
let Some(winner) = candidates.into_iter().next() else {
// §6.3: anything that hard-failed post-ffprobe is never grabbed
// again. The same release reappears under new infohashes, so the key
// is the normalised name.
let blacklisted = blacklisted_names(database).await?;
let Some(winner) = candidates
.into_iter()
.find(|candidate| !blacklisted.contains(&arr_parse::normalise(&candidate.name)))
else {
tracing::info!(
movie_id = movie.id,
title = movie.title,
@@ -558,6 +565,17 @@ async fn store_release(
}))
}
/// Every blacklist key (§6.3), for filtering candidates. Household scale: a
/// handful of rows, refetched per title rather than cached.
async fn blacklisted_names(database: &Db) -> Result<HashSet<String>, GrabError> {
let names = sqlx::query_scalar!(
r#"SELECT normalised_name AS "normalised_name!: String" FROM blacklist"#
)
.fetch_all(database.pool())
.await?;
Ok(names.into_iter().collect())
}
/// Record that the title was searched, so the next tick takes a different one.
async fn record_search(database: &Db, movie_id: i64) -> Result<(), GrabError> {
sqlx::query!(
@@ -605,20 +623,7 @@ fn verdict_columns(verdict: &Verdict) -> (&'static str, Option<String>) {
match verdict {
Verdict::Eligible => ("eligible", None),
Verdict::Waived(_) => ("waived", None),
Verdict::Rejected(rule) => ("rejected", Some(rule_name(rule))),
}
}
fn rule_name(rule: &Rule) -> String {
match rule {
Rule::RequiredAudio => "required_audio".into(),
Rule::DubBlacklist(_) => "dub_blacklist".into(),
Rule::PortugueseUnverified => "portuguese_unverified".into(),
Rule::DolbyVisionProfile(_) => "dolby_vision_profile".into(),
Rule::Resolution(_) => "resolution".into(),
Rule::Source(_) => "source".into(),
Rule::Size => "size".into(),
Rule::Other(name) => name.clone(),
Verdict::Rejected(rule) => ("rejected", Some(rule.name())),
}
}
@@ -990,6 +995,33 @@ mod tests {
assert!(fake.torrents().is_empty());
}
/// §6.3: a hard-failed release is never grabbed again, even when it is
/// still the best-scoring candidate — the next one down wins instead.
#[tokio::test]
async fn a_blacklisted_release_is_passed_over() {
let (_dir, database) = wanted_movie().await;
sqlx::query("INSERT INTO blacklist (infohash, normalised_name, reason) VALUES (?, ?, ?)")
.bind("ffff")
.bind(arr_parse::normalise(
"Dune.Part.Two.2024.2160p.WEB-DL.DDP5.1.Atmos",
))
.bind("dolby_vision_profile")
.execute(database.pool())
.await
.unwrap();
let indexer = prowlarr().await;
let (downloader, fake) = transmission().await;
action(&indexer, &downloader).tick(&database).await.unwrap();
assert_eq!(fake.torrents().len(), 1);
assert!(
fake.torrents()[0].source.ends_with("huge.torrent"),
"the remux wins once the WEB-DL is blacklisted: {}",
fake.torrents()[0].source
);
}
/// §6.3: `blocked` stops targeted search for a title.
#[tokio::test]
async fn a_blocked_title_is_not_searched() {