feat: adding recursive automatic extraction

This commit is contained in:
Chubby Granny Chaser
2025-04-02 11:25:56 +01:00
parent d15ef33a86
commit 5fa4d128c3
14 changed files with 120 additions and 59 deletions

View File

@@ -232,6 +232,7 @@
"stop_seeding": "Stop seeding",
"resume_seeding": "Resume seeding",
"options": "Manage",
"extract": "Extract files",
"extracting": "Extracting files…"
},
"settings": {

View File

@@ -221,6 +221,7 @@
"stop_seeding": "Parar de semear",
"resume_seeding": "Semear",
"options": "Gerenciar",
"extract": "Extrair arquivos",
"extracting": "Extraindo arquivos…"
},
"settings": {

View File

@@ -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";

View File

@@ -21,6 +21,8 @@ const updateExecutablePath = async (
await gamesSublevel.put(gameKey, {
...game,
executablePath: parsedPath,
automaticCloudSync:
executablePath === null ? false : game.automaticCloudSync,
});
};

View File

@@ -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();
}
}

View File

@@ -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 }
);

View File

@@ -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);

View File

@@ -9,3 +9,4 @@ export * from "./hydra-api";
export * from "./ludusavi";
export * from "./cloud-sync";
export * from "./7zip";
export * from "./game-files-manager";

View File

@@ -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">[]

View File

@@ -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

View File

@@ -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,

View File

@@ -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}

View File

@@ -134,6 +134,7 @@ export function GameOptionsModal({
const handleClearExecutablePath = async () => {
await window.electron.updateExecutablePath(game.shop, game.objectId, null);
updateGame();
};

View File

@@ -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"];