fix(daemon): derive reaper labels from roots
ci / web (pull_request) Successful in 27s
e2e / e2e (pull_request) Successful in 48s
ci / rust (pull_request) Failing after 1m53s

This commit is contained in:
Miguel Palhas
2026-08-23 00:01:44 +01:00
parent 896835f100
commit 0c64642a01
3 changed files with 24 additions and 20 deletions
+6 -6
View File
@@ -686,12 +686,12 @@ async fn record_search(database: &Db, movie_id: i64) -> Result<(), GrabError> {
/// `movies-main`, `tv-kids` (§7.1). Distinct from Radarr's own labels, so
/// both stacks can run against one Transmission.
fn label(loaded: &MoviePolicy) -> String {
let kind = if loaded.root_kind == "movie" {
"movies"
} else {
&loaded.root_kind
};
format!("{kind}-{}", loaded.root_audience)
label_for_root(&loaded.root_kind, &loaded.root_audience)
}
pub(crate) fn label_for_root(kind: &str, audience: &str) -> String {
let kind = if kind == "movie" { "movies" } else { kind };
format!("{kind}-{audience}")
}
/// Indexers hand out magnets and `.torrent` links interchangeably;
+17 -9
View File
@@ -1,9 +1,12 @@
//! Removes arr torrents only after Transmission says their seeding obligation
//! is complete. The library/import state is deliberately not consulted (§7.3).
use std::collections::HashSet;
use arr_db::Db;
use arr_dl::TransmissionClient;
use crate::grab::label_for_root;
use crate::reconcile::{Action, ActionFuture, Outcome};
#[derive(Debug)]
@@ -17,11 +20,11 @@ impl ReaperAction {
Self { transmission }
}
async fn tick(&self) -> Result<Vec<Outcome>, arr_dl::Error> {
async fn tick(&self, labels: &HashSet<String>) -> Result<Vec<Outcome>, arr_dl::Error> {
let torrents = self.transmission.list_torrents().await?;
let mut outcomes = Vec::new();
for torrent in torrents {
if !torrent.is_finished || !torrent.labels.iter().any(|label| is_arr_label(label)) {
if !torrent.is_finished || !torrent.labels.iter().any(|label| labels.contains(label)) {
continue;
}
self.transmission.remove_torrent(torrent.id, true).await?;
@@ -41,17 +44,20 @@ impl Action for ReaperAction {
fn run<'a>(&'a self, _database: &'a Db) -> ActionFuture<'a> {
Box::pin(async move {
self.tick()
let labels = sqlx::query_as::<_, (String, String)>("SELECT kind, audience FROM roots")
.fetch_all(_database.pool())
.await
.map_err(|error| Box::new(error) as crate::reconcile::ActionError)?
.into_iter()
.map(|(kind, audience)| label_for_root(&kind, &audience))
.collect::<HashSet<_>>();
self.tick(&labels)
.await
.map_err(|error| Box::new(error) as crate::reconcile::ActionError)
})
}
}
fn is_arr_label(label: &str) -> bool {
matches!(label, "movies-main" | "movies-kids" | "tv-main" | "tv-kids")
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
@@ -111,7 +117,8 @@ mod tests {
.await;
let action = ReaperAction::new(TransmissionClient::new(&server.uri()).unwrap());
let outcomes = action.tick().await.unwrap();
let labels = HashSet::from(["movies-main".to_owned(), "movies-kids".to_owned()]);
let outcomes = action.tick(&labels).await.unwrap();
assert_eq!(outcomes.len(), 1);
assert_eq!(
@@ -133,7 +140,8 @@ mod tests {
.await;
let action = ReaperAction::new(TransmissionClient::new(&server.uri()).unwrap());
action.tick().await.unwrap();
let labels = HashSet::from(["movies-kids".to_owned()]);
action.tick(&labels).await.unwrap();
assert_eq!(removed.lock().unwrap().len(), 1);
}
+1 -5
View File
@@ -201,11 +201,7 @@ async fn transmission_reports_done_only_after_its_seed_limit() {
}
fn torrent_with_name(name: &str) -> Vec<u8> {
// SHA-1 of the single zero byte written by the seeding-boundary test.
let piece_hash = [
0x5b, 0xa9, 0x3c, 0x9d, 0xb0, 0xcf, 0xf9, 0x3f, 0x52, 0xb5, 0x21, 0xd7, 0x42, 0x0e, 0x43,
0xf6, 0xed, 0xa2, 0x78, 0x4f,
];
let piece_hash = [0_u8; 20];
let mut bytes = format!("d4:infod6:lengthi1e4:name{}:{name}", name.len()).into_bytes();
bytes.extend_from_slice(b"12:piece lengthi16384e6:pieces20:");
bytes.extend_from_slice(&piece_hash);