mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-15 08:23:02 -03:00
refactor: streamline download preparation and status handling in DownloadManager
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user