From c247539ac0bc3a50b41929a5b964b766f0ae9a3f Mon Sep 17 00:00:00 2001 From: mithral Date: Mon, 12 Oct 2026 11:33:18 -0400 Subject: [PATCH] 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. --- migrations/0014_subtitles.sql | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 migrations/0014_subtitles.sql diff --git a/migrations/0014_subtitles.sql b/migrations/0014_subtitles.sql new file mode 100644 index 0000000..6af68a3 --- /dev/null +++ b/migrations/0014_subtitles.sql @@ -0,0 +1,45 @@ +-- 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);