Add the television schema: seasons and episodes

Vellum had no season or episode model. media_items was a flat tree and the
scanner never parsed S01E02, so a library of television was a list of files
in a folder. That is the difference between a movie server and a media
server, and it is also what blocked TV metadata: there was nothing to
attach a series' poster or overview to.

Schema only. The scanner and the API follow in separate commits, so this
one is safe on its own: the columns are additive and nullable, every
existing row is untouched, and a film keeps NULL for both numbers.

Two decisions, stated in the migration because both are cheap now and
expensive later:

- An EPISODE STAYS kind='video'. A new 'episode' kind would mean touching
  every query that says video for no gain, since the parent chain already
  tells an episode from a film. kind only gains 'series' and 'season'.
- A series and a season have no file, so their path is the directory they
  stand for. That satisfies the existing UNIQUE (library_id, path) and
  gives the row a stable identity so the scanner can upsert instead of
  duplicating a series on every scan.

The tree itself is not new: parent_id already carries albums and audiobook
chapters. This is that same shape applied to television.

Verified on both paths, because only the upgrade can hurt: a database at
version 12 upgraded cleanly to 13 with the columns and both indexes added,
and a fresh database applied 0001..0013 cleanly.
This commit is contained in:
mithral 2026-10-12 01:19:06 -04:00
parent 3207b89205
commit 8d6ea6db20

View file

@ -0,0 +1,40 @@
-- 0013_series.sql — television: series, seasons, episodes.
--
-- Vellum had no season or episode model. media_items was a flat tree and the
-- scanner never parsed S01E02, so a library of television was a list of files
-- in a folder. That is the difference between a movie server and a media
-- server, and it is also what blocks TV metadata: there is nothing to attach a
-- series' poster or overview to.
--
-- Two decisions worth stating, because both are cheap now and expensive later.
--
-- * The new columns are additive and nullable. A film keeps NULL for both
-- numbers, so every existing row is untouched and every existing query, the
-- web app and the Jellyfin surface keep working exactly as they do today.
-- Nothing is rewritten, so there is no backfill and no migration risk.
--
-- * kind gains 'series' and 'season', but an EPISODE STAYS kind='video'. A
-- new 'episode' kind would mean touching every query that says video, for
-- no gain: the parent chain already tells an episode from a film, and a
-- series row is the only genuinely new concept. Keeping video also means a
-- library that has not been rescanned yet behaves identically.
--
-- A series and a season have no file of their own, so their path is the
-- directory they stand for. That is unique per library, which is what the
-- existing UNIQUE (library_id, path) requires, and it gives the row a stable
-- identity across rescans — the scanner can upsert on it rather than
-- duplicating a series every time it runs.
--
-- The tree itself is not new. parent_id already carries albums and audiobook
-- chapters, so this is the same shape applied to television: library -> series
-- -> season -> episode.
ALTER TABLE media_items ADD COLUMN season_number INTEGER;
ALTER TABLE media_items ADD COLUMN episode_number INTEGER;
-- Listing a series' seasons and a season's episodes is the hot path, and both
-- read by parent and then order by the number.
CREATE INDEX idx_items_parent_season ON media_items(parent_id, season_number);
-- Finding every series in a library, and every season of one, without a scan.
CREATE INDEX idx_items_kind_parent ON media_items(kind, parent_id);