fix(arr): drop the forced flag from subtitle grab

§15 gives forced tracks no sidecar name and never lets them
satisfy a want; ranking already rejects every forced candidate
(#222). A grab endpoint accepting `forced: true` had nowhere
coherent to put the result, so the flag and its stale test are
gone (#233).
This commit is contained in:
Miguel Palhas
2026-08-25 08:42:56 +01:00
parent 1126a52bbb
commit 097e081f4d
2 changed files with 25 additions and 44 deletions
+21 -41
View File
@@ -14,8 +14,10 @@
//! handlers do the work inline and answer with the result, rather than
//! returning 202 and leaving the operator to poll. The consequence the client
//! has to know about: candidate ids are meaningful only to the provider that
//! issued them, and a grab therefore repeats the facts (`forced`, `sdh`) the
//! search reported, because nothing on the server remembers them.
//! issued them, and a grab therefore repeats the fact (`sdh`) the search
//! reported, because nothing on the server remembers it. A forced candidate
//! is never eligible (§15, #185), so a grab has no `forced` flag to accept —
//! there is nowhere to put one (issue #233).
//!
//! Verdict vocabulary is §9.3's, unchanged: `eligible` or `rejected` plus the
//! name of the rule that killed it, exactly as `Release` spells it, so the
@@ -282,11 +284,11 @@ pub struct SubtitleGrabInput {
pub provider: String,
pub candidate_id: String,
pub language: String,
/// The candidate's own flags, as the search reported them. Nothing on
/// the server remembers a search, so they travel with the grab; they are
/// facts about the subtitle and are stored with it.
#[serde(default)]
pub forced: bool,
/// The candidate's own SDH flag, as the search reported it. Nothing on
/// the server remembers a search, so it travels with the grab; it is a
/// fact about the subtitle and is stored with it. No `forced` flag
/// exists here: ranking rejects every forced candidate outright (§15),
/// so a grab never has one to record (issue #233).
#[serde(default)]
pub sdh: bool,
}
@@ -989,20 +991,10 @@ pub async fn grab(
&destination.to_string_lossy(),
)
.sync(db_sync_state(sync.state));
if input.forced {
record = record.forced();
}
if input.sdh {
record = record.sdh();
}
finish(
&state,
&record,
target.media_file_id,
&language,
input.forced,
)
.await
finish(&state, &record, target.media_file_id, &language).await
}
#[utoipa::path(
@@ -1082,14 +1074,7 @@ pub async fn translate(
&destination.to_string_lossy(),
)
.sync(db_sync_state(sync.state));
finish(
&state,
&record,
target.media_file_id,
&target_language,
false,
)
.await
finish(&state, &record, target.media_file_id, &target_language).await
}
#[utoipa::path(
@@ -1259,20 +1244,17 @@ async fn write_sidecar(destination: &Path, text: &str) -> Result<(), ApiError> {
/// The `mark_satisfied` is §15's "manual actions bypass the wanted-set
/// logic": the loop stops working on that language whether or not it was in
/// the wanted set, so a manually requested Spanish subtitle is never treated
/// as a gap and never replaced. A forced track is the exception the same
/// section names — it covers signs only and satisfies nothing — so it is
/// recorded and left out of the satisfaction claim.
/// as a gap and never replaced. Every grab reaching this point is a plain or
/// SDH subtitle — a forced candidate is never eligible, so there is no
/// exception left to carve out of the satisfaction claim (issue #233).
async fn finish(
state: &AppState,
record: &arr_db::NewSubtitleFile,
media_file_id: i64,
language: &Language,
forced: bool,
) -> Result<(StatusCode, Json<Subtitle>), ApiError> {
let id = db::record_file(pool(state)?, record).await?;
if !forced {
db::mark_satisfied(pool(state)?, media_file_id, &language.to_string()).await?;
}
db::mark_satisfied(pool(state)?, media_file_id, &language.to_string()).await?;
let subtitle = db::files_for(pool(state)?, media_file_id)
.await?
.into_iter()
@@ -2197,18 +2179,16 @@ mod tests {
);
}
/// §15: a forced track covers signs only, so it is recorded and it still
/// satisfies nothing.
/// §233: the grab endpoint has no `forced` flag to accept — a forced
/// candidate is never eligible (§15), so nothing sent it one.
#[tokio::test]
async fn a_forced_grab_is_recorded_without_satisfying_the_language() {
async fn a_grab_has_no_forced_flag_to_send() {
let fixture = stub_application().await;
let mut body = pt();
body["candidate_id"] = serde_json::json!("forced");
body["forced"] = serde_json::json!(true);
let body = pt();
assert!(body.get("forced").is_none());
let (status, subtitle) = grab(&fixture, body).await;
assert_eq!(status, StatusCode::CREATED);
assert_eq!(subtitle["forced"], true);
assert_eq!(fixture.attempt_state("pt-PT").await, None);
assert_eq!(subtitle["forced"], false);
}
/// §15 has no in-place replacement: delete first, then fetch.