mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-25 20:01:03 +00:00
refactor: enhance download management by validating URLs and adding file size handling
This commit is contained in:
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -118,6 +118,7 @@ export interface StartGameDownloadPayload {
|
||||
downloadPath: string;
|
||||
downloader: Downloader;
|
||||
automaticallyExtract: boolean;
|
||||
fileSize?: string | null;
|
||||
}
|
||||
|
||||
export interface UserFriend {
|
||||
|
||||
Reference in New Issue
Block a user