feat: updating real-debrid translations

This commit is contained in:
Chubby Granny Chaser
2024-05-29 16:22:30 +01:00
parent 183b85d66a
commit 08a336b392
38 changed files with 284 additions and 228 deletions

View File

@@ -0,0 +1,27 @@
import path from "node:path";
import { spawn } from "node:child_process";
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { app } from "electron";
export const startAria2 = (): Promise<ChildProcessWithoutNullStreams> => {
return new Promise((resolve) => {
const binaryPath = app.isPackaged
? path.join(process.resourcesPath, "aria2", "aria2c")
: path.join(__dirname, "..", "..", "aria2", "aria2c");
const cp = spawn(binaryPath, [
"--enable-rpc",
"--rpc-listen-all",
"--file-allocation=none",
"--allow-overwrite=true",
]);
cp.stdout.on("data", async (data) => {
const msg = Buffer.from(data).toString("utf-8");
if (msg.includes("IPv6 RPC: listening on TCP")) {
resolve(cp);
}
});
});
};

View File

@@ -1,57 +1,39 @@
import Aria2, { StatusResponse } from "aria2";
import { spawn } from "node:child_process";
import { gameRepository, userPreferencesRepository } from "@main/repository";
import path from "node:path";
import { WindowManager } from "./window-manager";
import { RealDebridClient } from "./real-debrid";
import { Notification, app } from "electron";
import { Notification } from "electron";
import { t } from "i18next";
import { Downloader } from "@shared";
import { DownloadProgress } from "@types";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
import { Game } from "@main/entity";
import { startAria2 } from "./aria2";
export class DownloadManager {
private static downloads = new Map<number, string>();
private static connected = false;
private static gid: string | null = null;
private static gameId: number | null = null;
private static game: Game | null = null;
private static realDebridTorrentId: string | null = null;
private static aria2 = new Aria2({});
private static connect(): Promise<boolean> {
return new Promise((resolve) => {
const binaryPath = app.isPackaged
? path.join(process.resourcesPath, "aria2", "aria2c")
: path.join(__dirname, "..", "..", "aria2", "aria2c");
const cp = spawn(binaryPath, [
"--enable-rpc",
"--rpc-listen-all",
"--file-allocation=none",
"--allow-overwrite=true",
]);
cp.stdout.on("data", async (data) => {
const msg = Buffer.from(data).toString("utf-8");
if (msg.includes("IPv6 RPC: listening on TCP")) {
await this.aria2.open();
this.connected = true;
resolve(true);
}
});
});
private static async connect() {
await startAria2();
await this.aria2.open();
this.connected = true;
}
private static getETA(status: StatusResponse) {
const remainingBytes =
Number(status.totalLength) - Number(status.completedLength);
const speed = Number(status.downloadSpeed);
private static getETA(
totalLength: number,
completedLength: number,
speed: number
) {
const remainingBytes = totalLength - completedLength;
if (remainingBytes >= 0 && speed > 0) {
return (remainingBytes / speed) * 1000;
@@ -65,9 +47,7 @@ export class DownloadManager {
where: { id: 1 },
});
if (userPreferences?.downloadNotificationsEnabled && this.gameId) {
const game = await this.getGame(this.gameId);
if (userPreferences?.downloadNotificationsEnabled && this.game) {
new Notification({
title: t("download_complete", {
ns: "notifications",
@@ -76,7 +56,7 @@ export class DownloadManager {
body: t("game_ready_to_install", {
ns: "notifications",
lng: userPreferences.language,
title: game?.title,
title: this.game.title,
}),
}).show();
}
@@ -87,8 +67,73 @@ export class DownloadManager {
return "";
}
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))
);
}
}
return null;
}
public static async watchDownloads() {
if (!this.gid || !this.gameId) return;
if (!this.game) return;
if (!this.gid && this.realDebridTorrentId) {
const options = { dir: this.game.downloadPath! };
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.realDebridTorrentId = null;
}
}
if (!this.gid) return;
const status = await this.aria2.call("tellStatus", this.gid);
@@ -96,7 +141,7 @@ export class DownloadManager {
if (status.followedBy?.length) {
this.gid = status.followedBy[0];
this.downloads.set(this.gameId, this.gid);
this.downloads.set(this.game.id, this.gid);
return;
}
@@ -113,7 +158,7 @@ export class DownloadManager {
if (!isNaN(progress)) update.progress = progress;
await gameRepository.update(
{ id: this.gameId },
{ id: this.game.id },
{
...update,
status: status.status,
@@ -123,17 +168,18 @@ export class DownloadManager {
}
const game = await gameRepository.findOne({
where: { id: this.gameId, isDeleted: false },
where: { id: this.game.id, isDeleted: false },
relations: { repack: true },
});
if (progress === 1 && game && !isDownloadingMetadata) {
if (progress === 1 && this.game && !isDownloadingMetadata) {
await this.publishNotification();
/*
Only cancel bittorrent downloads to stop seeding
*/
if (status.bittorrent) {
await this.cancelDownload(game.id);
await this.cancelDownload(this.game.id);
} else {
this.clearCurrentDownload();
}
@@ -143,13 +189,14 @@ export class DownloadManager {
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
const payload = {
progress,
bytesDownloaded: Number(status.completedLength),
fileSize: Number(status.totalLength),
numPeers: Number(status.connections),
numSeeds: Number(status.numSeeders ?? 0),
downloadSpeed: Number(status.downloadSpeed),
timeRemaining: this.getETA(status),
timeRemaining: this.getETA(
Number(status.totalLength),
Number(status.completedLength),
Number(status.downloadSpeed)
),
isDownloadingMetadata: !!isDownloadingMetadata,
game,
} as DownloadProgress;
@@ -161,20 +208,12 @@ export class DownloadManager {
}
}
static async getGame(gameId: number) {
return gameRepository.findOne({
where: { id: gameId, isDeleted: false },
relations: {
repack: true,
},
});
}
private static clearCurrentDownload() {
if (this.gameId) {
this.downloads.delete(this.gameId);
if (this.game) {
this.downloads.delete(this.game.id);
this.gid = null;
this.gameId = null;
this.game = null;
this.realDebridTorrentId = null;
}
}
@@ -198,50 +237,42 @@ export class DownloadManager {
if (this.gid) {
await this.aria2.call("forcePause", this.gid);
this.gid = null;
this.gameId = null;
this.game = null;
this.realDebridTorrentId = null;
WindowManager.mainWindow?.setProgressBar(-1);
}
}
static async resumeDownload(gameId: number) {
if (this.downloads.has(gameId)) {
const gid = this.downloads.get(gameId)!;
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.gameId = gameId;
this.game = game;
} else {
return this.startDownload(gameId);
return this.startDownload(game);
}
}
static async startDownload(gameId: number) {
static async startDownload(game: Game) {
if (!this.connected) await this.connect();
const game = await this.getGame(gameId)!;
const options = {
dir: game.downloadPath!,
};
if (game) {
const options = {
dir: game.downloadPath!,
};
if (game.downloader === Downloader.RealDebrid) {
this.realDebridTorrentId = await RealDebridClient.getTorrentId(
game!.repack.magnet
);
} else {
this.gid = await this.aria2.call("addUri", [game.repack.magnet], options);
if (game.downloader === Downloader.RealDebrid) {
const downloadUrl = decodeURIComponent(
await RealDebridClient.getDownloadUrl(game)
);
this.gid = await this.aria2.call("addUri", [downloadUrl], options);
} else {
this.gid = await this.aria2.call(
"addUri",
[game.repack.magnet],
options
);
}
this.gameId = gameId;
this.downloads.set(gameId, this.gid);
this.downloads.set(game.id, this.gid);
}
this.game = game;
}
}

View File

@@ -1,5 +1,5 @@
import { Game } from "@main/entity";
import axios, { AxiosInstance } from "axios";
import parseTorrent from "parse-torrent";
import type {
RealDebridAddMagnet,
RealDebridTorrentInfo,
@@ -7,10 +7,18 @@ import type {
RealDebridUser,
} from "@types";
const base = "https://api.real-debrid.com/rest/1.0";
export class RealDebridClient {
private static instance: AxiosInstance;
private static baseURL = "https://api.real-debrid.com/rest/1.0";
static authorize(apiToken: string) {
this.instance = axios.create({
baseURL: this.baseURL,
headers: {
Authorization: `Bearer ${apiToken}`,
},
});
}
static async addMagnet(magnet: string) {
const searchParams = new URLSearchParams({ magnet });
@@ -23,7 +31,7 @@ export class RealDebridClient {
return response.data;
}
static async getInfo(id: string) {
static async getTorrentInfo(id: string) {
const response = await this.instance.get<RealDebridTorrentInfo>(
`/torrents/info/${id}`
);
@@ -55,50 +63,24 @@ export class RealDebridClient {
return response.data;
}
static async getAllTorrentsFromUser() {
private static async getAllTorrentsFromUser() {
const response =
await this.instance.get<RealDebridTorrentInfo[]>("/torrents");
return response.data;
}
static extractSHA1FromMagnet(magnet: string) {
return magnet.match(/btih:([0-9a-fA-F]*)/)?.[1].toLowerCase();
}
static async getTorrentId(magnetUri: string) {
const userTorrents = await RealDebridClient.getAllTorrentsFromUser();
static async getDownloadUrl(game: Game) {
const torrents = await RealDebridClient.getAllTorrentsFromUser();
const hash = RealDebridClient.extractSHA1FromMagnet(game!.repack.magnet);
let torrent = torrents.find((t) => t.hash === hash);
const { infoHash } = await parseTorrent(magnetUri);
const userTorrent = userTorrents.find(
(userTorrent) => userTorrent.hash === infoHash
);
// User haven't downloaded this torrent yet
if (!torrent) {
const magnet = await RealDebridClient.addMagnet(game!.repack.magnet);
if (userTorrent) return userTorrent.id;
if (magnet) {
await RealDebridClient.selectAllFiles(magnet.id);
torrent = await RealDebridClient.getInfo(magnet.id);
const { links } = torrent;
const { download } = await RealDebridClient.unrestrictLink(links[0]);
if (!download) {
throw new Error("Torrent not cached on Real Debrid");
}
return download;
}
}
throw new Error();
}
static authorize(apiToken: string) {
this.instance = axios.create({
baseURL: base,
headers: {
Authorization: `Bearer ${apiToken}`,
},
});
const torrent = await RealDebridClient.addMagnet(magnetUri);
return torrent.id;
}
}