Files
Jonathan efdd592761 feat(discover): Last.fm album-popularity helper
LastfmAlbumPopularity.top_albums(conn, name) ranks an artist's albums by
Last.fm playcount (read-through ApiCache, 12h; errors -> []). registry
build_album_popularity returns it only when lastfm.api_key is set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 23:36:41 +02:00

78 lines
3.2 KiB
Python

from lyra_worker._mbbrowser import CachingMbBrowser, MusicBrainzBrowser
from lyra_worker._musicbrainz import MusicBrainzResolver
from lyra_worker._mutagen import MutagenTagger
from lyra_worker._probe import MutagenProbe
from lyra_worker.adapters._slskd import SlskdClient
from lyra_worker.adapters._streamrip import StreamripClient
from lyra_worker.adapters._ytdlp import YtDlpClient
from lyra_worker.adapters.base import SourceAdapter
from lyra_worker.adapters.qobuz import QobuzAdapter
from lyra_worker.adapters.soulseek import SoulseekAdapter
from lyra_worker.adapters.youtube import YouTubeAdapter
from lyra_worker.browser import MbBrowser
from lyra_worker.probe import AudioProbe
from lyra_worker.resolver import MbResolver
from lyra_worker.similarity._lastfm import LastfmSource
from lyra_worker.similarity._lastfm_albums import LastfmAlbumPopularity
from lyra_worker.similarity._listenbrainz import ListenBrainzSource
from lyra_worker.similarity.base import SimilaritySource
from lyra_worker.tagger import Tagger
def build_adapters(config: dict) -> list[SourceAdapter]:
"""Real source adapters, best-tier first, filtered to those that are configured.
An adapter is included only if its health() is true (Qobuz needs credentials,
Soulseek needs an slskd URL + API key). YouTube needs no config.
"""
candidates: list[SourceAdapter] = [
QobuzAdapter(StreamripClient(config)),
SoulseekAdapter(SlskdClient(config)),
YouTubeAdapter(YtDlpClient()),
]
return [a for a in candidates if a.health()]
def build_resolver() -> MbResolver:
"""The MusicBrainz resolver used to canonicalize requests in intake."""
return MusicBrainzResolver()
def build_tagger() -> Tagger:
"""The tagger that writes canonical metadata onto downloaded files."""
return MutagenTagger()
def build_browser(dsn: str | None = None) -> MbBrowser:
"""The MusicBrainz browser used by the background monitor. When a DSN is given, wrap it
in the shared ApiCache read-through so scans/discovery stop re-fetching discographies
(and warm the same cache the web modals read)."""
inner = MusicBrainzBrowser()
return CachingMbBrowser(inner, dsn) if dsn else inner
def build_probe() -> AudioProbe:
"""The audio probe used by the library scan."""
return MutagenProbe()
def build_similarity_sources(config: dict, dsn: str | None = None) -> list[SimilaritySource]:
"""Similarity sources for discovery. ListenBrainz needs no credentials, so it is
always constructed; per-run reachability is checked via each source's health().
Last.fm is added only when an API key is configured. Its MB name→MBID lookups ride the
shared ApiCache when a DSN is given."""
sources: list[SimilaritySource] = [
ListenBrainzSource(base_url=config.get("discover.listenBrainzUrl") or "")
]
key = config.get("lastfm.api_key")
if key:
sources.append(LastfmSource(api_key=key, browser=build_browser(dsn)))
return sources
def build_album_popularity(config: dict, dsn: str | None = None):
"""Last.fm album-popularity provider for discovery, or None when no Last.fm key is set
(discovery then surfaces newest albums only)."""
key = config.get("lastfm.api_key")
return LastfmAlbumPopularity(api_key=key) if key else None