""" Author: Uyanide pywang0608@foxmail.com Date: 2026-03-25 02:33:26 Description: Fetcher pipeline — registry and types """ from typing import Literal, Optional from loguru import logger from .base import BaseFetcher from .local import LocalFetcher from .cache_search import CacheSearchFetcher from .spotify import SpotifyFetcher from .lrclib import LrclibFetcher from .lrclib_search import LrclibSearchFetcher from .musixmatch import MusixmatchFetcher, MusixmatchSpotifyFetcher from .netease import NeteaseFetcher from .qqmusic import QQMusicFetcher from ..authenticators import ( BaseAuthenticator, SpotifyAuthenticator, MusixmatchAuthenticator, QQMusicAuthenticator, ) from ..cache import CacheEngine from ..models import TrackMeta FetcherMethodType = Literal[ "local", "cache-search", "spotify", "lrclib", "musixmatch-spotify", "lrclib-search", "netease", "qqmusic", "musixmatch", ] # Fetchers within a group run in parallel; groups run sequentially. # A group that produces any trusted and synced result stops the pipeline. _FETCHER_GROUPS: list[list[FetcherMethodType]] = [ ["local"], ["cache-search"], ["spotify"], ["lrclib", "musixmatch-spotify"], ["lrclib-search", "musixmatch"], ["netease", "qqmusic"], ] def create_fetchers( cache: CacheEngine, authenticators: dict[str, BaseAuthenticator], ) -> dict[FetcherMethodType, BaseFetcher]: """Instantiate all fetchers. Returns a dict keyed by source name.""" spotify_auth = authenticators["spotify"] mxm_auth = authenticators["musixmatch"] qqmusic_auth = authenticators.get("qqmusic") assert isinstance(spotify_auth, SpotifyAuthenticator) assert isinstance(mxm_auth, MusixmatchAuthenticator) assert isinstance(qqmusic_auth, QQMusicAuthenticator) fetchers: dict[FetcherMethodType, BaseFetcher] = { "local": LocalFetcher(), "cache-search": CacheSearchFetcher(cache), "spotify": SpotifyFetcher(spotify_auth), "lrclib": LrclibFetcher(), "musixmatch-spotify": MusixmatchSpotifyFetcher(mxm_auth), "lrclib-search": LrclibSearchFetcher(), "netease": NeteaseFetcher(), "qqmusic": QQMusicFetcher(qqmusic_auth), "musixmatch": MusixmatchFetcher(mxm_auth), } return fetchers def build_plan( fetchers: dict[FetcherMethodType, BaseFetcher], track: TrackMeta, force_method: Optional[FetcherMethodType] = None, ) -> list[list[BaseFetcher]]: """Return the fetch plan as a list of groups (each group runs in parallel).""" if force_method: if force_method not in fetchers: logger.error(f"Unknown method: {force_method}") return [] return [[fetchers[force_method]]] plan: list[list[BaseFetcher]] = [] for group_methods in _FETCHER_GROUPS: group = [ fetchers[m] for m in group_methods if m in fetchers and fetchers[m].is_available(track) ] if group: plan.append(group) logger.debug(f"Fetch plan: {[[f.source_name for f in g] for g in plan]}") return plan