show processing reasons as pills on pipeline cards
Build and Push Docker Image / build (push) Successful in 1m1s

Analyzer now computes structured reason tags (Remove tracks, Reorder,
Extract subs, Transcode, Fix default, Fix language tag, Fix title) and
stores them as JSON in review_plans.reasons. Pipeline cards show these
as badges next to the copy/transcode pill so users know why a file
needs processing. Replaces the old transcode_reasons computed from
stream_decisions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 11:01:54 +02:00
parent 0a8996dc33
commit 50e1ea66f4
8 changed files with 50 additions and 49 deletions
+22 -26
View File
@@ -396,15 +396,16 @@ export function reanalyze(
db
.prepare(`
INSERT INTO review_plans (item_id, status, is_noop, auto_class, apple_compat, job_type, notes)
VALUES (?, 'pending', ?, ?, ?, ?, ?)
INSERT INTO review_plans (item_id, status, is_noop, auto_class, apple_compat, job_type, notes, reasons)
VALUES (?, 'pending', ?, ?, ?, ?, ?, ?)
ON CONFLICT(item_id) DO UPDATE SET
status = 'pending',
is_noop = excluded.is_noop,
auto_class = excluded.auto_class,
apple_compat = excluded.apple_compat,
job_type = excluded.job_type,
notes = excluded.notes
notes = excluded.notes,
reasons = excluded.reasons
`)
.run(
itemId,
@@ -413,6 +414,7 @@ export function reanalyze(
analysis.apple_compat,
analysis.job_type,
analysis.notes.length > 0 ? analysis.notes.join("\n") : null,
analysis.reasons.length > 0 ? JSON.stringify(analysis.reasons) : null,
);
const plan = db.prepare("SELECT id FROM review_plans WHERE item_id = ?").get(itemId) as { id: number };
@@ -528,35 +530,28 @@ interface PipelineAudioStream {
action: "keep" | "remove";
}
type EnrichableRow = { id?: number; plan_id?: number; item_id: number } & {
transcode_reasons?: string[];
type EnrichableRow = { id?: number; plan_id?: number; item_id: number; reasons_raw?: string | null } & {
reasons?: string[];
audio_streams?: PipelineAudioStream[];
};
/**
* Enrich review/queued rows with transcode-reason badges and pre-checked audio
* streams. Works for both the Review column (where `id` is the plan id) and
* Enrich review/queued rows with reason badges and pre-checked audio streams.
* Reasons come from the stored review_plans.reasons JSON column (set by the
* analyzer). Works for both the Review column (where `id` is the plan id) and
* the Queued column (where `plan_id` is explicit and `id` is the job id).
*/
function enrichWithStreamsAndReasons(db: ReturnType<typeof getDb>, rows: EnrichableRow[]): void {
if (rows.length === 0) return;
const planIdFor = (r: EnrichableRow): number => (r.plan_id ?? r.id) as number;
const planIds = rows.map(planIdFor);
const itemIds = rows.map((r) => r.item_id);
const reasonPh = planIds.map(() => "?").join(",");
const allReasons = db
.prepare(`
SELECT DISTINCT sd.plan_id, ms.codec, sd.transcode_codec
FROM stream_decisions sd
JOIN media_streams ms ON ms.id = sd.stream_id
WHERE sd.plan_id IN (${reasonPh}) AND sd.transcode_codec IS NOT NULL
`)
.all(...planIds) as { plan_id: number; codec: string | null; transcode_codec: string }[];
const reasonsByPlan = new Map<number, string[]>();
for (const r of allReasons) {
if (!reasonsByPlan.has(r.plan_id)) reasonsByPlan.set(r.plan_id, []);
reasonsByPlan.get(r.plan_id)!.push(`${(r.codec ?? "").toUpperCase()}${r.transcode_codec.toUpperCase()}`);
// Parse stored reasons JSON
for (const r of rows) {
try {
r.reasons = r.reasons_raw ? JSON.parse(r.reasons_raw) : [];
} catch {
r.reasons = [];
}
}
const streamPh = itemIds.map(() => "?").join(",");
@@ -595,7 +590,6 @@ function enrichWithStreamsAndReasons(db: ReturnType<typeof getDb>, rows: Enricha
}
for (const r of rows) {
r.transcode_reasons = reasonsByPlan.get(planIdFor(r)) ?? [];
r.audio_streams = streamsByItem.get(r.item_id) ?? [];
}
}
@@ -620,7 +614,8 @@ interface ReviewItemRow {
original_language: string | null;
orig_lang_source: string | null;
file_path: string;
transcode_reasons?: string[];
reasons_raw?: string | null;
reasons?: string[];
audio_streams?: PipelineAudioStream[];
}
@@ -662,7 +657,8 @@ export function buildReviewGroups(
const order = orderClause(opts.sort ?? defaultSort);
const rows = db
.prepare(`
SELECT rp.*, mi.name, mi.series_name, mi.series_key,
SELECT rp.*, rp.reasons as reasons_raw,
mi.name, mi.series_name, mi.series_key,
mi.season_number, mi.episode_number, mi.type, mi.container,
mi.original_language, mi.orig_lang_source, mi.file_path,
mi.last_scanned_at
@@ -838,7 +834,7 @@ app.get("/pipeline", (c) => {
mi.season_number, mi.episode_number, mi.type, mi.container,
mi.original_language, mi.orig_lang_source, mi.file_path,
rp.id as plan_id, rp.job_type, rp.apple_compat,
rp.auto_class, rp.is_noop
rp.auto_class, rp.is_noop, rp.reasons as reasons_raw
FROM jobs j
JOIN media_items mi ON mi.id = j.item_id
JOIN review_plans rp ON rp.item_id = j.item_id