This commit is contained in:
2026-01-11 04:54:27 -08:00
parent bf8ef6d128
commit 5f8f49c530
6 changed files with 239 additions and 69 deletions

View File

@@ -47,13 +47,23 @@ async def _suppress_aioslsk_asyncio_task_noise() -> Any:
try:
exc = context.get("exception")
msg = str(context.get("message") or "")
# Only suppress un-retrieved task exceptions from aioslsk connection failures.
if msg == "Task exception was never retrieved" and exc is not None:
cls = getattr(exc, "__class__", None)
name = getattr(cls, "__name__", "")
mod = getattr(cls, "__module__", "")
if name == "ConnectionFailedError" and str(mod).startswith("aioslsk"):
return
if exc is not None:
# Suppress internal asyncio AssertionError on Windows teardown (Proactor loop)
if isinstance(exc, AssertionError):
m_lower = msg.lower()
if "proactor" in m_lower or "_start_serving" in m_lower or "self._sockets is not None" in str(exc):
return
# Only suppress un-retrieved task exceptions from aioslsk connection failures.
if msg == "Task exception was never retrieved":
cls = getattr(exc, "__class__", None)
name = getattr(cls, "__name__", "")
mod = getattr(cls, "__module__", "")
# Suppress ConnectionFailedError from aioslsk
if name == "ConnectionFailedError" and str(mod).startswith("aioslsk"):
return
except Exception:
# If our filter logic fails, fall through to default handling.
pass
@@ -206,10 +216,34 @@ def _suppress_aioslsk_noise() -> Any:
class Soulseek(Provider):
TABLE_AUTO_STAGES = {
"soulseek": ["download-file"],
"soulseek": ["download-file", "-provider", "soulseek"],
}
"""Search provider for Soulseek P2P network."""
def selector(
self,
selected_items: List[Any],
*,
ctx: Any,
stage_is_last: bool = True,
**_kwargs: Any,
) -> bool:
"""Handle Soulseek selection.
Currently defaults to download-file via TABLE_AUTO_STAGES, but this
hook allows for future 'Browse User' or 'Browse Folder' drill-down.
"""
if not stage_is_last:
return False
# If we wanted to handle drill-down (like HIFI.py) we would:
# 1. Fetch more data (e.g. user shares)
# 2. Create a new ResultTable
# 3. ctx.set_current_stage_table(new_table)
# 4. return True
return False
@classmethod
def config(cls) -> List[Dict[str, Any]]:
return [
@@ -277,19 +311,34 @@ class Soulseek(Provider):
username = full_metadata.get("username")
filename = full_metadata.get("filename") or result.path
if not username or not filename:
# If we were invoked via generic download-file on a SearchResult
# that has minimal data (e.g. from table selection), try to rescue it.
if isinstance(result, SearchResult) and result.full_metadata:
username = result.full_metadata.get("username")
filename = result.full_metadata.get("filename")
if not username or not filename:
log(
f"[soulseek] Missing metadata for download: {result.title}",
file=sys.stderr
)
return None
# Use tempfile directory as default if '.' or generic placeholder was passed
# by a caller that didn't know better.
target_dir = Path(output_dir)
if str(target_dir) == "." or str(target_dir) == "downloads":
import tempfile
target_dir = Path(tempfile.gettempdir()) / "Medios" / "Soulseek"
target_dir.mkdir(parents=True, exist_ok=True)
# This cmdlet stack is synchronous; use asyncio.run for clarity.
return asyncio.run(
download_soulseek_file(
username=username,
filename=filename,
output_dir=output_dir,
output_dir=target_dir,
timeout=self.MAX_WAIT_TRANSFER,
)
)
@@ -328,7 +377,7 @@ class Soulseek(Provider):
from aioslsk.client import SoulSeekClient
from aioslsk.settings import CredentialsSettings, Settings
os.makedirs(self.DOWNLOAD_DIR, exist_ok=True)
# Removed legacy os.makedirs(self.DOWNLOAD_DIR) - specific commands handle output dirs.
settings = Settings(
credentials=CredentialsSettings(
@@ -376,8 +425,9 @@ class Soulseek(Provider):
await client.stop()
except Exception:
pass
# Give Proactor/Windows loop a moment to drain internal buffers after stop.
try:
await asyncio.sleep(0)
await asyncio.sleep(0.2)
except Exception:
pass
@@ -386,33 +436,19 @@ class Soulseek(Provider):
for result in getattr(search_request, "results", []):
username = getattr(result, "username", "?")
def _add(file_data: Any) -> None:
flat.append({
"file": file_data,
"username": username,
"filename": getattr(file_data, "filename", "?"),
"size": getattr(file_data, "filesize", 0)
})
for file_data in getattr(result, "shared_items", []):
flat.append(
{
"file": file_data,
"username": username,
"filename": getattr(file_data,
"filename",
"?"),
"size": getattr(file_data,
"filesize",
0),
}
)
_add(file_data)
for file_data in getattr(result, "locked_results", []):
flat.append(
{
"file": file_data,
"username": username,
"filename": getattr(file_data,
"filename",
"?"),
"size": getattr(file_data,
"filesize",
0),
}
)
_add(file_data)
return flat
@@ -440,6 +476,11 @@ class Soulseek(Provider):
) -> List[SearchResult]:
filters = filters or {}
# Ensure temp download dir structure exists, but don't create legacy ./downloads here.
import tempfile
base_tmp = Path(tempfile.gettempdir()) / "Medios" / "Soulseek"
base_tmp.mkdir(parents=True, exist_ok=True)
try:
flat_results = asyncio.run(
self.perform_search(query,
@@ -561,6 +602,7 @@ class Soulseek(Provider):
media_kind="audio",
size_bytes=item["size"],
columns=columns,
selection_action=["download-file", "-provider", "soulseek"],
full_metadata={
"username": item["username"],
"filename": item["filename"],
@@ -568,6 +610,7 @@ class Soulseek(Provider):
"album": item["album"],
"track_num": item["track_num"],
"ext": item["ext"],
"provider": "soulseek"
},
)
)
@@ -785,8 +828,9 @@ async def download_soulseek_file(
except Exception:
pass
# Let cancellation/cleanup callbacks run while our exception handler is still installed.
# Increased to 0.2s for Windows Proactor loop stability.
try:
await asyncio.sleep(0)
await asyncio.sleep(0.2)
except Exception:
pass