This commit is contained in:
Andy 2024-06-26 16:33:19 +02:00 committed by GitHub
parent 94284a427f
commit df839eddaf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,58 +1,22 @@
import Aria2, { StatusResponse } from "aria2"; import WebTorrent, { Torrent } from "webtorrent";
import path from "node:path"; import path from "node:path";
import { downloadQueueRepository, gameRepository } from "@main/repository"; import { downloadQueueRepository, gameRepository } from "@main/repository";
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
import { RealDebridClient } from "./real-debrid"; import { RealDebridClient } from "./real-debrid";
import { Downloader } from "@shared"; import { Downloader } from "@shared";
import { DownloadProgress } from "@types"; import { DownloadProgress } from "@types";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
import { Game } from "@main/entity"; import { Game } from "@main/entity";
import { startAria2 } from "./aria2c";
import { sleep } from "@main/helpers"; import { sleep } from "@main/helpers";
import { logger } from "./logger"; import { logger } from "./logger";
import type { ChildProcess } from "node:child_process";
import { publishDownloadCompleteNotification } from "./notifications"; import { publishDownloadCompleteNotification } from "./notifications";
export class DownloadManager { export class DownloadManager {
private static downloads = new Map<number, string>(); private static downloads = new Map<number, Torrent>();
private static client = new WebTorrent();
private static connected = false;
private static gid: string | null = null;
private static game: Game | null = null; private static game: Game | null = null;
private static realDebridTorrentId: string | null = null; private static realDebridTorrentId: string | null = null;
private static aria2c: ChildProcess | null = null; private static statusUpdateInterval: NodeJS.Timeout | 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 getETA( private static getETA(
totalLength: number, totalLength: number,
@ -60,29 +24,22 @@ export class DownloadManager {
speed: number speed: number
) { ) {
const remainingBytes = totalLength - completedLength; const remainingBytes = totalLength - completedLength;
if (remainingBytes >= 0 && speed > 0) { if (remainingBytes >= 0 && speed > 0) {
return (remainingBytes / speed) * 1000; return (remainingBytes / speed) * 1000;
} }
return -1; return -1;
} }
private static getFolderName(status: StatusResponse) { private static getFolderName(torrent: Torrent) {
if (status.bittorrent?.info) return status.bittorrent.info.name; return torrent.name;
const [file] = status.files;
if (file) return path.win32.basename(file.path);
return null;
} }
private static async getRealDebridDownloadUrl() { private static async getRealDebridDownloadUrl() {
try {
if (this.realDebridTorrentId) { if (this.realDebridTorrentId) {
const torrentInfo = await RealDebridClient.getTorrentInfo( const torrentInfo = await RealDebridClient.getTorrentInfo(
this.realDebridTorrentId this.realDebridTorrentId
); );
const { status, links } = torrentInfo; const { status, links } = torrentInfo;
if (status === "waiting_files_selection") { if (status === "waiting_files_selection") {
@ -100,7 +57,9 @@ export class DownloadManager {
const progress = torrentInfo.progress / 100; const progress = torrentInfo.progress / 100;
const totalDownloaded = progress * torrentInfo.bytes; const totalDownloaded = progress * torrentInfo.bytes;
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress); WindowManager.mainWindow.setProgressBar(
progress === 1 ? -1 : progress
);
const payload = { const payload = {
numPeers: 0, numPeers: 0,
@ -125,76 +84,58 @@ export class DownloadManager {
); );
} }
} }
} catch (error) {
logger.error("Error getting RealDebrid download URL:", error);
}
return null; return null;
} }
public static async watchDownloads() { private static async updateDownloadStatus() {
if (!this.game) return; if (!this.game) return;
if (!this.gid && this.realDebridTorrentId) { if (!this.downloads.has(this.game.id) && this.realDebridTorrentId) {
const options = { dir: this.game.downloadPath! };
const downloadUrl = await this.getRealDebridDownloadUrl(); const downloadUrl = await this.getRealDebridDownloadUrl();
if (downloadUrl) { if (downloadUrl) {
this.gid = await this.aria2.call("addUri", [downloadUrl], options); this.startDownloadFromUrl(downloadUrl);
this.downloads.set(this.game.id, this.gid);
this.realDebridTorrentId = null; 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;
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<Game> = { const update: QueryDeepPartialEntity<Game> = {
bytesDownloaded: Number(status.completedLength), bytesDownloaded: torrent.downloaded,
fileSize: Number(status.totalLength), fileSize: torrent.length,
status: status.status, status: status,
progress: progress,
}; };
if (!isNaN(progress)) update.progress = progress;
await gameRepository.update( await gameRepository.update(
{ id: this.game.id }, { id: this.game.id },
{ { ...update, status, folderName: this.getFolderName(torrent) }
...update,
status: status.status,
folderName: this.getFolderName(status),
}
); );
}
const game = await gameRepository.findOne({ const game = await gameRepository.findOne({
where: { id: this.game.id, isDeleted: false }, where: { id: this.game.id, isDeleted: false },
}); });
if (WindowManager.mainWindow && game) { if (WindowManager.mainWindow && game) {
if (!isNaN(progress))
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress); WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
const payload = { const payload = {
numPeers: Number(status.connections), numPeers: torrent.numPeers,
numSeeds: Number(status.numSeeders ?? 0), numSeeds: torrent.numPeers, // WebTorrent doesn't differentiate between seeds and peers
downloadSpeed: Number(status.downloadSpeed), downloadSpeed: torrent.downloadSpeed,
timeRemaining: this.getETA( timeRemaining: this.getETA(
Number(status.totalLength), torrent.length,
Number(status.completedLength), torrent.downloaded,
Number(status.downloadSpeed) torrent.downloadSpeed
), ),
isDownloadingMetadata: !!isDownloadingMetadata, isDownloadingMetadata: false,
game, game,
} as DownloadProgress; } as DownloadProgress;
@ -204,27 +145,14 @@ export class DownloadManager {
); );
} }
if (progress === 1 && this.game && !isDownloadingMetadata) { if (progress === 1 && this.game) {
publishDownloadCompleteNotification(this.game); publishDownloadCompleteNotification(this.game);
await downloadQueueRepository.delete({ game: 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({ const [nextQueueItem] = await downloadQueueRepository.find({
order: { order: { id: "DESC" },
id: "DESC", relations: { game: true },
},
relations: {
game: true,
},
}); });
if (nextQueueItem) { if (nextQueueItem) {
@ -236,69 +164,98 @@ export class DownloadManager {
private static clearCurrentDownload() { private static clearCurrentDownload() {
if (this.game) { if (this.game) {
this.downloads.delete(this.game.id); this.downloads.delete(this.game.id);
this.gid = null;
this.game = null; this.game = null;
this.realDebridTorrentId = null; this.realDebridTorrentId = null;
} }
} }
static async cancelDownload(gameId: number) { static async cancelDownload(gameId: number) {
const gid = this.downloads.get(gameId); try {
const torrent = this.downloads.get(gameId);
if (gid) { if (torrent) {
await this.aria2.call("forceRemove", gid); torrent.destroy();
if (this.gid === gid) {
this.clearCurrentDownload();
WindowManager.mainWindow?.setProgressBar(-1);
} else {
this.downloads.delete(gameId); this.downloads.delete(gameId);
} }
} catch (error) {
logger.error("Error canceling download:", error);
} }
} }
static async pauseDownload() { static async pauseDownload() {
if (this.gid) { if (this.game) {
await this.aria2.call("forcePause", this.gid); const torrent = this.downloads.get(this.game.id);
this.gid = null; if (torrent) {
torrent.pause();
} }
this.game = null; this.game = null;
this.realDebridTorrentId = null; this.realDebridTorrentId = null;
WindowManager.mainWindow?.setProgressBar(-1); WindowManager.mainWindow?.setProgressBar(-1);
if (this.statusUpdateInterval) {
clearInterval(this.statusUpdateInterval);
this.statusUpdateInterval = null;
}
}
} }
static async resumeDownload(game: Game) { static async resumeDownload(game: Game) {
try {
if (this.downloads.has(game.id)) { if (this.downloads.has(game.id)) {
const gid = this.downloads.get(game.id)!; const torrent = this.downloads.get(game.id)!;
await this.aria2.call("unpause", gid); torrent.resume();
this.gid = gid;
this.game = game; this.game = game;
this.realDebridTorrentId = null; this.realDebridTorrentId = null;
this.startStatusUpdateInterval();
} else { } else {
return this.startDownload(game); return this.startDownload(game);
} }
} catch (error) {
logger.error("Error resuming download:", error);
}
} }
static async startDownload(game: Game) { 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) { if (game.downloader === Downloader.RealDebrid) {
this.realDebridTorrentId = await RealDebridClient.getTorrentId( this.realDebridTorrentId = await RealDebridClient.getTorrentId(
game!.uri! game.uri!
); );
} else { } else {
this.gid = await this.aria2.call("addUri", [game.uri!], options); this.startDownloadFromUrl(game.uri!, options);
this.downloads.set(game.id, this.gid);
} }
this.game = game; this.game = game;
this.startStatusUpdateInterval();
} catch (error) {
logger.error("Error starting download:", error);
}
}
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
} }
} }