Merge pull request #125 from lilezek/main

Adds support to real debrid.
This commit is contained in:
Hydra 2024-05-04 18:39:22 +01:00 committed by GitHub
commit ea2c002419
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 710 additions and 157 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
node_modules node_modules
hydra-download-manager hydra-download-manager
fastlist.exe fastlist.exe
unrar.wasm
__pycache__ __pycache__
dist dist
out out

View File

@ -105,6 +105,13 @@ yarn make
<sub><b>Null</b></sub> <sub><b>Null</b></sub>
</a> </a>
</td> </td>
<td align="center">
<a href="https://github.com/lilezek">
<img src="https://avatars.githubusercontent.com/u/2767229?v=4" width="100;" alt="lilezek"/>
<br />
<sub><b>Null</b></sub>
</a>
</td>
<td align="center"> <td align="center">
<a href="https://github.com/JackEnx"> <a href="https://github.com/JackEnx">
<img src="https://avatars.githubusercontent.com/u/167036558?v=4" width="100;" alt="JackEnx"/> <img src="https://avatars.githubusercontent.com/u/167036558?v=4" width="100;" alt="JackEnx"/>
@ -118,21 +125,14 @@ yarn make
<br /> <br />
<sub><b>Magrid</b></sub> <sub><b>Magrid</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/fhilipecrash"> <a href="https://github.com/fhilipecrash">
<img src="https://avatars.githubusercontent.com/u/36455575?v=4" width="100;" alt="fhilipecrash"/> <img src="https://avatars.githubusercontent.com/u/36455575?v=4" width="100;" alt="fhilipecrash"/>
<br /> <br />
<sub><b>Fhilipe Coelho</b></sub> <sub><b>Fhilipe Coelho</b></sub>
</a> </a>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/jps14">
<img src="https://avatars.githubusercontent.com/u/168477146?v=4" width="100;" alt="jps14"/>
<br />
<sub><b>José Luís</b></sub>
</a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/shadowtosser"> <a href="https://github.com/shadowtosser">
@ -141,6 +141,13 @@ yarn make
<sub><b>Null</b></sub> <sub><b>Null</b></sub>
</a> </a>
</td> </td>
<td align="center">
<a href="https://github.com/jps14">
<img src="https://avatars.githubusercontent.com/u/168477146?v=4" width="100;" alt="jps14"/>
<br />
<sub><b>José Luís</b></sub>
</a>
</td>
<td align="center"> <td align="center">
<a href="https://github.com/pmenta"> <a href="https://github.com/pmenta">
<img src="https://avatars.githubusercontent.com/u/71457671?v=4" width="100;" alt="pmenta"/> <img src="https://avatars.githubusercontent.com/u/71457671?v=4" width="100;" alt="pmenta"/>
@ -161,15 +168,15 @@ yarn make
<br /> <br />
<sub><b>Guilherme Viana</b></sub> <sub><b>Guilherme Viana</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/eltociear"> <a href="https://github.com/eltociear">
<img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="100;" alt="eltociear"/> <img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="100;" alt="eltociear"/>
<br /> <br />
<sub><b>Ikko Eltociear Ashimine</b></sub> <sub><b>Ikko Eltociear Ashimine</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/Netflixyapp"> <a href="https://github.com/Netflixyapp">
<img src="https://avatars.githubusercontent.com/u/91623880?v=4" width="100;" alt="Netflixyapp"/> <img src="https://avatars.githubusercontent.com/u/91623880?v=4" width="100;" alt="Netflixyapp"/>

View File

@ -6,6 +6,7 @@ extraResources:
- hydra-download-manager - hydra-download-manager
- hydra.db - hydra.db
- fastlist.exe - fastlist.exe
- unrar.wasm
files: files:
- "!**/.vscode/*" - "!**/.vscode/*"
- "!src/*" - "!src/*"

View File

@ -31,6 +31,7 @@ export default defineConfig(({ mode }) => {
"@main": resolve("src/main"), "@main": resolve("src/main"),
"@locales": resolve("src/locales"), "@locales": resolve("src/locales"),
"@resources": resolve("resources"), "@resources": resolve("resources"),
"@globals": resolve("src/globals"),
}, },
}, },
plugins: [externalizeDepsPlugin(), swcPlugin(), sentryPlugin], plugins: [externalizeDepsPlugin(), swcPlugin(), sentryPlugin],
@ -46,6 +47,7 @@ export default defineConfig(({ mode }) => {
alias: { alias: {
"@renderer": resolve("src/renderer/src"), "@renderer": resolve("src/renderer/src"),
"@locales": resolve("src/locales"), "@locales": resolve("src/locales"),
"@globals": resolve("src/globals"),
}, },
}, },
plugins: [svgr(), react(), vanillaExtractPlugin(), sentryPlugin], plugins: [svgr(), react(), vanillaExtractPlugin(), sentryPlugin],

View File

@ -43,6 +43,7 @@
"classnames": "^2.5.1", "classnames": "^2.5.1",
"color.js": "^1.2.0", "color.js": "^1.2.0",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"electron-dl-manager": "^3.0.0",
"fetch-cookie": "^3.0.1", "fetch-cookie": "^3.0.1",
"flexsearch": "^0.7.43", "flexsearch": "^0.7.43",
"i18next": "^23.11.2", "i18next": "^23.11.2",
@ -50,6 +51,7 @@
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lottie-react": "^2.4.0", "lottie-react": "^2.4.0",
"node-unrar-js": "^2.0.2",
"parse-torrent": "^11.0.16", "parse-torrent": "^11.0.16",
"ps-list": "^8.1.1", "ps-list": "^8.1.1",
"react-i18next": "^14.1.0", "react-i18next": "^14.1.0",

View File

@ -6,3 +6,5 @@ if (process.platform === "win32") {
"fastlist.exe" "fastlist.exe"
); );
} }
fs.copyFileSync("node_modules/node-unrar-js/esm/js/unrar.wasm", "unrar.wasm");

25
src/globals.ts Normal file
View File

