Cin-1: Make DownloadSourcePlugin inheritance explicit on every client

Cin's review feedback: the plugin contract was discoverable only
from the registry, not from the client files themselves. Reading
`youtube_client.py` cold gave no signal that the class participates
in the DownloadSourcePlugin contract.

Every download client class now inherits DownloadSourcePlugin
explicitly:
- SoulseekClient(DownloadSourcePlugin)
- YouTubeClient(DownloadSourcePlugin)
- TidalDownloadClient(DownloadSourcePlugin)
- QobuzClient(DownloadSourcePlugin)
- HiFiClient(DownloadSourcePlugin)
- DeezerDownloadClient(DownloadSourcePlugin)
- SoundcloudClient(DownloadSourcePlugin)
- LidarrDownloadClient(DownloadSourcePlugin)

Adjustments:
- core/download_plugins/base.py — moved TrackResult/AlbumResult/
  DownloadStatus imports under TYPE_CHECKING since they're only
  used in type annotations. Without this, clients inheriting the
  contract create a circular import.
- core/download_plugins/__init__.py — drops DownloadPluginRegistry
  re-export. Importing the package no longer triggers the registry's
  eager client imports (which would also be circular for clients
  that import from the package). Callers that need the registry
  import it directly: `from core.download_plugins.registry import
  DownloadPluginRegistry`.

Suite still green (335 download tests).
This commit is contained in:
Broque Thomas 2026-05-04 22:19:52 -07:00
parent 0ee092979e
commit ea654f664e
10 changed files with 53 additions and 15 deletions

View file

@ -79,7 +79,10 @@ def _decrypt_chunk(chunk: bytes, key: bytes) -> bytes:
) from exc
class DeezerDownloadClient:
from core.download_plugins.base import DownloadSourcePlugin
class DeezerDownloadClient(DownloadSourcePlugin):
"""Deezer download client using ARL token authentication."""
def __init__(self, download_path: str = None):

View file

@ -17,9 +17,18 @@ See `core/download_plugins/base.py` for the protocol contract and
"""
from core.download_plugins.base import DownloadSourcePlugin
from core.download_plugins.registry import DownloadPluginRegistry
# NOTE: DownloadPluginRegistry is intentionally NOT re-exported here.
# Importing the registry triggers eager imports of every client class
# (see registry.py for why eager — test fixtures inject mock modules
# at collection time and we need real bindings before that). Clients
# inherit from DownloadSourcePlugin (Cin's review feedback — visible
# contract conformance), so importing the package via ``from
# core.download_plugins import DownloadSourcePlugin`` from a client
# file would create a circular import if registry came along for the
# ride. Callers that need the registry import it directly:
# from core.download_plugins.registry import DownloadPluginRegistry
__all__ = [
"DownloadSourcePlugin",
"DownloadPluginRegistry",
]

View file

@ -27,12 +27,17 @@ makes the implicit contract explicit so:
from __future__ import annotations
from typing import List, Optional, Protocol, Tuple, runtime_checkable
from typing import TYPE_CHECKING, List, Optional, Protocol, Tuple, runtime_checkable
# Soulseek client owns the canonical TrackResult / AlbumResult / DownloadStatus
# dataclasses — every other source already imports from there. Keep it that way
# until a future PR lifts those into a neutral location.
from core.soulseek_client import AlbumResult, DownloadStatus, TrackResult
# Soulseek client owns the canonical TrackResult / AlbumResult /
# DownloadStatus dataclasses — every other source already imports
# from there. We only need them for type annotations on this
# protocol; using TYPE_CHECKING avoids a circular import once the
# clients themselves inherit from DownloadSourcePlugin (Cin's
# review feedback — clients explicitly declare conformance instead
# of relying on structural typing).
if TYPE_CHECKING:
from core.soulseek_client import AlbumResult, DownloadStatus, TrackResult
@runtime_checkable

View file

@ -86,7 +86,10 @@ DEFAULT_INSTANCES = [
]
class HiFiClient:
from core.download_plugins.base import DownloadSourcePlugin
class HiFiClient(DownloadSourcePlugin):
"""
HiFi API client for searching and downloading lossless music.
Uses public hifi-api instances (Tidal backend) no auth required.

View file

@ -33,7 +33,10 @@ from core.soulseek_client import TrackResult, AlbumResult, DownloadStatus
logger = get_logger("lidarr_client")
class LidarrDownloadClient:
from core.download_plugins.base import DownloadSourcePlugin
class LidarrDownloadClient(DownloadSourcePlugin):
"""Lidarr download client — uses Lidarr as a download source for Usenet/torrent content.
Implements the same interface as SoulseekClient, QobuzClient, TidalDownloadClient

View file

@ -104,7 +104,10 @@ QOBUZ_QUALITY_MAP = {
}
class QobuzClient:
from core.download_plugins.base import DownloadSourcePlugin
class QobuzClient(DownloadSourcePlugin):
"""
Qobuz download client using Qobuz REST API.
Provides search, matching, and download capabilities as a drop-in alternative to Soulseek/YouTube/Tidal.

View file

@ -188,7 +188,10 @@ class DownloadStatus:
time_remaining: Optional[int] = None
file_path: Optional[str] = None
class SoulseekClient:
from core.download_plugins.base import DownloadSourcePlugin
class SoulseekClient(DownloadSourcePlugin):
def __init__(self):
self.base_url: Optional[str] = None
self.api_key: Optional[str] = None

View file

@ -82,7 +82,10 @@ def _sanitize_filename(name: str) -> str:
return cleaned or 'soundcloud_track'
class SoundcloudClient:
from core.download_plugins.base import DownloadSourcePlugin
class SoundcloudClient(DownloadSourcePlugin):
"""SoundCloud download client built on yt-dlp's SoundCloud extractor.
Mirrors the public surface of TidalDownloadClient / QobuzClient so the

View file

@ -92,7 +92,10 @@ HLS_QUALITY_MAP = {
HLS_MAP_TAG_RE = re.compile(r'#EXT-X-MAP:.*URI="([^"]+)"')
class TidalDownloadClient:
from core.download_plugins.base import DownloadSourcePlugin
class TidalDownloadClient(DownloadSourcePlugin):
"""
Tidal download client using tidalapi.
Provides search, matching, and download capabilities as a drop-in alternative to YouTube/Soulseek.

View file

@ -91,7 +91,10 @@ class YouTubeSearchResult:
self.parsed_artist = self.channel
class YouTubeClient:
from core.download_plugins.base import DownloadSourcePlugin
class YouTubeClient(DownloadSourcePlugin):
"""
YouTube download client using yt-dlp.
Provides search, matching, and download capabilities with full Spotify metadata integration.