//! Hybrid MusicBrainz fetcher: local DB first, API fallback. //! //! Tries the local SQLite database for instant lookups. If the local DB is not //! configured, not available, or doesn't have the requested entity, falls back //! to the rate-limited MusicBrainz API. use crate::error::DataResult; use crate::mb_local::{LocalMbStats, LocalMusicBrainzFetcher}; use crate::musicbrainz::MusicBrainzFetcher; use crate::traits::MetadataFetcher; use crate::types::{ ArtistInfo, ArtistSearchResult, DiscographyEntry, RecordingDetails, RecordingMatch, ReleaseGroupEntry, ReleaseMatch, ReleaseTrack, }; /// A [`MetadataFetcher`] that tries a local MusicBrainz SQLite database first, /// then falls back to the remote MusicBrainz API. pub struct HybridMusicBrainzFetcher { local: Option, remote: MusicBrainzFetcher, } impl HybridMusicBrainzFetcher { /// Create a hybrid fetcher. If `local` is `None`, all queries go to the API. pub fn new(local: Option, remote: MusicBrainzFetcher) -> Self { Self { local, remote } } /// Whether a local database is configured and has data. pub fn has_local_db(&self) -> bool { self.local.as_ref().is_some_and(|l| l.is_available()) } /// Get stats from the local database (if available). pub fn local_stats(&self) -> Option { self.local .as_ref() .filter(|l| l.is_available()) .map(|l| l.stats()) } /// Get a reference to the underlying remote fetcher (for methods not on the trait). pub fn remote(&self) -> &MusicBrainzFetcher { &self.remote } /// Returns a reference to the local fetcher if available and populated. fn local_if_available(&self) -> Option<&LocalMusicBrainzFetcher> { self.local.as_ref().filter(|l| l.is_available()) } /// Look up an artist by MBID. Tries local first, then remote. pub async fn get_artist_by_mbid(&self, mbid: &str) -> DataResult<(String, Option)> { if let Some(local) = self.local_if_available() && let Ok(result) = local.get_artist_by_mbid_sync(mbid) { return Ok(result); } self.remote.get_artist_by_mbid(mbid).await } /// Get detailed artist info by MBID. Tries local first, then remote. pub async fn get_artist_info(&self, mbid: &str) -> DataResult { if let Some(local) = self.local_if_available() && let Ok(result) = local.get_artist_info_sync(mbid) { return Ok(result); } self.remote.get_artist_info(mbid).await } /// Get a clone of the rate limiter for sharing with other MB clients. pub fn limiter(&self) -> crate::http::RateLimiter { self.remote.limiter() } } /// Try a local search; returns `Some(results)` if non-empty, `None` to fall through. async fn try_local_vec>>>( f: F, ) -> Option>> { let results = f.await; match results { Ok(ref r) if !r.is_empty() => Some(results), _ => None, } } impl MetadataFetcher for HybridMusicBrainzFetcher { async fn search_recording(&self, artist: &str, title: &str) -> DataResult> { if let Some(local) = self.local_if_available() && let Some(results) = try_local_vec(local.search_recording(artist, title)).await { return results; } self.remote.search_recording(artist, title).await } async fn search_release(&self, artist: &str, album: &str) -> DataResult> { if let Some(local) = self.local_if_available() && let Some(results) = try_local_vec(local.search_release(artist, album)).await { return results; } self.remote.search_release(artist, album).await } async fn get_recording(&self, mbid: &str) -> DataResult { if let Some(local) = self.local_if_available() && let Ok(result) = local.get_recording(mbid).await { return Ok(result); } self.remote.get_recording(mbid).await } async fn search_artist(&self, query: &str, limit: u32) -> DataResult> { if let Some(local) = self.local_if_available() && let Some(results) = try_local_vec(local.search_artist(query, limit)).await { return results; } self.remote.search_artist(query, limit).await } async fn get_artist_releases( &self, artist_mbid: &str, limit: u32, ) -> DataResult> { if let Some(local) = self.local_if_available() && let Some(results) = try_local_vec(local.get_artist_releases(artist_mbid, limit)).await { return results; } self.remote.get_artist_releases(artist_mbid, limit).await } async fn get_release_tracks(&self, release_mbid: &str) -> DataResult> { if let Some(local) = self.local_if_available() && let Ok(tracks) = local.get_release_tracks(release_mbid).await { return Ok(tracks); } self.remote.get_release_tracks(release_mbid).await } async fn get_artist_release_groups( &self, artist_mbid: &str, ) -> DataResult> { if let Some(local) = self.local_if_available() && let Some(results) = try_local_vec(local.get_artist_release_groups(artist_mbid)).await { return results; } self.remote.get_artist_release_groups(artist_mbid).await } async fn resolve_release_from_group(&self, release_group_mbid: &str) -> DataResult { if let Some(local) = self.local_if_available() && let Ok(result) = local.resolve_release_from_group(release_group_mbid).await { return Ok(result); } self.remote .resolve_release_from_group(release_group_mbid) .await } }