Add dedicated rename support logging

This commit is contained in:
Sucukdeluxe 2026-03-09 04:48:58 +01:00
parent 08cd1c4bf8
commit 55e0ebd0f8
16 changed files with 583 additions and 3 deletions

View File

@ -207,6 +207,7 @@ Runtime files are stored in Electron's `userData` directory, including:
- `rd_history.json` - `rd_history.json`
- `rd_downloader.log` - `rd_downloader.log`
- `audit.log` - `audit.log`
- `rename.log`
- `debug_ai_manifest.json` - `debug_ai_manifest.json`
- `trace.log` - `trace.log`
- `trace_config.json` - `trace_config.json`
@ -214,7 +215,7 @@ Runtime files are stored in Electron's `userData` directory, including:
- `package-logs/package_*.txt` - `package-logs/package_*.txt`
- `item-logs/item_*.txt` - `item-logs/item_*.txt`
`audit.log` and `trace.log` are rotated automatically. The current file is kept plus one `.old` backup, and outdated backups are purged automatically. `audit.log`, `rename.log`, and `trace.log` are rotated automatically. The current file is kept plus one `.old` backup, and outdated backups are purged automatically.
### Remote debug server ### Remote debug server
@ -253,6 +254,7 @@ Available endpoints after restart:
- `GET /log?lines=100&grep=keyword` - `GET /log?lines=100&grep=keyword`
- `GET /logs/main?lines=100&grep=keyword` - `GET /logs/main?lines=100&grep=keyword`
- `GET /logs/audit?lines=100&grep=keyword` - `GET /logs/audit?lines=100&grep=keyword`
- `GET /logs/rename?lines=100&grep=keyword`
- `GET /logs/trace?lines=100&grep=keyword` - `GET /logs/trace?lines=100&grep=keyword`
- `GET /logs/session?lines=100&grep=keyword` - `GET /logs/session?lines=100&grep=keyword`
- `GET /logs/package?package=Release&lines=100&grep=keyword` - `GET /logs/package?package=Release&lines=100&grep=keyword`
@ -277,6 +279,7 @@ Invoke-RestMethod "http://SERVER:9868/history?token=YOUR_TOKEN&limit=20"
Invoke-RestMethod "http://SERVER:9868/debug/setup?token=YOUR_TOKEN" Invoke-RestMethod "http://SERVER:9868/debug/setup?token=YOUR_TOKEN"
Invoke-RestMethod "http://SERVER:9868/self-check?token=YOUR_TOKEN" Invoke-RestMethod "http://SERVER:9868/self-check?token=YOUR_TOKEN"
Invoke-RestMethod "http://SERVER:9868/logs/audit?token=YOUR_TOKEN&lines=200" Invoke-RestMethod "http://SERVER:9868/logs/audit?token=YOUR_TOKEN&lines=200"
Invoke-RestMethod "http://SERVER:9868/logs/rename?token=YOUR_TOKEN&lines=200"
Invoke-RestMethod "http://SERVER:9868/logs/trace?token=YOUR_TOKEN&lines=200" Invoke-RestMethod "http://SERVER:9868/logs/trace?token=YOUR_TOKEN&lines=200"
Invoke-RestMethod "http://SERVER:9868/trace/config?token=YOUR_TOKEN&enable=1&note=support&durationMinutes=120" Invoke-RestMethod "http://SERVER:9868/trace/config?token=YOUR_TOKEN&enable=1&note=support&durationMinutes=120"
Invoke-RestMethod "http://SERVER:9868/logs/package?token=YOUR_TOKEN&package=Release&lines=200" Invoke-RestMethod "http://SERVER:9868/logs/package?token=YOUR_TOKEN&package=Release&lines=200"
@ -285,7 +288,7 @@ Invoke-RestMethod "http://SERVER:9868/host/diagnostics?token=YOUR_TOKEN"
Invoke-WebRequest "http://SERVER:9868/support/bundle?token=YOUR_TOKEN" -OutFile ".\\rd-support-bundle.zip" Invoke-WebRequest "http://SERVER:9868/support/bundle?token=YOUR_TOKEN" -OutFile ".\\rd-support-bundle.zip"
``` ```
This makes it easy to share one URL plus token during support, so current package status, session state, history, redacted account/settings state, audit actions, trace data, package/session/item logs, host-side Windows crash hints, disk space, support-log volume, support-bundle size estimates, and even a full ZIP support bundle can be inspected remotely. This makes it easy to share one URL plus token during support, so current package status, session state, history, redacted account/settings state, audit actions, rename/MKV move traces, trace data, package/session/item logs, host-side Windows crash hints, disk space, support-log volume, support-bundle size estimates, and even a full ZIP support bundle can be inspected remotely.
## Troubleshooting ## Troubleshooting

View File

