Files
musicseerr/backend/services/search_enrichment_service.py
2026-04-03 15:53:00 +01:00

240 lines
8.2 KiB
Python

import asyncio
import logging
from typing import Optional
from api.v1.schemas.search import (
ArtistEnrichment,
AlbumEnrichment,
EnrichmentBatchRequest,
EnrichmentResponse,
EnrichmentSource,
)
from repositories.protocols import (
MusicBrainzRepositoryProtocol,
ListenBrainzRepositoryProtocol,
LastFmRepositoryProtocol,
)
from services.preferences_service import PreferencesService
logger = logging.getLogger(__name__)
MAX_ENRICHMENT = 10
class SearchEnrichmentService:
def __init__(
self,
mb_repo: MusicBrainzRepositoryProtocol,
lb_repo: ListenBrainzRepositoryProtocol,
preferences_service: PreferencesService,
lastfm_repo: Optional[LastFmRepositoryProtocol] = None,
):
self._mb_repo = mb_repo
self._lb_repo = lb_repo
self._preferences_service = preferences_service
self._lastfm_repo = lastfm_repo
def _is_listenbrainz_enabled(self) -> bool:
lb_settings = self._preferences_service.get_listenbrainz_connection()
return lb_settings.enabled and bool(lb_settings.username)
def _is_lastfm_enabled(self) -> bool:
try:
lfm_settings = self._preferences_service.get_lastfm_connection()
return lfm_settings.enabled and bool(lfm_settings.api_key)
except Exception: # noqa: BLE001
return False
def _get_enrichment_source(self) -> EnrichmentSource:
lb_enabled = self._is_listenbrainz_enabled()
lfm_enabled = self._is_lastfm_enabled() and self._lastfm_repo is not None
if not lb_enabled and not lfm_enabled:
return "none"
try:
primary = self._preferences_service.get_primary_music_source()
preferred = primary.source
except Exception: # noqa: BLE001
preferred = "listenbrainz"
if preferred == "lastfm" and lfm_enabled:
return "lastfm"
if preferred == "listenbrainz" and lb_enabled:
return "listenbrainz"
if lb_enabled:
return "listenbrainz"
if lfm_enabled:
return "lastfm"
return "none"
async def enrich(
self,
artist_mbids: list[str],
album_mbids: list[str],
) -> EnrichmentResponse:
source = self._get_enrichment_source()
artist_mbids = artist_mbids[:MAX_ENRICHMENT]
album_mbids = album_mbids[:MAX_ENRICHMENT]
artist_tasks = [
self._enrich_artist(mbid, source)
for mbid in artist_mbids
]
album_listen_counts: dict[str, int] = {}
if source == "listenbrainz" and album_mbids:
try:
album_listen_counts = await self._lb_repo.get_release_group_popularity_batch(
album_mbids
)
except Exception as e: # noqa: BLE001
logger.debug(f"Failed to get album popularity batch: {e}")
artist_results = await asyncio.gather(*artist_tasks, return_exceptions=True)
artists: list[ArtistEnrichment] = []
for result in artist_results:
if isinstance(result, Exception):
logger.debug(f"Artist enrichment failed: {result}")
continue
if result:
artists.append(result)
albums: list[AlbumEnrichment] = []
for mbid in album_mbids:
albums.append(AlbumEnrichment(
musicbrainz_id=mbid,
track_count=None,
listen_count=album_listen_counts.get(mbid),
))
return EnrichmentResponse(
artists=artists,
albums=albums,
source=source,
)
async def enrich_batch(self, request: EnrichmentBatchRequest) -> EnrichmentResponse:
source = self._get_enrichment_source()
artist_requests = request.artists[:MAX_ENRICHMENT]
album_requests = request.albums[:MAX_ENRICHMENT]
artist_tasks = [
self._enrich_artist(req.musicbrainz_id, source, name=req.name)
for req in artist_requests
]
album_tasks: list[asyncio.Task[AlbumEnrichment]] = []
album_listen_counts: dict[str, int] = {}
if source == "listenbrainz" and album_requests:
mbids = [r.musicbrainz_id for r in album_requests]
try:
album_listen_counts = await self._lb_repo.get_release_group_popularity_batch(mbids)
except Exception as e: # noqa: BLE001
logger.debug(f"Failed to get album popularity batch: {e}")
elif source == "lastfm" and album_requests and self._lastfm_repo:
album_tasks = [
self._enrich_album_lastfm(req.musicbrainz_id, req.artist_name, req.album_name)
for req in album_requests
]
artist_results = await asyncio.gather(*artist_tasks, return_exceptions=True)
artists: list[ArtistEnrichment] = []
for result in artist_results:
if isinstance(result, Exception):
logger.debug(f"Artist enrichment failed: {result}")
continue
if result:
artists.append(result)
albums: list[AlbumEnrichment] = []
if album_tasks:
album_results = await asyncio.gather(*album_tasks, return_exceptions=True)
for result in album_results:
if isinstance(result, Exception):
logger.debug(f"Album enrichment failed: {result}")
continue
if result:
albums.append(result)
else:
for req in album_requests:
albums.append(AlbumEnrichment(
musicbrainz_id=req.musicbrainz_id,
track_count=None,
listen_count=album_listen_counts.get(req.musicbrainz_id),
))
return EnrichmentResponse(
artists=artists,
albums=albums,
source=source,
)
async def _enrich_artist(
self,
mbid: str,
source: EnrichmentSource,
name: str = "",
) -> Optional[ArtistEnrichment]:
release_count: Optional[int] = None
listen_count: Optional[int] = None
try:
_, total_count = await self._mb_repo.get_artist_release_groups(
artist_mbid=mbid,
offset=0,
limit=1,
)
release_count = total_count
except Exception as e: # noqa: BLE001
logger.debug(f"Failed to get release count for artist {mbid}: {e}")
if source == "listenbrainz":
try:
top_releases = await self._lb_repo.get_artist_top_release_groups(mbid, count=5)
if top_releases:
listen_count = sum(r.listen_count for r in top_releases)
except Exception as e: # noqa: BLE001
logger.debug(f"Failed to get LB popularity for artist {mbid}: {e}")
elif source == "lastfm" and self._lastfm_repo and name:
try:
info = await self._lastfm_repo.get_artist_info(artist=name, mbid=mbid)
if info and info.listeners is not None:
listen_count = info.listeners
except Exception as e: # noqa: BLE001
logger.debug(f"Failed to get Last.fm info for artist {name}: {e}")
return ArtistEnrichment(
musicbrainz_id=mbid,
release_group_count=release_count,
listen_count=listen_count,
)
async def _enrich_album_lastfm(
self,
mbid: str,
artist_name: str,
album_name: str,
) -> AlbumEnrichment:
listen_count: Optional[int] = None
if self._lastfm_repo and artist_name and album_name:
try:
info = await self._lastfm_repo.get_album_info(
artist=artist_name, album=album_name, mbid=mbid
)
if info and info.playcount is not None:
listen_count = info.playcount
except Exception as e: # noqa: BLE001
logger.debug(f"Failed to get Last.fm info for album {album_name}: {e}")
return AlbumEnrichment(
musicbrainz_id=mbid,
track_count=None,
listen_count=listen_count,
)