Files
arr/web/src/queues.ts
T
naps62-yolo 22024d4be2
ci / web (push) Successful in 24s
ci / rust (push) Successful in 1m3s
e2e / e2e (push) Successful in 53s
Attention queues: no-PT-source and needs-decision (#89)
2026-08-22 23:57:42 +01:00

87 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Hand-written mirror of arr-api's /api/queues/attention schema — same
// reasoning as search.ts: the generated client (src/api/) is uncommitted,
// so CI's tsc cannot see it.
import { type ActionOutcome, queueSearch } from "./releases";
import type { LibraryMovie } from "./search";
/** A queue entry: the movie plus its §6.2 search history. */
export interface AttentionMovie extends LibraryMovie {
search_attempts: number;
last_searched_at: string | null;
}
export interface AttentionQueues {
no_pt_source: AttentionMovie[];
needs_decision: AttentionMovie[];
}
export type AttentionOutcome =
| { kind: "results"; queues: AttentionQueues }
| { kind: "error"; detail: string };
export async function fetchAttention(): Promise<AttentionOutcome> {
try {
const response = await fetch("/api/queues/attention");
if (!response.ok) {
return { kind: "error", detail: await errorDetail(response) };
}
return { kind: "results", queues: (await response.json()) as AttentionQueues };
} catch {
return { kind: "error", detail: "daemon unreachable" };
}
}
export type AllowEnglishOutcome =
| { kind: "done"; searchQueued: boolean }
| { kind: "error"; detail: string; overrideWritten: boolean };
/**
* The §5.2 one click: write `allow_english_audio` on the title, then queue
* the normal search. Stored verdicts are stale until the daemon
* reclassifies, so emptying the queue is the search's job, not a re-read's.
*/
export async function allowEnglishAudio(movieId: number): Promise<AllowEnglishOutcome> {
try {
const current = await fetch(`/api/movies/${movieId}`);
if (!current.ok) {
return { kind: "error", detail: await errorDetail(current), overrideWritten: false };
}
const movie = (await current.json()) as { overrides: Record<string, unknown> };
const patch = await fetch(`/api/movies/${movieId}`, {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ overrides: { ...movie.overrides, allow_english_audio: true } }),
});
if (!patch.ok) {
return { kind: "error", detail: await errorDetail(patch), overrideWritten: false };
}
} catch {
return { kind: "error", detail: "daemon unreachable", overrideWritten: false };
}
const search: ActionOutcome = await queueSearch(movieId);
if (search.kind === "error") {
return { kind: "error", detail: `search not queued — ${search.detail}`, overrideWritten: true };
}
return { kind: "done", searchQueued: true };
}
/** Entries across both queues — the rail badge's number. */
export function attentionTotal(queues: AttentionQueues): number {
return queues.no_pt_source.length + queues.needs_decision.length;
}
/** `searched 3×`, or null before the first attempt ever lands here. */
export function attemptsLabel(attempts: number): string | null {
return attempts > 0 ? `searched ${attempts}×` : null;
}
async function errorDetail(response: Response): Promise<string> {
try {
const body = (await response.json()) as { error?: string };
return body.error ?? `http ${response.status}`;
} catch {
return `http ${response.status}`;
}
}