@ -39,6 +39,7 @@ import { encryptBackup, decryptBackup } from "./backup-crypto";
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log"; import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log";
import { getDebugSetupCheck } from "./debug-setup"; import { getDebugSetupCheck } from "./debug-setup";
import { buildLinkExportSelection, serializeLinkExportText } from "./link-export"; import { buildLinkExportSelection, serializeLinkExportText } from "./link-export";
import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "./rename-log";
import { buildAccountSummary, diffAccountSummary } from "./support-data"; import { buildAccountSummary, diffAccountSummary } from "./support-data";
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle"; import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
import { getTraceConfig, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "./trace-log"; import { getTraceConfig, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "./trace-log";
@ -82,6 +83,7 @@ export class AppController {
initPackageLogs(this.storagePaths.baseDir); initPackageLogs(this.storagePaths.baseDir);
initItemLogs(this.storagePaths.baseDir); initItemLogs(this.storagePaths.baseDir);
initAuditLog(this.storagePaths.baseDir); initAuditLog(this.storagePaths.baseDir);
initRenameLog(this.storagePaths.baseDir);
initTraceLog(this.storagePaths.baseDir); initTraceLog(this.storagePaths.baseDir);
this.settings = loadSettings(this.storagePaths); this.settings = loadSettings(this.storagePaths);
const session = loadSession(this.storagePaths); const session = loadSession(this.storagePaths);
@ -186,6 +188,10 @@ export class AppController {
return getAuditLogPath(); return getAuditLogPath();
} }
public getRenameLogPath(): string | null {
return getRenameLogPath();
}
public getTraceLogPath(): string | null { public getTraceLogPath(): string | null {
return getTraceLogPath(); return getTraceLogPath();
} }
@ -643,6 +649,7 @@ export class AppController {
shutdownSessionLog(); shutdownSessionLog();
shutdownPackageLogs(); shutdownPackageLogs();
shutdownItemLogs(); shutdownItemLogs();
shutdownRenameLog();
this.audit("INFO", "App beendet"); this.audit("INFO", "App beendet");
shutdownTraceLog(); shutdownTraceLog();
shutdownAuditLog(); shutdownAuditLog();

View File

@ -9,6 +9,7 @@ import { logger, getLogFilePath } from "./logger";
import { getItemLogPath as getPersistedItemLogPath } from "./item-log"; import { getItemLogPath as getPersistedItemLogPath } from "./item-log";
import { getSessionLogPath } from "./session-log"; import { getSessionLogPath } from "./session-log";
import { getPackageLogPath as getPersistedPackageLogPath } from "./package-log"; import { getPackageLogPath as getPersistedPackageLogPath } from "./package-log";
import { getRenameLogPath } from "./rename-log";
import { createStoragePaths, loadHistory, loadSettings } from "./storage"; import { createStoragePaths, loadHistory, loadSettings } from "./storage";
import { buildAccountSummary, buildRedactedSettingsPayload, buildStatsPayload, summarizeHistoryEntry } from "./support-data"; import { buildAccountSummary, buildRedactedSettingsPayload, buildStatsPayload, summarizeHistoryEntry } from "./support-data";
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle"; import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
@ -38,6 +39,7 @@ const DEBUG_ENDPOINTS: DebugEndpointDescriptor[] = [
{ method: "GET", path: "/log", queryExample: "lines=100&grep=keyword", description: "Legacy alias for the main application log tail." }, { method: "GET", path: "/log", queryExample: "lines=100&grep=keyword", description: "Legacy alias for the main application log tail." },
{ method: "GET", path: "/logs/main", queryExample: "lines=100&grep=keyword", description: "Reads the main application log tail." }, { method: "GET", path: "/logs/main", queryExample: "lines=100&grep=keyword", description: "Reads the main application log tail." },
{ method: "GET", path: "/logs/audit", queryExample: "lines=100&grep=keyword", description: "Reads the audit log for support-relevant UI and admin actions." }, { method: "GET", path: "/logs/audit", queryExample: "lines=100&grep=keyword", description: "Reads the audit log for support-relevant UI and admin actions." },
{ method: "GET", path: "/logs/rename", queryExample: "lines=100&grep=keyword", description: "Reads the dedicated rename and MKV move log." },
{ method: "GET", path: "/logs/trace", queryExample: "lines=100&grep=keyword", description: "Reads the optional support trace log." }, { method: "GET", path: "/logs/trace", queryExample: "lines=100&grep=keyword", description: "Reads the optional support trace log." },
{ method: "GET", path: "/logs/session", queryExample: "lines=100&grep=keyword", description: "Reads the session log tail." }, { method: "GET", path: "/logs/session", queryExample: "lines=100&grep=keyword", description: "Reads the session log tail." },
{ method: "GET", path: "/logs/package", queryExample: "package=Release&lines=100&grep=keyword", description: "Reads the package log for a specific package name or id." }, { method: "GET", path: "/logs/package", queryExample: "package=Release&lines=100&grep=keyword", description: "Reads the package log for a specific package name or id." },
@ -240,7 +242,7 @@ function buildAiManifest(baseDir: string): Record<string, unknown> {
"If remote access is needed, ask the user only for the server IP or DNS name.", "If remote access is needed, ask the user only for the server IP or DNS name.",
"Call /meta first to confirm the server is reachable and to re-read the endpoint list.", "Call /meta first to confirm the server is reachable and to re-read the endpoint list.",
"Use /self-check or /debug/setup to quickly verify whether token, host, manifest, trace, disk space, and log sizes are in a good support state.", "Use /self-check or /debug/setup to quickly verify whether token, host, manifest, trace, disk space, and log sizes are in a good support state.",
"Use /diagnostics for an overview, then drill into /logs/item, /logs/package, /status, /packages, /items, /settings, /accounts, /stats, /history, or /logs/trace.", "Use /diagnostics for an overview, then drill into /logs/item, /logs/package, /logs/rename, /status, /packages, /items, /settings, /accounts, /stats, /history, or /logs/trace.",
"If a full handoff is needed, download /support/bundle as a ZIP." "If a full handoff is needed, download /support/bundle as a ZIP."
], ],
auth: { auth: {
@ -257,6 +259,7 @@ function buildAiManifest(baseDir: string): Record<string, unknown> {
tokenFile: path.join(baseDir, "debug_token.txt"), tokenFile: path.join(baseDir, "debug_token.txt"),
mainLogFile: getLogFilePath(), mainLogFile: getLogFilePath(),
auditLogFile: getAuditLogPath(), auditLogFile: getAuditLogPath(),
renameLogFile: getRenameLogPath(),
traceLogFile: getTraceLogPath(), traceLogFile: getTraceLogPath(),
traceConfigFile: getTraceConfigPath(), traceConfigFile: getTraceConfigPath(),
sessionLogFile: getSessionLogPath(), sessionLogFile: getSessionLogPath(),
@ -483,6 +486,7 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
logPaths: { logPaths: {
main: getLogFilePath(), main: getLogFilePath(),
audit: getAuditLogPath(), audit: getAuditLogPath(),
rename: getRenameLogPath(),
session: getSessionLogPath(), session: getSessionLogPath(),
trace: getTraceLogPath() trace: getTraceLogPath()
}, },
@ -522,6 +526,19 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
return; return;
} }
if (pathname === "/logs/rename") {
const count = normalizeLinesParam(url.searchParams.get("lines"), 100);
const grep = url.searchParams.get("grep") || "";
const logPath = getRenameLogPath();
const lines = filterLines(readLogTailFromFile(logPath, count), grep);
jsonResponse(res, 200, {
path: logPath,
lines,
count: lines.length
});
return;
}
if (pathname === "/logs/trace") { if (pathname === "/logs/trace") {
const count = normalizeLinesParam(url.searchParams.get("lines"), 100); const count = normalizeLinesParam(url.searchParams.get("lines"), 100);
const grep = url.searchParams.get("grep") || ""; const grep = url.searchParams.get("grep") || "";
@ -847,6 +864,10 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
path: getAuditLogPath(), path: getAuditLogPath(),
lines: filterLines(readLogTailFromFile(getAuditLogPath(), lineCount), grep) lines: filterLines(readLogTailFromFile(getAuditLogPath(), lineCount), grep)
}, },
rename: {
path: getRenameLogPath(),
lines: filterLines(readLogTailFromFile(getRenameLogPath(), lineCount), grep)
},
trace: { trace: {
path: getTraceLogPath(), path: getTraceLogPath(),
config: getTraceConfig(), config: getTraceConfig(),

View File

@ -279,6 +279,8 @@ function getSupportBundleEstimate(
+ Number(logSummary.mainBackup.exists) + Number(logSummary.mainBackup.exists)
+ Number(logSummary.audit.exists) + Number(logSummary.audit.exists)
+ Number(logSummary.auditBackup.exists) + Number(logSummary.auditBackup.exists)
+ Number(logSummary.rename.exists)
+ Number(logSummary.renameBackup.exists)
+ Number(logSummary.session.exists) + Number(logSummary.session.exists)
+ Number(logSummary.trace.exists) + Number(logSummary.trace.exists)
+ Number(logSummary.traceBackup.exists) + Number(logSummary.traceBackup.exists)
@ -317,6 +319,8 @@ export function getDebugSetupCheck(baseDir: string): DebugSetupCheckResult {
mainBackup: getFileSizeInfo(path.join(baseDir, "rd_downloader.log.old")), mainBackup: getFileSizeInfo(path.join(baseDir, "rd_downloader.log.old")),
audit: getFileSizeInfo(path.join(baseDir, "audit.log")), audit: getFileSizeInfo(path.join(baseDir, "audit.log")),
auditBackup: getFileSizeInfo(path.join(baseDir, "audit.log.old")), auditBackup: getFileSizeInfo(path.join(baseDir, "audit.log.old")),
rename: getFileSizeInfo(path.join(baseDir, "rename.log")),
renameBackup: getFileSizeInfo(path.join(baseDir, "rename.log.old")),
session: getFileSizeInfo(sessionLogPath), session: getFileSizeInfo(sessionLogPath),
trace: getFileSizeInfo(traceLogPath), trace: getFileSizeInfo(traceLogPath),
traceBackup: getFileSizeInfo(path.join(baseDir, "trace.log.old")), traceBackup: getFileSizeInfo(path.join(baseDir, "trace.log.old")),
@ -330,6 +334,8 @@ export function getDebugSetupCheck(baseDir: string): DebugSetupCheckResult {
logSummary.mainBackup.bytes, logSummary.mainBackup.bytes,
logSummary.audit.bytes, logSummary.audit.bytes,
logSummary.auditBackup.bytes, logSummary.auditBackup.bytes,
logSummary.rename.bytes,
logSummary.renameBackup.bytes,
logSummary.session.bytes, logSummary.session.bytes,
logSummary.trace.bytes, logSummary.trace.bytes,
logSummary.traceBackup.bytes, logSummary.traceBackup.bytes,

View File

@ -55,6 +55,7 @@ import { validateFileAgainstManifest } from "./integrity";
import { logger } from "./logger"; import { logger } from "./logger";
import { ensureItemLog, getItemLogPath as getPersistedItemLogPath, logItemEvent as writeItemLogEvent } from "./item-log"; import { ensureItemLog, getItemLogPath as getPersistedItemLogPath, logItemEvent as writeItemLogEvent } from "./item-log";
import { ensurePackageLog, getPackageLogPath as getPersistedPackageLogPath, logPackageEvent as writePackageLogEvent } from "./package-log"; import { ensurePackageLog, getPackageLogPath as getPersistedPackageLogPath, logPackageEvent as writePackageLogEvent } from "./package-log";
import { logRenameEvent as writeRenameLogEvent } from "./rename-log";
import { StoragePaths, saveSession, saveSessionAsync, saveSettings, saveSettingsAsync } from "./storage"; import { StoragePaths, saveSession, saveSessionAsync, saveSettings, saveSettingsAsync } from "./storage";
import { compactErrorText, ensureDirPath, filenameFromUrl, formatEta, humanSize, looksLikeOpaqueFilename, nowMs, sanitizeFilename, sleep } from "./utils"; import { compactErrorText, ensureDirPath, filenameFromUrl, formatEta, humanSize, looksLikeOpaqueFilename, nowMs, sanitizeFilename, sleep } from "./utils";
@ -185,6 +186,28 @@ function inspectPackageItemDiskState(pkg: PackageEntry, item: DownloadItem): Pac
} }
} }
function stripArchiveSuffixForMatching(fileName: string): string {
const trimmed = path.basename(String(fileName || "").trim());
if (!trimmed) {
return "";
}
let next = trimmed.replace(/\.(?:part\d+\.rar|zip\.\d+|7z\.\d+|rar|r\d{2,3}|zip|7z|\d{3})$/i, "");
next = next.replace(/\.part\d+$/i, "").replace(/\.vol\d+[+\d]*$/i, "");
return next.toLowerCase();
}
function isPreferredArchiveEntryPointName(fileName: string): boolean {
const normalized = path.basename(String(fileName || "").trim()).toLowerCase();
if (!normalized) {
return false;
}
return /\.part0*1\.rar$/.test(normalized)
|| (/\.rar$/.test(normalized) && !/\.part\d+\.rar$/.test(normalized) && !/\.r\d{2,3}$/.test(normalized))
|| /\.zip\.001$/.test(normalized)
|| /\.7z\.001$/.test(normalized)
|| (/\.001$/.test(normalized) && !/\.(zip|7z)\.001$/.test(normalized));
}
function getDownloadStallTimeoutMs(): number { function getDownloadStallTimeoutMs(): number {
const fromEnv = Number(process.env.RD_STALL_TIMEOUT_MS ?? NaN); const fromEnv = Number(process.env.RD_STALL_TIMEOUT_MS ?? NaN);
if (Number.isFinite(fromEnv) && fromEnv >= 2000 && fromEnv <= 600000) { if (Number.isFinite(fromEnv) && fromEnv >= 2000 && fromEnv <= 600000) {
@ -1438,6 +1461,141 @@ export class DownloadManager extends EventEmitter {
}); });
} }
private logItemOnly(
item: DownloadItem,
level: "INFO" | "WARN" | "ERROR",
message: string,
fields?: Record<string, unknown>
): void {
const pkg = this.session.packages[item.packageId];
this.ensureItemLogForItem(item);
writeItemLogEvent(item.id, level, message, {
packageId: item.packageId,
packageName: pkg?.name || "",
itemId: item.id,
fileName: item.fileName,
status: item.status,
targetPath: item.targetPath,
...fields
});
}
private collectRenameMatchTokensForItem(pkg: PackageEntry, item: DownloadItem): string[] {
const tokens = new Set<string>();
const maybeAdd = (value: string | null | undefined): void => {
const normalized = String(value || "").trim().toLowerCase();
if (!normalized || normalized.length < 4) {
return;
}
tokens.add(normalized);
};
maybeAdd(stripArchiveSuffixForMatching(item.fileName || ""));
maybeAdd(stripArchiveSuffixForMatching(item.targetPath ? path.basename(item.targetPath) : ""));
const diskPath = resolvePackageItemDiskPath(pkg, item);
if (diskPath) {
maybeAdd(stripArchiveSuffixForMatching(path.basename(diskPath)));
}
const episodeToken = extractEpisodeToken(item.fileName || path.basename(item.targetPath || ""));
if (episodeToken) {
maybeAdd(episodeToken);
}
return [...tokens].sort((a, b) => b.length - a.length);
}
private inferItemForMediaLog(
pkg: PackageEntry,
...candidates: Array<string | null | undefined>
): { item: DownloadItem | null; matchedBy: string | null } {
const items = pkg.itemIds
.map((itemId) => this.session.items[itemId])
.filter(Boolean) as DownloadItem[];
if (items.length === 0) {
return { item: null, matchedBy: null };
}
if (items.length === 1) {
return { item: items[0] || null, matchedBy: items[0] ? "single_item_package" : null };
}
const haystack = candidates
.map((value) => String(value || "").trim().toLowerCase())
.filter(Boolean)
.join(" || ");
if (!haystack) {
return { item: null, matchedBy: null };
}
let bestItem: DownloadItem | null = null;
let bestScore = 0;
let bestMatchedBy: string | null = null;
let bestPreferredEntry = false;
let ambiguous = false;
for (const item of items) {
const fileName = item.fileName || path.basename(item.targetPath || "");
const preferredEntry = isPreferredArchiveEntryPointName(fileName);
let score = preferredEntry ? 5 : 0;
let matchedBy: string | null = preferredEntry ? "entry_point" : null;
const episodeToken = extractEpisodeToken(fileName);
if (episodeToken && haystack.includes(episodeToken.toLowerCase())) {
score = 110 + (preferredEntry ? 5 : 0);
matchedBy = "episode_token";
} else {
for (const token of this.collectRenameMatchTokensForItem(pkg, item)) {
if (haystack.includes(token)) {
score = Math.max(score, Math.min(100, 40 + token.length) + (preferredEntry ? 5 : 0));
matchedBy = token === episodeToken?.toLowerCase() ? "episode_token" : `token:${token}`;
break;
}
}
}
if (score > bestScore || (score === bestScore && score > 0 && preferredEntry && !bestPreferredEntry)) {
bestItem = item;
bestScore = score;
bestMatchedBy = matchedBy;
bestPreferredEntry = preferredEntry;
ambiguous = false;
continue;
}
if (score > 0 && score === bestScore && preferredEntry === bestPreferredEntry) {
ambiguous = true;
}
}
if (ambiguous || !bestItem || bestScore <= 0) {
return { item: null, matchedBy: null };
}
return { item: bestItem, matchedBy: bestMatchedBy };
}
private logRenameProcess(
pkg: PackageEntry,
level: "INFO" | "WARN" | "ERROR",
stage: "auto-rename" | "mkv-move",
message: string,
fields?: Record<string, unknown>,
item?: DownloadItem | null,
matchedBy?: string | null
): void {
writeRenameLogEvent(level, message, {
stage,
packageId: pkg.id,
packageName: pkg.name,
...(item ? { itemId: item.id, fileName: item.fileName } : {}),
...(matchedBy ? { matchedBy } : {}),
...fields
});
if (item) {
this.logItemOnly(item, level, message, {
stage,
...(matchedBy ? { matchedBy } : {}),
...fields
});
}
}
public setSettings(next: AppSettings): void { public setSettings(next: AppSettings): void {
const previous = this.settings; const previous = this.settings;
next.totalDownloadedAllTime = Math.max(next.totalDownloadedAllTime || 0, this.settings.totalDownloadedAllTime || 0); next.totalDownloadedAllTime = Math.max(next.totalDownloadedAllTime || 0, this.settings.totalDownloadedAllTime || 0);
@ -2793,6 +2951,10 @@ export class DownloadManager extends EventEmitter {
extractDir, extractDir,
videoFiles: videoFiles.length videoFiles: videoFiles.length
}); });
this.logRenameProcess(pkg, "INFO", "auto-rename", "Auto-Rename Scan gestartet", {
extractDir,
videoFiles: videoFiles.length
});
} }
let renamed = 0; let renamed = 0;
@ -2840,6 +3002,12 @@ export class DownloadManager extends EventEmitter {
const targetBaseName = buildAutoRenameBaseNameFromFoldersWithOptions(folderCandidates, sourceBaseName, { const targetBaseName = buildAutoRenameBaseNameFromFoldersWithOptions(folderCandidates, sourceBaseName, {
forceEpisodeForSeasonFolder: true forceEpisodeForSeasonFolder: true
}); });
const resolveRenameItem = (...extra: Array<string | null | undefined>): { item: DownloadItem | null; matchedBy: string | null } => {
if (!pkg) {
return { item: null, matchedBy: null };
}
return this.inferItemForMediaLog(pkg, sourcePath, sourceName, folderCandidates.join(" "), targetBaseName || "", ...extra);
};
if (!targetBaseName) { if (!targetBaseName) {
if (pkg) { if (pkg) {
this.logPackageForPackage(pkg, "WARN", "Auto-Rename übersprungen: kein Zielname", { this.logPackageForPackage(pkg, "WARN", "Auto-Rename übersprungen: kein Zielname", {
@ -2847,6 +3015,13 @@ export class DownloadManager extends EventEmitter {
sourceBaseName, sourceBaseName,
folders: folderCandidates.join(", ") folders: folderCandidates.join(", ")
}); });
const resolved = resolveRenameItem();
this.logRenameProcess(pkg, "WARN", "auto-rename", "Auto-Rename übersprungen: kein Zielname", {
sourcePath,
sourceName,
sourceBaseName,
folders: folderCandidates.join(", ")
}, resolved.item, resolved.matchedBy);
} }
logger.info(`Auto-Rename: kein Zielname für ${sourceName} (folders=${folderCandidates.join(", ")})`); logger.info(`Auto-Rename: kein Zielname für ${sourceName} (folders=${folderCandidates.join(", ")})`);
continue; continue;
@ -2859,6 +3034,16 @@ export class DownloadManager extends EventEmitter {
targetPath = this.buildSafeAutoRenameTargetPath(sourcePath, fallbackBaseName, sourceExt); targetPath = this.buildSafeAutoRenameTargetPath(sourcePath, fallbackBaseName, sourceExt);
if (targetPath) { if (targetPath) {
logger.warn(`Auto-Rename Fallback wegen Pfadlänge: ${sourceName} -> ${path.basename(targetPath)}`); logger.warn(`Auto-Rename Fallback wegen Pfadlänge: ${sourceName} -> ${path.basename(targetPath)}`);
if (pkg) {
const resolved = resolveRenameItem(targetPath, fallbackBaseName);
this.logRenameProcess(pkg, "WARN", "auto-rename", "Auto-Rename Fallback wegen Pfadlänge gewählt", {
sourcePath,
sourceName,
targetPath,
targetBaseName,
fallbackBaseName
}, resolved.item, resolved.matchedBy);
}
} }
} }
if (!targetPath) { if (!targetPath) {
@ -2867,6 +3052,16 @@ export class DownloadManager extends EventEmitter {
targetPath = this.buildSafeAutoRenameTargetPath(sourcePath, veryShortFallback, sourceExt); targetPath = this.buildSafeAutoRenameTargetPath(sourcePath, veryShortFallback, sourceExt);
if (targetPath) { if (targetPath) {
logger.warn(`Auto-Rename Kurz-Fallback wegen Pfadlänge: ${sourceName} -> ${path.basename(targetPath)}`); logger.warn(`Auto-Rename Kurz-Fallback wegen Pfadlänge: ${sourceName} -> ${path.basename(targetPath)}`);
if (pkg) {
const resolved = resolveRenameItem(targetPath, veryShortFallback);
this.logRenameProcess(pkg, "WARN", "auto-rename", "Auto-Rename Kurz-Fallback wegen Pfadlänge gewählt", {
sourcePath,
sourceName,
targetPath,
targetBaseName,
fallbackBaseName: veryShortFallback
}, resolved.item, resolved.matchedBy);
}
} }
} }
} }
@ -2878,11 +3073,27 @@ export class DownloadManager extends EventEmitter {
sourceBaseName, sourceBaseName,
targetBaseName targetBaseName
}); });
const resolved = resolveRenameItem();
this.logRenameProcess(pkg, "WARN", "auto-rename", "Auto-Rename übersprungen: Zielpfad ungültig", {
sourcePath,
sourceName,
sourceBaseName,
targetBaseName
}, resolved.item, resolved.matchedBy);
} }
logger.warn(`Auto-Rename übersprungen (Zielpfad zu lang/ungültig): ${sourcePath}`); logger.warn(`Auto-Rename übersprungen (Zielpfad zu lang/ungültig): ${sourcePath}`);
continue; continue;
} }
if (pathKey(targetPath) === pathKey(sourcePath)) { if (pathKey(targetPath) === pathKey(sourcePath)) {
if (pkg) {
const resolved = resolveRenameItem(targetPath);
this.logRenameProcess(pkg, "INFO", "auto-rename", "Auto-Rename übersprungen: Name bereits passend", {
sourcePath,
sourceName,
targetPath,
targetBaseName
}, resolved.item, resolved.matchedBy);
}
continue; continue;
} }
if (await this.existsAsync(targetPath)) { if (await this.existsAsync(targetPath)) {
@ -2891,6 +3102,13 @@ export class DownloadManager extends EventEmitter {
sourceName, sourceName,
targetPath targetPath
}); });
const resolved = resolveRenameItem(targetPath);
this.logRenameProcess(pkg, "WARN", "auto-rename", "Auto-Rename übersprungen: Ziel existiert", {
sourcePath,
sourceName,
targetPath,
targetBaseName
}, resolved.item, resolved.matchedBy);
} }
logger.warn(`Auto-Rename übersprungen (Ziel existiert): ${targetPath}`); logger.warn(`Auto-Rename übersprungen (Ziel existiert): ${targetPath}`);
continue; continue;
@ -2904,6 +3122,14 @@ export class DownloadManager extends EventEmitter {
targetPath, targetPath,
sourceName sourceName
}); });
const resolved = resolveRenameItem(targetPath);
this.logRenameProcess(pkg, "INFO", "auto-rename", "Auto-Rename durchgeführt", {
sourcePath,
targetPath,
sourceName,
targetBaseName,
folders: folderCandidates.join(", ")
}, resolved.item, resolved.matchedBy);
} }
logger.info(`Auto-Rename: ${sourceName} -> ${path.basename(targetPath)}`); logger.info(`Auto-Rename: ${sourceName} -> ${path.basename(targetPath)}`);
renamed += 1; renamed += 1;
@ -2926,6 +3152,16 @@ export class DownloadManager extends EventEmitter {
await this.renamePathWithExdevFallback(sourcePath, fallbackPath); await this.renamePathWithExdevFallback(sourcePath, fallbackPath);
logger.warn(`Auto-Rename Fallback wegen Pfadlänge: ${sourceName} -> ${path.basename(fallbackPath)}`); logger.warn(`Auto-Rename Fallback wegen Pfadlänge: ${sourceName} -> ${path.basename(fallbackPath)}`);
renamed += 1; renamed += 1;
if (pkg) {
const resolved = resolveRenameItem(fallbackPath, fallbackBaseName);
this.logRenameProcess(pkg, "WARN", "auto-rename", "Auto-Rename Fallback durchgeführt", {
sourcePath,
sourceName,
targetPath: fallbackPath,
targetBaseName,
fallbackBaseName
}, resolved.item, resolved.matchedBy);
}
fallbackRenamed = true; fallbackRenamed = true;
break; break;
} catch { } catch {
@ -2942,6 +3178,15 @@ export class DownloadManager extends EventEmitter {
sourceName, sourceName,
error: compactErrorText(error) error: compactErrorText(error)
}); });
const resolved = resolveRenameItem(targetPath);
this.logRenameProcess(pkg, "WARN", "auto-rename", "Auto-Rename fehlgeschlagen", {
sourcePath,
sourceName,
targetPath,
targetBaseName,
folders: folderCandidates.join(", "),
error: compactErrorText(error)
}, resolved.item, resolved.matchedBy);
} }
} }
} }
@ -2952,6 +3197,10 @@ export class DownloadManager extends EventEmitter {
this.logPackageForPackage(pkg, "INFO", "Auto-Rename abgeschlossen", { this.logPackageForPackage(pkg, "INFO", "Auto-Rename abgeschlossen", {
renamed renamed
}); });
this.logRenameProcess(pkg, "INFO", "auto-rename", "Auto-Rename abgeschlossen", {
extractDir,
renamed
});
} }
} }
return renamed; return renamed;
@ -3152,6 +3401,12 @@ export class DownloadManager extends EventEmitter {
return; return;
} }
this.logRenameProcess(pkg, "INFO", "mkv-move", "MKV-Sammelordner Scan gestartet", {
sourceDir,
targetDir,
mkvFiles: mkvFiles.length
});
const reservedTargets = new Set<string>(); const reservedTargets = new Set<string>();
let moved = 0; let moved = 0;
let skipped = 0; let skipped = 0;
@ -3174,6 +3429,12 @@ export class DownloadManager extends EventEmitter {
} }
if (sourceSize === 0) { if (sourceSize === 0) {
logger.warn(`MKV-Sammelordner: überspringe 0-Byte-Datei ${path.basename(sourcePath)}`); logger.warn(`MKV-Sammelordner: überspringe 0-Byte-Datei ${path.basename(sourcePath)}`);
const resolved = this.inferItemForMediaLog(pkg, sourcePath, path.basename(sourcePath), targetDir);
this.logRenameProcess(pkg, "WARN", "mkv-move", "MKV übersprungen: 0-Byte-Datei", {
sourcePath,
targetDir,
sourceSize
}, resolved.item, resolved.matchedBy);
skipped += 1; skipped += 1;
continue; continue;
} }
@ -3184,6 +3445,12 @@ export class DownloadManager extends EventEmitter {
const existingStat = await fs.promises.stat(idealTargetPath); const existingStat = await fs.promises.stat(idealTargetPath);
if (existingStat.size === sourceSize) { if (existingStat.size === sourceSize) {
logger.info(`MKV-Sammelordner: Duplikat übersprungen (gleiche Größe ${humanSize(sourceSize)}): ${path.basename(sourcePath)}`); logger.info(`MKV-Sammelordner: Duplikat übersprungen (gleiche Größe ${humanSize(sourceSize)}): ${path.basename(sourcePath)}`);
const resolved = this.inferItemForMediaLog(pkg, sourcePath, path.basename(sourcePath), idealTargetPath);
this.logRenameProcess(pkg, "INFO", "mkv-move", "MKV-Duplikat übersprungen", {
sourcePath,
targetPath: idealTargetPath,
sourceSize
}, resolved.item, resolved.matchedBy);
// Remove the duplicate source file to avoid future re-processing // Remove the duplicate source file to avoid future re-processing
try { await fs.promises.unlink(sourcePath); } catch { /* ignore */ } try { await fs.promises.unlink(sourcePath); } catch { /* ignore */ }
skipped += 1; skipped += 1;
@ -3207,6 +3474,12 @@ export class DownloadManager extends EventEmitter {
targetPath, targetPath,
sourceSize sourceSize
}); });
const resolved = this.inferItemForMediaLog(pkg, sourcePath, path.basename(sourcePath), targetPath);
this.logRenameProcess(pkg, "INFO", "mkv-move", "MKV verschoben", {
sourcePath,
targetPath,
sourceSize
}, resolved.item, resolved.matchedBy);
} catch (error) { } catch (error) {
failed += 1; failed += 1;
logger.warn(`MKV verschieben fehlgeschlagen: ${sourcePath} -> ${targetPath} (${compactErrorText(error)})`); logger.warn(`MKV verschieben fehlgeschlagen: ${sourcePath} -> ${targetPath} (${compactErrorText(error)})`);
@ -3215,6 +3488,13 @@ export class DownloadManager extends EventEmitter {
targetPath, targetPath,
error: compactErrorText(error) error: compactErrorText(error)
}); });
const resolved = this.inferItemForMediaLog(pkg, sourcePath, path.basename(sourcePath), targetPath);
this.logRenameProcess(pkg, "WARN", "mkv-move", "MKV verschieben fehlgeschlagen", {
sourcePath,
targetPath,
sourceSize,
error: compactErrorText(error)
}, resolved.item, resolved.matchedBy);
} }
} }
@ -3230,6 +3510,13 @@ export class DownloadManager extends EventEmitter {
} }
logger.info(`MKV-Sammelordner: pkg=${pkg.name}, packageId=${packageId}, moved=${moved}, skipped=${skipped}, failed=${failed}, target=${targetDir}`); logger.info(`MKV-Sammelordner: pkg=${pkg.name}, packageId=${packageId}, moved=${moved}, skipped=${skipped}, failed=${failed}, target=${targetDir}`);
this.logRenameProcess(pkg, "INFO", "mkv-move", "MKV-Sammelordner abgeschlossen", {
sourceDir,
targetDir,
moved,
skipped,
failed
});
} }
public cancelPackage(packageId: string): void { public cancelPackage(packageId: string): void {

View File

@ -550,6 +550,13 @@ function registerIpcHandlers(): void {
} }
}); });
ipcMain.handle(IPC_CHANNELS.OPEN_RENAME_LOG, async () => {
const logPath = controller.getRenameLogPath();
if (logPath) {
await shell.openPath(logPath);
}
});
ipcMain.handle(IPC_CHANNELS.OPEN_SESSION_LOG, async () => { ipcMain.handle(IPC_CHANNELS.OPEN_SESSION_LOG, async () => {
const logPath = controller.getSessionLogPath(); const logPath = controller.getSessionLogPath();
if (logPath) { if (logPath) {

123
src/main/rename-log.ts Normal file
View File

@ -0,0 +1,123 @@
import fs from "node:fs";
import path from "node:path";
type RenameLogLevel = "INFO" | "WARN" | "ERROR";
const RENAME_LOG_MAX_FILE_BYTES = Number(process.env.RD_RENAME_LOG_MAX_BYTES || 10 * 1024 * 1024);
const RENAME_LOG_RETENTION_DAYS = Number(process.env.RD_RENAME_LOG_RETENTION_DAYS || 30);
let renameLogPath: string | null = null;
function sanitizeFieldValue(value: unknown): string {
if (value === undefined || value === null) {
return "";
}
if (typeof value === "string") {
return value.replace(/\r?\n/g, "\\n");
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
try {
return JSON.stringify(value).replace(/\r?\n/g, "\\n");
} catch {
return String(value);
}
}
function formatFields(fields?: Record<string, unknown>): string {
if (!fields) {
return "";
}
const parts = Object.entries(fields)
.filter(([, value]) => value !== undefined && value !== null && sanitizeFieldValue(value) !== "")
.map(([key, value]) => `${key}=${sanitizeFieldValue(value)}`);
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
}
function rotateIfNeeded(filePath: string): void {
try {
const stat = fs.statSync(filePath);
if (stat.size < RENAME_LOG_MAX_FILE_BYTES) {
return;
}
const backup = `${filePath}.old`;
try {
fs.rmSync(backup, { force: true });
} catch {
// ignore
}
fs.renameSync(filePath, backup);
} catch {
// ignore
}
}
function cleanupOldBackup(filePath: string): void {
const backup = `${filePath}.old`;
try {
const stat = fs.statSync(backup);
const cutoff = Date.now() - RENAME_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
if (stat.mtimeMs < cutoff) {
fs.rmSync(backup, { force: true });
}
} catch {
// ignore
}
}
export function initRenameLog(baseDir: string): void {
renameLogPath = path.join(baseDir, "rename.log");
try {
fs.mkdirSync(path.dirname(renameLogPath), { recursive: true });
cleanupOldBackup(renameLogPath);
if (!fs.existsSync(renameLogPath)) {
fs.writeFileSync(renameLogPath, "", "utf8");
}
rotateIfNeeded(renameLogPath);
if (!fs.existsSync(renameLogPath)) {
fs.writeFileSync(renameLogPath, "", "utf8");
}
fs.appendFileSync(renameLogPath, `=== Rename-Log Start: ${new Date().toISOString()} ===\n`, "utf8");
} catch {
renameLogPath = null;
}
}
export function logRenameEvent(level: RenameLogLevel, message: string, fields?: Record<string, unknown>): void {
if (!renameLogPath) {
return;
}
try {
rotateIfNeeded(renameLogPath);
if (!fs.existsSync(renameLogPath)) {
fs.writeFileSync(renameLogPath, "", "utf8");
}
fs.appendFileSync(
renameLogPath,
`${new Date().toISOString()} [${level}] ${message}${formatFields(fields)}\n`,
"utf8"
);
} catch {
// ignore write errors
}
}
export function getRenameLogPath(): string | null {
if (!renameLogPath) {
return null;
}
return fs.existsSync(renameLogPath) ? renameLogPath : null;
}
export function shutdownRenameLog(): void {
if (!renameLogPath) {
return;
}
try {
fs.appendFileSync(renameLogPath, `=== Rename-Log Ende: ${new Date().toISOString()} ===\n`, "utf8");
} catch {
// ignore
}
renameLogPath = null;
}

View File

@ -6,6 +6,7 @@ import { getAuditLogPath } from "./audit-log";
import { getDebugSetupCheck } from "./debug-setup"; import { getDebugSetupCheck } from "./debug-setup";
import { getLogFilePath } from "./logger"; import { getLogFilePath } from "./logger";
import { getPackageLogPath } from "./package-log"; import { getPackageLogPath } from "./package-log";
import { getRenameLogPath } from "./rename-log";
import { getSessionLogPath } from "./session-log"; import { getSessionLogPath } from "./session-log";
import { createStoragePaths, loadHistory, loadSettings } from "./storage"; import { createStoragePaths, loadHistory, loadSettings } from "./storage";
import { buildAccountSummary, buildRedactedSettingsPayload, buildStatsPayload, summarizeHistoryEntry } from "./support-data"; import { buildAccountSummary, buildRedactedSettingsPayload, buildStatsPayload, summarizeHistoryEntry } from "./support-data";
@ -120,6 +121,8 @@ export function buildSupportBundle(manager: DownloadManager, baseDir: string): B
addFileIfExists(zip, `${getLogFilePath()}.old`, "logs/rd_downloader.log.old"); addFileIfExists(zip, `${getLogFilePath()}.old`, "logs/rd_downloader.log.old");
addFileIfExists(zip, getAuditLogPath(), "logs/audit.log"); addFileIfExists(zip, getAuditLogPath(), "logs/audit.log");
addFileIfExists(zip, getAuditLogPath() ? `${getAuditLogPath()}.old` : null, "logs/audit.log.old"); addFileIfExists(zip, getAuditLogPath() ? `${getAuditLogPath()}.old` : null, "logs/audit.log.old");
addFileIfExists(zip, getRenameLogPath(), "logs/rename.log");
addFileIfExists(zip, getRenameLogPath() ? `${getRenameLogPath()}.old` : null, "logs/rename.log.old");
addFileIfExists(zip, getSessionLogPath(), "logs/session.log"); addFileIfExists(zip, getSessionLogPath(), "logs/session.log");
addFileIfExists(zip, getTraceLogPath(), "logs/trace.log"); addFileIfExists(zip, getTraceLogPath(), "logs/trace.log");
addFileIfExists(zip, getTraceLogPath() ? `${getTraceLogPath()}.old` : null, "logs/trace.log.old"); addFileIfExists(zip, getTraceLogPath() ? `${getTraceLogPath()}.old` : null, "logs/trace.log.old");

View File

@ -61,6 +61,7 @@ const api: ElectronApi = {
exportSupportBundle: (): Promise<{ saved: boolean; filePath?: string }> => ipcRenderer.invoke(IPC_CHANNELS.EXPORT_SUPPORT_BUNDLE), exportSupportBundle: (): Promise<{ saved: boolean; filePath?: string }> => ipcRenderer.invoke(IPC_CHANNELS.EXPORT_SUPPORT_BUNDLE),
openLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_LOG), openLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_LOG),
openAuditLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_AUDIT_LOG), openAuditLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_AUDIT_LOG),
openRenameLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_RENAME_LOG),
openSessionLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_SESSION_LOG), openSessionLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_SESSION_LOG),
openTraceLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_TRACE_LOG), openTraceLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_TRACE_LOG),
openPackageLog: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_PACKAGE_LOG, packageId), openPackageLog: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_PACKAGE_LOG, packageId),

