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:
parent
0ee092979e
commit
ea654f664e
10 changed files with 53 additions and 15 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue