-- TV side of the separate aggregates in DESIGN.md §4. This migration is -- additive: the movie tables stay concrete and unchanged. CREATE TABLE series ( id INTEGER PRIMARY KEY, tmdb_id INTEGER NOT NULL UNIQUE, title TEXT NOT NULL, year INTEGER, original_language TEXT, root_id INTEGER NOT NULL REFERENCES roots (id), auto_track INTEGER NOT NULL DEFAULT 0 CHECK (auto_track IN (0, 1)), overrides TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(overrides)), -- §4.2 needs upstream completion to derive `ended` rather than store it. upstream_ended INTEGER NOT NULL DEFAULT 0 CHECK (upstream_ended IN (0, 1)), blocked INTEGER NOT NULL DEFAULT 0 CHECK (blocked IN (0, 1)), 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')) ) STRICT; CREATE INDEX series_root ON series (root_id); -- Before TV roots existed, the roots foreign key was sufficient to keep a -- movie on a movie root. Preserve that invariant now that both kinds exist. CREATE TRIGGER movies_require_movie_root BEFORE INSERT ON movies WHEN EXISTS (SELECT 1 FROM roots WHERE id = NEW.root_id AND kind != 'movie') BEGIN SELECT RAISE(ABORT, 'movies require a movie root'); END; CREATE TRIGGER movies_require_movie_root_on_update BEFORE UPDATE OF root_id ON movies WHEN EXISTS (SELECT 1 FROM roots WHERE id = NEW.root_id AND kind != 'movie') BEGIN SELECT RAISE(ABORT, 'movies require a movie root'); END; CREATE TRIGGER series_require_tv_root BEFORE INSERT ON series WHEN EXISTS (SELECT 1 FROM roots WHERE id = NEW.root_id AND kind != 'tv') BEGIN SELECT RAISE(ABORT, 'series require a TV root'); END; CREATE TRIGGER series_require_tv_root_on_update BEFORE UPDATE OF root_id ON series WHEN EXISTS (SELECT 1 FROM roots WHERE id = NEW.root_id AND kind != 'tv') BEGIN SELECT RAISE(ABORT, 'series require a TV root'); END; CREATE TABLE seasons ( id INTEGER PRIMARY KEY, series_id INTEGER NOT NULL REFERENCES series (id) ON DELETE CASCADE, number INTEGER NOT NULL CHECK (number >= 0), tracked INTEGER NOT NULL DEFAULT 0 CHECK (tracked IN (0, 1)), 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')), UNIQUE (series_id, number) ) STRICT; CREATE TABLE episodes ( id INTEGER PRIMARY KEY, season_id INTEGER NOT NULL REFERENCES seasons (id) ON DELETE CASCADE, number INTEGER NOT NULL CHECK (number >= 0), title TEXT NOT NULL, air_date TEXT, wanted INTEGER NOT NULL DEFAULT 0 CHECK (wanted IN (0, 1)), -- Mirrors arr-core's canonical MediaState vocabulary. The older movies -- table uses legacy names; its additive correction is tracked in #73. state TEXT NOT NULL DEFAULT 'missing' CHECK (state IN ('missing', 'downloading', 'available')), search_attempts INTEGER NOT NULL DEFAULT 0, last_searched_at 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')), UNIQUE (season_id, number) ) STRICT; CREATE INDEX episodes_pending_search ON episodes (last_searched_at) WHERE wanted = 1 AND state = 'missing'; CREATE INDEX episodes_state ON episodes (state); INSERT INTO policies ( name, required_audio, dub_blacklist, hdr_rules, size_bands, resolution_pref, source_weights, score_weights ) VALUES ( 'TV — main', json('{"require":"original_language","allow_english_fallback":false}'), json('["pt-BR"]'), json('{"dv_profile_allow":["8.1"],"dv_profile_reject":["5","7"],"allow_hdr10":true,"allow_hdr10plus":true,"allow_sdr":true}'), json('{"2160p":{"floor_gib":8,"target_gib":22,"penalty_points_per_gib_over":60},"1080p":{"floor_gib":3,"target_gib":8,"penalty_points_per_gib_over":60}}'), json('["2160p","1080p"]'), -- §5.5 deliberately keeps source tier as Remux > BluRay > WEB-DL. json('{"Remux":4,"BluRay":3,"WEB-DL":2,"WEBRip":1,"HDTV":0}'), json('{"size_at_target":1000,"source_tier":25,"seeder_doubling":8}') ); INSERT INTO policies ( name, required_audio, dub_blacklist, hdr_rules, size_bands, resolution_pref, source_weights, score_weights ) VALUES ( 'TV — kids', json('{"require":"any_of","langs":["pt-PT"],"or_original_when_portuguese":true,"allow_english_fallback":false}'), json('["pt-BR"]'), json('{"dv_profile_allow":["8.1"],"dv_profile_reject":["5","7"],"allow_hdr10":true,"allow_hdr10plus":true,"allow_sdr":true}'), json('{"2160p":{"floor_gib":8,"target_gib":22,"penalty_points_per_gib_over":60},"1080p":{"floor_gib":3,"target_gib":8,"penalty_points_per_gib_over":60}}'), json('["2160p","1080p"]'), -- §5.2: pt-PT dubs are concentrated in streaming releases. json('{"WEB-DL":4,"WEBRip":2,"BluRay":1,"Remux":1,"HDTV":0}'), json('{"size_at_target":1000,"source_tier":25,"seeder_doubling":8}') ); INSERT INTO roots (kind, audience, path, policy_id) SELECT 'tv', 'main', '/mnt/media/tv/main', id FROM policies WHERE name = 'TV — main'; INSERT INTO roots (kind, audience, path, policy_id) SELECT 'tv', 'kids', '/mnt/media/tv/kids', id FROM policies WHERE name = 'TV — kids';