lkjlkj
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
@@ -11,6 +15,143 @@ from Provider._base import SearchProvider, SearchResult
|
||||
from SYS.logger import log, debug
|
||||
|
||||
|
||||
_SOULSEEK_NOISE_SUBSTRINGS = (
|
||||
"search reply ticket does not match any search request",
|
||||
"failed to receive transfer ticket on file connection",
|
||||
"aioslsk.exceptions.ConnectionReadError",
|
||||
)
|
||||
|
||||
|
||||
def _configure_aioslsk_logging() -> None:
|
||||
"""Reduce aioslsk internal log noise.
|
||||
|
||||
Some aioslsk components emit non-fatal warnings/errors during high churn
|
||||
(search + download + disconnect). We keep our own debug output, but push
|
||||
aioslsk to ERROR and stop propagation so it doesn't spam the CLI.
|
||||
"""
|
||||
for name in (
|
||||
"aioslsk",
|
||||
"aioslsk.network",
|
||||
"aioslsk.search",
|
||||
"aioslsk.transfer",
|
||||
"aioslsk.transfer.manager",
|
||||
):
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(logging.ERROR)
|
||||
logger.propagate = False
|
||||
|
||||
|
||||
class _LineFilterStream(io.TextIOBase):
|
||||
"""A minimal stream wrapper that filters known noisy lines.
|
||||
|
||||
It also suppresses entire traceback blocks when they contain known non-fatal
|
||||
aioslsk noise (e.g. ConnectionReadError during peer init).
|
||||
"""
|
||||
|
||||
def __init__(self, underlying: Any, suppress_substrings: tuple[str, ...]):
|
||||
super().__init__()
|
||||
self._underlying = underlying
|
||||
self._suppress = suppress_substrings
|
||||
self._buf = ""
|
||||
self._in_tb = False
|
||||
self._tb_lines: list[str] = []
|
||||
self._tb_suppress = False
|
||||
|
||||
def writable(self) -> bool: # pragma: no cover
|
||||
return True
|
||||
|
||||
def _should_suppress_line(self, line: str) -> bool:
|
||||
return any(sub in line for sub in self._suppress)
|
||||
|
||||
def _flush_tb(self) -> None:
|
||||
if not self._tb_lines:
|
||||
return
|
||||
if not self._tb_suppress:
|
||||
for l in self._tb_lines:
|
||||
try:
|
||||
self._underlying.write(l + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
self._tb_lines = []
|
||||
self._tb_suppress = False
|
||||
self._in_tb = False
|
||||
|
||||
def write(self, s: str) -> int:
|
||||
self._buf += str(s)
|
||||
while "\n" in self._buf:
|
||||
line, self._buf = self._buf.split("\n", 1)
|
||||
self._handle_line(line)
|
||||
return len(s)
|
||||
|
||||
def _handle_line(self, line: str) -> None:
|
||||
# Start capturing tracebacks so we can suppress the whole block if it matches.
|
||||
if not self._in_tb and line.startswith("Traceback (most recent call last):"):
|
||||
self._in_tb = True
|
||||
self._tb_lines = [line]
|
||||
self._tb_suppress = False
|
||||
return
|
||||
|
||||
if self._in_tb:
|
||||
self._tb_lines.append(line)
|
||||
if self._should_suppress_line(line):
|
||||
self._tb_suppress = True
|
||||
# End traceback block on blank line.
|
||||
if line.strip() == "":
|
||||
self._flush_tb()
|
||||
return
|
||||
|
||||
# Non-traceback line
|
||||
if self._should_suppress_line(line):
|
||||
return
|
||||
try:
|
||||
self._underlying.write(line + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def flush(self) -> None:
|
||||
# Flush any pending traceback block.
|
||||
if self._in_tb:
|
||||
# If the traceback ends without a trailing blank line, decide here.
|
||||
self._flush_tb()
|
||||
if self._buf:
|
||||
line = self._buf
|
||||
self._buf = ""
|
||||
if not self._should_suppress_line(line):
|
||||
try:
|
||||
self._underlying.write(line)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self._underlying.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _suppress_aioslsk_noise() -> Any:
|
||||
"""Temporarily suppress known aioslsk noise printed to stdout/stderr.
|
||||
|
||||
Opt out by setting DOWNLOW_SOULSEEK_VERBOSE=1.
|
||||
"""
|
||||
if os.environ.get("DOWNLOW_SOULSEEK_VERBOSE"):
|
||||
yield
|
||||
return
|
||||
|
||||
_configure_aioslsk_logging()
|
||||
old_out, old_err = sys.stdout, sys.stderr
|
||||
sys.stdout = _LineFilterStream(old_out, _SOULSEEK_NOISE_SUBSTRINGS)
|
||||
sys.stderr = _LineFilterStream(old_err, _SOULSEEK_NOISE_SUBSTRINGS)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
try:
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
except Exception:
|
||||
pass
|
||||
sys.stdout, sys.stderr = old_out, old_err
|
||||
|
||||
|
||||
class Soulseek(SearchProvider):
|
||||
"""Search provider for Soulseek P2P network."""
|
||||
|
||||
@@ -90,7 +231,6 @@ class Soulseek(SearchProvider):
|
||||
async def perform_search(self, query: str, timeout: float = 9.0, limit: int = 50) -> List[Dict[str, Any]]:
|
||||
"""Perform async Soulseek search."""
|
||||
|
||||
import os
|
||||
from aioslsk.client import SoulSeekClient
|
||||
from aioslsk.settings import CredentialsSettings, Settings
|
||||
|
||||
@@ -99,25 +239,37 @@ class Soulseek(SearchProvider):
|
||||
settings = Settings(credentials=CredentialsSettings(username=self.USERNAME, password=self.PASSWORD))
|
||||
client = SoulSeekClient(settings)
|
||||
|
||||
try:
|
||||
await client.start()
|
||||
await client.login()
|
||||
except Exception as exc:
|
||||
log(f"[soulseek] Login failed: {type(exc).__name__}: {exc}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
try:
|
||||
search_request = await client.searches.search(query)
|
||||
await self._collect_results(search_request, timeout=timeout)
|
||||
return self._flatten_results(search_request)[:limit]
|
||||
except Exception as exc:
|
||||
log(f"[soulseek] Search error: {type(exc).__name__}: {exc}", file=sys.stderr)
|
||||
return []
|
||||
finally:
|
||||
with _suppress_aioslsk_noise():
|
||||
try:
|
||||
await client.stop()
|
||||
except Exception:
|
||||
pass
|
||||
await client.start()
|
||||
await client.login()
|
||||
except Exception as exc:
|
||||
log(f"[soulseek] Login failed: {type(exc).__name__}: {exc}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
try:
|
||||
search_request = await client.searches.search(query)
|
||||
await self._collect_results(search_request, timeout=timeout)
|
||||
return self._flatten_results(search_request)[:limit]
|
||||
except Exception as exc:
|
||||
log(f"[soulseek] Search error: {type(exc).__name__}: {exc}", file=sys.stderr)
|
||||
return []
|
||||
finally:
|
||||
# Best-effort: try to cancel/close the search request before stopping
|
||||
# the client to reduce stray reply spam.
|
||||
try:
|
||||
if "search_request" in locals() and search_request is not None:
|
||||
cancel = getattr(search_request, "cancel", None)
|
||||
if callable(cancel):
|
||||
maybe = cancel()
|
||||
if asyncio.iscoroutine(maybe):
|
||||
await maybe
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
await client.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _flatten_results(self, search_request: Any) -> List[dict]:
|
||||
flat: List[dict] = []
|
||||
@@ -322,55 +474,56 @@ async def download_soulseek_file(
|
||||
settings = Settings(credentials=CredentialsSettings(username=Soulseek.USERNAME, password=Soulseek.PASSWORD))
|
||||
client = SoulSeekClient(settings)
|
||||
|
||||
try:
|
||||
await client.start()
|
||||
await client.login()
|
||||
debug(f"[soulseek] Logged in as {Soulseek.USERNAME}")
|
||||
with _suppress_aioslsk_noise():
|
||||
try:
|
||||
await client.start()
|
||||
await client.login()
|
||||
debug(f"[soulseek] Logged in as {Soulseek.USERNAME}")
|
||||
|
||||
debug(f"[soulseek] Requesting download from {username}: {filename}")
|
||||
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)
|
||||
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)
|
||||
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
|
||||
|
||||
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)
|
||||
log(
|
||||
f"[soulseek] Download failed: state={transfer.state.VALUE} "
|
||||
f"bytes={transfer.bytes_transfered}/{transfer.filesize}",
|
||||
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:
|
||||
try:
|
||||
await client.stop()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
await client.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except ImportError:
|
||||
log("[soulseek] aioslsk not installed. Install with: pip install aioslsk", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user