mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 13:34:54 +03:00
feat: adding aria2c
This commit is contained in:
parent
c6d4b658a1
commit
d7e06d6622
BIN
aria2/aria2c
Normal file
BIN
aria2/aria2c
Normal file
Binary file not shown.
47
python_rpc/http_downloader.py
Normal file
47
python_rpc/http_downloader.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import aria2p
|
||||||
|
|
||||||
|
class HttpDownloader:
|
||||||
|
def __init__(self):
|
||||||
|
self.download = None
|
||||||
|
self.aria2 = aria2p.API(
|
||||||
|
aria2p.Client(
|
||||||
|
host="http://localhost",
|
||||||
|
port=6800,
|
||||||
|
secret=""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def start_download(self, url: str, save_path: str, header: str):
|
||||||
|
if self.download:
|
||||||
|
self.aria2.resume([self.download])
|
||||||
|
else:
|
||||||
|
downloads = self.aria2.add(url, options={"header": header, "dir": save_path})
|
||||||
|
self.download = downloads[0]
|
||||||
|
|
||||||
|
def pause_download(self):
|
||||||
|
if self.download:
|
||||||
|
self.aria2.pause([self.download])
|
||||||
|
|
||||||
|
def cancel_download(self):
|
||||||
|
if self.download:
|
||||||
|
self.aria2.remove([self.download])
|
||||||
|
self.download = None
|
||||||
|
|
||||||
|
def get_download_status(self):
|
||||||
|
if self.download == None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
download = self.aria2.get_download(self.download.gid)
|
||||||
|
|
||||||
|
response = {
|
||||||
|
'folderName': str(download.dir) + "/" + download.name,
|
||||||
|
'fileSize': download.total_length,
|
||||||
|
'progress': download.completed_length / download.total_length if download.total_length else 0,
|
||||||
|
'downloadSpeed': download.download_speed,
|
||||||
|
'numPeers': 0,
|
||||||
|
'numSeeds': 0,
|
||||||
|
'status': download.status,
|
||||||
|
'bytesDownloaded': download.completed_length,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
136
python_rpc/main.py
Normal file
136
python_rpc/main.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
from flask import Flask, request, jsonify
|
||||||
|
import sys, json, urllib.parse, psutil
|
||||||
|
from torrent_downloader import TorrentDownloader
|
||||||
|
from http_downloader import HttpDownloader
|
||||||
|
from profile_image_processor import ProfileImageProcessor
|
||||||
|
import libtorrent as lt
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Retrieve command line arguments
|
||||||
|
torrent_port = sys.argv[1]
|
||||||
|
http_port = sys.argv[2]
|
||||||
|
rpc_password = sys.argv[3]
|
||||||
|
|
||||||
|
downloads = {}
|
||||||
|
# This can be streamed down from Node
|
||||||
|
downloading_game_id = -1
|
||||||
|
|
||||||
|
torrent_session = lt.session({'listen_interfaces': '0.0.0.0:{port}'.format(port=torrent_port)})
|
||||||
|
|
||||||
|
def validate_rpc_password():
|
||||||
|
"""Middleware to validate RPC password."""
|
||||||
|
header_password = request.headers.get('x-hydra-rpc-password')
|
||||||
|
if header_password != rpc_password:
|
||||||
|
return jsonify({"error": "Unauthorized"}), 401
|
||||||
|
|
||||||
|
@app.route("/status", methods=["GET"])
|
||||||
|
def status():
|
||||||
|
auth_error = validate_rpc_password()
|
||||||
|
if auth_error:
|
||||||
|
return auth_error
|
||||||
|
|
||||||
|
downloader = downloads.get(downloading_game_id)
|
||||||
|
if downloader:
|
||||||
|
status = downloads.get(downloading_game_id).get_download_status()
|
||||||
|
return jsonify(status), 200
|
||||||
|
else:
|
||||||
|
return jsonify(None)
|
||||||
|
|
||||||
|
@app.route("/seed-status", methods=["GET"])
|
||||||
|
def seed_status():
|
||||||
|
auth_error = validate_rpc_password()
|
||||||
|
if auth_error:
|
||||||
|
return auth_error
|
||||||
|
|
||||||
|
status = torrent_downloader.get_seed_status()
|
||||||
|
return jsonify(status), 200
|
||||||
|
|
||||||
|
@app.route("/healthcheck", methods=["GET"])
|
||||||
|
def healthcheck():
|
||||||
|
return "", 200
|
||||||
|
|
||||||
|
@app.route("/process-list", methods=["GET"])
|
||||||
|
def process_list():
|
||||||
|
auth_error = validate_rpc_password()
|
||||||
|
if auth_error:
|
||||||
|
return auth_error
|
||||||
|
|
||||||
|
process_list = [proc.info for proc in psutil.process_iter(['exe', 'pid', 'username'])]
|
||||||
|
return jsonify(process_list), 200
|
||||||
|
|
||||||
|
@app.route("/profile-image", methods=["POST"])
|
||||||
|
def profile_image():
|
||||||
|
auth_error = validate_rpc_password()
|
||||||
|
if auth_error:
|
||||||
|
return auth_error
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
image_path = data.get('image_path')
|
||||||
|
|
||||||
|
try:
|
||||||
|
processed_image_path, mime_type = ProfileImageProcessor.process_image(image_path)
|
||||||
|
return jsonify({'imagePath': processed_image_path, 'mimeType': mime_type}), 200
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 400
|
||||||
|
|
||||||
|
@app.route("/action", methods=["POST"])
|
||||||
|
def action():
|
||||||
|
global torrent_session
|
||||||
|
global downloading_game_id
|
||||||
|
|
||||||
|
auth_error = validate_rpc_password()
|
||||||
|
if auth_error:
|
||||||
|
return auth_error
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
action = data.get('action')
|
||||||
|
game_id = data.get('game_id')
|
||||||
|
|
||||||
|
print(data)
|
||||||
|
|
||||||
|
if action == 'start':
|
||||||
|
url = data.get('url')
|
||||||
|
|
||||||
|
existing_downloader = downloads.get(game_id)
|
||||||
|
|
||||||
|
if existing_downloader:
|
||||||
|
# This will resume the download
|
||||||
|
existing_downloader.start_download(url, data['save_path'], data.get('header'))
|
||||||
|
else:
|
||||||
|
if url.startswith('magnet'):
|
||||||
|
torrent_downloader = TorrentDownloader(torrent_session)
|
||||||
|
downloads[game_id] = torrent_downloader
|
||||||
|
torrent_downloader.start_download(url, data['save_path'], "")
|
||||||
|
else:
|
||||||
|
http_downloader = HttpDownloader()
|
||||||
|
downloads[game_id] = http_downloader
|
||||||
|
http_downloader.start_download(url, data['save_path'], data.get('header'))
|
||||||
|
|
||||||
|
downloading_game_id = game_id
|
||||||
|
|
||||||
|
elif action == 'pause':
|
||||||
|
downloader = downloads.get(game_id)
|
||||||
|
if downloader:
|
||||||
|
downloader.pause_download()
|
||||||
|
downloading_game_id = -1
|
||||||
|
elif action == 'cancel':
|
||||||
|
downloader = downloads.get(game_id)
|
||||||
|
if downloader:
|
||||||
|
downloader.cancel_download()
|
||||||
|
|
||||||
|
# elif action == 'kill-torrent':
|
||||||
|
# torrent_downloader.abort_session()
|
||||||
|
# torrent_downloader = None
|
||||||
|
# elif action == 'pause-seeding':
|
||||||
|
# torrent_downloader.pause_seeding(game_id)
|
||||||
|
# elif action == 'resume-seeding':
|
||||||
|
# torrent_downloader.resume_seeding(game_id, data['url'], data['save_path'])
|
||||||
|
else:
|
||||||
|
return jsonify({"error": "Invalid action"}), 400
|
||||||
|
|
||||||
|
return "", 200
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="0.0.0.0", port=int(http_port))
|
||||||
|
|
30
python_rpc/profile_image_processor.py
Normal file
30
python_rpc/profile_image_processor.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from PIL import Image
|
||||||
|
import os, uuid, tempfile
|
||||||
|
|
||||||
|
class ProfileImageProcessor:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_parsed_image_data(image_path):
|
||||||
|
Image.MAX_IMAGE_PIXELS = 933120000
|
||||||
|
|
||||||
|
image = Image.open(image_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
image.seek(1)
|
||||||
|
except EOFError:
|
||||||
|
mime_type = image.get_format_mimetype()
|
||||||
|
return image_path, mime_type
|
||||||
|
else:
|
||||||
|
newUUID = str(uuid.uuid4())
|
||||||
|
new_image_path = os.path.join(tempfile.gettempdir(), newUUID) + ".webp"
|
||||||
|
image.save(new_image_path)
|
||||||
|
|
||||||
|
new_image = Image.open(new_image_path)
|
||||||
|
mime_type = new_image.get_format_mimetype()
|
||||||
|
|
||||||
|
return new_image_path, mime_type
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def process_image(image_path):
|
||||||
|
return ProfileImageProcessor.get_parsed_image_data(image_path)
|
20
python_rpc/setup.py
Normal file
20
python_rpc/setup.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from cx_Freeze import setup, Executable
|
||||||
|
|
||||||
|
# Dependencies are automatically detected, but it might need fine tuning.
|
||||||
|
build_exe_options = {
|
||||||
|
"packages": ["libtorrent"],
|
||||||
|
"build_exe": "hydra-python-rpc",
|
||||||
|
"include_msvcr": True
|
||||||
|
}
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="hydra-python-rpc",
|
||||||
|
version="0.1",
|
||||||
|
description="Hydra",
|
||||||
|
options={"build_exe": build_exe_options},
|
||||||
|
executables=[Executable(
|
||||||
|
"python_rpc/main.py",
|
||||||
|
target_name="hydra-python-rpc",
|
||||||
|
icon="build/icon.ico"
|
||||||
|
)]
|
||||||
|
)
|
191
python_rpc/torrent_downloader.py
Normal file
191
python_rpc/torrent_downloader.py
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import libtorrent as lt
|
||||||
|
|
||||||
|
class TorrentDownloader:
|
||||||
|
def __init__(self, torrent_session):
|
||||||
|
self.torrent_handle = None
|
||||||
|
self.session = torrent_session
|
||||||
|
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, magnet: str, save_path: str, header: str):
|
||||||
|
params = {'url': magnet, 'save_path': save_path, 'trackers': self.trackers}
|
||||||
|
self.torrent_handle = self.session.add_torrent(params)
|
||||||
|
self.torrent_handle.set_flags(lt.torrent_flags.auto_managed)
|
||||||
|
self.torrent_handle.resume()
|
||||||
|
|
||||||
|
def pause_download(self):
|
||||||
|
if self.torrent_handle:
|
||||||
|
self.torrent_handle.pause()
|
||||||
|
self.torrent_handle.unset_flags(lt.torrent_flags.auto_managed)
|
||||||
|
|
||||||
|
def cancel_download(self):
|
||||||
|
if self.torrent_handle:
|
||||||
|
self.torrent_handle.pause()
|
||||||
|
self.session.remove_torrent(self.torrent_handle)
|
||||||
|
self.torrent_handle = None
|
||||||
|
|
||||||
|
def abort_session(self):
|
||||||
|
for game_id in self.torrent_handles:
|
||||||
|
self.torrent_handle = self.torrent_handles[game_id]
|
||||||
|
self.torrent_handle.pause()
|
||||||
|
self.session.remove_torrent(self.torrent_handle)
|
||||||
|
|
||||||
|
self.session.abort()
|
||||||
|
self.torrent_handle = None
|
||||||
|
|
||||||
|
def get_download_status(self):
|
||||||
|
if self.torrent_handle == None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
status = self.torrent_handle.status()
|
||||||
|
info = self.torrent_handle.get_torrent_info()
|
||||||
|
|
||||||
|
response = {
|
||||||
|
'folderName': info.name() if info else "",
|
||||||
|
'fileSize': info.total_size() if info else 0,
|
||||||
|
'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,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
# def get_seed_status(self):
|
||||||
|
# response = []
|
||||||
|
|
||||||
|
# for game_id, torrent_handle in self.torrent_handles.items():
|
||||||
|
# if game_id == self.downloading_game_id:
|
||||||
|
# continue
|
||||||
|
|
||||||
|
# status = torrent_handle.status()
|
||||||
|
# info = torrent_handle.torrent_file()
|
||||||
|
|
||||||
|
# torrent_info = {
|
||||||
|
# 'folderName': info.name() if info else "",
|
||||||
|
# 'fileSize': info.total_size() if info else 0,
|
||||||
|
# 'gameId': game_id,
|
||||||
|
# 'progress': status.progress,
|
||||||
|
# 'downloadSpeed': status.download_rate,
|
||||||
|
# 'uploadSpeed': status.upload_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,
|
||||||
|
# }
|
||||||
|
|
||||||
|
# if status.state == 5:
|
||||||
|
# response.append(torrent_info)
|
||||||
|
|
||||||
|
# return response
|
||||||
|
|
||||||
|
# def pause_seeding(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.session.remove_torrent(torrent_handle)
|
||||||
|
# self.torrent_handles.pop(game_id, None)
|
||||||
|
|
||||||
|
# def resume_seeding(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()
|
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…
Reference in New Issue
Block a user