mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
feat: adding recursive automatic extraction
This commit is contained in:
@@ -232,6 +232,7 @@
|
||||
"stop_seeding": "Stop seeding",
|
||||
"resume_seeding": "Resume seeding",
|
||||
"options": "Manage",
|
||||
"extract": "Extract files",
|
||||
"extracting": "Extracting files…"
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -221,6 +221,7 @@
|
||||
"stop_seeding": "Parar de semear",
|
||||
"resume_seeding": "Semear",
|
||||
"options": "Gerenciar",
|
||||
"extract": "Extrair arquivos",
|
||||
"extracting": "Extraindo arquivos…"
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -20,6 +20,7 @@ import "./library/close-game";
|
||||
import "./library/delete-game-folder";
|
||||
import "./library/get-game-by-object-id";
|
||||
import "./library/get-library";
|
||||
import "./library/extract-game-download";
|
||||
import "./library/open-game";
|
||||
import "./library/open-game-executable-path";
|
||||
import "./library/open-game-installer";
|
||||
|
||||
@@ -21,6 +21,8 @@ const updateExecutablePath = async (
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
executablePath: parsedPath,
|
||||
automaticCloudSync:
|
||||
executablePath === null ? false : game.automaticCloudSync,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { app } from "electron";
|
||||
import cp from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { logger } from "./logger";
|
||||
|
||||
export const binaryName = {
|
||||
linux: "7zzs",
|
||||
@@ -20,19 +21,55 @@ export class _7Zip {
|
||||
);
|
||||
|
||||
public static extractFile(
|
||||
filePath: string,
|
||||
outputPath: string,
|
||||
cb: () => void
|
||||
) {
|
||||
const child = cp.spawn(this.binaryPath, [
|
||||
"x",
|
||||
{
|
||||
filePath,
|
||||
`-o"${outputPath}"`,
|
||||
"-y",
|
||||
]);
|
||||
outputPath,
|
||||
cwd,
|
||||
passwords = [],
|
||||
}: {
|
||||
filePath: string;
|
||||
outputPath?: string;
|
||||
cwd?: string;
|
||||
passwords?: string[];
|
||||
},
|
||||
cb: (success: boolean) => void
|
||||
) {
|
||||
const tryPassword = (index = -1) => {
|
||||
const password = passwords[index] ?? "";
|
||||
logger.info(`Trying password ${password} on ${filePath}`);
|
||||
|
||||
child.on("exit", () => {
|
||||
cb();
|
||||
});
|
||||
const args = ["x", filePath, "-y", "-p" + password];
|
||||
|
||||
if (outputPath) {
|
||||
args.push("-o" + outputPath);
|
||||
}
|
||||
|
||||
const child = cp.execFile(this.binaryPath, args, {
|
||||
cwd,
|
||||
});
|
||||
|
||||
child.once("exit", (code) => {
|
||||
console.log("EXIT CALLED", code, filePath);
|
||||
|
||||
if (code === 0) {
|
||||
cb(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (index < passwords.length - 1) {
|
||||
logger.info(
|
||||
`Failed to extract file: ${filePath} with password: ${password}. Trying next password...`
|
||||
);
|
||||
|
||||
tryPassword(index + 1);
|
||||
} else {
|
||||
logger.info(`Failed to extract file: ${filePath}`);
|
||||
|
||||
cb(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
tryPassword();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@ export class Aria2 {
|
||||
"--rpc-listen-all",
|
||||
"--file-allocation=none",
|
||||
"--allow-overwrite=true",
|
||||
"-s",
|
||||
"16",
|
||||
"-x",
|
||||
"16",
|
||||
"-k",
|
||||
"1M",
|
||||
],
|
||||
{ stdio: "inherit", windowsHide: true }
|
||||
);
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { Downloader, DownloadError } from "@shared";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import {
|
||||
publishDownloadCompleteNotification,
|
||||
publishExtractionCompleteNotification,
|
||||
} from "../notifications";
|
||||
import { publishDownloadCompleteNotification } from "../notifications";
|
||||
import type { Download, DownloadProgress, UserPreferences } from "@types";
|
||||
import {
|
||||
GofileApi,
|
||||
@@ -25,11 +22,11 @@ import { logger } from "../logger";
|
||||
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||
import { sortBy } from "lodash-es";
|
||||
import { TorBoxClient } from "./torbox";
|
||||
import { _7Zip } from "../7zip";
|
||||
import { FILE_EXTENSIONS_TO_EXTRACT } from "@shared";
|
||||
import { GameFilesManager } from "../game-files-manager";
|
||||
|
||||
export class DownloadManager {
|
||||
private static downloadingGameId: string | null = null;
|
||||
private static readonly extensionsToExtract = [".rar", ".zip", ".7z"];
|
||||
|
||||
public static async startRPC(
|
||||
download?: Download,
|
||||
@@ -155,12 +152,7 @@ export class DownloadManager {
|
||||
queued: false,
|
||||
});
|
||||
} else {
|
||||
const shouldExtract =
|
||||
download.downloader !== Downloader.Torrent &&
|
||||
this.extensionsToExtract.some((ext) =>
|
||||
download.folderName?.endsWith(ext)
|
||||
) &&
|
||||
download.automaticallyExtract;
|
||||
const shouldExtract = download.automaticallyExtract;
|
||||
|
||||
downloadsSublevel.put(gameId, {
|
||||
...download,
|
||||
@@ -171,29 +163,26 @@ export class DownloadManager {
|
||||
});
|
||||
|
||||
if (shouldExtract) {
|
||||
_7Zip.extractFile(
|
||||
path.join(download.downloadPath, download.folderName!),
|
||||
path.join(
|
||||
download.downloadPath,
|
||||
path.parse(download.folderName!).name
|
||||
),
|
||||
async () => {
|
||||
const download = await downloadsSublevel.get(gameId);
|
||||
|
||||
downloadsSublevel.put(gameId, {
|
||||
...download!,
|
||||
extracting: false,
|
||||
});
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
"on-extraction-complete",
|
||||
game.shop,
|
||||
game.objectId
|
||||
);
|
||||
|
||||
publishExtractionCompleteNotification(game);
|
||||
}
|
||||
const gameFilesManager = new GameFilesManager(
|
||||
game.shop,
|
||||
game.objectId
|
||||
);
|
||||
|
||||
if (
|
||||
FILE_EXTENSIONS_TO_EXTRACT.some((ext) =>
|
||||
download.folderName?.endsWith(ext)
|
||||
)
|
||||
) {
|
||||
gameFilesManager.extractDownloadedFile();
|
||||
} else {
|
||||
gameFilesManager
|
||||
.extractFilesInDirectory(
|
||||
path.join(download.downloadPath, download.folderName!)
|
||||
)
|
||||
.then(() => {
|
||||
gameFilesManager.setExtractionComplete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.cancelDownload(gameId);
|
||||
|
||||
@@ -9,3 +9,4 @@ export * from "./hydra-api";
|
||||
export * from "./ludusavi";
|
||||
export * from "./cloud-sync";
|
||||
export * from "./7zip";
|
||||
export * from "./game-files-manager";
|
||||
|
||||
@@ -178,6 +178,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("getGameByObjectId", shop, objectId),
|
||||
resetGameAchievements: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("resetGameAchievements", shop, objectId),
|
||||
extractGameDownload: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("extractGameDownload", shop, objectId),
|
||||
onGamesRunning: (
|
||||
cb: (
|
||||
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
||||
|
||||
5
src/renderer/src/declaration.d.ts
vendored
5
src/renderer/src/declaration.d.ts
vendored
@@ -149,6 +149,8 @@ declare global {
|
||||
onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer;
|
||||
resetGameAchievements: (shop: GameShop, objectId: string) => Promise<void>;
|
||||
/* User preferences */
|
||||
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
||||
authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>;
|
||||
getUserPreferences: () => Promise<UserPreferences | null>;
|
||||
updateUserPreferences: (
|
||||
preferences: Partial<UserPreferences>
|
||||
@@ -157,8 +159,7 @@ declare global {
|
||||
enabled: boolean;
|
||||
minimized: boolean;
|
||||
}) => Promise<void>;
|
||||
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
||||
authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>;
|
||||
extractGameDownload: (shop: GameShop, objectId: string) => Promise<boolean>;
|
||||
onAchievementUnlocked: (cb: () => void) => () => Electron.IpcRenderer;
|
||||
onExtractionComplete: (
|
||||
cb: (shop: GameShop, objectId: string) => void
|
||||
|
||||
@@ -10,11 +10,11 @@ import {
|
||||
|
||||
import { Downloader, formatBytes, steamUrlBuilder } from "@shared";
|
||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||
import { useAppSelector, useDownload } from "@renderer/hooks";
|
||||
import { useAppSelector, useDownload, useLibrary } from "@renderer/hooks";
|
||||
|
||||
import "./download-group.scss";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuItem,
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import {
|
||||
ColumnsIcon,
|
||||
DownloadIcon,
|
||||
FileDirectoryIcon,
|
||||
LinkIcon,
|
||||
PlayIcon,
|
||||
QuestionIcon,
|
||||
@@ -56,6 +57,8 @@ export function DownloadGroup({
|
||||
(state) => state.userPreferences.value
|
||||
);
|
||||
|
||||
const { updateLibrary } = useLibrary();
|
||||
|
||||
const {
|
||||
lastPacket,
|
||||
progress,
|
||||
@@ -89,6 +92,14 @@ export function DownloadGroup({
|
||||
return map;
|
||||
}, [seedingStatus]);
|
||||
|
||||
const extractGameDownload = useCallback(
|
||||
async (shop: GameShop, objectId: string) => {
|
||||
await window.electron.extractGameDownload(shop, objectId);
|
||||
updateLibrary();
|
||||
},
|
||||
[updateLibrary]
|
||||
);
|
||||
|
||||
const getGameInfo = (game: LibraryGame) => {
|
||||
const download = game.download!;
|
||||
|
||||
@@ -201,6 +212,14 @@ export function DownloadGroup({
|
||||
},
|
||||
icon: <DownloadIcon />,
|
||||
},
|
||||
{
|
||||
label: t("extract"),
|
||||
disabled: game.download.extracting,
|
||||
icon: <FileDirectoryIcon />,
|
||||
onClick: () => {
|
||||
extractGameDownload(game.shop, game.objectId);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("stop_seeding"),
|
||||
disabled: deleting,
|
||||
|
||||
@@ -238,15 +238,13 @@ export function DownloadSettingsModal({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{selectedDownloader !== Downloader.Torrent && (
|
||||
<CheckboxField
|
||||
label={t("automatically_extract_downloaded_files")}
|
||||
checked={automaticExtractionEnabled}
|
||||
onChange={() =>
|
||||
setAutomaticExtractionEnabled(!automaticExtractionEnabled)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<CheckboxField
|
||||
label={t("automatically_extract_downloaded_files")}
|
||||
checked={automaticExtractionEnabled}
|
||||
onChange={() =>
|
||||
setAutomaticExtractionEnabled(!automaticExtractionEnabled)
|
||||
}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={handleStartClick}
|
||||
|
||||
@@ -134,6 +134,7 @@ export function GameOptionsModal({
|
||||
|
||||
const handleClearExecutablePath = async () => {
|
||||
await window.electron.updateExecutablePath(game.shop, game.objectId, null);
|
||||
|
||||
updateGame();
|
||||
};
|
||||
|
||||
|
||||
@@ -57,3 +57,5 @@ export enum DownloadError {
|
||||
GofileQuotaExceeded = "download_error_gofile_quota_exceeded",
|
||||
RealDebridAccountNotAuthorized = "download_error_real_debrid_account_not_authorized",
|
||||
}
|
||||
|
||||
export const FILE_EXTENSIONS_TO_EXTRACT = [".rar", ".zip", ".7z"];
|
||||
|
||||
Reference in New Issue
Block a user