From 5fa4d128c3a5f1319a5117b7ad8434a20dc971e8 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Wed, 2 Apr 2025 11:25:56 +0100
Subject: [PATCH] feat: adding recursive automatic extraction
---
src/locales/en/translation.json | 1 +
src/locales/pt-BR/translation.json | 1 +
src/main/events/index.ts | 1 +
.../events/library/update-executable-path.ts | 2 +
src/main/services/7zip.ts | 61 +++++++++++++++----
src/main/services/aria2.ts | 6 ++
.../services/download/download-manager.ts | 57 +++++++----------
src/main/services/index.ts | 1 +
src/preload/index.ts | 2 +
src/renderer/src/declaration.d.ts | 5 +-
.../src/pages/downloads/download-group.tsx | 23 ++++++-
.../modals/download-settings-modal.tsx | 16 +++--
.../modals/game-options-modal.tsx | 1 +
src/shared/constants.ts | 2 +
14 files changed, 120 insertions(+), 59 deletions(-)
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 83252232..04bb5493 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -232,6 +232,7 @@
"stop_seeding": "Stop seeding",
"resume_seeding": "Resume seeding",
"options": "Manage",
+ "extract": "Extract files",
"extracting": "Extracting files…"
},
"settings": {
diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json
index a56a4f55..00c55da2 100644
--- a/src/locales/pt-BR/translation.json
+++ b/src/locales/pt-BR/translation.json
@@ -221,6 +221,7 @@
"stop_seeding": "Parar de semear",
"resume_seeding": "Semear",
"options": "Gerenciar",
+ "extract": "Extrair arquivos",
"extracting": "Extraindo arquivos…"
},
"settings": {
diff --git a/src/main/events/index.ts b/src/main/events/index.ts
index bb3399e0..9ccf24c8 100644
--- a/src/main/events/index.ts
+++ b/src/main/events/index.ts
@@ -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";
diff --git a/src/main/events/library/update-executable-path.ts b/src/main/events/library/update-executable-path.ts
index e753706b..c60638d7 100644
--- a/src/main/events/library/update-executable-path.ts
+++ b/src/main/events/library/update-executable-path.ts
@@ -21,6 +21,8 @@ const updateExecutablePath = async (
await gamesSublevel.put(gameKey, {
...game,
executablePath: parsedPath,
+ automaticCloudSync:
+ executablePath === null ? false : game.automaticCloudSync,
});
};
diff --git a/src/main/services/7zip.ts b/src/main/services/7zip.ts
index e997032f..cc057637 100644
--- a/src/main/services/7zip.ts
+++ b/src/main/services/7zip.ts
@@ -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();
}
}
diff --git a/src/main/services/aria2.ts b/src/main/services/aria2.ts
index 98fd0e13..a927a1bd 100644
--- a/src/main/services/aria2.ts
+++ b/src/main/services/aria2.ts
@@ -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 }
);
diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts
index 99176654..84289039 100644
--- a/src/main/services/download/download-manager.ts
+++ b/src/main/services/download/download-manager.ts
@@ -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);
diff --git a/src/main/services/index.ts b/src/main/services/index.ts
index 50dac560..b942aa79 100644
--- a/src/main/services/index.ts
+++ b/src/main/services/index.ts
@@ -9,3 +9,4 @@ export * from "./hydra-api";
export * from "./ludusavi";
export * from "./cloud-sync";
export * from "./7zip";
+export * from "./game-files-manager";
diff --git a/src/preload/index.ts b/src/preload/index.ts
index ebe9dc04..5f90ca19 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -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[]
diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts
index 87afa16d..0ae28e2f 100644
--- a/src/renderer/src/declaration.d.ts
+++ b/src/renderer/src/declaration.d.ts
@@ -149,6 +149,8 @@ declare global {
onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer;
resetGameAchievements: (shop: GameShop, objectId: string) => Promise;
/* User preferences */
+ authenticateRealDebrid: (apiToken: string) => Promise;
+ authenticateTorBox: (apiToken: string) => Promise;
getUserPreferences: () => Promise;
updateUserPreferences: (
preferences: Partial
@@ -157,8 +159,7 @@ declare global {
enabled: boolean;
minimized: boolean;
}) => Promise;
- authenticateRealDebrid: (apiToken: string) => Promise;
- authenticateTorBox: (apiToken: string) => Promise;
+ extractGameDownload: (shop: GameShop, objectId: string) => Promise;
onAchievementUnlocked: (cb: () => void) => () => Electron.IpcRenderer;
onExtractionComplete: (
cb: (shop: GameShop, objectId: string) => void
diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx
index abbc6595..33f4b812 100644
--- a/src/renderer/src/pages/downloads/download-group.tsx
+++ b/src/renderer/src/pages/downloads/download-group.tsx
@@ -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: ,
},
+ {
+ label: t("extract"),
+ disabled: game.download.extracting,
+ icon: ,
+ onClick: () => {
+ extractGameDownload(game.shop, game.objectId);
+ },
+ },
{
label: t("stop_seeding"),
disabled: deleting,
diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx
index 63abeb2e..0200609f 100644
--- a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx
+++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx
@@ -238,15 +238,13 @@ export function DownloadSettingsModal({
- {selectedDownloader !== Downloader.Torrent && (
-
- setAutomaticExtractionEnabled(!automaticExtractionEnabled)
- }
- />
- )}
+
+ setAutomaticExtractionEnabled(!automaticExtractionEnabled)
+ }
+ />