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

View File

@@ -1,6 +1,6 @@
import { registerEvent } from "../register-event";
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 {
downloadsSublevel,
@@ -8,6 +8,8 @@ import {
gamesSublevel,
levelKeys,
} from "@main/level";
import { Downloader, DownloadError, parseBytes } from "@shared";
import { AxiosError } from "axios";
const addGameToQueue = async (
_event: Electron.IpcMainInvokeEvent,
@@ -21,10 +23,86 @@ const addGameToQueue = async (
downloader,
uri,
automaticallyExtract,
fileSize,
} = payload;
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 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 {
await downloadsSublevel.put(gameKey, download);

View File

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

View File

@@ -499,18 +499,20 @@ export class DownloadManager {
}
static async cancelDownload(downloadKey = this.downloadingGameId) {
if (this.usingJsDownloader && this.jsDownloader) {
logger.log("[DownloadManager] Cancelling JS download");
this.jsDownloader.cancelDownload();
this.jsDownloader = null;
this.usingJsDownloader = false;
} else if (!this.isPreparingDownload) {
await PythonRPC.rpc
.post("/action", { action: "cancel", game_id: downloadKey })
.catch((err) => logger.error("Failed to cancel game download", err));
}
const isActiveDownload = downloadKey === this.downloadingGameId;
if (isActiveDownload) {
if (this.usingJsDownloader && this.jsDownloader) {
logger.log("[DownloadManager] Cancelling JS download");
this.jsDownloader.cancelDownload();
this.jsDownloader = null;
this.usingJsDownloader = false;
} 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?.webContents.send("on-download-progress", 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) {
const useJsDownloader = await this.shouldUseJsDownloader();
const isHttp = this.isHttpDownloader(download.downloader);

View File

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

View File

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

View File

@@ -51,6 +51,25 @@ export const formatBytes = (bytes: number): string => {
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 => {
const bitsPerSecond = bytesPerSecond * 8;
const mbps = bitsPerSecond / (1024 * 1024);

View File

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