stream auto review progress over SSE so large inboxes don't feel frozen
Build and Push Docker Image / build (push) Successful in 1m18s
Build and Push Docker Image / build (push) Successful in 1m18s
sortInbox is now async, yields every 10 items, and emits inbox_sort_start + inbox_sort_progress via optional hooks. the pipeline route handler wires those hooks to the existing job events stream and guards against a second concurrent sort with a 409. the inbox column swaps its Auto Review button for a live "Sorting N/T" counter and progress bar while the sort is in flight; the auto-process toggle hides to give the progress the full subtitle line. the previous behaviour was a frozen button for the entire duration of the sort. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -62,7 +62,7 @@ function seedItem(db: Database, opts: SeedOpts): void {
|
||||
}
|
||||
|
||||
describe("sortInbox", () => {
|
||||
test("authoritative OG with only OG-language audio → auto → queue", () => {
|
||||
test("authoritative OG with only OG-language audio → auto → queue", async () => {
|
||||
const db = makeDb();
|
||||
seedItem(db, {
|
||||
id: 1,
|
||||
@@ -71,7 +71,7 @@ describe("sortInbox", () => {
|
||||
audio: [{ stream_index: 1, language: "eng" }],
|
||||
});
|
||||
|
||||
const result = sortInbox(db, []);
|
||||
const result = await sortInbox(db, []);
|
||||
|
||||
expect(result.moved_to_queue).toBe(1);
|
||||
expect(result.moved_to_review).toBe(0);
|
||||
@@ -87,7 +87,7 @@ describe("sortInbox", () => {
|
||||
expect(job.status).toBe("pending");
|
||||
});
|
||||
|
||||
test("commentary track triggers auto_heuristic → review, no job", () => {
|
||||
test("commentary track triggers auto_heuristic → review, no job", async () => {
|
||||
const db = makeDb();
|
||||
seedItem(db, {
|
||||
id: 1,
|
||||
@@ -99,7 +99,7 @@ describe("sortInbox", () => {
|
||||
],
|
||||
});
|
||||
|
||||
const result = sortInbox(db, []);
|
||||
const result = await sortInbox(db, []);
|
||||
|
||||
expect(result.moved_to_queue).toBe(0);
|
||||
expect(result.moved_to_review).toBe(1);
|
||||
@@ -115,7 +115,7 @@ describe("sortInbox", () => {
|
||||
expect(jobCount).toBe(0);
|
||||
});
|
||||
|
||||
test("missing authoritative OG → manual → review, no job", () => {
|
||||
test("missing authoritative OG → manual → review, no job", async () => {
|
||||
const db = makeDb();
|
||||
seedItem(db, {
|
||||
id: 1,
|
||||
@@ -125,7 +125,7 @@ describe("sortInbox", () => {
|
||||
audio: [{ stream_index: 1, language: "eng" }],
|
||||
});
|
||||
|
||||
const result = sortInbox(db, []);
|
||||
const result = await sortInbox(db, []);
|
||||
|
||||
expect(result.moved_to_review).toBe(1);
|
||||
const plan = db.prepare("SELECT status, sorted, auto_class FROM review_plans WHERE item_id = 1").get() as {
|
||||
@@ -138,7 +138,7 @@ describe("sortInbox", () => {
|
||||
expect(plan.auto_class).toBe("manual");
|
||||
});
|
||||
|
||||
test("already sorted plans are untouched", () => {
|
||||
test("already sorted plans are untouched", async () => {
|
||||
const db = makeDb();
|
||||
seedItem(db, {
|
||||
id: 1,
|
||||
@@ -148,7 +148,7 @@ describe("sortInbox", () => {
|
||||
});
|
||||
db.prepare("UPDATE review_plans SET sorted = 1 WHERE item_id = 1").run();
|
||||
|
||||
const result = sortInbox(db, []);
|
||||
const result = await sortInbox(db, []);
|
||||
|
||||
expect(result.moved_to_queue).toBe(0);
|
||||
expect(result.moved_to_review).toBe(0);
|
||||
@@ -158,7 +158,7 @@ describe("sortInbox", () => {
|
||||
// followed by "back to inbox" + "auto review" left the old decisions in
|
||||
// place. sortInbox must re-run the analyzer against the current config
|
||||
// so the dropped language is actually removed this time around.
|
||||
test("reanalyzes on each run → honors current audio_languages", () => {
|
||||
test("reanalyzes on each run → honors current audio_languages", async () => {
|
||||
const db = makeDb();
|
||||
seedItem(db, {
|
||||
id: 1,
|
||||
@@ -172,7 +172,7 @@ describe("sortInbox", () => {
|
||||
|
||||
// First pass: user had "keep German" on, so German is kept and the
|
||||
// plan auto-queues with both tracks preserved.
|
||||
const firstPass = sortInbox(db, ["deu"]);
|
||||
const firstPass = await sortInbox(db, ["deu"]);
|
||||
expect(firstPass.moved_to_queue).toBe(1);
|
||||
const firstActions = db
|
||||
.prepare(`
|
||||
@@ -194,7 +194,7 @@ describe("sortInbox", () => {
|
||||
db.prepare("UPDATE review_plans SET status = 'pending', sorted = 0, reviewed_at = NULL WHERE item_id = 1").run();
|
||||
db.prepare("DELETE FROM jobs WHERE item_id = 1").run();
|
||||
|
||||
const secondPass = sortInbox(db, []);
|
||||
const secondPass = await sortInbox(db, []);
|
||||
expect(secondPass.moved_to_queue).toBe(1);
|
||||
const secondActions = db
|
||||
.prepare(`
|
||||
@@ -220,4 +220,30 @@ describe("sortInbox", () => {
|
||||
expect(job?.command).toContain("-map 0:a:0");
|
||||
expect(job?.command).not.toContain("-map 0:a:1");
|
||||
});
|
||||
|
||||
test("emits start + progress hooks once per item", async () => {
|
||||
const db = makeDb();
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
seedItem(db, {
|
||||
id: i,
|
||||
origLang: "eng",
|
||||
origLangSource: "radarr",
|
||||
audio: [{ stream_index: 1, language: "eng" }],
|
||||
});
|
||||
}
|
||||
|
||||
const startCalls: number[] = [];
|
||||
const progressCalls: Array<{ processed: number; total: number }> = [];
|
||||
await sortInbox(db, [], {
|
||||
onStart: (total) => startCalls.push(total),
|
||||
onProgress: (processed, total) => progressCalls.push({ processed, total }),
|
||||
});
|
||||
|
||||
expect(startCalls).toEqual([3]);
|
||||
expect(progressCalls).toEqual([
|
||||
{ processed: 1, total: 3 },
|
||||
{ processed: 2, total: 3 },
|
||||
{ processed: 3, total: 3 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user