clean media stream titles, verify metadata preflight

This commit is contained in:
2026-04-24 09:51:11 +02:00
parent 42189d95bb
commit 3198002836
15 changed files with 426 additions and 115 deletions
+18 -10
View File
@@ -2,20 +2,20 @@ import { unlinkSync } from "node:fs";
import { Hono } from "hono";
import { getAllConfig, getConfig, getDb } from "../db/index";
import { log, error as logError } from "../lib/log";
import { parsePath } from "../services/path-parser";
import { probeFile } from "../services/probe";
import { upsertScannedItem } from "../services/rescan";
import { isOneOf, parseId } from "../lib/validate";
import { analyzeItem, assignTargetOrder } from "../services/analyzer";
import { buildCommand, LANG_NAMES } from "../services/ffmpeg";
import { buildCommand, LANG_NAMES, trackTitle } from "../services/ffmpeg";
import { type LanguageResolverConfig, resolveLanguage } from "../services/language-resolver";
import { normalizeLanguage } from "../services/language-utils";
import { parsePath } from "../services/path-parser";
import { probeFile } from "../services/probe";
import {
loadLibrary as loadRadarrLibrary,
type RadarrLibrary,
isUsable as radarrUsable,
triggerMovieRefetch,
} from "../services/radarr";
import { upsertScannedItem } from "../services/rescan";
import {
loadLibrary as loadSonarrLibrary,
type SonarrLibrary,
@@ -151,9 +151,9 @@ export async function processInbox(
// Also pick up noop items that have never been analyzed with the reasons
// system (reasons IS NULL). Reanalyze may flip them to non-noop if the
// title/language/default checks now catch something the old code missed.
const staleNoops = db
.prepare("SELECT item_id FROM review_plans WHERE is_noop = 1 AND reasons IS NULL")
.all() as { item_id: number }[];
const staleNoops = db.prepare("SELECT item_id FROM review_plans WHERE is_noop = 1 AND reasons IS NULL").all() as {
item_id: number;
}[];
for (const { item_id } of staleNoops) {
reanalyze(db, item_id, audioLanguages);
}
@@ -483,12 +483,13 @@ function recomputePlanAfterToggle(db: ReturnType<typeof getDb>, itemId: number):
if (!plan) return;
const decisions = db
.prepare(
"SELECT stream_id, action, target_index, custom_language, transcode_codec FROM stream_decisions WHERE plan_id = ?",
"SELECT stream_id, action, target_index, custom_title, custom_language, transcode_codec FROM stream_decisions WHERE plan_id = ?",
)
.all(plan.id) as {
stream_id: number;
action: "keep" | "remove";
target_index: number | null;
custom_title: string | null;
custom_language: string | null;
transcode_codec: string | null;
}[];
@@ -516,12 +517,19 @@ function recomputePlanAfterToggle(db: ReturnType<typeof getDb>, itemId: number):
const updateIdx = db.prepare("UPDATE stream_decisions SET target_index = ? WHERE plan_id = ? AND stream_id = ?");
for (const d of decWithIdx) updateIdx.run(d.target_index, plan.id, d.stream_id);
// Recompute is_noop: audio removed OR reordered OR subs exist OR transcode needed
// Recompute is_noop: audio removed OR reordered OR subs exist OR transcode/metadata needed
const anyAudioRemoved = streams.some(
(s) => s.type === "Audio" && decWithIdx.find((d) => d.stream_id === s.id)?.action === "remove",
);
const hasSubs = streams.some((s) => s.type === "Subtitle");
const needsTranscode = decWithIdx.some((d) => d.transcode_codec != null && d.action === "keep");
const titleMismatch = streams.some((s) => {
if (s.type !== "Video" && s.type !== "Audio") return false;
const d = decisions.find((dec) => dec.stream_id === s.id);
if (d?.action !== "keep") return false;
const expected = d.custom_title ?? trackTitle(s, d.custom_language);
return expected != null && s.title !== expected;
});
const keptAudio = streams
.filter((s) => s.type === "Audio" && decWithIdx.find((d) => d.stream_id === s.id)?.action === "keep")
@@ -535,7 +543,7 @@ function recomputePlanAfterToggle(db: ReturnType<typeof getDb>, itemId: number):
}
}
const isNoop = !anyAudioRemoved && !audioOrderChanged && !hasSubs && !needsTranscode;
const isNoop = !anyAudioRemoved && !audioOrderChanged && !hasSubs && !needsTranscode && !titleMismatch;
// Only flip is_noop to 1 when the plan is unsorted (inbox). If the user is
// actively reviewing a sorted plan, marking all tracks "keep" should NOT