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/34] 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 zcmZ|SbyQS+9>?)1ks3u5P*DLz0RsseR1~oly8~40e(ZQ`Q8B?T>~8D=yRf^vu>;%v zyxrZiyMOG=dB4uRb7$_IGiS~{zwhvr^c=(0RQD|L21D>2gTbby!C<;=Fc^wYD;(?> zZ}9IImqB|5C-A9k-Dz!aztgU#U4ZQb%R);ZOLeQK#nIxpdE98TDKt1C+3cQ>ZuYSF zMEC|IZtw^-7f9-D8Dwj*wlWk;cH8A*wjd)GAXmNs0_+RWmLAxPT4C5<*2MGlgg}|R2Jo|vZ`##MY$?Bm0jgf zIhDK0rE;r0%0qc7FO^s2Q{F1SDxiFnukup`RUuVa6;b}Gs0vVlDo6#ZVyd_*p-QS! zs6VlUoYnE+FOdfkbtHDq(yK>tRkP=hQ z!9M7z!4ULBJ&ql4Dn_+e9aKlvNp)6TR9Dqabyq!9Pt{BHR)48Js;}y&`l|tIpcE&O;%IXR5eY-s{}P&%}_JdEHzur zQFGNiHD4`I3ss_8q!z0sYN=YLma7$NrCOy{t2Jt^TBp{l4Qiv>q&BN9DoJfs+thZo zL+w<%)NZv$?N$5KewD0J)B$x+9a4wY5p`4@Q^(Z_byA&Dsp_;kqt2>x>b$z3E~-oF zvbv(Ks%z@Hx}k2WTk5vDqwcDE>b`oQ9;!#`v3jDOs%PrCdZAvbH1$foR&Uf>^-jH4 zAJj+nNqtu7>WliSzNzo(hx)00so!w}P8p{tFo78?kO6EUBiMo+*nA zhX&9P8bM=d0!^VAG=~<@5?VoPh=OQn18t!l#6WxK03D$dbcQa_6}mxp=m9;U7xad| zpbzwge$XEVz(5!TgJB2^g}-4K42Kag5=Oyj7z1M=7RJGNm;iAw5hlT8m;zH_8pJ~a zOotgT6K26|m;-ZR9?XXYun-bq5iEu!uoRZTa##T?VHK=~HLw=e!Ft#L8(|Y{hAof; zTVWe)haIpJcEN7g1AAc~?1yAXfdg<54#8nK0!QH(9ETHd5>7!XoQ5-S7S6$WxBwU7 z5?qEWa22k>b+`dH;TGJ6J8&27!F_lD58)9!h9~e8p22f?0WTp9Ucqa418?CSyoV3) z5kA3ZNQW=*6~4iD_yIrR7yLHr_Gi+D5lmnP3uFKr$OyJz2ln6qj$nmMkQtmH3phho z$ObOp3T}`cazIXShg^^w@_+|;f*0h4eBcfFp#b=RFZe-0C3%$2!KEcf?y~H z#i0b0gi=r%%0LK&LRlyWmF(zW=xuDDq>7rnI2*;o46rQsJTL7fQEu0|1~Pu_(bTk z(TSV%y0=ZxQ^z!m>z^GD*T4?Z-C}~CIHsA?++6hc3+@x$#y9BCSmEXQ=b%re*dSe6 z2Pa49VAmKvK|bNrZjMQ8l`GWz_Wyi^qzSe$7Gs_N3?(^DzQO+-Sxi0=nnmWC7KS;S zEK@8|eN3V+^n?B|00zP!7z{&TDEtk>U^t9`kuVBI!x$I~u`mwC!vu(fi7*K!!xWeb z(;yxaU^>iznJ^1x!yK3k^I$$KfQ669@q=}U_T^73LJoga0m{=5jYCR;5eLslW+=B;WV6qvv3a1 z!v(kqm*6s7fva#0uEPzu3Af-j+=07r5AMSQcnFW+F+72%@C=^A3wQ}>@CshT8+Z%v z;5~eRkMIdTLppqcuka1N!w>igzu>ni&JtzTh7n9)1`A{W8^{Q@U%e};0kV#9dbZUaEDxw8}fh$c!C$?g?!)*`Jn*#fG_w#K_~=;p$PaxQ3!xQ e2!dcJ2F0NSl!Q`H8p=Qjgqo8rQDx0PhW!KGt1*xO delta 14627 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 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 (