feat: add metadata enrichers & refactor
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Author: Uyanide pywang0608@foxmail.com
|
||||
Date: 2026-03-25 02:33:26
|
||||
Description: Fetcher pipeline — registry and types
|
||||
"""
|
||||
|
||||
from typing import Literal
|
||||
|
||||
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 .netease import NeteaseFetcher
|
||||
from .qqmusic import QQMusicFetcher
|
||||
from ..cache import CacheEngine
|
||||
|
||||
METHODS = (
|
||||
"local",
|
||||
"cache-search",
|
||||
"spotify",
|
||||
"lrclib",
|
||||
"lrclib-search",
|
||||
"netease",
|
||||
"qqmusic",
|
||||
)
|
||||
|
||||
FetcherMethodType = Literal[
|
||||
"local",
|
||||
"cache-search",
|
||||
"spotify",
|
||||
"lrclib",
|
||||
"lrclib-search",
|
||||
"netease",
|
||||
"qqmusic",
|
||||
]
|
||||
|
||||
|
||||
def create_fetchers(cache: CacheEngine) -> dict[str, BaseFetcher]:
|
||||
"""Instantiate all fetchers. Returns a dict keyed by source name."""
|
||||
fetchers: dict[str, BaseFetcher] = {
|
||||
"local": LocalFetcher(),
|
||||
"cache-search": CacheSearchFetcher(cache),
|
||||
"spotify": SpotifyFetcher(),
|
||||
"lrclib": LrclibFetcher(),
|
||||
"lrclib-search": LrclibSearchFetcher(),
|
||||
"netease": NeteaseFetcher(),
|
||||
"qqmusic": QQMusicFetcher(),
|
||||
}
|
||||
assert set(fetchers) == set(METHODS), (
|
||||
f"METHODS and fetchers out of sync: {set(METHODS) ^ set(fetchers)}"
|
||||
)
|
||||
return fetchers
|
||||
|
||||
@@ -17,7 +17,14 @@ class BaseFetcher(ABC):
|
||||
"""Name of the fetcher source."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def self_cached(self) -> bool:
|
||||
"""True if this fetcher manages its own cache (skip per-source cache check)."""
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
def fetch(self, track: TrackMeta) -> Optional[LyricResult]:
|
||||
def fetch(
|
||||
self, track: TrackMeta, bypass_cache: bool = False
|
||||
) -> Optional[LyricResult]:
|
||||
"""Fetch lyrics for the given track. Returns None if unable to fetch."""
|
||||
pass
|
||||
|
||||
@@ -26,7 +26,17 @@ class CacheSearchFetcher(BaseFetcher):
|
||||
def source_name(self) -> str:
|
||||
return "cache-search"
|
||||
|
||||
def fetch(self, track: TrackMeta) -> Optional[LyricResult]:
|
||||
@property
|
||||
def self_cached(self) -> bool:
|
||||
return True
|
||||
|
||||
def fetch(
|
||||
self, track: TrackMeta, bypass_cache: bool = False
|
||||
) -> Optional[LyricResult]:
|
||||
if bypass_cache:
|
||||
logger.debug("Cache-search: bypassed by caller")
|
||||
return None
|
||||
|
||||
if not track.title:
|
||||
logger.debug("Cache-search: skipped — no title")
|
||||
return None
|
||||
|
||||
@@ -25,7 +25,9 @@ class LocalFetcher(BaseFetcher):
|
||||
def source_name(self) -> str:
|
||||
return "local"
|
||||
|
||||
def fetch(self, track: TrackMeta) -> Optional[LyricResult]:
|
||||
def fetch(
|
||||
self, track: TrackMeta, bypass_cache: bool = False
|
||||
) -> Optional[LyricResult]:
|
||||
"""Attempt to read lyrics from local filesystem."""
|
||||
if not track.is_local or not track.url:
|
||||
return None
|
||||
|
||||
@@ -30,7 +30,9 @@ class LrclibFetcher(BaseFetcher):
|
||||
def source_name(self) -> str:
|
||||
return "lrclib"
|
||||
|
||||
def fetch(self, track: TrackMeta) -> Optional[LyricResult]:
|
||||
def fetch(
|
||||
self, track: TrackMeta, bypass_cache: bool = False
|
||||
) -> Optional[LyricResult]:
|
||||
"""Fetch lyrics from LRCLIB. Requires complete metadata."""
|
||||
if not track.is_complete:
|
||||
logger.debug("LRCLIB: skipped — incomplete metadata")
|
||||
|
||||
@@ -32,7 +32,9 @@ class LrclibSearchFetcher(BaseFetcher):
|
||||
def source_name(self) -> str:
|
||||
return "lrclib-search"
|
||||
|
||||
def fetch(self, track: TrackMeta) -> Optional[LyricResult]:
|
||||
def fetch(
|
||||
self, track: TrackMeta, bypass_cache: bool = False
|
||||
) -> Optional[LyricResult]:
|
||||
"""Search LRCLIB for lyrics. Requires at least a title."""
|
||||
if not track.title:
|
||||
logger.debug("LRCLIB-search: skipped — no title")
|
||||
|
||||
@@ -194,7 +194,9 @@ class NeteaseFetcher(BaseFetcher):
|
||||
logger.error(f"Netease: lyric fetch failed for song_id={song_id}: {e}")
|
||||
return LyricResult(status=CacheStatus.NETWORK_ERROR, ttl=TTL_NETWORK_ERROR)
|
||||
|
||||
def fetch(self, track: TrackMeta) -> Optional[LyricResult]:
|
||||
def fetch(
|
||||
self, track: TrackMeta, bypass_cache: bool = False
|
||||
) -> Optional[LyricResult]:
|
||||
"""Search for the track and fetch its lyrics."""
|
||||
query = f"{track.artist or ''} {track.title or ''}".strip()
|
||||
if not query:
|
||||
|
||||
@@ -155,7 +155,9 @@ class QQMusicFetcher(BaseFetcher):
|
||||
logger.error(f"QQMusic: lyric fetch failed for mid={mid}: {e}")
|
||||
return LyricResult(status=CacheStatus.NETWORK_ERROR, ttl=TTL_NETWORK_ERROR)
|
||||
|
||||
def fetch(self, track: TrackMeta) -> Optional[LyricResult]:
|
||||
def fetch(
|
||||
self, track: TrackMeta, bypass_cache: bool = False
|
||||
) -> Optional[LyricResult]:
|
||||
"""Search for the track and fetch its lyrics."""
|
||||
if not QQ_MUSIC_API_URL:
|
||||
logger.debug("QQMusic: skipped — QQ_MUSIC_API_URL not configured")
|
||||
|
||||
@@ -274,7 +274,9 @@ class SpotifyFetcher(BaseFetcher):
|
||||
continue
|
||||
return False
|
||||
|
||||
def fetch(self, track: TrackMeta) -> Optional[LyricResult]:
|
||||
def fetch(
|
||||
self, track: TrackMeta, bypass_cache: bool = False
|
||||
) -> Optional[LyricResult]:
|
||||
"""Fetch lyrics for a Spotify track by its track ID."""
|
||||
if not track.trackid:
|
||||
logger.debug("Spotify: skipped — no trackid in metadata")
|
||||
|
||||
Reference in New Issue
Block a user