Vellum/migrations/0014_subtitles.sql
mithral c247539ac0 Add the subtitle schema
Vellum had no subtitle support of any kind. This is the storage half.

Three sources, and they are genuinely different shapes: a sidecar file beside
the video has a path, an embedded stream has an index into the container and
no file of its own, and a downloaded track has a path but is regenerable.

stream_index is -1 for the file-backed sources rather than NULL, and that is
deliberate: NULL is never equal to NULL in SQL, so a unique index over a
nullable column would happily accept the same sidecar twice. With -1 the
UNIQUE (item_id, source, path, stream_index) actually does its job and a
rescan can upsert.

Nothing here is authoritative about the media. Deleting every row and
rescanning reproduces it exactly, which is what lets a vanished subtitle file
have its row removed - unlike a vanished video, which is marked instead
(ADR-003) because progress hangs off it and cannot be regenerated.

Verified on both paths: a real database at version 13 upgraded to 14 by an
older binary-then-newer sequence, and a fresh database applied 0001..0014.
2026-10-12 11:33:18 -04:00

45 lines
2.4 KiB
SQL

-- 0014_subtitles.sql — subtitle tracks per item.
--
-- Vellum had no subtitle support at all. This is the storage half: a row per
-- track, whether it came from a file next to the video, a stream muxed into it,
-- or a download.
--
-- Three sources, and they are genuinely different shapes:
--
-- * 'sidecar' a file beside the video (".en.srt"). Has a path, no stream.
-- * 'embedded' a stream inside the container. Has a stream index, no path
-- of its own — it only exists inside the video file.
-- * 'downloaded' fetched from a provider, cached under the config dir. Has a
-- path, like a sidecar, but is regenerable and can be
-- deleted without losing anything.
--
-- `stream_index` is -1 for the two file-backed sources rather than NULL, so the
-- UNIQUE below can do its job: NULL is never equal to NULL in SQL, and a unique
-- index over a nullable column would happily accept the same sidecar twice.
--
-- `language` is BCP-47 where we can tell ("en", "pt-BR") and empty when we
-- cannot. It is a display and preference key, not a correctness one: a track
-- with no language still plays, it just cannot be auto-selected.
--
-- This table is a cache of what is on disk. Deleting every row and rescanning
-- must reproduce it exactly, which is why nothing here is authoritative about
-- the media itself.
CREATE TABLE subtitles (
id INTEGER PRIMARY KEY,
item_id INTEGER NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
source TEXT NOT NULL, -- 'sidecar' | 'embedded' | 'downloaded'
path TEXT NOT NULL DEFAULT '', -- relative to the library root; empty when embedded
stream_index INTEGER NOT NULL DEFAULT -1, -- ffmpeg stream index; -1 when file-backed
format TEXT NOT NULL DEFAULT '', -- 'srt' | 'ass' | 'ssa' | 'vtt' | 'sub'
language TEXT NOT NULL DEFAULT '', -- BCP-47 where known
label TEXT NOT NULL DEFAULT '', -- what the client shows; the filename or stream title
bytes INTEGER NOT NULL DEFAULT 0,
added_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
UNIQUE (item_id, source, path, stream_index)
);
-- The read is always "every track for this item", and it happens on every
-- player open, so it is the one index worth having.
CREATE INDEX idx_subtitles_item ON subtitles(item_id, source, language);