@ -0,0 +1,25 @@
export enum GameStatus {
Seeding = "seeding",
Downloading = "downloading",
Paused = "paused",
CheckingFiles = "checking_files",
DownloadingMetadata = "downloading_metadata",
Cancelled = "cancelled",
Finished = "finished",
Decompressing = "decompressing",
}
export namespace GameStatus {
export const isDownloading = (status: GameStatus | null) =>
status === GameStatus.Downloading ||
status === GameStatus.DownloadingMetadata ||
status === GameStatus.CheckingFiles;
export const isVerifying = (status: GameStatus | null) =>
GameStatus.DownloadingMetadata == status ||
GameStatus.CheckingFiles == status ||
GameStatus.Decompressing == status;
export const isReady = (status: GameStatus | null) =>
status === GameStatus.Finished || status === GameStatus.Seeding;
}

View File

@ -137,6 +137,7 @@
"enable_repack_list_notifications": "When a new repack is added", "enable_repack_list_notifications": "When a new repack is added",
"telemetry": "Telemetry", "telemetry": "Telemetry",
"telemetry_description": "Enable anonymous usage statistics", "telemetry_description": "Enable anonymous usage statistics",
"real_debrid_api_token_description": "(Optional) Real Debrid API token",
"behavior": "Behavior", "behavior": "Behavior",
"quit_app_instead_hiding": "Close app instead of minimizing to tray" "quit_app_instead_hiding": "Close app instead of minimizing to tray"
}, },

View File

@ -69,7 +69,7 @@
"remove_from_library": "Eliminar de la biblioteca", "remove_from_library": "Eliminar de la biblioteca",
"no_downloads": "No hay descargas disponibles", "no_downloads": "No hay descargas disponibles",
"next_suggestion": "Siguiente sugerencia", "next_suggestion": "Siguiente sugerencia",
"play_time": "Jugado por {{cantidad}}", "play_time": "Jugado {{amount}}",
"install": "Instalar", "install": "Instalar",
"play": "Jugar", "play": "Jugar",
"not_played_yet": "Aún no has jugado a {{title}}", "not_played_yet": "Aún no has jugado a {{title}}",
@ -130,7 +130,8 @@
"enable_download_notifications": "Cuando se completa una descarga", "enable_download_notifications": "Cuando se completa una descarga",
"enable_repack_list_notifications": "Cuando se añade un repack nuevo", "enable_repack_list_notifications": "Cuando se añade un repack nuevo",
"telemetry": "Telemetría", "telemetry": "Telemetría",
"telemetry_description": "Habilitar recopilación de datos de manera anónima" "telemetry_description": "Habilitar recopilación de datos de manera anónima",
"real_debrid_api_token_description": "(Opcional) Real Debrid API token"
}, },
"notifications": { "notifications": {
"download_complete": "Descarga completada", "download_complete": "Descarga completada",

View File

@ -115,7 +115,8 @@
"enable_download_notifications": "Quand un téléchargement est terminé", "enable_download_notifications": "Quand un téléchargement est terminé",
"enable_repack_list_notifications": "Quand un nouveau repack est ajouté", "enable_repack_list_notifications": "Quand un nouveau repack est ajouté",
"telemetry": "Télémétrie", "telemetry": "Télémétrie",
"telemetry_description": "Activer les statistiques d'utilisation anonymes" "telemetry_description": "Activer les statistiques d'utilisation anonymes",
"real_debrid_api_token_description": "(Facultatif) Real Debrid API token"
}, },
"notifications": { "notifications": {
"download_complete": "Téléchargement terminé", "download_complete": "Téléchargement terminé",

View File

@ -124,7 +124,8 @@
"enable_download_notifications": "Amikor egy letöltés befejeződik", "enable_download_notifications": "Amikor egy letöltés befejeződik",
"enable_repack_list_notifications": "Amikor egy új repack hozzáadásra kerül", "enable_repack_list_notifications": "Amikor egy új repack hozzáadásra kerül",
"telemetry": "Telemetria", "telemetry": "Telemetria",
"telemetry_description": "Névtelen felhasználási statisztikák engedélyezése" "telemetry_description": "Névtelen felhasználási statisztikák engedélyezése",
"real_debrid_api_token_description": "(Választható) Real Debrid API token"
}, },
"notifications": { "notifications": {
"download_complete": "Letöltés befejeződött", "download_complete": "Letöltés befejeződött",

View File

@ -136,7 +136,8 @@
"enable_download_notifications": "Quando un download è completo", "enable_download_notifications": "Quando un download è completo",
"enable_repack_list_notifications": "Quando viene aggiunto un nuovo repack", "enable_repack_list_notifications": "Quando viene aggiunto un nuovo repack",
"telemetry": "Telemetria", "telemetry": "Telemetria",
"telemetry_description": "Abilita statistiche di utilizzo anonime" "telemetry_description": "Abilita statistiche di utilizzo anonime",
"real_debrid_api_token_description": "(Facoltativo) Real Debrid API token"
}, },
"notifications": { "notifications": {
"download_complete": "Download completato", "download_complete": "Download completato",

View File

@ -133,6 +133,7 @@
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada", "enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
"telemetry": "Telemetria", "telemetry": "Telemetria",
"telemetry_description": "Habilitar estatísticas de uso anônimas", "telemetry_description": "Habilitar estatísticas de uso anônimas",
"real_debrid_api_token_description": "(Opcional) Real Debrid API token",
"behavior": "Comportamento", "behavior": "Comportamento",
"quit_app_instead_hiding": "Fechar o aplicativo em vez de minimizá-lo" "quit_app_instead_hiding": "Fechar o aplicativo em vez de minimizá-lo"
}, },

View File

