CREATE TABLE subtitle_sources ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL CHECK (char_length(btrim(name)) BETWEEN 1 AND 128), source_type text NOT NULL DEFAULT 'json_rest_v1' CHECK (source_type IN ('json_rest_v1')), base_url text NOT NULL CHECK (base_url ~ '^https://'), api_key_cipher bytea, enabled boolean NOT NULL DEFAULT true, languages text[] NOT NULL DEFAULT '{}', priority integer NOT NULL DEFAULT 100 CHECK (priority BETWEEN 1 AND 10000), timeout_seconds integer NOT NULL DEFAULT 10 CHECK (timeout_seconds BETWEEN 2 AND 30), created_by uuid NOT NULL REFERENCES users(id), updated_by uuid NOT NULL REFERENCES users(id), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE (name) ); CREATE INDEX subtitle_sources_enabled_idx ON subtitle_sources(priority,id) WHERE enabled; CREATE TABLE subtitle_search_candidates ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), source_id uuid NOT NULL REFERENCES subtitle_sources(id) ON DELETE CASCADE, media_item_id uuid NOT NULL, media_source_id uuid NOT NULL, external_id text NOT NULL CHECK (char_length(external_id) BETWEEN 1 AND 512), title text NOT NULL CHECK (char_length(title) BETWEEN 1 AND 512), language text NOT NULL DEFAULT '' CHECK (char_length(language) <= 64), codec text NOT NULL CHECK (codec IN ('srt','ass','ssa','vtt','sup','sub','idx')), release_group text NOT NULL DEFAULT '' CHECK (char_length(release_group) <= 256), score double precision NOT NULL DEFAULT 0 CHECK (score >= 0 AND score <= 1), download_path text NOT NULL CHECK (char_length(download_path) BETWEEN 1 AND 2048), hearing_impaired boolean NOT NULL DEFAULT false, created_by uuid NOT NULL REFERENCES users(id), created_at timestamptz NOT NULL DEFAULT now(), expires_at timestamptz NOT NULL DEFAULT (now() + interval '1 hour') ); CREATE INDEX subtitle_search_candidates_actor_idx ON subtitle_search_candidates(created_by,created_at DESC); CREATE INDEX subtitle_search_candidates_expiry_idx ON subtitle_search_candidates(expires_at); CREATE TABLE subtitle_imports ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), candidate_id uuid NOT NULL REFERENCES subtitle_search_candidates(id) ON DELETE CASCADE, actor_user_id uuid NOT NULL REFERENCES users(id), idempotency_key text NOT NULL CHECK (char_length(idempotency_key) BETWEEN 16 AND 128), state text NOT NULL DEFAULT 'processing' CHECK (state IN ('processing','succeeded','failed')), result jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(result)='object'), error_code text NOT NULL DEFAULT '', created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE(actor_user_id,idempotency_key) );