From 666b1afcb62a3a4d6b324919539b5a3e5a1b2e54 Mon Sep 17 00:00:00 2001 From: lilezek Date: Mon, 29 Apr 2024 20:57:04 +0200 Subject: [PATCH] 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";