Files
silo-server/migrations/169_request_multi_instance.up.sql
T
ea3b5a2e29 feat(requests): multi-instance Sonarr/Radarr routing with HD/4K defaults and anime overrides (#39)
* 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>
2026-06-02 11:25:18 -04:00

87 lines
3.8 KiB
SQL

-- request_integrations: one-row-per-kind -> many instances keyed by id.
ALTER TABLE public.request_integrations
ADD COLUMN IF NOT EXISTS id text,
ADD COLUMN IF NOT EXISTS name text NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS is_4k boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS is_default boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS is_default_4k boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS anime_enabled boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS anime_quality_profile_id integer,
ADD COLUMN IF NOT EXISTS anime_root_folder text NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS anime_tags integer[] NOT NULL DEFAULT '{}';
-- Backfill: the lone existing row per kind becomes that kind's HD default.
UPDATE public.request_integrations
SET id = gen_random_uuid()::text,
name = initcap(kind),
is_default = enabled
WHERE id IS NULL;
-- Swap the primary key from kind to id; keep kind as a plain column.
ALTER TABLE public.request_integrations
DROP CONSTRAINT request_integrations_pkey;
ALTER TABLE public.request_integrations
ALTER COLUMN id SET NOT NULL,
ADD PRIMARY KEY (id);
-- Quality-role invariants: at most one default / one 4K-default per kind.
CREATE UNIQUE INDEX IF NOT EXISTS idx_request_integrations_default_per_kind
ON public.request_integrations (kind) WHERE is_default;
CREATE UNIQUE INDEX IF NOT EXISTS idx_request_integrations_default4k_per_kind
ON public.request_integrations (kind) WHERE is_default_4k;
-- Targets: one request -> N fulfillment targets.
CREATE TABLE IF NOT EXISTS public.media_request_targets (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
request_id text NOT NULL REFERENCES public.media_requests(id) ON DELETE CASCADE,
integration_id text REFERENCES public.request_integrations(id) ON DELETE SET NULL,
integration_kind text NOT NULL DEFAULT '',
quality text NOT NULL,
is_anime boolean NOT NULL DEFAULT false,
external_id text NOT NULL DEFAULT '',
external_status text NOT NULL DEFAULT '',
status text NOT NULL DEFAULT 'queued',
last_error text NOT NULL DEFAULT '',
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT media_request_targets_quality_check CHECK (quality IN ('1080p', '2160p')),
CONSTRAINT media_request_targets_status_check
CHECK (status IN ('queued', 'downloading', 'completed', 'failed'))
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_media_request_targets_request_quality
ON public.media_request_targets (request_id, quality);
CREATE INDEX IF NOT EXISTS idx_media_request_targets_request
ON public.media_request_targets (request_id);
-- Backfill targets from already-submitted requests (those with an external id).
INSERT INTO public.media_request_targets
(request_id, integration_id, integration_kind, quality, is_anime,
external_id, external_status, status, created_at, updated_at)
SELECT mr.id,
ri.id,
mr.integration_kind,
'1080p',
false,
mr.external_id,
mr.external_status,
CASE
WHEN mr.status = 'completed' THEN 'completed'
WHEN mr.status = 'downloading' THEN 'downloading'
WHEN mr.outcome = 'failed' THEN 'failed'
ELSE 'queued'
END,
mr.created_at,
mr.updated_at
FROM public.media_requests mr
LEFT JOIN public.request_integrations ri ON ri.kind = mr.integration_kind
WHERE mr.external_id <> '';
-- media_requests: add is_anime, move per-fulfillment columns out to targets.
ALTER TABLE public.media_requests
ADD COLUMN IF NOT EXISTS is_anime boolean NOT NULL DEFAULT false;
ALTER TABLE public.media_requests
DROP COLUMN IF EXISTS integration_kind,
DROP COLUMN IF EXISTS external_id,
DROP COLUMN IF EXISTS external_status;