remove review badges, add help page, auto-rename noop items
Build and Push Docker Image / build (push) Successful in 1m52s

Three threads:

1. Drop the " Auto-approve" / " Needs decision" pills on PipelineCard
   and the "N auto · M need decisions" subtitle on ReviewColumn — noise
   for a workflow that wants to be unattended. Card amber tint stays as
   a softer cue. Remove the now-unused reviewManualCount field on the
   pipeline payload.

2. New /help route in the nav. Documents what netfelix actually does
   end-to-end, the folder/SxxExx/ID brackets we require, and that the
   codec/quality/audio brackets are *arr's job — we trigger their rename
   API instead of parsing them ourselves. Links to TRaSH guides.

3. Refactor triggerMovieRename / triggerSeriesRename to return a
   basename → new-basename map instead of one path. Add a batched
   triggerRenameFor in execute.ts that dedupes by movie and by series
   (one Sonarr call covers every episode of a series). Hook into
   processInbox: when an item becomes noop, fire a rename pass so
   lying filenames on already-clean files self-heal. Idempotent —
   *arr returns no work to do when names already match.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 08:14:10 +02:00
parent 0e2f027f7a
commit 5d0af08b79
12 changed files with 261 additions and 119 deletions
+64 -21
View File
@@ -593,37 +593,80 @@ export function parseFFmpegProgress(line: string): number | null {
return h * 3600 + m * 60 + s;
}
// ─── Post-job Radarr/Sonarr rename ───────────────────────────────────────────
// ─── Radarr/Sonarr rename ─────────────────────────────────────────────────────
/**
* After a successful FFmpeg job, tell Radarr/Sonarr to rescan the file and
* rename it according to its naming convention. If the path changes, update
* our media_items row so subsequent operations use the correct path.
* Trigger Radarr/Sonarr rename for the given items, deduplicated: one call
* per movie, one call per series (covers every episode of that series in
* one shot). After each call, updates file_path on every media_items row
* whose basename was renamed.
*
* Idempotent — *arr returns no work to do when filenames already match.
* Used after a job completes (fix-up post-transcode) and after processInbox
* classifies items as noop (catch lying filenames on already-clean files).
*/
async function triggerPostJobRename(itemId: number): Promise<void> {
async function triggerRenameFor(itemIds: number[]): Promise<void> {
if (itemIds.length === 0) return;
const db = getDb();
const item = db.prepare("SELECT * FROM media_items WHERE id = ?").get(itemId) as MediaItem | undefined;
if (!item) return;
const placeholders = itemIds.map(() => "?").join(",");
const items = db
.prepare(`SELECT id, type, file_path, tmdb_id, imdb_id, tvdb_id FROM media_items WHERE id IN (${placeholders})`)
.all(...itemIds) as Pick<MediaItem, "id" | "type" | "file_path" | "tmdb_id" | "imdb_id" | "tvdb_id">[];
let result: { ok: boolean; newPath?: string; error?: string };
if (item.type === "Movie") {
const cfg: radarr.RadarrConfig = { url: getConfig("radarr_url") ?? "", apiKey: getConfig("radarr_api_key") ?? "" };
result = await radarr.triggerMovieRename(cfg, { tmdbId: item.tmdb_id, imdbId: item.imdb_id }, item.file_path);
} else {
const cfg: sonarr.SonarrConfig = { url: getConfig("sonarr_url") ?? "", apiKey: getConfig("sonarr_api_key") ?? "" };
result = await sonarr.triggerSeriesRename(cfg, { tvdbId: item.tvdb_id }, item.file_path);
const movies = items.filter((i) => i.type === "Movie");
const seriesByTvdb = new Map<string, typeof items>();
for (const ep of items.filter((i) => i.type === "Episode")) {
if (!ep.tvdb_id) continue;
const list = seriesByTvdb.get(ep.tvdb_id) ?? [];
list.push(ep);
seriesByTvdb.set(ep.tvdb_id, list);
}
if (!result.ok) {
warn(`Rename for item ${itemId}: ${result.error}`);
return;
const radarrCfg: radarr.RadarrConfig = { url: getConfig("radarr_url") ?? "", apiKey: getConfig("radarr_api_key") ?? "" };
const sonarrCfg: sonarr.SonarrConfig = { url: getConfig("sonarr_url") ?? "", apiKey: getConfig("sonarr_api_key") ?? "" };
for (const movie of movies) {
const result = await radarr.triggerMovieRename(radarrCfg, { tmdbId: movie.tmdb_id, imdbId: movie.imdb_id });
if (!result.ok) {
warn(`Rename for movie ${movie.id}: ${result.error}`);
continue;
}
applyRenamesToDb(db, result.renames);
}
if (result.newPath && result.newPath !== item.file_path) {
db.prepare("UPDATE media_items SET file_path = ? WHERE id = ?").run(result.newPath, itemId);
log(`Item ${itemId} renamed: ${item.file_path}${result.newPath}`);
for (const tvdbId of seriesByTvdb.keys()) {
const result = await sonarr.triggerSeriesRename(sonarrCfg, { tvdbId });
if (!result.ok) {
warn(`Rename for series tvdb=${tvdbId}: ${result.error}`);
continue;
}
applyRenamesToDb(db, result.renames);
}
}
function applyRenamesToDb(db: ReturnType<typeof getDb>, renames: Map<string, string>): void {
if (renames.size === 0) return;
const sel = db.prepare("SELECT id, file_path FROM media_items WHERE file_path LIKE ?");
const upd = db.prepare("UPDATE media_items SET file_path = ? WHERE id = ?");
for (const [oldName, newName] of renames) {
if (oldName === newName) continue;
const rows = sel.all(`%/${oldName}`) as { id: number; file_path: string }[];
for (const r of rows) {
const newPath = `${r.file_path.slice(0, r.file_path.length - oldName.length)}${newName}`;
upd.run(newPath, r.id);
log(`Item ${r.id} renamed: ${r.file_path}${newPath}`);
}
}
}
/** Fire-and-forget rename trigger. Errors are logged, never thrown. */
export function triggerRenameForItems(itemIds: number[]): void {
triggerRenameFor(itemIds).catch((e) => warn(`Rename batch failed: ${e}`));
}
/** Single-item helper used by the per-job post-success path. */
function triggerPostJobRename(itemId: number): Promise<void> {
return triggerRenameFor([itemId]);
}
export default app;