diff --git a/.sqlx/query-29d6fdda533e0552f5da24a8e5180e5935b3550ee2c024e09aef9b76fd47a708.json b/.sqlx/query-29d6fdda533e0552f5da24a8e5180e5935b3550ee2c024e09aef9b76fd47a708.json new file mode 100644 index 0000000..92132e9 --- /dev/null +++ b/.sqlx/query-29d6fdda533e0552f5da24a8e5180e5935b3550ee2c024e09aef9b76fd47a708.json @@ -0,0 +1,56 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT s.id AS \"series_id!: i64\", s.tmdb_id AS \"tmdb_id!: i64\",\n s.title AS \"title!: String\", s.year,\n e.id AS \"episode_id!: i64\",\n se.number AS \"season_number!: i64\", e.number AS \"episode_number!: i64\"\n FROM episodes e\n JOIN seasons se ON se.id = e.season_id\n JOIN series s ON s.id = se.series_id\n JOIN roots root ON root.id = s.root_id\n WHERE root.audience = 'kids'\n AND s.blocked = 0\n AND e.wanted = 1 AND e.state = 'missing' AND e.search_attempts > 0\n AND NOT EXISTS (\n SELECT 1 FROM episode_releases er\n JOIN releases r ON r.id = er.release_id\n WHERE er.episode_id = e.id AND r.verdict IN ('eligible', 'waived')\n )\n ORDER BY se.number, e.number\n ", + "describe": { + "columns": [ + { + "name": "series_id!: i64", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "tmdb_id!: i64", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "title!: String", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "year", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "episode_id!: i64", + "ordinal": 4, + "type_info": "Integer" + }, + { + "name": "season_number!: i64", + "ordinal": 5, + "type_info": "Integer" + }, + { + "name": "episode_number!: i64", + "ordinal": 6, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + true, + true, + false, + false + ] + }, + "hash": "29d6fdda533e0552f5da24a8e5180e5935b3550ee2c024e09aef9b76fd47a708" +} diff --git a/.sqlx/query-cd09d4c34f0596319fa2ade088ea9009fa24e453f669bcc2357fdcd999b06193.json b/.sqlx/query-7930e2d10b25627dcbf81f60a5ac077c27b647a6f0b411e13105398a2963cd51.json similarity index 59% rename from .sqlx/query-cd09d4c34f0596319fa2ade088ea9009fa24e453f669bcc2357fdcd999b06193.json rename to .sqlx/query-7930e2d10b25627dcbf81f60a5ac077c27b647a6f0b411e13105398a2963cd51.json index 2dba66a..bdd7c6c 100644 --- a/.sqlx/query-cd09d4c34f0596319fa2ade088ea9009fa24e453f669bcc2357fdcd999b06193.json +++ b/.sqlx/query-7930e2d10b25627dcbf81f60a5ac077c27b647a6f0b411e13105398a2963cd51.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT s.id AS \"series_id!: i64\", s.tmdb_id AS \"tmdb_id!: i64\",\n s.title AS \"title!: String\", s.year, g.target_id AS \"season_id!: i64\"\n FROM grabs g\n JOIN seasons se ON g.target_kind = 'season' AND se.id = g.target_id\n JOIN series s ON s.id = se.series_id\n WHERE g.state = 'failed'\n GROUP BY s.id, s.tmdb_id, s.title, s.year, se.id\n ", + "query": "\n SELECT s.id AS \"series_id!: i64\", s.tmdb_id AS \"tmdb_id!: i64\",\n s.title AS \"title!: String\", s.year,\n g.target_id AS \"season_id!: i64\", se.number AS \"season_number!: i64\"\n FROM grabs g\n JOIN seasons se ON g.target_kind = 'season' AND se.id = g.target_id\n JOIN series s ON s.id = se.series_id\n WHERE g.state = 'failed'\n GROUP BY s.id, s.tmdb_id, s.title, s.year, se.id, se.number\n ", "describe": { "columns": [ { @@ -27,6 +27,11 @@ "name": "season_id!: i64", "ordinal": 4, "type_info": "Integer" + }, + { + "name": "season_number!: i64", + "ordinal": 5, + "type_info": "Integer" } ], "parameters": { @@ -37,8 +42,9 @@ false, false, true, + false, false ] }, - "hash": "cd09d4c34f0596319fa2ade088ea9009fa24e453f669bcc2357fdcd999b06193" + "hash": "7930e2d10b25627dcbf81f60a5ac077c27b647a6f0b411e13105398a2963cd51" } diff --git a/.sqlx/query-a36a611d7c587f1c1880113e334861867891d55dc8bb7fe79b1dc42e36054379.json b/.sqlx/query-a36a611d7c587f1c1880113e334861867891d55dc8bb7fe79b1dc42e36054379.json deleted file mode 100644 index 37c4946..0000000 --- a/.sqlx/query-a36a611d7c587f1c1880113e334861867891d55dc8bb7fe79b1dc42e36054379.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT s.id AS \"series_id!: i64\", s.tmdb_id AS \"tmdb_id!: i64\",\n s.title AS \"title!: String\", s.year, e.id AS \"episode_id!: i64\"\n FROM episodes e\n JOIN seasons se ON se.id = e.season_id\n JOIN series s ON s.id = se.series_id\n JOIN roots root ON root.id = s.root_id\n WHERE root.audience = 'kids'\n AND s.blocked = 0\n AND e.wanted = 1 AND e.state = 'missing' AND e.search_attempts > 0\n AND NOT EXISTS (\n SELECT 1 FROM episode_releases er\n JOIN releases r ON r.id = er.release_id\n WHERE er.episode_id = e.id AND r.verdict IN ('eligible', 'waived')\n )\n ORDER BY se.number, e.number\n ", - "describe": { - "columns": [ - { - "name": "series_id!: i64", - "ordinal": 0, - "type_info": "Integer" - }, - { - "name": "tmdb_id!: i64", - "ordinal": 1, - "type_info": "Integer" - }, - { - "name": "title!: String", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "year", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "episode_id!: i64", - "ordinal": 4, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false, - false, - false, - true, - true - ] - }, - "hash": "a36a611d7c587f1c1880113e334861867891d55dc8bb7fe79b1dc42e36054379" -} diff --git a/.sqlx/query-aaafc2e7577fad8be202f0d27e16e5f88ffa4644999dee42af3e387dd2cf8702.json b/.sqlx/query-aaafc2e7577fad8be202f0d27e16e5f88ffa4644999dee42af3e387dd2cf8702.json new file mode 100644 index 0000000..b730f17 --- /dev/null +++ b/.sqlx/query-aaafc2e7577fad8be202f0d27e16e5f88ffa4644999dee42af3e387dd2cf8702.json @@ -0,0 +1,56 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT s.id AS \"series_id!: i64\", s.tmdb_id AS \"tmdb_id!: i64\",\n s.title AS \"title!: String\", s.year,\n g.target_id AS \"episode_id!: i64\",\n se.number AS \"season_number!: i64\", e.number AS \"episode_number!: i64\"\n FROM grabs g\n JOIN episodes e ON g.target_kind = 'episode' AND e.id = g.target_id\n JOIN seasons se ON se.id = e.season_id\n JOIN series s ON s.id = se.series_id\n WHERE g.state = 'failed'\n GROUP BY s.id, s.tmdb_id, s.title, s.year, e.id, se.number, e.number\n HAVING count(DISTINCT g.release_id) >= 2\n ", + "describe": { + "columns": [ + { + "name": "series_id!: i64", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "tmdb_id!: i64", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "title!: String", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "year", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "episode_id!: i64", + "ordinal": 4, + "type_info": "Integer" + }, + { + "name": "season_number!: i64", + "ordinal": 5, + "type_info": "Integer" + }, + { + "name": "episode_number!: i64", + "ordinal": 6, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false + ] + }, + "hash": "aaafc2e7577fad8be202f0d27e16e5f88ffa4644999dee42af3e387dd2cf8702" +} diff --git a/.sqlx/query-b4b58a625ce7b57ba423d39296cb310c6b90c3feb283885b3573ee7666b1725b.json b/.sqlx/query-b4b58a625ce7b57ba423d39296cb310c6b90c3feb283885b3573ee7666b1725b.json deleted file mode 100644 index 654e1a8..0000000 --- a/.sqlx/query-b4b58a625ce7b57ba423d39296cb310c6b90c3feb283885b3573ee7666b1725b.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT s.id AS \"series_id!: i64\", s.tmdb_id AS \"tmdb_id!: i64\",\n s.title AS \"title!: String\", s.year, g.target_id AS \"episode_id!: i64\"\n FROM grabs g\n JOIN episodes e ON g.target_kind = 'episode' AND e.id = g.target_id\n JOIN seasons se ON se.id = e.season_id\n JOIN series s ON s.id = se.series_id\n WHERE g.state = 'failed'\n GROUP BY s.id, s.tmdb_id, s.title, s.year, e.id\n HAVING count(DISTINCT g.release_id) >= 2\n ", - "describe": { - "columns": [ - { - "name": "series_id!: i64", - "ordinal": 0, - "type_info": "Integer" - }, - { - "name": "tmdb_id!: i64", - "ordinal": 1, - "type_info": "Integer" - }, - { - "name": "title!: String", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "year", - "ordinal": 3, - "type_info": "Integer" - }, - { - "name": "episode_id!: i64", - "ordinal": 4, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false, - false, - false, - true, - false - ] - }, - "hash": "b4b58a625ce7b57ba423d39296cb310c6b90c3feb283885b3573ee7666b1725b" -} diff --git a/crates/arr-api/src/movies.rs b/crates/arr-api/src/movies.rs index 599b030..d4dc02e 100644 --- a/crates/arr-api/src/movies.rs +++ b/crates/arr-api/src/movies.rs @@ -100,6 +100,22 @@ pub struct AttentionQueues { pub tv_needs_decision: Vec, } +/// An episode that put its series in a TV attention queue, named for the +/// operator (`SxxEyy` comes from season and episode numbers, #140). +#[derive(Debug, Clone, Serialize, ToSchema)] +pub struct QueuedEpisode { + pub id: i64, + pub season_number: i64, + pub episode_number: i64, +} + +/// A season whose failed pack put its series in a TV attention queue. +#[derive(Debug, Clone, Serialize, ToSchema)] +pub struct QueuedSeason { + pub id: i64, + pub number: i64, +} + /// A series in a TV attention queue, with what put it there. One row per /// series — the notification rolls up the same way (§9.5). #[derive(Debug, Clone, Serialize, ToSchema)] @@ -109,9 +125,9 @@ pub struct SeriesAttention { pub title: String, pub year: Option, /// Episodes that put the series in this queue. - pub episodes: Vec, + pub episodes: Vec, /// Seasons whose pack grab failed and fell back to per-episode grabbing. - pub seasons: Vec, + pub seasons: Vec, } #[derive(Debug, Clone, Serialize, ToSchema)] @@ -667,7 +683,9 @@ async fn tv_attention( let no_pt_rows = sqlx::query!( r#" SELECT s.id AS "series_id!: i64", s.tmdb_id AS "tmdb_id!: i64", - s.title AS "title!: String", s.year, e.id AS "episode_id!: i64" + s.title AS "title!: String", s.year, + e.id AS "episode_id!: i64", + se.number AS "season_number!: i64", e.number AS "episode_number!: i64" FROM episodes e JOIN seasons se ON se.id = e.season_id JOIN series s ON s.id = se.series_id @@ -688,13 +706,15 @@ async fn tv_attention( let episode_hard_fails = sqlx::query!( r#" SELECT s.id AS "series_id!: i64", s.tmdb_id AS "tmdb_id!: i64", - s.title AS "title!: String", s.year, g.target_id AS "episode_id!: i64" + s.title AS "title!: String", s.year, + g.target_id AS "episode_id!: i64", + se.number AS "season_number!: i64", e.number AS "episode_number!: i64" FROM grabs g JOIN episodes e ON g.target_kind = 'episode' AND e.id = g.target_id JOIN seasons se ON se.id = e.season_id JOIN series s ON s.id = se.series_id WHERE g.state = 'failed' - GROUP BY s.id, s.tmdb_id, s.title, s.year, e.id + GROUP BY s.id, s.tmdb_id, s.title, s.year, e.id, se.number, e.number HAVING count(DISTINCT g.release_id) >= 2 "# ) @@ -703,12 +723,13 @@ async fn tv_attention( let season_pack_fails = sqlx::query!( r#" SELECT s.id AS "series_id!: i64", s.tmdb_id AS "tmdb_id!: i64", - s.title AS "title!: String", s.year, g.target_id AS "season_id!: i64" + s.title AS "title!: String", s.year, + g.target_id AS "season_id!: i64", se.number AS "season_number!: i64" FROM grabs g JOIN seasons se ON g.target_kind = 'season' AND se.id = g.target_id JOIN series s ON s.id = se.series_id WHERE g.state = 'failed' - GROUP BY s.id, s.tmdb_id, s.title, s.year, se.id + GROUP BY s.id, s.tmdb_id, s.title, s.year, se.id, se.number "# ) .fetch_all(database) @@ -722,7 +743,11 @@ async fn tv_attention( row.tmdb_id, row.title, row.year, - row.episode_id, + QueuedEpisode { + id: row.episode_id, + season_number: row.season_number, + episode_number: row.episode_number, + }, ); } @@ -734,22 +759,30 @@ async fn tv_attention( row.tmdb_id, row.title, row.year, - row.episode_id, + QueuedEpisode { + id: row.episode_id, + season_number: row.season_number, + episode_number: row.episode_number, + }, ); } for row in season_pack_fails { + let season = QueuedSeason { + id: row.season_id, + number: row.season_number, + }; match tv_needs_decision .iter_mut() .find(|entry| entry.series_id == row.series_id) { - Some(entry) => entry.seasons.push(row.season_id), + Some(entry) => entry.seasons.push(season), None => tv_needs_decision.push(SeriesAttention { series_id: row.series_id, tmdb_id: row.tmdb_id, title: row.title, year: row.year, episodes: Vec::new(), - seasons: vec![row.season_id], + seasons: vec![season], }), } } @@ -765,13 +798,13 @@ fn merge_episode( tmdb_id: i64, title: String, year: Option, - episode_id: i64, + episode: QueuedEpisode, ) { if let Some(entry) = entries .iter_mut() .find(|entry| entry.series_id == series_id) { - entry.episodes.push(episode_id); + entry.episodes.push(episode); return; } entries.push(SeriesAttention { @@ -779,7 +812,7 @@ fn merge_episode( tmdb_id, title, year, - episodes: vec![episode_id], + episodes: vec![episode], seasons: Vec::new(), }); } @@ -1350,21 +1383,32 @@ mod tests { .json() .await .expect("queues json"); + let episode_ids: Vec = sqlx::query_scalar( + "SELECT id FROM episodes WHERE season_id = ? ORDER BY number", + ) + .bind(season_id) + .fetch_all(pool) + .await + .expect("episode ids"); + assert_eq!(queues["tv_no_pt_source"][0]["series_id"], series_id); assert_eq!( - queues["tv_no_pt_source"][0]["episodes"] - .as_array() - .map(Vec::len), - Some(2) + queues["tv_no_pt_source"][0]["episodes"], + serde_json::json!([ + { "id": episode_ids[0], "season_number": 1, "episode_number": 1 }, + { "id": episode_ids[1], "season_number": 1, "episode_number": 2 }, + ]) ); assert_eq!(queues["tv_needs_decision"][0]["series_id"], series_id); assert_eq!( queues["tv_needs_decision"][0]["episodes"], - serde_json::json!([episode_id]) + serde_json::json!([ + { "id": episode_id, "season_number": 1, "episode_number": 3 }, + ]) ); assert_eq!( queues["tv_needs_decision"][0]["seasons"], - serde_json::json!([season_id]) + serde_json::json!([{ "id": season_id, "number": 1 }]) ); // Blocking the series empties its no-PT-source lane.