From 1967f2d1e46a0a2f0d55cbec1ffb483330d7fe61 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 16 Mar 2026 09:49:55 -0400 Subject: [PATCH] Fix: Use daemon streaming endpoint when P2P peers unavailable - Add fallback to daemon's local streaming endpoint (http://localhost:5280/stream/{sd_hash}) - Reduces P2P wait timeout from 300s to 60s for faster fallback - Downloads content reliably even when no P2P peers are available - Shows progress bar during streaming download - Preserves all metadata and checksums --- downloader.py | 109 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 98 insertions(+), 11 deletions(-) diff --git a/downloader.py b/downloader.py index 22e69af..88f6100 100644 --- a/downloader.py +++ b/downloader.py @@ -11,6 +11,11 @@ from lbry_client import LbryClient from models import Config, DownloadAction from utils import compute_sha256 +try: + import requests +except ImportError: + requests = None + logger = logging.getLogger(__name__) @@ -58,7 +63,7 @@ class Downloader: logger.info(f"Downloading: {action.claim_name}") try: - # Call daemon to download + # Call daemon to start P2P download logger.info(f"Starting download: {action.claim_name}") result = self.client.get( uri=action.uri, @@ -66,23 +71,41 @@ class Downloader: ) claim_id = result.get("claim_id") or action.claim_id + sd_hash = result.get("sd_hash") or action.metadata.get("sd_hash") # Wait for download to complete - this can take time logger.info(f"Waiting for download to complete...") - file_info = self._wait_for_download(claim_id, version_dir, timeout=300) + file_info = self._wait_for_download(claim_id, version_dir, timeout=60) if not file_info: - logger.warning( - f"Download incomplete for {action.claim_name}, saving metadata only" + logger.info( + f"P2P download incomplete, trying daemon streaming endpoint..." ) - # Write metadata even if download didn't complete - self._write_metadata( - version_dir, - action, - {"download_path": None, "file_name": action.claim_name}, + # Try streaming endpoint if P2P fails + target_file = version_dir / ( + action.metadata.get("source_name") or f"{action.claim_name}.zip" ) - self._write_download_json(version_dir, result) - return False + if sd_hash and self._download_via_streaming( + sd_hash, target_file, result, action + ): + file_info = { + "download_path": str(target_file), + "file_name": target_file.name, + "size": target_file.stat().st_size, + "media_type": result.get("mime_type"), + } + else: + logger.warning( + f"Download failed for {action.claim_name} - P2P peers not available" + ) + # Write metadata even if download didn't complete + self._write_metadata( + version_dir, + action, + {"download_path": None, "file_name": action.claim_name}, + ) + self._write_download_json(version_dir, result) + return False download_path_str = file_info.get("download_path") or file_info.get( "file_name" @@ -233,6 +256,70 @@ class Downloader: logger.warning(f"Download timed out after {timeout}s") return None + def _download_via_streaming( + self, sd_hash: str, target_path: Path, file_info: Dict, action: DownloadAction + ) -> bool: + """ + Download file using daemon's streaming endpoint. + + This is more reliable than P2P when peers are unavailable. + The daemon fetches the content and serves it via local HTTP. + + Args: + sd_hash: The SD hash from the claim + target_path: Where to save the file + file_info: File metadata from daemon + action: Download action + + Returns: + True if download succeeded + """ + if not sd_hash or not requests: + return False + + # Build streaming URL from daemon API URL + streaming_url = f"http://localhost:5280/stream/{sd_hash}" + + logger.info(f"Downloading via daemon streaming endpoint...") + + try: + response = requests.get(streaming_url, stream=True, timeout=300) + response.raise_for_status() + + total_size = int(response.headers.get("content-length", 0)) + downloaded = 0 + chunk_size = 8192 + + print(f"\nšŸ“„ Downloading: {action.claim_name}") + print(f" Size: {total_size / (1024 * 1024):.2f} MB") + print() + + with open(target_path, "wb") as f: + for chunk in response.iter_content(chunk_size=chunk_size): + if chunk: + f.write(chunk) + downloaded += len(chunk) + + if total_size > 0: + percent = min(100, int((downloaded / total_size) * 100)) + filled = int(40 * percent / 100) + bar = "ā–ˆ" * filled + "ā–‘" * (40 - filled) + print(f"\r [{bar}] {percent}%", end="", flush=True) + + print() # New line after progress + print(f" āœ… Download complete: {target_path.name}") + + # Update file_info with actual path + file_info["download_path"] = str(target_path) + file_info["file_name"] = target_path.name + file_info["written_bytes"] = target_path.stat().st_size + + return True + + except Exception as e: + logger.warning(f"Streaming download failed: {e}") + return False + def _write_metadata( self, version_dir: Path,