Merge pull request #807 from hydralauncher/feature/adding-generic-http-downloads

feat: adding generic http downloads
This commit is contained in:
Chubby Granny Chaser 2024-08-15 20:53:36 +01:00 committed by GitHub
commit c549d53492
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 561 additions and 407 deletions

1
.gitignore vendored
View File

@ -1,7 +1,6 @@
.vscode .vscode
node_modules node_modules
hydra-download-manager/ hydra-download-manager/
aria2/
fastlist.exe fastlist.exe
__pycache__ __pycache__
dist dist

View File

@ -3,7 +3,6 @@ productName: Hydra
directories: directories:
buildResources: build buildResources: build
extraResources: extraResources:
- aria2
- hydra-download-manager - hydra-download-manager
- seeds - seeds
- from: node_modules/create-desktop-shortcuts/src/windows.vbs - from: node_modules/create-desktop-shortcuts/src/windows.vbs

View File

@ -23,7 +23,7 @@
"start": "electron-vite preview", "start": "electron-vite preview",
"dev": "electron-vite dev", "dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build", "build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps && node ./postinstall.cjs", "postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir", "build:unpack": "npm run build && electron-builder --dir",
"build:win": "electron-vite build && electron-builder --win", "build:win": "electron-vite build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac", "build:mac": "electron-vite build && electron-builder --mac",
@ -34,15 +34,13 @@
"dependencies": { "dependencies": {
"@electron-toolkit/preload": "^3.0.0", "@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0", "@electron-toolkit/utils": "^3.0.0",
"@fontsource/fira-mono": "^5.0.13", "@fontsource/noto-sans": "^5.0.22",
"@fontsource/fira-sans": "^5.0.20",
"@primer/octicons-react": "^19.9.0", "@primer/octicons-react": "^19.9.0",
"@reduxjs/toolkit": "^2.2.3", "@reduxjs/toolkit": "^2.2.3",
"@sentry/electron": "^5.1.0", "@sentry/electron": "^5.1.0",
"@vanilla-extract/css": "^1.14.2", "@vanilla-extract/css": "^1.14.2",
"@vanilla-extract/dynamic": "^2.1.1", "@vanilla-extract/dynamic": "^2.1.1",
"@vanilla-extract/recipes": "^0.5.2", "@vanilla-extract/recipes": "^0.5.2",
"aria2": "^4.1.2",
"auto-launch": "^5.0.6", "auto-launch": "^5.0.6",
"axios": "^1.6.8", "axios": "^1.6.8",
"better-sqlite3": "^9.5.0", "better-sqlite3": "^9.5.0",
@ -97,7 +95,7 @@
"@types/user-agents": "^1.0.4", "@types/user-agents": "^1.0.4",
"@vanilla-extract/vite-plugin": "^4.0.7", "@vanilla-extract/vite-plugin": "^4.0.7",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"electron": "^30.0.9", "electron": "^30.3.0",
"electron-builder": "^24.9.1", "electron-builder": "^24.9.1",
"electron-vite": "^2.0.0", "electron-vite": "^2.0.0",
"eslint": "^8.56.0", "eslint": "^8.56.0",

View File

@ -1,50 +0,0 @@
const { default: axios } = require("axios");
const util = require("node:util");
const fs = require("node:fs");
const exec = util.promisify(require("node:child_process").exec);
const downloadAria2 = async () => {
if (fs.existsSync("aria2")) {
console.log("Aria2 already exists, skipping download...");
return;
}
const file =
process.platform === "win32"
? "aria2-1.37.0-win-64bit-build1.zip"
: "aria2-1.37.0-1-x86_64.pkg.tar.zst";
const downloadUrl =
process.platform === "win32"
? `https://github.com/aria2/aria2/releases/download/release-1.37.0/${file}`
: "https://archlinux.org/packages/extra/x86_64/aria2/download/";
console.log(`Downloading ${file}...`);
const response = await axios.get(downloadUrl, { responseType: "stream" });
const stream = response.data.pipe(fs.createWriteStream(file));
stream.on("finish", async () => {
console.log(`Downloaded ${file}, extracting...`);
if (process.platform === "win32") {
await exec(`npx extract-zip ${file}`);
console.log("Extracted. Renaming folder...");
fs.renameSync(file.replace(".zip", ""), "aria2");
} else {
await exec(`tar --zstd -xvf ${file} usr/bin/aria2c`);
console.log("Extracted. Copying binary file...");
fs.mkdirSync("aria2");
fs.copyFileSync("usr/bin/aria2c", "aria2/aria2c");
fs.rmSync("usr", { recursive: true });
}
console.log(`Extracted ${file}, removing compressed downloaded file...`);
fs.rmSync(file);
});
};
downloadAria2();

View File

@ -1,80 +0,0 @@
declare module "aria2" {
export type Aria2Status =
| "active"
| "waiting"
| "paused"
| "error"
| "complete"
| "removed";
export interface StatusResponse {
gid: string;
status: Aria2Status;
totalLength: string;
completedLength: string;
uploadLength: string;
bitfield: string;
downloadSpeed: string;
uploadSpeed: string;
infoHash?: string;
numSeeders?: string;
seeder?: boolean;
pieceLength: string;
numPieces: string;
connections: string;
errorCode?: string;
errorMessage?: string;
followedBy?: string[];
following: string;
belongsTo: string;
dir: string;
files: {
path: string;
length: string;
completedLength: string;
selected: string;
}[];
bittorrent?: {
announceList: string[][];
comment: string;
creationDate: string;
mode: "single" | "multi";
info: {
name: string;
verifiedLength: string;
verifyIntegrityPending: string;
};
};
}
export default class Aria2 {
constructor(options: any);
open: () => Promise<void>;
call(
method: "addUri",
uris: string[],
options: { dir: string }
): Promise<string>;
call(
method: "tellStatus",
gid: string,
keys?: string[]
): Promise<StatusResponse>;
call(method: "pause", gid: string): Promise<string>;
call(method: "forcePause", gid: string): Promise<string>;
call(method: "unpause", gid: string): Promise<string>;
call(method: "remove", gid: string): Promise<string>;
call(method: "forceRemove", gid: string): Promise<string>;
call(method: "pauseAll"): Promise<string>;
call(method: "forcePauseAll"): Promise<string>;
listNotifications: () => [
"onDownloadStart",
"onDownloadPause",
"onDownloadStop",
"onDownloadComplete",
"onDownloadError",
"onBtDownloadComplete",
];
on: (event: string, callback: (params: any) => void) => void;
}
}

View File

@ -26,6 +26,8 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
/* Disconnects libtorrent */ /* Disconnects libtorrent */
PythonInstance.killTorrent(); PythonInstance.killTorrent();
HydraApi.handleSignOut();
await Promise.all([ await Promise.all([
databaseOperations, databaseOperations,
HydraApi.post("/auth/logout").catch(() => {}), HydraApi.post("/auth/logout").catch(() => {}),

View File

@ -22,8 +22,9 @@ const loadState = async (userPreferences: UserPreferences | null) => {
import("./events"); import("./events");
if (userPreferences?.realDebridApiToken) if (userPreferences?.realDebridApiToken) {
RealDebridClient.authorize(userPreferences?.realDebridApiToken); RealDebridClient.authorize(userPreferences?.realDebridApiToken);
}
HydraApi.setupApi().then(() => { HydraApi.setupApi().then(() => {
uploadGamesBatch(); uploadGamesBatch();

View File

@ -1,20 +0,0 @@
import path from "node:path";
import { spawn } from "node:child_process";
import { app } from "electron";
export const startAria2 = () => {
const binaryPath = app.isPackaged
? path.join(process.resourcesPath, "aria2", "aria2c")
: path.join(__dirname, "..", "..", "aria2", "aria2c");
return spawn(
binaryPath,
[
"--enable-rpc",
"--rpc-listen-all",
"--file-allocation=none",
"--allow-overwrite=true",
],
{ stdio: "inherit", windowsHide: true }
);
};

View File

@ -6,6 +6,8 @@ import { downloadQueueRepository, gameRepository } from "@main/repository";
import { publishDownloadCompleteNotification } from "../notifications"; import { publishDownloadCompleteNotification } from "../notifications";
import { RealDebridDownloader } from "./real-debrid-downloader"; import { RealDebridDownloader } from "./real-debrid-downloader";
import type { DownloadProgress } from "@types"; import type { DownloadProgress } from "@types";
import { GofileApi } from "../hosters";
import { GenericHTTPDownloader } from "./generic-http-downloader";
export class DownloadManager { export class DownloadManager {
private static currentDownloader: Downloader | null = null; private static currentDownloader: Downloader | null = null;
@ -13,10 +15,12 @@ export class DownloadManager {
public static async watchDownloads() { public static async watchDownloads() {
let status: DownloadProgress | null = null; let status: DownloadProgress | null = null;
if (this.currentDownloader === Downloader.RealDebrid) { if (this.currentDownloader === Downloader.Torrent) {
status = await PythonInstance.getStatus();
} else if (this.currentDownloader === Downloader.RealDebrid) {
status = await RealDebridDownloader.getStatus(); status = await RealDebridDownloader.getStatus();
} else { } else {
status = await PythonInstance.getStatus(); status = await GenericHTTPDownloader.getStatus();
} }
if (status) { if (status) {
@ -62,10 +66,12 @@ export class DownloadManager {
} }
static async pauseDownload() { static async pauseDownload() {
if (this.currentDownloader === Downloader.RealDebrid) { if (this.currentDownloader === Downloader.Torrent) {
await PythonInstance.pauseDownload();
} else if (this.currentDownloader === Downloader.RealDebrid) {
await RealDebridDownloader.pauseDownload(); await RealDebridDownloader.pauseDownload();
} else { } else {
await PythonInstance.pauseDownload(); await GenericHTTPDownloader.pauseDownload();
} }
WindowManager.mainWindow?.setProgressBar(-1); WindowManager.mainWindow?.setProgressBar(-1);
@ -73,20 +79,16 @@ export class DownloadManager {
} }
static async resumeDownload(game: Game) { static async resumeDownload(game: Game) {
if (game.downloader === Downloader.RealDebrid) { return this.startDownload(game);
RealDebridDownloader.startDownload(game);
this.currentDownloader = Downloader.RealDebrid;
} else {
PythonInstance.startDownload(game);
this.currentDownloader = Downloader.Torrent;
}
} }
static async cancelDownload(gameId: number) { static async cancelDownload(gameId: number) {
if (this.currentDownloader === Downloader.RealDebrid) { if (this.currentDownloader === Downloader.Torrent) {
PythonInstance.cancelDownload(gameId);
} else if (this.currentDownloader === Downloader.RealDebrid) {
RealDebridDownloader.cancelDownload(gameId); RealDebridDownloader.cancelDownload(gameId);
} else { } else {
PythonInstance.cancelDownload(gameId); GenericHTTPDownloader.cancelDownload(gameId);
} }
WindowManager.mainWindow?.setProgressBar(-1); WindowManager.mainWindow?.setProgressBar(-1);
@ -94,12 +96,28 @@ export class DownloadManager {
} }
static async startDownload(game: Game) { static async startDownload(game: Game) {
if (game.downloader === Downloader.RealDebrid) { if (game.downloader === Downloader.Gofile) {
RealDebridDownloader.startDownload(game); const id = game!.uri!.split("/").pop();
this.currentDownloader = Downloader.RealDebrid;
} else { const token = await GofileApi.authorize();
const downloadLink = await GofileApi.getDownloadLink(id!);
GenericHTTPDownloader.startDownload(game, downloadLink, {
Cookie: `accountToken=${token}`,
});
} else if (game.downloader === Downloader.PixelDrain) {
const id = game!.uri!.split("/").pop();
await GenericHTTPDownloader.startDownload(
game,
`https://pixeldrain.com/api/file/${id}?download`
);
} else if (game.downloader === Downloader.Torrent) {
PythonInstance.startDownload(game); PythonInstance.startDownload(game);
this.currentDownloader = Downloader.Torrent; } else if (game.downloader === Downloader.RealDebrid) {
RealDebridDownloader.startDownload(game);
} }
this.currentDownloader = game.downloader;
} }
} }

View File

@ -0,0 +1,107 @@
import { Game } from "@main/entity";
import { gameRepository } from "@main/repository";
import { calculateETA } from "./helpers";
import { DownloadProgress } from "@types";
import { HttpDownload } from "./http-download";
export class GenericHTTPDownloader {
private static downloads = new Map<number, string>();
private static downloadingGame: Game | null = null;
public static async getStatus() {
if (this.downloadingGame) {
const gid = this.downloads.get(this.downloadingGame.id)!;
const status = HttpDownload.getStatus(gid);
if (status) {
const progress =
Number(status.completedLength) / Number(status.totalLength);
await gameRepository.update(
{ id: this.downloadingGame!.id },
{
bytesDownloaded: Number(status.completedLength),
fileSize: Number(status.totalLength),
progress,
status: "active",
folderName: status.folderName,
}
);
const result = {
numPeers: 0,
numSeeds: 0,
downloadSpeed: status.downloadSpeed,
timeRemaining: calculateETA(
status.totalLength,
status.completedLength,
status.downloadSpeed
),
isDownloadingMetadata: false,
isCheckingFiles: false,
progress,
gameId: this.downloadingGame!.id,
} as DownloadProgress;
if (progress === 1) {
this.downloads.delete(this.downloadingGame.id);
this.downloadingGame = null;
}
return result;
}
}
return null;
}
static async pauseDownload() {
if (this.downloadingGame) {
const gid = this.downloads.get(this.downloadingGame!.id!);
if (gid) {
await HttpDownload.pauseDownload(gid);
}
this.downloadingGame = null;
}
}
static async startDownload(
game: Game,
downloadUrl: string,
headers?: Record<string, string>
) {
this.downloadingGame = game;
if (this.downloads.has(game.id)) {
await this.resumeDownload(game.id!);
return;
}
const gid = await HttpDownload.startDownload(
game.downloadPath!,
downloadUrl,
headers
);
this.downloads.set(game.id!, gid);
}
static async cancelDownload(gameId: number) {
const gid = this.downloads.get(gameId);
if (gid) {
await HttpDownload.cancelDownload(gid);
this.downloads.delete(gameId);
}
}
static async resumeDownload(gameId: number) {
const gid = this.downloads.get(gameId);
if (gid) {
await HttpDownload.resumeDownload(gid);
}
}
}

View File

@ -1,68 +1,69 @@
import type { ChildProcess } from "node:child_process"; import { DownloadItem } from "electron";
import { logger } from "../logger"; import { WindowManager } from "../window-manager";
import { sleep } from "@main/helpers"; import path from "node:path";
import { startAria2 } from "../aria2c";
import Aria2 from "aria2";
export class HttpDownload { export class HttpDownload {
private static connected = false; private static id = 0;
private static aria2c: ChildProcess | null = null;
private static aria2 = new Aria2({}); private static downloads: Record<string, DownloadItem> = {};
private static async connect() { public static getStatus(gid: string): {
this.aria2c = startAria2(); completedLength: number;
totalLength: number;
let retries = 0; downloadSpeed: number;
folderName: string;
while (retries < 4 && !this.connected) { } | null {
try { const downloadItem = this.downloads[gid];
await this.aria2.open(); if (downloadItem) {
logger.log("Connected to aria2"); return {
completedLength: downloadItem.getReceivedBytes(),
this.connected = true; totalLength: downloadItem.getTotalBytes(),
} catch (err) { downloadSpeed: downloadItem.getCurrentBytesPerSecond(),
await sleep(100); folderName: downloadItem.getFilename(),
logger.log("Failed to connect to aria2, retrying..."); };
retries++;
}
}
}
public static getStatus(gid: string) {
if (this.connected) {
return this.aria2.call("tellStatus", gid);
} }
return null; return null;
} }
public static disconnect() {
if (this.aria2c) {
this.aria2c.kill();
this.connected = false;
}
}
static async cancelDownload(gid: string) { static async cancelDownload(gid: string) {
await this.aria2.call("forceRemove", gid); const downloadItem = this.downloads[gid];
downloadItem?.cancel();
delete this.downloads[gid];
} }
static async pauseDownload(gid: string) { static async pauseDownload(gid: string) {
await this.aria2.call("forcePause", gid); const downloadItem = this.downloads[gid];
downloadItem?.pause();
} }
static async resumeDownload(gid: string) { static async resumeDownload(gid: string) {
await this.aria2.call("unpause", gid); const downloadItem = this.downloads[gid];
downloadItem?.resume();
} }
static async startDownload(downloadPath: string, downloadUrl: string) { static async startDownload(
if (!this.connected) await this.connect(); downloadPath: string,
downloadUrl: string,
headers?: Record<string, string>
) {
return new Promise<string>((resolve) => {
const options = headers ? { headers } : {};
WindowManager.mainWindow?.webContents.downloadURL(downloadUrl, options);
const options = { const gid = ++this.id;
dir: downloadPath,
};
return this.aria2.call("addUri", [downloadUrl], options); WindowManager.mainWindow?.webContents.session.on(
"will-download",
(_event, item, _webContents) => {
this.downloads[gid.toString()] = item;
// Set the save path, making Electron not to prompt a save dialog.
item.setSavePath(path.join(downloadPath, item.getFilename()));
resolve(gid.toString());
}
);
});
} }
} }

View File

@ -13,22 +13,36 @@ export class RealDebridDownloader {
private static async getRealDebridDownloadUrl() { private static async getRealDebridDownloadUrl() {
if (this.realDebridTorrentId) { if (this.realDebridTorrentId) {
const torrentInfo = await RealDebridClient.getTorrentInfo( let torrentInfo = await RealDebridClient.getTorrentInfo(
this.realDebridTorrentId this.realDebridTorrentId
); );
const { status, links } = torrentInfo; if (torrentInfo.status === "waiting_files_selection") {
if (status === "waiting_files_selection") {
await RealDebridClient.selectAllFiles(this.realDebridTorrentId); await RealDebridClient.selectAllFiles(this.realDebridTorrentId);
return null;
torrentInfo = await RealDebridClient.getTorrentInfo(
this.realDebridTorrentId
);
} }
const { links, status } = torrentInfo;
if (status === "downloaded") { if (status === "downloaded") {
const [link] = links; const [link] = links;
const { download } = await RealDebridClient.unrestrictLink(link); const { download } = await RealDebridClient.unrestrictLink(link);
return decodeURIComponent(download); return decodeURIComponent(download);
} }
return null;
}
if (this.downloadingGame?.uri) {
const { download } = await RealDebridClient.unrestrictLink(
this.downloadingGame?.uri
);
return decodeURIComponent(download);
} }
return null; return null;
@ -37,7 +51,7 @@ export class RealDebridDownloader {
public static async getStatus() { public static async getStatus() {
if (this.downloadingGame) { if (this.downloadingGame) {
const gid = this.downloads.get(this.downloadingGame.id)!; const gid = this.downloads.get(this.downloadingGame.id)!;
const status = await HttpDownload.getStatus(gid); const status = HttpDownload.getStatus(gid);
if (status) { if (status) {
const progress = const progress =
@ -50,6 +64,7 @@ export class RealDebridDownloader {
fileSize: Number(status.totalLength), fileSize: Number(status.totalLength),
progress, progress,
status: "active", status: "active",
folderName: status.folderName,
} }
); );
@ -78,40 +93,15 @@ export class RealDebridDownloader {
} }
} }
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; return null;
} }
static async pauseDownload() { static async pauseDownload() {
const gid = this.downloads.get(this.downloadingGame!.id!); if (this.downloadingGame) {
if (gid) { const gid = this.downloads.get(this.downloadingGame.id);
await HttpDownload.pauseDownload(gid); if (gid) {
await HttpDownload.pauseDownload(gid);
}
} }
this.realDebridTorrentId = null; this.realDebridTorrentId = null;
@ -119,15 +109,19 @@ export class RealDebridDownloader {
} }
static async startDownload(game: Game) { static async startDownload(game: Game) {
this.downloadingGame = game;
if (this.downloads.has(game.id)) { if (this.downloads.has(game.id)) {
await this.resumeDownload(game.id!); await this.resumeDownload(game.id!);
this.downloadingGame = game;
return; return;
} }
this.realDebridTorrentId = await RealDebridClient.getTorrentId(game!.uri!); if (game.uri?.startsWith("magnet:")) {
this.realDebridTorrentId = await RealDebridClient.getTorrentId(
game!.uri!
);
}
this.downloadingGame = game;
const downloadUrl = await this.getRealDebridDownloadUrl(); const downloadUrl = await this.getRealDebridDownloadUrl();
@ -150,6 +144,9 @@ export class RealDebridDownloader {
await HttpDownload.cancelDownload(gid); await HttpDownload.cancelDownload(gid);
this.downloads.delete(gameId); this.downloads.delete(gameId);
} }
this.realDebridTorrentId = null;
this.downloadingGame = null;
} }
static async resumeDownload(gameId: number) { static async resumeDownload(gameId: number) {

View File

@ -0,0 +1,61 @@
import axios from "axios";
export interface GofileAccountsReponse {
id: string;
token: string;
}
export interface GofileContentChild {
id: string;
link: string;
}
export interface GofileContentsResponse {
id: string;
type: string;
children: Record<string, GofileContentChild>;
}
export class GofileApi {
private static token: string;
public static async authorize() {
const response = await axios.post<{
status: string;
data: GofileAccountsReponse;
}>("https://api.gofile.io/accounts");
if (response.data.status === "ok") {
this.token = response.data.data.token;
return this.token;
}
throw new Error("Failed to authorize");
}
public static async getDownloadLink(id: string) {
const searchParams = new URLSearchParams({
wt: "4fd6sg89d7s6",
});
const response = await axios.get<{
status: string;
data: GofileContentsResponse;
}>(`https://api.gofile.io/contents/${id}?${searchParams.toString()}`, {
headers: {
Authorization: `Bearer ${this.token}`,
},
});
if (response.data.status === "ok") {
if (response.data.data.type !== "folder") {
throw new Error("Only folders are supported");
}
const [firstChild] = Object.values(response.data.data.children);
return firstChild.link;
}
throw new Error("Failed to get download link");
}
}

View File

@ -0,0 +1 @@
export * from "./gofile";

View File

@ -64,6 +64,14 @@ export class HydraApi {
} }
} }
static handleSignOut() {
this.userAuth = {
authToken: "",
refreshToken: "",
expirationTimestamp: 0,
};
}
static async setupApi() { static async setupApi() {
this.instance = axios.create({ this.instance = axios.create({
baseURL: import.meta.env.MAIN_VITE_API_URL, baseURL: import.meta.env.MAIN_VITE_API_URL,

View File

@ -46,7 +46,7 @@ export class RealDebridClient {
static async selectAllFiles(id: string) { static async selectAllFiles(id: string) {
const searchParams = new URLSearchParams({ files: "all" }); const searchParams = new URLSearchParams({ files: "all" });
await this.instance.post( return this.instance.post(
`/torrents/selectFiles/${id}`, `/torrents/selectFiles/${id}`,
searchParams.toString() searchParams.toString()
); );

View File

@ -26,7 +26,7 @@ globalStyle("html, body, #root, main", {
globalStyle("body", { globalStyle("body", {
overflow: "hidden", overflow: "hidden",
userSelect: "none", userSelect: "none",
fontFamily: "'Fira Mono', monospace", fontFamily: "Noto Sans, sans-serif",
fontSize: vars.size.body, fontSize: vars.size.body,
background: vars.color.background, background: vars.color.background,
color: vars.color.body, color: vars.color.body,

View File

@ -45,7 +45,6 @@ export const description = style({
maxWidth: "700px", maxWidth: "700px",
color: vars.color.muted, color: vars.color.muted,
textAlign: "left", textAlign: "left",
fontFamily: "'Fira Sans', sans-serif",
lineHeight: "20px", lineHeight: "20px",
marginTop: `${SPACING_UNIT * 2}px`, marginTop: `${SPACING_UNIT * 2}px`,
}); });

View File

@ -24,6 +24,7 @@ export const modal = recipe({
animation: `${scaleFadeIn} 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none running`, animation: `${scaleFadeIn} 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none running`,
backgroundColor: vars.color.background, backgroundColor: vars.color.background,
borderRadius: "4px", borderRadius: "4px",
minWidth: "400px",
maxWidth: "600px", maxWidth: "600px",
color: vars.color.body, color: vars.color.body,
maxHeight: "100%", maxHeight: "100%",

View File

@ -5,4 +5,6 @@ export const VERSION_CODENAME = "Leviticus";
export const DOWNLOADER_NAME = { export const DOWNLOADER_NAME = {
[Downloader.RealDebrid]: "Real-Debrid", [Downloader.RealDebrid]: "Real-Debrid",
[Downloader.Torrent]: "Torrent", [Downloader.Torrent]: "Torrent",
[Downloader.Gofile]: "Gofile",
[Downloader.PixelDrain]: "PixelDrain",
}; };

View File

@ -8,12 +8,10 @@ import { HashRouter, Route, Routes } from "react-router-dom";
import * as Sentry from "@sentry/electron/renderer"; import * as Sentry from "@sentry/electron/renderer";
import "@fontsource/fira-mono/400.css"; import "@fontsource/noto-sans/400.css";
import "@fontsource/fira-mono/500.css"; import "@fontsource/noto-sans/500.css";
import "@fontsource/fira-mono/700.css"; import "@fontsource/noto-sans/700.css";
import "@fontsource/fira-sans/400.css";
import "@fontsource/fira-sans/500.css";
import "@fontsource/fira-sans/700.css";
import "react-loading-skeleton/dist/skeleton.css"; import "react-loading-skeleton/dist/skeleton.css";
import { App } from "./app"; import { App } from "./app";

View File

@ -132,9 +132,7 @@ export function Downloads() {
<ArrowDownIcon size={24} /> <ArrowDownIcon size={24} />
</div> </div>
<h2>{t("no_downloads_title")}</h2> <h2>{t("no_downloads_title")}</h2>
<p style={{ fontFamily: "Fira Sans" }}> <p>{t("no_downloads_description")}</p>
{t("no_downloads_description")}
</p>
</div> </div>
)} )}
</> </>

View File

@ -19,7 +19,10 @@ export function DescriptionHeader() {
date: shopDetails?.release_date.date, date: shopDetails?.release_date.date,
})} })}
</p> </p>
<p>{t("publisher", { publisher: shopDetails.publishers[0] })}</p>
{Array.isArray(shopDetails.publishers) && (
<p>{t("publisher", { publisher: shopDetails.publishers[0] })}</p>
)}
</section> </section>
</div> </div>
); );

View File

@ -101,7 +101,6 @@ export const descriptionContent = style({
export const description = style({ export const description = style({
userSelect: "text", userSelect: "text",
lineHeight: "22px", lineHeight: "22px",
fontFamily: "'Fira Sans', sans-serif",
fontSize: "16px", fontSize: "16px",
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`, padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
"@media": { "@media": {

View File

@ -9,6 +9,7 @@ export const panel = recipe({
height: "72px", height: "72px",
minHeight: "72px", minHeight: "72px",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`, padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
backgroundColor: vars.color.darkBackground,
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",

View File

@ -1,11 +1,11 @@
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { DiskSpace } from "check-disk-space"; import { DiskSpace } from "check-disk-space";
import * as styles from "./download-settings-modal.css"; import * as styles from "./download-settings-modal.css";
import { Button, Link, Modal, TextField } from "@renderer/components"; import { Button, Link, Modal, TextField } from "@renderer/components";
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react"; import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
import { Downloader, formatBytes } from "@shared"; import { Downloader, formatBytes, getDownloadersForUri } from "@shared";
import type { GameRepack } from "@types"; import type { GameRepack } from "@types";
import { SPACING_UNIT } from "@renderer/theme.css"; import { SPACING_UNIT } from "@renderer/theme.css";
@ -23,8 +23,6 @@ export interface DownloadSettingsModalProps {
repack: GameRepack | null; repack: GameRepack | null;
} }
const downloaders = [Downloader.Torrent, Downloader.RealDebrid];
export function DownloadSettingsModal({ export function DownloadSettingsModal({
visible, visible,
onClose, onClose,
@ -36,9 +34,8 @@ export function DownloadSettingsModal({
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace | null>(null); const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace | null>(null);
const [selectedPath, setSelectedPath] = useState(""); const [selectedPath, setSelectedPath] = useState("");
const [downloadStarting, setDownloadStarting] = useState(false); const [downloadStarting, setDownloadStarting] = useState(false);
const [selectedDownloader, setSelectedDownloader] = useState( const [selectedDownloader, setSelectedDownloader] =
Downloader.Torrent useState<Downloader | null>(null);
);
const userPreferences = useAppSelector( const userPreferences = useAppSelector(
(state) => state.userPreferences.value (state) => state.userPreferences.value
@ -50,6 +47,10 @@ export function DownloadSettingsModal({
} }
}, [visible, selectedPath]); }, [visible, selectedPath]);
const downloaders = useMemo(() => {
return getDownloadersForUri(repack?.magnet ?? "");
}, [repack?.magnet]);
useEffect(() => { useEffect(() => {
if (userPreferences?.downloadsPath) { if (userPreferences?.downloadsPath) {
setSelectedPath(userPreferences.downloadsPath); setSelectedPath(userPreferences.downloadsPath);
@ -59,9 +60,27 @@ export function DownloadSettingsModal({
.then((defaultDownloadsPath) => setSelectedPath(defaultDownloadsPath)); .then((defaultDownloadsPath) => setSelectedPath(defaultDownloadsPath));
} }
if (userPreferences?.realDebridApiToken) const filteredDownloaders = downloaders.filter((downloader) => {
setSelectedDownloader(Downloader.RealDebrid); if (downloader === Downloader.RealDebrid)
}, [userPreferences?.downloadsPath, userPreferences?.realDebridApiToken]); return userPreferences?.realDebridApiToken;
return true;
});
/* Gives preference to Real Debrid */
const selectedDownloader = filteredDownloaders.includes(
Downloader.RealDebrid
)
? Downloader.RealDebrid
: filteredDownloaders[0];
setSelectedDownloader(
selectedDownloader === undefined ? null : selectedDownloader
);
}, [
userPreferences?.downloadsPath,
downloaders,
userPreferences?.realDebridApiToken,
]);
const getDiskFreeSpace = (path: string) => { const getDiskFreeSpace = (path: string) => {
window.electron.getDiskFreeSpace(path).then((result) => { window.electron.getDiskFreeSpace(path).then((result) => {
@ -85,7 +104,7 @@ export function DownloadSettingsModal({
if (repack) { if (repack) {
setDownloadStarting(true); setDownloadStarting(true);
startDownload(repack, selectedDownloader, selectedPath).finally(() => { startDownload(repack, selectedDownloader!, selectedPath).finally(() => {
setDownloadStarting(false); setDownloadStarting(false);
onClose(); onClose();
}); });
@ -167,7 +186,10 @@ export function DownloadSettingsModal({
</p> </p>
</div> </div>
<Button onClick={handleStartClick} disabled={downloadStarting}> <Button
onClick={handleStartClick}
disabled={downloadStarting || selectedDownloader === null}
>
<DownloadIcon /> <DownloadIcon />
{t("download_now")} {t("download_now")}
</Button> </Button>

View File

@ -15,7 +15,6 @@ export const gameOptionHeader = style({
}); });
export const gameOptionHeaderDescription = style({ export const gameOptionHeaderDescription = style({
fontFamily: "'Fira Sans', sans-serif",
fontWeight: "400", fontWeight: "400",
}); });

View File

@ -44,8 +44,10 @@ export function RepacksModal({
}, [repacks]); }, [repacks]);
const getInfoHash = useCallback(async () => { const getInfoHash = useCallback(async () => {
const torrent = await parseTorrent(game?.uri ?? ""); if (game?.uri?.startsWith("magnet:")) {
if (torrent.infoHash) setInfoHash(torrent.infoHash); const torrent = await parseTorrent(game?.uri ?? "");
if (torrent.infoHash) setInfoHash(torrent.infoHash);
}
}, [game]); }, [game]);
useEffect(() => { useEffect(() => {

View File

@ -46,7 +46,6 @@ export const requirementButton = style({
export const requirementsDetails = style({ export const requirementsDetails = style({
padding: `${SPACING_UNIT * 2}px`, padding: `${SPACING_UNIT * 2}px`,
lineHeight: "22px", lineHeight: "22px",
fontFamily: "'Fira Sans', sans-serif",
fontSize: "16px", fontSize: "16px",
}); });

View File

@ -82,9 +82,7 @@ export function SettingsDownloadSources() {
onAddDownloadSource={handleAddDownloadSource} onAddDownloadSource={handleAddDownloadSource}
/> />
<p style={{ fontFamily: '"Fira Sans"' }}> <p>{t("download_sources_description")}</p>
{t("download_sources_description")}
</p>
<div className={styles.downloadSourcesHeader}> <div className={styles.downloadSourcesHeader}>
<Button <Button

View File

@ -9,6 +9,5 @@ export const form = style({
}); });
export const description = style({ export const description = style({
fontFamily: "'Fira Sans', sans-serif",
marginBottom: `${SPACING_UNIT * 2}px`, marginBottom: `${SPACING_UNIT * 2}px`,
}); });

View File

@ -25,9 +25,7 @@ export const UserBlockModal = ({
onClose={onClose} onClose={onClose}
> >
<div className={styles.signOutModalContent}> <div className={styles.signOutModalContent}>
<p style={{ fontFamily: "Fira Sans" }}> <p>{t("user_block_modal_text", { displayName })}</p>
{t("user_block_modal_text", { displayName })}
</p>
<div className={styles.signOutModalButtonsContainer}> <div className={styles.signOutModalButtonsContainer}>
<Button onClick={onConfirm} theme="danger"> <Button onClick={onConfirm} theme="danger">
{t("block_user")} {t("block_user")}

View File

@ -371,11 +371,7 @@ export function UserContent({
<TelescopeIcon size={24} /> <TelescopeIcon size={24} />
</div> </div>
<h2>{t("no_recent_activity_title")}</h2> <h2>{t("no_recent_activity_title")}</h2>
{isMe && ( {isMe && <p>{t("no_recent_activity_description")}</p>}
<p style={{ fontFamily: "Fira Sans" }}>
{t("no_recent_activity_description")}
</p>
)}
</div> </div>
) : ( ) : (
<div <div

View File

@ -23,7 +23,7 @@ export const UserSignOutModal = ({
onClose={onClose} onClose={onClose}
> >
<div className={styles.signOutModalContent}> <div className={styles.signOutModalContent}>
<p style={{ fontFamily: "Fira Sans" }}>{t("sign_out_modal_text")}</p> <p>{t("sign_out_modal_text")}</p>
<div className={styles.signOutModalButtonsContainer}> <div className={styles.signOutModalButtonsContainer}>
<Button onClick={onConfirm} theme="danger"> <Button onClick={onConfirm} theme="danger">
{t("sign_out")} {t("sign_out")}

View File

@ -1,6 +1,8 @@
export enum Downloader { export enum Downloader {
RealDebrid, RealDebrid,
Torrent, Torrent,
Gofile,
PixelDrain,
} }
export enum DownloadSourceStatus { export enum DownloadSourceStatus {
@ -51,6 +53,8 @@ export const removeSpecialEditionFromName = (name: string) =>
export const removeDuplicateSpaces = (name: string) => export const removeDuplicateSpaces = (name: string) =>
name.replace(/\s{2,}/g, " "); name.replace(/\s{2,}/g, " ");
export const replaceDotsWithSpace = (name: string) => name.replace(/\./g, " ");
export const replaceUnderscoreWithSpace = (name: string) => export const replaceUnderscoreWithSpace = (name: string) =>
name.replace(/_/g, " "); name.replace(/_/g, " ");
@ -58,8 +62,24 @@ export const formatName = pipe<string>(
removeReleaseYearFromName, removeReleaseYearFromName,
removeSpecialEditionFromName, removeSpecialEditionFromName,
replaceUnderscoreWithSpace, replaceUnderscoreWithSpace,
replaceDotsWithSpace,
(str) => str.replace(/DIRECTOR'S CUT/g, ""), (str) => str.replace(/DIRECTOR'S CUT/g, ""),
removeSymbolsFromName, removeSymbolsFromName,
removeDuplicateSpaces, removeDuplicateSpaces,
(str) => str.trim() (str) => str.trim()
); );
const realDebridHosts = ["https://1fichier.com", "https://mediafire.com"];
export const getDownloadersForUri = (uri: string) => {
if (uri.startsWith("https://gofile.io")) return [Downloader.Gofile];
if (uri.startsWith("https://pixeldrain.com")) return [Downloader.PixelDrain];
if (realDebridHosts.some((host) => uri.startsWith(host)))
return [Downloader.RealDebrid];
if (uri.startsWith("magnet:"))
return [Downloader.Torrent, Downloader.RealDebrid];
return [];
};

View File

@ -1,62 +0,0 @@
import libtorrent as lt
class Downloader:
def __init__(self, port: str):
self.torrent_handles = {}
self.downloading_game_id = -1
self.session = lt.session({'listen_interfaces': '0.0.0.0:{port}'.format(port=port)})
def start_download(self, game_id: int, magnet: str, save_path: str):
params = {'url': magnet, 'save_path': save_path}
torrent_handle = self.session.add_torrent(params)
self.torrent_handles[game_id] = torrent_handle
torrent_handle.set_flags(lt.torrent_flags.auto_managed)
torrent_handle.resume()
self.downloading_game_id = game_id
def pause_download(self, game_id: int):
torrent_handle = self.torrent_handles.get(game_id)
if torrent_handle:
torrent_handle.pause()
torrent_handle.unset_flags(lt.torrent_flags.auto_managed)
self.downloading_game_id = -1
def cancel_download(self, game_id: int):
torrent_handle = self.torrent_handles.get(game_id)
if torrent_handle:
torrent_handle.pause()
self.session.remove_torrent(torrent_handle)
self.torrent_handles[game_id] = None
self.downloading_game_id = -1
def abort_session(self):
for game_id in self.torrent_handles:
torrent_handle = self.torrent_handles[game_id]
torrent_handle.pause()
self.session.remove_torrent(torrent_handle)
self.session.abort()
self.torrent_handles = {}
self.downloading_game_id = -1
def get_download_status(self):
if self.downloading_game_id == -1:
return None
torrent_handle = self.torrent_handles.get(self.downloading_game_id)
status = torrent_handle.status()
info = torrent_handle.get_torrent_info()
return {
'folderName': info.name() if info else "",
'fileSize': info.total_size() if info else 0,
'gameId': self.downloading_game_id,
'progress': status.progress,
'downloadSpeed': status.download_rate,
'numPeers': status.num_peers,
'numSeeds': status.num_seeds,
'status': status.state,
'bytesDownloaded': status.progress * info.total_size() if info else status.all_time_download,
}

View File

@ -3,19 +3,19 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
import json import json
import urllib.parse import urllib.parse
import psutil import psutil
from downloader import Downloader from torrent_downloader import TorrentDownloader
torrent_port = sys.argv[1] torrent_port = sys.argv[1]
http_port = sys.argv[2] http_port = sys.argv[2]
rpc_password = sys.argv[3] rpc_password = sys.argv[3]
start_download_payload = sys.argv[4] start_download_payload = sys.argv[4]
downloader = None torrent_downloader = None
if start_download_payload: if start_download_payload:
initial_download = json.loads(urllib.parse.unquote(start_download_payload)) initial_download = json.loads(urllib.parse.unquote(start_download_payload))
downloader = Downloader(torrent_port) torrent_downloader = TorrentDownloader(torrent_port)
downloader.start_download(initial_download['game_id'], initial_download['magnet'], initial_download['save_path']) torrent_downloader.start_download(initial_download['game_id'], initial_download['magnet'], initial_download['save_path'])
class Handler(BaseHTTPRequestHandler): class Handler(BaseHTTPRequestHandler):
rpc_password_header = 'x-hydra-rpc-password' rpc_password_header = 'x-hydra-rpc-password'
@ -31,7 +31,7 @@ class Handler(BaseHTTPRequestHandler):
self.send_header("Content-type", "application/json") self.send_header("Content-type", "application/json")
self.end_headers() self.end_headers()
status = downloader.get_download_status() status = torrent_downloader.get_download_status()
self.wfile.write(json.dumps(status).encode('utf-8')) self.wfile.write(json.dumps(status).encode('utf-8'))
@ -54,7 +54,7 @@ class Handler(BaseHTTPRequestHandler):
self.wfile.write(json.dumps(process_list).encode('utf-8')) self.wfile.write(json.dumps(process_list).encode('utf-8'))
def do_POST(self): def do_POST(self):
global downloader global torrent_downloader
if self.path == "/action": if self.path == "/action":
if self.headers.get(self.rpc_password_header) != rpc_password: if self.headers.get(self.rpc_password_header) != rpc_password:
@ -66,18 +66,18 @@ class Handler(BaseHTTPRequestHandler):
post_data = self.rfile.read(content_length) post_data = self.rfile.read(content_length)
data = json.loads(post_data.decode('utf-8')) data = json.loads(post_data.decode('utf-8'))
if downloader is None: if torrent_downloader is None:
downloader = Downloader(torrent_port) torrent_downloader = TorrentDownloader(torrent_port)
if data['action'] == 'start': if data['action'] == 'start':
downloader.start_download(data['game_id'], data['magnet'], data['save_path']) torrent_downloader.start_download(data['game_id'], data['magnet'], data['save_path'])
elif data['action'] == 'pause': elif data['action'] == 'pause':
downloader.pause_download(data['game_id']) torrent_downloader.pause_download(data['game_id'])
elif data['action'] == 'cancel': elif data['action'] == 'cancel':
downloader.cancel_download(data['game_id']) torrent_downloader.cancel_download(data['game_id'])
elif data['action'] == 'kill-torrent': elif data['action'] == 'kill-torrent':
downloader.abort_session() torrent_downloader.abort_session()
downloader = None torrent_downloader = None
self.send_response(200) self.send_response(200)
self.end_headers() self.end_headers()

View File

@ -0,0 +1,158 @@
import libtorrent as lt
class TorrentDownloader:
def __init__(self, port: str):
self.torrent_handles = {}
self.downloading_game_id = -1
self.session = lt.session({'listen_interfaces': '0.0.0.0:{port}'.format(port=port)})
self.trackers = [
"udp://tracker.opentrackr.org:1337/announce",
"http://tracker.opentrackr.org:1337/announce",
"udp://open.tracker.cl:1337/announce",
"udp://open.demonii.com:1337/announce",
"udp://open.stealth.si:80/announce",
"udp://tracker.torrent.eu.org:451/announce",
"udp://exodus.desync.com:6969/announce",
"udp://tracker.theoks.net:6969/announce",
"udp://tracker-udp.gbitt.info:80/announce",
"udp://explodie.org:6969/announce",
"https://tracker.tamersunion.org:443/announce",
"udp://tracker2.dler.org:80/announce",
"udp://tracker1.myporn.club:9337/announce",
"udp://tracker.tiny-vps.com:6969/announce",
"udp://tracker.dler.org:6969/announce",
"udp://tracker.bittor.pw:1337/announce",
"udp://tracker.0x7c0.com:6969/announce",
"udp://retracker01-msk-virt.corbina.net:80/announce",
"udp://opentracker.io:6969/announce",
"udp://open.free-tracker.ga:6969/announce",
"udp://new-line.net:6969/announce",
"udp://moonburrow.club:6969/announce",
"udp://leet-tracker.moe:1337/announce",
"udp://bt2.archive.org:6969/announce",
"udp://bt1.archive.org:6969/announce",
"http://tracker2.dler.org:80/announce",
"http://tracker1.bt.moack.co.kr:80/announce",
"http://tracker.dler.org:6969/announce",
"http://tr.kxmp.cf:80/announce",
"udp://u.peer-exchange.download:6969/announce",
"udp://ttk2.nbaonlineservice.com:6969/announce",
"udp://tracker.tryhackx.org:6969/announce",
"udp://tracker.srv00.com:6969/announce",
"udp://tracker.skynetcloud.site:6969/announce",
"udp://tracker.jamesthebard.net:6969/announce",
"udp://tracker.fnix.net:6969/announce",
"udp://tracker.filemail.com:6969/announce",
"udp://tracker.farted.net:6969/announce",
"udp://tracker.edkj.club:6969/announce",
"udp://tracker.dump.cl:6969/announce",
"udp://tracker.deadorbit.nl:6969/announce",
"udp://tracker.darkness.services:6969/announce",
"udp://tracker.ccp.ovh:6969/announce",
"udp://tamas3.ynh.fr:6969/announce",
"udp://ryjer.com:6969/announce",
"udp://run.publictracker.xyz:6969/announce",
"udp://public.tracker.vraphim.com:6969/announce",
"udp://p4p.arenabg.com:1337/announce",
"udp://p2p.publictracker.xyz:6969/announce",
"udp://open.u-p.pw:6969/announce",
"udp://open.publictracker.xyz:6969/announce",
"udp://open.dstud.io:6969/announce",
"udp://open.demonoid.ch:6969/announce",
"udp://odd-hd.fr:6969/announce",
"udp://martin-gebhardt.eu:25/announce",
"udp://jutone.com:6969/announce",
"udp://isk.richardsw.club:6969/announce",
"udp://evan.im:6969/announce",
"udp://epider.me:6969/announce",
"udp://d40969.acod.regrucolo.ru:6969/announce",
"udp://bt.rer.lol:6969/announce",
"udp://amigacity.xyz:6969/announce",
"udp://1c.premierzal.ru:6969/announce",
"https://trackers.run:443/announce",
"https://tracker.yemekyedim.com:443/announce",
"https://tracker.renfei.net:443/announce",
"https://tracker.pmman.tech:443/announce",
"https://tracker.lilithraws.org:443/announce",
"https://tracker.imgoingto.icu:443/announce",
"https://tracker.cloudit.top:443/announce",
"https://tracker-zhuqiy.dgj055.icu:443/announce",
"http://tracker.renfei.net:8080/announce",
"http://tracker.mywaifu.best:6969/announce",
"http://tracker.ipv6tracker.org:80/announce",
"http://tracker.files.fm:6969/announce",
"http://tracker.edkj.club:6969/announce",
"http://tracker.bt4g.com:2095/announce",
"http://tracker-zhuqiy.dgj055.icu:80/announce",
"http://t1.aag.moe:17715/announce",
"http://t.overflow.biz:6969/announce",
"http://bittorrent-tracker.e-n-c-r-y-p-t.net:1337/announce",
"udp://torrents.artixlinux.org:6969/announce",
"udp://mail.artixlinux.org:6969/announce",
"udp://ipv4.rer.lol:2710/announce",
"udp://concen.org:6969/announce",
"udp://bt.rer.lol:2710/announce",
"udp://aegir.sexy:6969/announce",
"https://www.peckservers.com:9443/announce",
"https://tracker.ipfsscan.io:443/announce",
"https://tracker.gcrenwp.top:443/announce",
"http://www.peckservers.com:9000/announce",
"http://tracker1.itzmx.com:8080/announce",
"http://ch3oh.ru:6969/announce",
"http://bvarf.tracker.sh:2086/announce",
]
def start_download(self, game_id: int, magnet: str, save_path: str):
params = {'url': magnet, 'save_path': save_path, 'trackers': self.trackers}
torrent_handle = self.session.add_torrent(params)
self.torrent_handles[game_id] = torrent_handle
torrent_handle.set_flags(lt.torrent_flags.auto_managed)
torrent_handle.resume()
self.downloading_game_id = game_id
def pause_download(self, game_id: int):
torrent_handle = self.torrent_handles.get(game_id)
if torrent_handle:
torrent_handle.pause()
torrent_handle.unset_flags(lt.torrent_flags.auto_managed)
self.downloading_game_id = -1
def cancel_download(self, game_id: int):
torrent_handle = self.torrent_handles.get(game_id)
if torrent_handle:
torrent_handle.pause()
self.session.remove_torrent(torrent_handle)
self.torrent_handles[game_id] = None
self.downloading_game_id = -1
def abort_session(self):
for game_id in self.torrent_handles:
torrent_handle = self.torrent_handles[game_id]
torrent_handle.pause()
self.session.remove_torrent(torrent_handle)
self.session.abort()
self.torrent_handles = {}
self.downloading_game_id = -1
def get_download_status(self):
if self.downloading_game_id == -1:
return None
torrent_handle = self.torrent_handles.get(self.downloading_game_id)
status = torrent_handle.status()
info = torrent_handle.get_torrent_info()
return {
'folderName': info.name() if info else "",
'fileSize': info.total_size() if info else 0,
'gameId': self.downloading_game_id,
'progress': status.progress,
'downloadSpeed': status.download_rate,
'numPeers': status.num_peers,
'numSeeds': status.num_seeds,
'status': status.state,
'bytesDownloaded': status.progress * info.total_size() if info else status.all_time_download,
}

View File

@ -943,15 +943,10 @@
resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz" resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz"
integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==
"@fontsource/fira-mono@^5.0.13": "@fontsource/noto-sans@^5.0.22":
version "5.0.13" version "5.0.22"
resolved "https://registry.npmjs.org/@fontsource/fira-mono/-/fira-mono-5.0.13.tgz" resolved "https://registry.yarnpkg.com/@fontsource/noto-sans/-/noto-sans-5.0.22.tgz#2c5249347ba84fef16e71a58e0ec01b460174093"
integrity sha512-fZDjR2BdAqmauEbTjcIT62zYzbOgDa5+IQH34D2k8Pxmy1T815mAqQkZciWZVQ9dc/BgdTtTUV9HJ2ulBNwchg== integrity sha512-PwjvKPGFbgpwfKjWZj1zeUvd7ExUW2AqHE9PF9ysAJ2gOuzIHWE6mEVIlchYif7WC2pQhn+g0w6xooCObVi+4A==
"@fontsource/fira-sans@^5.0.20":
version "5.0.20"
resolved "https://registry.npmjs.org/@fontsource/fira-sans/-/fira-sans-5.0.20.tgz"
integrity sha512-inmUjoKPrgnO4uUaZTAgP0b6YdhDfA52axHXvdTwgLvwd2kn3ZgK52UZoxD0VnrvTOjLA/iE4oC0tNtz4nyb5g==
"@humanwhocodes/config-array@^0.11.14": "@humanwhocodes/config-array@^0.11.14":
version "0.11.14" version "0.11.14"
@ -2677,14 +2672,6 @@ aria-query@^5.3.0:
dependencies: dependencies:
dequal "^2.0.3" dequal "^2.0.3"
aria2@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/aria2/-/aria2-4.1.2.tgz#0ecbc50beea82856c88b4de71dac336154f67362"
integrity sha512-qTBr2RY8RZQmiUmbj2KXFvkErNxU4aTHZszszzwhE8svy2PEVX+IYR/c4Rp9Tuw4QkeU8cylGy6McV6Yl8i7Qw==
dependencies:
node-fetch "^2.6.1"
ws "^7.4.0"
array-buffer-byte-length@^1.0.1: array-buffer-byte-length@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz" resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz"
@ -3751,10 +3738,10 @@ electron-vite@^2.0.0:
magic-string "^0.30.5" magic-string "^0.30.5"
picocolors "^1.0.0" picocolors "^1.0.0"
electron@^30.0.9: electron@^30.3.0:
version "30.0.9" version "30.3.1"
resolved "https://registry.npmjs.org/electron/-/electron-30.0.9.tgz" resolved "https://registry.yarnpkg.com/electron/-/electron-30.3.1.tgz#fe27ca2a4739bec832b2edd6f46140ab46bf53a0"
integrity sha512-ArxgdGHVu3o5uaP+Tqj8cJDvU03R6vrGrOqiMs7JXLnvQHMqXJIIxmFKQAIdJW8VoT3ac3hD21tA7cPO10RLow== integrity sha512-Ai/OZ7VlbFAVYMn9J5lyvtr+ZWyEbXDVd5wBLb5EVrp4352SRmMAmN5chcIe3n9mjzcgehV9n4Hwy15CJW+YbA==
dependencies: dependencies:
"@electron/get" "^2.0.0" "@electron/get" "^2.0.0"
"@types/node" "^20.9.0" "@types/node" "^20.9.0"
@ -5833,7 +5820,7 @@ node-domexception@^1.0.0:
resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz" resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz"
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
node-fetch@^2.6.1, node-fetch@^2.6.7: node-fetch@^2.6.7:
version "2.7.0" version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
@ -7629,11 +7616,6 @@ wrappy@1:
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
ws@^7.4.0:
version "7.5.10"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
ws@^8.16.0: ws@^8.16.0:
version "8.17.0" version "8.17.0"
resolved "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz" resolved "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz"