* docs: design spec for multi-instance Sonarr/Radarr request routing
Seerr-style multi-instance arr management inside Silo's request system:
many instances per kind, HD/4K default routing, entitlement-driven
dual-quality fan-out, per-instance anime overrides (keyword 210024),
and a one-to-many media_request_targets model.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: implementation plan for multi-instance arr request routing
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(requests): migration for multi-instance arr routing
Adds migration 169 to convert request_integrations from a one-row-per-kind
table keyed on `kind` to a multi-instance table keyed on `id`, with HD/4K
defaults, anime overrides, and a new one-to-many media_request_targets table
for per-quality fulfillment tracking.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(requests): instance, target, and dual-quality types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(requests): id-based integration CRUD
Replace upsert-by-kind (UpsertIntegration/UpsertIntegrations) with
GetIntegration, CreateIntegration, UpdateIntegration, DeleteIntegration,
and ClearDefault. Rewrites scanIntegration and integrationColumns to cover
all new multi-instance columns (id, name, is_4k, is_default, is_default_4k,
anime_* fields). Updates the Store interface accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(requests): target persistence and aggregate status
* feat(tmdb): expose keyword ids and original language on detail
* feat(requests): Seerr-exact anime detection (keyword 210024)
* feat(requests): quality/anime routing engine
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(requests): force_dual_quality setting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(requests): multi-target fulfillment, reconcile, retry, and instance CRUD
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(api): request integration CRUD endpoints, targets in responses, entitlement wiring
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(web): multi-instance request integration types and CRUD hooks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(web): multi-instance arr manager, dual-quality toggle, per-target queue
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): UX review fixes for arr manager (delete confirm, switch hints, test feedback, dirty + target status)
* fix(requests): address code-review findings (test-connection by id, HD-only default ceiling, retryable partial failure, idempotent submit, transactional defaults, presence/target reconcile, auto-approve gate)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review (anime override fallback, non-null slices, save gate, a11y, DeleteTarget not-found)
- routing: anime fields only override standard root/profile/tags when set,
so enabling anime with blank fields reuses standard values instead of
clearing them into an invalid submission
- api: normalize nil Tags/AnimeTags to [] so they serialize as arrays not null
- web: require an API key before saving a NEW instance; add aria-expanded/
aria-controls to the anime-overrides disclosure toggle
- repo: DeleteTarget returns ErrNotFound when no row was deleted
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(requests): address PR review findings
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>
- GetExternalIDs now uses the dedicated /movie/{id}/external_ids and
/tv/{id}/external_ids endpoints instead of fetching the full detail
with append_to_response=external_ids. The dedicated payload is
one or two orders of magnitude smaller for the same fields.
- Document PosterPath/BackdropPath on MediaResult as raw TMDB path
fragments that callers must prefix with the image base URL.
- normalizeCast switches from inline insertion sort to sort.SliceStable.
The output is identical; the new form is one line and O(n log n).
- normalizeIntegration no longer reuses integration.Tags' backing
array via Tags[:0]; the slice is callable code, so reusing the
array would silently corrupt the caller's slice if it kept a
reference. Allocate a fresh slice instead.
- HandleGet now requires a profile, matching the rest of the
/requests user-group handlers. Router middleware enforces this
already, but the inline check is defense-in-depth for any future
remount.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The radarr and sonarr clients carried byte-identical copies of
rootFolderResource, qualityProfileResource, tagResource (and the
corresponding list helpers) plus acceptedWithoutResponse and
statusFromQueueEvaluation. Move the shared wire types and helpers
into the arrclient package and update the callers to use the
exported helpers. No behavior change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each homepage discovery section fired a serial TMDB round trip with
its own presence lookup, so the response time grew linearly with the
number of sections (~1.2 s at 6 sections * 200 ms). Fan the calls out
across a bounded errgroup using the same concurrency cap as
external-id hydration. The first section to error cancels the rest
via the group context.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owners can now POST /requests/{id}/cancel to withdraw a pending
request; admins can cancel any active request that has not entered
the fulfillment pipeline. The route is mounted on both the user
group (with profile required) and the admin group. The cancelled
outcome was already reserved in the migration's CHECK constraint
but was unreachable from any handler.
Decline now also rejects approved requests — between Approve setting
StatusApproved and the reconciler picking the request up, an admin
could declare the request declined while submission was about to
fire. The reconciler's outcome filter would skip the request, but
the narrow window meant external state could diverge from Silo's
view. Refuse decline once a request is approved; callers should
wait for completion or use the failed/retry path.
Reconcile now emits a slog.WarnContext at the per-request failure
site with request id, media type, tmdb id, status, and integration
kind. Aggregated counters in ReconcileResult are unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Radarr and Sonarr can return HTTP 201 with no body when a movie or
series is added. The previous code returned an "accepted_without_response"
result with an empty ExternalID, which trapped the reconciler: every
subsequent CheckStatus call short-circuited on the empty ID and the
request never advanced past queued.
When the add POST decodes empty, look the freshly-added record up by
TMDB or TVDB ID via the standard list endpoints and use the resulting
Arr ID. Fall back to the previous accepted-without-response result
only when the lookup also returns no match, preserving the original
behavior as a safety net.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CreateRequest previously read the user's request count outside the
insert transaction, so two concurrent submissions at MaxRequests-1
could both pass the quota gate and end up at MaxRequests+1. Move the
count inside the same transaction as the insert and acquire a per-user
advisory lock so concurrent inserts serialize. The store reports
ErrQuotaExceeded when the racing path catches the user at the limit
and the service maps it back to QuotaError.
normalizeListFilter previously reset limit to 50 when callers asked
for more than 100, which is surprising. Clamp to the cap instead so a
request for 150 returns 100 and a request for 1_000_000 still cannot
hit the database with an unbounded scan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Batch integration upserts in a single transaction
- Treat radarr/sonarr lookup results as arrays and require exact matches
- Prefer queue failures over downloading state when evaluating arr queues
- Allow retrying queued/downloading requests and block declines once fulfillment started
- Fall back to pending when auto-approval integration check fails
- Rename requests query hooks file and fix discover card request affordance
- Add `media_type=all` to request search, backed by TMDB `/search/multi` filtered to movies and series
- Default the Requests page filter to All and refresh search results grid styling
- Refine RequestPosterCard with status accent bar, richer fallback poster, and fluid grid layout
Wire curated TMDB-backed studios/networks/genres discovery into the requests service and UI, replacing on-demand logo fetches with fixed duotone logos and adding browse routes plus tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add GetMediaDetail TMDB client returning normalized detail with cast, crew, recommendations, and certifications
- Add /api/requests/detail/{media_type}/{tmdb_id} endpoint overlaying availability and request state
- Add RequestDetail page and link poster cards to it
- Treat empty/truncated Radarr/Sonarr POST responses as accepted; drop pre-submit existence lookups
- Add request domain, repository, service, and reconcile task
- Add Radarr/Sonarr fulfillment adapters and TMDB discovery
- Expose user and admin request APIs with quota and approval rules
- Add web UI for browsing, requesting, and admin queue management
- Migration 139 introduces media_requests and related tables