mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feat: adding aria2c
This commit is contained in:
parent
c6d4b658a1
commit
d7e06d6622
11 changed files with 844 additions and 0 deletions
32
src/main/services/aria2.ts
Normal file
32
src/main/services/aria2.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import path from "node:path";
|
||||
import cp from "node:child_process";
|
||||
import { app } from "electron";
|
||||
|
||||
export const startAria2 = () => {};
|
||||
|
||||
export class Aria2 {
|
||||
private static process: cp.ChildProcess | null = null;
|
||||
|
||||
public static spawn() {
|
||||
const binaryPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "aria2", "aria2c")
|
||||
: path.join(__dirname, "..", "..", "aria2", "aria2c");
|
||||
|
||||
this.process = cp.spawn(
|
||||
binaryPath,
|
||||
[
|
||||
"--enable-rpc",
|
||||
"--rpc-listen-all",
|
||||
"--file-allocation=none",
|
||||
"--allow-overwrite=true",
|
||||
],
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
|
||||
console.log(this.process);
|
||||
}
|
||||
|
||||
public static kill() {
|
||||
this.process?.kill();
|
||||
}
|
||||
}
|
119
src/main/services/download/real-debrid.ts
Normal file
119
src/main/services/download/real-debrid.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import axios, { AxiosInstance } from "axios";
|
||||
import parseTorrent from "parse-torrent";
|
||||
import type {
|
||||
RealDebridAddMagnet,
|
||||
RealDebridTorrentInfo,
|
||||
RealDebridUnrestrictLink,
|
||||
RealDebridUser,
|
||||
} from "@types";
|
||||
|
||||
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 });
|
||||
|
||||
const response = await this.instance.post<RealDebridAddMagnet>(
|
||||
"/torrents/addMagnet",
|
||||
searchParams.toString()
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
static async getTorrentInfo(id: string) {
|
||||
const response = await this.instance.get<RealDebridTorrentInfo>(
|
||||
`/torrents/info/${id}`
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
static async getUser() {
|
||||
const response = await this.instance.get<RealDebridUser>(`/user`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
static async selectAllFiles(id: string) {
|
||||
const searchParams = new URLSearchParams({ files: "all" });
|
||||
|
||||
return this.instance.post(
|
||||
`/torrents/selectFiles/${id}`,
|
||||
searchParams.toString()
|
||||
);
|
||||
}
|
||||
|
||||
static async unrestrictLink(link: string) {
|
||||
const searchParams = new URLSearchParams({ link });
|
||||
|
||||
const response = await this.instance.post<RealDebridUnrestrictLink>(
|
||||
"/unrestrict/link",
|
||||
searchParams.toString()
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
private static async getAllTorrentsFromUser() {
|
||||
const response =
|
||||
await this.instance.get<RealDebridTorrentInfo[]>("/torrents");
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
static async getTorrentId(magnetUri: string) {
|
||||
const userTorrents = await RealDebridClient.getAllTorrentsFromUser();
|
||||
|
||||
const { infoHash } = await parseTorrent(magnetUri);
|
||||
const userTorrent = userTorrents.find(
|
||||
(userTorrent) => userTorrent.hash === infoHash
|
||||
);
|
||||
|
||||
if (userTorrent) return userTorrent.id;
|
||||
|
||||
const torrent = await RealDebridClient.addMagnet(magnetUri);
|
||||
return torrent.id;
|
||||
}
|
||||
|
||||
public static async getDownloadUrl(uri: string) {
|
||||
let realDebridTorrentId: string | null = null;
|
||||
|
||||
if (uri.startsWith("magnet:")) {
|
||||
realDebridTorrentId = await this.getTorrentId(uri);
|
||||
}
|
||||
|
||||
if (realDebridTorrentId) {
|
||||
let torrentInfo = await this.getTorrentInfo(realDebridTorrentId);
|
||||
|
||||
if (torrentInfo.status === "waiting_files_selection") {
|
||||
await this.selectAllFiles(realDebridTorrentId);
|
||||
|
||||
torrentInfo = await this.getTorrentInfo(realDebridTorrentId);
|
||||
}
|
||||
|
||||
const { links, status } = torrentInfo;
|
||||
|
||||
if (status === "downloaded") {
|
||||
const [link] = links;
|
||||
|
||||
const { download } = await this.unrestrictLink(link);
|
||||
return decodeURIComponent(download);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const { download } = await this.unrestrictLink(uri);
|
||||
|
||||
return decodeURIComponent(download);
|
||||
}
|
||||
}
|
96
src/main/services/download/torbox.ts
Normal file
96
src/main/services/download/torbox.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
import axios, { AxiosInstance } from "axios";
|
||||
import parseTorrent from "parse-torrent";
|
||||
import type {
|
||||
TorBoxUserRequest,
|
||||
TorBoxTorrentInfoRequest,
|
||||
TorBoxAddTorrentRequest,
|
||||
TorBoxRequestLinkRequest,
|
||||
} from "@types";
|
||||
|
||||
export class TorBoxClient {
|
||||
private static instance: AxiosInstance;
|
||||
private static baseURL = "https://api.torbox.app/v1/api";
|
||||
public static apiToken: string;
|
||||
|
||||
static authorize(apiToken: string) {
|
||||
this.instance = axios.create({
|
||||
baseURL: this.baseURL,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
},
|
||||
});
|
||||
this.apiToken = apiToken;
|
||||
}
|
||||
|
||||
static async addMagnet(magnet: string) {
|
||||
const form = new FormData();
|
||||
form.append("magnet", magnet);
|
||||
|
||||
const response = await this.instance.post<TorBoxAddTorrentRequest>(
|
||||
"/torrents/createtorrent",
|
||||
form
|
||||
);
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
static async getTorrentInfo(id: number) {
|
||||
const response =
|
||||
await this.instance.get<TorBoxTorrentInfoRequest>("/torrents/mylist");
|
||||
const data = response.data.data;
|
||||
|
||||
const info = data.find((item) => item.id === id);
|
||||
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
static async getUser() {
|
||||
const response = await this.instance.get<TorBoxUserRequest>(`/user/me`);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
static async requestLink(id: number) {
|
||||
const searchParams = new URLSearchParams({});
|
||||
|
||||
searchParams.set("token", this.apiToken);
|
||||
searchParams.set("torrent_id", id.toString());
|
||||
searchParams.set("zip_link", "true");
|
||||
|
||||
const response = await this.instance.get<TorBoxRequestLinkRequest>(
|
||||
"/torrents/requestdl?" + searchParams.toString()
|
||||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.error(response.data.error);
|
||||
console.error(response.data.detail);
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
private static async getAllTorrentsFromUser() {
|
||||
const response =
|
||||
await this.instance.get<TorBoxTorrentInfoRequest>("/torrents/mylist");
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
static async getTorrentId(magnetUri: string) {
|
||||
const userTorrents = await this.getAllTorrentsFromUser();
|
||||
|
||||
const { infoHash } = await parseTorrent(magnetUri);
|
||||
const userTorrent = userTorrents.find(
|
||||
(userTorrent) => userTorrent.hash === infoHash
|
||||
);
|
||||
|
||||
if (userTorrent) return userTorrent.id;
|
||||
|
||||
const torrent = await this.addMagnet(magnetUri);
|
||||
return torrent.torrent_id;
|
||||
}
|
||||
}
|
96
src/main/services/python-rpc.ts
Normal file
96
src/main/services/python-rpc.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
import axios from "axios";
|
||||
|
||||
import cp from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { logger } from "./logger";
|
||||
import { Readable } from "node:stream";
|
||||
import { app, dialog } from "electron";
|
||||
|
||||
const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
|
||||
darwin: "hydra-python-rpc",
|
||||
linux: "hydra-python-rpc",
|
||||
win32: "hydra-python-rpc.exe",
|
||||
};
|
||||
|
||||
export class PythonRPC {
|
||||
public static readonly BITTORRENT_PORT = "5881";
|
||||
public static readonly RPC_PORT = "8084";
|
||||
private static readonly RPC_PASSWORD = crypto.randomBytes(32).toString("hex");
|
||||
|
||||
private static pythonProcess: cp.ChildProcess | null = null;
|
||||
|
||||
public static rpc = axios.create({
|
||||
baseURL: `http://localhost:${this.RPC_PORT}`,
|
||||
headers: {
|
||||
"x-hydra-rpc-password": this.RPC_PASSWORD,
|
||||
},
|
||||
});
|
||||
|
||||
private static logStderr(readable: Readable | null) {
|
||||
if (!readable) return;
|
||||
|
||||
readable.setEncoding("utf-8");
|
||||
readable.on("data", logger.log);
|
||||
}
|
||||
|
||||
public static spawn() {
|
||||
console.log([this.BITTORRENT_PORT, this.RPC_PORT, this.RPC_PASSWORD]);
|
||||
const commonArgs = [this.BITTORRENT_PORT, this.RPC_PORT, this.RPC_PASSWORD];
|
||||
|
||||
if (app.isPackaged) {
|
||||
const binaryName = binaryNameByPlatform[process.platform]!;
|
||||
const binaryPath = path.join(
|
||||
process.resourcesPath,
|
||||
"hydra-python-rpc",
|
||||
binaryName
|
||||
);
|
||||
|
||||
if (!fs.existsSync(binaryPath)) {
|
||||
dialog.showErrorBox(
|
||||
"Fatal",
|
||||
"Hydra Python Instance binary not found. Please check if it has been removed by Windows Defender."
|
||||
);
|
||||
|
||||
app.quit();
|
||||
}
|
||||
|
||||
const childProcess = cp.spawn(binaryPath, commonArgs, {
|
||||
windowsHide: true,
|
||||
stdio: ["inherit", "inherit"],
|
||||
});
|
||||
|
||||
this.logStderr(childProcess.stderr);
|
||||
|
||||
this.pythonProcess = childProcess;
|
||||
} else {
|
||||
const scriptPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"python_rpc",
|
||||
"main.py"
|
||||
);
|
||||
|
||||
console.log(scriptPath);
|
||||
|
||||
const childProcess = cp.spawn("python3", [scriptPath, ...commonArgs], {
|
||||
stdio: ["inherit", "inherit"],
|
||||
});
|
||||
|
||||
this.logStderr(childProcess.stderr);
|
||||
|
||||
this.pythonProcess = childProcess;
|
||||
}
|
||||
}
|
||||
|
||||
public static kill() {
|
||||
if (this.pythonProcess) {
|
||||
logger.log("Killing python process");
|
||||
this.pythonProcess.kill();
|
||||
this.pythonProcess = null;
|
||||
}
|
||||
}
|
||||
}
|
77
src/types/torbox.types.ts
Normal file
77
src/types/torbox.types.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
export interface TorBoxUser {
|
||||
id: number;
|
||||
email: string;
|
||||
plan: string;
|
||||
expiration: string;
|
||||
}
|
||||
|
||||
export interface TorBoxUserRequest {
|
||||
success: boolean;
|
||||
detail: string;
|
||||
error: string;
|
||||
data: TorBoxUser;
|
||||
}
|
||||
|
||||
export interface TorBoxFile {
|
||||
id: number;
|
||||
md5: string;
|
||||
s3_path: string;
|
||||
name: string;
|
||||
size: number;
|
||||
mimetype: string;
|
||||
short_name: string;
|
||||
}
|
||||
|
||||
export interface TorBoxTorrentInfo {
|
||||
id: number;
|
||||
hash: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
magnet: string;
|
||||
size: number;
|
||||
active: boolean;
|
||||
cached: boolean;
|
||||
auth_id: string;
|
||||
download_state:
|
||||
| "downloading"
|
||||
| "uploading"
|
||||
| "stalled (no seeds)"
|
||||
| "paused"
|
||||
| "completed"
|
||||
| "cached"
|
||||
| "metaDL"
|
||||
| "checkingResumeData";
|
||||
seeds: number;
|
||||
ratio: number;
|
||||
progress: number;
|
||||
download_speed: number;
|
||||
upload_speed: number;
|
||||
name: string;
|
||||
eta: number;
|
||||
files: TorBoxFile[];
|
||||
}
|
||||
|
||||
export interface TorBoxTorrentInfoRequest {
|
||||
success: boolean;
|
||||
detail: string;
|
||||
error: string;
|
||||
data: TorBoxTorrentInfo[];
|
||||
}
|
||||
|
||||
export interface TorBoxAddTorrentRequest {
|
||||
success: boolean;
|
||||
detail: string;
|
||||
error: string;
|
||||
data: {
|
||||
torrent_id: number;
|
||||
name: string;
|
||||
hash: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TorBoxRequestLinkRequest {
|
||||
success: boolean;
|
||||
detail: string;
|
||||
error: string;
|
||||
data: string;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue