feat(arr): add subtitle tables
DESIGN.md §15 needs two shapes: what subtitles exist for a media file, and what arr has tried per wanted language. An embedded track carries no path — it is recorded because it satisfies a language, not because there is a file — and the CHECK constraints tie provider, engine and path to the origin so an impossible row cannot be written.
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
-- #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';
|
||||
Reference in New Issue
Block a user