From df839eddaf4a6665e2fb6243c7fbf13d16a6aa50 Mon Sep 17 00:00:00 2001 From: Andy <39628283+Rxflex@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:33:19 +0200 Subject: [PATCH] wip --- src/main/services/download-manager.ts | 351 +++++++++++--------------- 1 file changed, 154 insertions(+), 197 deletions(-) diff --git a/src/main/services/download-manager.ts b/src/main/services/download-manager.ts index 379e25cd..326b086f 100644 --- a/src/main/services/download-manager.ts +++ b/src/main/services/download-manager.ts @@ -1,58 +1,22 @@ -import Aria2, { StatusResponse } from "aria2"; - +import WebTorrent, { Torrent } from "webtorrent"; import path from "node:path"; - import { downloadQueueRepository, gameRepository } from "@main/repository"; - import { WindowManager } from "./window-manager"; import { RealDebridClient } from "./real-debrid"; - import { Downloader } from "@shared"; import { DownloadProgress } from "@types"; import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; import { Game } from "@main/entity"; -import { startAria2 } from "./aria2c"; import { sleep } from "@main/helpers"; import { logger } from "./logger"; -import type { ChildProcess } from "node:child_process"; import { publishDownloadCompleteNotification } from "./notifications"; export class DownloadManager { - private static downloads = new Map(); - - private static connected = false; - private static gid: string | null = null; + private static downloads = new Map(); + private static client = new WebTorrent(); private static game: Game | null = null; private static realDebridTorrentId: string | null = null; - private static aria2c: ChildProcess | null = null; - - private static aria2 = new Aria2({}); - - private static async connect() { - this.aria2c = startAria2(); - - let retries = 0; - - while (retries < 4 && !this.connected) { - try { - await this.aria2.open(); - logger.log("Connected to aria2"); - - this.connected = true; - } catch (err) { - await sleep(100); - logger.log("Failed to connect to aria2, retrying..."); - retries++; - } - } - } - - public static disconnect() { - if (this.aria2c) { - this.aria2c.kill(); - this.connected = false; - } - } + private static statusUpdateInterval: NodeJS.Timeout | null = null; private static getETA( totalLength: number, @@ -60,141 +24,118 @@ export class DownloadManager { speed: number ) { const remainingBytes = totalLength - completedLength; - if (remainingBytes >= 0 && speed > 0) { return (remainingBytes / speed) * 1000; } - return -1; } - private static getFolderName(status: StatusResponse) { - if (status.bittorrent?.info) return status.bittorrent.info.name; - - const [file] = status.files; - if (file) return path.win32.basename(file.path); - - return null; + private static getFolderName(torrent: Torrent) { + return torrent.name; } 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); - } - - if (WindowManager.mainWindow) { - const progress = torrentInfo.progress / 100; - const totalDownloaded = progress * torrentInfo.bytes; - - WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress); - - const payload = { - numPeers: 0, - numSeeds: torrentInfo.seeders, - downloadSpeed: torrentInfo.speed, - timeRemaining: this.getETA( - torrentInfo.bytes, - totalDownloaded, - torrentInfo.speed - ), - isDownloadingMetadata: status === "magnet_conversion", - game: { - ...this.game, - bytesDownloaded: progress * torrentInfo.bytes, - progress, - }, - } as DownloadProgress; - - WindowManager.mainWindow.webContents.send( - "on-download-progress", - JSON.parse(JSON.stringify(payload)) + try { + 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); + } + + if (WindowManager.mainWindow) { + const progress = torrentInfo.progress / 100; + const totalDownloaded = progress * torrentInfo.bytes; + + WindowManager.mainWindow.setProgressBar( + progress === 1 ? -1 : progress + ); + + const payload = { + numPeers: 0, + numSeeds: torrentInfo.seeders, + downloadSpeed: torrentInfo.speed, + timeRemaining: this.getETA( + torrentInfo.bytes, + totalDownloaded, + torrentInfo.speed + ), + isDownloadingMetadata: status === "magnet_conversion", + game: { + ...this.game, + bytesDownloaded: progress * torrentInfo.bytes, + progress, + }, + } as DownloadProgress; + + WindowManager.mainWindow.webContents.send( + "on-download-progress", + JSON.parse(JSON.stringify(payload)) + ); + } + } + } catch (error) { + logger.error("Error getting RealDebrid download URL:", error); + } return null; } - public static async watchDownloads() { + private static async updateDownloadStatus() { if (!this.game) return; - if (!this.gid && this.realDebridTorrentId) { - const options = { dir: this.game.downloadPath! }; + if (!this.downloads.has(this.game.id) && this.realDebridTorrentId) { const downloadUrl = await this.getRealDebridDownloadUrl(); - if (downloadUrl) { - this.gid = await this.aria2.call("addUri", [downloadUrl], options); - this.downloads.set(this.game.id, this.gid); + this.startDownloadFromUrl(downloadUrl); this.realDebridTorrentId = null; } } - if (!this.gid) return; + if (!this.downloads.has(this.game.id)) return; - const status = await this.aria2.call("tellStatus", this.gid); + const torrent = this.downloads.get(this.game.id)!; + const progress = torrent.progress; + const status = torrent.done ? "complete" : "downloading"; - const isDownloadingMetadata = status.bittorrent && !status.bittorrent?.info; + const update: QueryDeepPartialEntity = { + bytesDownloaded: torrent.downloaded, + fileSize: torrent.length, + status: status, + progress: progress, + }; - if (status.followedBy?.length) { - this.gid = status.followedBy[0]; - this.downloads.set(this.game.id, this.gid); - return; - } - - const progress = - Number(status.completedLength) / Number(status.totalLength); - - if (!isDownloadingMetadata) { - const update: QueryDeepPartialEntity = { - bytesDownloaded: Number(status.completedLength), - fileSize: Number(status.totalLength), - status: status.status, - }; - - if (!isNaN(progress)) update.progress = progress; - - await gameRepository.update( - { id: this.game.id }, - { - ...update, - status: status.status, - folderName: this.getFolderName(status), - } - ); - } + await gameRepository.update( + { id: this.game.id }, + { ...update, status, folderName: this.getFolderName(torrent) } + ); const game = await gameRepository.findOne({ where: { id: this.game.id, isDeleted: false }, }); if (WindowManager.mainWindow && game) { - if (!isNaN(progress)) - WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress); + WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress); const payload = { - numPeers: Number(status.connections), - numSeeds: Number(status.numSeeders ?? 0), - downloadSpeed: Number(status.downloadSpeed), + numPeers: torrent.numPeers, + numSeeds: torrent.numPeers, // WebTorrent doesn't differentiate between seeds and peers + downloadSpeed: torrent.downloadSpeed, timeRemaining: this.getETA( - Number(status.totalLength), - Number(status.completedLength), - Number(status.downloadSpeed) + torrent.length, + torrent.downloaded, + torrent.downloadSpeed ), - isDownloadingMetadata: !!isDownloadingMetadata, + isDownloadingMetadata: false, game, } as DownloadProgress; @@ -204,27 +145,14 @@ export class DownloadManager { ); } - if (progress === 1 && this.game && !isDownloadingMetadata) { + if (progress === 1 && this.game) { publishDownloadCompleteNotification(this.game); - await downloadQueueRepository.delete({ game: this.game }); - - /* - Only cancel bittorrent downloads to stop seeding - */ - if (status.bittorrent) { - await this.cancelDownload(this.game.id); - } else { - this.clearCurrentDownload(); - } + this.clearCurrentDownload(); const [nextQueueItem] = await downloadQueueRepository.find({ - order: { - id: "DESC", - }, - relations: { - game: true, - }, + order: { id: "DESC" }, + relations: { game: true }, }); if (nextQueueItem) { @@ -236,69 +164,98 @@ export class DownloadManager { private static clearCurrentDownload() { if (this.game) { this.downloads.delete(this.game.id); - this.gid = null; this.game = null; this.realDebridTorrentId = null; } } static async cancelDownload(gameId: number) { - const gid = this.downloads.get(gameId); - - if (gid) { - await this.aria2.call("forceRemove", gid); - - if (this.gid === gid) { - this.clearCurrentDownload(); - - WindowManager.mainWindow?.setProgressBar(-1); - } else { + try { + const torrent = this.downloads.get(gameId); + if (torrent) { + torrent.destroy(); this.downloads.delete(gameId); } + } catch (error) { + logger.error("Error canceling download:", error); } } static async pauseDownload() { - if (this.gid) { - await this.aria2.call("forcePause", this.gid); - this.gid = null; + if (this.game) { + const torrent = this.downloads.get(this.game.id); + if (torrent) { + torrent.pause(); + } + this.game = null; + this.realDebridTorrentId = null; + WindowManager.mainWindow?.setProgressBar(-1); + if (this.statusUpdateInterval) { + clearInterval(this.statusUpdateInterval); + this.statusUpdateInterval = null; + } } - - this.game = null; - this.realDebridTorrentId = null; - - WindowManager.mainWindow?.setProgressBar(-1); } static async resumeDownload(game: Game) { - if (this.downloads.has(game.id)) { - const gid = this.downloads.get(game.id)!; - await this.aria2.call("unpause", gid); - - this.gid = gid; - this.game = game; - this.realDebridTorrentId = null; - } else { - return this.startDownload(game); + try { + if (this.downloads.has(game.id)) { + const torrent = this.downloads.get(game.id)!; + torrent.resume(); + this.game = game; + this.realDebridTorrentId = null; + this.startStatusUpdateInterval(); + } else { + return this.startDownload(game); + } + } catch (error) { + logger.error("Error resuming download:", error); } } static async startDownload(game: Game) { - if (!this.connected) await this.connect(); + try { + const options = { path: game.downloadPath! }; - const options = { - dir: game.downloadPath!, - }; + if (game.downloader === Downloader.RealDebrid) { + this.realDebridTorrentId = await RealDebridClient.getTorrentId( + game.uri! + ); + } else { + this.startDownloadFromUrl(game.uri!, options); + } - if (game.downloader === Downloader.RealDebrid) { - this.realDebridTorrentId = await RealDebridClient.getTorrentId( - game!.uri! - ); - } else { - this.gid = await this.aria2.call("addUri", [game.uri!], options); - this.downloads.set(game.id, this.gid); + this.game = game; + this.startStatusUpdateInterval(); + } catch (error) { + logger.error("Error starting download:", error); } + } - this.game = game; + private static startDownloadFromUrl(url: string, options?: any) { + this.client.add(url, options, (torrent) => { + this.downloads.set(this.game!.id, torrent); + + torrent.on("download", () => { + // We handle status updates with a setInterval now + }); + + torrent.on("done", () => { + this.updateDownloadStatus(); + }); + + torrent.on("error", (err) => { + logger.error("Torrent error:", err); + }); + }); + } + + private static startStatusUpdateInterval() { + if (this.statusUpdateInterval) { + clearInterval(this.statusUpdateInterval); + } + this.statusUpdateInterval = setInterval(() => { + this.updateDownloadStatus(); + }, 5000); // Update every 5 seconds } }