View File

@ -169,6 +169,7 @@ function buildDebugSetupDetails(setup: DebugSetupCheckResult): string {
formatFileLine("Gesamt", setup.logSummary.totalBytes), formatFileLine("Gesamt", setup.logSummary.totalBytes),
formatFileLine("Hauptlog", setup.logSummary.main.bytes + setup.logSummary.mainBackup.bytes), formatFileLine("Hauptlog", setup.logSummary.main.bytes + setup.logSummary.mainBackup.bytes),
formatFileLine("Audit", setup.logSummary.audit.bytes + setup.logSummary.auditBackup.bytes), formatFileLine("Audit", setup.logSummary.audit.bytes + setup.logSummary.auditBackup.bytes),
formatFileLine("Rename", setup.logSummary.rename.bytes + setup.logSummary.renameBackup.bytes),
formatFileLine("Trace", setup.logSummary.trace.bytes + setup.logSummary.traceBackup.bytes), formatFileLine("Trace", setup.logSummary.trace.bytes + setup.logSummary.traceBackup.bytes),
`${formatFileLine("Session-Logs", setup.logSummary.session.bytes + setup.logSummary.sessionLogs.bytes)} | Dateien: ${setup.logSummary.sessionLogs.fileCount}`, `${formatFileLine("Session-Logs", setup.logSummary.session.bytes + setup.logSummary.sessionLogs.bytes)} | Dateien: ${setup.logSummary.sessionLogs.fileCount}`,
`${formatFileLine("Paket-Logs", setup.logSummary.packageLogs.bytes)} | Dateien: ${setup.logSummary.packageLogs.fileCount}`, `${formatFileLine("Paket-Logs", setup.logSummary.packageLogs.bytes)} | Dateien: ${setup.logSummary.packageLogs.fileCount}`,
@ -3917,6 +3918,9 @@ export function App(): ReactElement {
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openAuditLog().catch(() => {}); }}> <button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openAuditLog().catch(() => {}); }}>
<span>Audit-Log öffnen</span> <span>Audit-Log öffnen</span>
</button> </button>
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openRenameLog().catch(() => {}); }}>
<span>Rename-Log öffnen</span>
</button>
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openSessionLog().catch(() => {}); }}> <button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openSessionLog().catch(() => {}); }}>
<span>Session-Log öffnen</span> <span>Session-Log öffnen</span>
</button> </button>

