Prevent queue loss during app updates
- Increase quit timeout from 900ms to 5000ms to ensure pending saves complete - Add persistNowSync() called before update install to flush queue to disk - Remove blockAllPersistence from shutdown save condition — shutdown must always persist to prevent data loss across restarts - Add temp file recovery as last resort when both primary and backup session files are corrupted Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ffb48a8883
commit
5aeab9ecad
@ -24,26 +24,26 @@ import { APP_VERSION } from "./constants";
|
|||||||
import { DownloadManager } from "./download-manager";
|
import { DownloadManager } from "./download-manager";
|
||||||
import { fetchAllDebridHostInfo, fetchDebridLinkHostLimits } from "./debrid";
|
import { fetchAllDebridHostInfo, fetchDebridLinkHostLimits } from "./debrid";
|
||||||
import { parseCollectorInput } from "./link-parser";
|
import { parseCollectorInput } from "./link-parser";
|
||||||
import { configureLogger, getLogFilePath, logger } from "./logger";
|
import { configureLogger, getLogFilePath, logger } from "./logger";
|
||||||
import { AllDebridWebFallback } from "./all-debrid-web";
|
import { AllDebridWebFallback } from "./all-debrid-web";
|
||||||
import { BestDebridWebFallback } from "./bestdebrid-web";
|
import { BestDebridWebFallback } from "./bestdebrid-web";
|
||||||
import { RealDebridWebFallback } from "./realdebrid-web";
|
import { RealDebridWebFallback } from "./realdebrid-web";
|
||||||
import { getItemLogPath, initItemLogs, shutdownItemLogs } from "./item-log";
|
import { getItemLogPath, initItemLogs, shutdownItemLogs } from "./item-log";
|
||||||
import { getPackageLogPath, initPackageLogs, shutdownPackageLogs } from "./package-log";
|
import { getPackageLogPath, initPackageLogs, shutdownPackageLogs } from "./package-log";
|
||||||
import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session-log";
|
import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session-log";
|
||||||
import { MegaWebFallback } from "./mega-web-fallback";
|
import { MegaWebFallback } from "./mega-web-fallback";
|
||||||
import { addHistoryEntry, addHistoryEntryForRetention, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistoryForRetention, loadSession, loadSettings, normalizeHistoryEntry, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntry, resetHistoryForRetention, saveHistory, saveSession, saveSettings } from "./storage";
|
import { addHistoryEntry, addHistoryEntryForRetention, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistoryForRetention, loadSession, loadSettings, normalizeHistoryEntry, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntry, resetHistoryForRetention, saveHistory, saveSession, saveSettings } from "./storage";
|
||||||
import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update";
|
import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update";
|
||||||
import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server";
|
import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server";
|
||||||
import { encryptBackup, decryptBackup } from "./backup-crypto";
|
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 { 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";
|
||||||
import type { DebugSetupCheckResult, SupportTraceConfig } from "../shared/types";
|
import type { DebugSetupCheckResult, SupportTraceConfig } from "../shared/types";
|
||||||
|
|
||||||
function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> {
|
function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> {
|
||||||
const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined);
|
const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined);
|
||||||
@ -73,22 +73,22 @@ export class AppController {
|
|||||||
|
|
||||||
private storagePaths = createStoragePaths(path.join(app.getPath("userData"), "runtime"));
|
private storagePaths = createStoragePaths(path.join(app.getPath("userData"), "runtime"));
|
||||||
|
|
||||||
private onStateHandler: ((snapshot: UiSnapshot) => void) | null = null;
|
private onStateHandler: ((snapshot: UiSnapshot) => void) | null = null;
|
||||||
|
|
||||||
private autoResumePending = false;
|
|
||||||
private runtimeStatsTimer: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
public constructor() {
|
private autoResumePending = false;
|
||||||
configureLogger(this.storagePaths.baseDir);
|
private runtimeStatsTimer: NodeJS.Timeout | null = null;
|
||||||
initSessionLog(this.storagePaths.baseDir);
|
|
||||||
initPackageLogs(this.storagePaths.baseDir);
|
public constructor() {
|
||||||
initItemLogs(this.storagePaths.baseDir);
|
configureLogger(this.storagePaths.baseDir);
|
||||||
initAuditLog(this.storagePaths.baseDir);
|
initSessionLog(this.storagePaths.baseDir);
|
||||||
initRenameLog(this.storagePaths.baseDir);
|
initPackageLogs(this.storagePaths.baseDir);
|
||||||
initTraceLog(this.storagePaths.baseDir);
|
initItemLogs(this.storagePaths.baseDir);
|
||||||
this.settings = loadSettings(this.storagePaths);
|
initAuditLog(this.storagePaths.baseDir);
|
||||||
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
initRenameLog(this.storagePaths.baseDir);
|
||||||
const session = loadSession(this.storagePaths);
|
initTraceLog(this.storagePaths.baseDir);
|
||||||
|
this.settings = loadSettings(this.storagePaths);
|
||||||
|
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
||||||
|
const session = loadSession(this.storagePaths);
|
||||||
this.megaWebFallback = new MegaWebFallback(() => ({
|
this.megaWebFallback = new MegaWebFallback(() => ({
|
||||||
login: this.settings.megaLogin,
|
login: this.settings.megaLogin,
|
||||||
password: this.settings.megaPassword
|
password: this.settings.megaPassword
|
||||||
@ -98,31 +98,31 @@ export class AppController {
|
|||||||
this.bestDebridWebFallback = new BestDebridWebFallback(() => this.settings.rememberToken);
|
this.bestDebridWebFallback = new BestDebridWebFallback(() => this.settings.rememberToken);
|
||||||
this.manager = new DownloadManager(this.settings, session, this.storagePaths, {
|
this.manager = new DownloadManager(this.settings, session, this.storagePaths, {
|
||||||
megaWebUnrestrict: (link: string, signal?: AbortSignal) => this.megaWebFallback.unrestrict(link, signal),
|
megaWebUnrestrict: (link: string, signal?: AbortSignal) => this.megaWebFallback.unrestrict(link, signal),
|
||||||
allDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.allDebridWebFallback.unrestrict(link, signal),
|
allDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.allDebridWebFallback.unrestrict(link, signal),
|
||||||
realDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.realDebridWebFallback.unrestrict(link, signal),
|
realDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.realDebridWebFallback.unrestrict(link, signal),
|
||||||
bestDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.bestDebridWebFallback.unrestrict(link, signal),
|
bestDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.bestDebridWebFallback.unrestrict(link, signal),
|
||||||
invalidateMegaSession: () => this.megaWebFallback.invalidateSession(),
|
invalidateMegaSession: () => this.megaWebFallback.invalidateSession(),
|
||||||
onHistoryEntry: (entry: HistoryEntry) => {
|
onHistoryEntry: (entry: HistoryEntry) => {
|
||||||
addHistoryEntryForRetention(this.storagePaths, this.settings.historyRetentionMode, entry);
|
addHistoryEntryForRetention(this.storagePaths, this.settings.historyRetentionMode, entry);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.manager.on("state", (snapshot: UiSnapshot) => {
|
this.manager.on("state", (snapshot: UiSnapshot) => {
|
||||||
this.onStateHandler?.(snapshot);
|
this.onStateHandler?.(snapshot);
|
||||||
});
|
});
|
||||||
logger.info(`App gestartet v${APP_VERSION}`);
|
logger.info(`App gestartet v${APP_VERSION}`);
|
||||||
logger.info(`Log-Datei: ${getLogFilePath()}`);
|
logger.info(`Log-Datei: ${getLogFilePath()}`);
|
||||||
logAuditEvent("INFO", "App gestartet", {
|
logAuditEvent("INFO", "App gestartet", {
|
||||||
appVersion: APP_VERSION,
|
appVersion: APP_VERSION,
|
||||||
runtimeDir: this.storagePaths.baseDir
|
runtimeDir: this.storagePaths.baseDir
|
||||||
});
|
});
|
||||||
startDebugServer(this.manager, this.storagePaths.baseDir);
|
startDebugServer(this.manager, this.storagePaths.baseDir);
|
||||||
this.runtimeStatsTimer = setInterval(() => {
|
this.runtimeStatsTimer = setInterval(() => {
|
||||||
this.manager.persistRuntimeStats();
|
this.manager.persistRuntimeStats();
|
||||||
this.settings = this.manager.getSettings();
|
this.settings = this.manager.getSettings();
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
this.runtimeStatsTimer.unref?.();
|
this.runtimeStatsTimer.unref?.();
|
||||||
|
|
||||||
if (this.settings.autoResumeOnStart) {
|
if (this.settings.autoResumeOnStart) {
|
||||||
const snapshot = this.manager.getSnapshot();
|
const snapshot = this.manager.getSnapshot();
|
||||||
const hasPending = Object.values(snapshot.session.items).some((item) => item.status === "queued" || item.status === "reconnect_wait");
|
const hasPending = Object.values(snapshot.session.items).some((item) => item.status === "queued" || item.status === "reconnect_wait");
|
||||||
if (hasPending) {
|
if (hasPending) {
|
||||||
@ -187,46 +187,46 @@ export class AppController {
|
|||||||
return APP_VERSION;
|
return APP_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSettings(): AppSettings {
|
public getSettings(): AppSettings {
|
||||||
return this.settings;
|
return this.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAuditLogPath(): string | null {
|
public getAuditLogPath(): string | null {
|
||||||
return getAuditLogPath();
|
return getAuditLogPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRenameLogPath(): string | null {
|
public getRenameLogPath(): string | null {
|
||||||
return getRenameLogPath();
|
return getRenameLogPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTraceLogPath(): string | null {
|
public getTraceLogPath(): string | null {
|
||||||
return getTraceLogPath();
|
return getTraceLogPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTraceConfig(): SupportTraceConfig {
|
public getTraceConfig(): SupportTraceConfig {
|
||||||
return getTraceConfig();
|
return getTraceConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
public rotateDebugToken(): { path: string; token: string } {
|
public rotateDebugToken(): { path: string; token: string } {
|
||||||
const rotated = rotateDebugToken(this.storagePaths.baseDir);
|
const rotated = rotateDebugToken(this.storagePaths.baseDir);
|
||||||
this.audit("WARN", "Debug-Token rotiert", { path: rotated.path });
|
this.audit("WARN", "Debug-Token rotiert", { path: rotated.path });
|
||||||
return rotated;
|
return rotated;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDebugSetupCheck(): DebugSetupCheckResult {
|
public getDebugSetupCheck(): DebugSetupCheckResult {
|
||||||
return getDebugSetupCheck(this.storagePaths.baseDir);
|
return getDebugSetupCheck(this.storagePaths.baseDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
private audit(level: "INFO" | "WARN" | "ERROR", message: string, fields?: Record<string, unknown>): void {
|
private audit(level: "INFO" | "WARN" | "ERROR", message: string, fields?: Record<string, unknown>): void {
|
||||||
logAuditEvent(level, message, fields);
|
logAuditEvent(level, message, fields);
|
||||||
logTraceEvent(level, "audit", message, fields);
|
logTraceEvent(level, "audit", message, fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setTraceEnabled(enabled: boolean, note = "", durationMs?: number): SupportTraceConfig {
|
public setTraceEnabled(enabled: boolean, note = "", durationMs?: number): SupportTraceConfig {
|
||||||
const next = setTraceEnabled(enabled, note, durationMs);
|
const next = setTraceEnabled(enabled, note, durationMs);
|
||||||
this.audit("INFO", enabled ? "Support-Trace aktiviert" : "Support-Trace deaktiviert", { note });
|
this.audit("INFO", enabled ? "Support-Trace aktiviert" : "Support-Trace deaktiviert", { note });
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateSettings(partial: Partial<AppSettings>): AppSettings {
|
public updateSettings(partial: Partial<AppSettings>): AppSettings {
|
||||||
const sanitizedPatch = sanitizeSettingsPatch(partial);
|
const sanitizedPatch = sanitizeSettingsPatch(partial);
|
||||||
@ -240,32 +240,32 @@ export class AppController {
|
|||||||
return previousSettings;
|
return previousSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preserve the live all-time counters from the download manager
|
// Preserve the live all-time counters from the download manager
|
||||||
const liveSettings = this.manager.getSettings();
|
const liveSettings = this.manager.getSettings();
|
||||||
nextSettings.totalDownloadedAllTime = Math.max(nextSettings.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0);
|
nextSettings.totalDownloadedAllTime = Math.max(nextSettings.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0);
|
||||||
nextSettings.totalCompletedFilesAllTime = Math.max(nextSettings.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0);
|
nextSettings.totalCompletedFilesAllTime = Math.max(nextSettings.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0);
|
||||||
nextSettings.totalRuntimeAllTimeMs = Math.max(nextSettings.totalRuntimeAllTimeMs || 0, this.manager.getLiveTotalRuntimeMs());
|
nextSettings.totalRuntimeAllTimeMs = Math.max(nextSettings.totalRuntimeAllTimeMs || 0, this.manager.getLiveTotalRuntimeMs());
|
||||||
nextSettings.providerDailyUsageDay = liveSettings.providerDailyUsageDay;
|
nextSettings.providerDailyUsageDay = liveSettings.providerDailyUsageDay;
|
||||||
nextSettings.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) };
|
nextSettings.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) };
|
||||||
nextSettings.providerTotalUsageBytes = { ...(liveSettings.providerTotalUsageBytes || {}) };
|
nextSettings.providerTotalUsageBytes = { ...(liveSettings.providerTotalUsageBytes || {}) };
|
||||||
nextSettings.debridLinkApiKeyDailyUsageBytes = Object.fromEntries(
|
nextSettings.debridLinkApiKeyDailyUsageBytes = Object.fromEntries(
|
||||||
Object.entries(liveSettings.debridLinkApiKeyDailyUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(nextSettings.debridLinkApiKeys).includes(keyId))
|
Object.entries(liveSettings.debridLinkApiKeyDailyUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(nextSettings.debridLinkApiKeys).includes(keyId))
|
||||||
);
|
);
|
||||||
nextSettings.debridLinkApiKeyTotalUsageBytes = Object.fromEntries(
|
nextSettings.debridLinkApiKeyTotalUsageBytes = Object.fromEntries(
|
||||||
Object.entries(liveSettings.debridLinkApiKeyTotalUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(nextSettings.debridLinkApiKeys).includes(keyId))
|
Object.entries(liveSettings.debridLinkApiKeyTotalUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(nextSettings.debridLinkApiKeys).includes(keyId))
|
||||||
);
|
);
|
||||||
const retentionChanged = previousSettings.historyRetentionMode !== nextSettings.historyRetentionMode;
|
const retentionChanged = previousSettings.historyRetentionMode !== nextSettings.historyRetentionMode;
|
||||||
this.settings = nextSettings;
|
this.settings = nextSettings;
|
||||||
if (retentionChanged) {
|
if (retentionChanged) {
|
||||||
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
||||||
}
|
}
|
||||||
saveSettings(this.storagePaths, this.settings);
|
saveSettings(this.storagePaths, this.settings);
|
||||||
this.manager.setSettings(this.settings);
|
this.manager.setSettings(this.settings);
|
||||||
this.audit("INFO", "Einstellungen aktualisiert", {
|
this.audit("INFO", "Einstellungen aktualisiert", {
|
||||||
changedKeys: Object.keys(sanitizedPatch),
|
changedKeys: Object.keys(sanitizedPatch),
|
||||||
accountChanges: diffAccountSummary(previousSettings, this.settings)
|
accountChanges: diffAccountSummary(previousSettings, this.settings)
|
||||||
});
|
});
|
||||||
if (previousSettings.rememberToken && !this.settings.rememberToken) {
|
if (previousSettings.rememberToken && !this.settings.rememberToken) {
|
||||||
void this.realDebridWebFallback.clearSessions().catch((error) => {
|
void this.realDebridWebFallback.clearSessions().catch((error) => {
|
||||||
logger.warn(`Real-Debrid Web-Session konnte nicht gelöscht werden: ${String(error)}`);
|
logger.warn(`Real-Debrid Web-Session konnte nicht gelöscht werden: ${String(error)}`);
|
||||||
});
|
});
|
||||||
@ -285,12 +285,12 @@ export class AppController {
|
|||||||
...liveSettings,
|
...liveSettings,
|
||||||
...resetProviderDailyUsage(liveSettings, provider)
|
...resetProviderDailyUsage(liveSettings, provider)
|
||||||
});
|
});
|
||||||
this.settings = nextSettings;
|
this.settings = nextSettings;
|
||||||
saveSettings(this.storagePaths, this.settings);
|
saveSettings(this.storagePaths, this.settings);
|
||||||
this.manager.setSettings(this.settings);
|
this.manager.setSettings(this.settings);
|
||||||
this.audit("INFO", "Provider-Tagesnutzung zurückgesetzt", { provider });
|
this.audit("INFO", "Provider-Tagesnutzung zurückgesetzt", { provider });
|
||||||
return this.settings;
|
return this.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public resetDebridLinkApiKeyDailyUsage(keyId: string): AppSettings {
|
public resetDebridLinkApiKeyDailyUsage(keyId: string): AppSettings {
|
||||||
const liveSettings = this.manager.getSettings();
|
const liveSettings = this.manager.getSettings();
|
||||||
@ -298,31 +298,31 @@ export class AppController {
|
|||||||
...liveSettings,
|
...liveSettings,
|
||||||
...resetDebridLinkApiKeyDailyUsage(liveSettings, keyId)
|
...resetDebridLinkApiKeyDailyUsage(liveSettings, keyId)
|
||||||
});
|
});
|
||||||
this.settings = nextSettings;
|
this.settings = nextSettings;
|
||||||
saveSettings(this.storagePaths, this.settings);
|
saveSettings(this.storagePaths, this.settings);
|
||||||
this.manager.setSettings(this.settings);
|
this.manager.setSettings(this.settings);
|
||||||
this.audit("INFO", "Debrid-Link-Key-Tagesnutzung zurückgesetzt", { keyId });
|
this.audit("INFO", "Debrid-Link-Key-Tagesnutzung zurückgesetzt", { keyId });
|
||||||
return this.settings;
|
return this.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openRealDebridLoginWindow(): Promise<void> {
|
public async openRealDebridLoginWindow(): Promise<void> {
|
||||||
this.audit("INFO", "Real-Debrid Login-Fenster geöffnet");
|
this.audit("INFO", "Real-Debrid Login-Fenster geöffnet");
|
||||||
await this.realDebridWebFallback.openLoginWindow();
|
await this.realDebridWebFallback.openLoginWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openAllDebridLoginWindow(): Promise<void> {
|
public async openAllDebridLoginWindow(): Promise<void> {
|
||||||
this.audit("INFO", "AllDebrid Login-Fenster geöffnet");
|
this.audit("INFO", "AllDebrid Login-Fenster geöffnet");
|
||||||
await this.allDebridWebFallback.openLoginWindow();
|
await this.allDebridWebFallback.openLoginWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async importBestDebridCookies(filePath: string): Promise<number> {
|
public async importBestDebridCookies(filePath: string): Promise<number> {
|
||||||
const imported = await this.bestDebridWebFallback.importCookiesFromFile(filePath);
|
const imported = await this.bestDebridWebFallback.importCookiesFromFile(filePath);
|
||||||
this.audit("INFO", "BestDebrid Cookies importiert", {
|
this.audit("INFO", "BestDebrid Cookies importiert", {
|
||||||
filePath,
|
filePath,
|
||||||
imported
|
imported
|
||||||
});
|
});
|
||||||
return imported;
|
return imported;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAllDebridHostInfo(host = "rapidgator"): Promise<AllDebridHostInfo> {
|
public async getAllDebridHostInfo(host = "rapidgator"): Promise<AllDebridHostInfo> {
|
||||||
if (this.settings.allDebridUseWebLogin) {
|
if (this.settings.allDebridUseWebLogin) {
|
||||||
@ -354,6 +354,9 @@ export class AppController {
|
|||||||
if (this.manager.isSessionRunning()) {
|
if (this.manager.isSessionRunning()) {
|
||||||
this.manager.stop();
|
this.manager.stop();
|
||||||
}
|
}
|
||||||
|
// Flush any pending async saves BEFORE the update process starts.
|
||||||
|
// This ensures the queue is fully persisted to disk so it survives the restart.
|
||||||
|
this.manager.persistNowSync();
|
||||||
|
|
||||||
const cacheAgeMs = Date.now() - this.lastUpdateCheckAt;
|
const cacheAgeMs = Date.now() - this.lastUpdateCheckAt;
|
||||||
const cached = this.lastUpdateCheck && !this.lastUpdateCheck.error && cacheAgeMs <= 10 * 60 * 1000
|
const cached = this.lastUpdateCheck && !this.lastUpdateCheck.error && cacheAgeMs <= 10 * 60 * 1000
|
||||||
@ -367,38 +370,38 @@ export class AppController {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public addLinks(payload: AddLinksPayload): { addedPackages: number; addedLinks: number; invalidCount: number } {
|
public addLinks(payload: AddLinksPayload): { addedPackages: number; addedLinks: number; invalidCount: number } {
|
||||||
const parsed = parseCollectorInput(payload.rawText, payload.packageName || this.settings.packageName);
|
const parsed = parseCollectorInput(payload.rawText, payload.packageName || this.settings.packageName);
|
||||||
if (parsed.length === 0) {
|
if (parsed.length === 0) {
|
||||||
this.audit("WARN", "Links hinzufügen ohne gültigen Inhalt", {
|
this.audit("WARN", "Links hinzufügen ohne gültigen Inhalt", {
|
||||||
hasPackageName: Boolean(payload.packageName)
|
hasPackageName: Boolean(payload.packageName)
|
||||||
});
|
});
|
||||||
return { addedPackages: 0, addedLinks: 0, invalidCount: 1 };
|
return { addedPackages: 0, addedLinks: 0, invalidCount: 1 };
|
||||||
}
|
}
|
||||||
const result = this.manager.addPackages(parsed);
|
const result = this.manager.addPackages(parsed);
|
||||||
this.audit("INFO", "Links hinzugefügt", {
|
this.audit("INFO", "Links hinzugefügt", {
|
||||||
addedPackages: result.addedPackages,
|
addedPackages: result.addedPackages,
|
||||||
addedLinks: result.addedLinks,
|
addedLinks: result.addedLinks,
|
||||||
requestedPackages: parsed.length
|
requestedPackages: parsed.length
|
||||||
});
|
});
|
||||||
return { ...result, invalidCount: 0 };
|
return { ...result, invalidCount: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addContainers(filePaths: string[]): Promise<{ addedPackages: number; addedLinks: number }> {
|
public async addContainers(filePaths: string[]): Promise<{ addedPackages: number; addedLinks: number }> {
|
||||||
const packages = await importDlcContainers(filePaths);
|
const packages = await importDlcContainers(filePaths);
|
||||||
const merged: ParsedPackageInput[] = packages.map((pkg) => ({
|
const merged: ParsedPackageInput[] = packages.map((pkg) => ({
|
||||||
name: pkg.name,
|
name: pkg.name,
|
||||||
links: pkg.links,
|
links: pkg.links,
|
||||||
...(pkg.fileNames ? { fileNames: pkg.fileNames } : {})
|
...(pkg.fileNames ? { fileNames: pkg.fileNames } : {})
|
||||||
}));
|
}));
|
||||||
const result = this.manager.addPackages(merged);
|
const result = this.manager.addPackages(merged);
|
||||||
this.audit("INFO", "Container importiert", {
|
this.audit("INFO", "Container importiert", {
|
||||||
files: filePaths.length,
|
files: filePaths.length,
|
||||||
addedPackages: result.addedPackages,
|
addedPackages: result.addedPackages,
|
||||||
addedLinks: result.addedLinks
|
addedLinks: result.addedLinks
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getStartConflicts(): Promise<StartConflictEntry[]> {
|
public async getStartConflicts(): Promise<StartConflictEntry[]> {
|
||||||
return this.manager.getStartConflicts();
|
return this.manager.getStartConflicts();
|
||||||
@ -408,163 +411,163 @@ export class AppController {
|
|||||||
return this.manager.resolveStartConflict(packageId, policy);
|
return this.manager.resolveStartConflict(packageId, policy);
|
||||||
}
|
}
|
||||||
|
|
||||||
public clearAll(): void {
|
public clearAll(): void {
|
||||||
this.audit("WARN", "Queue komplett geleert");
|
this.audit("WARN", "Queue komplett geleert");
|
||||||
this.manager.clearAll();
|
this.manager.clearAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start(): Promise<void> {
|
|
||||||
this.audit("INFO", "Session-Start ausgelöst");
|
|
||||||
await this.manager.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async startPackages(packageIds: string[]): Promise<void> {
|
|
||||||
this.audit("INFO", "Paket-Start ausgelöst", { packageIds });
|
|
||||||
await this.manager.startPackages(packageIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async startItems(itemIds: string[]): Promise<void> {
|
|
||||||
this.audit("INFO", "Item-Start ausgelöst", { itemIds });
|
|
||||||
await this.manager.startItems(itemIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public stop(): void {
|
|
||||||
this.audit("INFO", "Session-Stopp ausgelöst");
|
|
||||||
this.manager.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public togglePause(): boolean {
|
|
||||||
const paused = this.manager.togglePause();
|
|
||||||
this.audit("INFO", "Pause umgeschaltet", { paused });
|
|
||||||
return paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
public retryExtraction(packageId: string): void {
|
|
||||||
this.audit("INFO", "Extraktion manuell wiederholt", { packageId });
|
|
||||||
this.manager.retryExtraction(packageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public extractNow(packageId: string): void {
|
|
||||||
this.audit("INFO", "Jetzt entpacken ausgelöst", { packageId });
|
|
||||||
this.manager.extractNow(packageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public resetPackage(packageId: string): void {
|
|
||||||
this.audit("INFO", "Paket zurückgesetzt", { packageId });
|
|
||||||
this.manager.resetPackage(packageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public cancelPackage(packageId: string): void {
|
|
||||||
this.audit("WARN", "Paket abgebrochen", { packageId });
|
|
||||||
this.manager.cancelPackage(packageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public renamePackage(packageId: string, newName: string): void {
|
|
||||||
this.audit("INFO", "Paket umbenannt", { packageId, newName });
|
|
||||||
this.manager.renamePackage(packageId, newName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public reorderPackages(packageIds: string[]): void {
|
|
||||||
this.audit("INFO", "Paketreihenfolge geändert", { packageIds });
|
|
||||||
this.manager.reorderPackages(packageIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeItem(itemId: string): void {
|
|
||||||
this.audit("WARN", "Item entfernt", { itemId });
|
|
||||||
this.manager.removeItem(itemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public togglePackage(packageId: string): void {
|
public async start(): Promise<void> {
|
||||||
this.audit("INFO", "Paket aktiviert/deaktiviert", { packageId });
|
this.audit("INFO", "Session-Start ausgelöst");
|
||||||
this.manager.togglePackage(packageId);
|
await this.manager.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public exportPackageSelection(packageIds: string[]): { text: string; defaultFileName: string; packageCount: number; linkCount: number } {
|
|
||||||
const selection = buildLinkExportSelection(this.manager.getSnapshot(), packageIds, []);
|
|
||||||
this.audit("INFO", "Paket-Auswahl exportiert", {
|
|
||||||
packageCount: selection.packageCount,
|
|
||||||
linkCount: selection.linkCount,
|
|
||||||
packageIds
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
text: serializeLinkExportText(selection.packages),
|
|
||||||
defaultFileName: selection.defaultFileName,
|
|
||||||
packageCount: selection.packageCount,
|
|
||||||
linkCount: selection.linkCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public exportItemSelection(itemIds: string[]): { text: string; defaultFileName: string; packageCount: number; linkCount: number } {
|
|
||||||
const selection = buildLinkExportSelection(this.manager.getSnapshot(), [], itemIds);
|
|
||||||
this.audit("INFO", "Item-Auswahl exportiert", {
|
|
||||||
packageCount: selection.packageCount,
|
|
||||||
linkCount: selection.linkCount,
|
|
||||||
itemIds
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
text: serializeLinkExportText(selection.packages),
|
|
||||||
defaultFileName: selection.defaultFileName,
|
|
||||||
packageCount: selection.packageCount,
|
|
||||||
linkCount: selection.linkCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public exportQueue(): string {
|
|
||||||
return this.manager.exportQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
public importQueue(json: string): { addedPackages: number; addedLinks: number } {
|
|
||||||
const result = this.manager.importQueue(json);
|
|
||||||
this.audit("INFO", "Import-Datei verarbeitet", result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getSessionStats(): SessionStats {
|
public async startPackages(packageIds: string[]): Promise<void> {
|
||||||
return this.manager.getSessionStats();
|
this.audit("INFO", "Paket-Start ausgelöst", { packageIds });
|
||||||
}
|
await this.manager.startPackages(packageIds);
|
||||||
|
}
|
||||||
public resetSessionStats(): void {
|
|
||||||
this.audit("INFO", "Session-Statistik zurückgesetzt");
|
|
||||||
this.manager.resetSessionStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
public resetDownloadStats(): void {
|
|
||||||
this.manager.resetDownloadStats();
|
|
||||||
this.settings = this.manager.getSettings();
|
|
||||||
this.audit("INFO", "Download-Statistik zurückgesetzt");
|
|
||||||
}
|
|
||||||
|
|
||||||
public exportBackup(): Buffer {
|
public async startItems(itemIds: string[]): Promise<void> {
|
||||||
const settings = { ...this.settings };
|
this.audit("INFO", "Item-Start ausgelöst", { itemIds });
|
||||||
const session = this.manager.getSession();
|
await this.manager.startItems(itemIds);
|
||||||
const history = loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
}
|
||||||
|
|
||||||
|
public stop(): void {
|
||||||
|
this.audit("INFO", "Session-Stopp ausgelöst");
|
||||||
|
this.manager.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public togglePause(): boolean {
|
||||||
|
const paused = this.manager.togglePause();
|
||||||
|
this.audit("INFO", "Pause umgeschaltet", { paused });
|
||||||
|
return paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
public retryExtraction(packageId: string): void {
|
||||||
|
this.audit("INFO", "Extraktion manuell wiederholt", { packageId });
|
||||||
|
this.manager.retryExtraction(packageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public extractNow(packageId: string): void {
|
||||||
|
this.audit("INFO", "Jetzt entpacken ausgelöst", { packageId });
|
||||||
|
this.manager.extractNow(packageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetPackage(packageId: string): void {
|
||||||
|
this.audit("INFO", "Paket zurückgesetzt", { packageId });
|
||||||
|
this.manager.resetPackage(packageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancelPackage(packageId: string): void {
|
||||||
|
this.audit("WARN", "Paket abgebrochen", { packageId });
|
||||||
|
this.manager.cancelPackage(packageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public renamePackage(packageId: string, newName: string): void {
|
||||||
|
this.audit("INFO", "Paket umbenannt", { packageId, newName });
|
||||||
|
this.manager.renamePackage(packageId, newName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public reorderPackages(packageIds: string[]): void {
|
||||||
|
this.audit("INFO", "Paketreihenfolge geändert", { packageIds });
|
||||||
|
this.manager.reorderPackages(packageIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeItem(itemId: string): void {
|
||||||
|
this.audit("WARN", "Item entfernt", { itemId });
|
||||||
|
this.manager.removeItem(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public togglePackage(packageId: string): void {
|
||||||
|
this.audit("INFO", "Paket aktiviert/deaktiviert", { packageId });
|
||||||
|
this.manager.togglePackage(packageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public exportPackageSelection(packageIds: string[]): { text: string; defaultFileName: string; packageCount: number; linkCount: number } {
|
||||||
|
const selection = buildLinkExportSelection(this.manager.getSnapshot(), packageIds, []);
|
||||||
|
this.audit("INFO", "Paket-Auswahl exportiert", {
|
||||||
|
packageCount: selection.packageCount,
|
||||||
|
linkCount: selection.linkCount,
|
||||||
|
packageIds
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
text: serializeLinkExportText(selection.packages),
|
||||||
|
defaultFileName: selection.defaultFileName,
|
||||||
|
packageCount: selection.packageCount,
|
||||||
|
linkCount: selection.linkCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public exportItemSelection(itemIds: string[]): { text: string; defaultFileName: string; packageCount: number; linkCount: number } {
|
||||||
|
const selection = buildLinkExportSelection(this.manager.getSnapshot(), [], itemIds);
|
||||||
|
this.audit("INFO", "Item-Auswahl exportiert", {
|
||||||
|
packageCount: selection.packageCount,
|
||||||
|
linkCount: selection.linkCount,
|
||||||
|
itemIds
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
text: serializeLinkExportText(selection.packages),
|
||||||
|
defaultFileName: selection.defaultFileName,
|
||||||
|
packageCount: selection.packageCount,
|
||||||
|
linkCount: selection.linkCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public exportQueue(): string {
|
||||||
|
return this.manager.exportQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public importQueue(json: string): { addedPackages: number; addedLinks: number } {
|
||||||
|
const result = this.manager.importQueue(json);
|
||||||
|
this.audit("INFO", "Import-Datei verarbeitet", result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSessionStats(): SessionStats {
|
||||||
|
return this.manager.getSessionStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetSessionStats(): void {
|
||||||
|
this.audit("INFO", "Session-Statistik zurückgesetzt");
|
||||||
|
this.manager.resetSessionStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetDownloadStats(): void {
|
||||||
|
this.manager.resetDownloadStats();
|
||||||
|
this.settings = this.manager.getSettings();
|
||||||
|
this.audit("INFO", "Download-Statistik zurückgesetzt");
|
||||||
|
}
|
||||||
|
|
||||||
|
public exportBackup(): Buffer {
|
||||||
|
const settings = { ...this.settings };
|
||||||
|
const session = this.manager.getSession();
|
||||||
|
const history = loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
version: 2,
|
version: 2,
|
||||||
appVersion: APP_VERSION,
|
appVersion: APP_VERSION,
|
||||||
exportedAt: new Date().toISOString(),
|
exportedAt: new Date().toISOString(),
|
||||||
settings,
|
settings,
|
||||||
session,
|
session,
|
||||||
history
|
history
|
||||||
});
|
});
|
||||||
this.audit("INFO", "Backup exportiert", {
|
this.audit("INFO", "Backup exportiert", {
|
||||||
historyEntries: history.length,
|
historyEntries: history.length,
|
||||||
sessionItems: Object.keys(session.items).length,
|
sessionItems: Object.keys(session.items).length,
|
||||||
sessionPackages: Object.keys(session.packages).length
|
sessionPackages: Object.keys(session.packages).length
|
||||||
});
|
});
|
||||||
return encryptBackup(payload);
|
return encryptBackup(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
public exportSupportBundle(): { buffer: Buffer; defaultFileName: string } {
|
public exportSupportBundle(): { buffer: Buffer; defaultFileName: string } {
|
||||||
this.audit("INFO", "Support-Bundle exportiert");
|
this.audit("INFO", "Support-Bundle exportiert");
|
||||||
logTraceEvent("INFO", "support", "Support-Bundle erstellt", {
|
logTraceEvent("INFO", "support", "Support-Bundle erstellt", {
|
||||||
packageCount: Object.keys(this.manager.getSnapshot().session.packages).length,
|
packageCount: Object.keys(this.manager.getSnapshot().session.packages).length,
|
||||||
itemCount: Object.keys(this.manager.getSnapshot().session.items).length
|
itemCount: Object.keys(this.manager.getSnapshot().session.items).length
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
buffer: buildSupportBundle(this.manager, this.storagePaths.baseDir),
|
buffer: buildSupportBundle(this.manager, this.storagePaths.baseDir),
|
||||||
defaultFileName: getSupportBundleDefaultFileName()
|
defaultFileName: getSupportBundleDefaultFileName()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public importBackup(data: Buffer): { restored: boolean; message: string } {
|
public importBackup(data: Buffer): { restored: boolean; message: string } {
|
||||||
let parsed: Record<string, unknown>;
|
let parsed: Record<string, unknown>;
|
||||||
@ -586,21 +589,21 @@ export class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Restore settings — ALL credentials are included (no more masking)
|
// Restore settings — ALL credentials are included (no more masking)
|
||||||
const importedSettings = parsed.settings as AppSettings;
|
const importedSettings = parsed.settings as AppSettings;
|
||||||
const importedSettingsRecord = importedSettings as unknown as Record<string, unknown>;
|
const importedSettingsRecord = importedSettings as unknown as Record<string, unknown>;
|
||||||
const currentSettingsRecord = this.settings as unknown as Record<string, unknown>;
|
const currentSettingsRecord = this.settings as unknown as Record<string, unknown>;
|
||||||
// Legacy backup compatibility: if credentials were masked with ***, keep current values
|
// Legacy backup compatibility: if credentials were masked with ***, keep current values
|
||||||
const SENSITIVE_KEYS: (keyof AppSettings)[] = [
|
const SENSITIVE_KEYS: (keyof AppSettings)[] = [
|
||||||
"token", "megaLogin", "megaPassword", "bestToken", "allDebridToken",
|
"token", "megaLogin", "megaPassword", "bestToken", "allDebridToken",
|
||||||
"ddownloadLogin", "ddownloadPassword", "oneFichierApiKey",
|
"ddownloadLogin", "ddownloadPassword", "oneFichierApiKey",
|
||||||
"debridLinkApiKeys", "linkSnappyLogin", "linkSnappyPassword"
|
"debridLinkApiKeys", "linkSnappyLogin", "linkSnappyPassword"
|
||||||
];
|
];
|
||||||
for (const key of SENSITIVE_KEYS) {
|
for (const key of SENSITIVE_KEYS) {
|
||||||
const val = importedSettingsRecord[key];
|
const val = importedSettingsRecord[key];
|
||||||
if (typeof val === "string" && val.startsWith("***")) {
|
if (typeof val === "string" && val.startsWith("***")) {
|
||||||
importedSettingsRecord[key] = currentSettingsRecord[key];
|
importedSettingsRecord[key] = currentSettingsRecord[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const restoredSettings = normalizeSettings(importedSettings);
|
const restoredSettings = normalizeSettings(importedSettings);
|
||||||
this.settings = restoredSettings;
|
this.settings = restoredSettings;
|
||||||
saveSettings(this.storagePaths, this.settings);
|
saveSettings(this.storagePaths, this.settings);
|
||||||
@ -629,93 +632,93 @@ export class AppController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
||||||
|
|
||||||
// Prevent prepareForShutdown from overwriting the restored data
|
// Prevent prepareForShutdown from overwriting the restored data
|
||||||
this.manager.skipShutdownPersist = true;
|
this.manager.skipShutdownPersist = true;
|
||||||
this.manager.blockAllPersistence = true;
|
this.manager.blockAllPersistence = true;
|
||||||
logger.info("Backup wiederhergestellt (verschlüsseltes Format)");
|
logger.info("Backup wiederhergestellt (verschlüsseltes Format)");
|
||||||
this.audit("WARN", "Backup importiert", {
|
this.audit("WARN", "Backup importiert", {
|
||||||
historyEntries: Array.isArray(parsed.history) ? parsed.history.length : 0,
|
historyEntries: Array.isArray(parsed.history) ? parsed.history.length : 0,
|
||||||
accountSummary: buildAccountSummary(this.settings)
|
accountSummary: buildAccountSummary(this.settings)
|
||||||
});
|
});
|
||||||
return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." };
|
return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." };
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSessionLogPath(): string | null {
|
public getSessionLogPath(): string | null {
|
||||||
return getSessionLogPath();
|
return getSessionLogPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPackageLogPath(packageId: string): string | null {
|
public getPackageLogPath(packageId: string): string | null {
|
||||||
return this.manager.getPackageLogPath(packageId) || getPackageLogPath(packageId);
|
return this.manager.getPackageLogPath(packageId) || getPackageLogPath(packageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getItemLogPath(itemId: string): string | null {
|
public getItemLogPath(itemId: string): string | null {
|
||||||
return this.manager.getItemLogPath(itemId) || getItemLogPath(itemId);
|
return this.manager.getItemLogPath(itemId) || getItemLogPath(itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public shutdown(): void {
|
public shutdown(): void {
|
||||||
if (this.runtimeStatsTimer) {
|
if (this.runtimeStatsTimer) {
|
||||||
clearInterval(this.runtimeStatsTimer);
|
clearInterval(this.runtimeStatsTimer);
|
||||||
this.runtimeStatsTimer = null;
|
this.runtimeStatsTimer = null;
|
||||||
}
|
}
|
||||||
stopDebugServer();
|
stopDebugServer();
|
||||||
abortActiveUpdateDownload();
|
abortActiveUpdateDownload();
|
||||||
this.manager.prepareForShutdown();
|
this.manager.prepareForShutdown();
|
||||||
this.megaWebFallback.dispose();
|
this.megaWebFallback.dispose();
|
||||||
this.realDebridWebFallback.dispose();
|
this.realDebridWebFallback.dispose();
|
||||||
this.allDebridWebFallback.dispose();
|
this.allDebridWebFallback.dispose();
|
||||||
this.bestDebridWebFallback.dispose();
|
this.bestDebridWebFallback.dispose();
|
||||||
shutdownSessionLog();
|
shutdownSessionLog();
|
||||||
shutdownPackageLogs();
|
shutdownPackageLogs();
|
||||||
shutdownItemLogs();
|
shutdownItemLogs();
|
||||||
shutdownRenameLog();
|
shutdownRenameLog();
|
||||||
this.audit("INFO", "App beendet");
|
this.audit("INFO", "App beendet");
|
||||||
shutdownTraceLog();
|
shutdownTraceLog();
|
||||||
shutdownAuditLog();
|
shutdownAuditLog();
|
||||||
if (this.settings.historyRetentionMode === "session") {
|
if (this.settings.historyRetentionMode === "session") {
|
||||||
clearHistory(this.storagePaths);
|
clearHistory(this.storagePaths);
|
||||||
}
|
}
|
||||||
logger.info("App beendet");
|
logger.info("App beendet");
|
||||||
}
|
}
|
||||||
|
|
||||||
public getHistory(): HistoryEntry[] {
|
|
||||||
return loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearHistory(): void {
|
public getHistory(): HistoryEntry[] {
|
||||||
this.audit("WARN", "Verlauf geleert");
|
return loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
||||||
clearHistory(this.storagePaths);
|
}
|
||||||
}
|
|
||||||
|
public clearHistory(): void {
|
||||||
public setPackagePriority(packageId: string, priority: PackagePriority): void {
|
this.audit("WARN", "Verlauf geleert");
|
||||||
this.audit("INFO", "Paket-Priorität geändert", { packageId, priority });
|
clearHistory(this.storagePaths);
|
||||||
this.manager.setPackagePriority(packageId, priority);
|
}
|
||||||
}
|
|
||||||
|
public setPackagePriority(packageId: string, priority: PackagePriority): void {
|
||||||
public skipItems(itemIds: string[]): void {
|
this.audit("INFO", "Paket-Priorität geändert", { packageId, priority });
|
||||||
this.audit("INFO", "Items übersprungen", { itemIds });
|
this.manager.setPackagePriority(packageId, priority);
|
||||||
this.manager.skipItems(itemIds);
|
}
|
||||||
}
|
|
||||||
|
public skipItems(itemIds: string[]): void {
|
||||||
public resetItems(itemIds: string[]): void {
|
this.audit("INFO", "Items übersprungen", { itemIds });
|
||||||
this.audit("INFO", "Items zurückgesetzt", { itemIds });
|
this.manager.skipItems(itemIds);
|
||||||
this.manager.resetItems(itemIds);
|
}
|
||||||
}
|
|
||||||
|
public resetItems(itemIds: string[]): void {
|
||||||
public removeHistoryEntry(entryId: string): void {
|
this.audit("INFO", "Items zurückgesetzt", { itemIds });
|
||||||
this.audit("INFO", "Verlaufseintrag entfernt", { entryId });
|
this.manager.resetItems(itemIds);
|
||||||
removeHistoryEntry(this.storagePaths, entryId);
|
}
|
||||||
}
|
|
||||||
|
public removeHistoryEntry(entryId: string): void {
|
||||||
public addToHistory(entry: HistoryEntry): void {
|
this.audit("INFO", "Verlaufseintrag entfernt", { entryId });
|
||||||
this.audit("INFO", "Verlaufseintrag hinzugefügt", {
|
removeHistoryEntry(this.storagePaths, entryId);
|
||||||
id: entry.id,
|
}
|
||||||
name: entry.name,
|
|
||||||
status: entry.status,
|
public addToHistory(entry: HistoryEntry): void {
|
||||||
provider: entry.provider,
|
this.audit("INFO", "Verlaufseintrag hinzugefügt", {
|
||||||
fileCount: entry.fileCount
|
id: entry.id,
|
||||||
});
|
name: entry.name,
|
||||||
addHistoryEntry(this.storagePaths, entry);
|
status: entry.status,
|
||||||
}
|
provider: entry.provider,
|
||||||
|
fileCount: entry.fileCount
|
||||||
|
});
|
||||||
|
addHistoryEntry(this.storagePaths, entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4696,9 +4696,12 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.pacedStartReservationByItem.clear();
|
this.pacedStartReservationByItem.clear();
|
||||||
this.nonResumableActive = 0;
|
this.nonResumableActive = 0;
|
||||||
this.session.summaryText = "";
|
this.session.summaryText = "";
|
||||||
// Persist synchronously on shutdown to guarantee data is written before process exits
|
// Persist synchronously on shutdown to guarantee data is written before process exits.
|
||||||
// Skip if a backup was just imported — the restored session on disk must not be overwritten
|
// Only skip if a backup was just imported (skipShutdownPersist) — the restored session
|
||||||
if (!this.skipShutdownPersist && !this.blockAllPersistence) {
|
// on disk must not be overwritten. blockAllPersistence is intentionally NOT checked
|
||||||
|
// here: it guards async/periodic saves during runtime, but shutdown must always persist
|
||||||
|
// to prevent queue loss across restarts/updates.
|
||||||
|
if (!this.skipShutdownPersist) {
|
||||||
const pkgCount = Object.keys(this.session.packages).length;
|
const pkgCount = Object.keys(this.session.packages).length;
|
||||||
const itemCount = Object.keys(this.session.items).length;
|
const itemCount = Object.keys(this.session.items).length;
|
||||||
logger.info(`Shutdown-Save: ${pkgCount} Pakete, ${itemCount} Items`);
|
logger.info(`Shutdown-Save: ${pkgCount} Pakete, ${itemCount} Items`);
|
||||||
@ -5030,6 +5033,18 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Synchronous persist — guarantees state is on disk before returning.
|
||||||
|
* Used before update installs to prevent queue loss. */
|
||||||
|
public persistNowSync(): void {
|
||||||
|
this.clearPersistTimer();
|
||||||
|
const pkgCount = Object.keys(this.session.packages).length;
|
||||||
|
const itemCount = Object.keys(this.session.items).length;
|
||||||
|
logger.info(`Pre-Update Sync-Save: ${pkgCount} Pakete, ${itemCount} Items`);
|
||||||
|
this.foldRuntimeIntoSettings(nowMs());
|
||||||
|
saveSession(this.storagePaths, this.session);
|
||||||
|
saveSettings(this.storagePaths, this.settings);
|
||||||
|
}
|
||||||
|
|
||||||
private emitState(force = false): void {
|
private emitState(force = false): void {
|
||||||
const now = nowMs();
|
const now = nowMs();
|
||||||
const MIN_FORCE_GAP_MS = 120;
|
const MIN_FORCE_GAP_MS = 120;
|
||||||
|
|||||||
286
src/main/main.ts
286
src/main/main.ts
@ -257,7 +257,7 @@ function registerIpcHandlers(): void {
|
|||||||
if (result.started) {
|
if (result.started) {
|
||||||
updateQuitTimer = setTimeout(() => {
|
updateQuitTimer = setTimeout(() => {
|
||||||
app.quit();
|
app.quit();
|
||||||
}, 900);
|
}, 5000);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
@ -379,48 +379,48 @@ function registerIpcHandlers(): void {
|
|||||||
validateString(itemId, "itemId");
|
validateString(itemId, "itemId");
|
||||||
return controller.removeItem(itemId);
|
return controller.removeItem(itemId);
|
||||||
});
|
});
|
||||||
ipcMain.handle(IPC_CHANNELS.TOGGLE_PACKAGE, (_event: IpcMainInvokeEvent, packageId: string) => {
|
ipcMain.handle(IPC_CHANNELS.TOGGLE_PACKAGE, (_event: IpcMainInvokeEvent, packageId: string) => {
|
||||||
validateString(packageId, "packageId");
|
validateString(packageId, "packageId");
|
||||||
return controller.togglePackage(packageId);
|
return controller.togglePackage(packageId);
|
||||||
});
|
});
|
||||||
ipcMain.handle(IPC_CHANNELS.EXPORT_PACKAGE_SELECTION, async (_event: IpcMainInvokeEvent, packageIds: string[]) => {
|
ipcMain.handle(IPC_CHANNELS.EXPORT_PACKAGE_SELECTION, async (_event: IpcMainInvokeEvent, packageIds: string[]) => {
|
||||||
const validPackageIds = validateStringArray(packageIds ?? [], "packageIds");
|
const validPackageIds = validateStringArray(packageIds ?? [], "packageIds");
|
||||||
const exported = controller.exportPackageSelection(validPackageIds);
|
const exported = controller.exportPackageSelection(validPackageIds);
|
||||||
if (exported.packageCount === 0 || exported.linkCount === 0) {
|
if (exported.packageCount === 0 || exported.linkCount === 0) {
|
||||||
return { saved: false, packageCount: 0, linkCount: 0 };
|
return { saved: false, packageCount: 0, linkCount: 0 };
|
||||||
}
|
}
|
||||||
const options = {
|
const options = {
|
||||||
defaultPath: exported.defaultFileName,
|
defaultPath: exported.defaultFileName,
|
||||||
filters: [{ name: "Link Export", extensions: ["txt"] }]
|
filters: [{ name: "Link Export", extensions: ["txt"] }]
|
||||||
};
|
};
|
||||||
const result = mainWindow ? await dialog.showSaveDialog(mainWindow, options) : await dialog.showSaveDialog(options);
|
const result = mainWindow ? await dialog.showSaveDialog(mainWindow, options) : await dialog.showSaveDialog(options);
|
||||||
if (result.canceled || !result.filePath) {
|
if (result.canceled || !result.filePath) {
|
||||||
return { saved: false, packageCount: exported.packageCount, linkCount: exported.linkCount };
|
return { saved: false, packageCount: exported.packageCount, linkCount: exported.linkCount };
|
||||||
}
|
}
|
||||||
await fs.promises.writeFile(result.filePath, exported.text, "utf8");
|
await fs.promises.writeFile(result.filePath, exported.text, "utf8");
|
||||||
return { saved: true, packageCount: exported.packageCount, linkCount: exported.linkCount, filePath: result.filePath };
|
return { saved: true, packageCount: exported.packageCount, linkCount: exported.linkCount, filePath: result.filePath };
|
||||||
});
|
});
|
||||||
ipcMain.handle(IPC_CHANNELS.EXPORT_ITEM_SELECTION, async (_event: IpcMainInvokeEvent, itemIds: string[]) => {
|
ipcMain.handle(IPC_CHANNELS.EXPORT_ITEM_SELECTION, async (_event: IpcMainInvokeEvent, itemIds: string[]) => {
|
||||||
const validItemIds = validateStringArray(itemIds ?? [], "itemIds");
|
const validItemIds = validateStringArray(itemIds ?? [], "itemIds");
|
||||||
const exported = controller.exportItemSelection(validItemIds);
|
const exported = controller.exportItemSelection(validItemIds);
|
||||||
if (exported.packageCount === 0 || exported.linkCount === 0) {
|
if (exported.packageCount === 0 || exported.linkCount === 0) {
|
||||||
return { saved: false, packageCount: 0, linkCount: 0 };
|
return { saved: false, packageCount: 0, linkCount: 0 };
|
||||||
}
|
}
|
||||||
const options = {
|
const options = {
|
||||||
defaultPath: exported.defaultFileName,
|
defaultPath: exported.defaultFileName,
|
||||||
filters: [{ name: "Link Export", extensions: ["txt"] }]
|
filters: [{ name: "Link Export", extensions: ["txt"] }]
|
||||||
};
|
};
|
||||||
const result = mainWindow ? await dialog.showSaveDialog(mainWindow, options) : await dialog.showSaveDialog(options);
|
const result = mainWindow ? await dialog.showSaveDialog(mainWindow, options) : await dialog.showSaveDialog(options);
|
||||||
if (result.canceled || !result.filePath) {
|
if (result.canceled || !result.filePath) {
|
||||||
return { saved: false, packageCount: exported.packageCount, linkCount: exported.linkCount };
|
return { saved: false, packageCount: exported.packageCount, linkCount: exported.linkCount };
|
||||||
}
|
}
|
||||||
await fs.promises.writeFile(result.filePath, exported.text, "utf8");
|
await fs.promises.writeFile(result.filePath, exported.text, "utf8");
|
||||||
return { saved: true, packageCount: exported.packageCount, linkCount: exported.linkCount, filePath: result.filePath };
|
return { saved: true, packageCount: exported.packageCount, linkCount: exported.linkCount, filePath: result.filePath };
|
||||||
});
|
});
|
||||||
ipcMain.handle(IPC_CHANNELS.RETRY_EXTRACTION, (_event: IpcMainInvokeEvent, packageId: string) => {
|
ipcMain.handle(IPC_CHANNELS.RETRY_EXTRACTION, (_event: IpcMainInvokeEvent, packageId: string) => {
|
||||||
validateString(packageId, "packageId");
|
validateString(packageId, "packageId");
|
||||||
return controller.retryExtraction(packageId);
|
return controller.retryExtraction(packageId);
|
||||||
});
|
});
|
||||||
ipcMain.handle(IPC_CHANNELS.EXTRACT_NOW, (_event: IpcMainInvokeEvent, packageId: string) => {
|
ipcMain.handle(IPC_CHANNELS.EXTRACT_NOW, (_event: IpcMainInvokeEvent, packageId: string) => {
|
||||||
validateString(packageId, "packageId");
|
validateString(packageId, "packageId");
|
||||||
return controller.extractNow(packageId);
|
return controller.extractNow(packageId);
|
||||||
@ -496,12 +496,12 @@ function registerIpcHandlers(): void {
|
|||||||
};
|
};
|
||||||
const result = mainWindow ? await dialog.showOpenDialog(mainWindow, options) : await dialog.showOpenDialog(options);
|
const result = mainWindow ? await dialog.showOpenDialog(mainWindow, options) : await dialog.showOpenDialog(options);
|
||||||
return result.canceled ? [] : result.filePaths;
|
return result.canceled ? [] : result.filePaths;
|
||||||
});
|
});
|
||||||
ipcMain.handle(IPC_CHANNELS.GET_SESSION_STATS, () => controller.getSessionStats());
|
ipcMain.handle(IPC_CHANNELS.GET_SESSION_STATS, () => controller.getSessionStats());
|
||||||
ipcMain.handle(IPC_CHANNELS.RESET_SESSION_STATS, () => controller.resetSessionStats());
|
ipcMain.handle(IPC_CHANNELS.RESET_SESSION_STATS, () => controller.resetSessionStats());
|
||||||
ipcMain.handle(IPC_CHANNELS.RESET_DOWNLOAD_STATS, () => controller.resetDownloadStats());
|
ipcMain.handle(IPC_CHANNELS.RESET_DOWNLOAD_STATS, () => controller.resetDownloadStats());
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.RESTART, () => {
|
ipcMain.handle(IPC_CHANNELS.RESTART, () => {
|
||||||
app.relaunch();
|
app.relaunch();
|
||||||
app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
@ -510,106 +510,106 @@ function registerIpcHandlers(): void {
|
|||||||
app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.EXPORT_BACKUP, async () => {
|
ipcMain.handle(IPC_CHANNELS.EXPORT_BACKUP, async () => {
|
||||||
const options = {
|
const options = {
|
||||||
defaultPath: `mdd-backup-${new Date().toISOString().slice(0, 10)}.mdd`,
|
defaultPath: `mdd-backup-${new Date().toISOString().slice(0, 10)}.mdd`,
|
||||||
filters: [{ name: "MDD Backup", extensions: ["mdd"] }]
|
filters: [{ name: "MDD Backup", extensions: ["mdd"] }]
|
||||||
};
|
};
|
||||||
const result = mainWindow ? await dialog.showSaveDialog(mainWindow, options) : await dialog.showSaveDialog(options);
|
const result = mainWindow ? await dialog.showSaveDialog(mainWindow, options) : await dialog.showSaveDialog(options);
|
||||||
if (result.canceled || !result.filePath) {
|
if (result.canceled || !result.filePath) {
|
||||||
return { saved: false };
|
return { saved: false };
|
||||||
}
|
}
|
||||||
const encrypted = controller.exportBackup();
|
const encrypted = controller.exportBackup();
|
||||||
await fs.promises.writeFile(result.filePath, encrypted);
|
await fs.promises.writeFile(result.filePath, encrypted);
|
||||||
return { saved: true };
|
return { saved: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.EXPORT_SUPPORT_BUNDLE, async () => {
|
ipcMain.handle(IPC_CHANNELS.EXPORT_SUPPORT_BUNDLE, async () => {
|
||||||
const exported = controller.exportSupportBundle();
|
const exported = controller.exportSupportBundle();
|
||||||
const options = {
|
const options = {
|
||||||
defaultPath: exported.defaultFileName,
|
defaultPath: exported.defaultFileName,
|
||||||
filters: [{ name: "Support Bundle", extensions: ["zip"] }]
|
filters: [{ name: "Support Bundle", extensions: ["zip"] }]
|
||||||
};
|
};
|
||||||
const result = mainWindow ? await dialog.showSaveDialog(mainWindow, options) : await dialog.showSaveDialog(options);
|
const result = mainWindow ? await dialog.showSaveDialog(mainWindow, options) : await dialog.showSaveDialog(options);
|
||||||
if (result.canceled || !result.filePath) {
|
if (result.canceled || !result.filePath) {
|
||||||
return { saved: false };
|
return { saved: false };
|
||||||
}
|
}
|
||||||
await fs.promises.writeFile(result.filePath, exported.buffer);
|
await fs.promises.writeFile(result.filePath, exported.buffer);
|
||||||
return { saved: true, filePath: result.filePath };
|
return { saved: true, filePath: result.filePath };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.OPEN_LOG, async () => {
|
ipcMain.handle(IPC_CHANNELS.OPEN_LOG, async () => {
|
||||||
const logPath = getLogFilePath();
|
const logPath = getLogFilePath();
|
||||||
await shell.openPath(logPath);
|
await shell.openPath(logPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.OPEN_AUDIT_LOG, async () => {
|
ipcMain.handle(IPC_CHANNELS.OPEN_AUDIT_LOG, async () => {
|
||||||
const logPath = controller.getAuditLogPath();
|
const logPath = controller.getAuditLogPath();
|
||||||
if (logPath) {
|
if (logPath) {
|
||||||
await shell.openPath(logPath);
|
await shell.openPath(logPath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.OPEN_RENAME_LOG, async () => {
|
ipcMain.handle(IPC_CHANNELS.OPEN_RENAME_LOG, async () => {
|
||||||
const logPath = controller.getRenameLogPath();
|
const logPath = controller.getRenameLogPath();
|
||||||
if (logPath) {
|
if (logPath) {
|
||||||
await shell.openPath(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) {
|
||||||
await shell.openPath(logPath);
|
await shell.openPath(logPath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.OPEN_TRACE_LOG, async () => {
|
ipcMain.handle(IPC_CHANNELS.OPEN_TRACE_LOG, async () => {
|
||||||
const logPath = controller.getTraceLogPath();
|
const logPath = controller.getTraceLogPath();
|
||||||
if (logPath) {
|
if (logPath) {
|
||||||
await shell.openPath(logPath);
|
await shell.openPath(logPath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.OPEN_PACKAGE_LOG, async (_event: IpcMainInvokeEvent, packageId: string) => {
|
ipcMain.handle(IPC_CHANNELS.OPEN_PACKAGE_LOG, async (_event: IpcMainInvokeEvent, packageId: string) => {
|
||||||
validateString(packageId, "packageId");
|
validateString(packageId, "packageId");
|
||||||
const logPath = controller.getPackageLogPath(packageId);
|
const logPath = controller.getPackageLogPath(packageId);
|
||||||
if (logPath) {
|
if (logPath) {
|
||||||
await shell.openPath(logPath);
|
await shell.openPath(logPath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.GET_DEBUG_SETUP_CHECK, async () => controller.getDebugSetupCheck());
|
ipcMain.handle(IPC_CHANNELS.GET_DEBUG_SETUP_CHECK, async () => controller.getDebugSetupCheck());
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.GET_TRACE_CONFIG, async () => controller.getTraceConfig());
|
ipcMain.handle(IPC_CHANNELS.GET_TRACE_CONFIG, async () => controller.getTraceConfig());
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.SET_TRACE_ENABLED, async (_event: IpcMainInvokeEvent, enabled: boolean, note?: string, durationMinutes?: number) => {
|
ipcMain.handle(IPC_CHANNELS.SET_TRACE_ENABLED, async (_event: IpcMainInvokeEvent, enabled: boolean, note?: string, durationMinutes?: number) => {
|
||||||
if (typeof enabled !== "boolean") {
|
if (typeof enabled !== "boolean") {
|
||||||
throw new Error("enabled muss ein Boolean sein");
|
throw new Error("enabled muss ein Boolean sein");
|
||||||
}
|
}
|
||||||
if (note !== undefined) {
|
if (note !== undefined) {
|
||||||
validateString(note, "note");
|
validateString(note, "note");
|
||||||
}
|
}
|
||||||
if (durationMinutes !== undefined && (!Number.isFinite(durationMinutes) || durationMinutes <= 0)) {
|
if (durationMinutes !== undefined && (!Number.isFinite(durationMinutes) || durationMinutes <= 0)) {
|
||||||
throw new Error("durationMinutes muss eine positive Zahl sein");
|
throw new Error("durationMinutes muss eine positive Zahl sein");
|
||||||
}
|
}
|
||||||
return controller.setTraceEnabled(enabled, note, durationMinutes ? durationMinutes * 60 * 1000 : undefined);
|
return controller.setTraceEnabled(enabled, note, durationMinutes ? durationMinutes * 60 * 1000 : undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.ROTATE_DEBUG_TOKEN, async () => {
|
ipcMain.handle(IPC_CHANNELS.ROTATE_DEBUG_TOKEN, async () => {
|
||||||
const rotated = controller.rotateDebugToken();
|
const rotated = controller.rotateDebugToken();
|
||||||
return { path: rotated.path };
|
return { path: rotated.path };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.OPEN_ITEM_LOG, async (_event: IpcMainInvokeEvent, itemId: string) => {
|
ipcMain.handle(IPC_CHANNELS.OPEN_ITEM_LOG, async (_event: IpcMainInvokeEvent, itemId: string) => {
|
||||||
validateString(itemId, "itemId");
|
validateString(itemId, "itemId");
|
||||||
const logPath = controller.getItemLogPath(itemId);
|
const logPath = controller.getItemLogPath(itemId);
|
||||||
if (logPath) {
|
if (logPath) {
|
||||||
await shell.openPath(logPath);
|
await shell.openPath(logPath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.OPEN_REALDEBRID_LOGIN, async () => {
|
ipcMain.handle(IPC_CHANNELS.OPEN_REALDEBRID_LOGIN, async () => {
|
||||||
await controller.openRealDebridLoginWindow();
|
await controller.openRealDebridLoginWindow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -881,7 +881,25 @@ export function loadSession(paths: StoragePaths): SessionState {
|
|||||||
return backup;
|
return backup;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error("Session konnte nicht geladen werden (auch Backup fehlgeschlagen)");
|
// Last resort: try to recover from temp files left by interrupted writes
|
||||||
|
for (const kind of ["sync", "async"] as const) {
|
||||||
|
const tmpPath = sessionTempPath(paths.sessionFile, kind);
|
||||||
|
if (fs.existsSync(tmpPath)) {
|
||||||
|
const tmpSession = readSessionFile(tmpPath);
|
||||||
|
if (tmpSession && Object.keys(tmpSession.packages).length > 0) {
|
||||||
|
logger.warn(`Session aus temporaerer Datei wiederhergestellt: ${tmpPath} (${Object.keys(tmpSession.packages).length} Pakete)`);
|
||||||
|
try {
|
||||||
|
const payload = JSON.stringify({ ...tmpSession, updatedAt: Date.now() });
|
||||||
|
fs.writeFileSync(paths.sessionFile, payload, "utf8");
|
||||||
|
} catch {
|
||||||
|
// ignore restore write failure
|
||||||
|
}
|
||||||
|
return tmpSession;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error("Session konnte nicht geladen werden (Primary, Backup und Temp-Dateien fehlgeschlagen)");
|
||||||
return emptySession();
|
return emptySession();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user