updated plugin refactor and added FTP and SCP plugins , also hydrusnetwork plugin migration

This commit is contained in:
2026-04-27 21:17:53 -07:00
parent bfd5c20dc3
commit 8685fbb723
24 changed files with 3650 additions and 405 deletions
+87 -11
View File
@@ -12,10 +12,18 @@ from pathlib import Path
from typing import Any, Dict, List, Optional
from ProviderCore.base import Provider, SearchResult
from SYS.logger import log, debug
from SYS.logger import log, debug, debug_panel
from SYS.models import ProgressBar
_SOULSEEK_NOISE_SUBSTRINGS = (
"unhandled exception on loop",
"Task exception was never retrieved",
"future: <Task finished",
"ConnectionFailedError",
"PeerConnectionError",
"indirect connection failed",
"indirect connection timed out",
"failed to connect",
"search reply ticket does not match any search request",
"failed to receive transfer ticket on file connection",
"aioslsk.exceptions.ConnectionReadError",
@@ -59,10 +67,10 @@ async def _suppress_aioslsk_asyncio_task_noise() -> Any:
if msg == "Task exception was never retrieved":
cls = getattr(exc, "__class__", None)
name = getattr(cls, "__name__", "")
mod = getattr(cls, "__module__", "")
exc_text = str(exc or "").lower()
# Suppress ConnectionFailedError from aioslsk
if name == "ConnectionFailedError" and str(mod).startswith("aioslsk"):
# Suppress expected peer direct-connect failures from aioslsk.
if name == "ConnectionFailedError" or "failed to connect" in exc_text:
return
except Exception:
# If our filter logic fails, fall through to default handling.
@@ -117,6 +125,9 @@ class _LineFilterStream(io.TextIOBase):
self._in_tb = False
self._tb_lines: list[str] = []
self._tb_suppress = False
self._in_task_block = False
self._task_lines: list[str] = []
self._task_suppress = False
def writable(self) -> bool: # pragma: no cover
return True
@@ -137,6 +148,19 @@ class _LineFilterStream(io.TextIOBase):
self._tb_suppress = False
self._in_tb = False
def _flush_task_block(self) -> None:
if not self._task_lines:
return
if not self._task_suppress:
for l in self._task_lines:
try:
self._underlying.write(l + "\n")
except Exception:
pass
self._task_lines = []
self._task_suppress = False
self._in_task_block = False
def write(self, s: str) -> int:
self._buf += str(s)
while "\n" in self._buf:
@@ -145,6 +169,29 @@ class _LineFilterStream(io.TextIOBase):
return len(s)
def _handle_line(self, line: str) -> None:
if not self._in_task_block and self._should_suppress_line(line) and (
line.startswith("Task exception was never retrieved")
or line.startswith("future: <Task finished")
or line.startswith("unhandled exception on loop")
):
self._in_task_block = True
self._task_lines = [line]
self._task_suppress = True
return
if self._in_task_block:
if line.startswith("Traceback (most recent call last):"):
self._in_tb = True
self._tb_lines = [line]
self._tb_suppress = True
return
self._task_lines.append(line)
if self._should_suppress_line(line):
self._task_suppress = True
if line.strip() == "":
self._flush_task_block()
return
# 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
@@ -159,6 +206,8 @@ class _LineFilterStream(io.TextIOBase):
# End traceback block on blank line.
if line.strip() == "":
self._flush_tb()
if self._in_task_block:
self._flush_task_block()
return
# Non-traceback line
@@ -174,6 +223,8 @@ class _LineFilterStream(io.TextIOBase):
if self._in_tb:
# If the traceback ends without a trailing blank line, decide here.
self._flush_tb()
if self._in_task_block:
self._flush_task_block()
if self._buf:
line = self._buf
self._buf = ""
@@ -422,14 +473,14 @@ class Soulseek(Provider):
try:
search_request = await client.searches.search(query)
await self._collect_results(search_request, timeout=timeout)
return self._flatten_results(search_request)[:limit]
summary = await self._collect_results(search_request, timeout=timeout)
return self._flatten_results(search_request)[:limit], summary
except Exception as exc:
log(
f"[soulseek] Search error: {type(exc).__name__}: {exc}",
file=sys.stderr
)
return []
return [], {}
finally:
# Best-effort: try to cancel/close the search request before stopping
# the client to reduce stray reply spam.
@@ -477,16 +528,24 @@ class Soulseek(Provider):
self,
search_request: Any,
timeout: float = 75.0
) -> None:
end = time.time() + timeout
) -> Dict[str, Any]:
start = time.time()
end = start + timeout
last_count = 0
update_count = 0
while time.time() < end:
current_count = len(getattr(search_request, "results", []))
if current_count > last_count:
debug(f"[soulseek] Got {current_count} result(s)...")
last_count = current_count
update_count += 1
await asyncio.sleep(0.5)
return {
"peer_hits": last_count,
"count_updates": update_count,
"elapsed_seconds": round(max(0.0, time.time() - start), 1),
}
def search(
self,
query: str,
@@ -503,7 +562,7 @@ class Soulseek(Provider):
base_tmp.mkdir(parents=True, exist_ok=True)
try:
flat_results = asyncio.run(
flat_results, search_summary = asyncio.run(
self.perform_search(query,
timeout=9.0,
limit=limit)
@@ -636,6 +695,23 @@ class Soulseek(Provider):
)
)
try:
debug_panel(
"soulseek search",
[
("query", query),
("peer_hits", search_summary.get("peer_hits", 0)),
("file_hits", len(flat_results)),
("audio_hits", len(music_results)),
("results", len(results)),
("poll_updates", search_summary.get("count_updates", 0)),
("elapsed_s", search_summary.get("elapsed_seconds", 0.0)),
],
border_style="magenta",
)
except Exception:
pass
return results
except Exception as exc: