From c6e4ba4789b79cf6ceb87739405c40ab8cb062d9 Mon Sep 17 00:00:00 2001 From: lilezek Date: Mon, 29 Apr 2024 20:44:14 +0200 Subject: [PATCH 01/30] feat: add the dependencies for real debrid --- package.json | 4 +++ yarn.lock | 69 +++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index b8396713..64021f00 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@reduxjs/toolkit": "^2.2.2", "@sentry/electron": "^4.22.0", "@sentry/react": "^7.110.1", + "@types/node-fetch": "^2.6.11", "@vanilla-extract/css": "^1.14.1", "@vanilla-extract/recipes": "^0.5.2", "axios": "^1.6.8", @@ -80,6 +81,7 @@ "color": "^4.2.3", "color.js": "^1.2.0", "date-fns": "^3.5.0", + "electron-dl-manager": "^3.0.0", "electron-squirrel-startup": "^1.0.0", "flexsearch": "^0.7.43", "got-scraping": "^4.0.5", @@ -88,6 +90,8 @@ "jsdom": "^24.0.0", "lodash": "^4.17.21", "lottie-react": "^2.4.0", + "node-fetch": "^2.6.1", + "node-unrar-js": "^2.0.2", "parse-torrent": "9.1.5", "ps-list": "^8.1.1", "react": "^18.2.0", diff --git a/yarn.lock b/yarn.lock index 78c336e0..b4bac2e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2877,6 +2877,14 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== +"@types/node-fetch@^2.6.11": + version "2.6.11" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24" + integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g== + dependencies: + "@types/node" "*" + form-data "^4.0.0" + "@types/node-forge@^1.3.0": version "1.3.11" resolved "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz" @@ -4974,6 +4982,14 @@ ee-first@1.1.1: resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== +electron-dl-manager@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/electron-dl-manager/-/electron-dl-manager-3.0.0.tgz#1b6ef6ee59f45733a5f13e8e916cb8189a21f8c8" + integrity sha512-DRyic9aY/6mSg7MvokrFWWY+NLYOnZcKGarujcBE4snobWND0hvV79s9b91kbo7+PLlANroK+jc/NDVliMSfbQ== + dependencies: + ext-name "^5.0.0" + unused-filename "^3.0.1" + electron-installer-common@^0.10.2: version "0.10.3" resolved "https://registry.yarnpkg.com/electron-installer-common/-/electron-installer-common-0.10.3.tgz#40f9db644ca60eb28673d545b67ee0113aef4444" @@ -5768,6 +5784,21 @@ express@^4.17.1, express@^4.17.3: utils-merge "1.0.1" vary "~1.1.2" +ext-list@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37" + integrity sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA== + dependencies: + mime-db "^1.28.0" + +ext-name@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ext-name/-/ext-name-5.0.0.tgz#70781981d183ee15d13993c8822045c506c8f0a6" + integrity sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ== + dependencies: + ext-list "^2.0.0" + sort-keys-length "^1.0.0" + ext@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz" @@ -7041,6 +7072,11 @@ is-path-inside@^3.0.3: resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-plain-obj@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== + is-plain-obj@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz" @@ -7756,7 +7792,7 @@ micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" -mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2", mime-db@^1.28.0: version "1.52.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== @@ -8064,9 +8100,9 @@ node-dir@^0.1.17: dependencies: minimatch "^3.0.2" -node-fetch@^2.6.7: +node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.7.0" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== dependencies: whatwg-url "^5.0.0" @@ -8121,6 +8157,11 @@ node-releases@^2.0.14: resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== +node-unrar-js@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/node-unrar-js/-/node-unrar-js-2.0.2.tgz#03ef602052497263b9aed8ff1e7afb315024f9ec" + integrity sha512-hLNmoJzqaKJnod8yiTVGe9hnlNRHotUi0CreSv/8HtfRi/3JnRC8DvsmKfeGGguRjTEulhZK6zXX5PXoVuDZ2w== + nopt@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz" @@ -9724,6 +9765,20 @@ socks@^2.6.2: ip-address "^9.0.5" smart-buffer "^4.2.0" +sort-keys-length@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188" + integrity sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw== + dependencies: + sort-keys "^1.0.0" + +sort-keys@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" + integrity sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg== + dependencies: + is-plain-obj "^1.0.0" + source-map-js@^1.0.1, source-map-js@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz" @@ -10536,6 +10591,14 @@ unplugin@1.0.1: webpack-sources "^3.2.3" webpack-virtual-modules "^0.5.0" +unused-filename@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/unused-filename/-/unused-filename-3.0.1.tgz#41b0600f8909e39cbdbbcf2467591bd3dd83fa7b" + integrity sha512-UbMRaEaT+/3mGh40GBRnF2++1VqFG1w0Kjzd5q/uQjagKn5pkCS8goJTgYDpQ6e0tB2GywamMJy1BzbSrMcIWw== + dependencies: + escape-string-regexp "^4.0.0" + path-exists "^4.0.0" + update-browserslist-db@^1.0.13: version "1.0.13" resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz" From 6fa4c178a77b8e2b8127264c05e51fd4dac87033 Mon Sep 17 00:00:00 2001 From: lilezek Date: Mon, 29 Apr 2024 20:50:10 +0200 Subject: [PATCH 02/30] refactor: moved the game status to a global file (accessible to the render part) and used it in game entity --- src/globals.ts | 10 ++++++++++ src/main/constants.ts | 9 --------- src/main/entity/game.entity.ts | 3 ++- src/main/events/library/get-library.ts | 2 +- src/main/events/torrenting/cancel-game-download.ts | 2 +- src/main/events/torrenting/delete-game-folder.ts | 2 +- src/main/events/torrenting/pause-game-download.ts | 2 +- .../events/torrenting/remove-game-from-download.ts | 2 +- src/main/events/torrenting/resume-game-download.ts | 2 +- src/main/events/torrenting/start-game-download.ts | 2 +- src/main/index.ts | 3 ++- src/types/index.ts | 4 +++- tsconfig.json | 3 ++- 13 files changed, 26 insertions(+), 20 deletions(-) create mode 100644 src/globals.ts diff --git a/src/globals.ts b/src/globals.ts new file mode 100644 index 00000000..e4675aa7 --- /dev/null +++ b/src/globals.ts @@ -0,0 +1,10 @@ +export enum GameStatus { + Seeding = "seeding", + Downloading = "downloading", + Paused = "paused", + CheckingFiles = "checking_files", + DownloadingMetadata = "downloading_metadata", + Cancelled = "cancelled", + Finished = "finished", + Decompressing = "decompressing", +} \ No newline at end of file diff --git a/src/main/constants.ts b/src/main/constants.ts index 43d9d44e..1a87c27b 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -34,15 +34,6 @@ export const months = [ "Dec", ]; -export enum GameStatus { - Seeding = "seeding", - Downloading = "downloading", - Paused = "paused", - CheckingFiles = "checking_files", - DownloadingMetadata = "downloading_metadata", - Cancelled = "cancelled", -} - export const defaultDownloadsPath = path.join(os.homedir(), "downloads"); export const databasePath = path.join( diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts index 811ecf74..8798a15d 100644 --- a/src/main/entity/game.entity.ts +++ b/src/main/entity/game.entity.ts @@ -9,6 +9,7 @@ import { } from "typeorm"; import type { GameShop } from "@types"; import { Repack } from "./repack.entity"; +import { GameStatus } from "@globals"; @Entity("game") export class Game { @@ -40,7 +41,7 @@ export class Game { shop: GameShop; @Column("text", { nullable: true }) - status: string; + status: GameStatus | ""; @Column("float", { default: 0 }) progress: number; diff --git a/src/main/events/library/get-library.ts b/src/main/events/library/get-library.ts index c86d1902..047d848b 100644 --- a/src/main/events/library/get-library.ts +++ b/src/main/events/library/get-library.ts @@ -1,9 +1,9 @@ import { gameRepository } from "@main/repository"; -import { GameStatus } from "@main/constants"; import { searchRepacks } from "../helpers/search-games"; import { registerEvent } from "../register-event"; import sortBy from "lodash/sortBy"; +import { GameStatus } from "@globals"; const getLibrary = async (_event: Electron.IpcMainInvokeEvent) => gameRepository diff --git a/src/main/events/torrenting/cancel-game-download.ts b/src/main/events/torrenting/cancel-game-download.ts index a1a2e6b7..bcd4fdab 100644 --- a/src/main/events/torrenting/cancel-game-download.ts +++ b/src/main/events/torrenting/cancel-game-download.ts @@ -1,10 +1,10 @@ -import { GameStatus } from "@main/constants"; import { gameRepository } from "@main/repository"; import { registerEvent } from "../register-event"; import { WindowManager, writePipe } from "@main/services"; import { In } from "typeorm"; +import { GameStatus } from "@globals"; const cancelGameDownload = async ( _event: Electron.IpcMainInvokeEvent, diff --git a/src/main/events/torrenting/delete-game-folder.ts b/src/main/events/torrenting/delete-game-folder.ts index c8821415..e913a23a 100644 --- a/src/main/events/torrenting/delete-game-folder.ts +++ b/src/main/events/torrenting/delete-game-folder.ts @@ -1,7 +1,7 @@ import path from "node:path"; import fs from "node:fs"; -import { GameStatus } from "@main/constants"; +import { GameStatus } from "@globals"; import { gameRepository } from "@main/repository"; import { getDownloadsPath } from "../helpers/get-downloads-path"; diff --git a/src/main/events/torrenting/pause-game-download.ts b/src/main/events/torrenting/pause-game-download.ts index d89f2f72..e1da552a 100644 --- a/src/main/events/torrenting/pause-game-download.ts +++ b/src/main/events/torrenting/pause-game-download.ts @@ -1,9 +1,9 @@ import { WindowManager, writePipe } from "@main/services"; import { registerEvent } from "../register-event"; -import { GameStatus } from "../../constants"; import { gameRepository } from "../../repository"; import { In } from "typeorm"; +import { GameStatus } from "@globals"; const pauseGameDownload = async ( _event: Electron.IpcMainInvokeEvent, diff --git a/src/main/events/torrenting/remove-game-from-download.ts b/src/main/events/torrenting/remove-game-from-download.ts index 47c1ebe6..2ca608ef 100644 --- a/src/main/events/torrenting/remove-game-from-download.ts +++ b/src/main/events/torrenting/remove-game-from-download.ts @@ -1,4 +1,4 @@ -import { GameStatus } from "@main/constants"; +import { GameStatus } from "@globals"; import { gameRepository } from "@main/repository"; import { registerEvent } from "../register-event"; diff --git a/src/main/events/torrenting/resume-game-download.ts b/src/main/events/torrenting/resume-game-download.ts index c1e2e798..9d96ab18 100644 --- a/src/main/events/torrenting/resume-game-download.ts +++ b/src/main/events/torrenting/resume-game-download.ts @@ -1,9 +1,9 @@ import { registerEvent } from "../register-event"; -import { GameStatus } from "../../constants"; import { gameRepository } from "../../repository"; import { getDownloadsPath } from "../helpers/get-downloads-path"; import { In } from "typeorm"; import { writePipe } from "@main/services"; +import { GameStatus } from "@globals"; const resumeGameDownload = async ( _event: Electron.IpcMainInvokeEvent, diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index 1bdb1a6b..a5853208 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -1,12 +1,12 @@ import { getSteamGameIconUrl, writePipe } from "@main/services"; import { gameRepository, repackRepository } from "@main/repository"; -import { GameStatus } from "@main/constants"; import { registerEvent } from "../register-event"; import type { GameShop } from "@types"; import { getImageBase64 } from "@main/helpers"; import { In } from "typeorm"; +import { GameStatus } from "@globals"; const startGameDownload = async ( _event: Electron.IpcMainInvokeEvent, diff --git a/src/main/index.ts b/src/main/index.ts index 5637bbd3..1657540d 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,5 +1,5 @@ import { stateManager } from "./state-manager"; -import { GameStatus, repackers } from "./constants"; +import { repackers } from "./constants"; import { getNewGOGGames, getNewRepacksFromCPG, @@ -22,6 +22,7 @@ import { Repack } from "./entity"; import { Notification } from "electron"; import { t } from "i18next"; import { In } from "typeorm"; +import { GameStatus } from "@globals"; startProcessWatcher(); diff --git a/src/types/index.ts b/src/types/index.ts index 768f110b..d9070451 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,5 @@ +import { GameStatus } from "@globals"; + export type GameShop = "steam" | "epic"; export type CatalogueCategory = "recently_added" | "trending"; @@ -75,7 +77,7 @@ export interface Game extends Omit { id: number; title: string; iconUrl: string; - status: string; + status: GameStatus | ""; folderName: string; downloadPath: string | null; repacks: GameRepack[]; diff --git a/tsconfig.json b/tsconfig.json index ee7e5c2b..aeb1de53 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,8 @@ "@main/*": ["src/main/*"], "@renderer/*": ["src/renderer/*"], "@types": ["src/types/index.ts"], - "@locales": ["src/locales/index.ts"] + "@locales": ["src/locales/index.ts"], + "@globals": ["src/globals.ts"] } }, "include": ["src/**/*"] From 3ef2f87412a93fcf54fe133614deb9f98a0ba7c9 Mon Sep 17 00:00:00 2001 From: lilezek Date: Mon, 29 Apr 2024 20:52:53 +0200 Subject: [PATCH 03/30] feat: added fields in entities to support rar decompression progress, and real debrid api token settings --- src/main/entity/game.entity.ts | 9 +++++++++ src/main/entity/user-preferences.entity.ts | 4 ++++ src/renderer/hooks/use-download.ts | 14 +++++++++----- src/renderer/pages/settings/settings.tsx | 10 ++++++++++ src/types/index.ts | 2 ++ 5 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts index 8798a15d..60daeb64 100644 --- a/src/main/entity/game.entity.ts +++ b/src/main/entity/game.entity.ts @@ -34,6 +34,9 @@ export class Game { @Column("text", { nullable: true }) executablePath: string | null; + @Column("text", { nullable: true }) + rarPath: string | null; + @Column("int", { default: 0 }) playTimeInMilliseconds: number; @@ -43,12 +46,18 @@ export class Game { @Column("text", { nullable: true }) status: GameStatus | ""; + /** + * Progress is a float between 0 and 1 + */ @Column("float", { default: 0 }) progress: number; @Column("float", { default: 0 }) fileVerificationProgress: number; + @Column("float", { default: 0 }) + decompressionProgress: number; + @Column("int", { default: 0 }) bytesDownloaded: number; diff --git a/src/main/entity/user-preferences.entity.ts b/src/main/entity/user-preferences.entity.ts index 40f1a26a..8cb06aa5 100644 --- a/src/main/entity/user-preferences.entity.ts +++ b/src/main/entity/user-preferences.entity.ts @@ -17,6 +17,9 @@ export class UserPreferences { @Column("text", { default: "en" }) language: string; + @Column("text", { nullable: true }) + realDebridApiToken: string | null; + @Column("boolean", { default: false }) downloadNotificationsEnabled: boolean; @@ -32,3 +35,4 @@ export class UserPreferences { @UpdateDateColumn() updatedAt: Date; } + diff --git a/src/renderer/hooks/use-download.ts b/src/renderer/hooks/use-download.ts index 0c649229..861a0591 100644 --- a/src/renderer/hooks/use-download.ts +++ b/src/renderer/hooks/use-download.ts @@ -12,6 +12,7 @@ import { import type { GameShop, TorrentProgress } from "@types"; import { useDate } from "./use-date"; import { formatBytes } from "@renderer/utils"; +import { GameStatus } from "@globals"; export function useDownload() { const { updateLibrary } = useLibrary(); @@ -63,9 +64,10 @@ export function useDownload() { updateLibrary(); }); - const isVerifying = ["downloading_metadata", "checking_files"].includes( - lastPacket?.game.status - ); + const isVerifying = + GameStatus.DownloadingMetadata == lastPacket?.game.status || + GameStatus.CheckingFiles == lastPacket?.game.status || + GameStatus.Decompressing == lastPacket?.game.status; const getETA = () => { if (isVerifying || !isFinite(lastPacket?.timeRemaining)) { @@ -84,8 +86,10 @@ export function useDownload() { }; const getProgress = () => { - if (lastPacket?.game.status === "checking_files") { + if (lastPacket?.game.status === GameStatus.CheckingFiles) { return formatDownloadProgress(lastPacket?.game.fileVerificationProgress); + } else if (lastPacket?.game.status === GameStatus.Decompressing) { + return formatDownloadProgress(lastPacket?.game.decompressionProgress); } return formatDownloadProgress(lastPacket?.game.progress); @@ -98,7 +102,7 @@ export function useDownload() { dispatch(setGameDeleting(gameId)); return window.electron.deleteGameFolder(gameId); }) - .catch(() => {}) + .catch(() => { }) .finally(() => { updateLibrary(); dispatch(removeGameFromDeleting(gameId)); diff --git a/src/renderer/pages/settings/settings.tsx b/src/renderer/pages/settings/settings.tsx index 47bd604b..23451b35 100644 --- a/src/renderer/pages/settings/settings.tsx +++ b/src/renderer/pages/settings/settings.tsx @@ -11,6 +11,7 @@ export function Settings() { downloadNotificationsEnabled: false, repackUpdatesNotificationsEnabled: false, telemetryEnabled: false, + realDebridApiToken: null, }); const { t } = useTranslation("settings"); @@ -27,6 +28,7 @@ export function Settings() { repackUpdatesNotificationsEnabled: userPreferences?.repackUpdatesNotificationsEnabled, telemetryEnabled: userPreferences?.telemetryEnabled, + realDebridApiToken: userPreferences.realDebridApiToken, }); }); }, []); @@ -107,6 +109,14 @@ export function Settings() { updateUserPreferences("telemetryEnabled", !form.telemetryEnabled) } /> + + { + updateUserPreferences("realDebridApiToken", event.target.value); + }} + /> ); diff --git a/src/types/index.ts b/src/types/index.ts index d9070451..a8071a6f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -84,6 +84,7 @@ export interface Game extends Omit { repack: GameRepack; progress: number; fileVerificationProgress: number; + decompressionProgress: number; bytesDownloaded: number; playTimeInMilliseconds: number; executablePath: string | null; @@ -107,6 +108,7 @@ export interface UserPreferences { downloadNotificationsEnabled: boolean; repackUpdatesNotificationsEnabled: boolean; telemetryEnabled: boolean; + realDebridApiToken: string | null; } export interface HowLongToBeatCategory { From 76a64fca4a982b80acb2e9ff6c8cb399ba568597 Mon Sep 17 00:00:00 2001 From: lilezek Date: Mon, 29 Apr 2024 20:53:58 +0200 Subject: [PATCH 04/30] feat: added support to unrar files --- src/main/services/unrar.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/main/services/unrar.ts diff --git a/src/main/services/unrar.ts b/src/main/services/unrar.ts new file mode 100644 index 00000000..b7c35393 --- /dev/null +++ b/src/main/services/unrar.ts @@ -0,0 +1,25 @@ +import { Extractor, createExtractorFromFile } from 'node-unrar-js'; +import fs from 'node:fs'; + +const wasmBinary = fs.readFileSync(require.resolve('node-unrar-js/esm/js/unrar.wasm')); + +export class Unrar { + private constructor(private extractor: Extractor) { } + + static async fromFilePath(filePath: string, targetFolder: string) { + console.log(filePath, targetFolder); + 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); + } + } +} From 6bb22655e850208d6dbf5aab81724c3e22125cbc Mon Sep 17 00:00:00 2001 From: lilezek Date: Mon, 29 Apr 2024 20:54:27 +0200 Subject: [PATCH 05/30] feat: added support to add magnets and download from real debrid --- .../services/donwloaders/real-debrid.d.ts | 65 +++++++++++++++++++ src/main/services/donwloaders/real-debrid.ts | 55 ++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 src/main/services/donwloaders/real-debrid.d.ts create mode 100644 src/main/services/donwloaders/real-debrid.ts diff --git a/src/main/services/donwloaders/real-debrid.d.ts b/src/main/services/donwloaders/real-debrid.d.ts new file mode 100644 index 00000000..4ee5228a --- /dev/null +++ b/src/main/services/donwloaders/real-debrid.d.ts @@ -0,0 +1,65 @@ +interface RealDebridUnrestrictLink { + id: string; + filename: string; + mimeType: string; + filesize: number; + link: string; + host: string; + host_icon: string; + chunks: number; + crc: number; + download: string; + streamable: number; +} + +interface RealDebridAddMagnet { + "id": string, + // URL of the created ressource + "uri": string +} + +interface RealDebridTorrentInfo { + "id": string, + "filename": string, + "original_filename": string, // Original name of the torrent + "hash": string, // SHA1 Hash of the torrent + "bytes": number, // Size of selected files only + "original_bytes": number, // Total size of the torrent + "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 + "added": string, // jsonDate + "files": [ + { + "id": number, + "path": string, // Path to the file inside the torrent, starting with "/" + "bytes": number, + "selected": number // 0 or 1 + }, + { + "id": number, + "path": string, // Path to the file inside the torrent, starting with "/" + "bytes": number, + "selected": number // 0 or 1 + } + ], + "links": [ + "string" // Host URL + ], + "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 +} + +declare module 'real-debrid-api' { + interface Torrent { + addMagnet(magnet: string): Promise; + info(id: string): Promise; + } + + export default class { + constructor(token: string); + torrents: Torrent; + } +} \ No newline at end of file diff --git a/src/main/services/donwloaders/real-debrid.ts b/src/main/services/donwloaders/real-debrid.ts new file mode 100644 index 00000000..0ab290d0 --- /dev/null +++ b/src/main/services/donwloaders/real-debrid.ts @@ -0,0 +1,55 @@ +/// +import { userPreferencesRepository } from "@main/repository"; +import fetch from "node-fetch"; + +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) { + const response = 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 getApiToken() { + return userPreferencesRepository.findOne({ where: { id: 1 } }).then(userPreferences => userPreferences.realDebridApiToken); + } +} \ No newline at end of file From 666b1afcb62a3a4d6b324919539b5a3e5a1b2e54 Mon Sep 17 00:00:00 2001 From: lilezek Date: Mon, 29 Apr 2024 20:57:04 +0200 Subject: [PATCH 06/30] feat: added a Downloader helper to choose between real debrid and torrent when downloading --- .../events/torrenting/cancel-game-download.ts | 5 +- .../events/torrenting/pause-game-download.ts | 5 +- .../events/torrenting/resume-game-download.ts | 11 +- .../events/torrenting/start-game-download.ts | 17 +- src/main/index.ts | 10 +- src/main/services/donwloaders/downloader.ts | 147 ++++++++++++++++++ .../services/donwloaders/http-downloader.ts | 89 +++++++++++ .../{ => donwloaders}/torrent-client.ts | 65 ++------ src/main/services/index.ts | 2 +- 9 files changed, 268 insertions(+), 83 deletions(-) create mode 100644 src/main/services/donwloaders/downloader.ts create mode 100644 src/main/services/donwloaders/http-downloader.ts rename src/main/services/{ => donwloaders}/torrent-client.ts (62%) diff --git a/src/main/events/torrenting/cancel-game-download.ts b/src/main/events/torrenting/cancel-game-download.ts index bcd4fdab..32aa79ae 100644 --- a/src/main/events/torrenting/cancel-game-download.ts +++ b/src/main/events/torrenting/cancel-game-download.ts @@ -1,9 +1,10 @@ import { gameRepository } from "@main/repository"; import { registerEvent } from "../register-event"; -import { WindowManager, writePipe } from "@main/services"; +import { WindowManager } from "@main/services"; import { In } from "typeorm"; +import { Downloader } from "@main/services/donwloaders/downloader"; import { GameStatus } from "@globals"; const cancelGameDownload = async ( @@ -41,7 +42,7 @@ const cancelGameDownload = async ( game.status !== GameStatus.Paused && game.status !== GameStatus.Seeding ) { - writePipe.write({ action: "cancel" }); + 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 e1da552a..6e728ede 100644 --- a/src/main/events/torrenting/pause-game-download.ts +++ b/src/main/events/torrenting/pause-game-download.ts @@ -1,8 +1,9 @@ -import { WindowManager, writePipe } from "@main/services"; +import { WindowManager } from "@main/services"; import { registerEvent } from "../register-event"; import { gameRepository } from "../../repository"; import { In } from "typeorm"; +import { Downloader } from "@main/services/donwloaders/downloader"; import { GameStatus } from "@globals"; const pauseGameDownload = async ( @@ -23,7 +24,7 @@ const pauseGameDownload = async ( ) .then((result) => { if (result.affected) { - writePipe.write({ action: "pause" }); + Downloader.pauseDownload(); 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 9d96ab18..a394c84a 100644 --- a/src/main/events/torrenting/resume-game-download.ts +++ b/src/main/events/torrenting/resume-game-download.ts @@ -2,7 +2,7 @@ import { registerEvent } from "../register-event"; import { gameRepository } from "../../repository"; import { getDownloadsPath } from "../helpers/get-downloads-path"; import { In } from "typeorm"; -import { writePipe } from "@main/services"; +import { Downloader } from "@main/services/donwloaders/downloader"; import { GameStatus } from "@globals"; const resumeGameDownload = async ( @@ -18,17 +18,12 @@ const resumeGameDownload = async ( if (!game) return; - writePipe.write({ action: "pause" }); + Downloader.resumeDownload(); if (game.status === GameStatus.Paused) { const downloadsPath = game.downloadPath ?? (await getDownloadsPath()); - writePipe.write({ - action: "start", - game_id: gameId, - magnet: game.repack.magnet, - save_path: downloadsPath, - }); + Downloader.downloadGame(game, game.repack); await gameRepository.update( { diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index a5853208..f6125c8a 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -6,6 +6,7 @@ import { registerEvent } from "../register-event"; import type { GameShop } from "@types"; import { getImageBase64 } from "@main/helpers"; import { In } from "typeorm"; +import { Downloader } from "@main/services/donwloaders/downloader"; import { GameStatus } from "@globals"; const startGameDownload = async ( @@ -35,7 +36,7 @@ const startGameDownload = async ( return; } - writePipe.write({ action: "pause" }); + Downloader.pauseDownload(); await gameRepository.update( { @@ -61,12 +62,7 @@ const startGameDownload = async ( } ); - writePipe.write({ - action: "start", - game_id: game.id, - magnet: repack.magnet, - save_path: downloadPath, - }); + Downloader.downloadGame(game, repack); game.status = GameStatus.DownloadingMetadata; @@ -84,12 +80,7 @@ const startGameDownload = async ( repack: { id: repackId }, }); - writePipe.write({ - action: "start", - game_id: createdGame.id, - magnet: repack.magnet, - save_path: downloadPath, - }); + Downloader.downloadGame(createdGame, repack); const { repack: _, ...rest } = createdGame; diff --git a/src/main/index.ts b/src/main/index.ts index 1657540d..cc89a58e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -17,11 +17,12 @@ import { steamGameRepository, userPreferencesRepository, } from "./repository"; -import { TorrentClient } from "./services/torrent-client"; +import { TorrentClient } from "./services/donwloaders/torrent-client"; import { Repack } from "./entity"; import { Notification } from "electron"; import { t } from "i18next"; import { In } from "typeorm"; +import { Downloader } from "./services/donwloaders/downloader"; import { GameStatus } from "@globals"; startProcessWatcher(); @@ -41,12 +42,7 @@ Promise.all([writePipe.createPipe(), readPipe.createPipe()]).then(async () => { }); if (game) { - writePipe.write({ - action: "start", - game_id: game.id, - magnet: game.repack.magnet, - save_path: game.downloadPath, - }); + Downloader.downloadGame(game, game.repack); } readPipe.socket.on("data", (data) => { diff --git a/src/main/services/donwloaders/downloader.ts b/src/main/services/donwloaders/downloader.ts new file mode 100644 index 00000000..9c3f1a72 --- /dev/null +++ b/src/main/services/donwloaders/downloader.ts @@ -0,0 +1,147 @@ +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 { WindowManager } from "../window-manager"; +import { TorrentUpdate } from "./torrent-client"; +import { HTTPDownloader } from "./http-downloader"; +import { Unrar } from "../unrar"; +import { GameStatus } from "@globals"; + +interface DownloadStatus { + numPeers: number; + numSeeds: number; + downloadSpeed: number; + timeRemaining: number; +} + +export class Downloader { + private static lastHttpDownloader: HTTPDownloader | null = null; + + 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 { + const torrent = await RealDebridClient.addMagnet(repack.magnet); + if (torrent && torrent.id) { + await RealDebridClient.selectAllFiles(torrent.id); + const { links } = await RealDebridClient.getInfo(torrent.id); + const { download } = await RealDebridClient.unrestrictLink(links[0]); + this.lastHttpDownloader = new HTTPDownloader(); + this.lastHttpDownloader.download(download, game.downloadPath, game.id); + } + } catch (e) { + console.error(e); + } + } + } + + static async updateGameProgress(gameId: number, gameUpdate: QueryDeepPartialEntity, downloadStatus: DownloadStatus) { + await gameRepository.update({ id: gameId }, gameUpdate); + + const game = await gameRepository.findOne({ + where: { id: gameId }, + relations: { repack: true }, + }); + + if (gameUpdate.progress === 1 && gameUpdate.status !== GameStatus.Decompressing) { + 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 (gameUpdate.decompressionProgress === 0 && gameUpdate.status === GameStatus.Decompressing) { + const unrar = await Unrar.fromFilePath(game.rarPath, game.downloadPath); + unrar.extract(); + this.updateGameProgress(gameId, { + decompressionProgress: 1, + status: GameStatus.Finished, + }, downloadStatus); + } + + if (WindowManager.mainWindow) { + 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 + })) + ); + } + } + + static getGameProgress(game: Game) { + if (game.status === GameStatus.CheckingFiles) return game.fileVerificationProgress; + if (game.status === GameStatus.Decompressing) return game.decompressionProgress; + return game.progress; + } +} \ No newline at end of file diff --git a/src/main/services/donwloaders/http-downloader.ts b/src/main/services/donwloaders/http-downloader.ts new file mode 100644 index 00000000..aec963e0 --- /dev/null +++ b/src/main/services/donwloaders/http-downloader.ts @@ -0,0 +1,89 @@ +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'; + +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}`, + }; + 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); + } + } +} \ No newline at end of file diff --git a/src/main/services/torrent-client.ts b/src/main/services/donwloaders/torrent-client.ts similarity index 62% rename from src/main/services/torrent-client.ts rename to src/main/services/donwloaders/torrent-client.ts index fa1cd59d..8e48bbbd 100644 --- a/src/main/services/torrent-client.ts +++ b/src/main/services/donwloaders/torrent-client.ts @@ -2,13 +2,12 @@ import path from "node:path"; import cp from "node:child_process"; import fs from "node:fs"; import * as Sentry from "@sentry/electron/main"; -import { Notification, app, dialog } from "electron"; +import { app, dialog } from "electron"; import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; import { Game } from "@main/entity"; -import { gameRepository, userPreferencesRepository } from "@main/repository"; -import { t } from "i18next"; -import { WindowManager } from "./window-manager"; +import { Downloader } from "./downloader"; +import { GameStatus } from "@globals"; const binaryNameByPlatform: Partial> = { darwin: "hydra-download-manager", @@ -75,6 +74,7 @@ export class TorrentClient { __dirname, "..", "..", + "..", "torrent-client", "main.py" ); @@ -85,20 +85,15 @@ export class TorrentClient { } private static getTorrentStateName(state: TorrentState) { - if (state === TorrentState.CheckingFiles) return "checking_files"; - if (state === TorrentState.Downloading) return "downloading"; + if (state === TorrentState.CheckingFiles) return GameStatus.CheckingFiles; + if (state === TorrentState.Downloading) return GameStatus.Downloading; if (state === TorrentState.DownloadingMetadata) - return "downloading_metadata"; - if (state === TorrentState.Finished) return "finished"; - if (state === TorrentState.Seeding) return "seeding"; + return GameStatus.DownloadingMetadata; + if (state === TorrentState.Finished) return GameStatus.Finished; + if (state === TorrentState.Seeding) return GameStatus.Seeding; return ""; } - private static getGameProgress(game: Game) { - if (game.status === "checking_files") return game.fileVerificationProgress; - return game.progress; - } - public static async onSocketData(data: Buffer) { const message = Buffer.from(data).toString("utf-8"); @@ -127,44 +122,14 @@ export class TorrentClient { updatePayload.progress = payload.progress; } - await gameRepository.update({ id: payload.gameId }, updatePayload); - - const game = await gameRepository.findOne({ - where: { id: payload.gameId }, - relations: { repack: true }, + Downloader.updateGameProgress(payload.gameId, updatePayload, { + numPeers: payload.numPeers, + numSeeds: payload.numSeeds, + downloadSpeed: payload.downloadSpeed, + timeRemaining: payload.timeRemaining, }); - - 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) { - const progress = this.getGameProgress(game); - WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress); - - WindowManager.mainWindow.webContents.send( - "on-download-progress", - JSON.parse(JSON.stringify({ ...payload, game })) - ); - } } catch (err) { Sentry.captureException(err); } } -} +} \ No newline at end of file diff --git a/src/main/services/index.ts b/src/main/services/index.ts index 2544c6f4..215cd016 100644 --- a/src/main/services/index.ts +++ b/src/main/services/index.ts @@ -6,6 +6,6 @@ export * from "./steam-grid"; export * from "./update-resolver"; export * from "./window-manager"; export * from "./fifo"; -export * from "./torrent-client"; +export * from "./donwloaders/torrent-client"; export * from "./how-long-to-beat"; export * from "./process-watcher"; From f1d2a8844281ef99f75dfb06b73d544fe7e63951 Mon Sep 17 00:00:00 2001 From: lilezek Date: Tue, 30 Apr 2024 09:29:29 +0200 Subject: [PATCH 07/30] feat: added helper functions to the game status to keep it simple to read. --- src/globals.ts | 16 ++++++++++++++++ .../events/torrenting/cancel-game-download.ts | 2 ++ .../components/bottom-panel/bottom-panel.tsx | 5 +++-- src/renderer/components/sidebar/sidebar.tsx | 13 +++++-------- src/renderer/hooks/use-download.ts | 5 +---- src/renderer/pages/downloads/downloads.tsx | 19 ++++++++++--------- .../pages/game-details/hero-panel-actions.tsx | 9 +++++---- .../pages/game-details/hero-panel.tsx | 7 ++++--- 8 files changed, 46 insertions(+), 30 deletions(-) diff --git a/src/globals.ts b/src/globals.ts index e4675aa7..b240172a 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -7,4 +7,20 @@ export enum GameStatus { Cancelled = "cancelled", Finished = "finished", Decompressing = "decompressing", +} + +export namespace GameStatus { + export const isDownloading = (status: GameStatus | "") => + status === GameStatus.Downloading || + status === GameStatus.DownloadingMetadata || + status === GameStatus.CheckingFiles; + + export const isVerifying = (status: GameStatus | "") => + GameStatus.DownloadingMetadata == status || + GameStatus.CheckingFiles == status || + GameStatus.Decompressing == status; + + export const isReady = (status: GameStatus | "") => + status === GameStatus.Finished || + status === GameStatus.Seeding; } \ No newline at end of file diff --git a/src/main/events/torrenting/cancel-game-download.ts b/src/main/events/torrenting/cancel-game-download.ts index 32aa79ae..af8ae89f 100644 --- a/src/main/events/torrenting/cancel-game-download.ts +++ b/src/main/events/torrenting/cancel-game-download.ts @@ -20,6 +20,8 @@ const cancelGameDownload = async ( GameStatus.CheckingFiles, GameStatus.Paused, GameStatus.Seeding, + GameStatus.Finished, + GameStatus.Decompressing, ]), }, }); diff --git a/src/renderer/components/bottom-panel/bottom-panel.tsx b/src/renderer/components/bottom-panel/bottom-panel.tsx index c870823e..46e278ca 100644 --- a/src/renderer/components/bottom-panel/bottom-panel.tsx +++ b/src/renderer/components/bottom-panel/bottom-panel.tsx @@ -7,6 +7,7 @@ import { vars } from "@renderer/theme.css"; import { useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { VERSION_CODENAME } from "@renderer/constants"; +import { GameStatus } from "@globals"; export function BottomPanel() { const { t } = useTranslation("bottom_panel"); @@ -23,10 +24,10 @@ export function BottomPanel() { const status = useMemo(() => { if (isDownloading) { - if (game.status === "downloading_metadata") + if (game.status === GameStatus.DownloadingMetadata) return t("downloading_metadata", { title: game.title }); - if (game.status === "checking_files") + if (game.status === GameStatus.CheckingFiles) return t("checking_files", { title: game.title, percentage: progress, diff --git a/src/renderer/components/sidebar/sidebar.tsx b/src/renderer/components/sidebar/sidebar.tsx index 2d853553..cd4aff92 100644 --- a/src/renderer/components/sidebar/sidebar.tsx +++ b/src/renderer/components/sidebar/sidebar.tsx @@ -14,6 +14,7 @@ import { MarkGithubIcon } from "@primer/octicons-react"; import DiscordLogo from "@renderer/assets/discord-icon.svg"; import XLogo from "@renderer/assets/x-icon.svg"; import * as styles from "./sidebar.css"; +import { GameStatus } from "@globals"; const socials = [ { @@ -57,9 +58,7 @@ export function Sidebar() { }, [gameDownloading?.id, updateLibrary]); const isDownloading = library.some((game) => - ["downloading", "checking_files", "downloading_metadata"].includes( - game.status - ) + GameStatus.isDownloading(game.status) ); const sidebarRef = useRef(null); @@ -118,12 +117,10 @@ export function Sidebar() { }, [isResizing]); const getGameTitle = (game: Game) => { - if (game.status === "paused") return t("paused", { title: game.title }); + if (game.status === GameStatus.Paused) return t("paused", { title: game.title }); if (gameDownloading?.id === game.id) { - const isVerifying = ["downloading_metadata", "checking_files"].includes( - gameDownloading?.status - ); + const isVerifying = GameStatus.isVerifying(gameDownloading.status); if (isVerifying) return t(gameDownloading.status, { @@ -203,7 +200,7 @@ export function Sidebar() { className={styles.menuItem({ active: location.pathname === `/game/${game.shop}/${game.objectID}`, - muted: game.status === "cancelled", + muted: game.status === GameStatus.Cancelled, })} >