Merge main into blitz/subtitles

Feedback pass 2 and the size-band work landed on main while this branch
was finishing. Brings them in ahead of the merge back.

# Conflicts:
#	crates/arr-api/src/movies.rs
#	crates/arr-api/src/state.rs
#	crates/arr-daemon/src/main.rs
#	web/src/main.ts
This commit is contained in:
Miguel Palhas
2026-08-25 17:53:18 +01:00
76 changed files with 7184 additions and 416 deletions
+104 -1
View File
@@ -199,6 +199,26 @@ fake profile per root and ignoring whatever it sends.
Per-title `overrides` relax the root policy for one title. Same mechanism in
both directions — `only_4k` tightens, `allow_english_audio` loosens.
**Stored verdicts follow the effective policy.** A release's verdict is
stamped by the search that found it, and both §9.3's deck and the daemon's
manual-grab gate read that stored column. So the four operator actions that
change a title's effective policy — editing its overrides, moving it to a
root with a different policy, pointing a root at a different policy, and
editing the contents of a policy some root points at — re-derive the stored
verdicts of everything they touch, inline in the same request. The operator
is never left reading a verdict computed under a policy that no longer
applies.
The fourth is the widest: a repoint moves one library, a policy edit moves
every library sharing the policy. Inline still holds there. Re-evaluation is
pure and in-memory, a row whose verdict does not move is not rewritten, and
the ceiling is the whole database rather than something that grows with the
number of roots — roots partition titles, and a title has exactly one root.
Measured over 2000 titles and 10 000 stored releases split across two roots
sharing one policy: 0.36 s when the edit moves no verdict, 2.7 s in the
pathological case where it moves all 10 000. A rename changes no rule and
re-derives nothing.
### 5.2 Language
Requires a concept the release name does not carry: the **original language of
@@ -307,6 +327,26 @@ measured by its full size. That is the behaviour today, and it makes a pack look
oversized rather than undersized — it fails toward rejecting a good pack rather
than grabbing a bad one, and the next search after a refresh has the real count.
**A band describes a rate, not a fixed size per episode.** The shipped values
are read against a **reference runtime of 45 minutes**: a 1080p target of 2 GiB
means 2 GiB per 45 minutes of episode. Before the per-episode figure is compared
against them, a band's floor and target are both scaled by
`runtime / 45`, where `runtime` is the series' minutes-per-episode from TMDB
metadata. For a typical drama around 45 minutes the shipped numbers keep exactly
their current meaning; a 22-minute show is judged against roughly half the floor
and half the target instead of being rejected for weighing half of what an hour
of video weighs. The shipped band values themselves do not change — the
reference runtime is chosen so they do not have to.
**A missing or zero runtime is the reference runtime.** When metadata carries no
per-episode runtime, or carries zero, the scale factor is 1 and the band applies
unscaled — exactly today's behaviour. The rule does not guess a duration, for
the same reason the unknown episode count does not guess a number.
**Movies are not scaled.** This applies to episodes only. A movie's bands are
already tuned against feature length, so its floor and target keep their current
meaning regardless of the movie's own runtime.
Source tier (`Remux > BluRay > WEB-DL > WEBRip > HDTV`) survives as a small
tiebreaker. Seeders are log-scaled and small: enough to complete, past that it
does not matter. Telesync, CAM and screener are **hard filters**, not low
@@ -358,6 +398,46 @@ A policy violation found by `ffprobe` is not one thing.
Neither deletes the torrent. See §7.3.
**Two hard failures make a decision, and only for 30 days.** A movie, an
episode or a season enters the needs-a-decision queue (§9.5) when two grabs
against *different* releases hard-failed on it, and both of those failures
happened within the last 30 days. One bad torrent is not a decision — a
release that hard-failed is blacklisted (§6.3) and the next candidate is
grabbed, which is the system working.
The window runs from the failure, not the grab. The two are usually minutes
apart, but a torrent can sit stalling on a dead swarm for five weeks before
`ffprobe` finally condemns it — and that failure is fresh evidence the target
is broken now, not history. Measured from the grab it would be born outside
the window and a genuinely broken target could never surface. So `grabs`
records `failed_at` alongside `grabbed_at`, and the window reads it.
The window is what lets the queue be emptied. Nothing clears a `grabs` row, so
without it the queue only ever grows and the one season that wants attention
sits behind eight that were dealt with months ago. It is the queue's version of
§6.2's "it never gives up entirely, it goes quiet": a target the operator has
dealt with stops producing failures and drops out once the last one ages past
30 days, while a target that is still broken keeps producing them — the pack
guard retries at worst weekly (§6.2) — and stays queued for exactly as long as
it is genuinely broken. Nothing is dismissed by hand and no acknowledgement
state is stored, so there is no second thing to keep correct.
**Only a target still waiting for a file is queued.** The count and the
window already express one rule — bounded attention — and this is another
face of it: a failure history queues a target only while that target still
has a gap to fill. A movie or an episode is queued while it is `wanted` and
not `available`. A season holds no intent of its own (§4.1), so it is queued
while at least one of its episodes is still `wanted` and not `available`. A
season pack that hard-failed twice, fell back to per-episode grabbing
exactly as §6.2 says it should, and was then fully acquired leaves at once
rather than waiting out the 30 days — that is the system working, not a
decision. A target that is still broken keeps producing failures and stays.
The same rule applies on all three lanes and in both readers.
`GET /api/queues/attention` (§9.3) and the `ntfy` notification (§9.5) are two
views of one queue; filtering differently tells the operator two different
stories on two channels.
## 6. Sourcing
### 6.1 Prowlarr, per-indexer Torznab
@@ -400,6 +480,22 @@ its own, the escape hatch is the season release deck, not a lane exception.
Targeted search backs off `1h → 6h → 1d → 3d`, capped at 7d, reset when the
title's metadata changes. It never gives up entirely, it goes quiet.
**The ladder runs from the failure, not the grab.** A failed season-pack grab
quiets the pack lane on that same curve, and the rung is measured from the
moment the grab entered `failed``grabs.failed_at`, the column §5.7's
attention window reads — not from when it was sent. The two are usually
minutes apart, but a torrent can stall on a dead swarm for five weeks before
`ffprobe` condemns it at import. Measured from the grab, the whole ladder has
already elapsed by the time the failure lands, so the lane retries the source
that just failed at once, which is the one thing the backoff exists to
prevent. The ladder's job is to stay off a source that has recently failed,
and "recently" can only mean recently failed.
One anchor covers both features. §5.7's window and this ladder ask the same
question of the same event and read the same column; a target still broken
keeps producing fresh failures, and each one both re-arms this backoff and
holds the target in the attention queue.
**Do not search before the release exists.** TMDB carries release dates; a
movie with no digital release date gets zero targeted searches. This is the
single largest source of wasted queries in Radarr and it is free to avoid.
@@ -468,6 +564,13 @@ Media kind first, hard audience boundary second, people nowhere.
- **Release group is deliberately absent.** It is not a selection criterion and
it makes filenames long enough to break a terminal.
Changing a title's root relocates its title folder into the new root; roots
are assumed to share one filesystem, so the move is a rename, never a copy.
Changing a root's path is the same move over every title under it, and it
is all or nothing: one folder that cannot move puts back the ones that
already did and leaves the root's path alone, so the stored path always
describes the disk.
During transition, write into the existing roots so Jellyfin needs no
reconfiguration and new content appears immediately. Radarr will not touch a
folder it has no record of.
@@ -576,7 +679,7 @@ notifying on everything and being muted within a week.
- **Imported** → to the title's owners. The only good-news notification.
- **Needs a decision** → to the operator alone. Entered the no-PT-source queue,
or hard-failed twice on different releases.
or the needs-a-decision queue (§5.7).
- **Broken** → to the operator alone. Prowlarr, Transmission or TMDB
unreachable, disk full.