feat(api): expose vanished flag on episodes

This commit is contained in:
Miguel Palhas
2026-08-23 17:59:45 +01:00
parent 8b9a6d5a16
commit 34ccbeeb5a
6 changed files with 55 additions and 24 deletions
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
+11 -4
View File
@@ -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<String>,
}
@@ -197,6 +201,7 @@ struct EpisodeRow {
air_date: Option<String>,
wanted: bool,
state: String,
vanished: bool,
search_attempts: i64,
last_searched_at: Option<String>,
}
@@ -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<Series, ApiError> {
.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<Vec<Season>, 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<Vec<Season>, 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<Episode, ApiError> {
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<Episode, ApiError> {
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,
})
+4 -4
View File
@@ -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<i64, (i64, String, Option<String>, bool)>,
detail: &TmdbSeasonDetail,
) -> Result<bool, RefreshError> {
let upstream: HashMap<i64, ()> = detail
let upstream: HashSet<i64> = 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!(