Skip to content

Commit

Permalink
Merge pull request #1223 from hydralauncher/feature/seed-completed-do…
Browse files Browse the repository at this point in the history
…wnloads

Feature/seed completed downloads
Hachi-R authored Dec 23, 2024
2 parents 2c09520 + b8f5f90 commit 1479c15
Showing 62 changed files with 1,763 additions and 829 deletions.
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
MAIN_VITE_API_URL=API_URL
MAIN_VITE_AUTH_URL=AUTH_URL
MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -3,3 +3,4 @@ dist
out
.gitignore
migration.stub
hydra-python-rpc/
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ jobs:
run: pip install -r requirements.txt

- name: Build with cx_Freeze
run: python torrent-client/setup.py build
run: python python_rpc/setup.py build

- name: Build Linux
if: matrix.os == 'ubuntu-latest'
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
.vscode/
node_modules/
hydra-download-manager/
fastlist.exe
__pycache__
dist
out
.DS_Store
*.log*
.env
.vite
ludusavi/
ludusavi/
hydra-python-rpc/
aria2/
3 changes: 2 additions & 1 deletion electron-builder.yml
Original file line number Diff line number Diff line change
@@ -3,8 +3,9 @@ productName: Hydra
directories:
buildResources: build
extraResources:
- aria2
- ludusavi
- hydra-download-manager
- hydra-python-rpc
- seeds
- from: node_modules/create-desktop-shortcuts/src/windows.vbs
- from: resources/achievement.wav
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -37,6 +37,7 @@
"@fontsource/noto-sans": "^5.1.0",
"@hookform/resolvers": "^3.9.1",
"@primer/octicons-react": "^19.9.0",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@reduxjs/toolkit": "^2.2.3",
"@vanilla-extract/css": "^1.14.2",
"@vanilla-extract/dynamic": "^2.1.2",
47 changes: 47 additions & 0 deletions python_rpc/http_downloader.py
Original file line number Diff line number Diff line change
@@ -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
176 changes: 176 additions & 0 deletions python_rpc/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
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]
start_download_payload = sys.argv[4]
start_seeding_payload = sys.argv[5]

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)})

if start_download_payload:
initial_download = json.loads(urllib.parse.unquote(start_download_payload))
downloading_game_id = initial_download['game_id']

if initial_download['url'].startswith('magnet'):
torrent_downloader = TorrentDownloader(torrent_session)
downloads[initial_download['game_id']] = torrent_downloader
torrent_downloader.start_download(initial_download['url'], initial_download['save_path'], "")
else:
http_downloader = HttpDownloader()
downloads[initial_download['game_id']] = http_downloader
http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'))

if start_seeding_payload:
initial_seeding = json.loads(urllib.parse.unquote(start_seeding_payload))
for seed in initial_seeding:
torrent_downloader = TorrentDownloader(torrent_session)
downloads[seed['game_id']] = torrent_downloader
torrent_downloader.start_download(seed['url'], seed['save_path'], "")

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

seed_status = []

for game_id, downloader in downloads.items():
if not downloader:
continue

response = downloader.get_download_status()
if response is None:
continue

if response.get('status') == 5:
seed_status.append({
'gameId': game_id,
**response,
})

return jsonify(seed_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', 'name'])]
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 url.startswith('magnet'):
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
existing_downloader.start_download(url, data['save_path'], "")
else:
torrent_downloader = TorrentDownloader(torrent_session)
downloads[game_id] = torrent_downloader
torrent_downloader.start_download(url, data['save_path'], "")
else:
if existing_downloader and isinstance(existing_downloader, HttpDownloader):
existing_downloader.start_download(url, data['save_path'], data.get('header'))
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 == 'resume_seeding':
torrent_downloader = TorrentDownloader(torrent_session)
downloads[game_id] = torrent_downloader
torrent_downloader.start_download(data['url'], data['save_path'], "")
elif action == 'pause_seeding':
downloader = downloads.get(game_id)
if downloader:
downloader.cancel_download()

else:
return jsonify({"error": "Invalid action"}), 400

return "", 200

if __name__ == "__main__":
app.run(host="0.0.0.0", port=int(http_port))

Original file line number Diff line number Diff line change
@@ -15,8 +15,8 @@ def get_parsed_image_data(image_path):
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"
new_uuid = str(uuid.uuid4())
new_image_path = os.path.join(tempfile.gettempdir(), new_uuid) + ".webp"
image.save(new_image_path)

new_image = Image.open(new_image_path)
8 changes: 4 additions & 4 deletions torrent-client/setup.py → python_rpc/setup.py
Original file line number Diff line number Diff line change
@@ -3,18 +3,18 @@
# Dependencies are automatically detected, but it might need fine tuning.
build_exe_options = {
"packages": ["libtorrent"],
"build_exe": "hydra-download-manager",
"build_exe": "hydra-python-rpc",
"include_msvcr": True
}

setup(
name="hydra-download-manager",
name="hydra-python-rpc",
version="0.1",
description="Hydra",
options={"build_exe": build_exe_options},
executables=[Executable(
"torrent-client/main.py",
target_name="hydra-download-manager",
"python_rpc/main.py",
target_name="hydra-python-rpc",
icon="build/icon.ico"
)]
)
Loading

0 comments on commit 1479c15

Please sign in to comment.