refactor: streamline download preparation and status handling in DownloadManager

This commit is contained in:
Moyasee
2026-01-07 20:35:43 +02:00
parent ed3cce160f
commit ed044d797f
4 changed files with 83 additions and 82 deletions

View File

@@ -85,18 +85,8 @@ const startGameDownload = async (
}; };
try { try {
// Save download to DB immediately so UI can show it await DownloadManager.startDownload(download).then(() => {
await downloadsSublevel.put(gameKey, download); return downloadsSublevel.put(gameKey, download);
// Start download asynchronously (don't await) to avoid blocking UI
// This is especially important for Gofile/Mediafire which make API calls
DownloadManager.startDownload(download).catch((err) => {
logger.error("Failed to start download after save:", err);
// Update download status to error
downloadsSublevel.put(gameKey, {
...download,
status: "error",
});
}); });
const updatedGame = await gamesSublevel.get(gameKey); const updatedGame = await gamesSublevel.get(gameKey);

View File

@@ -35,6 +35,7 @@ export class DownloadManager {
private static downloadingGameId: string | null = null; private static downloadingGameId: string | null = null;
private static jsDownloader: JsHttpDownloader | null = null; private static jsDownloader: JsHttpDownloader | null = null;
private static usingJsDownloader = false; private static usingJsDownloader = false;
private static isPreparingDownload = false;
private static extractFilename( private static extractFilename(
url: string, url: string,
@@ -142,13 +143,37 @@ export class DownloadManager {
} }
private static async getDownloadStatusFromJs(): Promise<DownloadProgress | null> { private static async getDownloadStatusFromJs(): Promise<DownloadProgress | null> {
if (!this.jsDownloader || !this.downloadingGameId) return null; if (!this.downloadingGameId) return null;
const downloadId = this.downloadingGameId;
// Return a "preparing" status while fetching download options
if (this.isPreparingDownload) {
try {
const download = await downloadsSublevel.get(downloadId);
if (!download) return null;
return {
numPeers: 0,
numSeeds: 0,
downloadSpeed: 0,
timeRemaining: -1,
isDownloadingMetadata: true, // Use this to indicate "preparing"
isCheckingFiles: false,
progress: 0,
gameId: downloadId,
download,
};
} catch {
return null;
}
}
if (!this.jsDownloader) return null;
const status = this.jsDownloader.getDownloadStatus(); const status = this.jsDownloader.getDownloadStatus();
if (!status) return null; if (!status) return null;
const downloadId = this.downloadingGameId;
try { try {
const download = await downloadsSublevel.get(downloadId); const download = await downloadsSublevel.get(downloadId);
if (!download) return null; if (!download) return null;
@@ -156,13 +181,14 @@ export class DownloadManager {
const { progress, downloadSpeed, bytesDownloaded, fileSize, folderName } = const { progress, downloadSpeed, bytesDownloaded, fileSize, folderName } =
status; status;
const finalFileSize = // Only update fileSize in database if we actually know it (> 0)
fileSize && fileSize > 0 ? fileSize : download.fileSize; // Otherwise keep the existing value to avoid showing "0 B"
const effectiveFileSize = fileSize > 0 ? fileSize : download.fileSize;
const updatedDownload = { const updatedDownload = {
...download, ...download,
bytesDownloaded, bytesDownloaded,
fileSize: finalFileSize, fileSize: effectiveFileSize,
progress, progress,
folderName, folderName,
status: status:
@@ -180,7 +206,7 @@ export class DownloadManager {
numSeeds: 0, numSeeds: 0,
downloadSpeed, downloadSpeed,
timeRemaining: calculateETA( timeRemaining: calculateETA(
finalFileSize ?? 0, effectiveFileSize ?? 0,
bytesDownloaded, bytesDownloaded,
downloadSpeed downloadSpeed
), ),
@@ -420,7 +446,7 @@ export class DownloadManager {
this.jsDownloader.cancelDownload(); this.jsDownloader.cancelDownload();
this.jsDownloader = null; this.jsDownloader = null;
this.usingJsDownloader = false; this.usingJsDownloader = false;
} else { } else if (!this.isPreparingDownload) {
await PythonRPC.rpc await PythonRPC.rpc
.post("/action", { action: "cancel", game_id: downloadKey }) .post("/action", { action: "cancel", game_id: downloadKey })
.catch((err) => logger.error("Failed to cancel game download", err)); .catch((err) => logger.error("Failed to cancel game download", err));
@@ -430,6 +456,8 @@ export class DownloadManager {
WindowManager.mainWindow?.setProgressBar(-1); WindowManager.mainWindow?.setProgressBar(-1);
WindowManager.mainWindow?.webContents.send("on-download-progress", null); WindowManager.mainWindow?.webContents.send("on-download-progress", null);
this.downloadingGameId = null; this.downloadingGameId = null;
this.isPreparingDownload = false;
this.usingJsDownloader = false;
} }
} }
@@ -774,74 +802,45 @@ export class DownloadManager {
static async startDownload(download: Download) { static async startDownload(download: Download) {
const useJsDownloader = await this.shouldUseJsDownloader(); const useJsDownloader = await this.shouldUseJsDownloader();
const isHttp = this.isHttpDownloader(download.downloader); const isHttp = this.isHttpDownloader(download.downloader);
const downloadId = levelKeys.game(download.shop, download.objectId);
if (useJsDownloader && isHttp) { if (useJsDownloader && isHttp) {
logger.log("[DownloadManager] Using JS HTTP downloader"); logger.log("[DownloadManager] Using JS HTTP downloader");
this.downloadingGameId = levelKeys.game(download.shop, download.objectId);
// Get download options (this includes API calls for Gofile/Mediafire) // Set preparing state immediately so UI knows download is starting
// We still await this to catch errors, but start actual download async this.downloadingGameId = downloadId;
const options = await this.getJsDownloadOptions(download); this.isPreparingDownload = true;
if (!options) {
throw new Error("Failed to get download options for JS downloader");
}
// Try to get file size from HEAD request before starting download
// This ensures UI shows file size immediately instead of waiting
try {
const headResponse = await fetch(options.url, {
method: "HEAD",
headers: options.headers,
});
if (headResponse.ok) {
const contentLength = headResponse.headers.get("content-length");
if (contentLength) {
const fileSize = Number.parseInt(contentLength, 10);
const downloadId = this.downloadingGameId;
const currentDownload = await downloadsSublevel.get(downloadId);
if (currentDownload) {
await downloadsSublevel.put(downloadId, {
...currentDownload,
fileSize,
});
logger.log(
`[DownloadManager] Pre-fetched file size: ${fileSize} bytes`
);
}
}
}
} catch {
// If HEAD request fails, continue anyway - file size will be known from actual download
logger.log(
"[DownloadManager] Could not pre-fetch file size, will get from download response"
);
}
// Start download asynchronously (don't await) so UI returns immediately
this.jsDownloader = new JsHttpDownloader();
this.usingJsDownloader = true; this.usingJsDownloader = true;
// Start download in background try {
this.jsDownloader.startDownload(options).catch((err) => { const options = await this.getJsDownloadOptions(download);
logger.error("[DownloadManager] JS download error:", err);
this.usingJsDownloader = false;
this.jsDownloader = null;
});
// Poll status immediately after a short delay to get file size from response headers if (!options) {
// This ensures UI shows file size quickly instead of waiting for watchDownloads loop (2s interval) this.isPreparingDownload = false;
setTimeout(() => { this.usingJsDownloader = false;
this.getDownloadStatusFromJs().catch(() => { this.downloadingGameId = null;
// Ignore errors - status will be updated by watchDownloads loop throw new Error("Failed to get download options for JS downloader");
}
this.jsDownloader = new JsHttpDownloader();
this.isPreparingDownload = false;
this.jsDownloader.startDownload(options).catch((err) => {
logger.error("[DownloadManager] JS download error:", err);
this.usingJsDownloader = false;
this.jsDownloader = null;
}); });
}, 500); } catch (err) {
this.isPreparingDownload = false;
this.usingJsDownloader = false;
this.downloadingGameId = null;
throw err;
}
} else { } else {
logger.log("[DownloadManager] Using Python RPC downloader"); logger.log("[DownloadManager] Using Python RPC downloader");
const payload = await this.getDownloadPayload(download); const payload = await this.getDownloadPayload(download);
await PythonRPC.rpc.post("/action", payload); await PythonRPC.rpc.post("/action", payload);
this.downloadingGameId = levelKeys.game(download.shop, download.objectId); this.downloadingGameId = downloadId;
this.usingJsDownloader = false; this.usingJsDownloader = false;
} }
} }

View File

@@ -31,11 +31,16 @@ export const downloadSlice = createSlice({
reducers: { reducers: {
setLastPacket: (state, action: PayloadAction<DownloadProgress | null>) => { setLastPacket: (state, action: PayloadAction<DownloadProgress | null>) => {
state.lastPacket = action.payload; state.lastPacket = action.payload;
if (!state.gameId && action.payload) state.gameId = action.payload.gameId;
// Ensure payload exists and has a valid gameId before accessing
const payload = action.payload;
if (!state.gameId && payload?.gameId) {
state.gameId = payload.gameId;
}
// Track peak speed and speed history atomically when packet arrives // Track peak speed and speed history atomically when packet arrives
if (action.payload?.gameId && action.payload.downloadSpeed != null) { if (payload?.gameId && payload.downloadSpeed != null) {
const { gameId, downloadSpeed } = action.payload; const { gameId, downloadSpeed } = payload;
// Update peak speed if this is higher // Update peak speed if this is higher
const currentPeak = state.peakSpeeds[gameId] || 0; const currentPeak = state.peakSpeeds[gameId] || 0;

View File

@@ -613,11 +613,18 @@ export function DownloadGroup({
const download = game.download!; const download = game.download!;
const isGameDownloading = isGameDownloadingMap[game.id]; const isGameDownloading = isGameDownloadingMap[game.id];
if (download.fileSize != null) return formatBytes(download.fileSize); // Check lastPacket first for most up-to-date size during active downloads
if (
if (lastPacket?.download.fileSize && isGameDownloading) isGameDownloading &&
lastPacket?.download.fileSize &&
lastPacket.download.fileSize > 0
)
return formatBytes(lastPacket.download.fileSize); return formatBytes(lastPacket.download.fileSize);
// Then check the stored download size (must be > 0 to be valid)
if (download.fileSize != null && download.fileSize > 0)
return formatBytes(download.fileSize);
return "N/A"; return "N/A";
}; };