-- #186. What subtitles exist for a media file, and what arr has tried per -- wanted language (DESIGN.md §15). Settings rows (#198) and the budget -- buckets (#197) are deliberately absent. -- One row per subtitle arr knows about. An embedded track that was never -- extracted has no `path`: it is recorded because it satisfies a language, -- not because there is a file. Everything else is a sidecar next to the -- video (§15, §7.4), so the path is the row's identity on disk. CREATE TABLE subtitle_files ( id INTEGER PRIMARY KEY, media_file_id INTEGER NOT NULL REFERENCES media_files (id) ON DELETE CASCADE, -- As `arr_core::Language` spells it: `pt-PT`, `pt-BR`, `en`. language TEXT NOT NULL, origin TEXT NOT NULL CHECK (origin IN ('embedded', 'extracted', 'provider', 'translated')), -- §15. The named provider a fetch came from, and the candidate id it was -- ranked as, so a re-fetch of the same candidate is recognisable. provider TEXT, candidate_id TEXT, -- §15. The translation backend that produced it, when machine made. engine TEXT, -- §15. A forced track never satisfies a want; an SDH one does, ranked -- below a plain subtitle. Both are facts about the file, so both are -- stored and the ranking rule stays in `arr-core`. forced INTEGER NOT NULL DEFAULT 0 CHECK (forced IN (0, 1)), sdh INTEGER NOT NULL DEFAULT 0 CHECK (sdh IN (0, 1)), -- §15. `alass` reports no confidence, so its output is accepted unless -- implausible. `synced` is an accepted shift; `sync_rejected` means the -- unsynced original was kept and this file is flagged. Never both. synced INTEGER NOT NULL DEFAULT 0 CHECK (synced IN (0, 1)), sync_rejected INTEGER NOT NULL DEFAULT 0 CHECK (sync_rejected IN (0, 1)), path TEXT UNIQUE, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), CHECK ((path IS NULL) = (origin = 'embedded')), CHECK ((provider IS NOT NULL) = (origin = 'provider')), CHECK (candidate_id IS NULL OR provider IS NOT NULL), CHECK ((engine IS NOT NULL) = (origin = 'translated')), CHECK (NOT (synced = 1 AND sync_rejected = 1)) ) STRICT; -- §15's satisfaction check is "is there a subtitle in this language", asked -- per file per tick. CREATE INDEX subtitle_files_media_file ON subtitle_files (media_file_id, language); -- An embedded track is identified by what it satisfies, not by a file, so a -- second probe of the same video re-records it rather than duplicating it. CREATE UNIQUE INDEX subtitle_files_embedded ON subtitle_files (media_file_id, language, forced, sdh) WHERE origin = 'embedded'; -- One row per (media file, wanted language). What the reconcile loop reads to -- decide whether a language is a gap, what backs a failing provider off, and -- what the missing-subtitles queue renders. -- -- `capped` is a state and not an error (§15): being at a daily allowance is -- something the queue shows. The allowance itself is #197 and not here. -- `unavailable` is the end of the line — no provider has it and there is no -- text source to translate from, so retrying costs budget for nothing. CREATE TABLE subtitle_attempts ( media_file_id INTEGER NOT NULL REFERENCES media_files (id) ON DELETE CASCADE, language TEXT NOT NULL, state TEXT NOT NULL DEFAULT 'wanted' CHECK (state IN ('wanted', 'satisfied', 'failed', 'capped', 'unavailable')), attempts INTEGER NOT NULL DEFAULT 0 CHECK (attempts >= 0), last_attempt_at TEXT, -- Why the last attempt failed, for the queue and for backoff. Cleared -- when an attempt succeeds. last_failure TEXT, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), PRIMARY KEY (media_file_id, language), CHECK ((attempts = 0) = (last_attempt_at IS NULL)) ) STRICT; -- §8, §15. The work list is every language that is not satisfied yet, least -- recently attempted first within it; the ordering across files is -- newest-import-first and comes from the join to `media_files`. CREATE INDEX subtitle_attempts_pending ON subtitle_attempts (state, last_attempt_at) WHERE state <> 'satisfied';