From 4941709296dc42f8c0e84123229358ab802ba877 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Mon, 20 May 2024 02:21:11 +0100 Subject: [PATCH 01/23] feat: adding aria2 --- .github/workflows/build.yml | 11 - .github/workflows/release.yml | 11 - .gitignore | 2 +- electron-builder.yml | 1 - hydra.db | Bin 54386688 -> 54386688 bytes package.json | 1 + src/main/declaration.d.ts | 80 +++++ src/main/entity/game.entity.ts | 8 +- src/main/events/library/delete-game-folder.ts | 3 +- src/main/events/library/get-library.ts | 3 +- src/main/events/library/remove-game.ts | 3 +- .../events/torrenting/cancel-game-download.ts | 48 +-- .../events/torrenting/pause-game-download.ts | 23 +- .../events/torrenting/resume-game-download.ts | 30 +- .../events/torrenting/start-game-download.ts | 29 +- src/main/main.ts | 21 +- src/main/services/download-manager.ts | 254 +++++++++++--- src/main/services/downloaders/downloader.ts | 85 ----- src/main/services/downloaders/index.ts | 2 - .../downloaders/real-debrid.downloader.ts | 115 ------ .../downloaders/torrent.downloader.ts | 156 --------- src/main/services/fifo.ts | 38 -- src/main/services/index.ts | 2 - src/preload/index.ts | 6 +- src/renderer/src/app.tsx | 3 +- .../src/components/backdrop/backdrop.css.ts | 6 + .../src/components/backdrop/backdrop.tsx | 9 +- .../components/bottom-panel/bottom-panel.tsx | 18 +- .../src/components/sidebar/sidebar.tsx | 24 +- src/renderer/src/declaration.d.ts | 4 +- src/renderer/src/features/download-slice.ts | 6 +- src/renderer/src/hooks/use-download.ts | 51 +-- .../src/pages/downloads/downloads.tsx | 3 - .../pages/game-details/description-header.tsx | 16 +- .../src/pages/game-details/gallery-slider.tsx | 55 ++- .../game-details/game-details.context.tsx | 206 +++++++++++ .../src/pages/game-details/game-details.tsx | 328 ++++++------------ .../game-details/hero/hero-panel-actions.tsx | 147 ++++---- .../game-details/hero/hero-panel-playtime.tsx | 22 +- .../pages/game-details/hero/hero-panel.tsx | 110 ++---- .../src/pages/game-details/modals/index.ts | 3 + .../installation-guides/constants.ts | 0 .../dodi-installation-guide.css.ts | 2 +- .../dodi-installation-guide.tsx | 9 +- .../{ => modals}/installation-guides/index.ts | 0 .../online-fix-installation-guide.css.ts | 2 +- .../online-fix-installation-guide.tsx | 0 .../{ => modals}/repacks-modal.css.ts | 2 +- .../{ => modals}/repacks-modal.tsx | 9 +- .../{ => modals}/select-folder-modal.css.tsx | 2 +- .../{ => modals}/select-folder-modal.tsx | 5 +- .../pages/game-details/sidebar/sidebar.tsx | 45 ++- src/shared/index.ts | 19 +- src/types/index.ts | 13 +- torrent-client/fifo.py | 35 -- torrent-client/main.py | 103 ------ torrent-client/setup.py | 20 -- yarn.lock | 15 +- 58 files changed, 895 insertions(+), 1329 deletions(-) create mode 100644 src/main/declaration.d.ts delete mode 100644 src/main/services/downloaders/downloader.ts delete mode 100644 src/main/services/downloaders/index.ts delete mode 100644 src/main/services/downloaders/real-debrid.downloader.ts delete mode 100644 src/main/services/downloaders/torrent.downloader.ts delete mode 100644 src/main/services/fifo.ts create mode 100644 src/renderer/src/pages/game-details/game-details.context.tsx create mode 100644 src/renderer/src/pages/game-details/modals/index.ts rename src/renderer/src/pages/game-details/{ => modals}/installation-guides/constants.ts (100%) rename src/renderer/src/pages/game-details/{ => modals}/installation-guides/dodi-installation-guide.css.ts (94%) rename src/renderer/src/pages/game-details/{ => modals}/installation-guides/dodi-installation-guide.tsx (89%) rename src/renderer/src/pages/game-details/{ => modals}/installation-guides/index.ts (100%) rename src/renderer/src/pages/game-details/{ => modals}/installation-guides/online-fix-installation-guide.css.ts (71%) rename src/renderer/src/pages/game-details/{ => modals}/installation-guides/online-fix-installation-guide.tsx (100%) rename src/renderer/src/pages/game-details/{ => modals}/repacks-modal.css.ts (87%) rename src/renderer/src/pages/game-details/{ => modals}/repacks-modal.tsx (92%) rename src/renderer/src/pages/game-details/{ => modals}/select-folder-modal.css.tsx (86%) rename src/renderer/src/pages/game-details/{ => modals}/select-folder-modal.tsx (99%) delete mode 100644 torrent-client/fifo.py delete mode 100644 torrent-client/main.py delete mode 100644 torrent-client/setup.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 431df932..c9094117 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,17 +22,6 @@ jobs: - name: Install dependencies run: yarn - - name: Install Python - uses: actions/setup-python@v5 - with: - python-version: 3.9 - - - name: Install dependencies - run: pip install -r requirements.txt - - - name: Build with cx_Freeze - run: python torrent-client/setup.py build - - name: Build Linux if: matrix.os == 'ubuntu-latest' run: yarn build:linux diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c51d9ea6..4eee0aad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,17 +24,6 @@ jobs: - name: Install dependencies run: yarn - - name: Install Python - uses: actions/setup-python@v5 - with: - python-version: 3.9 - - - name: Install dependencies - run: pip install -r requirements.txt - - - name: Build with cx_Freeze - run: python torrent-client/setup.py build - - name: Build Linux if: matrix.os == 'ubuntu-latest' run: yarn build:linux diff --git a/.gitignore b/.gitignore index 69af659f..1cd10467 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .vscode node_modules -hydra-download-manager +aria2* fastlist.exe __pycache__ dist diff --git a/electron-builder.yml b/electron-builder.yml index 1dbac52a..b9a4acc6 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -3,7 +3,6 @@ productName: Hydra directories: buildResources: build extraResources: - - hydra-download-manager - hydra.db - fastlist.exe - seeds diff --git a/hydra.db b/hydra.db index 4522e1ae55d98964219de3dcaeabdc3dc830b1bc..0015965fc284f7c4e1f7ffca4f00bdd5642785e1 100644 GIT binary patch delta 4374 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 Xh_D0^Rv^L}MA$IzXGySS{! delta 14627 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 E03wHTpa1{> diff --git a/package.json b/package.json index 6cb748ac..97944d50 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@reduxjs/toolkit": "^2.2.3", "@vanilla-extract/css": "^1.14.2", "@vanilla-extract/recipes": "^0.5.2", + "aria2": "^4.1.2", "auto-launch": "^5.0.6", "axios": "^1.6.8", "better-sqlite3": "^9.5.0", diff --git a/src/main/declaration.d.ts b/src/main/declaration.d.ts new file mode 100644 index 00000000..ac2675a3 --- /dev/null +++ b/src/main/declaration.d.ts @@ -0,0 +1,80 @@ +declare module "aria2" { + export type Aria2Status = + | "active" + | "waiting" + | "paused" + | "error" + | "complete" + | "removed"; + + export interface StatusResponse { + gid: string; + status: Aria2Status; + totalLength: string; + completedLength: string; + uploadLength: string; + bitfield: string; + downloadSpeed: string; + uploadSpeed: string; + infoHash?: string; + numSeeders?: string; + seeder?: boolean; + pieceLength: string; + numPieces: string; + connections: string; + errorCode?: string; + errorMessage?: string; + followedBy?: string[]; + following: string; + belongsTo: string; + dir: string; + files: { + path: string; + length: string; + completedLength: string; + selected: string; + }[]; + bittorrent?: { + announceList: string[][]; + comment: string; + creationDate: string; + mode: "single" | "multi"; + info: { + name: string; + verifiedLength: string; + verifyIntegrityPending: string; + }; + }; + } + + export default class Aria2 { + constructor(options: any); + open: () => Promise; + call( + method: "addUri", + uris: string[], + options: { dir: string } + ): Promise; + call( + method: "tellStatus", + gid: string, + keys?: string[] + ): Promise; + call(method: "pause", gid: string): Promise; + call(method: "forcePause", gid: string): Promise; + call(method: "unpause", gid: string): Promise; + call(method: "remove", gid: string): Promise; + call(method: "forceRemove", gid: string): Promise; + call(method: "pauseAll"): Promise; + call(method: "forcePauseAll"): Promise; + listNotifications: () => [ + "onDownloadStart", + "onDownloadPause", + "onDownloadStop", + "onDownloadComplete", + "onDownloadError", + "onBtDownloadComplete", + ]; + on: (event: string, callback: (params: any) => void) => void; + } +} diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts index 91e19ea6..fd168f51 100644 --- a/src/main/entity/game.entity.ts +++ b/src/main/entity/game.entity.ts @@ -10,7 +10,8 @@ import { import { Repack } from "./repack.entity"; import type { GameShop } from "@types"; -import { Downloader, GameStatus } from "@shared"; +import { Downloader } from "@shared"; +import type { Aria2Status } from "aria2"; @Entity("game") export class Game { @@ -42,7 +43,7 @@ export class Game { shop: GameShop; @Column("text", { nullable: true }) - status: GameStatus | null; + status: Aria2Status | null; @Column("int", { default: Downloader.Torrent }) downloader: Downloader; @@ -53,9 +54,6 @@ export class Game { @Column("float", { default: 0 }) progress: number; - @Column("float", { default: 0 }) - fileVerificationProgress: number; - @Column("int", { default: 0 }) bytesDownloaded: number; diff --git a/src/main/events/library/delete-game-folder.ts b/src/main/events/library/delete-game-folder.ts index 954367a0..adfafefb 100644 --- a/src/main/events/library/delete-game-folder.ts +++ b/src/main/events/library/delete-game-folder.ts @@ -1,7 +1,6 @@ import path from "node:path"; import fs from "node:fs"; -import { GameStatus } from "@shared"; import { gameRepository } from "@main/repository"; import { getDownloadsPath } from "../helpers/get-downloads-path"; @@ -15,7 +14,7 @@ const deleteGameFolder = async ( const game = await gameRepository.findOne({ where: { id: gameId, - status: GameStatus.Cancelled, + status: "removed", isDeleted: false, }, }); diff --git a/src/main/events/library/get-library.ts b/src/main/events/library/get-library.ts index 2374c497..4fd4e254 100644 --- a/src/main/events/library/get-library.ts +++ b/src/main/events/library/get-library.ts @@ -2,7 +2,6 @@ import { gameRepository } from "@main/repository"; import { searchRepacks } from "../helpers/search-games"; import { registerEvent } from "../register-event"; -import { GameStatus } from "@shared"; import { sortBy } from "lodash-es"; const getLibrary = async () => @@ -24,7 +23,7 @@ const getLibrary = async () => ...game, repacks: searchRepacks(game.title), })), - (game) => (game.status !== GameStatus.Cancelled ? 0 : 1) + (game) => (game.status !== "removed" ? 0 : 1) ) ); diff --git a/src/main/events/library/remove-game.ts b/src/main/events/library/remove-game.ts index 57b10b37..54bf66b8 100644 --- a/src/main/events/library/remove-game.ts +++ b/src/main/events/library/remove-game.ts @@ -1,6 +1,5 @@ import { registerEvent } from "../register-event"; import { gameRepository } from "../../repository"; -import { GameStatus } from "@shared"; const removeGame = async ( _event: Electron.IpcMainInvokeEvent, @@ -9,7 +8,7 @@ const removeGame = async ( await gameRepository.update( { id: gameId, - status: GameStatus.Cancelled, + status: "removed", }, { status: null, diff --git a/src/main/events/torrenting/cancel-game-download.ts b/src/main/events/torrenting/cancel-game-download.ts index 18d29fde..3c9a0715 100644 --- a/src/main/events/torrenting/cancel-game-download.ts +++ b/src/main/events/torrenting/cancel-game-download.ts @@ -1,53 +1,25 @@ import { gameRepository } from "@main/repository"; import { registerEvent } from "../register-event"; -import { WindowManager } from "@main/services"; -import { In } from "typeorm"; import { DownloadManager } from "@main/services"; -import { GameStatus } from "@shared"; const cancelGameDownload = async ( _event: Electron.IpcMainInvokeEvent, gameId: number ) => { - const game = await gameRepository.findOne({ - where: { + await DownloadManager.cancelDownload(gameId); + + await gameRepository.update( + { id: gameId, - isDeleted: false, - status: In([ - GameStatus.Downloading, - GameStatus.DownloadingMetadata, - GameStatus.CheckingFiles, - GameStatus.Paused, - GameStatus.Seeding, - GameStatus.Finished, - ]), }, - }); - - if (!game) return; - DownloadManager.cancelDownload(); - - await gameRepository - .update( - { - id: game.id, - }, - { - status: GameStatus.Cancelled, - bytesDownloaded: 0, - progress: 0, - } - ) - .then((result) => { - if ( - game.status !== GameStatus.Paused && - game.status !== GameStatus.Seeding - ) { - if (result.affected) WindowManager.mainWindow?.setProgressBar(-1); - } - }); + { + status: "removed", + bytesDownloaded: 0, + progress: 0, + } + ); }; registerEvent("cancelGameDownload", cancelGameDownload); diff --git a/src/main/events/torrenting/pause-game-download.ts b/src/main/events/torrenting/pause-game-download.ts index ceda70cc..f9ed1102 100644 --- a/src/main/events/torrenting/pause-game-download.ts +++ b/src/main/events/torrenting/pause-game-download.ts @@ -1,30 +1,13 @@ import { registerEvent } from "../register-event"; import { gameRepository } from "../../repository"; -import { In } from "typeorm"; -import { DownloadManager, WindowManager } from "@main/services"; -import { GameStatus } from "@shared"; +import { DownloadManager } from "@main/services"; const pauseGameDownload = async ( _event: Electron.IpcMainInvokeEvent, gameId: number ) => { - DownloadManager.pauseDownload(); - - await gameRepository - .update( - { - id: gameId, - status: In([ - GameStatus.Downloading, - GameStatus.DownloadingMetadata, - GameStatus.CheckingFiles, - ]), - }, - { status: GameStatus.Paused } - ) - .then((result) => { - if (result.affected) WindowManager.mainWindow?.setProgressBar(-1); - }); + await DownloadManager.pauseDownload(); + await gameRepository.update({ id: gameId }, { status: "paused" }); }; registerEvent("pauseGameDownload", pauseGameDownload); diff --git a/src/main/events/torrenting/resume-game-download.ts b/src/main/events/torrenting/resume-game-download.ts index 6982d895..51a81996 100644 --- a/src/main/events/torrenting/resume-game-download.ts +++ b/src/main/events/torrenting/resume-game-download.ts @@ -1,9 +1,7 @@ import { registerEvent } from "../register-event"; import { gameRepository } from "../../repository"; -import { getDownloadsPath } from "../helpers/get-downloads-path"; -import { In } from "typeorm"; + import { DownloadManager } from "@main/services"; -import { GameStatus } from "@shared"; const resumeGameDownload = async ( _event: Electron.IpcMainInvokeEvent, @@ -18,31 +16,13 @@ const resumeGameDownload = async ( }); if (!game) return; - DownloadManager.pauseDownload(); - if (game.status === GameStatus.Paused) { - const downloadsPath = game.downloadPath ?? (await getDownloadsPath()); + if (game.status === "paused") { + await DownloadManager.pauseDownload(); - DownloadManager.resumeDownload(gameId); + await gameRepository.update({ status: "active" }, { status: "paused" }); - await gameRepository.update( - { - status: In([ - GameStatus.Downloading, - GameStatus.DownloadingMetadata, - GameStatus.CheckingFiles, - ]), - }, - { status: GameStatus.Paused } - ); - - await gameRepository.update( - { id: game.id }, - { - status: GameStatus.Downloading, - downloadPath: downloadsPath, - } - ); + await DownloadManager.resumeDownload(gameId); } }; diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index f94d0999..62bce369 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -8,9 +8,8 @@ import { registerEvent } from "../register-event"; import type { GameShop } from "@types"; import { getFileBase64, getSteamAppAsset } from "@main/helpers"; -import { In } from "typeorm"; import { DownloadManager } from "@main/services"; -import { Downloader, GameStatus } from "@shared"; +import { Downloader } from "@shared"; import { stateManager } from "@main/state-manager"; const startGameDownload = async ( @@ -42,19 +41,9 @@ const startGameDownload = async ( }), ]); - if (!repack || game?.status === GameStatus.Downloading) return; - DownloadManager.pauseDownload(); + if (!repack || game?.status === "active") return; - await gameRepository.update( - { - status: In([ - GameStatus.Downloading, - GameStatus.DownloadingMetadata, - GameStatus.CheckingFiles, - ]), - }, - { status: GameStatus.Paused } - ); + await gameRepository.update({ status: "active" }, { status: "paused" }); if (game) { await gameRepository.update( @@ -62,17 +51,17 @@ const startGameDownload = async ( id: game.id, }, { - status: GameStatus.DownloadingMetadata, - downloadPath: downloadPath, + status: "active", + downloadPath, downloader, repack: { id: repackId }, isDeleted: false, } ); - DownloadManager.downloadGame(game.id); + await DownloadManager.startDownload(game.id); - game.status = GameStatus.DownloadingMetadata; + game.status = "active"; return game; } else { @@ -91,7 +80,7 @@ const startGameDownload = async ( objectID, downloader, shop: gameShop, - status: GameStatus.Downloading, + status: "active", downloadPath, repack: { id: repackId }, }) @@ -105,7 +94,7 @@ const startGameDownload = async ( return result; }); - DownloadManager.downloadGame(createdGame.id); + DownloadManager.startDownload(createdGame.id); const { repack: _, ...rest } = createdGame; diff --git a/src/main/main.ts b/src/main/main.ts index e03a6ab8..a9f0ed19 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -13,17 +13,15 @@ import { repackRepository, userPreferencesRepository, } from "./repository"; -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 fs from "node:fs"; import path from "node:path"; import { RealDebridClient } from "./services/real-debrid"; import { orderBy } from "lodash-es"; import { SteamGame } from "@types"; +import { Not } from "typeorm"; startProcessWatcher(); @@ -72,7 +70,7 @@ const checkForNewRepacks = async (userPreferences: UserPreferences | null) => { }; const loadState = async (userPreferences: UserPreferences | null) => { - const repacks = await repackRepository.find({ + const repacks = repackRepository.find({ order: { createdAt: "desc", }, @@ -82,7 +80,7 @@ const loadState = async (userPreferences: UserPreferences | null) => { fs.readFileSync(path.join(seedsPath, "steam-games.json"), "utf-8") ) as SteamGame[]; - stateManager.setValue("repacks", repacks); + stateManager.setValue("repacks", await repacks); stateManager.setValue("steamGames", orderBy(steamGames, ["name"], "asc")); import("./events"); @@ -90,22 +88,19 @@ const loadState = async (userPreferences: UserPreferences | null) => { if (userPreferences?.realDebridApiToken) await RealDebridClient.authorize(userPreferences?.realDebridApiToken); + await DownloadManager.connect(); + const game = await gameRepository.findOne({ where: { - status: In([ - GameStatus.Downloading, - GameStatus.DownloadingMetadata, - GameStatus.CheckingFiles, - ]), + status: "active", + progress: Not(1), isDeleted: false, }, relations: { repack: true }, }); - await TorrentDownloader.startClient(); - if (game) { - DownloadManager.resumeDownload(game.id); + DownloadManager.startDownload(game.id); } }; diff --git a/src/main/services/download-manager.ts b/src/main/services/download-manager.ts index e345835a..94e19835 100644 --- a/src/main/services/download-manager.ts +++ b/src/main/services/download-manager.ts @@ -1,13 +1,156 @@ -import { gameRepository } from "@main/repository"; +import Aria2, { StatusResponse } from "aria2"; +import { spawn } from "node:child_process"; -import type { Game } from "@main/entity"; +import { gameRepository, userPreferencesRepository } from "@main/repository"; + +import path from "node:path"; +import { WindowManager } from "./window-manager"; +import { RealDebridClient } from "./real-debrid"; +import { Notification } from "electron"; +import { t } from "i18next"; import { Downloader } from "@shared"; - -import { writePipe } from "./fifo"; -import { RealDebridDownloader } from "./downloaders"; +import { DownloadProgress } from "@types"; export class DownloadManager { - private static gameDownloading: Game; + private static downloads = new Map(); + + private static gid: string | null = null; + private static gameId: number | null = null; + + private static aria2 = new Aria2({}); + + static async connect() { + const binary = path.join( + __dirname, + "..", + "..", + "aria2-1.37.0-win-64bit-build1", + "aria2c" + ); + + spawn(binary, ["--enable-rpc", "--rpc-listen-all"], { stdio: "inherit" }); + + await this.aria2.open(); + this.attachListener(); + } + + private static getETA(status: StatusResponse) { + const remainingBytes = + Number(status.totalLength) - Number(status.completedLength); + const speed = Number(status.downloadSpeed); + + if (remainingBytes >= 0 && speed > 0) { + return (remainingBytes / speed) * 1000; + } + + return -1; + } + + static async publishNotification() { + const userPreferences = await userPreferencesRepository.findOne({ + where: { id: 1 }, + }); + + if (userPreferences?.downloadNotificationsEnabled && this.gameId) { + const game = await this.getGame(this.gameId); + + new Notification({ + title: t("download_complete", { + ns: "notifications", + lng: userPreferences.language, + }), + body: t("game_ready_to_install", { + ns: "notifications", + lng: userPreferences.language, + title: game?.title, + }), + }).show(); + } + } + + private static getFolderName(status: StatusResponse) { + if (status.bittorrent?.info) return status.bittorrent.info.name; + return ""; + } + + private static async attachListener() { + while (true) { + try { + if (!this.gid || !this.gameId) { + continue; + } + + const status = await this.aria2.call("tellStatus", this.gid); + + const downloadingMetadata = + status.bittorrent && !status.bittorrent?.info; + + if (status.followedBy?.length) { + this.gid = status.followedBy[0]; + this.downloads.set(this.gameId, this.gid); + continue; + } + + const progress = + Number(status.completedLength) / Number(status.totalLength); + + await gameRepository.update( + { id: this.gameId }, + { + progress: + isNaN(progress) || downloadingMetadata ? undefined : progress, + bytesDownloaded: Number(status.completedLength), + fileSize: Number(status.totalLength), + status: status.status, + folderName: this.getFolderName(status), + } + ); + + const game = await gameRepository.findOne({ + where: { id: this.gameId, isDeleted: false }, + relations: { repack: true }, + }); + + if (progress === 1 && game && !downloadingMetadata) { + await this.publishNotification(); + /* + Only cancel bittorrent downloads to stop seeding + */ + if (status.bittorrent) { + await this.cancelDownload(game.id); + } else { + this.clearCurrentDownload(); + } + } + + if (WindowManager.mainWindow && game) { + WindowManager.mainWindow.setProgressBar( + progress === 1 || downloadingMetadata ? -1 : progress, + { mode: downloadingMetadata ? "indeterminate" : "normal" } + ); + + const payload = { + progress, + bytesDownloaded: Number(status.completedLength), + fileSize: Number(status.totalLength), + numPeers: Number(status.connections), + numSeeds: Number(status.numSeeders ?? 0), + downloadSpeed: Number(status.downloadSpeed), + timeRemaining: this.getETA(status), + downloadingMetadata: !!downloadingMetadata, + game, + } as DownloadProgress; + + WindowManager.mainWindow.webContents.send( + "on-download-progress", + JSON.parse(JSON.stringify(payload)) + ); + } + } finally { + await new Promise((resolve) => setTimeout(resolve, 500)); + } + } + } static async getGame(gameId: number) { return gameRepository.findOne({ @@ -18,59 +161,80 @@ export class DownloadManager { }); } - static async cancelDownload() { - if ( - this.gameDownloading && - this.gameDownloading.downloader === Downloader.Torrent - ) { - writePipe.write({ action: "cancel" }); - } else { - RealDebridDownloader.destroy(); + private static clearCurrentDownload() { + if (this.gameId) { + this.downloads.delete(this.gameId); + this.gid = null; + this.gameId = null; + } + } + + static async cancelDownload(gameId: number) { + const gid = this.downloads.get(gameId); + + if (gid) { + await this.aria2.call("remove", gid); + + if (this.gid === gid) { + this.clearCurrentDownload(); + + WindowManager.mainWindow?.setProgressBar(-1); + } else { + this.downloads.delete(gameId); + } } } static async pauseDownload() { - if ( - this.gameDownloading && - this.gameDownloading.downloader === Downloader.Torrent - ) { - writePipe.write({ action: "pause" }); - } else { - RealDebridDownloader.destroy(); + if (this.gid) { + await this.aria2.call("forcePause", this.gid); + this.gid = null; + this.gameId = null; + + WindowManager.mainWindow?.setProgressBar(-1); } } static async resumeDownload(gameId: number) { - const game = await this.getGame(gameId); + await this.aria2.call("forcePauseAll"); - if (game!.downloader === Downloader.Torrent) { - writePipe.write({ - action: "start", - game_id: game!.id, - magnet: game!.repack.magnet, - save_path: game!.downloadPath, - }); + if (this.downloads.has(gameId)) { + const gid = this.downloads.get(gameId)!; + await this.aria2.call("unpause", gid); + + this.gid = gid; + this.gameId = gameId; } else { - RealDebridDownloader.startDownload(game!); + return this.startDownload(gameId); } - - this.gameDownloading = game!; } - static async downloadGame(gameId: number) { - const game = await this.getGame(gameId); + static async startDownload(gameId: number) { + await this.aria2.call("forcePauseAll"); - if (game!.downloader === Downloader.Torrent) { - writePipe.write({ - action: "start", - game_id: game!.id, - magnet: game!.repack.magnet, - save_path: game!.downloadPath, - }); - } else { - RealDebridDownloader.startDownload(game!); + const game = await this.getGame(gameId)!; + + if (game) { + const options = { + dir: game.downloadPath!, + }; + + if (game.downloader === Downloader.RealDebrid) { + const downloadUrl = decodeURIComponent( + await RealDebridClient.getDownloadUrl(game) + ); + + this.gid = await this.aria2.call("addUri", [downloadUrl], options); + } else { + this.gid = await this.aria2.call( + "addUri", + [game.repack.magnet], + options + ); + } + + this.gameId = gameId; + this.downloads.set(gameId, this.gid); } - - this.gameDownloading = game!; } } diff --git a/src/main/services/downloaders/downloader.ts b/src/main/services/downloaders/downloader.ts deleted file mode 100644 index 14440676..00000000 --- a/src/main/services/downloaders/downloader.ts +++ /dev/null @@ -1,85 +0,0 @@ -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 type { TorrentUpdate } from "./torrent.downloader"; - -import { GameStatus } from "@shared"; -import { gameRepository, userPreferencesRepository } from "@main/repository"; - -interface DownloadStatus { - numPeers?: number; - numSeeds?: number; - downloadSpeed?: number; - timeRemaining?: number; -} - -export class Downloader { - static getGameProgress(game: Game) { - if (game.status === GameStatus.CheckingFiles) - return game.fileVerificationProgress; - - return game.progress; - } - - static async updateGameProgress( - gameId: number, - gameUpdate: QueryDeepPartialEntity, - downloadStatus: DownloadStatus - ) { - await gameRepository.update({ id: gameId }, gameUpdate); - - const game = await gameRepository.findOne({ - where: { id: gameId, isDeleted: false }, - relations: { repack: true }, - }); - - if (game?.progress === 1) { - const userPreferences = await userPreferencesRepository.findOne({ - where: { id: 1 }, - }); - - if (userPreferences?.downloadNotificationsEnabled) { - new Notification({ - title: t("download_complete", { - ns: "notifications", - lng: userPreferences.language, - }), - body: t("game_ready_to_install", { - ns: "notifications", - lng: userPreferences.language, - title: game?.title, - }), - }).show(); - } - } - - if (WindowManager.mainWindow && game) { - const progress = this.getGameProgress(game); - WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress); - - WindowManager.mainWindow.webContents.send( - "on-download-progress", - JSON.parse( - JSON.stringify({ - ...({ - progress: gameUpdate.progress, - bytesDownloaded: gameUpdate.bytesDownloaded, - fileSize: gameUpdate.fileSize, - gameId, - numPeers: downloadStatus.numPeers, - numSeeds: downloadStatus.numSeeds, - downloadSpeed: downloadStatus.downloadSpeed, - timeRemaining: downloadStatus.timeRemaining, - } as TorrentUpdate), - game, - }) - ) - ); - } - } -} diff --git a/src/main/services/downloaders/index.ts b/src/main/services/downloaders/index.ts deleted file mode 100644 index cd742107..00000000 --- a/src/main/services/downloaders/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./real-debrid.downloader"; -export * from "./torrent.downloader"; diff --git a/src/main/services/downloaders/real-debrid.downloader.ts b/src/main/services/downloaders/real-debrid.downloader.ts deleted file mode 100644 index 8a44f934..00000000 --- a/src/main/services/downloaders/real-debrid.downloader.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Game } from "@main/entity"; -import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; -import path from "node:path"; -import fs from "node:fs"; -import EasyDL from "easydl"; -import { GameStatus } from "@shared"; -// import { fullArchive } from "node-7z-archive"; - -import { Downloader } from "./downloader"; -import { RealDebridClient } from "../real-debrid"; - -export class RealDebridDownloader 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; - } - - private static createFolderIfNotExists(path: string) { - if (!fs.existsSync(path)) { - fs.mkdirSync(path); - } - } - - // private static async startDecompression( - // rarFile: string, - // dest: string, - // game: Game - // ) { - // await fullArchive(rarFile, dest); - - // const updatePayload: QueryDeepPartialEntity = { - // status: GameStatus.Finished, - // }; - - // await this.updateGameProgress(game.id, updatePayload, {}); - // } - - static destroy() { - if (this.download) { - this.download.destroy(); - } - } - - static async startDownload(game: Game) { - if (this.download) this.download.destroy(); - const downloadUrl = decodeURIComponent( - await RealDebridClient.getDownloadUrl(game) - ); - - const filename = path.basename(downloadUrl); - const folderName = path.basename(filename, path.extname(filename)); - - const downloadPath = path.join(game.downloadPath!, folderName); - this.createFolderIfNotExists(downloadPath); - - this.download = new EasyDL(downloadUrl, path.join(downloadPath, filename)); - - const metadata = await this.download.metadata(); - - this.downloadSize = metadata.size; - - const updatePayload: QueryDeepPartialEntity = { - status: GameStatus.Downloading, - fileSize: metadata.size, - folderName, - }; - - const downloadStatus = { - timeRemaining: Number.POSITIVE_INFINITY, - }; - - await this.updateGameProgress(game.id, updatePayload, downloadStatus); - - this.download.on("progress", async ({ total }) => { - const updatePayload: QueryDeepPartialEntity = { - status: GameStatus.Downloading, - progress: Math.min(0.99, 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); - }); - - this.download.on("end", async () => { - const updatePayload: QueryDeepPartialEntity = { - status: GameStatus.Finished, - progress: 1, - }; - - await this.updateGameProgress(game.id, updatePayload, { - timeRemaining: 0, - }); - - /* This has to be improved */ - // this.startDecompression( - // path.join(downloadPath, filename), - // downloadPath, - // game - // ); - }); - } -} diff --git a/src/main/services/downloaders/torrent.downloader.ts b/src/main/services/downloaders/torrent.downloader.ts deleted file mode 100644 index d5e039a8..00000000 --- a/src/main/services/downloaders/torrent.downloader.ts +++ /dev/null @@ -1,156 +0,0 @@ -import path from "node:path"; -import cp from "node:child_process"; -import fs from "node:fs"; -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, - }); - } 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, - }); - } else { - 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/fifo.ts b/src/main/services/fifo.ts deleted file mode 100644 index 866232cc..00000000 --- a/src/main/services/fifo.ts +++ /dev/null @@ -1,38 +0,0 @@ -import path from "node:path"; -import net from "node:net"; -import crypto from "node:crypto"; -import os from "node:os"; - -export class FIFO { - public socket: null | net.Socket = null; - public socketPath = this.generateSocketFilename(); - - private generateSocketFilename() { - const hash = crypto.randomBytes(16).toString("hex"); - - if (process.platform === "win32") { - return "\\\\.\\pipe\\" + hash; - } - - return path.join(os.tmpdir(), hash); - } - - public write(data: any) { - if (!this.socket) return; - this.socket.write(Buffer.from(JSON.stringify(data))); - } - - public createPipe() { - return new Promise((resolve) => { - const server = net.createServer((socket) => { - this.socket = socket; - resolve(null); - }); - - server.listen(this.socketPath); - }); - } -} - -export const writePipe = new FIFO(); -export const readPipe = new FIFO(); diff --git a/src/main/services/index.ts b/src/main/services/index.ts index 4b13d38d..4808736d 100644 --- a/src/main/services/index.ts +++ b/src/main/services/index.ts @@ -5,8 +5,6 @@ export * from "./steam-250"; export * from "./steam-grid"; export * from "./update-resolver"; export * from "./window-manager"; -export * from "./fifo"; -export * from "./downloaders"; export * from "./download-manager"; export * from "./how-long-to-beat"; export * from "./process-watcher"; diff --git a/src/preload/index.ts b/src/preload/index.ts index 6a209787..0e397a4a 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -5,7 +5,7 @@ import { contextBridge, ipcRenderer } from "electron"; import type { CatalogueCategory, GameShop, - TorrentProgress, + DownloadProgress, UserPreferences, } from "@types"; @@ -32,10 +32,10 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("pauseGameDownload", gameId), resumeGameDownload: (gameId: number) => ipcRenderer.invoke("resumeGameDownload", gameId), - onDownloadProgress: (cb: (value: TorrentProgress) => void) => { + onDownloadProgress: (cb: (value: DownloadProgress) => void) => { const listener = ( _event: Electron.IpcRendererEvent, - value: TorrentProgress + value: DownloadProgress ) => cb(value); ipcRenderer.on("on-download-progress", listener); return () => ipcRenderer.removeListener("on-download-progress", listener); diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index da95f292..adb2a613 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -19,7 +19,6 @@ import { setUserPreferences, toggleDraggingDisabled, } from "@renderer/features"; -import { GameStatusHelper } from "@shared"; document.body.classList.add(themeClass); @@ -54,7 +53,7 @@ export function App({ children }: AppProps) { useEffect(() => { const unsubscribe = window.electron.onDownloadProgress( (downloadProgress) => { - if (GameStatusHelper.isReady(downloadProgress.game.status)) { + if (downloadProgress.game.progress === 1) { clearDownload(); updateLibrary(); return; diff --git a/src/renderer/src/components/backdrop/backdrop.css.ts b/src/renderer/src/components/backdrop/backdrop.css.ts index 0a7b61bb..3b8cc4e2 100644 --- a/src/renderer/src/components/backdrop/backdrop.css.ts +++ b/src/renderer/src/components/backdrop/backdrop.css.ts @@ -43,5 +43,11 @@ export const backdrop = recipe({ backgroundColor: "rgba(0, 0, 0, 0)", }, }, + windows: { + true: { + // SPACING_UNIT * 3 + title bar spacing + paddingTop: `${SPACING_UNIT * 3 + 35}px`, + }, + }, }, }); diff --git a/src/renderer/src/components/backdrop/backdrop.tsx b/src/renderer/src/components/backdrop/backdrop.tsx index 5852d59d..f498e664 100644 --- a/src/renderer/src/components/backdrop/backdrop.tsx +++ b/src/renderer/src/components/backdrop/backdrop.tsx @@ -7,6 +7,13 @@ export interface BackdropProps { export function Backdrop({ isClosing = false, children }: BackdropProps) { return ( -
{children}
+
+ {children} +
); } diff --git a/src/renderer/src/components/bottom-panel/bottom-panel.tsx b/src/renderer/src/components/bottom-panel/bottom-panel.tsx index 44d125cd..310f31b4 100644 --- a/src/renderer/src/components/bottom-panel/bottom-panel.tsx +++ b/src/renderer/src/components/bottom-panel/bottom-panel.tsx @@ -7,17 +7,16 @@ 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, GameStatusHelper } from "@shared"; export function BottomPanel() { const { t } = useTranslation("bottom_panel"); const navigate = useNavigate(); - const { game, progress, downloadSpeed, eta } = useDownload(); + const { lastPacket, progress, downloadSpeed, eta } = useDownload(); const isGameDownloading = - game && GameStatusHelper.isDownloading(game.status ?? null); + lastPacket?.game && lastPacket?.game.status === "active"; const [version, setVersion] = useState(""); @@ -27,17 +26,8 @@ export function BottomPanel() { const status = useMemo(() => { if (isGameDownloading) { - if (game.status === GameStatus.DownloadingMetadata) - return t("downloading_metadata", { title: game.title }); - - if (game.status === GameStatus.CheckingFiles) - return t("checking_files", { - title: game.title, - percentage: progress, - }); - return t("downloading", { - title: game?.title, + title: lastPacket?.game.title, percentage: progress, eta, speed: downloadSpeed, @@ -45,7 +35,7 @@ export function BottomPanel() { } return t("no_downloads_in_progress"); - }, [t, isGameDownloading, game, progress, eta, downloadSpeed]); + }, [t, isGameDownloading, lastPacket?.game, progress, eta, downloadSpeed]); return (