From 74a99f5bc8f2475b61819e32a1c6ab64f0a22320 Mon Sep 17 00:00:00 2001 From: Hydra Date: Sun, 5 May 2024 19:18:48 +0100 Subject: [PATCH] fix: fixing errors with electron dl manager --- .gitignore | 1 - electron-builder.yml | 1 - electron.vite.config.ts | 4 +- package.json | 2 +- postinstall.cjs | 2 - src/globals.ts | 25 --- src/main/entity/game.entity.ts | 14 +- .../events/library/add-game-to-library.ts | 1 + src/main/events/library/close-game.ts | 4 +- src/main/events/library/delete-game-folder.ts | 3 +- src/main/events/library/get-library.ts | 2 +- .../events/library/open-game-installer.ts | 4 +- src/main/events/library/remove-game.ts | 2 +- .../events/torrenting/cancel-game-download.ts | 8 +- .../events/torrenting/pause-game-download.ts | 13 +- .../events/torrenting/resume-game-download.ts | 12 +- .../events/torrenting/start-game-download.ts | 37 ++-- .../update-user-preferences.ts | 5 + src/main/main.ts | 71 ++++---- src/main/services/download-manager.ts | 76 ++++++++ src/main/services/downloaders/downloader.ts | 167 ++---------------- .../services/downloaders/http-downloader.ts | 106 ----------- .../services/downloaders/http.downloader.ts | 101 +++++++++++ src/main/services/downloaders/index.ts | 2 + src/main/services/downloaders/real-debrid.ts | 74 -------- .../services/downloaders/torrent-client.ts | 133 -------------- .../downloaders/torrent.downloader.ts | 160 +++++++++++++++++ src/main/services/index.ts | 3 +- src/main/services/process-watcher.ts | 1 + src/main/services/real-debrid.ts | 73 ++++++++ ...l-debrid-types.ts => real-debrid.types.ts} | 6 +- src/main/services/unrar.ts | 30 ---- src/renderer/src/app.tsx | 3 +- .../components/bottom-panel/bottom-panel.tsx | 13 +- .../checkbox-field/checkbox-field.tsx | 2 +- .../src/components/sidebar/sidebar.tsx | 6 +- .../src/components/text-field/text-field.tsx | 2 +- src/renderer/src/hooks/index.ts | 1 + src/renderer/src/hooks/use-download.ts | 9 +- .../src/pages/downloads/downloads.tsx | 24 +-- .../src/pages/game-details/game-details.tsx | 4 +- .../game-details/hero/hero-panel-actions.tsx | 9 +- .../game-details/hero/hero-panel-playtime.tsx | 78 ++++++++ .../pages/game-details/hero/hero-panel.tsx | 77 ++------ src/renderer/src/pages/settings/settings.tsx | 1 + src/shared/index.ts | 35 ++++ src/types/index.ts | 3 +- torrent-client/fifo.py | 7 +- tsconfig.node.json | 4 +- tsconfig.web.json | 4 +- yarn.lock | 59 +------ 51 files changed, 718 insertions(+), 766 deletions(-) delete mode 100644 src/globals.ts create mode 100644 src/main/services/download-manager.ts delete mode 100644 src/main/services/downloaders/http-downloader.ts create mode 100644 src/main/services/downloaders/http.downloader.ts create mode 100644 src/main/services/downloaders/index.ts delete mode 100644 src/main/services/downloaders/real-debrid.ts delete mode 100644 src/main/services/downloaders/torrent-client.ts create mode 100644 src/main/services/downloaders/torrent.downloader.ts create mode 100644 src/main/services/real-debrid.ts rename src/main/services/{downloaders/real-debrid-types.ts => real-debrid.types.ts} (85%) delete mode 100644 src/main/services/unrar.ts create mode 100644 src/renderer/src/pages/game-details/hero/hero-panel-playtime.tsx create mode 100644 src/shared/index.ts diff --git a/.gitignore b/.gitignore index 95835897..675a83ee 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ node_modules hydra-download-manager fastlist.exe -unrar.wasm __pycache__ dist out diff --git a/electron-builder.yml b/electron-builder.yml index 83c7c80a..06f6a5c5 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -6,7 +6,6 @@ extraResources: - hydra-download-manager - hydra.db - fastlist.exe - - unrar.wasm files: - "!**/.vscode/*" - "!src/*" diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 5140f366..4368de53 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -31,7 +31,7 @@ export default defineConfig(({ mode }) => { "@main": resolve("src/main"), "@locales": resolve("src/locales"), "@resources": resolve("resources"), - "@globals": resolve("src/globals"), + "@shared": resolve("src/shared"), }, }, plugins: [externalizeDepsPlugin(), swcPlugin(), sentryPlugin], @@ -47,7 +47,7 @@ export default defineConfig(({ mode }) => { alias: { "@renderer": resolve("src/renderer/src"), "@locales": resolve("src/locales"), - "@globals": resolve("src/globals"), + "@shared": resolve("src/shared"), }, }, plugins: [svgr(), react(), vanillaExtractPlugin(), sentryPlugin], diff --git a/package.json b/package.json index 678d1bb0..f16909df 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "classnames": "^2.5.1", "color.js": "^1.2.0", "date-fns": "^3.6.0", - "electron-dl-manager": "^3.0.0", + "easydl": "^1.1.1", "fetch-cookie": "^3.0.1", "flexsearch": "^0.7.43", "i18next": "^23.11.2", diff --git a/postinstall.cjs b/postinstall.cjs index d2c91bca..8ca8f101 100644 --- a/postinstall.cjs +++ b/postinstall.cjs @@ -6,5 +6,3 @@ if (process.platform === "win32") { "fastlist.exe" ); } - -fs.copyFileSync("node_modules/node-unrar-js/esm/js/unrar.wasm", "unrar.wasm"); diff --git a/src/globals.ts b/src/globals.ts deleted file mode 100644 index 3ce216a8..00000000 --- a/src/globals.ts +++ /dev/null @@ -1,25 +0,0 @@ -export enum GameStatus { - Seeding = "seeding", - Downloading = "downloading", - Paused = "paused", - CheckingFiles = "checking_files", - DownloadingMetadata = "downloading_metadata", - Cancelled = "cancelled", - Finished = "finished", - Decompressing = "decompressing", -} - -export namespace GameStatus { - export const isDownloading = (status: GameStatus | null) => - status === GameStatus.Downloading || - status === GameStatus.DownloadingMetadata || - status === GameStatus.CheckingFiles; - - export const isVerifying = (status: GameStatus | null) => - GameStatus.DownloadingMetadata == status || - GameStatus.CheckingFiles == status || - GameStatus.Decompressing == status; - - export const isReady = (status: GameStatus | null) => - status === GameStatus.Finished || status === GameStatus.Seeding; -} diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts index 51e976fa..6280930b 100644 --- a/src/main/entity/game.entity.ts +++ b/src/main/entity/game.entity.ts @@ -7,9 +7,10 @@ import { OneToOne, JoinColumn, } from "typeorm"; -import type { GameShop } from "@types"; import { Repack } from "./repack.entity"; -import { GameStatus } from "@globals"; + +import type { GameShop } from "@types"; +import { Downloader, GameStatus } from "@shared"; @Entity("game") export class Game { @@ -34,9 +35,6 @@ export class Game { @Column("text", { nullable: true }) executablePath: string | null; - @Column("text", { nullable: true }) - rarPath: string | null; - @Column("int", { default: 0 }) playTimeInMilliseconds: number; @@ -46,6 +44,9 @@ export class Game { @Column("text", { nullable: true }) status: GameStatus | null; + @Column("int", { default: Downloader.Torrent }) + downloader: Downloader; + /** * Progress is a float between 0 and 1 */ @@ -55,9 +56,6 @@ export class Game { @Column("float", { default: 0 }) fileVerificationProgress: number; - @Column("float", { default: 0 }) - decompressionProgress: number; - @Column("int", { default: 0 }) bytesDownloaded: number; diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index 49f42d5f..2644d183 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -16,6 +16,7 @@ const addGameToLibrary = async ( const game = await gameRepository.findOne({ where: { objectID, + isDeleted: false, }, }); diff --git a/src/main/events/library/close-game.ts b/src/main/events/library/close-game.ts index d549f3b7..77613e21 100644 --- a/src/main/events/library/close-game.ts +++ b/src/main/events/library/close-game.ts @@ -10,7 +10,9 @@ const closeGame = async ( gameId: number ) => { const processes = await getProcesses(); - const game = await gameRepository.findOne({ where: { id: gameId } }); + const game = await gameRepository.findOne({ + where: { id: gameId, isDeleted: false }, + }); if (!game) return false; diff --git a/src/main/events/library/delete-game-folder.ts b/src/main/events/library/delete-game-folder.ts index e913a23a..66f37b28 100644 --- a/src/main/events/library/delete-game-folder.ts +++ b/src/main/events/library/delete-game-folder.ts @@ -1,7 +1,7 @@ import path from "node:path"; import fs from "node:fs"; -import { GameStatus } from "@globals"; +import { GameStatus } from "@shared"; import { gameRepository } from "@main/repository"; import { getDownloadsPath } from "../helpers/get-downloads-path"; @@ -16,6 +16,7 @@ const deleteGameFolder = async ( where: { id: gameId, status: GameStatus.Cancelled, + isDeleted: false, }, }); diff --git a/src/main/events/library/get-library.ts b/src/main/events/library/get-library.ts index 1e74ad81..2910d528 100644 --- a/src/main/events/library/get-library.ts +++ b/src/main/events/library/get-library.ts @@ -2,7 +2,7 @@ import { gameRepository } from "@main/repository"; import { searchRepacks } from "../helpers/search-games"; import { registerEvent } from "../register-event"; -import { GameStatus } from "@globals"; +import { GameStatus } from "@shared"; import { sortBy } from "lodash-es"; const getLibrary = async () => diff --git a/src/main/events/library/open-game-installer.ts b/src/main/events/library/open-game-installer.ts index 796e063b..27f110e5 100644 --- a/src/main/events/library/open-game-installer.ts +++ b/src/main/events/library/open-game-installer.ts @@ -13,7 +13,9 @@ const openGameInstaller = async ( _event: Electron.IpcMainInvokeEvent, gameId: number ) => { - const game = await gameRepository.findOne({ where: { id: gameId } }); + const game = await gameRepository.findOne({ + where: { id: gameId, isDeleted: false }, + }); if (!game) return true; diff --git a/src/main/events/library/remove-game.ts b/src/main/events/library/remove-game.ts index 6e2ee785..f207aea9 100644 --- a/src/main/events/library/remove-game.ts +++ b/src/main/events/library/remove-game.ts @@ -1,6 +1,6 @@ import { registerEvent } from "../register-event"; import { gameRepository } from "../../repository"; -import { GameStatus } from "@globals"; +import { GameStatus } from "@shared"; const removeGame = async ( _event: Electron.IpcMainInvokeEvent, diff --git a/src/main/events/torrenting/cancel-game-download.ts b/src/main/events/torrenting/cancel-game-download.ts index 73ae0bb6..d7603c76 100644 --- a/src/main/events/torrenting/cancel-game-download.ts +++ b/src/main/events/torrenting/cancel-game-download.ts @@ -4,8 +4,8 @@ import { registerEvent } from "../register-event"; import { WindowManager } from "@main/services"; import { In } from "typeorm"; -import { Downloader } from "@main/services/downloaders/downloader"; -import { GameStatus } from "@globals"; +import { DownloadManager } from "@main/services"; +import { GameStatus } from "@shared"; const cancelGameDownload = async ( _event: Electron.IpcMainInvokeEvent, @@ -14,6 +14,7 @@ const cancelGameDownload = async ( const game = await gameRepository.findOne({ where: { id: gameId, + isDeleted: false, status: In([ GameStatus.Downloading, GameStatus.DownloadingMetadata, @@ -21,12 +22,12 @@ const cancelGameDownload = async ( GameStatus.Paused, GameStatus.Seeding, GameStatus.Finished, - GameStatus.Decompressing, ]), }, }); if (!game) return; + DownloadManager.cancelDownload(); await gameRepository .update( @@ -44,7 +45,6 @@ const cancelGameDownload = async ( game.status !== GameStatus.Paused && game.status !== GameStatus.Seeding ) { - Downloader.cancelDownload(); if (result.affected) WindowManager.mainWindow?.setProgressBar(-1); } }); diff --git a/src/main/events/torrenting/pause-game-download.ts b/src/main/events/torrenting/pause-game-download.ts index 3d486562..bdc8bf41 100644 --- a/src/main/events/torrenting/pause-game-download.ts +++ b/src/main/events/torrenting/pause-game-download.ts @@ -1,15 +1,15 @@ -import { WindowManager } from "@main/services"; - import { registerEvent } from "../register-event"; import { gameRepository } from "../../repository"; import { In } from "typeorm"; -import { Downloader } from "@main/services/downloaders/downloader"; -import { GameStatus } from "@globals"; +import { DownloadManager, WindowManager } from "@main/services"; +import { GameStatus } from "@shared"; const pauseGameDownload = async ( _event: Electron.IpcMainInvokeEvent, gameId: number ) => { + DownloadManager.pauseDownload(); + await gameRepository .update( { @@ -23,10 +23,7 @@ const pauseGameDownload = async ( { status: GameStatus.Paused } ) .then((result) => { - if (result.affected) { - Downloader.pauseDownload(); - WindowManager.mainWindow?.setProgressBar(-1); - } + if (result.affected) WindowManager.mainWindow?.setProgressBar(-1); }); }; diff --git a/src/main/events/torrenting/resume-game-download.ts b/src/main/events/torrenting/resume-game-download.ts index 03f56a13..59ea9c4c 100644 --- a/src/main/events/torrenting/resume-game-download.ts +++ b/src/main/events/torrenting/resume-game-download.ts @@ -2,8 +2,8 @@ import { registerEvent } from "../register-event"; import { gameRepository } from "../../repository"; import { getDownloadsPath } from "../helpers/get-downloads-path"; import { In } from "typeorm"; -import { Downloader } from "@main/services/downloaders/downloader"; -import { GameStatus } from "@globals"; +import { DownloadManager } from "@main/services"; +import { GameStatus } from "@shared"; const resumeGameDownload = async ( _event: Electron.IpcMainInvokeEvent, @@ -12,18 +12,18 @@ const resumeGameDownload = async ( const game = await gameRepository.findOne({ where: { id: gameId, + isDeleted: false, }, relations: { repack: true }, }); if (!game) return; - - Downloader.resumeDownload(); + DownloadManager.pauseDownload(); if (game.status === GameStatus.Paused) { const downloadsPath = game.downloadPath ?? (await getDownloadsPath()); - Downloader.downloadGame(game, game.repack); + DownloadManager.resumeDownload(gameId); await gameRepository.update( { @@ -39,7 +39,7 @@ const resumeGameDownload = async ( await gameRepository.update( { id: game.id }, { - status: GameStatus.DownloadingMetadata, + status: GameStatus.Downloading, downloadPath: downloadsPath, } ); diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index 90716ae3..8ce62f0b 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -1,13 +1,17 @@ import { getSteamGameIconUrl } from "@main/services"; -import { gameRepository, repackRepository } from "@main/repository"; +import { + gameRepository, + repackRepository, + userPreferencesRepository, +} from "@main/repository"; import { registerEvent } from "../register-event"; import type { GameShop } from "@types"; import { getFileBase64 } from "@main/helpers"; import { In } from "typeorm"; -import { Downloader } from "@main/services/downloaders/downloader"; -import { GameStatus } from "@globals"; +import { DownloadManager } from "@main/services"; +import { Downloader, GameStatus } from "@shared"; const startGameDownload = async ( _event: Electron.IpcMainInvokeEvent, @@ -17,6 +21,14 @@ const startGameDownload = async ( gameShop: GameShop, downloadPath: string ) => { + const userPreferences = await userPreferencesRepository.findOne({ + where: { id: 1 }, + }); + + const downloader = userPreferences?.realDebridApiToken + ? Downloader.Http + : Downloader.Torrent; + const [game, repack] = await Promise.all([ gameRepository.findOne({ where: { @@ -30,13 +42,8 @@ const startGameDownload = async ( }), ]); - if (!repack) return; - - if (game?.status === GameStatus.Downloading) { - return; - } - - Downloader.pauseDownload(); + if (!repack || game?.status === GameStatus.Downloading) return; + DownloadManager.pauseDownload(); await gameRepository.update( { @@ -57,12 +64,13 @@ const startGameDownload = async ( { status: GameStatus.DownloadingMetadata, downloadPath: downloadPath, + downloader, repack: { id: repackId }, isDeleted: false, } ); - Downloader.downloadGame(game, repack); + DownloadManager.downloadGame(game.id); game.status = GameStatus.DownloadingMetadata; @@ -74,13 +82,14 @@ const startGameDownload = async ( title, iconUrl, objectID, + downloader, shop: gameShop, - status: GameStatus.DownloadingMetadata, - downloadPath: downloadPath, + status: GameStatus.Downloading, + downloadPath, repack: { id: repackId }, }); - Downloader.downloadGame(createdGame, repack); + DownloadManager.downloadGame(createdGame.id); const { repack: _, ...rest } = createdGame; diff --git a/src/main/events/user-preferences/update-user-preferences.ts b/src/main/events/user-preferences/update-user-preferences.ts index 000eca7b..89622166 100644 --- a/src/main/events/user-preferences/update-user-preferences.ts +++ b/src/main/events/user-preferences/update-user-preferences.ts @@ -2,11 +2,16 @@ import { userPreferencesRepository } from "@main/repository"; import { registerEvent } from "../register-event"; import type { UserPreferences } from "@types"; +import { RealDebridClient } from "@main/services/real-debrid"; const updateUserPreferences = async ( _event: Electron.IpcMainInvokeEvent, preferences: Partial ) => { + if (preferences.realDebridApiToken) { + RealDebridClient.authorize(preferences.realDebridApiToken); + } + await userPreferencesRepository.upsert( { id: 1, diff --git a/src/main/main.ts b/src/main/main.ts index 80f975eb..ab7a5003 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -6,9 +6,8 @@ import { getNewRepacksFromUser, getNewRepacksFromXatab, getNewRepacksFromOnlineFix, - readPipe, startProcessWatcher, - writePipe, + DownloadManager, } from "./services"; import { gameRepository, @@ -17,39 +16,16 @@ import { steamGameRepository, userPreferencesRepository, } from "./repository"; -import { TorrentClient } from "./services/downloaders/torrent-client"; -import { Repack } from "./entity"; +import { TorrentDownloader } from "./services"; +import { Repack, UserPreferences } from "./entity"; import { Notification } from "electron"; import { t } from "i18next"; +import { GameStatus } from "@shared"; import { In } from "typeorm"; -import { Downloader } from "./services/downloaders/downloader"; -import { GameStatus } from "@globals"; +import { RealDebridClient } from "./services/real-debrid"; startProcessWatcher(); -TorrentClient.startTorrentClient(writePipe.socketPath, readPipe.socketPath); - -Promise.all([writePipe.createPipe(), readPipe.createPipe()]).then(async () => { - const game = await gameRepository.findOne({ - where: { - status: In([ - GameStatus.Downloading, - GameStatus.DownloadingMetadata, - GameStatus.CheckingFiles, - ]), - }, - relations: { repack: true }, - }); - - if (game) { - Downloader.downloadGame(game, game.repack); - } - - readPipe.socket?.on("data", (data) => { - TorrentClient.onSocketData(data); - }); -}); - const track1337xUsers = async (existingRepacks: Repack[]) => { for (const repacker of repackers) { await getNewRepacksFromUser( @@ -59,11 +35,7 @@ const track1337xUsers = async (existingRepacks: Repack[]) => { } }; -const checkForNewRepacks = async () => { - const userPreferences = await userPreferencesRepository.findOne({ - where: { id: 1 }, - }); - +const checkForNewRepacks = async (userPreferences: UserPreferences | null) => { const existingRepacks = stateManager.getValue("repacks"); Promise.allSettled([ @@ -101,7 +73,7 @@ const checkForNewRepacks = async () => { }); }; -const loadState = async () => { +const loadState = async (userPreferences: UserPreferences | null) => { const [friendlyNames, repacks, steamGames] = await Promise.all([ repackerFriendlyNameRepository.find(), repackRepository.find({ @@ -121,6 +93,33 @@ const loadState = async () => { stateManager.setValue("steamGames", steamGames); import("./events"); + + if (userPreferences?.realDebridApiToken) + await RealDebridClient.authorize(userPreferences?.realDebridApiToken); + + const game = await gameRepository.findOne({ + where: { + status: In([ + GameStatus.Downloading, + GameStatus.DownloadingMetadata, + GameStatus.CheckingFiles, + ]), + isDeleted: false, + }, + relations: { repack: true }, + }); + + await TorrentDownloader.startClient(); + + if (game) { + DownloadManager.resumeDownload(game.id); + } }; -loadState().then(() => checkForNewRepacks()); +userPreferencesRepository + .findOne({ + where: { id: 1 }, + }) + .then((userPreferences) => { + loadState(userPreferences).then(() => checkForNewRepacks(userPreferences)); + }); diff --git a/src/main/services/download-manager.ts b/src/main/services/download-manager.ts new file mode 100644 index 00000000..ca98fe82 --- /dev/null +++ b/src/main/services/download-manager.ts @@ -0,0 +1,76 @@ +import { gameRepository } from "@main/repository"; + +import type { Game } from "@main/entity"; +import { Downloader } from "@shared"; + +import { writePipe } from "./fifo"; +import { HTTPDownloader } from "./downloaders"; + +export class DownloadManager { + private static gameDownloading: Game; + + static async getGame(gameId: number) { + return gameRepository.findOne({ + where: { id: gameId, isDeleted: false }, + relations: { + repack: true, + }, + }); + } + + static async cancelDownload() { + if ( + this.gameDownloading && + this.gameDownloading.downloader === Downloader.Torrent + ) { + writePipe.write({ action: "cancel" }); + } else { + HTTPDownloader.destroy(); + } + } + + static async pauseDownload() { + if ( + this.gameDownloading && + this.gameDownloading.downloader === Downloader.Torrent + ) { + writePipe.write({ action: "pause" }); + } else { + HTTPDownloader.destroy(); + } + } + + static async resumeDownload(gameId: number) { + const game = await this.getGame(gameId); + + if (game!.downloader === Downloader.Torrent) { + writePipe.write({ + action: "start", + game_id: game!.id, + magnet: game!.repack.magnet, + save_path: game!.downloadPath, + }); + } else { + HTTPDownloader.startDownload(game!); + } + + this.gameDownloading = game!; + } + + static async downloadGame(gameId: number) { + const game = await this.getGame(gameId); + + if (game!.downloader === Downloader.Torrent) { + writePipe.write({ + action: "start", + game_id: game!.id, + magnet: game!.repack.magnet, + save_path: game!.downloadPath, + }); + } else { + HTTPDownloader.startDownload(game!); + } + + this.gameDownloading = game!; + } +} diff --git a/src/main/services/downloaders/downloader.ts b/src/main/services/downloaders/downloader.ts index da1926f2..084936d9 100644 --- a/src/main/services/downloaders/downloader.ts +++ b/src/main/services/downloaders/downloader.ts @@ -1,105 +1,29 @@ -import { Game, Repack } from "@main/entity"; -import { writePipe } from "../fifo"; -import { gameRepository, userPreferencesRepository } from "@main/repository"; -import { RealDebridClient } from "./real-debrid"; -import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; import { t } from "i18next"; import { Notification } from "electron"; + +import { Game } from "@main/entity"; + +import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; + import { WindowManager } from "../window-manager"; -import { TorrentUpdate } from "./torrent-client"; -import { HTTPDownloader } from "./http-downloader"; -import { Unrar } from "../unrar"; -import { GameStatus } from "@globals"; -import path from "node:path"; -import crypto from "node:crypto"; -import fs from "node:fs"; -import { app } from "electron"; +import type { TorrentUpdate } from "./torrent.downloader"; + +import { GameStatus, GameStatusHelper } from "@shared"; +import { gameRepository, userPreferencesRepository } from "@main/repository"; interface DownloadStatus { - numPeers: number; - numSeeds: number; - downloadSpeed: number; - timeRemaining: number; + numPeers?: number; + numSeeds?: number; + downloadSpeed?: number; + timeRemaining?: number; } export class Downloader { - private static lastHttpDownloader: HTTPDownloader | null = null; + static getGameProgress(game: Game) { + if (game.status === GameStatus.CheckingFiles) + return game.fileVerificationProgress; - static async usesRealDebrid() { - const userPreferences = await userPreferencesRepository.findOne({ - where: { id: 1 }, - }); - return userPreferences!.realDebridApiToken !== null; - } - - static async cancelDownload() { - if (!(await this.usesRealDebrid())) { - writePipe.write({ action: "cancel" }); - } else { - if (this.lastHttpDownloader) { - this.lastHttpDownloader.cancel(); - } - } - } - - static async pauseDownload() { - if (!(await this.usesRealDebrid())) { - writePipe.write({ action: "pause" }); - } else { - if (this.lastHttpDownloader) { - this.lastHttpDownloader.pause(); - } - } - } - - static async resumeDownload() { - if (!(await this.usesRealDebrid())) { - writePipe.write({ action: "pause" }); - } else { - if (this.lastHttpDownloader) { - this.lastHttpDownloader.resume(); - } - } - } - - static async downloadGame(game: Game, repack: Repack) { - if (!(await this.usesRealDebrid())) { - writePipe.write({ - action: "start", - game_id: game.id, - magnet: repack.magnet, - save_path: game.downloadPath, - }); - } else { - try { - // Lets try first to find the torrent on RealDebrid - const torrents = await RealDebridClient.getAllTorrents(); - const hash = RealDebridClient.extractSHA1FromMagnet(repack.magnet); - let torrent = torrents.find((t) => t.hash === hash); - - if (!torrent) { - // Torrent is missing, lets add it - const magnet = await RealDebridClient.addMagnet(repack.magnet); - if (magnet && magnet.id) { - await RealDebridClient.selectAllFiles(magnet.id); - torrent = await RealDebridClient.getInfo(magnet.id); - } - } - - if (torrent) { - const { links } = torrent; - const { download } = await RealDebridClient.unrestrictLink(links[0]); - this.lastHttpDownloader = new HTTPDownloader(); - this.lastHttpDownloader.download( - download, - game.downloadPath!, - game.id - ); - } - } catch (e) { - console.error(e); - } - } + return game.progress; } static async updateGameProgress( @@ -110,23 +34,17 @@ export class Downloader { await gameRepository.update({ id: gameId }, gameUpdate); const game = await gameRepository.findOne({ - where: { id: gameId }, + where: { id: gameId, isDeleted: false }, relations: { repack: true }, }); - if ( - game?.progress === 1 && - gameUpdate.status !== GameStatus.Decompressing - ) { + if (game?.progress === 1) { const userPreferences = await userPreferencesRepository.findOne({ where: { id: 1 }, }); if (userPreferences?.downloadNotificationsEnabled) { - const iconPath = await this.createTempIcon(game.iconUrl); - new Notification({ - icon: iconPath, title: t("download_complete", { ns: "notifications", lng: userPreferences.language, @@ -140,26 +58,6 @@ export class Downloader { } } - if ( - game && - gameUpdate.decompressionProgress === 0 && - gameUpdate.status === GameStatus.Decompressing - ) { - const unrar = await Unrar.fromFilePath( - game.rarPath!, - path.join(game.downloadPath!, game.folderName!) - ); - unrar.extract(); - this.updateGameProgress( - gameId, - { - decompressionProgress: 1, - status: GameStatus.Finished, - }, - downloadStatus - ); - } - if (WindowManager.mainWindow && game) { const progress = this.getGameProgress(game); WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress); @@ -184,31 +82,4 @@ export class Downloader { ); } } - - static getGameProgress(game: Game) { - if (game.status === GameStatus.CheckingFiles) - return game.fileVerificationProgress; - if (game.status === GameStatus.Decompressing) - return game.decompressionProgress; - return game.progress; - } - - private static createTempIcon(encodedIcon: string): Promise { - return new Promise((resolve, reject) => { - const hash = crypto.randomBytes(16).toString("hex"); - const iconPath = path.join(app.getPath("temp"), `${hash}.png`); - - fs.writeFile( - iconPath, - Buffer.from( - encodedIcon.replace("data:image/jpeg;base64,", ""), - "base64" - ), - (err) => { - if (err) reject(err); - resolve(iconPath); - } - ); - }); - } } diff --git a/src/main/services/downloaders/http-downloader.ts b/src/main/services/downloaders/http-downloader.ts deleted file mode 100644 index c94a5755..00000000 --- a/src/main/services/downloaders/http-downloader.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { Game } from "@main/entity"; -import { ElectronDownloadManager } from "electron-dl-manager"; -import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; -import { WindowManager } from "../window-manager"; -import { Downloader } from "./downloader"; -import { GameStatus } from "@globals"; - -function dropExtension(fileName: string) { - return fileName.split(".").slice(0, -1).join("."); -} - -export class HTTPDownloader { - private downloadManager: ElectronDownloadManager; - private downloadId: string | null = null; - - constructor() { - this.downloadManager = new ElectronDownloadManager(); - } - - async download(url: string, destination: string, gameId: number) { - const window = WindowManager.mainWindow; - - this.downloadId = await this.downloadManager.download({ - url, - window: window!, - callbacks: { - onDownloadStarted: async (ev) => { - const updatePayload: QueryDeepPartialEntity = { - status: GameStatus.Downloading, - progress: 0, - bytesDownloaded: 0, - fileSize: ev.item.getTotalBytes(), - rarPath: `${destination}/.rd/${ev.resolvedFilename}`, - folderName: dropExtension(ev.resolvedFilename), - }; - const downloadStatus = { - numPeers: 0, - numSeeds: 0, - downloadSpeed: 0, - timeRemaining: Number.POSITIVE_INFINITY, - }; - await Downloader.updateGameProgress( - gameId, - updatePayload, - downloadStatus - ); - }, - onDownloadCompleted: async (ev) => { - const updatePayload: QueryDeepPartialEntity = { - progress: 1, - decompressionProgress: 0, - bytesDownloaded: ev.item.getReceivedBytes(), - status: GameStatus.Decompressing, - }; - const downloadStatus = { - numPeers: 1, - numSeeds: 1, - downloadSpeed: 0, - timeRemaining: 0, - }; - await Downloader.updateGameProgress( - gameId, - updatePayload, - downloadStatus - ); - }, - onDownloadProgress: async (ev) => { - const updatePayload: QueryDeepPartialEntity = { - progress: ev.percentCompleted / 100, - bytesDownloaded: ev.item.getReceivedBytes(), - }; - const downloadStatus = { - numPeers: 1, - numSeeds: 1, - downloadSpeed: ev.downloadRateBytesPerSecond, - timeRemaining: ev.estimatedTimeRemainingSeconds, - }; - await Downloader.updateGameProgress( - gameId, - updatePayload, - downloadStatus - ); - }, - }, - directory: `${destination}/.rd/`, - }); - } - - pause() { - if (this.downloadId) { - this.downloadManager.pauseDownload(this.downloadId); - } - } - - cancel() { - if (this.downloadId) { - this.downloadManager.cancelDownload(this.downloadId); - } - } - - resume() { - if (this.downloadId) { - this.downloadManager.resumeDownload(this.downloadId); - } - } -} diff --git a/src/main/services/downloaders/http.downloader.ts b/src/main/services/downloaders/http.downloader.ts new file mode 100644 index 00000000..3eafd4da --- /dev/null +++ b/src/main/services/downloaders/http.downloader.ts @@ -0,0 +1,101 @@ +import { Game } from "@main/entity"; +import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; +import path from "node:path"; +import EasyDL from "easydl"; +import { GameStatus } from "@shared"; + +import { Downloader } from "./downloader"; +import { RealDebridClient } from "../real-debrid"; + +export class HTTPDownloader extends Downloader { + private static download: EasyDL; + private static downloadSize = 0; + + private static getEta(bytesDownloaded: number, speed: number) { + const remainingBytes = this.downloadSize - bytesDownloaded; + + if (remainingBytes >= 0 && speed > 0) { + return (remainingBytes / speed) * 1000; + } + + return 1; + } + + static async getDownloadUrl(game: Game) { + const torrents = await RealDebridClient.getAllTorrentsFromUser(); + const hash = RealDebridClient.extractSHA1FromMagnet(game!.repack.magnet); + let torrent = torrents.find((t) => t.hash === hash); + + if (!torrent) { + const magnet = await RealDebridClient.addMagnet(game!.repack.magnet); + + if (magnet && magnet.id) { + await RealDebridClient.selectAllFiles(magnet.id); + torrent = await RealDebridClient.getInfo(magnet.id); + } + } + + if (torrent) { + const { links } = torrent; + const { download } = await RealDebridClient.unrestrictLink(links[0]); + + if (!download) { + throw new Error("Torrent not cached on Real Debrid"); + } + + return download; + } + + throw new Error(); + } + + static async startDownload(game: Game) { + if (this.download) this.download.destroy(); + const download = await this.getDownloadUrl(game); + + this.download = new EasyDL( + download, + path.join(game.downloadPath!, game.repack.title) + ); + + const metadata = await this.download.metadata(); + + this.downloadSize = metadata.size; + + const updatePayload: QueryDeepPartialEntity = { + status: GameStatus.Downloading, + fileSize: metadata.size, + folderName: game.repack.title, + }; + + const downloadStatus = { + timeRemaining: Number.POSITIVE_INFINITY, + }; + + await this.updateGameProgress(game.id, updatePayload, downloadStatus); + + this.download.on("progress", async ({ total }) => { + const updatePayload: QueryDeepPartialEntity = { + status: + total.percentage === 100 + ? GameStatus.Finished + : GameStatus.Downloading, + progress: total.percentage / 100, + bytesDownloaded: total.bytes, + }; + + const downloadStatus = { + downloadSpeed: total.speed, + timeRemaining: this.getEta(total.bytes ?? 0, total.speed ?? 0), + }; + + await this.updateGameProgress(game.id, updatePayload, downloadStatus); + }); + } + + static destroy() { + if (this.download) { + this.download.destroy(); + } + } +} diff --git a/src/main/services/downloaders/index.ts b/src/main/services/downloaders/index.ts new file mode 100644 index 00000000..54026581 --- /dev/null +++ b/src/main/services/downloaders/index.ts @@ -0,0 +1,2 @@ +export * from "./http.downloader"; +export * from "./torrent.downloader"; diff --git a/src/main/services/downloaders/real-debrid.ts b/src/main/services/downloaders/real-debrid.ts deleted file mode 100644 index 7f9ccb48..00000000 --- a/src/main/services/downloaders/real-debrid.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { userPreferencesRepository } from "@main/repository"; -import { - RealDebridAddMagnet, - RealDebridTorrentInfo, - RealDebridUnrestrictLink, -} from "./real-debrid-types"; - -const base = "https://api.real-debrid.com/rest/1.0"; - -export class RealDebridClient { - static async addMagnet(magnet: string) { - const response = await fetch(`${base}/torrents/addMagnet`, { - method: "POST", - headers: { - Authorization: `Bearer ${await this.getApiToken()}`, - }, - body: `magnet=${encodeURIComponent(magnet)}`, - }); - - return response.json() as Promise; - } - - static async getInfo(id: string) { - const response = await fetch(`${base}/torrents/info/${id}`, { - headers: { - Authorization: `Bearer ${await this.getApiToken()}`, - }, - }); - - return response.json() as Promise; - } - - static async selectAllFiles(id: string) { - await fetch(`${base}/torrents/selectFiles/${id}`, { - method: "POST", - headers: { - Authorization: `Bearer ${await this.getApiToken()}`, - }, - body: "files=all", - }); - } - - static async unrestrictLink(link: string) { - const response = await fetch(`${base}/unrestrict/link`, { - method: "POST", - headers: { - Authorization: `Bearer ${await this.getApiToken()}`, - }, - body: `link=${link}`, - }); - - return response.json() as Promise; - } - - static async getAllTorrents() { - const response = await fetch(`${base}/torrents`, { - headers: { - Authorization: `Bearer ${await this.getApiToken()}`, - }, - }); - - return response.json() as Promise; - } - - static getApiToken() { - return userPreferencesRepository - .findOne({ where: { id: 1 } }) - .then((userPreferences) => userPreferences!.realDebridApiToken); - } - - static extractSHA1FromMagnet(magnet: string) { - return magnet.match(/btih:([0-9a-fA-F]*)/)?.[1].toLowerCase(); - } -} diff --git a/src/main/services/downloaders/torrent-client.ts b/src/main/services/downloaders/torrent-client.ts deleted file mode 100644 index 0f26fa51..00000000 --- a/src/main/services/downloaders/torrent-client.ts +++ /dev/null @@ -1,133 +0,0 @@ -import path from "node:path"; -import cp from "node:child_process"; -import fs from "node:fs"; -import * as Sentry from "@sentry/electron/main"; -import { app, dialog } from "electron"; -import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; - -import { Game } from "@main/entity"; -import { Downloader } from "./downloader"; -import { GameStatus } from "@globals"; - -const binaryNameByPlatform: Partial> = { - darwin: "hydra-download-manager", - linux: "hydra-download-manager", - win32: "hydra-download-manager.exe", -}; - -enum TorrentState { - CheckingFiles = 1, - DownloadingMetadata = 2, - Downloading = 3, - Finished = 4, - Seeding = 5, -} - -export interface TorrentUpdate { - gameId: number; - progress: number; - downloadSpeed: number; - timeRemaining: number; - numPeers: number; - numSeeds: number; - status: TorrentState; - folderName: string; - fileSize: number; - bytesDownloaded: number; -} - -export const BITTORRENT_PORT = "5881"; - -export class TorrentClient { - public static startTorrentClient( - writePipePath: string, - readPipePath: string - ) { - const commonArgs = [BITTORRENT_PORT, writePipePath, readPipePath]; - - if (app.isPackaged) { - const binaryName = binaryNameByPlatform[process.platform]!; - const binaryPath = path.join( - process.resourcesPath, - "hydra-download-manager", - binaryName - ); - - if (!fs.existsSync(binaryPath)) { - dialog.showErrorBox( - "Fatal", - "Hydra download manager binary not found. Please check if it has been removed by Windows Defender." - ); - - app.quit(); - } - - cp.spawn(binaryPath, commonArgs, { - stdio: "inherit", - windowsHide: true, - }); - return; - } - - const scriptPath = path.join( - __dirname, - "..", - "..", - "torrent-client", - "main.py" - ); - - cp.spawn("python3", [scriptPath, ...commonArgs], { - stdio: "inherit", - }); - } - - private static getTorrentStateName(state: TorrentState) { - if (state === TorrentState.CheckingFiles) return GameStatus.CheckingFiles; - if (state === TorrentState.Downloading) return GameStatus.Downloading; - if (state === TorrentState.DownloadingMetadata) - return GameStatus.DownloadingMetadata; - if (state === TorrentState.Finished) return GameStatus.Finished; - if (state === TorrentState.Seeding) return GameStatus.Seeding; - return null; - } - - public static async onSocketData(data: Buffer) { - const message = Buffer.from(data).toString("utf-8"); - - try { - const payload = JSON.parse(message) as TorrentUpdate; - - const updatePayload: QueryDeepPartialEntity = { - bytesDownloaded: payload.bytesDownloaded, - status: this.getTorrentStateName(payload.status), - }; - - if (payload.status === TorrentState.CheckingFiles) { - updatePayload.fileVerificationProgress = payload.progress; - } else { - if (payload.folderName) { - updatePayload.folderName = payload.folderName; - updatePayload.fileSize = payload.fileSize; - } - } - - if ( - [TorrentState.Downloading, TorrentState.Seeding].includes( - payload.status - ) - ) { - updatePayload.progress = payload.progress; - } - - Downloader.updateGameProgress(payload.gameId, updatePayload, { - numPeers: payload.numPeers, - numSeeds: payload.numSeeds, - downloadSpeed: payload.downloadSpeed, - timeRemaining: payload.timeRemaining, - }); - } catch (err) { - Sentry.captureException(err); - } - } -} diff --git a/src/main/services/downloaders/torrent.downloader.ts b/src/main/services/downloaders/torrent.downloader.ts new file mode 100644 index 00000000..0590f6bf --- /dev/null +++ b/src/main/services/downloaders/torrent.downloader.ts @@ -0,0 +1,160 @@ +import path from "node:path"; +import cp from "node:child_process"; +import fs from "node:fs"; +import * as Sentry from "@sentry/electron/main"; +import { app, dialog } from "electron"; +import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; + +import { Game } from "@main/entity"; +import { GameStatus } from "@shared"; +import { Downloader } from "./downloader"; +import { readPipe, writePipe } from "../fifo"; + +const binaryNameByPlatform: Partial> = { + darwin: "hydra-download-manager", + linux: "hydra-download-manager", + win32: "hydra-download-manager.exe", +}; + +enum TorrentState { + CheckingFiles = 1, + DownloadingMetadata = 2, + Downloading = 3, + Finished = 4, + Seeding = 5, +} + +export interface TorrentUpdate { + gameId: number; + progress: number; + downloadSpeed: number; + timeRemaining: number; + numPeers: number; + numSeeds: number; + status: TorrentState; + folderName: string; + fileSize: number; + bytesDownloaded: number; +} + +export const BITTORRENT_PORT = "5881"; + +export class TorrentDownloader extends Downloader { + private static messageLength = 1024 * 2; + + public static async attachListener() { + // eslint-disable-next-line no-constant-condition + while (true) { + const buffer = readPipe.socket?.read(this.messageLength); + + if (buffer === null) { + await new Promise((resolve) => setTimeout(resolve, 100)); + continue; + } + + const message = Buffer.from( + buffer.slice(0, buffer.indexOf(0x00)) + ).toString("utf-8"); + + try { + const payload = JSON.parse(message) as TorrentUpdate; + + const updatePayload: QueryDeepPartialEntity = { + bytesDownloaded: payload.bytesDownloaded, + status: this.getTorrentStateName(payload.status), + }; + + if (payload.status === TorrentState.CheckingFiles) { + updatePayload.fileVerificationProgress = payload.progress; + } else { + if (payload.folderName) { + updatePayload.folderName = payload.folderName; + updatePayload.fileSize = payload.fileSize; + } + } + + if ( + [TorrentState.Downloading, TorrentState.Seeding].includes( + payload.status + ) + ) { + updatePayload.progress = payload.progress; + } + + this.updateGameProgress(payload.gameId, updatePayload, { + numPeers: payload.numPeers, + numSeeds: payload.numSeeds, + downloadSpeed: payload.downloadSpeed, + timeRemaining: payload.timeRemaining, + }); + } catch (err) { + Sentry.captureException(err); + } finally { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } + } + + public static startClient() { + return new Promise((resolve) => { + const commonArgs = [ + BITTORRENT_PORT, + writePipe.socketPath, + readPipe.socketPath, + ]; + + if (app.isPackaged) { + const binaryName = binaryNameByPlatform[process.platform]!; + const binaryPath = path.join( + process.resourcesPath, + "hydra-download-manager", + binaryName + ); + + if (!fs.existsSync(binaryPath)) { + dialog.showErrorBox( + "Fatal", + "Hydra download manager binary not found. Please check if it has been removed by Windows Defender." + ); + + app.quit(); + } + + cp.spawn(binaryPath, commonArgs, { + stdio: "inherit", + windowsHide: true, + }); + return; + } + + const scriptPath = path.join( + __dirname, + "..", + "..", + "torrent-client", + "main.py" + ); + + cp.spawn("python3", [scriptPath, ...commonArgs], { + stdio: "inherit", + }); + + Promise.all([writePipe.createPipe(), readPipe.createPipe()]).then( + async () => { + this.attachListener(); + resolve(null); + } + ); + }); + } + + private static getTorrentStateName(state: TorrentState) { + if (state === TorrentState.CheckingFiles) return GameStatus.CheckingFiles; + if (state === TorrentState.Downloading) return GameStatus.Downloading; + if (state === TorrentState.DownloadingMetadata) + return GameStatus.DownloadingMetadata; + if (state === TorrentState.Finished) return GameStatus.Finished; + if (state === TorrentState.Seeding) return GameStatus.Seeding; + return null; + } +} diff --git a/src/main/services/index.ts b/src/main/services/index.ts index 7db479a0..4b13d38d 100644 --- a/src/main/services/index.ts +++ b/src/main/services/index.ts @@ -6,6 +6,7 @@ export * from "./steam-grid"; export * from "./update-resolver"; export * from "./window-manager"; export * from "./fifo"; -export * from "./downloaders/torrent-client"; +export * from "./downloaders"; +export * from "./download-manager"; export * from "./how-long-to-beat"; export * from "./process-watcher"; diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index 1c5383de..16646934 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -16,6 +16,7 @@ export const startProcessWatcher = async () => { const games = await gameRepository.find({ where: { executablePath: Not(IsNull()), + isDeleted: false, }, }); diff --git a/src/main/services/real-debrid.ts b/src/main/services/real-debrid.ts new file mode 100644 index 00000000..b035907c --- /dev/null +++ b/src/main/services/real-debrid.ts @@ -0,0 +1,73 @@ +import type { + RealDebridAddMagnet, + RealDebridTorrentInfo, + RealDebridUnrestrictLink, +} from "./real-debrid.types"; +import axios, { AxiosInstance } from "axios"; + +const base = "https://api.real-debrid.com/rest/1.0"; + +export class RealDebridClient { + private static instance: AxiosInstance; + + static async addMagnet(magnet: string) { + const searchParams = new URLSearchParams(); + searchParams.append("magnet", magnet); + + const response = await this.instance.post( + "/torrents/addMagnet", + searchParams.toString() + ); + + return response.data; + } + + static async getInfo(id: string) { + const response = await this.instance.get( + `/torrents/info/${id}` + ); + return response.data; + } + + static async selectAllFiles(id: string) { + const searchParams = new URLSearchParams(); + searchParams.append("files", "all"); + + await this.instance.post( + `/torrents/selectFiles/${id}`, + searchParams.toString() + ); + } + + static async unrestrictLink(link: string) { + const searchParams = new URLSearchParams(); + searchParams.append("link", link); + + const response = await this.instance.post( + "/unrestrict/link", + searchParams.toString() + ); + + return response.data; + } + + static async getAllTorrentsFromUser() { + const response = + await this.instance.get("/torrents"); + + return response.data; + } + + static extractSHA1FromMagnet(magnet: string) { + return magnet.match(/btih:([0-9a-fA-F]*)/)?.[1].toLowerCase(); + } + + static async authorize(apiToken: string) { + this.instance = axios.create({ + baseURL: base, + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + } +} diff --git a/src/main/services/downloaders/real-debrid-types.ts b/src/main/services/real-debrid.types.ts similarity index 85% rename from src/main/services/downloaders/real-debrid-types.ts rename to src/main/services/real-debrid.types.ts index 6a3e7304..6707641f 100644 --- a/src/main/services/downloaders/real-debrid-types.ts +++ b/src/main/services/real-debrid.types.ts @@ -28,7 +28,7 @@ export interface RealDebridTorrentInfo { host: string; // Host main domain split: number; // Split size of links progress: number; // Possible values: 0 to 100 - status: "downloaded"; // Current status of the torrent: magnet_error, magnet_conversion, waiting_files_selection, queued, downloading, downloaded, error, virus, compressing, uploading, dead + status: string; // Current status of the torrent: magnet_error, magnet_conversion, waiting_files_selection, queued, downloading, downloaded, error, virus, compressing, uploading, dead added: string; // jsonDate files: [ { @@ -44,9 +44,7 @@ export interface RealDebridTorrentInfo { selected: number; // 0 or 1 }, ]; - links: [ - "string", // Host URL - ]; + links: string[]; ended: string; // !! Only present when finished, jsonDate speed: number; // !! Only present in "downloading", "compressing", "uploading" status seeders: number; // !! Only present in "downloading", "magnet_conversion" status diff --git a/src/main/services/unrar.ts b/src/main/services/unrar.ts deleted file mode 100644 index 992f2377..00000000 --- a/src/main/services/unrar.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Extractor, createExtractorFromFile } from "node-unrar-js"; -import fs from "node:fs"; -import path from "node:path"; -import { app } from "electron"; - -const wasmPath = app.isPackaged - ? path.join(process.resourcesPath, "unrar.wasm") - : path.join(__dirname, "..", "..", "unrar.wasm"); - -const wasmBinary = fs.readFileSync(require.resolve(wasmPath)); - -export class Unrar { - private constructor(private extractor: Extractor) {} - - static async fromFilePath(filePath: string, targetFolder: string) { - const extractor = await createExtractorFromFile({ - filepath: filePath, - targetPath: targetFolder, - wasmBinary, - }); - return new Unrar(extractor); - } - - extract() { - const files = this.extractor.extract().files; - for (const file of files) { - console.log("File:", file.fileHeader.name); - } - } -} diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index d5331336..80dadcb7 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -20,6 +20,7 @@ import { setRepackersFriendlyNames, toggleDraggingDisabled, } from "@renderer/features"; +import { GameStatusHelper } from "@shared"; document.body.classList.add(themeClass); @@ -57,7 +58,7 @@ export function App({ children }: AppProps) { useEffect(() => { const unsubscribe = window.electron.onDownloadProgress( (downloadProgress) => { - if (downloadProgress.game.progress === 1) { + if (GameStatusHelper.isReady(downloadProgress.game.status)) { clearDownload(); updateLibrary(); return; diff --git a/src/renderer/src/components/bottom-panel/bottom-panel.tsx b/src/renderer/src/components/bottom-panel/bottom-panel.tsx index 8a9cc090..44d125cd 100644 --- a/src/renderer/src/components/bottom-panel/bottom-panel.tsx +++ b/src/renderer/src/components/bottom-panel/bottom-panel.tsx @@ -7,14 +7,17 @@ import { vars } from "../../theme.css"; import { useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { VERSION_CODENAME } from "@renderer/constants"; -import { GameStatus } from "@globals"; +import { GameStatus, GameStatusHelper } from "@shared"; export function BottomPanel() { const { t } = useTranslation("bottom_panel"); const navigate = useNavigate(); - const { game, progress, downloadSpeed, eta, isDownloading } = useDownload(); + const { game, progress, downloadSpeed, eta } = useDownload(); + + const isGameDownloading = + game && GameStatusHelper.isDownloading(game.status ?? null); const [version, setVersion] = useState(""); @@ -23,7 +26,7 @@ export function BottomPanel() { }, []); const status = useMemo(() => { - if (isDownloading && game) { + if (isGameDownloading) { if (game.status === GameStatus.DownloadingMetadata) return t("downloading_metadata", { title: game.title }); @@ -42,13 +45,13 @@ export function BottomPanel() { } return t("no_downloads_in_progress"); - }, [t, game, progress, eta, isDownloading, downloadSpeed]); + }, [t, isGameDownloading, game, progress, eta, downloadSpeed]); return (