feat(db): add series, season and episode model (#71)
ci / rust (push) Successful in 1m36s
ci / web (push) Successful in 34s
e2e / e2e (push) Successful in 1m41s

This commit was merged in pull request #71.
This commit is contained in:
2026-08-22 22:04:27 +01:00
parent e5f4e44925
commit ac6ed8b798
6 changed files with 322 additions and 45 deletions
+128 -8
View File
@@ -156,13 +156,13 @@ mod tests {
let roots = db.list_roots().await.expect("list roots");
assert_eq!(roots.len(), 2, "§5.1: two movie roots, TV comes with #34");
assert!(roots.iter().all(|r| r.kind == "movie"));
assert_eq!(roots[0].audience, "kids");
assert_eq!(roots[0].path, "/mnt/media/movies/kids");
assert_eq!(roots[1].audience, "main");
assert_eq!(roots[1].path, "/mnt/media/movies/main");
assert_ne!(roots[0].policy_id, roots[1].policy_id);
let movie_roots: Vec<_> = roots.iter().filter(|root| root.kind == "movie").collect();
assert_eq!(movie_roots.len(), 2);
assert_eq!(movie_roots[0].audience, "kids");
assert_eq!(movie_roots[0].path, "/mnt/media/movies/kids");
assert_eq!(movie_roots[1].audience, "main");
assert_eq!(movie_roots[1].path, "/mnt/media/movies/main");
assert_ne!(movie_roots[0].policy_id, movie_roots[1].policy_id);
}
#[tokio::test]
@@ -200,7 +200,7 @@ mod tests {
.await
.expect("policies");
assert_eq!(rows.len(), 2);
assert_eq!(rows.len(), 4);
for row in rows {
let size_bands: String = row.get(0);
let score_weights: String = row.get(1);
@@ -247,4 +247,124 @@ mod tests {
.await
.expect_err("state is a closed set");
}
#[tokio::test]
async fn seeds_two_tv_roots_with_distinct_policies() {
let (_dir, db) = fresh().await;
let roots = db.list_roots().await.expect("list roots");
let tv_roots: Vec<_> = roots.iter().filter(|root| root.kind == "tv").collect();
assert_eq!(tv_roots.len(), 2);
assert_eq!(tv_roots[0].audience, "kids");
assert_eq!(tv_roots[0].path, "/mnt/media/tv/kids");
assert_eq!(tv_roots[1].audience, "main");
assert_eq!(tv_roots[1].path, "/mnt/media/tv/main");
assert_ne!(tv_roots[0].policy_id, tv_roots[1].policy_id);
}
#[tokio::test]
async fn episode_wanted_is_independent_of_tracking_rules() {
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, auto_track) VALUES (82728, 'Bluey', ?, 1)")
.bind(root_id)
.execute(db.pool())
.await
.expect("series")
.last_insert_rowid();
let season_id =
sqlx::query("INSERT INTO seasons (series_id, number, tracked) VALUES (?, 1, 1)")
.bind(series_id)
.execute(db.pool())
.await
.expect("season")
.last_insert_rowid();
sqlx::query("INSERT INTO episodes (season_id, number, title) VALUES (?, 2, 'Hospital')")
.bind(season_id)
.execute(db.pool())
.await
.expect("episode");
let wanted: bool = sqlx::query_scalar("SELECT wanted FROM episodes")
.fetch_one(db.pool())
.await
.expect("wanted");
assert!(
!wanted,
"auto_track and tracked are rules, not episode intent"
);
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("existing movie schema remains writable");
}
#[tokio::test]
async fn aggregates_require_roots_of_their_own_kind() {
let (_dir, db) = fresh().await;
let movie_root: i64 =
sqlx::query_scalar("SELECT id FROM roots WHERE kind = 'movie' LIMIT 1")
.fetch_one(db.pool())
.await
.expect("movie root");
let tv_root: i64 = sqlx::query_scalar("SELECT id FROM roots WHERE kind = 'tv' LIMIT 1")
.fetch_one(db.pool())
.await
.expect("TV root");
sqlx::query("INSERT INTO movies (tmdb_id, title, root_id) VALUES (1, 'x', ?)")
.bind(tv_root)
.execute(db.pool())
.await
.expect_err("movie cannot use TV root");
sqlx::query("INSERT INTO series (tmdb_id, title, root_id) VALUES (2, 'x', ?)")
.bind(movie_root)
.execute(db.pool())
.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 MediaState");
sqlx::query("UPDATE series SET upstream_ended = 2 WHERE id = ?")
.bind(series_id)
.execute(db.pool())
.await
.expect_err("upstream completion is boolean");
}
}