@ -33,15 +33,6 @@ export const months = [
"Dec", "Dec",
]; ];
export enum GameStatus {
Seeding = "seeding",
Downloading = "downloading",
Paused = "paused",
CheckingFiles = "checking_files",
DownloadingMetadata = "downloading_metadata",
Cancelled = "cancelled",
}
export const defaultDownloadsPath = app.getPath("downloads"); export const defaultDownloadsPath = app.getPath("downloads");
export const databasePath = path.join( export const databasePath = path.join(

View File

@ -9,6 +9,7 @@ import {
} from "typeorm"; } from "typeorm";
import type { GameShop } from "@types"; import type { GameShop } from "@types";
import { Repack } from "./repack.entity"; import { Repack } from "./repack.entity";
import { GameStatus } from "@globals";
@Entity("game") @Entity("game")
export class Game { export class Game {
@ -33,6 +34,9 @@ export class Game {
@Column("text", { nullable: true }) @Column("text", { nullable: true })
executablePath: string | null; executablePath: string | null;
@Column("text", { nullable: true })
rarPath: string | null;
@Column("int", { default: 0 }) @Column("int", { default: 0 })
playTimeInMilliseconds: number; playTimeInMilliseconds: number;
@ -40,14 +44,20 @@ export class Game {
shop: GameShop; shop: GameShop;
@Column("text", { nullable: true }) @Column("text", { nullable: true })
status: string | null; status: GameStatus | null;
/**
* Progress is a float between 0 and 1
*/
@Column("float", { default: 0 }) @Column("float", { default: 0 })
progress: number; progress: number;
@Column("float", { default: 0 }) @Column("float", { default: 0 })
fileVerificationProgress: number; fileVerificationProgress: number;
@Column("float", { default: 0 })
decompressionProgress: number;
@Column("int", { default: 0 }) @Column("int", { default: 0 })
bytesDownloaded: number; bytesDownloaded: number;

View File

@ -17,6 +17,9 @@ export class UserPreferences {
@Column("text", { default: "en" }) @Column("text", { default: "en" })
language: string; language: string;
@Column("text", { nullable: true })
realDebridApiToken: string | null;
@Column("boolean", { default: false }) @Column("boolean", { default: false })
downloadNotificationsEnabled: boolean; downloadNotificationsEnabled: boolean;

View File

@ -1,7 +1,7 @@
import path from "node:path"; import path from "node:path";
import fs from "node:fs"; import fs from "node:fs";
import { GameStatus } from "@main/constants"; import { GameStatus } from "@globals";
import { gameRepository } from "@main/repository"; import { gameRepository } from "@main/repository";
import { getDownloadsPath } from "../helpers/get-downloads-path"; import { getDownloadsPath } from "../helpers/get-downloads-path";

View File

@ -1,8 +1,8 @@
import { gameRepository } from "@main/repository"; import { gameRepository } from "@main/repository";
import { GameStatus } from "@main/constants";
import { searchRepacks } from "../helpers/search-games"; import { searchRepacks } from "../helpers/search-games";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { GameStatus } from "@globals";
import { sortBy } from "lodash-es"; import { sortBy } from "lodash-es";
const getLibrary = async () => const getLibrary = async () =>

View File

@ -1,6 +1,6 @@
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { gameRepository } from "../../repository"; import { gameRepository } from "../../repository";
import { GameStatus } from "@main/constants"; import { GameStatus } from "@globals";
const removeGame = async ( const removeGame = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,

View File

@ -1,10 +1,11 @@
import { GameStatus } from "@main/constants";
import { gameRepository } from "@main/repository"; import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { WindowManager, writePipe } from "@main/services"; import { WindowManager } from "@main/services";
import { In } from "typeorm"; import { In } from "typeorm";
import { Downloader } from "@main/services/downloaders/downloader";
import { GameStatus } from "@globals";
const cancelGameDownload = async ( const cancelGameDownload = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
@ -19,6 +20,8 @@ const cancelGameDownload = async (
GameStatus.CheckingFiles, GameStatus.CheckingFiles,
GameStatus.Paused, GameStatus.Paused,
GameStatus.Seeding, GameStatus.Seeding,
GameStatus.Finished,
GameStatus.Decompressing,
]), ]),
}, },
}); });
@ -41,7 +44,7 @@ const cancelGameDownload = async (
game.status !== GameStatus.Paused && game.status !== GameStatus.Paused &&
game.status !== GameStatus.Seeding game.status !== GameStatus.Seeding
) { ) {
writePipe.write({ action: "cancel" }); Downloader.cancelDownload();
if (result.affected) WindowManager.mainWindow?.setProgressBar(-1); if (result.affected) WindowManager.mainWindow?.setProgressBar(-1);
} }
}); });

View File

@ -1,9 +1,10 @@
import { WindowManager, writePipe } from "@main/services"; import { WindowManager } from "@main/services";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { GameStatus } from "../../constants";
import { gameRepository } from "../../repository"; import { gameRepository } from "../../repository";
import { In } from "typeorm"; import { In } from "typeorm";
import { Downloader } from "@main/services/downloaders/downloader";
import { GameStatus } from "@globals";
const pauseGameDownload = async ( const pauseGameDownload = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
@ -23,7 +24,7 @@ const pauseGameDownload = async (
) )
.then((result) => { .then((result) => {
if (result.affected) { if (result.affected) {
writePipe.write({ action: "pause" }); Downloader.pauseDownload();
WindowManager.mainWindow?.setProgressBar(-1); WindowManager.mainWindow?.setProgressBar(-1);
} }
}); });

View File

