detect dirty container title and comment, rewrite to canonical form
Build and Push Docker Image / build (push) Successful in 3m57s

Track format.tags.title and format.tags.comment on media_items via a new
containerTitle() helper producing "Name (Year)" for movies and
"Series (Year) - S01E02 - Title" for episodes. Analyzer and
recomputePlanAfterToggle now flag non-canonical container title and
non-empty comment as non-noop ("Fix container title", "Clear comment"),
and verifyDesiredState checks them post-ffmpeg. buildStreamFlags writes
the canonical title and clears comment on every run.

Existing libraries need a rescan to populate the new columns.
This commit is contained in:
2026-04-24 21:45:39 +02:00
parent e6684dd748
commit 748145a372
15 changed files with 350 additions and 38 deletions
+13 -8
View File
@@ -4,7 +4,7 @@ import { getAllConfig, getConfig, getDb } from "../db/index";
import { log, error as logError } from "../lib/log";
import { isOneOf, parseId } from "../lib/validate";
import { analyzeItem, assignTargetOrder } from "../services/analyzer";
import { buildCommand, LANG_NAMES, trackTitle } from "../services/ffmpeg";
import { buildCommand, containerTitle, 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";
@@ -404,12 +404,7 @@ export function reanalyze(
}
const analysis = analyzeItem(
{
original_language: item.original_language,
orig_lang_source: item.orig_lang_source,
needs_review: item.needs_review,
container: item.container,
},
item,
streams,
{ audioLanguages },
languageOverrides.size > 0 ? languageOverrides : undefined,
@@ -530,6 +525,9 @@ function recomputePlanAfterToggle(db: ReturnType<typeof getDb>, itemId: number):
const expected = d.custom_title ?? trackTitle(s, d.custom_language);
return expected != null && s.title !== expected;
});
const expectedContainerTitle = containerTitle(item);
const containerTitleMismatch = (item.container_title ?? null) !== (expectedContainerTitle ?? null);
const containerCommentDirty = !!item.container_comment && item.container_comment.length > 0;
const keptAudio = streams
.filter((s) => s.type === "Audio" && decWithIdx.find((d) => d.stream_id === s.id)?.action === "keep")
@@ -543,7 +541,14 @@ function recomputePlanAfterToggle(db: ReturnType<typeof getDb>, itemId: number):
}
}
const isNoop = !anyAudioRemoved && !audioOrderChanged && !hasSubs && !needsTranscode && !titleMismatch;
const isNoop =
!anyAudioRemoved &&
!audioOrderChanged &&
!hasSubs &&
!needsTranscode &&
!titleMismatch &&
!containerTitleMismatch &&
!containerCommentDirty;
// 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