mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-30 14:21:04 +00:00
refactor: enhance download management by prioritizing interrupted downloads and improving error logging
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
|||||||
DeckyPlugin,
|
DeckyPlugin,
|
||||||
DownloadSourcesChecker,
|
DownloadSourcesChecker,
|
||||||
WSClient,
|
WSClient,
|
||||||
|
logger,
|
||||||
} from "@main/services";
|
} from "@main/services";
|
||||||
import { migrateDownloadSources } from "./helpers/migrate-download-sources";
|
import { migrateDownloadSources } from "./helpers/migrate-download-sources";
|
||||||
|
|
||||||
@@ -73,18 +74,47 @@ export const loadState = async () => {
|
|||||||
return orderBy(games, "timestamp", "desc");
|
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) {
|
if (download.extracting) {
|
||||||
downloadsSublevel.put(levelKeys.game(download.shop, download.objectId), {
|
await downloadsSublevel.put(downloadKey, {
|
||||||
...download,
|
...download,
|
||||||
extracting: false,
|
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) =>
|
||||||
game.shouldSeed &&
|
game.shouldSeed &&
|
||||||
game.downloader === Downloader.Torrent &&
|
game.downloader === Downloader.Torrent &&
|
||||||
@@ -92,7 +122,22 @@ export const loadState = async () => {
|
|||||||
game.uri !== null
|
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();
|
startMainLoop();
|
||||||
|
|
||||||
|
|||||||
@@ -147,6 +147,32 @@ export class JsHttpDownloader {
|
|||||||
signal: this.abortController?.signal,
|
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) {
|
if (!response.ok && response.status !== 206) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user