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) + } + />