Files
arr/web/src/queues.ts
T
2026-08-23 19:23:19 +01:00

104 lines
3.5 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;
}
/** A series in a TV attention queue, with what put it there (§9.5). */
export interface SeriesAttention {
series_id: number;
tmdb_id: number;
title: string;
year: number | null;
episodes: number[];
seasons: number[];
}
export interface AttentionQueues {
no_pt_source: AttentionMovie[];
needs_decision: AttentionMovie[];
tv_no_pt_source: SeriesAttention[];
tv_needs_decision: SeriesAttention[];
}
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 all four lanes — the rail badge's number. */
export function attentionTotal(queues: AttentionQueues): number {
return (
queues.no_pt_source.length +
queues.needs_decision.length +
queues.tv_no_pt_source.length +
queues.tv_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}`;
}
}