Added the mb db download. Big upsides and downsides
All checks were successful
CI / check (push) Successful in 1m11s
CI / docker (push) Successful in 2m21s

This commit is contained in:
Connor Johnstone
2026-03-21 23:22:49 -04:00
parent 31d54651e6
commit 51f2c2ae8f
9 changed files with 2181 additions and 142 deletions

View File

@@ -0,0 +1,171 @@
//! 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<LocalMusicBrainzFetcher>,
remote: MusicBrainzFetcher,
}
impl HybridMusicBrainzFetcher {
/// Create a hybrid fetcher. If `local` is `None`, all queries go to the API.
pub fn new(local: Option<LocalMusicBrainzFetcher>, 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<LocalMbStats> {
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<String>)> {
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<ArtistInfo> {
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<T, F: std::future::Future<Output = DataResult<Vec<T>>>>(
f: F,
) -> Option<DataResult<Vec<T>>> {
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<Vec<RecordingMatch>> {
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<Vec<ReleaseMatch>> {
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<RecordingDetails> {
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<Vec<ArtistSearchResult>> {
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<Vec<DiscographyEntry>> {
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<Vec<ReleaseTrack>> {
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<Vec<ReleaseGroupEntry>> {
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<String> {
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
}
}