fix(api): give an expired subtitle candidate a typed error
A grab naming a stale candidate_id now fails as ApiError::SubtitleCandidateExpired (404, code candidate_expired) instead of the generic upstream 503 string the panel had to pattern-match for 'not found'.
This commit is contained in:
+4
-3
@@ -1320,9 +1320,10 @@ function subtitleSection(status: SubtitleStatus, refresh: () => void): SubtitleS
|
||||
say(note, "fetching, syncing, writing…");
|
||||
const outcome = await grabSubtitle(mediaFileId, candidate);
|
||||
if (outcome.kind === "error") {
|
||||
const stale = /not found/i.test(outcome.detail)
|
||||
? " — search again, a candidate id does not outlive its search"
|
||||
: "";
|
||||
const stale =
|
||||
outcome.code === "candidate_expired"
|
||||
? " — search again, a candidate id does not outlive its search"
|
||||
: "";
|
||||
say(note, `grab failed — ${outcome.detail}${stale}`, "fault");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -463,3 +463,18 @@ export async function errorDetail(response: Response): Promise<string> {
|
||||
return `http ${response.status}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link errorDetail}, but also surfaces `ErrorBody.code` — the
|
||||
* machine-readable discriminant a client switches on instead of matching the
|
||||
* message text (issue #221). `code` is `undefined` on every error that has
|
||||
* none, which is most of them.
|
||||
*/
|
||||
export async function errorBody(response: Response): Promise<{ detail: string; code?: string }> {
|
||||
try {
|
||||
const body = (await response.json()) as { error?: string; code?: string };
|
||||
return { detail: body.error ?? `http ${response.status}`, code: body.code };
|
||||
} catch {
|
||||
return { detail: `http ${response.status}` };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// schemas — same reasoning as search.ts: the generated client (src/api/) is
|
||||
// uncommitted, so CI's tsc cannot see it.
|
||||
|
||||
import { errorDetail } from "./releases";
|
||||
import { errorBody, errorDetail } from "./releases";
|
||||
|
||||
/** One subtitle arr knows about, as `/subtitles` and `/subtitles/status` render it. */
|
||||
export interface Subtitle {
|
||||
@@ -201,12 +201,16 @@ export async function searchSubtitles(
|
||||
|
||||
export type SubtitleWriteOutcome =
|
||||
| { kind: "done"; subtitle: Subtitle }
|
||||
| { kind: "error"; detail: string };
|
||||
| { kind: "error"; detail: string; code?: string };
|
||||
|
||||
/**
|
||||
* One click: the daemon fetches the candidate, runs `alass` over it and
|
||||
* writes the sidecar (§15). The candidate's own flags travel with it —
|
||||
* nothing on the server remembers a search.
|
||||
*
|
||||
* A grab can fail because the candidate itself expired between the search
|
||||
* and the click (issue #221) — the server reports that as `code:
|
||||
* "candidate_expired"`, not by wording the message a particular way.
|
||||
*/
|
||||
export async function grabSubtitle(
|
||||
mediaFileId: number,
|
||||
@@ -225,7 +229,8 @@ export async function grabSubtitle(
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
return { kind: "error", detail: await errorDetail(response) };
|
||||
const { detail, code } = await errorBody(response);
|
||||
return { kind: "error", detail, code };
|
||||
}
|
||||
return { kind: "done", subtitle: (await response.json()) as Subtitle };
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user