feat(api): name attention episodes and seasons

This commit is contained in:
Miguel Palhas
2026-08-23 19:45:33 +01:00
parent 11d2952863
commit 7f7f26dbc7
6 changed files with 184 additions and 110 deletions
+64 -20
View File
@@ -100,6 +100,22 @@ pub struct AttentionQueues {
pub tv_needs_decision: Vec<SeriesAttention>,
}
/// 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<i64>,
/// Episodes that put the series in this queue.
pub episodes: Vec<i64>,
pub episodes: Vec<QueuedEpisode>,
/// Seasons whose pack grab failed and fell back to per-episode grabbing.
pub seasons: Vec<i64>,
pub seasons: Vec<QueuedSeason>,
}
#[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<i64>,
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<i64> = 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.