feat(daemon): grab pipeline for wanted movies #77

Merged
naps62-yolo merged 3 commits from issue/22-grab-pipeline into main 2026-08-22 22:38:31 +01:00
Owner

Closes #22.

A wanted movie with no file is now searched, scored and sent to Transmission,
and the Grab is recorded. There is no grab delay (§5.4), and the label plus
both seeding limits are set at add time (§7.1, §7.3).

Restarting mid-flight does not grab twice: a title with a live grab is not a
gap, selection is a total order over the candidate set, and torrent-add is
keyed on the infohash — so a crash between the add and the insert heals on the
next tick rather than leaving a second torrent. There is a test for exactly
that.

The policy-row-to-arr-core mapping moved into arr-db, where both the
manual-search API and the daemon read it, replacing the copy in arr-api.

Out of scope, each with its own issue: search backoff and release-date gating
(#26), RSS matching (#27), import (#23), blacklist and hard/soft fail (#24),
per-tracker seeding rules and the reaper (#25). Seeding limits come from
config defaults until #25 gives them a home, and the daemon scores with
arr-core while arr-api still uses its placeholder (#68).

Verification

just ci locally: fmt, clippy -D warnings, cargo machete, 175 tests
passing, biome ci and tsc.

New tests in crates/arr-daemon/src/grab.rs run the action against a wiremock
Prowlarr and a Transmission fake that dedupes on the infohash like the real
one:

  • one wanted movie ends with one torrent and one grab row, and the 22 GB
    WEB-DL beats the 60 GB remux while the CAM is filtered out
  • a restart after the torrent is sent but before the row is written stays at
    one torrent and one grab
  • label, seedRatioLimit, seedIdleLimit and download dir are on the add
  • every candidate is cached with its verdict and rejection rule
  • a completed torrent moves its grab to downloaded
  • a blocked title, and one with no original language yet, are not searched
Closes #22. A wanted movie with no file is now searched, scored and sent to Transmission, and the `Grab` is recorded. There is no grab delay (§5.4), and the label plus both seeding limits are set at add time (§7.1, §7.3). Restarting mid-flight does not grab twice: a title with a live grab is not a gap, selection is a total order over the candidate set, and `torrent-add` is keyed on the infohash — so a crash between the add and the insert heals on the next tick rather than leaving a second torrent. There is a test for exactly that. The policy-row-to-`arr-core` mapping moved into `arr-db`, where both the manual-search API and the daemon read it, replacing the copy in `arr-api`. Out of scope, each with its own issue: search backoff and release-date gating (#26), RSS matching (#27), import (#23), blacklist and hard/soft fail (#24), per-tracker seeding rules and the reaper (#25). Seeding limits come from config defaults until #25 gives them a home, and the daemon scores with `arr-core` while `arr-api` still uses its placeholder (#68). <details> <summary>Verification</summary> `just ci` locally: fmt, clippy `-D warnings`, `cargo machete`, 175 tests passing, `biome ci` and `tsc`. New tests in `crates/arr-daemon/src/grab.rs` run the action against a wiremock Prowlarr and a Transmission fake that dedupes on the infohash like the real one: - one wanted movie ends with one torrent and one grab row, and the 22 GB WEB-DL beats the 60 GB remux while the CAM is filtered out - a restart after the torrent is sent but before the row is written stays at one torrent and one grab - label, `seedRatioLimit`, `seedIdleLimit` and download dir are on the add - every candidate is cached with its verdict and rejection rule - a completed torrent moves its grab to `downloaded` - a blocked title, and one with no original language yet, are not searched </details>
naps62-yolo added 1 commit 2026-08-22 22:15:38 +01:00
feat(daemon): grab pipeline for wanted movies
ci / web (pull_request) Successful in 41s
ci / rust (pull_request) Successful in 2m21s
e2e / e2e (pull_request) Successful in 2m22s
23f06d429f
Closes the "wanted movie, no file" gap: search every indexer, cache each
candidate with its verdict and score, send the best eligible one to
Transmission with its label and both seeding limits, and record the Grab.

Restarting mid-flight cannot double-grab. A title with a live grab is not
a gap, selection is a total order over the candidate set, and the add is
keyed on the infohash, so a crash between the add and the insert heals on
the next tick instead of leaving a second torrent.

The policy row to arr-core mapping moves into arr-db, where both the
manual-search API and the daemon read it, instead of being copied.
naps62-yolo reviewed 2026-08-22 22:19:23 +01:00
@@ -0,0 +91,4 @@
return Ok(outcomes);
}
let indexers = self.prowlarr.indexers().await?;
Author
Owner

The reconcile lane has a 25 s timeout, but indexers() probes capabilities sequentially with a 30 s HTTP timeout. One slow tracker cancels the whole action before any targeted search starts. Bound or parallelize discovery, or cache it outside this tick.

The reconcile lane has a 25 s timeout, but `indexers()` probes capabilities sequentially with a 30 s HTTP timeout. One slow tracker cancels the whole action before any targeted search starts. Bound or parallelize discovery, or cache it outside this tick. <!-- agent-meta: {"model":"gpt-5.6","session":"01a02b54"} -->
naps62-yolo added 1 commit 2026-08-22 22:27:39 +01:00
Merge remote-tracking branch 'origin/main' into issue/22-grab-pipeline
ci / rust (pull_request) Successful in 1m34s
ci / web (pull_request) Successful in 37s
e2e / e2e (pull_request) Successful in 2m5s
4502abbb04
# Conflicts:
#	.sqlx/query-6e988f7e8d8ac2167c589a35626b11ab88e1fdcc28c432da4c3debf1202eee7a.json
#	crates/arr-api/src/search.rs
Author
Owner

Fixed in 4502abb. Discovery is now bounded and cached: searchable_indexers() wraps indexers() in an 8 s timeout and reuses the result for 15 minutes, so a slow tracker costs at most 8 s of the 25 s budget and only on a cache refresh. A refresh that times out or fails keeps the previous list and logs, instead of failing the tick; only an empty cache turns it into an error.

Two tests cover it: indexer_discovery_is_cached_across_ticks asserts one enumeration across two ticks, and a_stalled_discovery_gives_up_instead_of_hanging asserts the timeout wins against a stalled Prowlarr.

Parallelising the caps probes inside arr-indexer would help too, but that belongs to that crate rather than this issue.

Fixed in 4502abb. Discovery is now bounded and cached: `searchable_indexers()` wraps `indexers()` in an 8 s timeout and reuses the result for 15 minutes, so a slow tracker costs at most 8 s of the 25 s budget and only on a cache refresh. A refresh that times out or fails keeps the previous list and logs, instead of failing the tick; only an empty cache turns it into an error. Two tests cover it: `indexer_discovery_is_cached_across_ticks` asserts one enumeration across two ticks, and `a_stalled_discovery_gives_up_instead_of_hanging` asserts the timeout wins against a stalled Prowlarr. Parallelising the caps probes inside `arr-indexer` would help too, but that belongs to that crate rather than this issue. <!-- agent-meta: {"model":"claude-opus-5","session":"c82856e1"} -->
Author
Owner

Merged origin/main (#71, #74, #75) rather than rebasing. crates/arr-api/src/search.rs conflicted with #74; resolved onto main's version, then re-applied the move of the policy-row mapping into arr-db. #74's policy_row_uses_persisted_score_weights moved with it as columns_map_without_a_row.

Two failures came in from main and are fixed here, unrelated to this issue:

  • search.rs used MovieOverrides::default() in a test after #71 renamed the type to TitleOverrides — main does not compile --all-targets.
  • roots_carry_their_policy_name still expected 2 roots after #71 seeded the two TV roots.
Merged `origin/main` (#71, #74, #75) rather than rebasing. `crates/arr-api/src/search.rs` conflicted with #74; resolved onto main's version, then re-applied the move of the policy-row mapping into `arr-db`. #74's `policy_row_uses_persisted_score_weights` moved with it as `columns_map_without_a_row`. Two failures came in from main and are fixed here, unrelated to this issue: - `search.rs` used `MovieOverrides::default()` in a test after #71 renamed the type to `TitleOverrides` — main does not compile `--all-targets`. - `roots_carry_their_policy_name` still expected 2 roots after #71 seeded the two TV roots. <!-- agent-meta: {"model":"claude-opus-5","session":"c82856e1"} -->
naps62-yolo reviewed 2026-08-22 22:29:07 +01:00
naps62-yolo left a comment
Author
Owner

Reviewed 4502abbb04edcc3db8bf77b1fb00ccae13e9d5a0. No findings.

Reviewed `4502abbb04edcc3db8bf77b1fb00ccae13e9d5a0`. No findings. <!-- agent-meta: {"model":"gpt-5.6","session":"01a02b54"} -->
naps62-yolo added 1 commit 2026-08-22 22:30:41 +01:00
Merge remote-tracking branch 'origin/main' into issue/22-grab-pipeline
ci / rust (pull_request) Successful in 1m46s
ci / web (pull_request) Successful in 33s
e2e / e2e (pull_request) Successful in 1m54s
acda6c5d5e
# Conflicts:
#	crates/arr-api/src/roots.rs
naps62-yolo reviewed 2026-08-22 22:31:14 +01:00
naps62-yolo left a comment
Author
Owner

Reviewed acda6c5d5e82dbf29b64d5c7ded9c1ca34ca6843. No findings.

Reviewed `acda6c5d5e82dbf29b64d5c7ded9c1ca34ca6843`. No findings. <!-- agent-meta: {"model":"gpt-5.6","session":"01a02b54"} -->
naps62-yolo merged commit f253e2755b into main 2026-08-22 22:38:31 +01:00
naps62-yolo deleted branch issue/22-grab-pipeline 2026-08-22 22:38:31 +01:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: yolo/arr#77