feat(core): derived status ignores season 0

This commit is contained in:
Miguel Palhas
2026-08-23 16:32:13 +01:00
parent 2fca8ec09f
commit 3bd613ec63
4 changed files with 205 additions and 25 deletions
+70 -13
View File
@@ -265,7 +265,21 @@ fn status_name(status: SeriesStatus) -> &'static str {
}
}
fn with_status(row: &SeriesRow, episodes: &[arr_core::Episode], now: SystemTime) -> Series {
fn core_season(id: i64, series_id: i64, number: i64) -> arr_core::Season {
arr_core::Season {
id: SeasonId(id),
series_id: SeriesId(series_id),
number: u16::try_from(number).unwrap_or_default(),
tracked: false,
}
}
fn with_status(
row: &SeriesRow,
seasons: &[arr_core::Season],
episodes: &[arr_core::Episode],
now: SystemTime,
) -> Series {
let wanted = episodes.iter().filter(|episode| episode.wanted);
let available = wanted
.clone()
@@ -281,20 +295,37 @@ fn with_status(row: &SeriesRow, episodes: &[arr_core::Episode], now: SystemTime)
overrides: row.overrides.clone(),
upstream_ended: row.upstream_ended,
blocked: row.blocked,
status: status_name(derive_series_status(&core_series(row), episodes, now)).to_owned(),
status: status_name(derive_series_status(
&core_series(row),
seasons,
episodes,
now,
))
.to_owned(),
wanted_episodes: i64::try_from(wanted.count()).unwrap_or(i64::MAX),
available_episodes: i64::try_from(available.count()).unwrap_or(i64::MAX),
}
}
/// Every episode in the library, keyed by the series it belongs to.
/// Every season and episode in the library, keyed by the series they belong to.
///
/// One query rather than one per series: the whole table is a few thousand
/// One query each rather than one per series: the whole table is a few thousand
/// rows for a single household (§10), and the status of every listed series
/// needs all of them anyway.
async fn episodes_by_series(
async fn tv_by_series(
state: &AppState,
) -> Result<HashMap<i64, Vec<arr_core::Episode>>, ApiError> {
) -> Result<
(
HashMap<i64, Vec<arr_core::Season>>,
HashMap<i64, Vec<arr_core::Episode>>,
),
ApiError,
> {
let season_rows = sqlx::query!(
r#"SELECT id AS "id!: i64", series_id AS "series_id!: i64", number AS "number!: i64" FROM seasons"#
)
.fetch_all(pool(state)?)
.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
@@ -303,14 +334,22 @@ async fn episodes_by_series(
.fetch_all(pool(state)?)
.await?;
let mut grouped: HashMap<i64, Vec<arr_core::Episode>> = HashMap::new();
let mut seasons: HashMap<i64, Vec<arr_core::Season>> = HashMap::new();
for row in &season_rows {
seasons.entry(row.series_id).or_default().push(core_season(
row.id,
row.series_id,
row.number,
));
}
let mut episodes: HashMap<i64, Vec<arr_core::Episode>> = HashMap::new();
for row in &rows {
grouped
episodes
.entry(row.series_id)
.or_default()
.push(core_episode(row));
}
Ok(grouped)
Ok((seasons, episodes))
}
async fn load_series_row(state: &AppState, id: i64) -> Result<SeriesRow, ApiError> {
@@ -322,6 +361,16 @@ async fn load_series_row(state: &AppState, id: i64) -> Result<SeriesRow, ApiErro
async fn load_series(state: &AppState, id: i64) -> Result<Series, ApiError> {
let row = load_series_row(state, id).await?;
let season_rows = sqlx::query!(
r#"SELECT id AS "id!: i64", series_id AS "series_id!: i64", number AS "number!: i64" FROM seasons WHERE series_id = ?"#,
id
)
.fetch_all(pool(state)?)
.await?;
let seasons: Vec<_> = season_rows
.iter()
.map(|row| core_season(row.id, row.series_id, row.number))
.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
@@ -331,7 +380,7 @@ async fn load_series(state: &AppState, id: i64) -> Result<Series, ApiError> {
.fetch_all(pool(state)?)
.await?;
let episodes: Vec<_> = episodes.iter().map(core_episode).collect();
Ok(with_status(&row, &episodes, SystemTime::now()))
Ok(with_status(&row, &seasons, &episodes, SystemTime::now()))
}
async fn require_tv_root(state: &AppState, root_id: i64) -> Result<(), ApiError> {
@@ -377,12 +426,20 @@ pub async fn list(
.await?
};
let episodes = episodes_by_series(&state).await?;
let (seasons, episodes) = tv_by_series(&state).await?;
let now = SystemTime::now();
let empty = Vec::new();
let no_seasons: Vec<arr_core::Season> = Vec::new();
let no_episodes: Vec<arr_core::Episode> = Vec::new();
Ok(Json(
rows.iter()
.map(|row| with_status(row, episodes.get(&row.id).unwrap_or(&empty), now))
.map(|row| {
with_status(
row,
seasons.get(&row.id).unwrap_or(&no_seasons),
episodes.get(&row.id).unwrap_or(&no_episodes),
now,
)
})
.collect(),
))
}