From a7c82de4a722d067d1b8c331d6ccc0ef2b98430a Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 6 Jan 2026 19:59:52 +0200 Subject: [PATCH] refactor: enhance download management by prioritizing interrupted downloads and improving error logging --- src/main/main.ts | 57 +++++++++++++++++-- .../services/download/js-http-downloader.ts | 26 +++++++++ 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/main/main.ts b/src/main/main.ts index 12f5ea26..35f205e5 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -18,6 +18,7 @@ import { DeckyPlugin, DownloadSourcesChecker, WSClient, + logger, } from "@main/services"; import { migrateDownloadSources } from "./helpers/migrate-download-sources"; @@ -73,18 +74,47 @@ export const loadState = async () => { return orderBy(games, "timestamp", "desc"); }); - downloads.forEach((download) => { + let interruptedDownload = null; + + for (const download of downloads) { + const downloadKey = levelKeys.game(download.shop, download.objectId); + + // Reset extracting state if (download.extracting) { - downloadsSublevel.put(levelKeys.game(download.shop, download.objectId), { + await downloadsSublevel.put(downloadKey, { ...download, extracting: false, }); } - }); - const [nextItemOnQueue] = downloads.filter((game) => game.queued); + // Find interrupted active download (download that was running when app closed) + // Mark it as paused but remember it for auto-resume + if (download.status === "active" && !interruptedDownload) { + interruptedDownload = download; + await downloadsSublevel.put(downloadKey, { + ...download, + status: "paused", + }); + } else if (download.status === "active") { + // Mark other active downloads as paused + await downloadsSublevel.put(downloadKey, { + ...download, + status: "paused", + }); + } + } - const downloadsToSeed = downloads.filter( + // Re-fetch downloads after status updates + const updatedDownloads = await downloadsSublevel + .values() + .all() + .then((games) => orderBy(games, "timestamp", "desc")); + + // Prioritize interrupted download, then queued downloads + const downloadToResume = + interruptedDownload ?? updatedDownloads.find((game) => game.queued); + + const downloadsToSeed = updatedDownloads.filter( (game) => game.shouldSeed && game.downloader === Downloader.Torrent && @@ -92,7 +122,22 @@ export const loadState = async () => { game.uri !== null ); - await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed); + // For torrents or if JS downloader is disabled, use Python RPC + const isTorrent = downloadToResume?.downloader === Downloader.Torrent; + const useJsDownloader = + userPreferences?.useNativeHttpDownloader && !isTorrent; + + if (useJsDownloader && downloadToResume) { + // Start Python RPC for seeding only, then resume HTTP download with JS + await DownloadManager.startRPC(undefined, downloadsToSeed); + await DownloadManager.startDownload(downloadToResume).catch((err) => { + // If resume fails, just log it - user can manually retry + logger.error("Failed to auto-resume download:", err); + }); + } else { + // Use Python RPC for everything (torrent or fallback) + await DownloadManager.startRPC(downloadToResume, downloadsToSeed); + } startMainLoop(); diff --git a/src/main/services/download/js-http-downloader.ts b/src/main/services/download/js-http-downloader.ts index 563d8a6c..44a3af49 100644 --- a/src/main/services/download/js-http-downloader.ts +++ b/src/main/services/download/js-http-downloader.ts @@ -147,6 +147,32 @@ export class JsHttpDownloader { signal: this.abortController?.signal, }); + // Handle 416 Range Not Satisfiable - existing file is larger than server file + // This happens when downloading same game from different source + if (response.status === 416 && startByte > 0) { + logger.log( + "[JsHttpDownloader] Range not satisfiable, deleting existing file and restarting" + ); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + this.bytesDownloaded = 0; + this.resetSpeedTracking(); + + // Retry without Range header + const headersWithoutRange = { ...requestHeaders }; + delete headersWithoutRange["Range"]; + + return this.executeDownload( + url, + headersWithoutRange, + filePath, + 0, + savePath, + usedFallback + ); + } + if (!response.ok && response.status !== 206) { throw new Error(`HTTP error! status: ${response.status}`); }