diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index 72ce14d7..f7553c9c 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -4,56 +4,58 @@ import { TorrentDownloader } from "./torrent-downloader"; import { WindowManager } from "../window-manager"; import { downloadQueueRepository, gameRepository } from "@main/repository"; import { publishDownloadCompleteNotification } from "../notifications"; +import { RealDebridDownloader } from "./real-debrid-downloader"; +import type { DownloadProgress } from "@types"; export class DownloadManager { private static currentDownloader: Downloader | null = null; public static async watchDownloads() { + let status: DownloadProgress | null = null; + if (this.currentDownloader === Downloader.RealDebrid) { - throw new Error(); + status = await RealDebridDownloader.getStatus(); } else { - const status = await TorrentDownloader.getStatus(); + status = await TorrentDownloader.getStatus(); + } - if (status) { - const { gameId, progress } = status; + if (status) { + const { gameId, progress } = status; - const game = await gameRepository.findOne({ - where: { id: gameId, isDeleted: false }, + const game = await gameRepository.findOne({ + where: { id: gameId, isDeleted: false }, + }); + + if (WindowManager.mainWindow && game) { + WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress); + + WindowManager.mainWindow.webContents.send( + "on-download-progress", + JSON.parse( + JSON.stringify({ + ...status, + game, + }) + ) + ); + } + + if (progress === 1 && game) { + publishDownloadCompleteNotification(game); + + await downloadQueueRepository.delete({ game }); + + const [nextQueueItem] = await downloadQueueRepository.find({ + order: { + id: "DESC", + }, + relations: { + game: true, + }, }); - if (WindowManager.mainWindow && game) { - WindowManager.mainWindow.setProgressBar( - progress === 1 ? -1 : progress - ); - - WindowManager.mainWindow.webContents.send( - "on-download-progress", - JSON.parse( - JSON.stringify({ - ...status, - game, - }) - ) - ); - } - - if (status.progress === 1 && game) { - publishDownloadCompleteNotification(game); - - await downloadQueueRepository.delete({ game }); - - const [nextQueueItem] = await downloadQueueRepository.find({ - order: { - id: "DESC", - }, - relations: { - game: true, - }, - }); - - if (nextQueueItem) { - this.resumeDownload(nextQueueItem.game); - } + if (nextQueueItem) { + this.resumeDownload(nextQueueItem.game); } } } @@ -61,7 +63,7 @@ export class DownloadManager { static async pauseDownload() { if (this.currentDownloader === Downloader.RealDebrid) { - throw new Error(); + RealDebridDownloader.pauseDownload(); } else { await TorrentDownloader.pauseDownload(); } @@ -72,7 +74,8 @@ export class DownloadManager { static async resumeDownload(game: Game) { if (game.downloader === Downloader.RealDebrid) { - throw new Error(); + RealDebridDownloader.startDownload(game); + this.currentDownloader = Downloader.RealDebrid; } else { TorrentDownloader.startDownload(game); this.currentDownloader = Downloader.Torrent; @@ -81,7 +84,7 @@ export class DownloadManager { static async cancelDownload(gameId: number) { if (this.currentDownloader === Downloader.RealDebrid) { - throw new Error(); + RealDebridDownloader.cancelDownload(); } else { TorrentDownloader.cancelDownload(gameId); } @@ -92,7 +95,8 @@ export class DownloadManager { static async startDownload(game: Game) { if (game.downloader === Downloader.RealDebrid) { - throw new Error(); + RealDebridDownloader.startDownload(game); + this.currentDownloader = Downloader.RealDebrid; } else { TorrentDownloader.startDownload(game); this.currentDownloader = Downloader.Torrent; diff --git a/src/main/services/download/real-debrid-downloader.ts b/src/main/services/download/real-debrid-downloader.ts new file mode 100644 index 00000000..fd092200 --- /dev/null +++ b/src/main/services/download/real-debrid-downloader.ts @@ -0,0 +1,152 @@ +import fs from "node:fs"; +import path from "node:path"; +import { Game } from "@main/entity"; +import { RealDebridClient } from "../real-debrid"; +import axios, { AxiosProgressEvent } from "axios"; +import { gameRepository } from "@main/repository"; +import { calculateETA } from "./helpers"; +import { DownloadProgress } from "@types"; + +export class RealDebridDownloader { + private static downloadingGame: Game | null = null; + + private static realDebridTorrentId: string | null = null; + private static lastProgressEvent: AxiosProgressEvent | null = null; + private static abortController: AbortController | null = null; + + private static async getRealDebridDownloadUrl() { + if (this.realDebridTorrentId) { + const torrentInfo = await RealDebridClient.getTorrentInfo( + this.realDebridTorrentId + ); + + const { status, links } = torrentInfo; + + if (status === "waiting_files_selection") { + await RealDebridClient.selectAllFiles(this.realDebridTorrentId); + return null; + } + + if (status === "downloaded") { + const [link] = links; + const { download } = await RealDebridClient.unrestrictLink(link); + return decodeURIComponent(download); + } + } + + return null; + } + + public static async getStatus() { + if (this.lastProgressEvent) { + await gameRepository.update( + { id: this.downloadingGame!.id }, + { + bytesDownloaded: this.lastProgressEvent.loaded, + fileSize: this.lastProgressEvent.total, + progress: this.lastProgressEvent.progress, + status: "active", + } + ); + + const progress = { + numPeers: 0, + numSeeds: 0, + downloadSpeed: this.lastProgressEvent.rate, + timeRemaining: calculateETA( + this.lastProgressEvent.total ?? 0, + this.lastProgressEvent.loaded, + this.lastProgressEvent.rate ?? 0 + ), + isDownloadingMetadata: false, + isCheckingFiles: false, + progress: this.lastProgressEvent.progress, + gameId: this.downloadingGame!.id, + } as DownloadProgress; + + if (this.lastProgressEvent.progress === 1) { + this.pauseDownload(); + } + + return progress; + } + + if (this.realDebridTorrentId && this.downloadingGame) { + const torrentInfo = await RealDebridClient.getTorrentInfo( + this.realDebridTorrentId + ); + + const { status } = torrentInfo; + + if (status === "downloaded") { + this.startDownload(this.downloadingGame); + } + + const progress = torrentInfo.progress / 100; + const totalDownloaded = progress * torrentInfo.bytes; + + return { + numPeers: 0, + numSeeds: torrentInfo.seeders, + downloadSpeed: torrentInfo.speed, + timeRemaining: calculateETA( + torrentInfo.bytes, + totalDownloaded, + torrentInfo.speed + ), + isDownloadingMetadata: status === "magnet_conversion", + } as DownloadProgress; + } + + return null; + } + + static async pauseDownload() { + if (this.abortController) { + this.abortController.abort(); + } + + this.abortController = null; + this.realDebridTorrentId = null; + this.lastProgressEvent = null; + this.downloadingGame = null; + } + + static async startDownload(game: Game) { + this.realDebridTorrentId = await RealDebridClient.getTorrentId(game!.uri!); + this.downloadingGame = game; + + const downloadUrl = await this.getRealDebridDownloadUrl(); + + if (downloadUrl) { + this.realDebridTorrentId = null; + this.abortController = new AbortController(); + + const response = await axios.get(downloadUrl, { + responseType: "stream", + signal: this.abortController.signal, + headers: { + Range: `bytes=0-`, + }, + onDownloadProgress: (progressEvent) => { + this.lastProgressEvent = progressEvent; + }, + }); + + const filename = path.win32.basename(downloadUrl); + + const downloadPath = path.join(game.downloadPath!, filename); + + await gameRepository.update( + { id: this.downloadingGame.id }, + { folderName: filename } + ); + + response.data.pipe(fs.createWriteStream(downloadPath, { flags: "a" })); + } + } + + static async cancelDownload() { + return this.pauseDownload(); + } +} diff --git a/src/main/services/download/torrent-downloader.ts b/src/main/services/download/torrent-downloader.ts index 4a7fa660..53b3df04 100644 --- a/src/main/services/download/torrent-downloader.ts +++ b/src/main/services/download/torrent-downloader.ts @@ -8,9 +8,9 @@ import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity import { calculateETA } from "./helpers"; import axios from "axios"; import { - type CancelDownloadPayload, - type StartDownloadPayload, - type PauseDownloadPayload, + CancelDownloadPayload, + StartDownloadPayload, + PauseDownloadPayload, LibtorrentStatus, LibtorrentPayload, } from "./types"; @@ -67,6 +67,7 @@ export class TorrentDownloader { bytesDownloaded, fileSize, progress, + status: "active", }; await gameRepository.update(