Library views with derived status (#83)
This commit was merged in pull request #83.
This commit is contained in:
@@ -297,6 +297,7 @@ mod tests {
|
||||
("/api/movies/{movie_id}/releases", "get"),
|
||||
("/api/movies/{movie_id}/releases/{release_id}/grab", "post"),
|
||||
("/api/queues/attention", "get"),
|
||||
("/api/series", "get"),
|
||||
] {
|
||||
assert!(
|
||||
json["paths"][path][method].is_object(),
|
||||
@@ -309,6 +310,7 @@ mod tests {
|
||||
"UpdateMovie",
|
||||
"Release",
|
||||
"AttentionQueues",
|
||||
"Series",
|
||||
] {
|
||||
assert!(
|
||||
json["components"]["schemas"][schema].is_object(),
|
||||
|
||||
@@ -22,6 +22,9 @@ pub struct Movie {
|
||||
pub blocked: bool,
|
||||
pub search_attempts: i64,
|
||||
pub last_searched_at: Option<String>,
|
||||
/// The relaxed rule recorded when a file was imported under a waiver
|
||||
/// (§5.7), so the UI can show "English, no dub" instead of a clean match.
|
||||
pub waiver: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
@@ -167,7 +170,7 @@ pub(crate) fn pool(state: &AppState) -> Result<&sqlx::SqlitePool, ApiError> {
|
||||
}
|
||||
|
||||
async fn load_movie(state: &AppState, id: i64) -> Result<Movie, ApiError> {
|
||||
Ok(sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at FROM movies WHERE id = ?"#, id)
|
||||
Ok(sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies WHERE id = ?"#, id)
|
||||
.fetch_one(pool(state)?)
|
||||
.await?)
|
||||
}
|
||||
@@ -192,11 +195,11 @@ pub async fn list(
|
||||
Query(query): Query<ListMoviesQuery>,
|
||||
) -> Result<Json<Vec<Movie>>, ApiError> {
|
||||
let movies = if let Some(owner_id) = query.owner_id {
|
||||
sqlx::query_as!(Movie, r#"SELECT m.id AS "id!: i64", m.tmdb_id AS "tmdb_id!: i64", m.title AS "title!: String", m.year, m.original_language, m.root_id AS "root_id!: i64", m.wanted AS "wanted!: bool", m.overrides AS "overrides!: serde_json::Value", m.state AS "state!: String", m.blocked AS "blocked!: bool", m.search_attempts AS "search_attempts!: i64", m.last_searched_at FROM movies m JOIN title_owners t ON t.title_kind = 'movie' AND t.title_id = m.id WHERE t.owner_id = ? ORDER BY m.title, m.year, m.id"#, owner_id)
|
||||
sqlx::query_as!(Movie, r#"SELECT m.id AS "id!: i64", m.tmdb_id AS "tmdb_id!: i64", m.title AS "title!: String", m.year, m.original_language, m.root_id AS "root_id!: i64", m.wanted AS "wanted!: bool", m.overrides AS "overrides!: serde_json::Value", m.state AS "state!: String", m.blocked AS "blocked!: bool", m.search_attempts AS "search_attempts!: i64", m.last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = m.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies m JOIN title_owners t ON t.title_kind = 'movie' AND t.title_id = m.id WHERE t.owner_id = ? ORDER BY m.title, m.year, m.id"#, owner_id)
|
||||
.fetch_all(pool(&state)?)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at FROM movies ORDER BY title, year, id"#)
|
||||
sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies ORDER BY title, year, id"#)
|
||||
.fetch_all(pool(&state)?)
|
||||
.await?
|
||||
};
|
||||
@@ -420,10 +423,10 @@ pub async fn grab(
|
||||
)
|
||||
)]
|
||||
pub async fn attention(State(state): State<AppState>) -> Result<Json<AttentionQueues>, ApiError> {
|
||||
let no_pt_source = sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at FROM movies WHERE id IN (SELECT m.id FROM movies m JOIN roots root ON root.id = m.root_id WHERE root.audience = 'kids' AND m.wanted = 1 AND m.blocked = 0 AND m.state = 'missing' AND m.search_attempts > 0 AND NOT EXISTS (SELECT 1 FROM movie_releases mr JOIN releases r ON r.id = mr.release_id WHERE mr.movie_id = m.id AND r.verdict IN ('eligible', 'waived'))) ORDER BY title"#)
|
||||
let no_pt_source = sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies WHERE id IN (SELECT m.id FROM movies m JOIN roots root ON root.id = m.root_id WHERE root.audience = 'kids' AND m.wanted = 1 AND m.blocked = 0 AND m.state = 'missing' AND m.search_attempts > 0 AND NOT EXISTS (SELECT 1 FROM movie_releases mr JOIN releases r ON r.id = mr.release_id WHERE mr.movie_id = m.id AND r.verdict IN ('eligible', 'waived'))) ORDER BY title"#)
|
||||
.fetch_all(pool(&state)?)
|
||||
.await?;
|
||||
let needs_decision = sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at FROM movies WHERE (SELECT count(DISTINCT g.release_id) FROM grabs g WHERE g.target_kind = 'movie' AND g.target_id = movies.id AND g.state = 'failed') >= 2 ORDER BY title"#)
|
||||
let needs_decision = sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies WHERE (SELECT count(DISTINCT g.release_id) FROM grabs g WHERE g.target_kind = 'movie' AND g.target_id = movies.id AND g.state = 'failed') >= 2 ORDER BY title"#)
|
||||
.fetch_all(pool(&state)?)
|
||||
.await?;
|
||||
Ok(Json(AttentionQueues {
|
||||
|
||||
@@ -110,11 +110,11 @@ pub async fn search(
|
||||
.parse::<i64>()
|
||||
.map_err(|_| ApiError::Invalid("invalid TMDB id".into()))?;
|
||||
let pattern = format!("%{}%", escape_like(input));
|
||||
sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at FROM movies WHERE tmdb_id = ? OR title LIKE ? ESCAPE '\' ORDER BY title, year, id"#, tmdb_id, pattern)
|
||||
sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies WHERE tmdb_id = ? OR title LIKE ? ESCAPE '\' ORDER BY title, year, id"#, tmdb_id, pattern)
|
||||
.fetch_all(database.pool()).await?
|
||||
} else {
|
||||
let pattern = format!("%{}%", escape_like(input));
|
||||
sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at FROM movies WHERE title LIKE ? ESCAPE '\' ORDER BY title, year, id"#, pattern)
|
||||
sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies WHERE title LIKE ? ESCAPE '\' ORDER BY title, year, id"#, pattern)
|
||||
.fetch_all(database.pool()).await?
|
||||
};
|
||||
|
||||
@@ -123,7 +123,7 @@ pub async fn search(
|
||||
if matches!(kind, SearchInputKind::ImdbId) {
|
||||
for result in &results {
|
||||
let tmdb_id = i64::from(result.tmdb_id);
|
||||
if let Some(movie) = sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at FROM movies WHERE tmdb_id = ?"#, tmdb_id)
|
||||
if let Some(movie) = sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at, (SELECT f.waiver FROM media_files f WHERE f.owner_kind = 'movie' AND f.owner_id = movies.id AND f.waiver IS NOT NULL ORDER BY f.id LIMIT 1) AS "waiver?: serde_json::Value" FROM movies WHERE tmdb_id = ?"#, tmdb_id)
|
||||
.fetch_optional(database.pool()).await?
|
||||
{
|
||||
library.push(movie);
|
||||
|
||||
@@ -50,6 +50,10 @@ pub struct Series {
|
||||
pub blocked: bool,
|
||||
/// `airing`, `incomplete`, `waiting`, `complete` or `ended` (§4.2).
|
||||
pub status: String,
|
||||
/// Episodes currently marked wanted (§4.1 — the only intent).
|
||||
pub wanted_episodes: i64,
|
||||
/// Wanted episodes already on disk.
|
||||
pub available_episodes: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
@@ -262,6 +266,10 @@ fn status_name(status: SeriesStatus) -> &'static str {
|
||||
}
|
||||
|
||||
fn with_status(row: &SeriesRow, episodes: &[arr_core::Episode], now: SystemTime) -> Series {
|
||||
let wanted = episodes.iter().filter(|episode| episode.wanted);
|
||||
let available = wanted
|
||||
.clone()
|
||||
.filter(|episode| episode.state == MediaState::Available);
|
||||
Series {
|
||||
id: row.id,
|
||||
tmdb_id: row.tmdb_id,
|
||||
@@ -274,6 +282,8 @@ fn with_status(row: &SeriesRow, episodes: &[arr_core::Episode], now: SystemTime)
|
||||
upstream_ended: row.upstream_ended,
|
||||
blocked: row.blocked,
|
||||
status: status_name(derive_series_status(&core_series(row), 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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1208,6 +1218,8 @@ mod tests {
|
||||
listed[0]["status"], "incomplete",
|
||||
"§4.2: an aired wanted episode with no file"
|
||||
);
|
||||
assert_eq!(listed[0]["wanted_episodes"], 1);
|
||||
assert_eq!(listed[0]["available_episodes"], 0);
|
||||
|
||||
sqlx::query("UPDATE episodes SET state = 'available' WHERE id = ?")
|
||||
.bind(episode_id)
|
||||
|
||||
Reference in New Issue
Block a user