fix(db): address TV model review
ci / rust (pull_request) Successful in 1m49s
ci / web (pull_request) Successful in 34s

This commit is contained in:
Miguel Palhas
2026-08-22 21:41:35 +01:00
parent 870f9d20e4
commit b4c6c872bb
3 changed files with 76 additions and 21 deletions
+5 -8
View File
@@ -158,11 +158,13 @@ pub struct Policy {
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct MovieOverrides {
pub struct TitleOverrides {
pub only_4k: bool,
pub allow_english_audio: bool,
}
pub type MovieOverrides = TitleOverrides;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MovieState {
Missing,
@@ -186,12 +188,6 @@ pub struct Movie {
pub last_searched_at: Option<SystemTime>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct SeriesOverrides {
pub only_4k: bool,
pub allow_english_audio: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Series {
pub id: SeriesId,
@@ -201,7 +197,8 @@ pub struct Series {
pub original_language: Language,
pub root_id: RootId,
pub auto_track: bool,
pub overrides: SeriesOverrides,
pub overrides: TitleOverrides,
pub upstream_ended: bool,
pub blocked: bool,
}
+29 -9
View File
@@ -1,3 +1,5 @@
-- 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,
@@ -7,6 +9,8 @@ CREATE TABLE series (
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'))
@@ -14,6 +18,8 @@ CREATE TABLE series (
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')
@@ -59,8 +65,9 @@ CREATE TABLE episodes (
title TEXT NOT NULL,
air_date TEXT,
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', 'grabbed', 'imported')),
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')),
@@ -77,18 +84,31 @@ 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
) SELECT
'TV — main', required_audio, dub_blacklist, hdr_rules, size_bands,
resolution_pref, source_weights, score_weights
FROM policies WHERE name = 'Movies — main';
) 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"]'),
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
) SELECT
'TV — kids', required_audio, dub_blacklist, hdr_rules, size_bands,
resolution_pref, source_weights, score_weights
FROM policies WHERE name = 'Movies — kids';
) 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';
+42 -4
View File
@@ -298,11 +298,10 @@ mod tests {
"auto_track and tracked are rules, not episode intent"
);
let movie_count: i64 = sqlx::query_scalar("SELECT count(*) FROM movies")
.fetch_one(db.pool())
sqlx::query("INSERT INTO movies (tmdb_id, title, root_id) SELECT 693134, 'Dune Part Two', id FROM roots WHERE kind = 'movie' AND audience = 'main'")
.execute(db.pool())
.await
.expect("movie schema remains available");
assert_eq!(movie_count, 0);
.expect("existing movie schema remains writable");
}
#[tokio::test]
@@ -329,4 +328,43 @@ mod tests {
.await
.expect_err("series cannot use movie root");
}
#[tokio::test]
async fn episode_state_and_upstream_completion_are_constrained() {
let (_dir, db) = fresh().await;
let root_id: i64 =
sqlx::query_scalar("SELECT id FROM roots WHERE kind = 'tv' AND audience = 'main'")
.fetch_one(db.pool())
.await
.expect("TV root");
let series_id = sqlx::query(
"INSERT INTO series (tmdb_id, title, root_id, upstream_ended)
VALUES (82728, 'Bluey', ?, 1)",
)
.bind(root_id)
.execute(db.pool())
.await
.expect("ended series")
.last_insert_rowid();
let season_id = sqlx::query("INSERT INTO seasons (series_id, number) VALUES (?, 1)")
.bind(series_id)
.execute(db.pool())
.await
.expect("season")
.last_insert_rowid();
sqlx::query(
"INSERT INTO episodes (season_id, number, title, state)
VALUES (?, 1, 'Magic Xylophone', 'grabbed')",
)
.bind(season_id)
.execute(db.pool())
.await
.expect_err("database mirrors EpisodeState");
sqlx::query("UPDATE series SET upstream_ended = 2 WHERE id = ?")
.bind(series_id)
.execute(db.pool())
.await
.expect_err("upstream completion is boolean");
}
}