diff --git a/.sqlx/query-7fc4e02be63e223a60b1bf7d78f68aad4106abc0e17f7da6b98efd818ecdc372.json b/.sqlx/query-3ab4e736a4fc73625fd66e38dc3f510193b9c97788503f06c5c23f859780349a.json similarity index 75% rename from .sqlx/query-7fc4e02be63e223a60b1bf7d78f68aad4106abc0e17f7da6b98efd818ecdc372.json rename to .sqlx/query-3ab4e736a4fc73625fd66e38dc3f510193b9c97788503f06c5c23f859780349a.json index cc1bb05..68d60f0 100644 --- a/.sqlx/query-7fc4e02be63e223a60b1bf7d78f68aad4106abc0e17f7da6b98efd818ecdc372.json +++ b/.sqlx/query-3ab4e736a4fc73625fd66e38dc3f510193b9c97788503f06c5c23f859780349a.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT se.series_id AS \"series_id!: i64\", e.id AS \"id!: i64\", e.season_id AS \"season_id!: i64\", e.number AS \"number!: i64\", e.title AS \"title!: String\", e.air_date, e.wanted AS \"wanted!: bool\", e.state AS \"state!: String\", e.search_attempts AS \"search_attempts!: i64\", e.last_searched_at\n FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE se.series_id = ?", + "query": "SELECT se.series_id AS \"series_id!: i64\", e.id AS \"id!: i64\", e.season_id AS \"season_id!: i64\", e.number AS \"number!: i64\", e.title AS \"title!: String\", e.air_date, e.wanted AS \"wanted!: bool\", e.state AS \"state!: String\", e.vanished AS \"vanished!: bool\", e.search_attempts AS \"search_attempts!: i64\", e.last_searched_at\n FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE se.series_id = ? ORDER BY se.number, e.number", "describe": { "columns": [ { @@ -44,13 +44,18 @@ "type_info": "Text" }, { - "name": "search_attempts!: i64", + "name": "vanished!: bool", "ordinal": 8, "type_info": "Integer" }, { - "name": "last_searched_at", + "name": "search_attempts!: i64", "ordinal": 9, + "type_info": "Integer" + }, + { + "name": "last_searched_at", + "ordinal": 10, "type_info": "Text" } ], @@ -67,8 +72,9 @@ false, false, false, + false, true ] }, - "hash": "7fc4e02be63e223a60b1bf7d78f68aad4106abc0e17f7da6b98efd818ecdc372" + "hash": "3ab4e736a4fc73625fd66e38dc3f510193b9c97788503f06c5c23f859780349a" } diff --git a/.sqlx/query-024a22e1d6974b40f4fdd86e4ad732ce6de6e7c92b93bc3c835cc48a296021fb.json b/.sqlx/query-448bdb6b59938fe6c8221092fd30c0e9b278dd4df85288aa165753c0efb1d2a8.json similarity index 76% rename from .sqlx/query-024a22e1d6974b40f4fdd86e4ad732ce6de6e7c92b93bc3c835cc48a296021fb.json rename to .sqlx/query-448bdb6b59938fe6c8221092fd30c0e9b278dd4df85288aa165753c0efb1d2a8.json index 78c5238..daea6c5 100644 --- a/.sqlx/query-024a22e1d6974b40f4fdd86e4ad732ce6de6e7c92b93bc3c835cc48a296021fb.json +++ b/.sqlx/query-448bdb6b59938fe6c8221092fd30c0e9b278dd4df85288aa165753c0efb1d2a8.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT se.series_id AS \"series_id!: i64\", e.id AS \"id!: i64\", e.season_id AS \"season_id!: i64\", e.number AS \"number!: i64\", e.title AS \"title!: String\", e.air_date, e.wanted AS \"wanted!: bool\", e.state AS \"state!: String\", e.search_attempts AS \"search_attempts!: i64\", e.last_searched_at\n FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE se.series_id = ? ORDER BY se.number, e.number", + "query": "SELECT se.series_id AS \"series_id!: i64\", e.id AS \"id!: i64\", e.season_id AS \"season_id!: i64\", e.number AS \"number!: i64\", e.title AS \"title!: String\", e.air_date, e.wanted AS \"wanted!: bool\", e.state AS \"state!: String\", e.vanished AS \"vanished!: bool\", e.search_attempts AS \"search_attempts!: i64\", e.last_searched_at\n FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE se.series_id = ?", "describe": { "columns": [ { @@ -44,13 +44,18 @@ "type_info": "Text" }, { - "name": "search_attempts!: i64", + "name": "vanished!: bool", "ordinal": 8, "type_info": "Integer" }, { - "name": "last_searched_at", + "name": "search_attempts!: i64", "ordinal": 9, + "type_info": "Integer" + }, + { + "name": "last_searched_at", + "ordinal": 10, "type_info": "Text" } ], @@ -67,8 +72,9 @@ false, false, false, + false, true ] }, - "hash": "024a22e1d6974b40f4fdd86e4ad732ce6de6e7c92b93bc3c835cc48a296021fb" + "hash": "448bdb6b59938fe6c8221092fd30c0e9b278dd4df85288aa165753c0efb1d2a8" } diff --git a/.sqlx/query-ae6d0463fd1bf373cbc9e0b5bbfc26e7934791024c216f876f940937dde45882.json b/.sqlx/query-633d5ff860d9c677eae5d06324d3098a0bbad30b46b23287131de72e64751256.json similarity index 77% rename from .sqlx/query-ae6d0463fd1bf373cbc9e0b5bbfc26e7934791024c216f876f940937dde45882.json rename to .sqlx/query-633d5ff860d9c677eae5d06324d3098a0bbad30b46b23287131de72e64751256.json index 00a8bfb..791176f 100644 --- a/.sqlx/query-ae6d0463fd1bf373cbc9e0b5bbfc26e7934791024c216f876f940937dde45882.json +++ b/.sqlx/query-633d5ff860d9c677eae5d06324d3098a0bbad30b46b23287131de72e64751256.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT se.series_id AS \"series_id!: i64\", e.id AS \"id!: i64\", e.season_id AS \"season_id!: i64\", e.number AS \"number!: i64\", e.title AS \"title!: String\", e.air_date, e.wanted AS \"wanted!: bool\", e.state AS \"state!: String\", e.search_attempts AS \"search_attempts!: i64\", e.last_searched_at\n FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE e.id = ?", + "query": "SELECT se.series_id AS \"series_id!: i64\", e.id AS \"id!: i64\", e.season_id AS \"season_id!: i64\", e.number AS \"number!: i64\", e.title AS \"title!: String\", e.air_date, e.wanted AS \"wanted!: bool\", e.state AS \"state!: String\", e.vanished AS \"vanished!: bool\", e.search_attempts AS \"search_attempts!: i64\", e.last_searched_at\n FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE e.id = ?", "describe": { "columns": [ { @@ -44,13 +44,18 @@ "type_info": "Text" }, { - "name": "search_attempts!: i64", + "name": "vanished!: bool", "ordinal": 8, "type_info": "Integer" }, { - "name": "last_searched_at", + "name": "search_attempts!: i64", "ordinal": 9, + "type_info": "Integer" + }, + { + "name": "last_searched_at", + "ordinal": 10, "type_info": "Text" } ], @@ -67,8 +72,9 @@ false, false, false, + false, true ] }, - "hash": "ae6d0463fd1bf373cbc9e0b5bbfc26e7934791024c216f876f940937dde45882" + "hash": "633d5ff860d9c677eae5d06324d3098a0bbad30b46b23287131de72e64751256" } diff --git a/.sqlx/query-3b3165092d403797f233f7a926daab43aea1c78c508790a2cca230ce0cc73b26.json b/.sqlx/query-c77c9cac0129fb4da57bef8ef2fb149c57293c76bad04f01603f11635789222d.json similarity index 77% rename from .sqlx/query-3b3165092d403797f233f7a926daab43aea1c78c508790a2cca230ce0cc73b26.json rename to .sqlx/query-c77c9cac0129fb4da57bef8ef2fb149c57293c76bad04f01603f11635789222d.json index 7d131e4..db9aa13 100644 --- a/.sqlx/query-3b3165092d403797f233f7a926daab43aea1c78c508790a2cca230ce0cc73b26.json +++ b/.sqlx/query-c77c9cac0129fb4da57bef8ef2fb149c57293c76bad04f01603f11635789222d.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT se.series_id AS \"series_id!: i64\", e.id AS \"id!: i64\", e.season_id AS \"season_id!: i64\", e.number AS \"number!: i64\", e.title AS \"title!: String\", e.air_date, e.wanted AS \"wanted!: bool\", e.state AS \"state!: String\", e.search_attempts AS \"search_attempts!: i64\", e.last_searched_at\n FROM episodes e JOIN seasons se ON se.id = e.season_id", + "query": "SELECT se.series_id AS \"series_id!: i64\", e.id AS \"id!: i64\", e.season_id AS \"season_id!: i64\", e.number AS \"number!: i64\", e.title AS \"title!: String\", e.air_date, e.wanted AS \"wanted!: bool\", e.state AS \"state!: String\", e.vanished AS \"vanished!: bool\", e.search_attempts AS \"search_attempts!: i64\", e.last_searched_at\n FROM episodes e JOIN seasons se ON se.id = e.season_id", "describe": { "columns": [ { @@ -44,13 +44,18 @@ "type_info": "Text" }, { - "name": "search_attempts!: i64", + "name": "vanished!: bool", "ordinal": 8, "type_info": "Integer" }, { - "name": "last_searched_at", + "name": "search_attempts!: i64", "ordinal": 9, + "type_info": "Integer" + }, + { + "name": "last_searched_at", + "ordinal": 10, "type_info": "Text" } ], @@ -67,8 +72,9 @@ false, false, false, + false, true ] }, - "hash": "3b3165092d403797f233f7a926daab43aea1c78c508790a2cca230ce0cc73b26" + "hash": "c77c9cac0129fb4da57bef8ef2fb149c57293c76bad04f01603f11635789222d" } diff --git a/crates/arr-api/src/series.rs b/crates/arr-api/src/series.rs index 0c00c28..56e765b 100644 --- a/crates/arr-api/src/series.rs +++ b/crates/arr-api/src/series.rs @@ -110,6 +110,10 @@ pub struct Episode { /// ยง4.1. The only intent in the TV aggregate. pub wanted: bool, pub state: String, + /// #122. The episode vanished upstream while a file of its own remained, + /// which is why the row still exists. A conflict for the operator to + /// resolve; nothing was deleted from disk. + pub vanished: bool, pub search_attempts: i64, pub last_searched_at: Option, } @@ -197,6 +201,7 @@ struct EpisodeRow { air_date: Option, wanted: bool, state: String, + vanished: bool, search_attempts: i64, last_searched_at: Option, } @@ -345,7 +350,7 @@ async fn tv_by_series( .await?; let rows = sqlx::query_as!( EpisodeRow, - r#"SELECT se.series_id AS "series_id!: i64", e.id AS "id!: i64", e.season_id AS "season_id!: i64", e.number AS "number!: i64", e.title AS "title!: String", e.air_date, e.wanted AS "wanted!: bool", e.state AS "state!: String", e.search_attempts AS "search_attempts!: i64", e.last_searched_at + r#"SELECT se.series_id AS "series_id!: i64", e.id AS "id!: i64", e.season_id AS "season_id!: i64", e.number AS "number!: i64", e.title AS "title!: String", e.air_date, e.wanted AS "wanted!: bool", e.state AS "state!: String", e.vanished AS "vanished!: bool", e.search_attempts AS "search_attempts!: i64", e.last_searched_at FROM episodes e JOIN seasons se ON se.id = e.season_id"# ) .fetch_all(pool(state)?) @@ -390,7 +395,7 @@ async fn load_series(state: &AppState, id: i64) -> Result { .collect(); let episodes = sqlx::query_as!( EpisodeRow, - r#"SELECT se.series_id AS "series_id!: i64", e.id AS "id!: i64", e.season_id AS "season_id!: i64", e.number AS "number!: i64", e.title AS "title!: String", e.air_date, e.wanted AS "wanted!: bool", e.state AS "state!: String", e.search_attempts AS "search_attempts!: i64", e.last_searched_at + r#"SELECT se.series_id AS "series_id!: i64", e.id AS "id!: i64", e.season_id AS "season_id!: i64", e.number AS "number!: i64", e.title AS "title!: String", e.air_date, e.wanted AS "wanted!: bool", e.state AS "state!: String", e.vanished AS "vanished!: bool", e.search_attempts AS "search_attempts!: i64", e.last_searched_at FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE se.series_id = ?"#, id ) @@ -602,7 +607,7 @@ async fn load_seasons(state: &AppState, series_id: i64) -> Result, A .await?; let episodes = sqlx::query_as!( EpisodeRow, - r#"SELECT se.series_id AS "series_id!: i64", e.id AS "id!: i64", e.season_id AS "season_id!: i64", e.number AS "number!: i64", e.title AS "title!: String", e.air_date, e.wanted AS "wanted!: bool", e.state AS "state!: String", e.search_attempts AS "search_attempts!: i64", e.last_searched_at + r#"SELECT se.series_id AS "series_id!: i64", e.id AS "id!: i64", e.season_id AS "season_id!: i64", e.number AS "number!: i64", e.title AS "title!: String", e.air_date, e.wanted AS "wanted!: bool", e.state AS "state!: String", e.vanished AS "vanished!: bool", e.search_attempts AS "search_attempts!: i64", e.last_searched_at FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE se.series_id = ? ORDER BY se.number, e.number"#, series_id ) @@ -627,6 +632,7 @@ async fn load_seasons(state: &AppState, series_id: i64) -> Result, A air_date: episode.air_date.clone(), wanted: episode.wanted, state: episode.state.clone(), + vanished: episode.vanished, search_attempts: episode.search_attempts, last_searched_at: episode.last_searched_at.clone(), }) @@ -831,7 +837,7 @@ pub async fn update_season( async fn load_episode(state: &AppState, id: i64) -> Result { let row = sqlx::query_as!( EpisodeRow, - r#"SELECT se.series_id AS "series_id!: i64", e.id AS "id!: i64", e.season_id AS "season_id!: i64", e.number AS "number!: i64", e.title AS "title!: String", e.air_date, e.wanted AS "wanted!: bool", e.state AS "state!: String", e.search_attempts AS "search_attempts!: i64", e.last_searched_at + r#"SELECT se.series_id AS "series_id!: i64", e.id AS "id!: i64", e.season_id AS "season_id!: i64", e.number AS "number!: i64", e.title AS "title!: String", e.air_date, e.wanted AS "wanted!: bool", e.state AS "state!: String", e.vanished AS "vanished!: bool", e.search_attempts AS "search_attempts!: i64", e.last_searched_at FROM episodes e JOIN seasons se ON se.id = e.season_id WHERE e.id = ?"#, id ) @@ -846,6 +852,7 @@ async fn load_episode(state: &AppState, id: i64) -> Result { air_date: row.air_date, wanted: row.wanted, state: row.state, + vanished: row.vanished, search_attempts: row.search_attempts, last_searched_at: row.last_searched_at, }) diff --git a/crates/arr-daemon/src/series_refresh.rs b/crates/arr-daemon/src/series_refresh.rs index 350fca0..12bac4e 100644 --- a/crates/arr-daemon/src/series_refresh.rs +++ b/crates/arr-daemon/src/series_refresh.rs @@ -22,7 +22,7 @@ //! //! Refresh is idempotent: over unchanged TMDB data only the stamp moves. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use arr_core::tracking::{apply_auto_track, apply_tracked, RefreshedSeason}; @@ -354,14 +354,14 @@ impl SeriesRefreshAction { known: &HashMap, bool)>, detail: &TmdbSeasonDetail, ) -> Result { - let upstream: HashMap = detail + let upstream: HashSet = detail .episodes .iter() - .map(|source| (i64::from(source.number), ())) + .map(|source| i64::from(source.number)) .collect(); let mut changed = false; for (number, &(id, ..)) in known { - if upstream.contains_key(number) { + if upstream.contains(number) { continue; } let has_file = sqlx::query_scalar!(