Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: using python http server #1406

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 141 additions & 134 deletions python_rpc/main.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
from flask import Flask, request, jsonify
import sys, json, urllib.parse, psutil
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
import urllib.parse
import sys
import 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]
http_port = int(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)})
torrent_session = lt.session({'listen_interfaces': f'0.0.0.0:{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
Expand All @@ -49,135 +49,142 @@
except Exception as e:
print("Error starting seeding", e)

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

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'], "")
class RequestHandler(BaseHTTPRequestHandler):
def validate_rpc_password(self):
header_password = self.headers.get('x-hydra-rpc-password')
if header_password != rpc_password:
self.send_response(401)
self.end_headers()
self.wfile.write(json.dumps({"error": "Unauthorized"}).encode('utf-8'))
return False
return True

def do_GET(self):
if self.path == "/status":
if not self.validate_rpc_password():
return

Comment on lines +64 to +65
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: early return without response body could cause client issues - should send 401 response like other endpoints

downloader = downloads.get(downloading_game_id)
if downloader:
status = downloader.get_download_status()
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps(status).encode('utf-8'))
else:
torrent_downloader = TorrentDownloader(torrent_session)
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps(None).encode('utf-8'))

elif self.path == "/seed-status":
if not self.validate_rpc_password():
return

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

self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps(seed_status).encode('utf-8'))

elif self.path == "/healthcheck":
self.send_response(200)
self.end_headers()

elif self.path == "/process-list":
if not self.validate_rpc_password():
return

process_list = [proc.info for proc in psutil.process_iter(['exe', 'pid', 'name'])]
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps(process_list).encode('utf-8'))

def do_POST(self):
if not self.validate_rpc_password():
return

content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
data = json.loads(post_data.decode('utf-8'))

if self.path == "/profile-image":
image_path = data.get('image_path')
try:
processed_image_path, mime_type = ProfileImageProcessor.process_image(image_path)
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps({'imagePath': processed_image_path, 'mimeType': mime_type}).encode('utf-8'))
except Exception as e:
self.send_response(400)
self.end_headers()
self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8'))

elif self.path == "/action":
global downloading_game_id
action = data.get('action')
game_id = data.get('game_id')

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, lt.torrent_flags.upload_mode)
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'))
torrent_downloader.start_download(data['url'], data['save_path'], "")
elif action == 'pause_seeding':
downloader = downloads.get(game_id)
if downloader:
downloader.cancel_download()
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, lt.torrent_flags.upload_mode)
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
self.send_response(400)
self.end_headers()
self.wfile.write(json.dumps({"error": "Invalid action"}).encode('utf-8'))
return

return "", 200
self.send_response(200)
self.end_headers()

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

server = HTTPServer(('0.0.0.0', http_port), RequestHandler)
print(f"Server running on port {http_port}")
server.serve_forever()
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@ cx_Logging; sys_platform == 'win32'
pywin32; sys_platform == 'win32'
psutil
Pillow
flask
aria2p
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: aria2p dependency may no longer be needed since HTTP downloads are now handled by the built-in http.server module

Loading