diff --git a/src/main/events/library/delete-game-folder.ts b/src/main/events/library/delete-game-folder.ts index b9cef25b..1bfa36e0 100644 --- a/src/main/events/library/delete-game-folder.ts +++ b/src/main/events/library/delete-game-folder.ts @@ -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); }; diff --git a/src/main/events/torrenting/add-game-to-queue.ts b/src/main/events/torrenting/add-game-to-queue.ts index 85f321b1..b0cd7962 100644 --- a/src/main/events/torrenting/add-game-to-queue.ts +++ b/src/main/events/torrenting/add-game-to-queue.ts @@ -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); diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index e44ba936..f178029f 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -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, diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index e66d04b6..7688ae66 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -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 { + 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); diff --git a/src/renderer/src/hooks/use-download.ts b/src/renderer/src/hooks/use-download.ts index 8adb9574..6405a85c 100644 --- a/src/renderer/src/hooks/use-download.ts +++ b/src/renderer/src/hooks/use-download.ts @@ -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); }; diff --git a/src/renderer/src/pages/game-details/game-details.tsx b/src/renderer/src/pages/game-details/game-details.tsx index f885596c..af12ef0f 100644 --- a/src/renderer/src/pages/game-details/game-details.tsx +++ b/src/renderer/src/pages/game-details/game-details.tsx @@ -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) { diff --git a/src/shared/index.ts b/src/shared/index.ts index 5da36bd9..4ab56405 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -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); diff --git a/src/types/index.ts b/src/types/index.ts index 39fd0791..e2bb9fb8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -118,6 +118,7 @@ export interface StartGameDownloadPayload { downloadPath: string; downloader: Downloader; automaticallyExtract: boolean; + fileSize?: string | null; } export interface UserFriend {