feat(db): add series, season and episode model #71
Merged
naps62-yolo
merged 9 commits from 2026-08-22 22:04:28 +01:00
issue/34-series-model into main
Labels
Clear labels
area/api
area/ci
area/compat
area/core
area/daemon
area/db
area/dl
area/indexer
area/infra
area/meta
area/parse
area/probe
area/subs
area/web
difficulty/easy
difficulty/hard
difficulty/moderate
difficulty/trivial
phase/1-skeleton
phase/2-logic
phase/3-sourcing
phase/4-movies
phase/5-ui
phase/6-tv
phase/7-people
phase/8-compat
phase/9-subtitles
type/bug
type/chore
type/feature
type/test
arr-api
CI and test harness
arr-compat
arr-core
arr-daemon
arr-db
arr-dl
arr-indexer
workspace and tooling
arr-meta
arr-parse
arr-probe
arr-subs crate: providers, extraction, translation, sync
web/
bounded, obvious approach, few files
subtle correctness or cross-cutting
multiple files, judgement, an interface to design
one file, mechanical, no design decisions
workspace, CI, config, database, empty API and SPA
parsing and policy engine, pure, no network
TMDB and Prowlarr, read-only
movies end to end
search, buckets, library views, queues
seasons, episodes, tracking, derived status
owner tags and notifications
Jellyseerr shim
build order: subtitles, DESIGN.md §15
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: yolo/arr#71
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Issue
Closes #34
Test plan
just ciReviewed
870f9d20e498573aff3fe160316020e3c75b44dd. Findings on the diff below, plus one that has no line to sit on:0003 added
movie_releasesso a cached release can be associated with a title. There is no episode counterpart, so §9.3 manual search has nowhere to record an episode/release association. Out of scope for #34 — worth its own issue rather than widening this one.@@ -184,2 +187,4 @@}#[derive(Clone, Debug, Default, Eq, PartialEq)]pub struct SeriesOverrides {SeriesOverridesis field-for-field identical toMovieOverrides, butpolicy::evaluatetakes&MovieOverrides(policy.rs:58,policy.rs:106). As written, a series cannot be evaluated against a policy without a conversion that does not exist yet, and the two structs will drift.DESIGN §5.1 describes one override mechanism, not one per aggregate. Reuse a single type — rename
MovieOverridestoTitleOverridesand drop this one.@@ -186,0 +214,4 @@}#[derive(Clone, Copy, Debug, Eq, PartialEq)]pub enum EpisodeState {EpisodeStateisMissing | Downloading | Available; the column added in this migration isCHECK (state IN ('missing', 'grabbed', 'imported')).Movies already have this split, and
0001_movies.sqlsays arr-core "owns the canonical enum this mirrors" — it does not mirror it. This PR makes it two mismatches instead of one. Pick one vocabulary here, or leave a comment saying which side is authoritative.@@ -0,0 +1,97 @@CREATE TABLE series (seriesstores nothing about upstream status, so DESIGN §4.2ended— "series finished upstream and complete" — cannot be derived from this schema. The other four statuses come from episode rows; this one needs TMDB's series status persisted.§4's sketch omits it, §4.2 requires it. Per CLAUDE.md the design document wins, so either add the column or record on #34 that
endedis unimplementable until it exists.@@ -0,0 +14,4 @@CREATE INDEX series_root ON series (root_id);CREATE TRIGGER movies_require_movie_rootTwo things about this block:
movies_require_movie_rootand its update twin change the movies invariant, in a migration named for series. Nothing in the file says why they arrive here (they do belong to this change — TV roots only start existing now — but a reader of0005_series.sqlhas no way to know that).@@ -0,0 +74,4 @@CREATE INDEX episodes_state ON episodes (state);INSERT INTO policies (The TV policies are seeded by copying rows matched on
name = 'Movies — main'/'Movies — kids', and the roots below are then matched onname = 'TV — main'/'TV — kids'.If a later migration renames or removes the 0002 seed rows, both inserts match zero rows and this migration succeeds having created no TV policies and no TV roots — a silent seed failure on any database migrating through it. Spell the policy values out as 0002 does, or guard with
RAISE(ABORT, ...)when the source row is absent.@@ -0,0 +76,4 @@INSERT INTO policies (name, required_audio, dub_blacklist, hdr_rules, size_bands,resolution_pref, source_weights, score_weightsThe TV policies inherit
source_weightsfrom the movie policies:{"Remux":4,"BluRay":3,"WEB-DL":2,...}for main, BluRay-biased.TV releases are overwhelmingly WEB-DL, so the tiebreaker points the wrong way out of the box. 0002 set these per root deliberately (the kids policy biases WEB-DL for pt-PT dubs); copying them wholesale drops that reasoning.
@@ -250,0 +302,4 @@.fetch_one(db.pool()).await.expect("movie schema remains available");assert_eq!(movie_count, 0);Counting rows in an untouched
moviestable asserts nothing — it is 0 whether or not this migration broke the movie schema. Either drop it or assert something the migration could plausibly break, e.g. that a movie insert against a movie root still succeeds.Kept the main TV source order because DESIGN §5.5 explicitly defines
Remux > BluRay > WEB-DLas the source tiebreaker. The TV kids policy still uses the §5.2 WEB-DL bias for pt-PT dubbing.Addressed the review in
b4c6c87: shared title overrides, canonical episode states, persisted upstream completion, explicit TV policy seeds, migration rationale, and a real movie-schema compatibility assertion.Kept the main TV source order because DESIGN §5.5 explicitly defines
Remux > BluRay > WEB-DL; the TV kids policy retains the §5.2 WEB-DL bias for pt-PT dubbing.Reviewed
b4c6c872bbae061a1eb61591044adc1da556b14e. Previous findings addressed — explicit policy seeds,upstream_ended,TitleOverrides, migration comments, and a test that actually writes a movie row. Three follow-ups on the new commit, all on the diff below.@@ -186,0 +211,4 @@}#[derive(Clone, Copy, Debug, Eq, PartialEq)]pub enum EpisodeState {MovieStateandEpisodeStateare now the same two-line enum twice over, which is the duplicationTitleOverridesjust removed one instance of. Worth the same treatment, or a comment saying the two are expected to diverge.Related:
pub type MovieOverrides = TitleOverrideskeeps the old name alive for five in-tree call sites (policy.rs:58,policy.rs:106,search.rs:6,search.rs:296,search.rs:463). Nothing outside this workspace consumes it, so the alias can go with a rename in the same pass.@@ -0,0 +67,4 @@wanted INTEGER NOT NULL DEFAULT 0 CHECK (wanted IN (0, 1)),-- Mirrors arr-core's canonical EpisodeState vocabulary.state TEXT NOT NULL DEFAULT 'missing'CHECK (state IN ('missing', 'downloading', 'available')),Episodes now mirror
EpisodeState, butmovies.statein0001_movies.sqlis stillCHECK (state IN ('missing', 'grabbed', 'imported')). One database, two vocabularies for the same concept, and 0001's comment — "arr-core (#7) owns the canonical enum this mirrors" — is now false for the table it sits on.Either bring
movies.stateacross in this migration (it is a rewrite of the CHECK plus an UPDATE mapping grabbed→downloading, imported→available), or open an issue and say here which table is the odd one out. Leaving it silent means the next reader has to diff two migrations to find out.@@ -0,0 +91,4 @@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"]'),json('{"Remux":4,"BluRay":3,"WEB-DL":2,"WEBRip":1,"HDTV":0}'),TV — kidspicked up the WEB-DL bias and a comment, butTV — mainstill carries the movie weights verbatim:Remux 4, BluRay 3, WEB-DL 2.Same point as before — TV releases are mostly WEB-DL, so on the main root the tiebreaker still leans towards the rarer source. If that is deliberate (you want the remux when one exists), a one-line comment saying so closes it.
Addressed the follow-up in
f7a6295: movies and episodes now shareMediaState, all in-tree callers useTitleOverridesdirectly, and the main TV policy documents its DESIGN §5.5 source ordering.The legacy movie database vocabulary requires rewriting its CHECK constraint, which #34 explicitly excludes; filed #73 for that additive migration and documented the exception here.
Reviewed
f7a62958fd9f4618e1bf7975da4647970ef24785. No findings.All three follow-ups closed:
MediaStatereplaces the duplicated enums and theMovieOverridesalias is gone with every call site renamed;movies.stateis tracked in #73 with the divergence noted at the column; and theTV — mainsource weights are right — DESIGN §5.5 fixes the tier asRemux > BluRay > WEB-DLas a small tiebreaker, so my earlier point about WEB-DL was wrong.Reviewed
c911cebceffa809440be2e0a795054d60d6e8b49. No findings.Clean merge of
origin/main(#70, #55). It touches nothing underarr-coreorarr-db, and theTitleOverrides/MediaStaterename holds — no stale references in the merged tree.