Feature: Tonspur-Ergebnis sichtbar am Paket (audioStripSummary)

Die Antwort auf "warum hat Paket X noch .DL.?" steht bisher nur in den
Rename-/Item-Logs — "kein Deutsch-Tag" ist INFO-Level und taucht nirgends im
UI auf. Jetzt speichert keepGermanAudioOnlyImpl pro Paket eine Zusammenfassung
(remuxed/kept-single/ohne-DE-Tag/ffmpeg-fehlt/Fehler + bis zu 100 Datei-Details
mit Aktion, Grund und erkannten Sprachen) direkt am PackageEntry:

- Status-Spalte zeigt "Tonspur: 5 OK / 1 ohne DE-Tag / ffmpeg fehlt" (rot bei
  Auffaelligkeiten, flacher Stil), Datei-Details als Tooltip.
- Auch der ffmpeg-nicht-gefunden-Fruehausstieg schreibt die Summary.
- pkg.updatedAt wird gesetzt + Feld im Paket-Delta-Hash, damit der Snapshot
  die Aenderung pusht; normalizeLoadedSession whitelistet das Feld mit
  Shape-Validierung, sonst waere es nach jedem App-Neustart weg.
- 2 neue Integrationstests (Summary-Zaehler + ffmpeg-fehlt-Pfad).
This commit is contained in:
Sucukdeluxe
2026-06-09 20:42:22 +02:00
parent 77c937888a
commit 2a1a55401e
6 changed files with 140 additions and 5 deletions
+41 -1
View File
@@ -7,6 +7,7 @@ import {
AllDebridHostInfo,
AppSettings,
DebridProvider,
AudioStripSummary,
DownloadItem,
DownloadStats,
DownloadSummary,
@@ -2189,7 +2190,7 @@ export class DownloadManager extends EventEmitter {
}
private buildPackageHash(pkg: PackageEntry): string {
return `${pkg.updatedAt}|${pkg.status}|${pkg.name}|${pkg.enabled ? 1 : 0}|${pkg.cancelled ? 1 : 0}|${pkg.priority || ""}|${pkg.itemIds.length}|${pkg.postProcessLabel || ""}`;
return `${pkg.updatedAt}|${pkg.status}|${pkg.name}|${pkg.enabled ? 1 : 0}|${pkg.cancelled ? 1 : 0}|${pkg.priority || ""}|${pkg.itemIds.length}|${pkg.postProcessLabel || ""}|${pkg.audioStripSummary?.at || 0}`;
}
public getSnapshotForEmit(forceFull = false): UiSnapshot {
@@ -3951,6 +3952,28 @@ export class DownloadManager extends EventEmitter {
this.logRenameProcess(pkg, "INFO", "audio-strip", "Tonspur-Bereinigung gestartet", { extractDir, candidates: targets.length, mode: this.settings.germanAudioMode });
}
const summary: AudioStripSummary = {
at: nowMs(),
candidates: targets.length,
remuxed: 0,
keptSingle: 0,
skippedNoGerman: 0,
skippedNoTool: 0,
failed: 0,
files: []
};
const recordFile = (name: string, action: string, reason: string, languages?: string): void => {
if (summary.files.length < 100) {
summary.files.push({ name, action, reason, ...(languages ? { languages } : {}) });
}
};
const writeSummary = (): void => {
if (pkg) {
pkg.audioStripSummary = summary;
pkg.updatedAt = nowMs();
}
};
// Resolve ffmpeg/ffprobe ONCE up front and log it loudly — a missing tool is
// the single most common reason the whole step silently does nothing.
const tooling = await resolveVideoTooling();
@@ -3959,6 +3982,11 @@ export class DownloadManager extends EventEmitter {
if (pkg) {
this.logRenameProcess(pkg, "WARN", "audio-strip", "Tonspur-Bereinigung uebersprungen: ffmpeg/ffprobe nicht gefunden", { candidates: targets.length });
}
summary.skippedNoTool = targets.length;
for (const p of targets) {
recordFile(path.basename(p), "skipped-no-tool", "ffmpeg/ffprobe nicht gefunden");
}
writeSummary();
return 0;
}
logger.info(`Tonspur-Bereinigung: ffmpeg=${tooling.ffmpeg} ffprobe=${tooling.ffprobe}`);
@@ -3992,18 +4020,29 @@ export class DownloadManager extends EventEmitter {
...(result.error ? { error: result.error } : {})
}, resolved.item, resolved.matchedBy);
}
recordFile(sourceName, result.action, result.error ? `${result.reason}${result.error}` : result.reason, langs || undefined);
// Per-file main-log lines so the cause of any unprocessed file is visible
// without opening the rename/item logs.
if (result.action === "error") {
failed += 1;
summary.failed += 1;
logger.warn(`Tonspur-Bereinigung FEHLER: ${sourceName}${result.reason}${result.error ? `${result.error}` : ""} (Spuren: ${langs || "?"}, ${result.totalAudioTracks ?? "?"} Audio)`);
} else if (result.action === "remuxed") {
processed += 1;
summary.remuxed += 1;
logger.info(`Tonspur-Bereinigung OK: ${sourceName} — Spur ${result.keptTrackIndex} behalten (${langs || "?"})`);
} else if (result.action === "kept-single") {
summary.keptSingle += 1;
} else if (result.action === "skipped-no-german") {
summary.skippedNoGerman += 1;
logger.info(`Tonspur-Bereinigung uebersprungen (kein Deutsch-Tag, Spuren: ${langs || "?"}): ${sourceName}`);
} else if (result.action === "skipped-no-space") {
summary.failed += 1;
logger.warn(`Tonspur-Bereinigung uebersprungen (zu wenig Speicher): ${sourceName}`);
} else if (result.action === "skipped-no-tool") {
summary.skippedNoTool += 1;
} else {
summary.failed += 1;
}
// Only strip ".DL." once the file is confirmed German-only (remuxed) or
// already single-track. Skips/errors leave the file fully untouched so the
@@ -4012,6 +4051,7 @@ export class DownloadManager extends EventEmitter {
await this.stripDualLangFromFileName(sourcePath, pkg);
}
}
writeSummary();
logger.info(`Tonspur-Bereinigung fertig: ${processed} verarbeitet, ${failed} Fehler von ${targets.length} Kandidaten in ${extractDir}`);
if (pkg) {
this.logRenameProcess(pkg, failed > 0 ? "WARN" : "INFO", "audio-strip", "Tonspur-Bereinigung fertig", { processed, failed, candidates: targets.length });
+33 -1
View File
@@ -3,7 +3,7 @@ import fsp from "node:fs/promises";
import path from "node:path";
import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys";
import { getMegaDebridAccountIds } from "../shared/mega-debrid-accounts";
import { AppSettings, BandwidthScheduleEntry, DebridAccountStatus, DebridFallbackProvider, DebridProvider, DownloadItem, DownloadStatus, HistoryEntry, HistoryRetentionMode, PackageEntry, PackagePriority, SessionState } from "../shared/types";
import { AppSettings, AudioStripSummary, BandwidthScheduleEntry, DebridAccountStatus, DebridFallbackProvider, DebridProvider, DownloadItem, DownloadStatus, HistoryEntry, HistoryRetentionMode, PackageEntry, PackagePriority, SessionState } from "../shared/types";
import { getProviderUsageDayKey } from "../shared/provider-daily-limits";
import { defaultSettings } from "./constants";
import { logger } from "./logger";
@@ -589,6 +589,37 @@ function asRecord(value: unknown): Record<string, unknown> | null {
return value as Record<string, unknown>;
}
function normalizeAudioStripSummary(raw: unknown): AudioStripSummary | undefined {
const parsed = asRecord(raw);
if (!parsed) {
return undefined;
}
const files = Array.isArray(parsed.files)
? parsed.files.slice(0, 100).flatMap((entry) => {
const file = asRecord(entry);
if (!file) {
return [];
}
const name = asText(file.name);
if (!name) {
return [];
}
const languages = asText(file.languages);
return [{ name, action: asText(file.action), reason: asText(file.reason), ...(languages ? { languages } : {}) }];
})
: [];
return {
at: clampNumber(parsed.at, 0, 0, Number.MAX_SAFE_INTEGER),
candidates: clampNumber(parsed.candidates, 0, 0, 1_000_000),
remuxed: clampNumber(parsed.remuxed, 0, 0, 1_000_000),
keptSingle: clampNumber(parsed.keptSingle, 0, 0, 1_000_000),
skippedNoGerman: clampNumber(parsed.skippedNoGerman, 0, 0, 1_000_000),
skippedNoTool: clampNumber(parsed.skippedNoTool, 0, 0, 1_000_000),
failed: clampNumber(parsed.failed, 0, 0, 1_000_000),
files
};
}
function readSettingsFile(filePath: string): AppSettings | null {
try {
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as AppSettings;
@@ -689,6 +720,7 @@ export function normalizeLoadedSession(raw: unknown): SessionState {
cancelled: Boolean(pkg.cancelled),
enabled: pkg.enabled === undefined ? true : Boolean(pkg.enabled),
priority: VALID_PACKAGE_PRIORITIES.has(asText(pkg.priority)) ? asText(pkg.priority) as PackagePriority : "normal",
audioStripSummary: normalizeAudioStripSummary(pkg.audioStripSummary),
downloadStartedAt: clampNumber(pkg.downloadStartedAt, 0, 0, Number.MAX_SAFE_INTEGER),
downloadCompletedAt: clampNumber(pkg.downloadCompletedAt, 0, 0, Number.MAX_SAFE_INTEGER),
createdAt: clampNumber(pkg.createdAt, now, 0, Number.MAX_SAFE_INTEGER),