diff --git a/package.json b/package.json index 5b575e6..c2c7b37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.4.75", + "version": "1.4.76", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index 2c5bd1b..0707580 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -21,6 +21,7 @@ import { configureLogger, getLogFilePath, logger } from "./logger"; import { MegaWebFallback } from "./mega-web-fallback"; import { createStoragePaths, loadSession, loadSettings, normalizeSettings, saveSettings } from "./storage"; import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update"; +import { startDebugServer, stopDebugServer } from "./debug-server"; function sanitizeSettingsPatch(partial: Partial): Partial { const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined); @@ -64,6 +65,7 @@ export class AppController { }); logger.info(`App gestartet v${APP_VERSION}`); logger.info(`Log-Datei: ${getLogFilePath()}`); + startDebugServer(this.manager, this.storagePaths.baseDir); if (this.settings.autoResumeOnStart) { const snapshot = this.manager.getSnapshot(); @@ -235,6 +237,7 @@ export class AppController { } public shutdown(): void { + stopDebugServer(); abortActiveUpdateDownload(); this.manager.prepareForShutdown(); this.megaWebFallback.dispose(); diff --git a/src/main/debug-server.ts b/src/main/debug-server.ts new file mode 100644 index 0000000..20dd07a --- /dev/null +++ b/src/main/debug-server.ts @@ -0,0 +1,236 @@ +import http from "node:http"; +import fs from "node:fs"; +import path from "node:path"; +import { logger, getLogFilePath } from "./logger"; +import type { DownloadManager } from "./download-manager"; + +const DEFAULT_PORT = 9868; +const MAX_LOG_LINES = 500; + +let server: http.Server | null = null; +let manager: DownloadManager | null = null; +let authToken = ""; + +function loadToken(baseDir: string): string { + const tokenPath = path.join(baseDir, "debug_token.txt"); + try { + return fs.readFileSync(tokenPath, "utf8").trim(); + } catch { + return ""; + } +} + +function getPort(baseDir: string): number { + const portPath = path.join(baseDir, "debug_port.txt"); + try { + const n = Number(fs.readFileSync(portPath, "utf8").trim()); + if (Number.isFinite(n) && n >= 1024 && n <= 65535) { + return n; + } + } catch { + // ignore + } + return DEFAULT_PORT; +} + +function checkAuth(req: http.IncomingMessage): boolean { + if (!authToken) { + return false; + } + const header = req.headers.authorization || ""; + if (header === `Bearer ${authToken}`) { + return true; + } + const url = new URL(req.url || "/", "http://localhost"); + return url.searchParams.get("token") === authToken; +} + +function jsonResponse(res: http.ServerResponse, status: number, data: unknown): void { + const body = JSON.stringify(data, null, 2); + res.writeHead(status, { + "Content-Type": "application/json; charset=utf-8", + "Access-Control-Allow-Origin": "*", + "Cache-Control": "no-cache" + }); + res.end(body); +} + +function readLogTail(lines: number): string[] { + const logPath = getLogFilePath(); + try { + const content = fs.readFileSync(logPath, "utf8"); + const allLines = content.split("\n").filter((l) => l.trim().length > 0); + return allLines.slice(-Math.min(lines, MAX_LOG_LINES)); + } catch { + return ["(Log-Datei nicht lesbar)"]; + } +} + +function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void { + if (req.method === "OPTIONS") { + res.writeHead(204, { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Authorization" + }); + res.end(); + return; + } + + if (!checkAuth(req)) { + jsonResponse(res, 401, { error: "Unauthorized" }); + return; + } + + const url = new URL(req.url || "/", "http://localhost"); + const pathname = url.pathname; + + if (pathname === "/health") { + jsonResponse(res, 200, { + status: "ok", + uptime: Math.floor(process.uptime()), + memoryMB: Math.round(process.memoryUsage().rss / 1024 / 1024) + }); + return; + } + + if (pathname === "/log") { + const count = Math.min(Number(url.searchParams.get("lines") || "100"), MAX_LOG_LINES); + const lines = readLogTail(count); + jsonResponse(res, 200, { lines, count: lines.length }); + return; + } + + if (pathname === "/status") { + if (!manager) { + jsonResponse(res, 503, { error: "Manager not initialized" }); + return; + } + const snapshot = manager.getSnapshot(); + const items = Object.values(snapshot.session.items); + const packages = Object.values(snapshot.session.packages); + + const byStatus: Record = {}; + for (const item of items) { + byStatus[item.status] = (byStatus[item.status] || 0) + 1; + } + + const activeItems = items + .filter((i) => i.status === "downloading" || i.status === "validating") + .map((i) => ({ + id: i.id, + fileName: i.fileName, + status: i.status, + fullStatus: i.fullStatus, + provider: i.provider, + progress: i.progressPercent, + speedMBs: +(i.speedBps / 1024 / 1024).toFixed(2), + downloadedMB: +(i.downloadedBytes / 1024 / 1024).toFixed(1), + totalMB: i.totalBytes ? +(i.totalBytes / 1024 / 1024).toFixed(1) : null, + retries: i.retries, + lastError: i.lastError + })); + + const failedItems = items + .filter((i) => i.status === "failed") + .map((i) => ({ + fileName: i.fileName, + lastError: i.lastError, + retries: i.retries, + provider: i.provider + })); + + jsonResponse(res, 200, { + running: snapshot.session.running, + paused: snapshot.session.paused, + speed: snapshot.speedText, + eta: snapshot.etaText, + itemCounts: byStatus, + totalItems: items.length, + packages: packages.map((p) => ({ + name: p.name, + status: p.status, + items: p.itemIds.length + })), + activeItems, + failedItems: failedItems.length > 0 ? failedItems : undefined + }); + return; + } + + if (pathname === "/items") { + if (!manager) { + jsonResponse(res, 503, { error: "Manager not initialized" }); + return; + } + const snapshot = manager.getSnapshot(); + const filter = url.searchParams.get("status"); + const pkg = url.searchParams.get("package"); + let items = Object.values(snapshot.session.items); + if (filter) { + items = items.filter((i) => i.status === filter); + } + if (pkg) { + const pkgLower = pkg.toLowerCase(); + const matchedPkg = Object.values(snapshot.session.packages) + .find((p) => p.name.toLowerCase().includes(pkgLower)); + if (matchedPkg) { + const ids = new Set(matchedPkg.itemIds); + items = items.filter((i) => ids.has(i.id)); + } + } + jsonResponse(res, 200, { + count: items.length, + items: items.map((i) => ({ + fileName: i.fileName, + status: i.status, + fullStatus: i.fullStatus, + provider: i.provider, + progress: i.progressPercent, + speedMBs: +(i.speedBps / 1024 / 1024).toFixed(2), + downloadedMB: +(i.downloadedBytes / 1024 / 1024).toFixed(1), + totalMB: i.totalBytes ? +(i.totalBytes / 1024 / 1024).toFixed(1) : null, + retries: i.retries, + lastError: i.lastError + })) + }); + return; + } + + jsonResponse(res, 404, { + error: "Not found", + endpoints: [ + "GET /health", + "GET /log?lines=100", + "GET /status", + "GET /items?status=downloading&package=Bloodline" + ] + }); +} + +export function startDebugServer(mgr: DownloadManager, baseDir: string): void { + authToken = loadToken(baseDir); + if (!authToken) { + logger.info("Debug-Server: Kein Token in debug_token.txt, Server wird nicht gestartet"); + return; + } + + manager = mgr; + const port = getPort(baseDir); + + server = http.createServer(handleRequest); + server.listen(port, "0.0.0.0", () => { + logger.info(`Debug-Server gestartet auf Port ${port}`); + }); + server.on("error", (err) => { + logger.warn(`Debug-Server Fehler: ${String(err)}`); + server = null; + }); +} + +export function stopDebugServer(): void { + if (server) { + server.close(); + server = null; + logger.info("Debug-Server gestoppt"); + } +}