refactor: enhance download management by validating URLs and adding file size handling

This commit is contained in:
Moyasee
2026-01-24 19:38:00 +02:00
parent fb1380356e
commit eea7148108
8 changed files with 153 additions and 46 deletions

View File

@@ -15,14 +15,7 @@ const deleteGameFolder = async (
const downloadKey = levelKeys.game(shop, objectId); const downloadKey = levelKeys.game(shop, objectId);
const download = await downloadsSublevel.get(downloadKey); const download = await downloadsSublevel.get(downloadKey);
if (!download?.folderName) return; if (!download) return;
const folderPath = path.join(
download.downloadPath ?? (await getDownloadsPath()),
download.folderName
);
const metaPath = `${folderPath}.meta`;
const deleteFile = async (filePath: string, isDirectory = false) => { const deleteFile = async (filePath: string, isDirectory = false) => {
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
@@ -47,8 +40,18 @@ const deleteGameFolder = async (
} }
}; };
await deleteFile(folderPath, true); if (download.folderName) {
await deleteFile(metaPath); const folderPath = path.join(
download.downloadPath ?? (await getDownloadsPath()),
download.folderName
);
const metaPath = `${folderPath}.meta`;
await deleteFile(folderPath, true);
await deleteFile(metaPath);
}
await downloadsSublevel.del(downloadKey); await downloadsSublevel.del(downloadKey);
}; };

View File

