Changing a root's path rewrote the row and moved nothing, so every title
under it was mislocated at once. It now reuses the #228 mover: plan every
rename, refuse a destination that already exists, rewrite the media_files
rows in the same transaction as the row change.
The move is all or nothing. A root row carries one path, so a half-moved
library would have to describe both places; instead one folder that
cannot move puts back the ones that already did and leaves the root's
path alone, and the same request is the retry.
just ci ran clean through the gate: 498 tests passed.
The season branch of the attention queue listed a season on one failed
grab of any age, so `GET /api/queues/attention` returned Rick and Morty
with every season it has and buried the one that needed attention.
Two changes, both stated in DESIGN.md §5.7:
- The season branch now enforces the same bar the episode branch does:
two grabs that hard-failed on *different* releases.
- A failed grab counts toward the queue for 30 days
(`arr_db::ATTENTION_WINDOW`). Nothing clears a `grabs` row, so without
a window the queue only grows and can never be emptied. #181 gave the
pack guard a backoff curve for the same reason; this is the queue's
version of §6.2's "it never gives up entirely, it goes quiet". A
season the operator dealt with stops failing and drops out; one still
breaking keeps failing (the pack guard retries at worst weekly) and
stays.
The window applies to all three hard-fail lanes — movie, episode and
season — because DESIGN.md states one rule for the queue, and to the
daemon's needs-a-decision notifier as well as the API, since both read
the same queue and a season-per-failure notification is the same noise
on a different channel. No schema change: `grabs.grabbed_at` already
carries the timestamp.
Gate: `just ci` green (486 tests).
Changing a movie's or series' root previously rewrote root_id and left
the files behind, so the §7.4 layout stopped describing the disk and
the root's policy applied to a library the files were not in. Series
had no root control at all.
All roots share one ZFS dataset, so the move is a rename of the title
folder into the new root, never a copy — hardlinks and the seeding
torrent survive it (§7.3). Disk first, row second: a destination that
already holds the folder is a 409, a failed rename leaves the row
unchanged, and a title with nothing on disk moves with no filesystem
work. media_files rows are rewritten in the same transaction as the
root_id, and a successful move triggers the §7.5 Jellyfin refresh.
Issue #228
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A band's floor and target now read as a rate against a 45-minute
reference runtime, scaled by the series' per-episode runtime from
TMDB. Missing or zero runtime falls back to the reference, keeping
today's behaviour. Movies are explicitly unscaled. Closes the axis
question in #208; #209 implements it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A release covering several episodes is measured by size divided by the
episode count, for the target penalty and the floor alike. An unknown
count divides by one, which fails toward rejecting rather than grabbing.
Refs #179
Untracking a season now clears its episodes' wanted flag, unified search
returns titles only, the library is the homepage and the signal chain moves
into settings, and the wordmark links home.
Refs #169
`resolution_pref` only gated eligibility, so every release was scored
against its own resolution's size band and a 23 GB 4K lost to an
at-target 1080p. Each step up the list is now worth `resolution_step`
points, seeded at 300: five gibibytes of 4K overshoot, so a 4K up to
27 GB wins and a bloated one still does not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Design for a single-service replacement of Radarr/Sonarr: domain model,
policy engine, sourcing via Prowlarr Torznab, import pipeline, reconcile
loop, crate layout, CI gate and build order.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>