View File

@ -41,6 +41,7 @@ export const IPC_CHANNELS = {
EXPORT_SUPPORT_BUNDLE: "app:export-support-bundle", EXPORT_SUPPORT_BUNDLE: "app:export-support-bundle",
OPEN_LOG: "app:open-log", OPEN_LOG: "app:open-log",
OPEN_AUDIT_LOG: "app:open-audit-log", OPEN_AUDIT_LOG: "app:open-audit-log",
OPEN_RENAME_LOG: "app:open-rename-log",
OPEN_SESSION_LOG: "app:open-session-log", OPEN_SESSION_LOG: "app:open-session-log",
OPEN_TRACE_LOG: "app:open-trace-log", OPEN_TRACE_LOG: "app:open-trace-log",
OPEN_PACKAGE_LOG: "app:open-package-log", OPEN_PACKAGE_LOG: "app:open-package-log",

View File

@ -58,6 +58,7 @@ export interface ElectronApi {
exportSupportBundle: () => Promise<{ saved: boolean; filePath?: string }>; exportSupportBundle: () => Promise<{ saved: boolean; filePath?: string }>;
openLog: () => Promise<void>; openLog: () => Promise<void>;
openAuditLog: () => Promise<void>; openAuditLog: () => Promise<void>;
openRenameLog: () => Promise<void>;
openSessionLog: () => Promise<void>; openSessionLog: () => Promise<void>;
openTraceLog: () => Promise<void>; openTraceLog: () => Promise<void>;
openPackageLog: (packageId: string) => Promise<void>; openPackageLog: (packageId: string) => Promise<void>;

View File

@ -399,6 +399,8 @@ export interface DebugSetupCheckResult {
mainBackup: SupportFileSizeInfo; mainBackup: SupportFileSizeInfo;
audit: SupportFileSizeInfo; audit: SupportFileSizeInfo;
auditBackup: SupportFileSizeInfo; auditBackup: SupportFileSizeInfo;
rename: SupportFileSizeInfo;
renameBackup: SupportFileSizeInfo;
session: SupportFileSizeInfo; session: SupportFileSizeInfo;
trace: SupportFileSizeInfo; trace: SupportFileSizeInfo;
traceBackup: SupportFileSizeInfo; traceBackup: SupportFileSizeInfo;

View File

@ -46,6 +46,7 @@ import { startDebugServer, stopDebugServer } from "../src/main/debug-server";
import { ensureItemLog, initItemLogs, shutdownItemLogs } from "../src/main/item-log"; import { ensureItemLog, initItemLogs, shutdownItemLogs } from "../src/main/item-log";
import { configureLogger, getLogFilePath, logger } from "../src/main/logger"; import { configureLogger, getLogFilePath, logger } from "../src/main/logger";
import { ensurePackageLog, initPackageLogs, shutdownPackageLogs } from "../src/main/package-log"; import { ensurePackageLog, initPackageLogs, shutdownPackageLogs } from "../src/main/package-log";
import { getRenameLogPath, initRenameLog, logRenameEvent, shutdownRenameLog } from "../src/main/rename-log";
import { getSessionLogPath, initSessionLog, shutdownSessionLog } from "../src/main/session-log"; import { getSessionLogPath, initSessionLog, shutdownSessionLog } from "../src/main/session-log";
import { createStoragePaths, saveHistory, saveSettings } from "../src/main/storage"; import { createStoragePaths, saveHistory, saveSettings } from "../src/main/storage";
import { getTraceConfigPath, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "../src/main/trace-log"; import { getTraceConfigPath, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "../src/main/trace-log";
@ -235,6 +236,9 @@ async function createFixture() {
} }
logAuditEvent("INFO", "AUDIT-LINE", { scope: "settings" }); logAuditEvent("INFO", "AUDIT-LINE", { scope: "settings" });
initRenameLog(baseDir);
logRenameEvent("INFO", "RENAME-LINE", { stage: "auto-rename", sourcePath: "C:\\extract\\old.mkv" });
initTraceLog(baseDir); initTraceLog(baseDir);
setTraceEnabled(true, "test-fixture"); setTraceEnabled(true, "test-fixture");
logTraceEvent("INFO", "support", "TRACE-EVENT", { scope: "fixture" }); logTraceEvent("INFO", "support", "TRACE-EVENT", { scope: "fixture" });
@ -294,6 +298,7 @@ afterEach(() => {
shutdownSessionLog(); shutdownSessionLog();
shutdownPackageLogs(); shutdownPackageLogs();
shutdownItemLogs(); shutdownItemLogs();
shutdownRenameLog();
shutdownTraceLog(); shutdownTraceLog();
shutdownAuditLog(); shutdownAuditLog();
while (tempDirs.length > 0) { while (tempDirs.length > 0) {
@ -324,6 +329,7 @@ describe("debug-server", () => {
expect(payload.selectedPackage?.name).toBe("server-package"); expect(payload.selectedPackage?.name).toBe("server-package");
expect((payload.logs?.main?.lines || []).join("\n")).toContain("MAIN-LINE"); expect((payload.logs?.main?.lines || []).join("\n")).toContain("MAIN-LINE");
expect((payload.logs?.audit?.lines || []).join("\n")).toContain("AUDIT-LINE"); expect((payload.logs?.audit?.lines || []).join("\n")).toContain("AUDIT-LINE");
expect((payload.logs?.rename?.lines || []).join("\n")).toContain("RENAME-LINE");
expect((payload.logs?.trace?.lines || []).join("\n")).toContain("TRACE-EVENT"); expect((payload.logs?.trace?.lines || []).join("\n")).toContain("TRACE-EVENT");
expect((payload.logs?.session?.lines || []).join("\n")).toContain("SESSION-LINE"); expect((payload.logs?.session?.lines || []).join("\n")).toContain("SESSION-LINE");
expect((payload.logs?.package?.lines || []).join("\n")).toContain("PACKAGE-LINE"); expect((payload.logs?.package?.lines || []).join("\n")).toContain("PACKAGE-LINE");
@ -353,6 +359,7 @@ describe("debug-server", () => {
expect(metaPayload.supportFiles?.aiManifest).toBe(manifestPath); expect(metaPayload.supportFiles?.aiManifest).toBe(manifestPath);
expect(metaPayload.supportFiles?.traceConfig).toBe(getTraceConfigPath()); expect(metaPayload.supportFiles?.traceConfig).toBe(getTraceConfigPath());
expect(metaPayload.supportFiles?.traceLog).toBe(getTraceLogPath()); expect(metaPayload.supportFiles?.traceLog).toBe(getTraceLogPath());
expect(metaPayload.logPaths?.rename).toBe(getRenameLogPath());
expect(metaPayload.supportChecks?.setup).toBe("/debug/setup"); expect(metaPayload.supportChecks?.setup).toBe("/debug/setup");
expect(metaPayload.supportChecks?.selfCheck).toBe("/self-check"); expect(metaPayload.supportChecks?.selfCheck).toBe("/self-check");
}); });
@ -376,6 +383,7 @@ describe("debug-server", () => {
expect(payload.diskSpace?.output?.freeBytes).toBeGreaterThan(0); expect(payload.diskSpace?.output?.freeBytes).toBeGreaterThan(0);
expect(payload.diskSpace?.extract?.freeBytes).toBeGreaterThan(0); expect(payload.diskSpace?.extract?.freeBytes).toBeGreaterThan(0);
expect(payload.logSummary?.totalBytes).toBeGreaterThan(0); expect(payload.logSummary?.totalBytes).toBeGreaterThan(0);
expect(payload.logSummary?.rename?.bytes).toBeGreaterThan(0);
expect(payload.logSummary?.packageLogs?.fileCount).toBe(1); expect(payload.logSummary?.packageLogs?.fileCount).toBe(1);
expect(payload.logSummary?.itemLogs?.fileCount).toBe(1); expect(payload.logSummary?.itemLogs?.fileCount).toBe(1);
expect(payload.supportBundle?.estimatedBytes).toBeGreaterThan(0); expect(payload.supportBundle?.estimatedBytes).toBeGreaterThan(0);
@ -437,6 +445,11 @@ describe("debug-server", () => {
const auditPayload = await auditResponse.json() as Record<string, any>; const auditPayload = await auditResponse.json() as Record<string, any>;
expect((auditPayload.lines || []).join("\n")).toContain("AUDIT-LINE"); expect((auditPayload.lines || []).join("\n")).toContain("AUDIT-LINE");
const renameResponse = await fetch(`${fixture.baseUrl}/logs/rename?token=${fixture.token}&lines=20`);
expect(renameResponse.ok).toBe(true);
const renamePayload = await renameResponse.json() as Record<string, any>;
expect((renamePayload.lines || []).join("\n")).toContain("RENAME-LINE");
const traceResponse = await fetch(`${fixture.baseUrl}/logs/trace?token=${fixture.token}&lines=50`); const traceResponse = await fetch(`${fixture.baseUrl}/logs/trace?token=${fixture.token}&lines=50`);
expect(traceResponse.ok).toBe(true); expect(traceResponse.ok).toBe(true);
const tracePayload = await traceResponse.json() as Record<string, any>; const tracePayload = await traceResponse.json() as Record<string, any>;
@ -492,6 +505,7 @@ describe("debug-server", () => {
expect(entries).toContain("overview/self-check.json"); expect(entries).toContain("overview/self-check.json");
expect(entries).toContain("overview/trace-config.json"); expect(entries).toContain("overview/trace-config.json");
expect(entries).toContain("logs/audit.log"); expect(entries).toContain("logs/audit.log");
expect(entries).toContain("logs/rename.log");
expect(entries).toContain("logs/trace.log"); expect(entries).toContain("logs/trace.log");
expect(entries).toContain("runtime/debug_ai_manifest.json"); expect(entries).toContain("runtime/debug_ai_manifest.json");
expect(entries).not.toContain("runtime/debug_token.txt"); expect(entries).not.toContain("runtime/debug_token.txt");

View File

@ -9,8 +9,10 @@ import { DownloadManager, extractArchiveNameFromExtractorLogMessage, getAuthorit
import { defaultSettings } from "../src/main/constants"; import { defaultSettings } from "../src/main/constants";
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys"; import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits"; import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
import { getItemLogPath, initItemLogs, shutdownItemLogs } from "../src/main/item-log";
import { createStoragePaths, emptySession } from "../src/main/storage"; import { createStoragePaths, emptySession } from "../src/main/storage";
import { primeDebridLinkRuntimeCooldownForTests, resetDebridLinkRuntimeStateForTests } from "../src/main/debrid"; import { primeDebridLinkRuntimeCooldownForTests, resetDebridLinkRuntimeStateForTests } from "../src/main/debrid";
import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "../src/main/rename-log";
const tempDirs: string[] = []; const tempDirs: string[] = [];
const originalFetch = globalThis.fetch; const originalFetch = globalThis.fetch;
@ -56,6 +58,8 @@ async function removeDirWithRetries(dir: string): Promise<void> {
afterEach(async () => { afterEach(async () => {
globalThis.fetch = originalFetch; globalThis.fetch = originalFetch;
resetDebridLinkRuntimeStateForTests(); resetDebridLinkRuntimeStateForTests();
shutdownItemLogs();
shutdownRenameLog();
for (const dir of tempDirs.splice(0)) { for (const dir of tempDirs.splice(0)) {
await removeDirWithRetries(dir); await removeDirWithRetries(dir);
} }
@ -6705,6 +6709,50 @@ describe("download manager", () => {
expect(fs.existsSync(originalExtractedPath)).toBe(false); expect(fs.existsSync(originalExtractedPath)).toBe(false);
}); });
it("writes auto-rename details into rename and item logs", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);
const packageName = "Asbest.S02.GERMAN.720p.WEB.AVC-4SF";
const sourceFileName = "4sf-asbest.web.7p-s02e01.mkv";
const expectedFileName = "Asbest.S02E01.GERMAN.720p.WEB.AVC-4SF.mkv";
const { session, itemId, extractDir } = createCompletedArchiveSession(root, packageName, sourceFileName);
const stateDir = path.join(root, "state");
initItemLogs(stateDir);
initRenameLog(stateDir);
new DownloadManager(
{
...defaultSettings(),
token: "rd-token",
outputDir: path.join(root, "downloads"),
extractDir: path.join(root, "extract"),
autoExtract: true,
autoRename4sf4sj: true,
enableIntegrityCheck: false,
cleanupMode: "none"
},
session,
createStoragePaths(stateDir)
);
const expectedPath = path.join(extractDir, expectedFileName);
await waitFor(() => fs.existsSync(expectedPath), 12000);
await new Promise((resolve) => setTimeout(resolve, 350));
const renameLogPath = getRenameLogPath();
expect(renameLogPath).not.toBeNull();
const renameContent = fs.readFileSync(renameLogPath!, "utf8");
expect(renameContent).toContain("Auto-Rename durchgeführt");
expect(renameContent).toContain(`targetPath=${expectedPath}`);
const itemLogPath = getItemLogPath(itemId);
expect(itemLogPath).not.toBeNull();
const itemContent = fs.readFileSync(itemLogPath!, "utf8");
expect(itemContent).toContain("Auto-Rename durchgeführt");
expect(itemContent).toContain("stage=auto-rename");
}, 20000);
it("adds REPACK marker from rp token and supports 4SJ folders", async () => { it("adds REPACK marker from rp token and supports 4SJ folders", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root); tempDirs.push(root);

52
tests/rename-log.test.ts Normal file
View File

@ -0,0 +1,52 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { getRenameLogPath, initRenameLog, logRenameEvent, shutdownRenameLog } from "../src/main/rename-log";
const tempDirs: string[] = [];
afterEach(() => {
shutdownRenameLog();
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
describe("rename-log", () => {
it("writes rename events to the rename log", () => {
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-rlog-"));
tempDirs.push(baseDir);
initRenameLog(baseDir);
logRenameEvent("INFO", "Auto-Rename durchgeführt", {
packageName: "Test Paket",
sourcePath: "C:\\extract\\old.mkv",
targetPath: "C:\\extract\\new.mkv"
});
const logPath = getRenameLogPath();
expect(logPath).not.toBeNull();
expect(fs.existsSync(logPath!)).toBe(true);
const content = fs.readFileSync(logPath!, "utf8");
expect(content).toContain("Rename-Log Start");
expect(content).toContain("Auto-Rename durchgeführt");
expect(content).toContain("sourcePath=C:\\extract\\old.mkv");
});
it("rotates oversized rename logs on startup", () => {
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-rlog-rotate-"));
tempDirs.push(baseDir);
const oversizedPath = path.join(baseDir, "rename.log");
fs.mkdirSync(baseDir, { recursive: true });
fs.writeFileSync(oversizedPath, "x".repeat(10 * 1024 * 1024 + 256), "utf8");
initRenameLog(baseDir);
expect(fs.existsSync(oversizedPath)).toBe(true);
expect(fs.existsSync(`${oversizedPath}.old`)).toBe(true);
const content = fs.readFileSync(oversizedPath, "utf8");
expect(content).toContain("Rename-Log Start");
});
});