@@ -1,6 +1,6 @@
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import type { Download, StartGameDownloadPayload } from "@types"; import type { Download, StartGameDownloadPayload } from "@types";
import { HydraApi, logger } from "@main/services"; import { DownloadManager, HydraApi, logger } from "@main/services";
import { createGame } from "@main/services/library-sync"; import { createGame } from "@main/services/library-sync";
import { import {
downloadsSublevel, downloadsSublevel,
@@ -8,6 +8,8 @@ import {
gamesSublevel, gamesSublevel,
levelKeys, levelKeys,
} from "@main/level"; } from "@main/level";
import { Downloader, DownloadError, parseBytes } from "@shared";
import { AxiosError } from "axios";
const addGameToQueue = async ( const addGameToQueue = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
@@ -21,10 +23,86 @@ const addGameToQueue = async (
downloader, downloader,
uri, uri,
automaticallyExtract, automaticallyExtract,
fileSize,
} = payload; } = payload;
const gameKey = levelKeys.game(shop, objectId); const gameKey = levelKeys.game(shop, objectId);
const download: Download = {
shop,
objectId,
status: "paused",
progress: 0,
bytesDownloaded: 0,
downloadPath,
downloader,
uri,
folderName: null,
fileSize: parseBytes(fileSize ?? null),
shouldSeed: false,
timestamp: Date.now(),
queued: true,
extracting: false,
automaticallyExtract,
extractionProgress: 0,
};
try {
await DownloadManager.validateDownloadUrl(download);
} catch (err: unknown) {
logger.error("Failed to validate download URL for queue", err);
if (err instanceof AxiosError) {
if (err.response?.status === 429 && downloader === Downloader.Gofile) {
return { ok: false, error: DownloadError.GofileQuotaExceeded };
}
if (
err.response?.status === 403 &&
downloader === Downloader.RealDebrid
) {
return {
ok: false,
error: DownloadError.RealDebridAccountNotAuthorized,
};
}
if (downloader === Downloader.TorBox) {
return { ok: false, error: err.response?.data?.detail };
}
}
if (err instanceof Error) {
if (downloader === Downloader.Buzzheavier) {
if (err.message.includes("Rate limit")) {
return { ok: false, error: "Buzzheavier: Rate limit exceeded" };
}
if (
err.message.includes("not found") ||
err.message.includes("deleted")
) {
return { ok: false, error: "Buzzheavier: File not found" };
}
}
if (downloader === Downloader.FuckingFast) {
if (err.message.includes("Rate limit")) {
return { ok: false, error: "FuckingFast: Rate limit exceeded" };
}
if (
err.message.includes("not found") ||
err.message.includes("deleted")
) {
return { ok: false, error: "FuckingFast: File not found" };
}
}
return { ok: false, error: err.message };
}
return { ok: false };
}
const game = await gamesSublevel.get(gameKey); const game = await gamesSublevel.get(gameKey);
const gameAssets = await gamesShopAssetsSublevel.get(gameKey); const gameAssets = await gamesShopAssetsSublevel.get(gameKey);
@@ -50,25 +128,6 @@ const addGameToQueue = async (
}); });
} }
const download: Download = {
shop,
objectId,
status: "paused",
progress: 0,
bytesDownloaded: 0,
downloadPath,
downloader,
uri,
folderName: null,
fileSize: null,
shouldSeed: false,
timestamp: Date.now(),
queued: true,
extracting: false,
automaticallyExtract,
extractionProgress: 0,
};
try { try {
await downloadsSublevel.put(gameKey, download); await downloadsSublevel.put(gameKey, download);

View File

@@ -2,7 +2,7 @@ import { registerEvent } from "../register-event";
import type { Download, StartGameDownloadPayload } from "@types"; import type { Download, StartGameDownloadPayload } from "@types";
import { DownloadManager, HydraApi, logger } from "@main/services"; import { DownloadManager, HydraApi, logger } from "@main/services";
import { createGame } from "@main/services/library-sync"; import { createGame } from "@main/services/library-sync";
import { Downloader, DownloadError } from "@shared"; import { Downloader, DownloadError, parseBytes } from "@shared";
import { import {
downloadsSublevel, downloadsSublevel,
gamesShopAssetsSublevel, gamesShopAssetsSublevel,
@@ -23,6 +23,7 @@ const startGameDownload = async (
downloader, downloader,
uri, uri,
automaticallyExtract, automaticallyExtract,
fileSize,
} = payload; } = payload;
const gameKey = levelKeys.game(shop, objectId); const gameKey = levelKeys.game(shop, objectId);
@@ -75,7 +76,7 @@ const startGameDownload = async (
downloader, downloader,
uri, uri,
folderName: null, folderName: null,
fileSize: null, fileSize: parseBytes(fileSize ?? null),
shouldSeed: false, shouldSeed: false,
timestamp: Date.now(), timestamp: Date.now(),
queued: true, queued: true,

View File

@@ -499,18 +499,20 @@ export class DownloadManager {
} }
static async cancelDownload(downloadKey = this.downloadingGameId) { static async cancelDownload(downloadKey = this.downloadingGameId) {
if (this.usingJsDownloader && this.jsDownloader) { const isActiveDownload = downloadKey === this.downloadingGameId;
logger.log("[DownloadManager] Cancelling JS download");
this.jsDownloader.cancelDownload(); if (isActiveDownload) {
this.jsDownloader = null; if (this.usingJsDownloader && this.jsDownloader) {
this.usingJsDownloader = false; logger.log("[DownloadManager] Cancelling JS download");
} else if (!this.isPreparingDownload) { this.jsDownloader.cancelDownload();
await PythonRPC.rpc this.jsDownloader = null;
.post("/action", { action: "cancel", game_id: downloadKey }) this.usingJsDownloader = false;
.catch((err) => logger.error("Failed to cancel game download", err)); } else if (!this.isPreparingDownload) {
} await PythonRPC.rpc
.post("/action", { action: "cancel", game_id: downloadKey })
.catch((err) => logger.error("Failed to cancel game download", err));
}
if (downloadKey === this.downloadingGameId) {
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;
@@ -932,6 +934,20 @@ export class DownloadManager {
} }
} }
static async validateDownloadUrl(download: Download): Promise<void> {
const useJsDownloader = await this.shouldUseJsDownloader();
const isHttp = this.isHttpDownloader(download.downloader);
if (useJsDownloader && isHttp) {
const options = await this.getJsDownloadOptions(download);
if (!options) {
throw new Error("Failed to validate download URL");
}
} else if (isHttp) {
await this.getDownloadPayload(download);
}
}
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);

View File

@@ -69,10 +69,16 @@ export function useDownload() {
}; };
const cancelDownload = async (shop: GameShop, objectId: string) => { const cancelDownload = async (shop: GameShop, objectId: string) => {
await window.electron.cancelGameDownload(shop, objectId); const gameId = `${shop}:${objectId}`;
dispatch(clearDownload()); const isActiveDownload = lastPacket?.gameId === gameId;
updateLibrary();
await window.electron.cancelGameDownload(shop, objectId);
if (isActiveDownload) {
dispatch(clearDownload());
}
updateLibrary();
removeGameInstaller(shop, objectId); removeGameInstaller(shop, objectId);
}; };

View File

@@ -112,6 +112,7 @@ export default function GameDetails() {
downloadPath, downloadPath,
uri: selectRepackUri(repack, downloader), uri: selectRepackUri(repack, downloader),
automaticallyExtract: automaticallyExtract, automaticallyExtract: automaticallyExtract,
fileSize: repack.fileSize,
}) })
: await startDownload({ : await startDownload({
objectId: objectId!, objectId: objectId!,
@@ -121,6 +122,7 @@ export default function GameDetails() {
downloadPath, downloadPath,
uri: selectRepackUri(repack, downloader), uri: selectRepackUri(repack, downloader),
automaticallyExtract: automaticallyExtract, automaticallyExtract: automaticallyExtract,
fileSize: repack.fileSize,
}); });
if (response.ok) { if (response.ok) {

View File

@@ -51,6 +51,25 @@ export const formatBytes = (bytes: number): string => {
return `${Math.trunc(formatedByte * 10) / 10} ${FORMAT[base]}`; return `${Math.trunc(formatedByte * 10) / 10} ${FORMAT[base]}`;
}; };
export const parseBytes = (sizeString: string | null): number | null => {
if (!sizeString) return null;
const regex = /^([\d.,]+)\s*([A-Za-z]+)$/;
const match = regex.exec(sizeString.trim());
if (!match) return null;
const value = Number.parseFloat(match[1].replaceAll(",", "."));
const unit = match[2].toUpperCase();
if (Number.isNaN(value)) return null;
const unitIndex = FORMAT.indexOf(unit);
if (unitIndex === -1) return null;
const byteKBase = 1024;
return Math.round(value * Math.pow(byteKBase, unitIndex));
};
export const formatBytesToMbps = (bytesPerSecond: number): string => { export const formatBytesToMbps = (bytesPerSecond: number): string => {
const bitsPerSecond = bytesPerSecond * 8; const bitsPerSecond = bytesPerSecond * 8;
const mbps = bitsPerSecond / (1024 * 1024); const mbps = bitsPerSecond / (1024 * 1024);

View File

@@ -118,6 +118,7 @@ export interface StartGameDownloadPayload {
downloadPath: string; downloadPath: string;
downloader: Downloader; downloader: Downloader;
automaticallyExtract: boolean; automaticallyExtract: boolean;
fileSize?: string | null;
} }
export interface UserFriend { export interface UserFriend {