diff --git a/package.json b/package.json index 30582c8..bcca75a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "netfelix-audio-fix", - "version": "2026.04.14", + "version": "2026.04.14.1", "scripts": { "dev:server": "NODE_ENV=development bun --hot server/index.tsx", "dev:client": "vite", diff --git a/server/api/review.ts b/server/api/review.ts index bfeecca..451f136 100644 --- a/server/api/review.ts +++ b/server/api/review.ts @@ -504,6 +504,24 @@ app.post("/approve-all", (c) => { return c.json({ ok: true, count: pending.length }); }); +// ─── Auto-approve high-confidence ──────────────────────────────────────────── +// Approves every pending plan whose original language came from an authoritative +// source (radarr/sonarr). Anything with low confidence keeps needing a human. +app.post("/auto-approve", (c) => { + const db = getDb(); + const pending = db + .prepare( + "SELECT rp.*, mi.id as item_id FROM review_plans rp JOIN media_items mi ON mi.id = rp.item_id WHERE rp.status = 'pending' AND rp.is_noop = 0 AND rp.confidence = 'high'", + ) + .all() as (ReviewPlan & { item_id: number })[]; + for (const plan of pending) { + db.prepare("UPDATE review_plans SET status = 'approved', reviewed_at = datetime('now') WHERE id = ?").run(plan.id); + const { item, streams, decisions } = loadItemDetail(db, plan.item_id); + if (item) enqueueAudioJob(db, plan.item_id, buildCommand(item, streams, decisions)); + } + return c.json({ ok: true, count: pending.length }); +}); + // ─── Detail ─────────────────────────────────────────────────────────────────── app.get("/:id", (c) => { diff --git a/src/features/pipeline/ColumnShell.tsx b/src/features/pipeline/ColumnShell.tsx index 5037fa8..e2533f5 100644 --- a/src/features/pipeline/ColumnShell.tsx +++ b/src/features/pipeline/ColumnShell.tsx @@ -1,37 +1,52 @@ import type { ReactNode } from "react"; +export interface ColumnAction { + label: string; + onClick: () => void; + disabled?: boolean; + danger?: boolean; + primary?: boolean; +} + interface ColumnShellProps { title: string; count: ReactNode; - action?: { label: string; onClick: () => void; disabled?: boolean; danger?: boolean }; + actions?: ColumnAction[]; children: ReactNode; } /** - * Equal-width pipeline column with a header (title + count + optional action button) + * Equal-width pipeline column with a header (title + count + optional action buttons) * and a scrolling body. All four pipeline columns share this shell so widths and * header layout stay consistent. */ -export function ColumnShell({ title, count, action, children }: ColumnShellProps) { +export function ColumnShell({ title, count, actions, children }: ColumnShellProps) { return (
{title} ({count}) - {action && ( - + {actions && actions.length > 0 && ( +
+ {actions.map((a) => ( + + ))} +
)}
{children}
diff --git a/src/features/pipeline/DoneColumn.tsx b/src/features/pipeline/DoneColumn.tsx index ad5619d..9a2ffd7 100644 --- a/src/features/pipeline/DoneColumn.tsx +++ b/src/features/pipeline/DoneColumn.tsx @@ -18,7 +18,7 @@ export function DoneColumn({ items, onMutate }: DoneColumnProps) { 0 ? { label: "Clear", onClick: clear } : undefined} + actions={items.length > 0 ? [{ label: "Clear", onClick: clear }] : undefined} > {items.map((item) => (
diff --git a/src/features/pipeline/ProcessingColumn.tsx b/src/features/pipeline/ProcessingColumn.tsx index 42dcb6c..698a9eb 100644 --- a/src/features/pipeline/ProcessingColumn.tsx +++ b/src/features/pipeline/ProcessingColumn.tsx @@ -51,7 +51,7 @@ export function ProcessingColumn({ items, progress, queueStatus, onMutate }: Pro {queueStatus && queueStatus.status !== "running" && (
diff --git a/src/features/pipeline/QueueColumn.tsx b/src/features/pipeline/QueueColumn.tsx index 83b1bd0..6c0c0bb 100644 --- a/src/features/pipeline/QueueColumn.tsx +++ b/src/features/pipeline/QueueColumn.tsx @@ -19,7 +19,7 @@ export function QueueColumn({ items, onMutate }: QueueColumnProps) { 0 ? { label: "Clear", onClick: clear } : undefined} + actions={items.length > 0 ? [{ label: "Clear", onClick: clear }] : undefined} > {items.map((item) => (
diff --git a/src/features/pipeline/ReviewColumn.tsx b/src/features/pipeline/ReviewColumn.tsx index c923fca..d7f9d19 100644 --- a/src/features/pipeline/ReviewColumn.tsx +++ b/src/features/pipeline/ReviewColumn.tsx @@ -27,6 +27,12 @@ export function ReviewColumn({ items, total, jellyfinUrl, onMutate }: ReviewColu onMutate(); }; + const autoApprove = async () => { + const res = await api.post<{ ok: boolean; count: number }>("/api/review/auto-approve"); + onMutate(); + if (res.count === 0) alert("No high-confidence items to auto-approve."); + }; + const approveItem = async (itemId: number) => { await api.post(`/api/review/${itemId}/approve`); onMutate(); @@ -62,7 +68,14 @@ export function ReviewColumn({ items, total, jellyfinUrl, onMutate }: ReviewColu 0 ? { label: "Skip all", onClick: skipAll } : undefined} + actions={ + total > 0 + ? [ + { label: "Auto Review", onClick: autoApprove, primary: true }, + { label: "Skip all", onClick: skipAll }, + ] + : undefined + } >
{allItems.map((entry) => {