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:
parent
77c937888a
commit
2a1a55401e
@ -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 });
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -5,6 +5,7 @@ import type {
|
||||
AllDebridHostInfo,
|
||||
AppSettings,
|
||||
AppTheme,
|
||||
AudioStripSummary,
|
||||
BandwidthScheduleEntry,
|
||||
DebugSetupCheckResult,
|
||||
DebridFallbackProvider,
|
||||
@ -984,6 +985,23 @@ function extractHoster(url: string): string {
|
||||
} catch { return ""; }
|
||||
}
|
||||
|
||||
function formatAudioStripSummary(summary: AudioStripSummary): { text: string; tooltip: string; attention: boolean } {
|
||||
const parts: string[] = [];
|
||||
const ok = summary.remuxed + summary.keptSingle;
|
||||
if (ok > 0) parts.push(`${ok} OK`);
|
||||
if (summary.skippedNoGerman > 0) parts.push(`${summary.skippedNoGerman} ohne DE-Tag`);
|
||||
if (summary.skippedNoTool > 0) parts.push("ffmpeg fehlt");
|
||||
if (summary.failed > 0) parts.push(`${summary.failed} Fehler`);
|
||||
const tooltip = summary.files
|
||||
.map((f) => `${f.name}: ${f.action} (${f.reason}${f.languages ? `, Spuren: ${f.languages}` : ""})`)
|
||||
.join("\n");
|
||||
return {
|
||||
text: `Tonspur: ${parts.join(" · ") || "—"}`,
|
||||
tooltip,
|
||||
attention: summary.skippedNoGerman > 0 || summary.skippedNoTool > 0 || summary.failed > 0
|
||||
};
|
||||
}
|
||||
|
||||
const settingsSubTabs: { key: SettingsSubTab; label: string }[] = [
|
||||
{ key: "allgemein", label: "Allgemein" },
|
||||
{ key: "accounts", label: "Accounts" },
|
||||
@ -6594,9 +6612,12 @@ const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, stripe
|
||||
case "prio": return (
|
||||
<span key={col} className={`pkg-col pkg-col-prio${pkg.priority === "high" ? " prio-high" : pkg.priority === "low" ? " prio-low" : ""}`}>{pkg.priority === "high" ? "Hoch" : pkg.priority === "low" ? "Niedrig" : ""}</span>
|
||||
);
|
||||
case "status": return (
|
||||
<span key={col} className="pkg-col pkg-col-status">[{done}/{total}{done === total && total > 0 ? " - Done" : ""}{failed > 0 ? ` | ${failed} Fehler` : ""}{cancelled > 0 ? ` | ${cancelled} abgebr.` : ""}]{pkg.postProcessLabel ? ` - ${pkg.postProcessLabel}` : ""}</span>
|
||||
case "status": {
|
||||
const audioStrip = pkg.audioStripSummary ? formatAudioStripSummary(pkg.audioStripSummary) : null;
|
||||
return (
|
||||
<span key={col} className="pkg-col pkg-col-status">[{done}/{total}{done === total && total > 0 ? " - Done" : ""}{failed > 0 ? ` | ${failed} Fehler` : ""}{cancelled > 0 ? ` | ${cancelled} abgebr.` : ""}]{pkg.postProcessLabel ? ` - ${pkg.postProcessLabel}` : ""}{audioStrip ? <span className={`pkg-audio-strip${audioStrip.attention ? " pkg-audio-strip-warn" : ""}`} title={audioStrip.tooltip}>{` · ${audioStrip.text}`}</span> : null}</span>
|
||||
);
|
||||
}
|
||||
case "speed": return (
|
||||
<span key={col} className="pkg-col pkg-col-speed">{packageSpeed > 0 ? formatSpeedMbps(packageSpeed) : ""}</span>
|
||||
);
|
||||
|
||||
@ -736,6 +736,14 @@ body,
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.pkg-audio-strip {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.pkg-audio-strip-warn {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.progress-size {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
@ -181,6 +181,24 @@ export interface DownloadItem {
|
||||
onlineStatus?: "online" | "offline" | "checking";
|
||||
}
|
||||
|
||||
export interface AudioStripFileResult {
|
||||
name: string;
|
||||
action: string;
|
||||
reason: string;
|
||||
languages?: string;
|
||||
}
|
||||
|
||||
export interface AudioStripSummary {
|
||||
at: number;
|
||||
candidates: number;
|
||||
remuxed: number;
|
||||
keptSingle: number;
|
||||
skippedNoGerman: number;
|
||||
skippedNoTool: number;
|
||||
failed: number;
|
||||
files: AudioStripFileResult[];
|
||||
}
|
||||
|
||||
export interface PackageEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
@ -192,6 +210,7 @@ export interface PackageEntry {
|
||||
enabled: boolean;
|
||||
priority?: PackagePriority;
|
||||
postProcessLabel?: string;
|
||||
audioStripSummary?: AudioStripSummary;
|
||||
downloadStartedAt?: number;
|
||||
downloadCompletedAt?: number;
|
||||
createdAt: number;
|
||||
|
||||
@ -146,5 +146,20 @@ describe("keepGermanAudioOnly integration", () => {
|
||||
expect(n).toBe(0);
|
||||
expect(mockedProcess).not.toHaveBeenCalled(); // bailed before touching any file
|
||||
expect(fs.readdirSync(extractDir)).toContain(DL_MKV); // untouched
|
||||
expect(pkg.audioStripSummary).toMatchObject({ candidates: 1, skippedNoTool: 1, remuxed: 0 });
|
||||
expect(pkg.audioStripSummary.files[0]).toMatchObject({ name: DL_MKV, action: "skipped-no-tool" });
|
||||
});
|
||||
|
||||
it("stores a per-package summary with counts and file details", async () => {
|
||||
const { extractDir, manager, pkg } = setup(true);
|
||||
stage(extractDir);
|
||||
mockedProcess.mockResolvedValue({ action: "skipped-no-german", reason: "no-german-track", totalAudioTracks: 2, audioLanguages: ["eng", "fre"] } as VideoProcessResult);
|
||||
|
||||
await (manager as any).keepGermanAudioOnlyImpl(extractDir, pkg);
|
||||
|
||||
expect(pkg.audioStripSummary).toMatchObject({ candidates: 1, skippedNoGerman: 1, remuxed: 0, failed: 0 });
|
||||
expect(pkg.audioStripSummary.files).toHaveLength(1);
|
||||
expect(pkg.audioStripSummary.files[0]).toMatchObject({ name: DL_MKV, action: "skipped-no-german", reason: "no-german-track", languages: "eng,fre" });
|
||||
expect(pkg.updatedAt).toBeGreaterThan(0); // bumped so the snapshot delta picks it up
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user