From 08b894c4110f7af7b9e42fbfdd8993358e610b55 Mon Sep 17 00:00:00 2001 From: mithral Date: Mon, 12 Oct 2026 11:45:14 -0400 Subject: [PATCH] Add a subtitle picker to the video player The API could serve tracks but nothing asked for them. The picker lists what the item has and attaches one at a time, removing the previous element first so switching never accumulates them. Default is Off. Auto-selecting a track would turn subtitles on for people who did not ask and cannot tell why, and the failure mode is worse than a missing feature: a track that loads over a film you did not want subtitled. Subtitles never block playback. A failed track list hides the picker, and a track that fails to load leaves the video playing - the media element's own state is never touched by any of this. Not verified by a test: the web package has no test runner, only svelte-check, so verification here is the type and a11y check (0 errors, 0 warnings) plus the API end-to-end below. Verified end-to-end against a running server: a video with "Show.en.srt" and "Show.pt-BR.srt" beside it produced two sidecar rows with the right languages, the list route returned both, and the track route served real WebVTT - header, Content-Type text/vtt, and the SRT commas converted to dots. --- web/src/lib/api.ts | 19 ++++++ web/src/lib/components/VideoPlayer.svelte | 80 +++++++++++++++++++++-- 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index bc4ca02..1d0928c 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -214,6 +214,10 @@ export const api = { fileUrlById: (id: number, fileId: number) => `/api/v1/items/${id}/files/${fileId}`, + subtitles: (id: number) => request<{ subtitles: Subtitle[] }>('GET', `/api/v1/items/${id}/subtitles`), + + subtitleUrl: (id: number, trackId: number) => `/api/v1/items/${id}/subtitles/${trackId}`, + progress: (id: number) => request<{ progress: Progress | null }>('GET', `/api/v1/items/${id}/progress`), putProgress: ( @@ -316,6 +320,21 @@ export interface ItemDetail { progress?: Progress } +// Subtitle carries the facts about one subtitle track. The server renders every +// format as WebVTT on the way out, so a client only needs the id to build the +// track URL; Path and StreamIndex stay server-side. +export interface Subtitle { + id: number + /** Where the track came from: "embedded", "sidecar" or "downloaded". */ + source: string + /** Language tag, or "" when the file did not carry one. */ + language: string + /** Human label for the picker, e.g. "English (SDH)". */ + label: string + /** Original format, e.g. "srt", "ass", "vtt". */ + format: string +} + // ── playback engines ──────────────────────────────────────────────────── // Playability is a property of the CLIENT, so a client declares the engines it // has and the server matches them against the file's codec facts. A browser diff --git a/web/src/lib/components/VideoPlayer.svelte b/web/src/lib/components/VideoPlayer.svelte index 01df8ec..59db388 100644 --- a/web/src/lib/components/VideoPlayer.svelte +++ b/web/src/lib/components/VideoPlayer.svelte @@ -1,6 +1,6 @@