feat(requests): show download progress while a request downloads - #1649
Conversation
Request-router plugins can now report how far a target's downloads are (TargetStatus.progress, declared with request_router.reports_download_progress). Store it on the target in new download_* columns, and show it on the Requests page, the title page's request bar and the admin queue: a bar once the size is known and "Downloading · 43% · about 12 min left", or the phase (waiting, paused, stalled, importing, import blocked). The reconcile pass records a download's first progress. A new one-minute refresh_request_downloads task then refreshes only downloading targets that have progress, from plugins that declare it. It shares a target-write lock with reconcile, which waits for it rather than skipping, and runs within a 90-second budget. Progress writes leave the target's updated_at, the request status and its history alone; progress clears on completion or failure, when the plugin stops reporting it, or 15 minutes after its server stops answering. A target's raw external status is kept in step with its phase. The v2 API adds RequestDownload on request targets, requests and the title detail's request state, and download_progress_supported on the requests status capability. Clients poll every 30 seconds while something they show downloads. TMDB title details are cached for two minutes so title pages polling a download share one fetch. The plugin SDK is pinned to the silo-plugin-sdk PR commit until v0.19.0 is tagged. Refs #1643 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8d2ee249e8
ℹ️ 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".
…ests from every live target The reconcile pass took its own advisory lock and the request target write lock on two pooled connections, so with database.max_connections at 2 its first query waited forever for a third. pglock gains Lock.AcquireAlso, which takes a second key on the session already holding a lock, and Release frees every key the session holds; the reconcile pass takes both locks through it. pglock.Acquire, which only this pass used, is gone. A request's combined download progress skipped a live target that had not reported progress yet, so a 1080p and 4K request could show 90% from the 1080p copy alone. Such a target now leaves the combined size unknown, as the docs already said. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
The Suggested replacement: Automated check: Macroscope check run agent (gpt-6-luna). No validation was performed. Posted via Macroscope — v1 validation impact |
…equest-download-progress # Conflicts: # contracts/api/v2/fixtures/get_system_info_ok.json
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6f3831f556
ℹ️ 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 download refresh pass, and its idle check on every API node, select downloading targets that have progress once a minute. Without an index on that predicate each run scanned all of media_request_targets, which grows with request history. A partial index on (request_id, download_checked_at) where status = 'downloading' and download_phase is set covers only those few rows, built concurrently. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ts (#1651) The profile-scoped v2 request operations (createRequest, listMyRequests, getRequest, cancelRequest) gave a requester every target's download server id, kind and name, the server's own id and raw status, the routing rule and the target error, plus the request's integration kind, external fields and submission error. These are admin details. They show how the admin named their servers and routing rules, and the errors can carry a plugin's raw text, such as a server URL or a release title. mediaRequestOf now takes the viewer and fills those fields for an admin only. A requester keeps the request's state and outcome_reason and each target's quality, status and download progress. An admin still sees everything, on the profile-scoped operations and on the admin request operations. /api/v1 is frozen and unchanged. The fields were already optional, so the contract diff reports no change; their descriptions now say admins only. Refs #1646 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: 8a50d47ea5
ℹ️ 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".
| reports, err := s.targetReportsProgress(ctx, fc, req.MediaType, t) | ||
| if err != nil { | ||
| return reconcileUnchanged, false, err |
There was a problem hiding this comment.
Rotate candidates after capability read errors
When reading reports_download_progress fails for a candidate, this returns before either applyTargetStatuses or MarkTargetDownloadChecked advances its download_checked_at. Because ListDownloadingRequests always selects the oldest 200 candidates, a persistent capability-metadata error affecting the first batch makes those same requests fill every pass and prevents healthy downloads behind them from ever refreshing. Preserve the error, but mark the affected progress-bearing targets as checked so failures still take their turn in the rotation.
AGENTS.md reference: AGENTS.md:L73-L75
Useful? React with 👍 / 👎.
…request pages and admin (#1632) * fix(requests): guard lifecycle transitions and make reconcile fair 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> * feat(requests): complete requests from the library without a router 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> * fix(web): give request pages the app shell and one status badge 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> * feat(requests): follow a title someone else requested 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> * feat(api): give requests one user-facing state and keep decline reasons 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> * feat(requests): capture routing facts when a request is created 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> * feat(requests): route requests to servers with ordered rules 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> * feat(api): manage request routing rules 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> * feat(admin): manage requests, servers and routing in Settings 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> * feat(requests): request individual seasons of a series 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> * feat(web): show titles outside the library on the shared detail layout 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> * feat(web): pick the seasons of a series to request 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> * chore(web): raise the launch bundle budget for season requests 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> * feat(admin): rework the request queue 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> * refactor(web): show request titles on the library's media card 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> * feat(admin): manage request access on accounts and access groups 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> * feat(web): make the Requests page read like the rest of the app 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> * chore(web): raise the launch bundle budget for the request queue and 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> * feat(requests): route on exclusions and content rating, and explain each 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> * feat(admin): make request routing a plain-language list with presets 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> * feat(admin): open a queued request in a dialog instead of a side sheet 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> * feat(requests): add Standard routing that needs no rules 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> * feat(requests): classify anime and ratings beyond TMDB's US data 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> * fix(admin): line settings controls up on one edge 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> * fix(admin): make the Requests settings link buttons visible on their 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> * feat(admin): lay out routing destinations as stacked panels 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> * fix(requests): use the library season carousel on request pages 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> * fix(requests): send anime series under Standard routing as Sonarr anime 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> * perf(web): keep request administration out of the launch bundle 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> * fix(web): refresh request search and reconnect state after request changes 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> * fix(admin): let Standard delete a server only Everything else points at Under Standard routing the server clears the hidden Everything else before it checks what still sends requests to a server, so a fallback reference never blocks the delete. The server editor still counted it and disabled Delete, for example on the normal Radarr when a 4K Radarr also exists. The editor now takes the routing mode and ignores fallback references under Standard; paused rules still block. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): keep an upcoming season open when an episode arrives early A season whose episodes are all dated after today counted as complete as soon as one of them was in the library, because the "no air dates" rule only looked at the aired count. An early episode could then refuse a request for that season as already available, or complete a request for it. Season counts now carry how many episodes are dated to air later. The any-episode rule applies only to a season with no dated episodes; a dated season waits for its first episode to air. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): show a season request's progress on the title detail The title detail derived an active season request's state without its season progress, so a request with some seasons already in the library read as approved or processing there, while the request lists showed it partially available. The detail now attaches the progress from the season counts it already reads. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * perf(requests): read season counts once per reconcile batch Reconciliation checked each season request with its own season-count query, so a full batch and its waiting batch could run well over a thousand queries every pass. The presence check now collects the season requests whose series is in the library and reads their counts in one query, as the request lists already do. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): keep the quota refund when an admin closes a failed request Closing a failed request from the admin queue turned its outcome from failed into cancelled, and the quota counts cancelled requests, so the cleanup took back the slot the failure had returned. A cancelled request that still carries a submission error now keeps its refund; a plain withdrawal still counts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): keep library season requests from a later download server A series already in the library can be requested for its missing seasons only while no download server takes series, because router plugins cannot receive seasons and would add the whole series. That was checked only when the request was made: if an admin set up a series download server before the request was approved or reconciled, the submission sent it as a whole-series request. Submission now checks again: a season request whose series is in the library stays approved and waits for the library instead of going to the router. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): key title follows by account as well as profile A profile id is only unique within its account: every account from before profiles has one named "default". Follows were keyed by profile id alone, so a second account's "default" profile could not follow a title the first one followed, could unfollow or see the other's follow, and was treated as the requester when the requester's profile id matched. Key media_request_follows by (media_type, tmdb_id, user_id, profile_id) in a new migration, scope follow lookups, unfollows and clears by account and profile, and compare the account when deciding whether the viewer made the request or was already told. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): clear a title's followers before stamping it notified The fulfilled pass stamped the request notified and then cleared its followers. If the clear failed, the stamped request never came back to the pass, and its follows stayed behind to fire for a later request of the same title. Clear first and stamp only after the clear succeeds; a retry re-sends into the per-recipient delivery dedupe. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): keep Standard's 4K server when a save turns Advanced on When Standard sent HD to one server and 4K to another, and an edit made the HD server a second 4K server, the switch to Advanced skipped the media type entirely because its HD server no longer fit. Everything else then kept no 4K destination and 4K copies stopped. The seed now carries each tier on its own, so the unchanged 4K server is still filled in. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): flag unusable route servers in preview and validation The route preview now reports every server problem a routed submission fails on: a server that no longer exists, is not bound to a plugin installation, has no API key, or does not take the media type. Before, an unbound or keyless server previewed as working. A route can no longer send a media type to a server whose supported media types exclude it, a routed submission refuses such a server with a retryable error instead of handing it the request, and a server used by a route cannot drop the route's media type. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(api): report request routing in the admin request capabilities getAdminRequestCapabilities gains a routing field that says whether the /admin/request-routes operations are available, so clients detect routing without probing the routes or reading the server version. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): only let the current claim defer a failed submission DeferSubmission matched on the request id and approved state alone. A submission that outlived its ten-minute lease while another server claimed the request again could still release that newer claim and push its next attempt back, letting a third claim overlap the live one. Pass the claim's lease expiry to DeferSubmission and require it to match the stored one. A stale attempt now gets ErrInvalidState and returns the current request without touching the newer claim. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(api): say the routing capability covers the routing mode The routing flag on the admin request capabilities was documented as covering only the rules under /admin/request-routes. The Standard/Advanced routing mode operations under /admin/request-routing are on the same interface and ship with them, so say the flag covers both. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(requests): say plainly when failed targets are kept A failed entitlement lookup or a skipped router connection keeps every failed target. The old sentence read as if those cases shrank the set and deleted the targets. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): only let the current claim fail a submission After the last allowed attempt, the submission was marked failed through SetOutcome, which checked only that the request was still approved and active. An attempt that outlived its ten-minute lease while another server claimed the request again could fail that newer, still running attempt. Add FailSubmission, fenced on the claim's submit_lease_until the same way DeferSubmission is. It marks the request failed and releases the claim. A stale attempt now gets ErrInvalidState and returns the current request. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): hold the open request while a follow is inserted The follow insert read the title's open request without locking it. A decline, cancel or completion could commit and clear the title's follows between that read and the insert, leaving a follow on a closed title that a later request for the same title would notify unasked. Take FOR SHARE on the open request in the insert. The closing UPDATE's row lock conflicts with it, so the two serialize: a follow that waited re-checks the updated row, finds it closed, and inserts nothing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(animeids): parse verbatim entries from the published anime list Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): keep Chinese and Korean productions out of list-only anime matches A title on the AniDB-based list now also needs to not be made in China or Korea, unless Japan co-produced it, before the list alone marks it anime. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(web): refresh the request queue when an admin's cancel is refused When another admin approves, retries, or closes a request first, the cancel answers a conflict. The queue is not polled, so the obsolete row kept its Cancel or Close action. Invalidate the request surfaces on settle, as approve, decline, and retry already do. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(requests): say that no failed request counts against the quota The re-request section still said other accounts' failed requests count against their quota, which this branch's quota rule no longer does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(web): scope access-group request limits to the admin's profile The limit's ETag names the profile that read it, but its cache key held only the group ID. After a household profile switch the editor reused the other profile's cached limit, and its next save failed with 412. Key the limit by the admin authority scope, as the access-group queries are, and have the group editor read and save it under the authority it read the group with. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(web): keep the Request to add page in the search URL The Request to add grid on a search kept its page only in component state, so Back from a title, a reload, or a shared search link returned to page 1. Catalog now keeps the page in a request_page URL parameter and passes it to the grid. A new query or filter builds fresh params and a scope change drops it, so those still start over at page 1. Paging the grid no longer counts as a new search for the library results. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): route older series requests under Standard while TMDB is down Standard's anime route has a condition, so a series request whose routing facts were never captured waited for TMDB before it could be sent. Standard only needs to know whether the title is anime, and the request already stores that, so it now routes on the stored flag when TMDB cannot answer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(web): name route destinations with their Send to label, and open reloaded settings The destination select is now named by its section and its visible label ("HD copies Send to"), so screen readers and voice control hear the label shown on screen. More settings opens when a conflict reload or a preset brings in a setting, instead of saving it collapsed and unseen. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): record a submission's targets only while its claim holds A router call that outlived its 10-minute lease could still write targets after the request was withdrawn, completed from the library, or claimed again. Recomputing the status from those targets moved a cancelled request back to queued. Targets are now written in one transaction that checks the claim's lease, like the defer and fail paths; a stale attempt drops its result, logs it, and returns the request as it stands. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(web): give the group-limit save test a complete request body Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): keep requests with the plugin that owned them before routing The routing migration seeded routes from the default Radarr and Sonarr servers of any plugin. Before routing, a media type's requests went to the plugin owning the first usable connection by name, so an install whose first connection is Seerr would have started sending requests to Radarr or Sonarr. A new migration removes seeded routes that send outside that owner and that no admin has saved since, which hands the media type back to the plugin, as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): keep checking targets a plugin returned without a connection Reconciliation grouped targets by the plugin that owns their server and skipped a target with no connection, so a queued or downloading target the plugin returned without connection_id never got its status checked again. Such a target is now checked through the plugin that routes the media type without rules, with all its connections, as before routing. A routed tier goes to one server, so a target returned for it without a connection is recorded on that server. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(web): hide request routing when the server does not offer it Settings -> Requests called the route endpoints whenever request settings were available. It now reads the routing capability and, when it is false, leaves the routing groups out and does not load the routes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(web): keep the Request to add pager when page one has nothing to request When TMDB's first page holds only titles already in the library but more pages exist, the grid now shows the section with its pager and a short note, so the viewer can reach the later pages. A single empty page still hides the section, and the dialog, which has no pager, is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): match connectionless statuses to the routed server A routed target the plugin returned without a connection is recorded on the route's server, but reconciliation matched statuses by the exact quality and connection pair, so a status that also came back without a connection never matched and the target stayed open. When every target checked through a plugin is on one server, a status without a connection is now taken as that server's. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): return season progress from create, approve, and retry A season request returned by a mutation lacked its library match and season counts, so its state read approved while a detail or list read of the same request said partially_available. Create, approve, and retry now attach them the way the reads do, with one presence lookup per mutation. A failed lookup is logged and the committed request returned. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): remove a media type's seeded routes together The owner repair removed each seeded route on its own, so when the fallback sent 4K to another plugin's server but the Anime route stayed on the owner's, only the fallback went. The surviving Anime route kept Silo routing on, and every title it did not match failed with no fallback. The repair now removes a media type's untouched seeded routes together when any of them leaves the owner, and leaves a media type alone when an admin has saved or added one of its routes, so its routing keeps a fallback. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(web): allow a request limit of 0 The API accepts a global request limit of 0, which with per-account limits lets only those accounts request, but the settings page refused anything below 1. The field now takes any whole number from 0 and says what 0 does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): clear a closed request's follows in the closing transaction Declining or withdrawing a request cleared the title's follows after the close committed. In that gap another node could open a replacement request and a new follower, and the title-wide delete removed the new follow. SetOutcome now deletes the follows inside its transaction, and leaves them when the title has another open request. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): refuse a server save that would hand Seerr's requests to another service Under Standard, a media type sent to a plugin that picks its own server (Seerr) gets no Everything else route when a save turns Advanced on. If another plugin installation now also takes that media type, the first connection by name would get its requests, so adding a Radarr beside Seerr could silently take Seerr's movies. The save is now refused with a message to switch to Advanced and set Everything else first. Seeding Seerr into Everything else instead would change how it handles 4K and anime, since a rule sends each tier on its own. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): count a withdrawal made during submission backoff against the quota The quota refunded any cancelled request that still carried a submission error, so an admin closing a failed request kept its refund. A request backing off after a failed attempt also carries that error while it stays active, and its owner may withdraw it, so during a router outage an account could request, wait for the first failed attempt, withdraw and request again without using quota. Cancelling now clears the error unless the request had failed, so only a closed failed request keeps its refund. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(web): keep the open request sheet in step with the refreshed queue The queue's request sheet kept its own copy of the request. When another admin acted first and a local action was refused, the queue refetched but the sheet went on showing the old state and actions. The sheet now keeps only the request's id and shows the view's current row, or the answer to this page's own action when that is newer. When the request has left the view and nothing newer is known, the sheet closes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): make Standard unavailable when Seerr shares a media type with another service Standard accepted Seerr as a media type's normal server with a Radarr marked 4K (or the other way round), since each tier had one server. But Standard hands a plugin that picks its own server the whole request, so submission fell back to the first installation by name and the 4K destination Standard showed was never used. A self-routing plugin in either tier now needs the other tier to be its own connection; otherwise Standard is unavailable with a reason, and a server save that creates the split turns Advanced on, where the existing Seerr guard refuses the save when Seerr's requests would go to another service. Radarrs or Sonarrs from different plugins stay allowed: they are routed tier by tier. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): drop routed targets returned for the other quality Each routed call asks the plugin for one quality on one server. A target it labels with the other quality was kept, and with both qualities routed it could take that quality's slot ahead of the target the right server returned, leaving Silo tracking the wrong server. Keep only the requested quality from each call and log the rest; a quality left with no target is still recorded as failed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): read the routing mode before the routing rules The fulfillment context read the rules and then the routing mode in separate queries. A switch from Standard to Advanced writes Everything else and the mode in one commit, so landing between the two reads paired the old rules (often none) with Advanced, and the request went to the first plugin by name. Reading the mode first means a request sees either Standard, which ignores the rules, or Advanced with the rules it was committed with. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(requests): name the split-service test's Radarr so goconst stays quiet Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): say why a request server can't be reached The options probe answered every failure (a missing scheme or key, a rejected key, a closed port, https on an http port) with one 503. The host now classifies the plugin's error into host-written messages: field errors for the URL and API key, a 503 detail for an unreachable server, and a plugin's own InvalidArgument/FailedPrecondition text as a form error. Upstream text still never reaches the response. Base URLs without a scheme get http:// on probe and save. The editor shows probe errors beside their fields, adds http:// on blur, and takes the server type and name from a plugin that detects Sonarr or Radarr. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): keep the v1 options failure and drop stale probes The v1 options route is frozen, so it keeps answering a failed probe with its original 500 instead of the new field errors. In the editor, a changed connection now discards a probe still in flight, and a name detection filled in follows a later detection until the admin types one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(web): keep probe defaults when detection sets the server type The first probe's root folder and quality profile defaults and the detected type were both written from the pre-probe config, so the second update dropped the first and a new server could not be added without a Test. Both now update from the latest config. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): normalize saved URLs on v2 only and drop stale Tests URL normalization on save moves from the shared service into the v2 adapter, so the frozen v1 create and update routes save base_url as given. The editor's Test ignores an answer for a connection the admin changed while it ran. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): keep v1 probes as given and settle Test and 4K naming The v1 options route now sends the submitted address unchanged; the v2 adapter normalizes it for both probe and save. In the editor, only a connection change makes a probe stale, so a Test the debounced probe overtook for the same address still reports, and a name detection filled in picks up the 4K switch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): hide only host-classified probe errors from v1 Probe errors the host classified now carry their own type, and the v1 options route keeps its 500 for those alone. A validation error the router returns itself keeps v1's 400 as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(requests): describe v2 integration URL and probe errors Records the v2 base_url normalization and the options probe's problem answers in the API contract, and limits "not Sonarr or Radarr" to an HTML body where the API should be, so a truncated JSON answer is not blamed on the URL. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(requests): send requested seasons to season-capable routers A series in the library could be requested for its missing seasons only when no download server took series, because the router contract carried no seasons and any router would add the whole series again. silo-plugin-sdk v0.18.0 adds RequestDescriptor.seasons and a manifest flag, request_router.supports_seasons. Move to it and: - send a series request's seasons in the descriptor for Fulfill and CheckStatus (empty still means the whole series) - read supports_seasons from the capability metadata stored at install, through plugins.Service.RequestRouterDescriptor, cached per fulfill context - offer missing seasons (missing_seasons_requestable, per-title requestability, create) when every enabled series server is bound to a plugin that declares the flag - submit a missing-seasons request for a series in the library only where the chosen servers take seasons: every series server without routing rules, the rule-chosen servers with them (every series destination until routing facts are captured); otherwise it waits for the library as before, and a server chosen after the claim that cannot take seasons fails the tier instead of receiving it Whole-series requests and plugins without the flag behave as before. Update the media-requests architecture doc and the status field's description. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(web): load more request browse titles as the viewer scrolls The studio, network, and genre browse page and a Discover row's "Explore all" page read TMDB page by page with useInfiniteQuery and an IntersectionObserver sentinel instead of a Previous/Next pager. Placeholders continue the grid while the next page loads, a title an earlier page already showed is dropped, and a failed later page keeps the loaded titles and offers Try again. Query keys no longer carry the page number, so realtime invalidation refetches every loaded page. A legacy ?page=N is ignored. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): make the routing editor's HD and 4K choices clearer Routing text says "version" instead of "copy", in the web UI and in the server messages it shows. HD dropdowns list only servers not marked 4K and 4K dropdowns only servers marked 4K; a saved choice that no longer fits stays visible with a "(marked 4K)" or "(not marked 4K)" label, and the editor says so when no server is marked 4K. The server refuses a route that sends a version to the wrong kind of server, and refuses turning a server's "4K server" switch on or off while routes depend on it. The rule conditions editor marks the AND between conditions and shows a live "Takes: ..." summary, and the preset dialog spaces its HD and 4K panels apart. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(web): show a spinner on the running request queue action Clicking Retry (or Approve, Decline, Cancel request) only disabled the row's buttons, so nothing showed the action was in progress. The queue now tracks which action is running and swaps that button's icon for a spinner until the server answers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(web): cover the request queue's running-action spinner Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(web): keep loading request browse pages past an empty page A rating-restricted profile can get a page with nothing it may see while more pages follow. The browse and Discover row pages said the list was empty and dropped the load control, so later titles were unreachable. They now show "nothing" only when no page is left, and a later page's failure offers Try again even when every loaded page was empty. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): leave the 4K switch free under Standard routing Standard pauses the stored routes, and the editor hides them, so an admin could not follow "change those routes first" when a paused route (such as the Everything else route made for the first server) sent the other version to a server whose 4K switch they changed. The tier check on a server's 4K switch now applies only under Advanced routing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): clear routes that changed tier under Standard, exempt Seerr Turning Advanced on, by hand or because a server save broke Standard, now clears every stored route destination whose server no longer fits its version, so a server marked 4K under Standard no longer receives HD versions from a paused route (and vice versa); that version falls through to Everything else. A server of another plugin (Seerr) has no 4K switch of ours and handles both versions, so the tier rule no longer applies to it, and the routing editor offers it for both. The inline "Everything else server" picker offers only HD servers. The media requests architecture doc records the tier rule. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): recheck a 4K switch change under the routing-mode lock The service checked a server's 4K switch against the routes with the mode it read before the save; Advanced turned on in between skipped the check. Both repository save paths now recheck under the routing-mode lock when the mode is Advanced. The check runs only when the switch changes, so a route saved before the tier rule no longer blocks unrelated edits to its server. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): recheck route servers' tiers inside the route save A route save validated its servers before its transaction, so a 4K switch change committed in between went unnoticed. The save now holds its servers' rows FOR SHARE and checks the tier rule again, and a server save locks its row FOR UPDATE before reading the routes, so whichever commits second sees the other. Servers are locked before the route row, the order a server save that turns Advanced on uses. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): read routing facts and library seasons before sending seasons A missing-seasons request whose routing facts were not captured was held whenever any series rule sent to a server that cannot take seasons, and no later pass read the facts, so it waited for the library even after TMDB recovered. The facts are now read first, as submission does; only while TMDB cannot answer does every rule's server have to take seasons. A season request approved after its seasons reached the library was still sent to the download server. It is now left for the reconcile pass, which completes it from the library. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(requests): name the route tier fields once Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(notifications): deliver request.fulfilled once per account and profile Profile ids repeat across accounts: every account from before profiles has a profile named "default". request.fulfilled now reaches followers on other accounts, but its at-most-once index was keyed by (profile_id, request_id), so a follower whose profile id matched the requester's, or another follower's, was silently deduped away. Key the index by (user_id, profile_id, request_id) in a new migration, and send an operational delivery's webhook, web push and mobile push only to the recipient profile's targets on the recipient's account, so the two accounts' copies do not both reach each account's devices. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): state that requested season numbers start at 1 The service already refuses a season below 1, but the v2 schema did not say so and the problem carried no field error. Add minimum 1 to the seasons items, so the schema documents the rule and a zero or negative season is refused with a validation error on that item. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(requests): refuse a season below 1 on the seasons field A per-item minimum in the v2 schema counts as a breaking change against the feature branch and would need an approval entry that main's gate then rejects, since seasons are new there. Keep the schema's shape: the service refuses a season below 1 with a validation error on seasons, which v2 renders at body.seasons, and the field description states the rule. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(requests): show download progress while a request downloads (#1649) * feat(requests): show download progress while a request downloads Request-router plugins can now report how far a target's downloads are (TargetStatus.progress, declared with request_router.reports_download_progress). Store it on the target in new download_* columns, and show it on the Requests page, the title page's request bar and the admin queue: a bar once the size is known and "Downloading · 43% · about 12 min left", or the phase (waiting, paused, stalled, importing, import blocked). The reconcile pass records a download's first progress. A new one-minute refresh_request_downloads task then refreshes only downloading targets that have progress, from plugins that declare it. It shares a target-write lock with reconcile, which waits for it rather than skipping, and runs within a 90-second budget. Progress writes leave the target's updated_at, the request status and its history alone; progress clears on completion or failure, when the plugin stops reporting it, or 15 minutes after its server stops answer…
Problem
Related issue: #1643
Validation tasks: reaches #1202 C3, #1203 C1, #1204 C2 (none run yet). Availability, the request lifecycle and notifications are unchanged; this adds display and a status-only refresh.
While Sonarr or Radarr downloads a requested title, Silo shows "Processing" and nothing else. Requesters can't tell whether a download is nearly done or stuck, and admins only see a raw status string such as
downloading/downloading. The Sonarr/Radarr plugin already reads each request's download queue, but it passes on only a queued or downloading state, and Silo asks for it every five minutes, which is too slow for a progress bar.This change stores download progress reported by request-router plugins and shows it to requesters, on the title page and in the admin queue. The labels read "Downloading · 43% · about 12 min left", or the phase: "Waiting to download", "Download paused", "Download stalled", "Importing", or "Waiting for import" ("Import blocked" for admins).
Approach
TargetStatus.progressand declare it withrequest_router.reports_download_progress(feat(requestrouter): let request routers report download progress silo-plugin-sdk#27). Silo ignores progress from plugins that don't declare it.download_*columns onmedia_request_targets. Writing it never touches the target'supdated_at(the stalled-target backstop reads it), the request's status or its history. It clears when a target completes or fails, when the plugin stops reporting it, or 15 minutes after the server stops answering. When the phase moves but the status doesn't, the target's rawexternal_statusis updated the same way, so the two agree.refresh_request_downloadstask then refreshes every minute, but only downloading targets that have progress, from declaring plugins. Queued targets and targets without progress stay on the five-minute pass: an unreleased movie can sit queued for months, and Seerr keeps media at Processing with nothing in its queue. The task skips when nothing is downloading.RequestDownloadobject (phase,percent,bytes_total,bytes_left,estimated_completion_at,downloads,updated_at) appears on request targets, on the request (all its targets combined), and on the title detail's request state.GET /api/v2/requests/statusadvertisesdownload_progress_supported./api/v1is unchanged.docs/architecture/media-requests.mdanddocs/architecture/api-contract.mddescribe the behavior.The SDK is pinned to the SDK PR's commit (
v0.18.1-0.20260928194839-1abd582d0304) until v0.19.0 is tagged. The Sonarr/Radarr (Silo-Community/silo-plugins-requests-arr#17) and Seerr (Silo-Community/silo-plugins-requests-seerr#7) plugins report progress. The Apple (Silo-Server/silo-apple#537) and Android (Silo-Server/silo-android#409) changes are ready on branches and follow once this merges.Validation
make lint-changed(baseorigin/feat/request-revamp) andmake test-go.verify-*target and the committed-artifact test.make test-web.make test-db-pinson a fresh database: 23 of 23 pins pass.Evidence: https://evidence.siloserver.org/r/silo-server/request-download-progress/
Risks
feat/request-revamp, which feat(requests): route requests by rules, request seasons, and rework request pages and admin #1632 merges into main. Before merge, bump the SDK pin to v0.19.0.Checklist
AI Disclosure
Harness: T3 Code (Claude Code agent harness)
Tool(s): Claude Code, including workflow subagents; GitHub CLI; Playwright MCP
Model(s): claude-opus-5-5
Involvement: Fully AI-generated at the maintainer's request. The agent wrote the change and ran the checks and the sandbox validation.
Adversarial review: Independent Claude subagents reviewed the backend and the web/API halves over two rounds, each in a fresh context. They covered the progress invariants, fast-pass selection, locking, SQL, the API mapping and web polling. They found these problems, all fixed:
A sandbox validation pass and a completeness critic then found four display defects, also fixed: bar width, a stale raw status, and two Apple issues.
🤖 Generated with Claude Code
Note
Show request download progress in API responses and web UI
refresh_request_downloadstask that polls plugins for progress on active downloading targets every minute, bounded to 200 requests and a 90-second budget, and stores per-target progress in newmedia_request_targetscolumns (migration)RequestDownloadobject on requests, targets, and media detail, with phase, percentage, byte counts, ETA, and download count; themediaRequestOfmapper now exposes download-server identities, raw statuses, and errors only to adminsRequestDownloadProgresscomponent in the Requests list, admin queue, details dialog, and title detail, and refetches request queries every 30 seconds while any loaded request or target has download dataGetMediaDetailnow caches successful details for 2 minutes and shares concurrent in-flight fetches for the same titlego.modpinssilo-plugin-sdkto a commit-based versionMacroscope summarized 8a50d47.