feat(requests): route requests by rules, request seasons, and rework request pages and admin - #1632
Conversation
Request status and outcome writes now name the states they may start from, so two admins approving at once, or an approval racing a decline, apply exactly one transition. Sending a request to the router plugin requires an atomic claim on the row, so admin approval, auto-approval and the reconcile pass on any server cannot submit it twice. A failed submission keeps the approval: it records the error and retries with backoff (5 minutes doubling to an hour), then marks the request failed after ten attempts. Approve, Retry and auto-approved creates return the saved request instead of a 500 after the approval has committed. Retry reopens a failed request in one write, answers 409 when another account has since requested the title, and drops failed targets for qualities the request no longer wants (#582). Re-requesting a failed title deletes only the requester's own failed rows, inside the create transaction and before the quota check; it used to delete every account's failed rows for the title. The reconcile pass takes a cluster advisory lock, rotates through candidates by last_reconciled_at, and completes from the library only when no submission claim is running. The admin queue offers Decline only for pending requests, matching the server. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Requests no longer need Sonarr, Radarr or another router plugin. When no router connection serves the media type, an approved request stays approved and the reconcile pass completes it once the title is in the library, then notifies the requester. Auto-approval no longer waits for a configured connection, so an auto-approved user's request skips pending on a server without one. A second reconcile rotation checks pending requests, and requests that failed in the last 30 days without delivering anything, against the library: a pending request whose title appears needs no approval any more, and a failed one whose title appears is complete. It uses one batched presence lookup per media type and runs apart from the requests that need router calls, so a backlog cannot slow router polling. The in-flight rotation now batches its presence lookups too. An approved request nothing has been sent for (no target, no submission in flight) can be declined by an admin or cancelled by its owner, so a request waiting for the library can still be closed. A router connection that exists but cannot be used (no API key, not bound to a plugin installation) now records the reason and retries with backoff instead of failing the request, so fixing the connection lets it through. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Requests hub and its browse pages now take the shell's single gutter like Recommendations and Calendar, and a title's request page collapses the sidebar like a library item page does. One RequestStatusBadge, built on the shared Badge variants, replaces the four hard-coded amber colour sets on the hub, the Yours tab, poster cards and the title page. Request pages use one vocabulary for request state: Pending, Approved, Processing, Available, Declined, Cancelled, Failed. Users can cancel their own requests from the Yours tab and the title page until something has been sent for them, behind a confirmation. Approved and declined notifications link to the title. In the search dialog, request rows join arrow-key navigation, close the dialog when opened, and show the request's status instead of a dead "Request" pill. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A profile that finds a title someone else already requested can now ask to
be notified when it becomes available, instead of being turned away with
"already requested". The follow is stored per title and profile, so it
survives the request failing and being requested again, and it is cleared
once the fulfilled notification has gone out. The requesting profile is
always notified and never needs one.
When a request's title reaches the library, the fulfilled notification now
goes to the requester and every follower. A follower's copy is marked so
every channel words it as a title they followed, not their own request. Each
profile's copy uses the existing per-profile unique index, so a retry is
told once, and the server-channel announcement now waits until an attempt
has reached every recipient, so a retry does not repeat it. Declining or
cancelling the request clears its title's follows.
API (v2 only; v1 is frozen):
- PUT and DELETE /api/v2/requests/follows/{media_type}/{tmdb_id}, both
naturally idempotent. Following needs the same access as requesting and
is refused for a title with no active request or one already in the
library.
- Request state gains following and requested_by_viewer.
- GET /api/v2/requests/status advertises follow_supported.
The title page shows "Notify me when available" on another profile's open
request, and "Stop notifying me" once followed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Requests now carry a state field that clients can show as is: pending, approved, processing, available, declined, cancelled or failed. The server derives it from status, outcome and library presence, so a request whose download finished but whose title the scan has not found yet reads as processing rather than available. Request state on discovery, search and detail carries it too. status and outcome stay for admin detail and older clients. Why a request was declined or cancelled is now stored on the request (outcome_reason) and returned, instead of living only in the event log and the decline notification. A migration fills it in for existing requests from their events. The web request pages use the server's state when present and show the reason on declined requests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Creating a request now reads the title's TMDB detail once, after the cheap refusals (already in the library, already requested), instead of an uncached detail read before them just to detect anime. The server's copy of the title and year replaces the one the client sent. The request stores a snapshot of what routing rules will match on: TMDB genre, keyword, network and company IDs, original language, origin countries, year and the anime flag. IDs rather than names, because names follow the configured TMDB language. The TMDB client now carries those IDs and origin countries, which it already received and dropped. When TMDB cannot answer, the request is still created from the client's copy and the facts stay uncaptured until routing needs them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Silo now decides which server each quality tier of a request goes to, instead of leaving it to the Sonarr/Radarr plugin's one-default-per-tier switches. That lets an admin send anime, a genre, a decade, a language, a country, a network or studio, or one account's requests to their own Sonarr or Radarr, with their own root folder, quality profile and tags. Routes belong to a media type and hold conditions and a destination per tier: a server plus overrides for its plugin config. Per tier, the first enabled route whose conditions match and that has a destination for the tier wins; each media type's fallback route comes last. A route can skip 4K for the titles it matches. Conditions match the routing facts captured at creation (fetched at routing time for older requests, retrying while TMDB is unreachable). A routed submission calls the plugin once per tier with only the chosen server, marked as the tier's default and carrying the route's overrides, so the existing plugin follows the route unchanged. Each target records the route that sent it (v2 route_name). Status checks now go through the plugin installation that owns each target's server rather than whichever installation happens to be first. The migration turns each media type's current default and default-4K servers into its fallback route, and a default server's anime settings into an Anime route, so routing is unchanged after upgrade. Media types with no routes, such as Seerr-only setups, keep the plugin's routing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adds the v2 administration surface for request routing, which until now
could only be seeded by the migration:
- GET /admin/request-routes lists every route in evaluation order, always
including each media type's fallback ("Everything else"), which reads as
revision zero until it is first saved.
- GET, PUT and DELETE /admin/request-routes/{id} read, replace and delete a
route; replacement and deletion require If-Match on the route's revision.
The fallback's first PUT creates it; it cannot be deleted.
- POST /admin/request-routes creates a rule after the media type's others,
and POST /admin/request-routes/order sets their order.
- POST /admin/request-routes/preview shows which server each quality tier
of a title would go to, from TMDB's current facts, and why a tier goes
nowhere.
Validation keeps rules meaningful: a rule needs a condition and an effect,
a destination must be a server of the media type's kind, routing-owned
config keys cannot be overridden, and no rule can be added before the media
type's fallback has an HD server, since the first rule moves the media type
to Silo's routing and unmatched titles would otherwise have nowhere to go.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Request configuration moves out of the Requests queue page into Settings › Requests, built on the settings shell: one save bar with an unsaved-changes guard, If-Match on every save, and a conflict prompt that keeps the admin's edits when someone else saved first. - General: allow requests, approval (automatic or by an admin), the request limit and its window, and whether to also request a 4K copy. - Servers: one tile per Sonarr or Radarr server showing where routing uses it. The editor puts the type, name, URL and API key first, then the plugin's settings without the switches routing now owns. Test reports the quality profiles and root folders it found. A server routing uses cannot be deleted or switched to the other type, and the editor says why. - Routing, per media type: the default destination (HD and 4K server with per-tier overrides from the plugin's own options), ordered rules with an editor for their conditions (anime, genres, keywords, networks or studios, languages, countries, years) and destinations, and "Test a title", which shows the facts and the server each tier would go to. With requests off, a title can be tested by its TMDB ID. The Requests page keeps the queue and user overrides and links to the new page; its old ?tab=settings and ?tab=integrations URLs redirect there. ⌘K and the settings overview find the page. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A series request now names the seasons it wants. When the requester names none, the server asks for every aired season that is not complete in the library. A season is complete when every aired episode has a file in an enabled library, judged by the library's own episode metadata, so no external service is involved. Episodes a multi-episode file spans count, and only aired episodes count toward aired ones. A season request completes, and notifies, when all its seasons are complete rather than when the first episode is scanned in, and reads as partially_available while only some are. Once the download server reports the request done, a season with any episode present also counts, so an episode the server cannot find does not hold the request open. Requests from before this change, and every v1 request, mean the whole series and keep the old any-episode rule. A series partly in the library can be requested for its missing seasons only when no download server takes series: the router plugin does not receive seasons yet (that needs an SDK and plugin release) and would add the whole series again. With a download server, such a series stays already_available, as before. The fulfilled-notification pass now stamps each request it checks without notifying, so requests still waiting on the library rotate behind newer completions instead of starving them. Request lists read every series' season counts in one query. API (v2): createRequest accepts seasons; series detail lists each regular season with its availability (missing, partial, available) and whether the active request covers it; requests carry seasons, season_progress and the partially_available state; GET /requests/status advertises season_requests_supported and missing_seasons_requestable. The TMDB client now carries a series' seasons. v1 is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A title the server does not have used its own request page, which reused
only the hero banner and built everything else itself. It now renders
through the same detail layout, sections and components as a library item:
the metadata badges, a score row with the TMDB score, the crew line, the
action bar, the cast section and a "More Like This" row.
The action bar's primary pill is "Request movie"/"Request series", or the
request's state once there is one ("Requested", "Approved",
"Processing"), with "Cancel request" and "Notify me when available" as
secondary actions and IMDb/TMDB as links. The action bar gained optional
primary, secondary and link slots; library items render exactly as before.
Titles now live at /title/:mediaType/:tmdbId, with the same shell as
/item. /requests/:mediaType/:tmdbId redirects there, and an unknown media
type shows the unavailable page instead of silently loading a movie. A
title already in the library redirects to its /item page when the viewer
can open it, and shows a disabled "In the library" when they cannot.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Requesting a series outside the library now opens a season picker. Aired seasons the library lacks start picked, as the server would choose on its own; seasons already in the library or already requested show but cannot be picked, and upcoming ones can be added. The title page also lists the series' seasons with what the library has and what is requested. A series already in the library offers "Request Seasons" in its More menu when the server allows missing seasons to be requested (the missing_seasons_requestable capability, true while no download server takes series). The dialog loads the series' request detail only when opened, so the series page makes no extra request on load. My requests and the admin queue show which seasons a request asks for; a request with only some seasons in the library reads "Partially available" with how many are in. My requests now groups a request under "Landed in your library" by its state, so a download the library has not scanned yet stays in motion. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Season requests and the shared title page add request code that search and title pages load at launch. The budget goes up until the request administration hooks leave the launch bundle later in this branch, which brings it back under the original budget. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The admin queue groups requests by what an admin does next: Needs
approval, In progress, Failed and Done, each tab with its count. It opens on
what needs approval, searches titles or a TMDB ID, filters by media type or
by one account (?user=), and loads more as the admin scrolls.
- Each row shows the poster, requested seasons, requester, state and, once
sent, each server the request went to with the rule that chose it.
- Actions follow the server's rules per view: approve or decline (with a
reason) what needs approval, cancel what nothing was sent for yet, retry or
close what failed. Needs approval also approves or declines a selection,
four requests at a time, and lists any that failed.
- A side sheet shows the request's details, where routing would send it now,
and its history.
- The admin sidebar shows how many requests need approval.
Server (additive v2): the admin list takes view, q, media_type and
requested_by_user_id; GET /admin/requests/counts counts each view; GET
/admin/requests/{id}/events returns a request's history. The list reads its
targets in one query. The v2 admin cancel can now close a failed request
(v1 is unchanged), a closed request stays closed when a target reports late,
and a request's history records each change of its status or outcome once
rather than once per target.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Titles known only from TMDB (Requests rows and the Yours tab, brand pages, "More Like This" on a title page, "Request to add" in search) used their own card. They now use the library's card frame: the same artwork box and missing-poster fallback, radius, hover lift, focus handling and caption, and they follow the viewer's poster-size and caption settings. Request takes the place of Play in the centre of the artwork; the request state badge and the Library chip sit where the library puts its badges. Under a caption a TMDB title always names its type and year, since a movie and a series can share a title, and a poster that fails to load shows the title instead of an empty box. The artwork box, caption classes and centre-action class move into MediaCardArtwork, shared by ItemCard, SectionItemCard and the request card; library cards render the same markup as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Who may request, whether an admin approves, and how many titles an account
may request now live where accounts and access groups are managed, and the
admin Requests page is the queue alone.
- Accounts: the user page gets a Requests section with the account's own
approval and limit, a line saying what applies now and where it comes
from, and a link to the account's requests in the queue.
- Access groups: a group sets approval and a limit beside its existing
requests switch (new v2 GET/PUT /admin/request-groups/{group_id}/limit,
guarded by If-Match). An account's own setting wins, then its group's, then
the server-wide default; admins never take a group's.
- One way to block: the requests switch on the account or its group (or
requests off server-wide). The old "blocked" limit and approval modes are
gone from the editors; a migration moves accounts blocked that way onto
their switch. The API still honors "blocked" when written. Because the
switch now feeds the request policy too, v1 and v2 detail and search report
"blocked" for such an account, as creating a request already did.
- Quota: declined and failed requests give their slot back; cancelled ones
still count, so requesting and withdrawing cannot repeat without limit.
- The User Overrides tab is removed; its old links open the Users page.
A detail page, and the discover rows, resolve the viewer's policy once per
call rather than once per page of results.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Requests page drops its marketing hero, its own search form and the status guide, and takes the header, rows and grids of the app's other browse pages. - Discover: carousel rows of the discover sections, each with Explore all (a new paged grid at /requests/discover/:section); studio, network and genre tiles are links. - Search: the header's search field opens the app's search page, where "Request to add" now reads like the People section, pages through TMDB results and follows the search scope. Old /requests?q= links redirect there. - Yours: a list instead of a poster grid, grouped by what happens next (needs attention, on the way, in your library, cancelled). Each row shows the state, requested seasons and progress, dates, and the decline reason or error, with Cancel and Open in library. A popover explains the states. - Request notifications refresh the list without a reload. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…access The request queue and request access hooks sit in the module search and title pages load at launch. The budget goes up until those hooks move to their own admin module later in this branch, which brings it back under the original budget. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ach decision
Server support for a simpler routing admin.
- Conditions: every list condition gets an exclude form ("original language
is not English"), and a US content-rating ceiling ("rated PG or lower")
matches titles rated at most that; a title with no US rating does not
match. The rating is captured with the other routing facts from the detail
TMDB already returns, and fetched at submission for older requests only when
a route checks ratings.
- The preview explains a decision route by route: which conditions each
failed and what it did for the HD and 4K copies. Try-a-title gets an admin
TMDB search (GET /admin/request-routes/titles) that works while requests
are off, and the preview matches rules for a chosen requester.
- Everything else with no 4K server makes no 4K copy, even when every
request asks for 4K, instead of failing the 4K copy: "no 4K copy" now means
the same on a rule and on the fallback.
- The first Radarr (Sonarr) server added becomes Everything else for movies
(series), and a migration does the same for installs with exactly one usable
server of a kind, so a single-server setup needs no routing. Deleting the
last server of a kind takes that Everything else with it when no rule routes
the media type.
- Validation messages use the words the admin sees ("Everything else",
"4K copies").
All v2 changes are additive.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Settings › Requests replaces its two "Movie routing" / "Series routing"
blocks with one "Where requests go" section and a Movies | Series switch.
- Each switch shows one ordered list: rules as sentences ("When a series is
anime → Sonarr · /tv/anime · Anime 1080p"), numbered, dragged or moved to
reorder, turned on and off in place, and Everything else pinned last.
Changes save as the admin goes; the save bar keeps only the general
settings.
- "Add a rule" starts from Anime, Foreign language or Kids & family, or a
custom rule. A preset asks only where the titles should go; the Anime
preset also sets Sonarr's series type to Anime.
- The rule editor shows only the conditions in use, each "is any of" or "is
none of", added from a menu. Where they go puts folder, quality and tags
up front and the rest under More settings, for HD copies and 4K copies
("Same as Everything else", another server, or no 4K copy).
- Try a title works while requests are off, can act as a given account, and
explains the decision rule by rule; the queue's side sheet shows the same
result.
- Rows warn about rules that can never match, anime below a language rule,
a server that is off or of the wrong kind, a Sonarr anime rule without the
Anime series type, and force-dual with no 4K server, with a fix where one
click will do.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The request detail (servers, where it would go, history, actions) now opens as a centered card with its actions pinned below a scrolling body, so it no longer covers the queue from the side. The row's details button drops the side-panel icon. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Most installs have one Radarr and one Sonarr, and maybe a 4K copy of each, yet the routing rules were the first thing they saw. Standard, now the default for such installs, sends each media type to its one server and 4K copies to its one server marked 4K, with each server's own settings; the rules stay stored but paused. Advanced is the rule-based routing. - request_routing holds the mode, guarded by its revision: GET/PUT /api/v2/admin/request-routing, which also says where Standard sends each media type or why it cannot be used. - Standard needs at most one enabled normal and one 4K server per media type. Adding or enabling a second turns Advanced on in the same transaction and gives Everything else the servers Standard was using, so requests keep going where they went. Every server write and mode switch takes one advisory lock first. - A migration puts installs whose routing Standard would change on Advanced, everyone else on Standard. - Settings > Requests: a Standard/Advanced choice, one line per media type under Standard, a "4K server" switch on Radarr/Sonarr servers, and a toast when a server save turns Advanced on. - Requests go without routing facts when TMDB is down and no rule has a condition, since the facts could not change where they go. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TMDB's anime keyword misses about one anime series in eight and one film in three, and routing only knew a title's US rating, so titles never rated in the US matched no rating rule. - Anime is Japanese animation: TMDB's anime keyword, Animation in Japanese or from Japan, or a title an AniDB-based list names (Kometa's Anime-IDs, matched by TVDB or IMDb ID). A listed series also needs a Japanese signal, since the list names some Western series and the flag can set Sonarr's series type. Chinese and Korean animation counts only when TMDB tags it. - A "Refresh Anime List" task downloads the list daily into anime_ids, one server at a time under a lease, with an ETag, a size cap, a same-host redirect guard and a truncation check; a failed download keeps the stored copy. Requests only read the table. - A title with no US rating (or only "NR") is routed on its own country's rating, stored as "JP:PG12". The TMDB client exposes every country's certifications; the US-only parental-control read is unchanged. - On a sample of 118 anime series and 60 films from the AniDB mappings, detection goes from 87%/62% to 100%/95%, with no Western, Chinese or Japanese live-action title flagged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A settings row's label column stops growing at its maximum width, and nothing pushed the control column to the right after it. On a wide window a row with a short label and a narrow control (a switch, a link button) left its control wherever the label column ended, so switches sat short of the selects and inputs above them and link buttons landed at different places. The control column now always sits on the row's right edge, as the row's own contract says, on every settings page. On Settings > Requests, a routing rule's warning fix now lines up with the rule switches, and Try a title's search fills the row with the requester select, now the same height, on the right edge. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…card The Related rows' outline buttons had a border and fill close to the card's own colour. They now use the dark field and border the settings inputs and selects use. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Everything else and rule editors borrowed the settings page's side-by-side rows inside a narrow dialog: a floating "Send to" beside the server select, an indented block of overrides, tag chips ragged against the right edge, and a stray field count on More settings. Each copy (HD, 4K) is now its own panel: a heading, the server, then Folder on a full row, the other settings in two columns, and tags as left-aligned chips that show a check when picked and say whose tags apply when none are. More settings says how many are changed instead of how many exist. Unset settings read "Server default (...)". The rule's Name field stacks the same way. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The request detail page rendered seasons in a native overflow row with a browser scrollbar and no drag scrolling. Extract the season carousel's Embla rail into SeasonRail and render request seasons inside it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Standard routing sent every series with the Sonarr server's own series type, so anime got standard episode numbering unless an admin switched to Advanced and wrote a rule. Anime series now go to the same servers with Sonarr's anime series type, as Seerr sends them; everything else keeps the server's settings. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Search and title pages load the request hooks at launch, and that module had grown to hold every admin hook too: the queue, request settings, servers, routing and limits, with the admin request API behind them. Those move to hooks/queries/admin/requests, which only the admin pages import, taking about 3 KB brotli off the launch bundle. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…anges A request notification refreshed the request list, title pages, and Discover, but not request search results, which also show each title's request state and availability. A reconnect also delivers request changes made while the socket was down as unread rows in the notifications snapshot rather than as notification.created events, so those changes never refreshed anything. Refresh the same request queries, now including search, from both paths. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…erver types under lock (#1653) * fix(requests): tell only a request's own followers when it arrives A series can have a completed request still waiting for the library beside a newer open request for other seasons. Follows belong to the title, so the completed request's notification went to, and cleared, follows made for the open request, and declining the open request deleted follows still waiting for the completed one. The notification now goes to the follows made before its request completed, and a decline keeps the follows a completed, not yet notified request of the title is waiting to tell. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): recheck a server's type against its routes under the row lock A route save and a server save each validated the other before their transactions and rechecked only the 4K switch under the server's row lock. Two admins saving at once could leave a movie route pointing at a server that had just become a Sonarr, or one that no longer takes movies, and every request it routed would fail when sent. Both saves now recheck the server's type and media types with the row locked, and a server save does so under Standard too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): bound a request's followers by the title's previous completion Two completed requests for one series can both wait for the library, and the newer can arrive first. Bounding followers only by the request's own completion time told the newer request's notification about the older request's followers too, and cleared them. A title has one open request at a time, so a request's followers are the follows made after the title's previous request completed and no later than it did. Clearing them is bounded the same way, so a profile that unfollowed and followed again during the dispatch keeps its new follow. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): record the open request on each follow Bounding a request's followers by timestamps broke when a follow committed while a completion was under way: the completion's timestamp comes from the start of its transaction, so it could be earlier than the follow's, and the follow was left out of the request it had read. A follow now records the request that was open when it was made, and a request's notification, its follow cleanup and a decline all go by that request. A new request takes over the follows of a failed request, or of one its requester replaced, so a follow still survives its request failing. Existing follows go to the title's open request, or else to its latest completed request that has not been notified. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): key follows by request so a profile can follow each one A profile following an older completed request of a series could not follow a newer request for other seasons: the key was per title, the insert kept the old row, and the title showed as followed. The backfill also gave every existing follow to the open request, even ones made for an older request. Follows are now keyed by account, profile and request, and the "following" state reads the follow on the title's open request. A new request takes over the follows of the title's failed requests before any it replaces are deleted. The backfill gives each follow the title's latest request created before it, which is the request that was open then. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): move adopted follows in place and backfill replaced requests' follows A new request took over a failed request's follows by deleting and re-inserting them. An unfollow running at the same moment waited on the deleted row, could not see the new one, and returned while the profile still followed the new request. The follows now move with an UPDATE, which the waiting unfollow re-checks and deletes. The backfill gave a follow the title's latest request created before it even when that request was already closed by then, as when the followed request was deleted by its requester's replacement. It now takes that request only if it could still have been open, and otherwise the title's open request. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * build: pin the plugin SDK to v0.19.0 The download-progress change pinned the SDK to its pull request commit, which was squash-merged as v0.19.0 and is no longer fetchable, so the Go jobs could not download the module. v0.19.0 has the same contents. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): backfill a failed request's follows to its replacement A follow made for a request that later failed stayed with the failed request when its replacement had already completed and was still waiting to notify, since the backfill only looked for an open request. It now gives such a follow the title's first request since the follow that still has a notification to send. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): keep a follow stamped during its request's completion A completion's timestamp is taken when its transaction begins, so a follow that committed while it ran can carry a later created_at than the request's completed_at. The backfill took that as the request having closed before the follow and moved or dropped the follow. A completed request that has not notified now keeps such a follow when the title has no request created after it; a follow whose request was replaced and deleted always has that replacement after it, so the two cases stay apart. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): backfill a follow to a failed replacement request A follow whose request was replaced and deleted, where the replacement has since failed too, was dropped: the fallback looked only at active requests. Such a follow now stays with the title's latest failed request, for the next request to take over. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Providing Context (Files & MCPs)Add these hints in your PR description (or a comment) to unlock deeper checks:
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
The download refresh pass stopped at a target whose router features could not be read, without moving it back in the rotation. A lasting failure kept the same requests at the head of every batch, and downloads outside the batch were never refreshed. The target is now settled as unanswered, like one its server did not answer for, and the request's other targets are still asked about. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The title page looked for the viewer's cancellable request among the account's newest 50 active requests, so an older pending request lost its Cancel action. The detail names the request, so it is now read directly and kept when the viewer's account made it; a detail without the ID still falls back to the list. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2a2ba20701
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 107bbe83ea
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
MarkAvailable's callers check that a request has no live target before calling it, but a submission could queue one in between, and the title arriving then completed a request that was still downloading. The guarded update now also requires that no target is queued or downloading. Retrying a failed request also takes back the follows a later request for the title took over when that request failed too, so they are told when the retried request arrives. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Providing Context (Files & MCPs)Add these hints in your PR description (or a comment) to unlock deeper checks:
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
The request suggestions in quick search pulled their cards, grid and status badge into the launch bundle, which put it over its budget once merged with main. They appear only after a search runs, so they now load on first use; their keyboard options come from the search query, so navigation is in place before they arrive. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Moving quick search's request suggestions out of the launch bundle left it 1.4 KB under its budget; the check asks for the saving to be recorded. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Providing Context (Files & MCPs)Add these hints in your PR description (or a comment) to unlock deeper checks:
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a846694f55
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The server-channel announcement went out inside NotifyFulfilled, before the followers were cleared and the request stamped as notified. If either write failed, the next pass notified the request again: personal deliveries deduplicate, but the announcement was posted a second time. It now goes out from AnnounceFulfilled, which the notify pass calls only when its stamp took. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With the suggestions loading lazily, their rows became keyboard options as soon as the search returned, so while the code loaded, or after it failed to load, arrow keys could select a title that was not shown and Enter could open it. A signal inside their Suspense boundary now marks them shown in the same commit that paints them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Providing Context (Files & MCPs)Add these hints in your PR description (or a comment) to unlock deeper checks:
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ac6f140f3f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Problem
Closes #1554
Related issue: #1201, #582, #587, #1643
Validation tasks: changes #1196 C1–C2, #1197 C1–C2 (group request policy: request approval and limits move to the account and access-group pages, #1563, and the invitation case checks the new group policy; re-validate after merge); reaches #1202, #1203, #1204, Silo-Server/silo-apple#328, Silo-Server/silo-apple#329, Silo-Server/silo-android#338, Silo-Server/silo-android#339, #1234, #1235, Silo-Server/silo-apple#336 and Silo-Server/silo-android#346 (none run yet)
Requests could only go to one Sonarr or Radarr per media type, chosen by switches on each connection. There was no way to send anime, kids' titles or 4K copies to different servers, no way to request some seasons of a series, and the request pages and admin queue did not match the rest of the app. The request lifecycle could also double-submit or stall when two actors (two admins, or an admin and the reconcile pass) acted on the same request.
This merges the request revamp branch. Its parts were reviewed and merged into
feat/request-revampas separate pull requests (#1559–#1565, #1609, #1619, #1620, #1622, #1623, #1649), and review findings on this pull request were fixed in #1644, #1653 and on the branch. The branch is up to date with main.Approach
state. A profile can follow a title someone else requested and is told when it arrives.reports_download_progress(plugin SDK v0.19.0); a status-only pass refreshes those downloads every minute.API changes are additive and v2-only;
/api/v1is unchanged.docs/architecture/media-requests.mdanddocs/architecture/api-contract.mddescribe the new behavior.Validation
go testpasses forinternal/requests,internal/apiv2,internal/api,internal/contractledger,internal/pluginsandinternal/metadata, including DB-backed tests. Four DB-backed tests outside requests fail against the shared local test database because of known fixture drift; they fail the same way on main.make lint-changedis clean.Risks
Checklist
AI Disclosure
🤖 Generated with Claude Code
Note
Add rule-based request routing, season requests, and rework request UI and admin
request_routestables, with admin CRUD, reordering, and preview endpoints plus a full settings UI in RequestsSettings.tsx and RequestRoutingList.tsx./requestshub with Discover and Yours tabs,/title/:mediaType/:tmdbIddetail pages with request/cancel/follow actions, request follows with follower-specific notifications, and download-progress reporting and polling./admin/requests?tab=URLs redirect to new locations; several SQL migrations add new tables and columns (request_routes,request_follows,request_group_limits, download-progress and submission-state columns).Macroscope summarized ac6f140.