mirror of
https://github.com/PR0M3TH3AN/LBRY-Downloader.git
synced 2026-10-27 08:22:24 +00:00
255 lines
8.8 KiB
Python
255 lines
8.8 KiB
Python
"""Simple direct downloader - uses Odysee's public API."""
|
|
|
|
import json
|
|
import logging
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Dict, Optional
|
|
|
|
try:
|
|
import requests
|
|
except ImportError:
|
|
raise ImportError("requests is required")
|
|
|
|
from models import Config, DownloadAction
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SimpleDirectDownloader:
|
|
"""Simple downloader that gets files from Odysee CDN."""
|
|
|
|
def __init__(self, config: Config, base_dir: Path):
|
|
self.config = config
|
|
self.base_dir = base_dir
|
|
self.session = requests.Session()
|
|
self.session.headers.update(
|
|
{
|
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0"
|
|
}
|
|
)
|
|
|
|
def download(self, action: DownloadAction, dry_run: bool = False) -> bool:
|
|
"""Download a file from Odysee."""
|
|
if action.action == "skip_existing":
|
|
return True
|
|
|
|
if dry_run:
|
|
print(f"[DRY-RUN] Would download: {action.claim_name}")
|
|
return True
|
|
|
|
version_dir = Path(action.target_dir)
|
|
version_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
try:
|
|
print(f"\n📥 Downloading: {action.claim_name}")
|
|
|
|
# Get the Odysee URL from the LBRY URI
|
|
odysee_url = self._get_odysee_url(action.uri)
|
|
print(f" URL: {odysee_url}")
|
|
|
|
# Method 1: Try to get the download link from the page
|
|
download_url = self._get_download_link_from_page(odysee_url)
|
|
|
|
if not download_url:
|
|
# Method 2: Try API
|
|
download_url = self._get_from_api(action.uri)
|
|
|
|
if not download_url:
|
|
print(f" ⚠️ Could not find download URL")
|
|
return False
|
|
|
|
print(f" Download URL found!")
|
|
|
|
# Download the file
|
|
return self._download_file(
|
|
download_url, version_dir, action.claim_name, action
|
|
)
|
|
|
|
except Exception as e:
|
|
print(f" ❌ Error: {e}")
|
|
logger.error(f"Download failed: {e}")
|
|
return False
|
|
|
|
def _get_odysee_url(self, lbry_uri: str) -> str:
|
|
"""Convert LBRY URI to Odysee URL."""
|
|
# lbry://@channel/claim#id -> https://odysee.com/@channel:claim
|
|
uri = lbry_uri.replace("lbry://", "")
|
|
parts = uri.split("/")
|
|
if len(parts) >= 2:
|
|
channel = parts[0].replace("#", ":")
|
|
claim = parts[1].split("#")[0]
|
|
return f"https://odysee.com/{channel}/{claim}"
|
|
return f"https://odysee.com/{uri.replace('#', ':')}"
|
|
|
|
def _get_download_link_from_page(self, odysee_url: str) -> Optional[str]:
|
|
"""Try to extract download link from Odysee page."""
|
|
try:
|
|
response = self.session.get(odysee_url, timeout=30)
|
|
if response.status_code == 200:
|
|
# Look for various patterns
|
|
patterns = [
|
|
r'"content\":\{\"url\":\"([^\"]+)\"',
|
|
r'"streaming_url\":\"([^\"]+)\"',
|
|
r'"download_url\":\"([^\"]+)\"',
|
|
r'"url\":\"(https://player\.odycdn\.com/[^\"]+)\"',
|
|
]
|
|
|
|
for pattern in patterns:
|
|
match = re.search(pattern, response.text)
|
|
if match:
|
|
url = match.group(1).replace("\\u0026", "&")
|
|
if url.startswith("http"):
|
|
return url
|
|
|
|
return None
|
|
except Exception as e:
|
|
logger.debug(f"Could not get from page: {e}")
|
|
return None
|
|
|
|
def _get_from_api(self, lbry_uri: str) -> Optional[str]:
|
|
"""Try to get download URL from Odysee API."""
|
|
try:
|
|
api_url = "https://api.lbry.tv/api/v1/proxy"
|
|
|
|
payload = {
|
|
"jsonrpc": "2.0",
|
|
"method": "resolve",
|
|
"params": {"urls": [lbry_uri]},
|
|
"id": 1,
|
|
}
|
|
|
|
response = self.session.post(api_url, json=payload, timeout=30)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
result = data.get("result", {}).get(lbry_uri, {})
|
|
|
|
# Get claim ID
|
|
claim_id = result.get("claim_id", "")
|
|
if claim_id:
|
|
# Try common Odysee CDN patterns
|
|
urls_to_try = [
|
|
f"https://player.odycdn.com/v6/streams/{claim_id}",
|
|
f"https://player.odycdn.com/v6/streams/{claim_id}/",
|
|
f"https://cdn.lbryplayer.xyz/content/claims/{claim_id}/stream",
|
|
]
|
|
|
|
for url in urls_to_try:
|
|
# Test if URL works
|
|
head_response = self.session.head(
|
|
url, timeout=10, allow_redirects=True
|
|
)
|
|
if head_response.status_code == 200:
|
|
return url
|
|
|
|
return None
|
|
except Exception as e:
|
|
logger.debug(f"API error: {e}")
|
|
return None
|
|
|
|
def _download_file(
|
|
self, url: str, version_dir: Path, claim_name: str, action: DownloadAction
|
|
) -> bool:
|
|
"""Download the file with progress."""
|
|
try:
|
|
# Try to determine filename
|
|
filename = f"{claim_name}.mp4" # Default
|
|
|
|
response = self.session.get(url, stream=True, timeout=300)
|
|
|
|
# Try to get filename from headers
|
|
cd_header = response.headers.get("content-disposition", "")
|
|
if "filename=" in cd_header:
|
|
import re
|
|
|
|
fname_match = re.search(r'filename="?([^"]+)"?', cd_header)
|
|
if fname_match:
|
|
filename = fname_match.group(1)
|
|
|
|
file_path = version_dir / filename
|
|
|
|
total_size = int(response.headers.get("content-length", 0))
|
|
|
|
if total_size > 0:
|
|
size_mb = total_size / (1024 * 1024)
|
|
print(f" Size: {size_mb:.2f} MB")
|
|
|
|
print(f" Saving to: {filename}")
|
|
print()
|
|
|
|
# Download with progress
|
|
downloaded = 0
|
|
start_time = time.time()
|
|
last_update = start_time
|
|
|
|
with open(file_path, "wb") as f:
|
|
for chunk in response.iter_content(chunk_size=8192):
|
|
if chunk:
|
|
f.write(chunk)
|
|
downloaded += len(chunk)
|
|
|
|
current_time = time.time()
|
|
if current_time - last_update >= 0.5:
|
|
self._print_progress(downloaded, total_size, start_time)
|
|
last_update = current_time
|
|
|
|
self._print_progress(downloaded, total_size, start_time, final=True)
|
|
print()
|
|
|
|
# Verify
|
|
if file_path.exists() and file_path.stat().st_size > 0:
|
|
size_mb = file_path.stat().st_size / (1024 * 1024)
|
|
elapsed = time.time() - start_time
|
|
speed = size_mb / elapsed if elapsed > 0 else 0
|
|
print(f" ✅ Complete: {size_mb:.2f} MB @ {speed:.2f} MB/s")
|
|
|
|
# Write metadata
|
|
self._write_metadata(version_dir, action, file_path)
|
|
return True
|
|
else:
|
|
print(f" ❌ Download failed")
|
|
return False
|
|
|
|
except Exception as e:
|
|
print(f" ❌ Error: {e}")
|
|
return False
|
|
|
|
def _print_progress(
|
|
self, downloaded: int, total: int, start_time: float, final: bool = False
|
|
):
|
|
"""Print progress bar."""
|
|
if total > 0:
|
|
percent = min(100, int((downloaded / total) * 100))
|
|
filled = int(40 * percent / 100)
|
|
bar = "█" * filled + "░" * (40 - filled)
|
|
|
|
elapsed = time.time() - start_time
|
|
if elapsed > 0:
|
|
speed = (downloaded / (1024 * 1024)) / elapsed
|
|
print(f"\r [{bar}] {percent}% | {speed:.2f} MB/s", end="", flush=True)
|
|
else:
|
|
mb = downloaded / (1024 * 1024)
|
|
print(f"\r Downloaded: {mb:.2f} MB", end="", flush=True)
|
|
|
|
def _write_metadata(
|
|
self, version_dir: Path, action: DownloadAction, file_path: Path
|
|
):
|
|
"""Write metadata file."""
|
|
metadata = {
|
|
"claim_id": action.claim_id,
|
|
"claim_name": action.claim_name,
|
|
"version_token": action.version_token,
|
|
"uri": action.uri,
|
|
"downloaded_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
"file_name": file_path.name,
|
|
"file_size": file_path.stat().st_size,
|
|
"download_source": "odysee_direct",
|
|
}
|
|
|
|
meta_path = version_dir / "metadata.json"
|
|
with open(meta_path, "w") as f:
|
|
json.dump(metadata, f, indent=2)
|
|
|
|
logger.info(f"Wrote metadata: {meta_path}")
|