diff --git a/package.json b/package.json index 3860f67..735f563 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "netfelix-audio-fix", - "version": "2026.04.21.12", + "version": "2026.04.21.13", "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 486b1bb..a526698 100644 --- a/server/api/review.ts +++ b/server/api/review.ts @@ -1101,9 +1101,8 @@ app.post("/approve-batch", async (c) => { let processInboxAbort: AbortController | null = null; /** - * Single entry point for launching processInbox. Manages the abort controller - * so every caller (manual button, auto-process toggle, post-scan auto-process) - * can be stopped via the /process-inbox/stop endpoint. + * One-shot trigger for the manual "Process Inbox →" button. Manages the abort + * controller so in-progress runs can be stopped via /process-inbox/stop. * Returns false if a run is already in progress. */ export function startProcessInbox(): boolean { @@ -1134,6 +1133,67 @@ export function stopProcessInbox(): boolean { return false; } +// ─── Auto-process loop ───────────────────────────────────────────────────── +// Continuous polling loop: while enabled, checks every few seconds for +// unsorted inbox items and processes them. The manual button is a one-shot; +// this is the "always on" counterpart. +const AUTO_PROCESS_POLL_MS = 5_000; +let autoProcessTimer: Timer | null = null; + +function scheduleAutoProcessTick() { + if (autoProcessTimer) return; + if (getConfig("auto_processing") !== "1") return; + + autoProcessTimer = setTimeout(autoProcessTick, AUTO_PROCESS_POLL_MS); +} + +async function autoProcessTick() { + autoProcessTimer = null; + if (getConfig("auto_processing") !== "1") return; + + // Don't overlap with a manual run + if (processInboxAbort) { + scheduleAutoProcessTick(); + return; + } + + const db = getDb(); + const { n } = db + .prepare("SELECT COUNT(*) as n FROM review_plans WHERE status = 'pending' AND is_noop = 0 AND sorted = 0") + .get() as { n: number }; + + if (n > 0) { + processInboxAbort = new AbortController(); + const { signal } = processInboxAbort; + + try { + const result = await processInbox(db, getAudioLanguages(), undefined, { + onStart: emitInboxSortStart, + onProgress: emitInboxSortProgress, + signal, + }); + emitInboxSorted(result); + } catch { + emitInboxSorted({ moved_to_queue: 0, moved_to_review: 0 }); + } finally { + processInboxAbort = null; + } + } + + scheduleAutoProcessTick(); +} + +export function startAutoProcessLoop() { + scheduleAutoProcessTick(); +} + +export function stopAutoProcessLoop() { + if (autoProcessTimer) { + clearTimeout(autoProcessTimer); + autoProcessTimer = null; + } +} + app.post("/process-inbox", async (c) => { if (!startProcessInbox()) { return c.json({ ok: false, error: "processing already running" }, 409); @@ -1534,10 +1594,7 @@ app.post("/:id/rescan", async (c) => { // Delete pending jobs db.prepare("DELETE FROM jobs WHERE item_id = ? AND status = 'pending'").run(id); - // Auto-process if enabled (processInbox handles language resolution + reanalysis) - if (getConfig("auto_processing") === "1") { - await processInbox(db, getAudioLanguages()); - } + // Auto-process loop (if enabled) picks up the reset item automatically. emitPipelineChanged(); return c.json({ ok: true, inInbox: true }); @@ -1571,10 +1628,7 @@ app.post("/rescan-series", async (c) => { db.prepare("DELETE FROM jobs WHERE item_id = ? AND status = 'pending'").run(item.id); } - // Auto-process if enabled - if (getConfig("auto_processing") === "1") { - await processInbox(db, getAudioLanguages()); - } + // Auto-process loop (if enabled) picks up reset items automatically. emitPipelineChanged(); return c.json({ ok: true, count: items.length }); diff --git a/server/api/scan.ts b/server/api/scan.ts index d8999d3..ed675d3 100644 --- a/server/api/scan.ts +++ b/server/api/scan.ts @@ -233,11 +233,7 @@ async function runScan(limit: number | null = null): Promise { setConfig("scan_running", "0"); log(`Scan complete: ${processed} scanned, ${errors} errors`); emitSse("complete", { scanned: processed, total, errors }); - - if (getConfig("auto_processing") === "1") { - const { startProcessInbox } = await import("./review"); - startProcessInbox(); - } + // Auto-process loop (if enabled) picks up new inbox items automatically. } export default app; diff --git a/server/api/settings.ts b/server/api/settings.ts index 2bb18dd..f13ee8f 100644 --- a/server/api/settings.ts +++ b/server/api/settings.ts @@ -79,9 +79,9 @@ app.post("/audio-languages", async (c) => { return c.json({ ok: true }); }); -// Toggle the auto-processing flag. When flipped on, trigger a one-shot -// sort-inbox pass so existing Inbox items drain immediately without waiting -// for the next scan. +// Toggle the auto-processing flag. When flipped on, start a continuous +// polling loop that monitors the inbox and processes items as they arrive. +// When flipped off, stop the loop and abort any in-progress auto-run. app.post("/auto-processing", async (c) => { const body = await c.req.json<{ enabled?: unknown }>().catch(() => ({ enabled: null })); if (typeof body.enabled !== "boolean") { @@ -90,10 +90,11 @@ app.post("/auto-processing", async (c) => { setConfig("auto_processing", body.enabled ? "1" : "0"); if (body.enabled) { - const { startProcessInbox } = await import("./review"); - startProcessInbox(); + const { startAutoProcessLoop } = await import("./review"); + startAutoProcessLoop(); } else { - const { stopProcessInbox } = await import("./review"); + const { stopAutoProcessLoop, stopProcessInbox } = await import("./review"); + stopAutoProcessLoop(); stopProcessInbox(); } return c.json({ ok: true, enabled: body.enabled }); diff --git a/server/index.tsx b/server/index.tsx index afb7181..df789f5 100644 --- a/server/index.tsx +++ b/server/index.tsx @@ -7,7 +7,7 @@ import pathsRoutes from "./api/paths"; import reviewRoutes from "./api/review"; import scanRoutes from "./api/scan"; import settingsRoutes from "./api/settings"; -import { getDb } from "./db/index"; +import { getConfig, getDb } from "./db/index"; import { log } from "./lib/log"; const app = new Hono(); @@ -66,6 +66,11 @@ log(`netfelix-audio-fix v${pkg.version} starting on http://localhost:${port}`); getDb(); +// Resume auto-process loop if it was enabled before the server restarted +if (getConfig("auto_processing") === "1") { + import("./api/review").then(({ startAutoProcessLoop }) => startAutoProcessLoop()); +} + export default { port, fetch: app.fetch,