show processing reasons as pills on pipeline cards
Build and Push Docker Image / build (push) Successful in 1m1s
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:
+22
-26
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user