@ -1,9 +1,9 @@
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { GameStatus } from "../../constants";
import { gameRepository } from "../../repository"; import { gameRepository } from "../../repository";
import { getDownloadsPath } from "../helpers/get-downloads-path"; import { getDownloadsPath } from "../helpers/get-downloads-path";
import { In } from "typeorm"; import { In } from "typeorm";
import { writePipe } from "@main/services"; import { Downloader } from "@main/services/downloaders/downloader";
import { GameStatus } from "@globals";
const resumeGameDownload = async ( const resumeGameDownload = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
@ -18,17 +18,12 @@ const resumeGameDownload = async (
if (!game) return; if (!game) return;
writePipe.write({ action: "pause" }); Downloader.resumeDownload();
if (game.status === GameStatus.Paused) { if (game.status === GameStatus.Paused) {
const downloadsPath = game.downloadPath ?? (await getDownloadsPath()); const downloadsPath = game.downloadPath ?? (await getDownloadsPath());
writePipe.write({ Downloader.downloadGame(game, game.repack);
action: "start",
game_id: gameId,
magnet: game.repack.magnet,
save_path: downloadsPath,
});
await gameRepository.update( await gameRepository.update(
{ {

View File

@ -1,12 +1,13 @@
import { getSteamGameIconUrl, writePipe } from "@main/services"; import { getSteamGameIconUrl } from "@main/services";
import { gameRepository, repackRepository } from "@main/repository"; import { gameRepository, repackRepository } from "@main/repository";
import { GameStatus } from "@main/constants";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import type { GameShop } from "@types"; import type { GameShop } from "@types";
import { getFileBase64 } from "@main/helpers"; import { getFileBase64 } from "@main/helpers";
import { In } from "typeorm"; import { In } from "typeorm";
import { Downloader } from "@main/services/downloaders/downloader";
import { GameStatus } from "@globals";
const startGameDownload = async ( const startGameDownload = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
@ -35,7 +36,7 @@ const startGameDownload = async (
return; return;
} }
writePipe.write({ action: "pause" }); Downloader.pauseDownload();
await gameRepository.update( await gameRepository.update(
{ {
@ -61,12 +62,7 @@ const startGameDownload = async (
} }
); );
writePipe.write({ Downloader.downloadGame(game, repack);
action: "start",
game_id: game.id,
magnet: repack.magnet,
save_path: downloadPath,
});
game.status = GameStatus.DownloadingMetadata; game.status = GameStatus.DownloadingMetadata;
@ -84,12 +80,7 @@ const startGameDownload = async (
repack: { id: repackId }, repack: { id: repackId },
}); });
writePipe.write({ Downloader.downloadGame(createdGame, repack);
action: "start",
game_id: createdGame.id,
magnet: repack.magnet,
save_path: downloadPath,
});
const { repack: _, ...rest } = createdGame; const { repack: _, ...rest } = createdGame;

View File

@ -1,5 +1,5 @@
import { stateManager } from "./state-manager"; import { stateManager } from "./state-manager";
import { GameStatus, repackers } from "./constants"; import { repackers } from "./constants";
import { import {
getNewGOGGames, getNewGOGGames,
getNewRepacksFromCPG, getNewRepacksFromCPG,
@ -17,11 +17,13 @@ import {
steamGameRepository, steamGameRepository,
userPreferencesRepository, userPreferencesRepository,
} from "./repository"; } from "./repository";
import { TorrentClient } from "./services/torrent-client"; import { TorrentClient } from "./services/downloaders/torrent-client";
import { Repack } from "./entity"; import { Repack } from "./entity";
import { Notification } from "electron"; import { Notification } from "electron";
import { t } from "i18next"; import { t } from "i18next";
import { In } from "typeorm"; import { In } from "typeorm";
import { Downloader } from "./services/downloaders/downloader";
import { GameStatus } from "@globals";
startProcessWatcher(); startProcessWatcher();
@ -40,12 +42,7 @@ Promise.all([writePipe.createPipe(), readPipe.createPipe()]).then(async () => {
}); });
if (game) { if (game) {
writePipe.write({ Downloader.downloadGame(game, game.repack);
action: "start",
game_id: game.id,
magnet: game.repack.magnet,
save_path: game.downloadPath,
});
} }
readPipe.socket?.on("data", (data) => { readPipe.socket?.on("data", (data) => {

View File

@ -0,0 +1,214 @@
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";
import path from "node:path";
import crypto from "node:crypto";
import fs from "node:fs";
import { app } from "electron";
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 {
// Lets try first to find the torrent on RealDebrid
const torrents = await RealDebridClient.getAllTorrents();
const hash = RealDebridClient.extractSHA1FromMagnet(repack.magnet);
let torrent = torrents.find((t) => t.hash === hash);
if (!torrent) {
// Torrent is missing, lets add it
const magnet = await RealDebridClient.addMagnet(repack.magnet);
if (magnet && magnet.id) {
await RealDebridClient.selectAllFiles(magnet.id);
torrent = await RealDebridClient.getInfo(magnet.id);
}
}
if (torrent) {
const { links } = torrent;
const { download } = await RealDebridClient.unrestrictLink(links[0]);
this.lastHttpDownloader = new HTTPDownloader();
this.lastHttpDownloader.download(
download,
game.downloadPath!,
game.id
);
}
} catch (e) {
console.error(e);
}
}
}
static async updateGameProgress(
gameId: number,
gameUpdate: QueryDeepPartialEntity<Game>,
downloadStatus: DownloadStatus
) {
await gameRepository.update({ id: gameId }, gameUpdate);
const game = await gameRepository.findOne({
where: { id: gameId },
relations: { repack: true },
});
if (
game?.progress === 1 &&
gameUpdate.status !== GameStatus.Decompressing
) {
const userPreferences = await userPreferencesRepository.findOne({
where: { id: 1 },
});
if (userPreferences?.downloadNotificationsEnabled) {
const iconPath = await this.createTempIcon(game.iconUrl);
new Notification({
icon: iconPath,
title: t("download_complete", {
ns: "notifications",
lng: userPreferences.language,
}),
body: t("game_ready_to_install", {
ns: "notifications",
lng: userPreferences.language,
title: game?.title,
}),
}).show();
}
}
if (
game &&
gameUpdate.decompressionProgress === 0 &&
gameUpdate.status === GameStatus.Decompressing
) {
const unrar = await Unrar.fromFilePath(
game.rarPath!,
path.join(game.downloadPath!, game.folderName!)
);
unrar.extract();
this.updateGameProgress(
gameId,
{
decompressionProgress: 1,
status: GameStatus.Finished,
},
downloadStatus
);
}
if (WindowManager.mainWindow && game) {
const progress = this.getGameProgress(game);
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
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;
}
private static createTempIcon(encodedIcon: string): Promise<string> {
return new Promise((resolve, reject) => {
const hash = crypto.randomBytes(16).toString("hex");
const iconPath = path.join(app.getPath("temp"), `${hash}.png`);
fs.writeFile(
iconPath,
Buffer.from(
encodedIcon.replace("data:image/jpeg;base64,", ""),
"base64"
),
(err) => {
if (err) reject(err);
resolve(iconPath);
}
);
});
}
}

View File

@ -0,0 +1,106 @@
import { Game } from "@main/entity";
import { ElectronDownloadManager } from "electron-dl-manager";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
import { WindowManager } from "../window-manager";
import { Downloader } from "./downloader";
import { GameStatus } from "@globals";
function dropExtension(fileName: string) {
return fileName.split(".").slice(0, -1).join(".");
}
export class HTTPDownloader {
private downloadManager: ElectronDownloadManager;
private downloadId: string | null = null;
constructor() {
this.downloadManager = new ElectronDownloadManager();
}
async download(url: string, destination: string, gameId: number) {
const window = WindowManager.mainWindow;
this.downloadId = await this.downloadManager.download({
url,
window: window!,
callbacks: {
onDownloadStarted: async (ev) => {
const updatePayload: QueryDeepPartialEntity<Game> = {
status: GameStatus.Downloading,
progress: 0,
bytesDownloaded: 0,
fileSize: ev.item.getTotalBytes(),
rarPath: `${destination}/.rd/${ev.resolvedFilename}`,
folderName: dropExtension(ev.resolvedFilename),
};
const downloadStatus = {
numPeers: 0,
numSeeds: 0,
downloadSpeed: 0,
timeRemaining: Number.POSITIVE_INFINITY,
};
await Downloader.updateGameProgress(
gameId,
updatePayload,
downloadStatus
);
},
onDownloadCompleted: async (ev) => {
const updatePayload: QueryDeepPartialEntity<Game> = {
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<Game> = {
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);
}
}
}

View File

@ -0,0 +1,53 @@
export 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;
}
export interface RealDebridAddMagnet {
id: string;
// URL of the created ressource
uri: string;
}
export 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
}

View File

@ -0,0 +1,74 @@
import { userPreferencesRepository } from "@main/repository";
import {
RealDebridAddMagnet,
RealDebridTorrentInfo,
RealDebridUnrestrictLink,
} from "./real-debrid-types";
const base = "https://api.real-debrid.com/rest/1.0";
export class RealDebridClient {
static async addMagnet(magnet: string) {
const response = await fetch(`${base}/torrents/addMagnet`, {
method: "POST",
headers: {
Authorization: `Bearer ${await this.getApiToken()}`,
},
body: `magnet=${encodeURIComponent(magnet)}`,
});
return response.json() as Promise<RealDebridAddMagnet>;
}
static async getInfo(id: string) {
const response = await fetch(`${base}/torrents/info/${id}`, {
headers: {
Authorization: `Bearer ${await this.getApiToken()}`,
},
});
return response.json() as Promise<RealDebridTorrentInfo>;
}
static async selectAllFiles(id: string) {
await fetch(`${base}/torrents/selectFiles/${id}`, {
method: "POST",
headers: {
Authorization: `Bearer ${await this.getApiToken()}`,
},
body: "files=all",
});
}
static async unrestrictLink(link: string) {
const response = await fetch(`${base}/unrestrict/link`, {
method: "POST",
headers: {
Authorization: `Bearer ${await this.getApiToken()}`,
},
body: `link=${link}`,
});
return response.json() as Promise<RealDebridUnrestrictLink>;
}
static async getAllTorrents() {
const response = await fetch(`${base}/torrents`, {
headers: {
Authorization: `Bearer ${await this.getApiToken()}`,
},
});
return response.json() as Promise<RealDebridTorrentInfo[]>;
}
static getApiToken() {
return userPreferencesRepository
.findOne({ where: { id: 1 } })
.then((userPreferences) => userPreferences!.realDebridApiToken);
}
static extractSHA1FromMagnet(magnet: string) {
return magnet.match(/btih:([0-9a-fA-F]*)/)?.[1].toLowerCase();
}
}

View File

@ -2,13 +2,12 @@ import path from "node:path";
import cp from "node:child_process"; import cp from "node:child_process";
import fs from "node:fs"; import fs from "node:fs";
import * as Sentry from "@sentry/electron/main"; 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 type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
import { Game } from "@main/entity"; import { Game } from "@main/entity";
import { gameRepository, userPreferencesRepository } from "@main/repository"; import { Downloader } from "./downloader";
import { t } from "i18next"; import { GameStatus } from "@globals";
import { WindowManager } from "./window-manager";
const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = { const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
darwin: "hydra-download-manager", darwin: "hydra-download-manager",
@ -74,6 +73,7 @@ export class TorrentClient {
__dirname, __dirname,
"..", "..",
"..", "..",
"..",
"torrent-client", "torrent-client",
"main.py" "main.py"
); );
@ -84,18 +84,13 @@ export class TorrentClient {
} }
private static getTorrentStateName(state: TorrentState) { private static getTorrentStateName(state: TorrentState) {
if (state === TorrentState.CheckingFiles) return "checking_files"; if (state === TorrentState.CheckingFiles) return GameStatus.CheckingFiles;
if (state === TorrentState.Downloading) return "downloading"; if (state === TorrentState.Downloading) return GameStatus.Downloading;
if (state === TorrentState.DownloadingMetadata) if (state === TorrentState.DownloadingMetadata)
return "downloading_metadata"; return GameStatus.DownloadingMetadata;
if (state === TorrentState.Finished) return "finished"; if (state === TorrentState.Finished) return GameStatus.Finished;
if (state === TorrentState.Seeding) return "seeding"; if (state === TorrentState.Seeding) return GameStatus.Seeding;
return ""; return null;
}
private static getGameProgress(game: Game) {
if (game.status === "checking_files") return game.fileVerificationProgress;
return game.progress;
} }
public static async onSocketData(data: Buffer) { public static async onSocketData(data: Buffer) {
@ -126,42 +121,12 @@ export class TorrentClient {
updatePayload.progress = payload.progress; updatePayload.progress = payload.progress;
} }
await gameRepository.update({ id: payload.gameId }, updatePayload); Downloader.updateGameProgress(payload.gameId, updatePayload, {
numPeers: payload.numPeers,
const game = await gameRepository.findOne({ numSeeds: payload.numSeeds,
where: { id: payload.gameId }, downloadSpeed: payload.downloadSpeed,
relations: { repack: true }, 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 && game) {
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) { } catch (err) {
Sentry.captureException(err); Sentry.captureException(err);
} }

View File

@ -6,6 +6,6 @@ export * from "./steam-grid";
export * from "./update-resolver"; export * from "./update-resolver";
export * from "./window-manager"; export * from "./window-manager";
export * from "./fifo"; export * from "./fifo";
export * from "./torrent-client"; export * from "./downloaders/torrent-client";
export * from "./how-long-to-beat"; export * from "./how-long-to-beat";
export * from "./process-watcher"; export * from "./process-watcher";

View File

@ -0,0 +1,30 @@
import { Extractor, createExtractorFromFile } from "node-unrar-js";
import fs from "node:fs";
import path from "node:path";
import { app } from "electron";
const wasmPath = app.isPackaged
? path.join(process.resourcesPath, "unrar.wasm")
: path.join(__dirname, "..", "..", "unrar.wasm");
const wasmBinary = fs.readFileSync(require.resolve(wasmPath));
export class Unrar {
private constructor(private extractor: Extractor<Uint8Array>) {}
static async fromFilePath(filePath: string, targetFolder: string) {
const extractor = await createExtractorFromFile({
filepath: filePath,
targetPath: targetFolder,
wasmBinary,
});
return new Unrar(extractor);
}
extract() {
const files = this.extractor.extract().files;
for (const file of files) {
console.log("File:", file.fileHeader.name);
}
}
}

View File

@ -7,6 +7,7 @@ import { vars } from "../../theme.css";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { VERSION_CODENAME } from "@renderer/constants"; import { VERSION_CODENAME } from "@renderer/constants";
import { GameStatus } from "@globals";
export function BottomPanel() { export function BottomPanel() {
const { t } = useTranslation("bottom_panel"); const { t } = useTranslation("bottom_panel");
@ -23,10 +24,10 @@ export function BottomPanel() {
const status = useMemo(() => { const status = useMemo(() => {
if (isDownloading && game) { if (isDownloading && game) {
if (game.status === "downloading_metadata") if (game.status === GameStatus.DownloadingMetadata)
return t("downloading_metadata", { title: game.title }); return t("downloading_metadata", { title: game.title });
if (game.status === "checking_files") if (game.status === GameStatus.CheckingFiles)
return t("checking_files", { return t("checking_files", {
title: game.title, title: game.title,
percentage: progress, percentage: progress,

View File

@ -14,6 +14,7 @@ import DiscordLogo from "@renderer/assets/discord-icon.svg?react";
import XLogo from "@renderer/assets/x-icon.svg?react"; import XLogo from "@renderer/assets/x-icon.svg?react";
import * as styles from "./sidebar.css"; import * as styles from "./sidebar.css";
import { GameStatus } from "@globals";
const SIDEBAR_MIN_WIDTH = 200; const SIDEBAR_MIN_WIDTH = 200;
const SIDEBAR_INITIAL_WIDTH = 250; const SIDEBAR_INITIAL_WIDTH = 250;
@ -60,9 +61,7 @@ export function Sidebar() {
}, [gameDownloading?.id, updateLibrary]); }, [gameDownloading?.id, updateLibrary]);
const isDownloading = library.some((game) => const isDownloading = library.some((game) =>
["downloading", "checking_files", "downloading_metadata"].includes( GameStatus.isDownloading(game.status)
game.status
)
); );
const sidebarRef = useRef<HTMLElement>(null); const sidebarRef = useRef<HTMLElement>(null);
@ -121,15 +120,14 @@ export function Sidebar() {
}, [isResizing]); }, [isResizing]);
const getGameTitle = (game: Game) => { 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) { if (gameDownloading?.id === game.id) {
const isVerifying = ["downloading_metadata", "checking_files"].includes( const isVerifying = GameStatus.isVerifying(gameDownloading.status);
gameDownloading?.status
);
if (isVerifying) if (isVerifying)
return t(gameDownloading.status, { return t(gameDownloading.status!, {
title: game.title, title: game.title,
percentage: progress, percentage: progress,
}); });
@ -204,7 +202,7 @@ export function Sidebar() {
className={styles.menuItem({ className={styles.menuItem({
active: active:
location.pathname === `/game/${game.shop}/${game.objectID}`, location.pathname === `/game/${game.shop}/${game.objectID}`,
muted: game.status === "cancelled", muted: game.status === GameStatus.Cancelled,
})} })}
> >
<button <button

View File

@ -12,6 +12,7 @@ import {
import type { GameShop, TorrentProgress } from "@types"; import type { GameShop, TorrentProgress } from "@types";
import { useDate } from "./use-date"; import { useDate } from "./use-date";
import { formatBytes } from "@renderer/utils"; import { formatBytes } from "@renderer/utils";
import { GameStatus } from "@globals";
export function useDownload() { export function useDownload() {
const { updateLibrary } = useLibrary(); const { updateLibrary } = useLibrary();
@ -63,9 +64,7 @@ export function useDownload() {
updateLibrary(); updateLibrary();
}); });
const isVerifying = ["downloading_metadata", "checking_files"].includes( const isVerifying = GameStatus.isVerifying(lastPacket?.game.status);
lastPacket?.game.status ?? ""
);
const getETA = () => { const getETA = () => {
if (isVerifying || !isFinite(lastPacket?.timeRemaining ?? 0)) { if (isVerifying || !isFinite(lastPacket?.timeRemaining ?? 0)) {
@ -84,8 +83,10 @@ export function useDownload() {
}; };
const getProgress = () => { const getProgress = () => {
if (lastPacket?.game.status === "checking_files") { if (lastPacket?.game.status === GameStatus.CheckingFiles) {
return formatDownloadProgress(lastPacket?.game.fileVerificationProgress); return formatDownloadProgress(lastPacket?.game.fileVerificationProgress);
} else if (lastPacket?.game.status === GameStatus.Decompressing) {
return formatDownloadProgress(lastPacket?.game.decompressionProgress);
} }
return formatDownloadProgress(lastPacket?.game.progress); return formatDownloadProgress(lastPacket?.game.progress);

View File

@ -11,6 +11,7 @@ import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
import * as styles from "./downloads.css"; import * as styles from "./downloads.css";
import { DeleteModal } from "./delete-modal"; import { DeleteModal } from "./delete-modal";
import { formatBytes } from "@renderer/utils"; import { formatBytes } from "@renderer/utils";
import { GameStatus } from "@globals";
export function Downloads() { export function Downloads() {
const { library, updateLibrary } = useLibrary(); const { library, updateLibrary } = useLibrary();
@ -78,7 +79,7 @@ export function Downloads() {
<> <>
<p>{progress}</p> <p>{progress}</p>
{gameDownloading?.status !== "downloading" ? ( {gameDownloading?.status !== GameStatus.Downloading ? (
<p>{t(gameDownloading?.status)}</p> <p>{t(gameDownloading?.status)}</p>
) : ( ) : (
<> <>
@ -95,7 +96,7 @@ export function Downloads() {
); );
} }
if (game?.status === "seeding") { if (GameStatus.isReady(game?.status)) {
return ( return (
<> <>
<p>{game?.repack.title}</p> <p>{game?.repack.title}</p>
@ -103,12 +104,11 @@ export function Downloads() {
</> </>
); );
} }
if (game?.status === GameStatus.Cancelled) return <p>{t("cancelled")}</p>;
if (game?.status === "cancelled") return <p>{t("cancelled")}</p>; if (game?.status === GameStatus.DownloadingMetadata)
if (game?.status === "downloading_metadata")
return <p>{t("starting_download")}</p>; return <p>{t("starting_download")}</p>;
if (game?.status === "paused") { if (game?.status === GameStatus.Paused) {
return ( return (
<> <>
<p>{formatDownloadProgress(game.progress)}</p> <p>{formatDownloadProgress(game.progress)}</p>
@ -143,7 +143,7 @@ export function Downloads() {
); );
} }
if (game?.status === "paused") { if (game?.status === GameStatus.Paused) {
return ( return (
<> <>
<Button onClick={() => resumeDownload(game.id)} theme="outline"> <Button onClick={() => resumeDownload(game.id)} theme="outline">
@ -156,7 +156,7 @@ export function Downloads() {
); );
} }
if (game?.status === "seeding") { if (GameStatus.isReady(game?.status)) {
return ( return (
<> <>
<Button <Button
@ -174,7 +174,7 @@ export function Downloads() {
); );
} }
if (game?.status === "downloading_metadata") { if (game?.status === GameStatus.DownloadingMetadata) {
return ( return (
<Button onClick={() => cancelDownload(game.id)} theme="outline"> <Button onClick={() => cancelDownload(game.id)} theme="outline">
{t("cancel")} {t("cancel")}
@ -239,7 +239,7 @@ export function Downloads() {
<li <li
key={game.id} key={game.id}
className={styles.download({ className={styles.download({
cancelled: game.status === "cancelled", cancelled: game.status === GameStatus.Cancelled,
})} })}
> >
<img <img

View File

@ -1,3 +1,4 @@
import { GameStatus } from "@globals";
import { NoEntryIcon, PlusCircleIcon } from "@primer/octicons-react"; import { NoEntryIcon, PlusCircleIcon } from "@primer/octicons-react";
import { Button } from "@renderer/components"; import { Button } from "@renderer/components";
@ -152,7 +153,7 @@ export function HeroPanelActions({
); );
} }
if (game?.status === "paused") { if (game?.status === GameStatus.Paused) {
return ( return (
<> <>
<Button <Button
@ -173,10 +174,10 @@ export function HeroPanelActions({
); );
} }
if (game?.status === "seeding" || (game && !game.status)) { if (GameStatus.isReady(game?.status) || (game && !game.status)) {
return ( return (
<> <>
{game?.status === "seeding" ? ( {GameStatus.isReady(game?.status) ? (
<Button <Button
onClick={openGameInstaller} onClick={openGameInstaller}
theme="outline" theme="outline"
@ -212,7 +213,7 @@ export function HeroPanelActions({
); );
} }
if (game?.status === "cancelled") { if (game?.status === GameStatus.Cancelled) {
return ( return (
<> <>
<Button <Button

View File

@ -9,6 +9,7 @@ import { formatDownloadProgress } from "@renderer/helpers";
import { useDate } from "@renderer/hooks/use-date"; import { useDate } from "@renderer/hooks/use-date";
import { formatBytes } from "@renderer/utils"; import { formatBytes } from "@renderer/utils";
import { HeroPanelActions } from "./hero-panel-actions"; import { HeroPanelActions } from "./hero-panel-actions";
import { GameStatus } from "@globals";
import { BinaryNotFoundModal } from "../../shared-modals/binary-not-found-modal"; import { BinaryNotFoundModal } from "../../shared-modals/binary-not-found-modal";
import * as styles from "./hero-panel.css"; import * as styles from "./hero-panel.css";
@ -106,7 +107,7 @@ export function HeroPanel({
{eta && <small>{t("eta", { eta })}</small>} {eta && <small>{t("eta", { eta })}</small>}
</p> </p>
{gameDownloading.status !== "downloading" ? ( {gameDownloading.status !== GameStatus.Downloading ? (
<> <>
<p>{t(gameDownloading.status)}</p> <p>{t(gameDownloading.status)}</p>
{eta && <small>{t("eta", { eta })}</small>} {eta && <small>{t("eta", { eta })}</small>}
@ -124,7 +125,7 @@ export function HeroPanel({
); );
} }
if (game?.status === "paused") { if (game?.status === GameStatus.Paused) {
return ( return (
<> <>
<p> <p>
@ -139,7 +140,7 @@ export function HeroPanel({
); );
} }
if (game?.status === "seeding" || (game && !game.status)) { if (GameStatus.isReady(game?.status) || (game && !game.status)) {
if (!game.lastTimePlayed) { if (!game.lastTimePlayed) {
return <p>{t("not_played_yet", { title: game.title })}</p>; return <p>{t("not_played_yet", { title: game.title })}</p>;
} }

View File

@ -11,6 +11,7 @@ export function Settings() {
downloadNotificationsEnabled: false, downloadNotificationsEnabled: false,
repackUpdatesNotificationsEnabled: false, repackUpdatesNotificationsEnabled: false,
telemetryEnabled: false, telemetryEnabled: false,
realDebridApiToken: null as string | null,
preferQuitInsteadOfHiding: false, preferQuitInsteadOfHiding: false,
}); });
@ -28,6 +29,7 @@ export function Settings() {
repackUpdatesNotificationsEnabled: repackUpdatesNotificationsEnabled:
userPreferences?.repackUpdatesNotificationsEnabled ?? false, userPreferences?.repackUpdatesNotificationsEnabled ?? false,
telemetryEnabled: userPreferences?.telemetryEnabled ?? false, telemetryEnabled: userPreferences?.telemetryEnabled ?? false,
realDebridApiToken: userPreferences?.realDebridApiToken ?? null,
preferQuitInsteadOfHiding: preferQuitInsteadOfHiding:
userPreferences?.preferQuitInsteadOfHiding ?? false, userPreferences?.preferQuitInsteadOfHiding ?? false,
}); });
@ -123,6 +125,14 @@ export function Settings() {
) )
} }
/> />
<TextField
label={t("real_debrid_api_token_description")}
value={form.realDebridApiToken ?? ""}
onChange={(event) => {
updateUserPreferences("realDebridApiToken", event.target.value);
}}
/>
</div> </div>
</section> </section>
); );

View File

@ -1,3 +1,5 @@
import { GameStatus } from "@globals";
export type GameShop = "steam" | "epic"; export type GameShop = "steam" | "epic";
export type CatalogueCategory = "recently_added" | "trending"; export type CatalogueCategory = "recently_added" | "trending";
@ -75,13 +77,14 @@ export interface Game extends Omit<CatalogueEntry, "cover"> {
id: number; id: number;
title: string; title: string;
iconUrl: string; iconUrl: string;
status: string; status: GameStatus | null;
folderName: string; folderName: string;
downloadPath: string | null; downloadPath: string | null;
repacks: GameRepack[]; repacks: GameRepack[];
repack: GameRepack; repack: GameRepack;
progress: number; progress: number;
fileVerificationProgress: number; fileVerificationProgress: number;
decompressionProgress: number;
bytesDownloaded: number; bytesDownloaded: number;
playTimeInMilliseconds: number; playTimeInMilliseconds: number;
executablePath: string | null; executablePath: string | null;
@ -105,6 +108,7 @@ export interface UserPreferences {
downloadNotificationsEnabled: boolean; downloadNotificationsEnabled: boolean;
repackUpdatesNotificationsEnabled: boolean; repackUpdatesNotificationsEnabled: boolean;
telemetryEnabled: boolean; telemetryEnabled: boolean;
realDebridApiToken: string | null;
preferQuitInsteadOfHiding: boolean; preferQuitInsteadOfHiding: boolean;
} }

View File

@ -1,6 +1,6 @@
{ {
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json", "extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/locales/index.ts"], "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/locales/index.ts", "src/globals.ts"],
"compilerOptions": { "compilerOptions": {
"module": "ESNext", "module": "ESNext",
"composite": true, "composite": true,
@ -14,7 +14,8 @@
"@renderer/*": ["src/renderer/*"], "@renderer/*": ["src/renderer/*"],
"@types": ["src/types/index.ts"], "@types": ["src/types/index.ts"],
"@locales": ["src/locales/index.ts"], "@locales": ["src/locales/index.ts"],
"@resources": ["src/resources/index.ts"] "@resources": ["src/resources/index.ts"],
"@globals": ["src/globals.ts"]
} }
} }
} }

View File

@ -5,7 +5,8 @@
"src/renderer/src/**/*", "src/renderer/src/**/*",
"src/renderer/src/**/*.tsx", "src/renderer/src/**/*.tsx",
"src/preload/*.d.ts", "src/preload/*.d.ts",
"src/locales/index.ts" "src/locales/index.ts",
"src/globals.ts"
], ],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
@ -16,7 +17,8 @@
"src/renderer/src/*" "src/renderer/src/*"
], ],
"@types": ["src/types/index.ts"], "@types": ["src/types/index.ts"],
"@locales": ["src/locales/index.ts"] "@locales": ["src/locales/index.ts"],
"@globals": ["src/globals.ts"]
} }
} }
} }

View File

@ -2361,6 +2361,14 @@ electron-builder@^24.9.1:
simple-update-notifier "2.0.0" simple-update-notifier "2.0.0"
yargs "^17.6.2" yargs "^17.6.2"
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-publish@24.13.1: electron-publish@24.13.1:
version "24.13.1" version "24.13.1"
resolved "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz" resolved "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz"
@ -2793,6 +2801,21 @@ expand-template@^2.0.3:
resolved "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz" resolved "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz"
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
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"
extract-zip@^2.0.1: extract-zip@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz" resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz"
@ -3607,6 +3630,11 @@ is-path-inside@^3.0.3:
resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== 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-potential-custom-element-name@^1.0.1: is-potential-custom-element-name@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz" resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz"
@ -4046,7 +4074,7 @@ micromatch@^4.0.4:
braces "^3.0.2" braces "^3.0.2"
picomatch "^2.3.1" picomatch "^2.3.1"
mime-db@1.52.0: mime-db@1.52.0, mime-db@^1.28.0:
version "1.52.0" version "1.52.0"
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
@ -4269,6 +4297,11 @@ node-releases@^2.0.14:
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz" resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz"
integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== 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==
normalize-path@^3.0.0, normalize-path@~3.0.0: normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz"
@ -5141,6 +5174,20 @@ snake-case@^3.0.4:
dot-case "^3.0.4" dot-case "^3.0.4"
tslib "^2.0.3" tslib "^2.0.3"
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.2.0: source-map-js@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz"
@ -5615,6 +5662,14 @@ unplugin@1.0.1:
webpack-sources "^3.2.3" webpack-sources "^3.2.3"
webpack-virtual-modules "^0.5.0" 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: update-browserslist-db@^1.0.13:
version "1.0.14" version "1.0.14"
resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.14.tgz" resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.14.tgz"