151135b545
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.
82 lines
4.4 KiB
SQL
82 lines
4.4 KiB
SQL
-- #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';
|