Hard/soft fail handling and release blacklist (#87)
ci / web (push) Successful in 26s
ci / rust (push) Successful in 1m6s
e2e / e2e (push) Successful in 52s

This commit was merged in pull request #87.
This commit is contained in:
2026-08-22 23:42:36 +01:00
parent 38bd4102bb
commit 01af397a40
14 changed files with 674 additions and 43 deletions
+71
View File
@@ -69,6 +69,23 @@ pub struct Release {
pub rejected_rule: Option<String>,
}
/// A library file and what it cost to accept it (`DESIGN.md` §5.7).
///
/// `waiver` names the rule that was relaxed to let a soft fail in. It is the
/// difference between a file that satisfies the policy and one that merely
/// plays, so it travels with the file everywhere the file does.
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct MovieFile {
pub id: i64,
pub path: String,
pub size: i64,
/// What `ffprobe` found (§5.6).
pub probed: Option<serde_json::Value>,
/// The relaxed rule's name, or `null` for a clean import. Shares its
/// vocabulary with `Release::rejected_rule`.
pub waiver: Option<String>,
}
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct AttentionQueues {
pub no_pt_source: Vec<Movie>,
@@ -385,6 +402,27 @@ pub async fn releases(
Ok(Json(releases))
}
#[utoipa::path(
get, path = "/api/movies/{movie_id}/files", tag = "movies",
params(("movie_id" = i64, Path, description = "Movie row id")),
responses(
(status = 200, body = [MovieFile]),
(status = 404, body = ErrorBody),
(status = 500, body = ErrorBody),
(status = 503, body = ErrorBody)
)
)]
pub async fn files(
State(state): State<AppState>,
Path(id): Path<i64>,
) -> Result<Json<Vec<MovieFile>>, ApiError> {
load_movie(&state, id).await?;
let files = sqlx::query_as!(MovieFile, r#"SELECT id AS "id!: i64", path AS "path!: String", size AS "size!: i64", probed AS "probed?: serde_json::Value", json_extract(waiver, '$.rule') AS "waiver?: String" FROM media_files WHERE owner_kind = 'movie' AND owner_id = ? ORDER BY path"#, id)
.fetch_all(pool(&state)?)
.await?;
Ok(Json(files))
}
#[utoipa::path(
post, path = "/api/movies/{movie_id}/releases/{release_id}/grab", tag = "movies",
params(("movie_id" = i64, Path), ("release_id" = i64, Path)),
@@ -563,6 +601,39 @@ mod tests {
response.json().await.expect("movie json")
}
/// §5.7: a soft-failed import is imported and waived, and the waiver
/// reaches the API — a file that merely plays must never read as a clean
/// match.
#[tokio::test]
async fn a_movie_file_carries_its_waiver() {
let (_dir, state, base) = application().await;
let movie = add_movie(&base, 693_134, 1).await;
let movie_id = movie["id"].as_i64().expect("movie id");
let pool = state.database().expect("database").pool();
sqlx::query(
r#"INSERT INTO media_files (owner_kind, owner_id, path, size, probed, waiver)
VALUES ('movie', ?, '/library/dune.mkv', 23622320128,
'{"resolution":"1080p"}', '{"rule":"required_audio"}')"#,
)
.bind(movie_id)
.execute(pool)
.await
.expect("media file");
let files: Vec<serde_json::Value> =
reqwest::get(format!("{base}/api/movies/{movie_id}/files"))
.await
.expect("files")
.json()
.await
.expect("json");
assert_eq!(files.len(), 1);
assert_eq!(files[0]["path"], "/library/dune.mkv");
assert_eq!(files[0]["waiver"], "required_audio");
assert_eq!(files[0]["probed"]["resolution"], "1080p");
}
#[tokio::test]
async fn crud_preserves_intent_and_overrides() {
let (_dir, _state, base) = application().await;