mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-27 21:01:02 +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 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user