dfslkjelf
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any, Dict, Iterable, List, Optional
|
||||
|
||||
from ProviderCore.base import SearchProvider, SearchResult
|
||||
from ProviderCore.download import sanitize_filename
|
||||
from SYS.logger import log
|
||||
|
||||
|
||||
@@ -66,6 +68,89 @@ class AllDebrid(SearchProvider):
|
||||
# Consider "available" when configured; actual API connectivity can vary.
|
||||
return bool(_get_debrid_api_key(self.config or {}))
|
||||
|
||||
def download(self, result: SearchResult, output_dir: Path) -> Optional[Path]:
|
||||
"""Download an AllDebrid SearchResult into output_dir.
|
||||
|
||||
AllDebrid magnet file listings often provide links that require an API
|
||||
"unlock" step to produce a true direct-download URL. Without unlocking,
|
||||
callers may download a small HTML/redirect page instead of file bytes.
|
||||
|
||||
This is used by the download-file cmdlet when a provider item is piped.
|
||||
"""
|
||||
try:
|
||||
api_key = _get_debrid_api_key(self.config or {})
|
||||
if not api_key:
|
||||
return None
|
||||
|
||||
target = str(getattr(result, "path", "") or "").strip()
|
||||
if not target.startswith(("http://", "https://")):
|
||||
return None
|
||||
|
||||
try:
|
||||
from API.alldebrid import AllDebridClient
|
||||
|
||||
client = AllDebridClient(api_key)
|
||||
except Exception as exc:
|
||||
log(f"[alldebrid] Failed to init client: {exc}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
# Quiet mode when download-file is mid-pipeline.
|
||||
quiet = bool(self.config.get("_quiet_background_output")) if isinstance(self.config, dict) else False
|
||||
|
||||
unlocked_url = target
|
||||
try:
|
||||
unlocked = client.unlock_link(target)
|
||||
if isinstance(unlocked, str) and unlocked.strip().startswith(("http://", "https://")):
|
||||
unlocked_url = unlocked.strip()
|
||||
except Exception as exc:
|
||||
# Fall back to the raw link, but warn.
|
||||
log(f"[alldebrid] Failed to unlock link: {exc}", file=sys.stderr)
|
||||
|
||||
# Prefer provider title as the output filename.
|
||||
suggested = sanitize_filename(str(getattr(result, "title", "") or "").strip())
|
||||
suggested_name = suggested if suggested else None
|
||||
|
||||
try:
|
||||
from SYS.download import _download_direct_file
|
||||
|
||||
dl_res = _download_direct_file(
|
||||
unlocked_url,
|
||||
Path(output_dir),
|
||||
quiet=quiet,
|
||||
suggested_filename=suggested_name,
|
||||
)
|
||||
downloaded_path = getattr(dl_res, "path", None)
|
||||
if downloaded_path is None:
|
||||
return None
|
||||
downloaded_path = Path(str(downloaded_path))
|
||||
|
||||
# Guard: if we got an HTML error/redirect page, treat as failure.
|
||||
try:
|
||||
if downloaded_path.exists():
|
||||
size = downloaded_path.stat().st_size
|
||||
if size > 0 and size <= 250_000 and downloaded_path.suffix.lower() not in (".html", ".htm"):
|
||||
head = downloaded_path.read_bytes()[:512]
|
||||
try:
|
||||
text = head.decode("utf-8", errors="ignore").lower()
|
||||
except Exception:
|
||||
text = ""
|
||||
if "<html" in text or "<!doctype html" in text:
|
||||
try:
|
||||
downloaded_path.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
log("[alldebrid] Download returned HTML page (not file bytes). Try again or check AllDebrid link status.", file=sys.stderr)
|
||||
return None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return downloaded_path if downloaded_path.exists() else None
|
||||
except Exception as exc:
|
||||
log(f"[alldebrid] Download failed: {exc}", file=sys.stderr)
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _flatten_files(items: Any) -> Iterable[Dict[str, Any]]:
|
||||
"""Flatten AllDebrid magnet file tree into file dicts.
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import Any, Dict, List, Optional
|
||||
|
||||
from ProviderCore.base import SearchProvider, SearchResult
|
||||
from SYS.logger import log, debug
|
||||
from models import ProgressBar
|
||||
|
||||
|
||||
_SOULSEEK_NOISE_SUBSTRINGS = (
|
||||
@@ -502,58 +503,145 @@ async def download_soulseek_file(
|
||||
raise RuntimeError("Soulseek credentials not configured (set provider=soulseek username/password)")
|
||||
|
||||
settings = Settings(credentials=CredentialsSettings(username=login_user, password=login_pass))
|
||||
client = SoulSeekClient(settings)
|
||||
|
||||
with _suppress_aioslsk_noise():
|
||||
try:
|
||||
await client.start()
|
||||
await client.login()
|
||||
debug(f"[soulseek] Logged in as {login_user}")
|
||||
|
||||
debug(f"[soulseek] Requesting download from {username}: {filename}")
|
||||
|
||||
transfer = await client.transfers.add(Transfer(username, filename, TransferDirection.DOWNLOAD))
|
||||
transfer.local_path = str(output_path)
|
||||
await client.transfers.queue(transfer)
|
||||
|
||||
start_time = time.time()
|
||||
last_log_time = 0.0
|
||||
while not transfer.is_finalized():
|
||||
if time.time() - start_time > timeout:
|
||||
log(f"[soulseek] Download timeout after {timeout}s", file=sys.stderr)
|
||||
return None
|
||||
|
||||
if time.time() - last_log_time >= 5.0 and transfer.bytes_transfered > 0:
|
||||
progress = (transfer.bytes_transfered / transfer.filesize * 100) if transfer.filesize else 0
|
||||
debug(
|
||||
f"[soulseek] Progress: {progress:.1f}% "
|
||||
f"({transfer.bytes_transfered}/{transfer.filesize})"
|
||||
)
|
||||
last_log_time = time.time()
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
if transfer.state.VALUE == TransferState.COMPLETE and transfer.local_path:
|
||||
downloaded_path = Path(transfer.local_path)
|
||||
if downloaded_path.exists():
|
||||
debug(f"[soulseek] Download complete: {downloaded_path}")
|
||||
return downloaded_path
|
||||
|
||||
log(f"[soulseek] Transfer completed but file missing: {downloaded_path}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
log(
|
||||
f"[soulseek] Download failed: state={transfer.state.VALUE} "
|
||||
f"bytes={transfer.bytes_transfered}/{transfer.filesize}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return None
|
||||
|
||||
finally:
|
||||
async def _attempt_once(attempt_num: int) -> tuple[Optional[Path], Any, int, float]:
|
||||
client = SoulSeekClient(settings)
|
||||
with _suppress_aioslsk_noise():
|
||||
try:
|
||||
await client.stop()
|
||||
except Exception:
|
||||
pass
|
||||
await client.start()
|
||||
await client.login()
|
||||
debug(f"[soulseek] Logged in as {login_user}")
|
||||
|
||||
log(
|
||||
f"[soulseek] Download attempt {attempt_num}: {username} :: {local_filename}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
debug(f"[soulseek] Requesting download from {username}: {filename}")
|
||||
|
||||
transfer = await client.transfers.add(Transfer(username, filename, TransferDirection.DOWNLOAD))
|
||||
transfer.local_path = str(output_path)
|
||||
await client.transfers.queue(transfer)
|
||||
|
||||
start_time = time.time()
|
||||
last_progress_time = start_time
|
||||
progress_bar = ProgressBar()
|
||||
|
||||
while not transfer.is_finalized():
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > timeout:
|
||||
log(f"[soulseek] Download timeout after {timeout}s", file=sys.stderr)
|
||||
bytes_done = int(getattr(transfer, "bytes_transfered", 0) or 0)
|
||||
state_val = getattr(getattr(transfer, "state", None), "VALUE", None)
|
||||
try:
|
||||
if getattr(sys.stderr, "isatty", lambda: False)():
|
||||
sys.stderr.write("\r" + (" " * 140) + "\r")
|
||||
sys.stderr.flush()
|
||||
except Exception:
|
||||
pass
|
||||
return None, state_val, bytes_done, elapsed
|
||||
|
||||
bytes_done = int(getattr(transfer, "bytes_transfered", 0) or 0)
|
||||
total_bytes = int(getattr(transfer, "filesize", 0) or 0)
|
||||
now = time.time()
|
||||
if now - last_progress_time >= 0.5:
|
||||
percent = (bytes_done / total_bytes) * 100.0 if total_bytes > 0 else 0.0
|
||||
speed = bytes_done / elapsed if elapsed > 0 else 0.0
|
||||
eta_str: Optional[str] = None
|
||||
if total_bytes > 0 and speed > 0:
|
||||
try:
|
||||
eta_seconds = max(0.0, float(total_bytes - bytes_done) / float(speed))
|
||||
minutes, seconds = divmod(int(eta_seconds), 60)
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
eta_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
||||
except Exception:
|
||||
eta_str = None
|
||||
|
||||
speed_str = progress_bar.format_bytes(speed) + "/s"
|
||||
progress_line = progress_bar.format_progress(
|
||||
percent_str=f"{percent:.1f}%",
|
||||
downloaded=bytes_done,
|
||||
total=total_bytes if total_bytes > 0 else None,
|
||||
speed_str=speed_str,
|
||||
eta_str=eta_str,
|
||||
)
|
||||
|
||||
try:
|
||||
if getattr(sys.stderr, "isatty", lambda: False)():
|
||||
sys.stderr.write("\r" + progress_line + " ")
|
||||
sys.stderr.flush()
|
||||
else:
|
||||
log(progress_line, file=sys.stderr)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
last_progress_time = now
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
final_state = getattr(getattr(transfer, "state", None), "VALUE", None)
|
||||
downloaded_path = Path(transfer.local_path) if getattr(transfer, "local_path", None) else output_path
|
||||
final_elapsed = time.time() - start_time
|
||||
|
||||
# Clear in-place progress bar.
|
||||
try:
|
||||
if getattr(sys.stderr, "isatty", lambda: False)():
|
||||
sys.stderr.write("\r" + (" " * 140) + "\r")
|
||||
sys.stderr.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# If a file was written, treat it as success even if state is odd.
|
||||
try:
|
||||
if downloaded_path.exists() and downloaded_path.stat().st_size > 0:
|
||||
if final_state != TransferState.COMPLETE:
|
||||
log(
|
||||
f"[soulseek] Transfer finalized as {final_state}, but file exists ({downloaded_path.stat().st_size} bytes). Keeping file.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return downloaded_path, final_state, int(downloaded_path.stat().st_size), final_elapsed
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if final_state == TransferState.COMPLETE and downloaded_path.exists():
|
||||
debug(f"[soulseek] Download complete: {downloaded_path}")
|
||||
return downloaded_path, final_state, int(downloaded_path.stat().st_size), final_elapsed
|
||||
|
||||
fail_bytes = int(getattr(transfer, "bytes_transfered", 0) or 0)
|
||||
fail_total = int(getattr(transfer, "filesize", 0) or 0)
|
||||
reason = getattr(transfer, "reason", None)
|
||||
log(
|
||||
f"[soulseek] Download failed: state={final_state} bytes={fail_bytes}/{fail_total} reason={reason}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
# Clean up 0-byte placeholder.
|
||||
try:
|
||||
if downloaded_path.exists() and downloaded_path.stat().st_size == 0:
|
||||
downloaded_path.unlink(missing_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
return None, final_state, fail_bytes, final_elapsed
|
||||
|
||||
finally:
|
||||
try:
|
||||
await client.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Retry a couple times only for fast 0-byte failures (common transient case).
|
||||
max_attempts = 3
|
||||
for attempt in range(1, max_attempts + 1):
|
||||
result_path, final_state, bytes_done, elapsed = await _attempt_once(attempt)
|
||||
if result_path:
|
||||
return result_path
|
||||
|
||||
should_retry = (bytes_done == 0) and (elapsed < 15.0)
|
||||
if attempt < max_attempts and should_retry:
|
||||
log(f"[soulseek] Retrying after fast failure (state={final_state})", file=sys.stderr)
|
||||
await asyncio.sleep(2)
|
||||
continue
|
||||
break
|
||||
return None
|
||||
|
||||
except ImportError:
|
||||
log("[soulseek] aioslsk not installed. Install with: pip install aioslsk", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user