mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-06-02 06:14:48 +02:00
66438a5657
* test * test2 * Fixed selecting drive * working transfer manual +drives * fixed transfer + % button display * Add file transfer speed, ETA, and cancel confirmation, switch from powershell to CMD move * Adds pause/resume controls for game file transfers Introduces handlers and UI elements to allow users to pause and resume game file transfers, improving control over transfer processes. Updates transfer error messages for greater user clarity. Enhances UX by making transfer states and errors more descriptive and actionable. * new branch * fix 1.0 * Delete Future-updates.md * fix 1.1 * BYTE-BASED progress * working on faster moving * steam alike moving files * feat: add native C++ move engine for game transfers * removed pause + resume * moving + cancel 1.0 * commit 1 * removed extra file + res/ps buttons * finall * final 1.1 * final 1.2 * Added `transfer-game-completed` * fixed cancel window * Fixed ETA format * Changing CSS * Done !! * fixed better cancel algorythm * deleted extra files + increased performance * added back transfer % button in the panel * fix security check + if game is in the same directory * fixing annotations * fixed security checks 2.0 * Fix line endings * fix prettier formatting * chore: remove unused Visual Studio files and update translation strings for file transfer feature - Deleted various Visual Studio workspace and project files to clean up the repository. - Updated translation files for English, Spanish, Portuguese, and Russian to include new keys related to the file transfer feature, enhancing user experience. - Enabled logging interceptor in the Hydra API for improved network logging. - Refactored logging in the process watcher to include detailed playtime trace logs for better tracking of game sessions. * chore: update husky scripts and clean up code formatting - Changed husky pre-commit and pre-push scripts to use 'yarn run' for consistency. - Updated the process-watcher to streamline the trackGamePlaytime function call. - Improved code formatting in the GeneralSettingsSection component for better readability. * refactor: improve drive querying and error handling in getAvailableDrives - Refactored the getAvailableDrives function to enhance readability and maintainability. - Introduced helper functions for PowerShell path retrieval and drive CSV parsing. - Replaced console logging with appropriate error handling and logging. - Updated the GeneralSettingsSection component to improve error handling and drive selection logic. - Added a new error display in the UI for better user feedback on drive selection issues. * refactor: enhance drive querying and UI components for game options - Updated drive querying logic to support both Windows and Linux platforms. - Replaced synchronous PowerShell calls with asynchronous file system operations for improved performance. - Added a new "locations" category in the game options modal with relevant UI components. - Enhanced the GeneralSettingsSection to conditionally render sections based on props, improving flexibility. - Updated styles for selected options in the game options modal for better visual feedback. * chore: clean up game options modal - Cleaned up the GeneralSettingsSection in the game options modal for improved readability and maintainability. * chore: remove move_engine.cc - Deleted the move_engine.cc file as part of project cleanup. * refactor: enhance game file transfer validation and update logic - Improved path normalization for game root and target root to ensure consistent comparisons across platforms. - Added checks for existing destination folders and enhanced error handling during database updates after file transfers. - Updated the game options initial category to "locations" in the hero panel actions for better user experience. * chore: format error handling in game file transfer - Reformatted the error handling code in transfer-game-files.ts for improved readability. * chore: update translation strings and remove TO-DO file - Added new translation keys for transfer-related messages in English, Spanish, Portuguese, and Russian to enhance user experience. - Removed the obsolete TO-DO.md file as it is no longer needed. - Updated subproject commit to indicate a dirty state. * fix: streamline error handling in game options modal - Simplified the error handling logic in the GameOptionsModal by removing unnecessary line breaks for better readability. --------- Co-authored-by: yassine <166349232+yassine808@users.noreply.github.com> Co-authored-by: Helio Kroger <me@heliokroger.com>
392 lines
15 KiB
Python
392 lines
15 KiB
Python
import logging
|
|
import threading
|
|
import time
|
|
from typing import List, Optional, Set
|
|
|
|
import libtorrent as lt
|
|
|
|
|
|
class TorrentDownloader:
|
|
def __init__(
|
|
self,
|
|
torrent_session,
|
|
flags=lt.torrent_flags.auto_managed,
|
|
session_lock: Optional[threading.RLock] = None,
|
|
):
|
|
self.torrent_handle = None
|
|
self.session = torrent_session
|
|
self.flags = flags
|
|
self.session_lock = session_lock or threading.RLock()
|
|
self.selected_file_indices = None
|
|
self.selected_size_bytes = None
|
|
self.logger = logging.getLogger("hydra.torrent")
|
|
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 set_download_limit(self, max_download_speed: int = None):
|
|
download_limit = (
|
|
max_download_speed if max_download_speed and max_download_speed > 0 else 0
|
|
)
|
|
try:
|
|
self.session.set_download_rate_limit(download_limit)
|
|
except Exception:
|
|
pass
|
|
|
|
def _wait_for_metadata(self, timeout_seconds: float = 30.0, poll_interval: float = 0.25):
|
|
if not self.torrent_handle or not self.torrent_handle.is_valid():
|
|
return False
|
|
|
|
deadline = time.monotonic() + max(timeout_seconds, 1.0)
|
|
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
status = self.torrent_handle.status()
|
|
except RuntimeError:
|
|
return False
|
|
|
|
if status.has_metadata:
|
|
return True
|
|
|
|
time.sleep(max(poll_interval, 0.05))
|
|
|
|
return False
|
|
|
|
def wait_for_metadata(self, timeout_seconds: float = 30.0):
|
|
return self._wait_for_metadata(timeout_seconds=timeout_seconds)
|
|
|
|
def _sanitize_file_indices(self, file_indices: List[int], files_storage):
|
|
if file_indices is None:
|
|
return None
|
|
|
|
if not isinstance(file_indices, list):
|
|
raise ValueError("invalid_file_indices")
|
|
|
|
max_index = files_storage.num_files() - 1
|
|
sanitized: Set[int] = set()
|
|
|
|
for index in file_indices:
|
|
if isinstance(index, bool) or not isinstance(index, int):
|
|
raise ValueError("invalid_file_indices")
|
|
|
|
if index < 0 or index > max_index:
|
|
raise ValueError("invalid_file_indices")
|
|
|
|
sanitized.add(index)
|
|
|
|
if not sanitized:
|
|
raise ValueError("empty_selection")
|
|
|
|
return sorted(sanitized)
|
|
|
|
def _set_selected_file_priorities(self, selected_indices: List[int], files_storage):
|
|
priorities = [0] * files_storage.num_files()
|
|
for index in selected_indices:
|
|
priorities[index] = 1
|
|
|
|
self.torrent_handle.prioritize_files(priorities)
|
|
|
|
deadline = time.monotonic() + 3.0
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
current_priorities = [int(priority) for priority in self.torrent_handle.get_file_priorities()]
|
|
except RuntimeError:
|
|
break
|
|
|
|
if current_priorities == priorities:
|
|
return
|
|
|
|
time.sleep(0.1)
|
|
|
|
self.logger.warning("File priority synchronization timeout")
|
|
|
|
def start_download(
|
|
self,
|
|
magnet: str,
|
|
save_path: str,
|
|
file_indices: Optional[List[int]] = None,
|
|
wait_timeout_seconds: float = 30.0,
|
|
):
|
|
selective_download = file_indices is not None
|
|
|
|
with self.session_lock:
|
|
if self.torrent_handle and self.torrent_handle.is_valid():
|
|
if not selective_download:
|
|
self.torrent_handle.set_flags(lt.torrent_flags.auto_managed)
|
|
self.torrent_handle.resume()
|
|
return
|
|
|
|
self.torrent_handle.pause()
|
|
self.session.remove_torrent(self.torrent_handle)
|
|
self.torrent_handle = None
|
|
|
|
initial_flags = self.flags | lt.torrent_flags.paused
|
|
|
|
if selective_download:
|
|
initial_flags |= lt.torrent_flags.default_dont_download
|
|
initial_flags |= lt.torrent_flags.auto_managed
|
|
else:
|
|
initial_flags |= lt.torrent_flags.auto_managed
|
|
|
|
params = {
|
|
"url": magnet,
|
|
"save_path": save_path,
|
|
"trackers": self.trackers,
|
|
"flags": initial_flags,
|
|
}
|
|
|
|
if self.torrent_handle is None or not self.torrent_handle.is_valid():
|
|
self.torrent_handle = self.session.add_torrent(params)
|
|
|
|
self.selected_file_indices = None
|
|
self.selected_size_bytes = None
|
|
|
|
if selective_download:
|
|
try:
|
|
self.torrent_handle.set_flags(lt.torrent_flags.auto_managed)
|
|
self.torrent_handle.resume()
|
|
|
|
if not self._wait_for_metadata(timeout_seconds=wait_timeout_seconds):
|
|
raise TimeoutError("metadata_timeout")
|
|
|
|
try:
|
|
info = self.torrent_handle.get_torrent_info()
|
|
files_storage = info.files()
|
|
except RuntimeError as error:
|
|
raise RuntimeError("metadata_incomplete") from error
|
|
|
|
self.torrent_handle.pause()
|
|
self.torrent_handle.unset_flags(lt.torrent_flags.auto_managed)
|
|
|
|
sanitized_indices = self._sanitize_file_indices(file_indices, files_storage)
|
|
self._set_selected_file_priorities(sanitized_indices, files_storage)
|
|
|
|
self.selected_file_indices = sanitized_indices
|
|
self.selected_size_bytes = sum(files_storage.file_size(index) for index in sanitized_indices)
|
|
except Exception:
|
|
self.cancel_download()
|
|
raise
|
|
|
|
self.torrent_handle.set_flags(lt.torrent_flags.auto_managed)
|
|
self.torrent_handle.resume()
|
|
|
|
def get_torrent_files(self, timeout_seconds: float = 30.0, max_files: int = 100000):
|
|
if not self._wait_for_metadata(timeout_seconds=timeout_seconds):
|
|
raise TimeoutError("metadata_timeout")
|
|
|
|
try:
|
|
info = self.torrent_handle.get_torrent_info()
|
|
except RuntimeError as error:
|
|
raise RuntimeError("metadata_incomplete") from error
|
|
|
|
files_storage = info.files()
|
|
file_count = files_storage.num_files()
|
|
|
|
if file_count > max_files:
|
|
raise OverflowError("too_many_files")
|
|
|
|
files = []
|
|
for index in range(file_count):
|
|
files.append(
|
|
{
|
|
"index": index,
|
|
"path": files_storage.file_path(index),
|
|
"length": files_storage.file_size(index),
|
|
}
|
|
)
|
|
|
|
return {
|
|
"name": info.name(),
|
|
"totalSize": info.total_size(),
|
|
"files": files,
|
|
}
|
|
|
|
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):
|
|
with self.session_lock:
|
|
if self.torrent_handle:
|
|
if self.torrent_handle.is_valid():
|
|
self.torrent_handle.pause()
|
|
self.session.remove_torrent(self.torrent_handle, lt.session.delete_partfile)
|
|
self.torrent_handle = None
|
|
self.selected_file_indices = None
|
|
self.selected_size_bytes = None
|
|
|
|
def abort_session(self):
|
|
self.cancel_download()
|
|
self.session.abort()
|
|
self.torrent_handle = None
|
|
self.selected_file_indices = None
|
|
self.selected_size_bytes = None
|
|
|
|
def _get_handle_status(self):
|
|
if self.torrent_handle is None:
|
|
return None
|
|
|
|
if not self.torrent_handle.is_valid():
|
|
return None
|
|
|
|
try:
|
|
return self.torrent_handle.status()
|
|
except RuntimeError:
|
|
return None
|
|
|
|
def _get_torrent_info_if_available(self, status):
|
|
if not status.has_metadata:
|
|
return None
|
|
|
|
try:
|
|
return self.torrent_handle.get_torrent_info()
|
|
except RuntimeError:
|
|
return None
|
|
|
|
def _get_file_size(self, status, info):
|
|
total_wanted = getattr(status, "total_wanted", 0)
|
|
if total_wanted > 0:
|
|
return total_wanted
|
|
|
|
if self.selected_size_bytes is not None:
|
|
return self.selected_size_bytes
|
|
|
|
if info:
|
|
return info.total_size()
|
|
|
|
return 0
|
|
|
|
def _get_bytes_downloaded(self, status, file_size):
|
|
total_wanted_done = getattr(status, "total_wanted_done", -1)
|
|
if total_wanted_done >= 0:
|
|
return total_wanted_done
|
|
|
|
if file_size > 0:
|
|
return int(status.progress * file_size)
|
|
|
|
return status.all_time_download
|
|
|
|
def _get_progress(self, status, file_size, bytes_downloaded):
|
|
if file_size <= 0:
|
|
return status.progress
|
|
|
|
return min(max(bytes_downloaded / file_size, 0), 1)
|
|
|
|
def get_download_status(self):
|
|
status = self._get_handle_status()
|
|
if status is None:
|
|
return None
|
|
|
|
info = self._get_torrent_info_if_available(status)
|
|
file_size = self._get_file_size(status, info)
|
|
bytes_downloaded = self._get_bytes_downloaded(status, file_size)
|
|
progress = self._get_progress(status, file_size, bytes_downloaded)
|
|
|
|
response = {
|
|
'folderName': info.name() if info else "",
|
|
'fileSize': file_size,
|
|
'progress': progress,
|
|
'downloadSpeed': status.download_rate,
|
|
'uploadSpeed': status.upload_rate,
|
|
'numPeers': status.num_peers,
|
|
'numSeeds': status.num_seeds,
|
|
'status': status.state,
|
|
'bytesDownloaded': bytes_downloaded,
|
|
}
|
|
|
|
return response
|