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

Feature/seed completed downloads #1223

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
21ca26d
feat: update schema with seeding columns
Hachi-R Nov 8, 2024
9c9c0e6
feat: seed after doenload
Hachi-R Nov 8, 2024
c556a00
feat: get seed status
Hachi-R Nov 8, 2024
b32952f
feat: add ablity to pause and resume the seeding process
Hachi-R Nov 9, 2024
94b65c0
lint
Hachi-R Nov 9, 2024
7c039ea
feat: add seeding management logic
Hachi-R Nov 9, 2024
5078946
refactor: change logic to seed new downloads
Hachi-R Nov 9, 2024
c314c39
feat: add option to disable seeding after download completes
Hachi-R Nov 9, 2024
8ec52bf
temp
Hachi-R Nov 9, 2024
5668794
feat: display upload speed during seeding
Hachi-R Nov 9, 2024
9619578
lint
Hachi-R Nov 9, 2024
518d919
feat: add locale strings
Hachi-R Nov 9, 2024
f66bdd7
feat: pause seeding before deleting game
Hachi-R Nov 9, 2024
610b6e5
Merge branch 'main' into feature/seed-completed-downloads
thegrannychaseroperation Nov 9, 2024
1416cd4
feat: seed downloads from previous versions
Hachi-R Nov 9, 2024
a7b8018
"feat: pause seeding if game folder is deleted"
Hachi-R Nov 11, 2024
40ec773
lint
Hachi-R Nov 11, 2024
2c1c3e3
feat: add dropdown menu component
Hachi-R Nov 12, 2024
ca953de
lint
Hachi-R Nov 12, 2024
94ef167
feat: add menu with download options on download page
Hachi-R Nov 12, 2024
9d1c04d
refactor: export with component definition
Hachi-R Nov 12, 2024
3303413
lint
Hachi-R Nov 12, 2024
130c236
Merge branch 'main' into feature/seed-completed-downloads
Hachi-R Nov 12, 2024
f2cc20c
refactor: removed dropdown title because its ugly
Hachi-R Nov 16, 2024
f35c34f
fix: fixing bottom panel scss
thegrannychaseroperation Nov 28, 2024
6259cf4
Merge branch 'main' of github.com:hydralauncher/hydra into feature/se…
thegrannychaseroperation Nov 28, 2024
2d8b63c
Merge branch 'feature/seed-completed-downloads' of github.com:hydrala…
thegrannychaseroperation Nov 28, 2024
4060f7a
feat: adding aria2c
thegrannychaseroperation Nov 28, 2024
fe8f962
feat: adding aria2c
thegrannychaseroperation Nov 28, 2024
a847f93
feat: adding aria2c
thegrannychaseroperation Nov 28, 2024
c6d4b65
feat: adding aria2c
thegrannychaseroperation Nov 28, 2024
d7e06d6
feat: adding aria2c
thegrannychaseroperation Nov 28, 2024
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
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
MAIN_VITE_API_URL=API_URL
MAIN_VITE_AUTH_URL=AUTH_URL
MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY
RENDERER_VITE_INTERCOM_APP_ID=YOUR_APP_ID
Binary file added aria2/aria2c
Copy link
Collaborator

Choose a reason for hiding this comment

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

Binary added to git by mistake

Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@hookform/resolvers": "^3.9.0",
"@intercom/messenger-js-sdk": "^0.0.14",
"@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.1",
Expand Down
73 changes: 73 additions & 0 deletions postinstall.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { default: axios } = require("axios");
const util = require("node:util");
const fs = require("node:fs");
const path = require("node:path");
const { spawnSync } = require("node:child_process");

const exec = util.promisify(require("node:child_process").exec);

Expand Down Expand Up @@ -46,4 +47,76 @@ const downloadLudusavi = async () => {
});
};

const downloadAria2WindowsAndLinux = 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);
});
};

const copyAria2Macos = async () => {
console.log("Checking if aria2 is installed...");

const isAria2Installed = spawnSync("which", ["aria2c"]).status;

if (isAria2Installed != 0) {
console.log("Please install aria2");
console.log("brew install aria2");
return;
}

console.log("Copying aria2 binary...");
fs.mkdirSync("aria2");
await exec(`cp $(which aria2c) aria2/aria2c`);
};

if (process.platform === "win32") {
fs.copyFileSync(
"node_modules/ps-list/vendor/fastlist-0.3.0-x64.exe",
"fastlist.exe"
);
}

if (process.platform == "darwin") {
copyAria2Macos();
} else {
downloadAria2WindowsAndLinux();
}

downloadLudusavi();
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
136 changes: 136 additions & 0 deletions python_rpc/main.py
Original file line number Diff line number Diff line change
@@ -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))

8 changes: 4 additions & 4 deletions torrent-client/setup.py → python_rpc/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading