Api submodule

This commit is contained in:
Wojciech Kozlowski 2024-09-15 16:20:24 +02:00
parent 38517caf4e
commit f4472ee95d
11 changed files with 150 additions and 142 deletions

View File

@ -84,7 +84,7 @@ mod tests {
machine::tests::{inner, inner_with_mb, music_hoard}, machine::tests::{inner, inner_with_mb, music_hoard},
Category, IApp, IAppAccess, Category, IApp, IAppAccess,
}, },
lib::interface::musicbrainz::MockIMusicBrainz, lib::interface::musicbrainz::api::MockIMusicBrainz,
testmod::COLLECTION, testmod::COLLECTION,
}; };

View File

@ -18,7 +18,10 @@ use crate::tui::{
AppPublicState, AppState, IAppEventFetch, IAppInteractFetch, MatchStateInfo, AppPublicState, AppState, IAppEventFetch, IAppInteractFetch, MatchStateInfo,
}, },
event::{Event, EventSender}, event::{Event, EventSender},
lib::interface::musicbrainz::{self, Error as MbError, IMusicBrainz}, lib::interface::musicbrainz::{
self,
api::{Error as MbError, IMusicBrainz},
},
}; };
pub struct FetchState { pub struct FetchState {
@ -147,7 +150,7 @@ impl AppMachine<FetchState> {
fn send_fetch_result( fn send_fetch_result(
fetch_tx: &FetchSender, fetch_tx: &FetchSender,
events: &EventSender, events: &EventSender,
result: Result<MatchStateInfo, musicbrainz::Error>, result: Result<MatchStateInfo, musicbrainz::api::Error>,
) -> Result<(), ()> { ) -> Result<(), ()> {
// If receiver disconnects just drop the rest. // If receiver disconnects just drop the rest.
fetch_tx.send(result).map_err(|_| ())?; fetch_tx.send(result).map_err(|_| ())?;
@ -199,7 +202,10 @@ mod tests {
AlbumMatches, ArtistMatches, IApp, AlbumMatches, ArtistMatches, IApp,
}, },
event::EventReceiver, event::EventReceiver,
lib::interface::musicbrainz::{self, Match, MockIMusicBrainz}, lib::interface::musicbrainz::{
self,
api::{Match, MockIMusicBrainz},
},
testmod::COLLECTION, testmod::COLLECTION,
EventChannel, EventChannel,
}; };
@ -415,7 +421,7 @@ mod tests {
fn recv_ok_fetch_err() { fn recv_ok_fetch_err() {
let (tx, rx) = mpsc::channel::<FetchResult>(); let (tx, rx) = mpsc::channel::<FetchResult>();
let fetch_result = Err(musicbrainz::Error::RateLimit); let fetch_result = Err(musicbrainz::api::Error::RateLimit);
tx.send(fetch_result).unwrap(); tx.send(fetch_result).unwrap();
let inner = inner(music_hoard(COLLECTION.clone())); let inner = inner(music_hoard(COLLECTION.clone()));

View File

@ -179,7 +179,7 @@ mod tests {
machine::tests::{inner, music_hoard}, machine::tests::{inner, music_hoard},
IApp, IAppAccess, IAppInput, IApp, IAppAccess, IAppInput,
}, },
lib::interface::musicbrainz::Match, lib::interface::musicbrainz::api::Match,
}; };
use super::*; use super::*;

View File

@ -16,7 +16,7 @@ use crate::tui::{
IAppAccess, IAppBase, IAppState, IAppAccess, IAppBase, IAppState,
}, },
event::EventSender, event::EventSender,
lib::{interface::musicbrainz::IMusicBrainz, IMusicHoard}, lib::{interface::musicbrainz::api::IMusicBrainz, IMusicHoard},
}; };
use browse_state::BrowseState; use browse_state::BrowseState;
@ -230,7 +230,7 @@ mod tests {
use crate::tui::{ use crate::tui::{
app::{AppState, IApp, IAppInput, IAppInteractBrowse}, app::{AppState, IApp, IAppInput, IAppInteractBrowse},
lib::{interface::musicbrainz::MockIMusicBrainz, MockIMusicHoard}, lib::{interface::musicbrainz::api::MockIMusicBrainz, MockIMusicHoard},
EventChannel, EventChannel,
}; };

View File

@ -6,7 +6,7 @@ pub use selection::{Category, Delta, Selection, WidgetState};
use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, Collection}; use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, Collection};
use crate::tui::lib::interface::musicbrainz::Match; use crate::tui::lib::interface::musicbrainz::api::Match;
pub enum AppState<B, I, R, S, F, M, E, C> { pub enum AppState<B, I, R, S, F, M, E, C> {
Browse(B), Browse(B),

View File

@ -0,0 +1,104 @@
//! Module for interacting with the [MusicBrainz API](https://musicbrainz.org/doc/MusicBrainz_API).
use std::collections::HashMap;
use musichoard::{
collection::{
album::{AlbumDate, AlbumMeta, AlbumSeq},
artist::ArtistMeta,
musicbrainz::Mbid,
},
external::musicbrainz::{
api::{
search::{
SearchArtistRequest, SearchArtistResponseArtist, SearchReleaseGroupRequest,
SearchReleaseGroupResponseReleaseGroup,
},
MusicBrainzClient,
},
IMusicBrainzHttp,
},
};
use crate::tui::lib::interface::musicbrainz::api::{Error, IMusicBrainz, Match};
// GRCOV_EXCL_START
pub struct MusicBrainz<Http> {
client: MusicBrainzClient<Http>,
}
impl<Http> MusicBrainz<Http> {
pub fn new(client: MusicBrainzClient<Http>) -> Self {
MusicBrainz { client }
}
}
impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error> {
let query = SearchArtistRequest::new().string(&artist.id.name);
let mb_response = self.client.search_artist(query)?;
Ok(mb_response
.artists
.into_iter()
.map(from_search_artist_response_artist)
.collect())
}
fn search_release_group(
&mut self,
arid: &Mbid,
album: &AlbumMeta,
) -> Result<Vec<Match<AlbumMeta>>, Error> {
// Some release groups may have a promotional early release messing up the search. Searching
// with just the year should be enough anyway.
let date = AlbumDate::new(album.date.year, None, None);
let query = SearchReleaseGroupRequest::new()
.arid(arid)
.and()
.first_release_date(&date)
.and()
.release_group(&album.id.title);
let mb_response = self.client.search_release_group(query)?;
Ok(mb_response
.release_groups
.into_iter()
.map(from_search_release_group_response_release_group)
.collect())
}
}
fn from_search_artist_response_artist(entity: SearchArtistResponseArtist) -> Match<ArtistMeta> {
Match {
score: entity.score,
item: ArtistMeta {
id: entity.name,
sort: entity.sort.map(Into::into),
musicbrainz: Some(entity.id.into()),
properties: HashMap::new(),
},
disambiguation: entity.disambiguation,
}
}
fn from_search_release_group_response_release_group(
entity: SearchReleaseGroupResponseReleaseGroup,
) -> Match<AlbumMeta> {
Match {
score: entity.score,
item: AlbumMeta {
id: entity.title,
date: entity.first_release_date,
seq: AlbumSeq::default(),
musicbrainz: Some(entity.id.into()),
primary_type: Some(entity.primary_type),
secondary_types: entity.secondary_types.unwrap_or_default(),
},
disambiguation: None,
}
}
// GRCOV_EXCL_STOP

View File

@ -1,104 +1 @@
//! Module for interacting with the [MusicBrainz API](https://musicbrainz.org/doc/MusicBrainz_API). pub mod api;
use std::collections::HashMap;
use musichoard::{
collection::{
album::{AlbumDate, AlbumMeta, AlbumSeq},
artist::ArtistMeta,
musicbrainz::Mbid,
},
external::musicbrainz::{
api::{
search::{
SearchArtistRequest, SearchArtistResponseArtist, SearchReleaseGroupRequest,
SearchReleaseGroupResponseReleaseGroup,
},
MusicBrainzClient,
},
IMusicBrainzHttp,
},
};
use crate::tui::lib::interface::musicbrainz::{Error, IMusicBrainz, Match};
// GRCOV_EXCL_START
pub struct MusicBrainz<Http> {
client: MusicBrainzClient<Http>,
}
impl<Http> MusicBrainz<Http> {
pub fn new(client: MusicBrainzClient<Http>) -> Self {
MusicBrainz { client }
}
}
impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error> {
let query = SearchArtistRequest::new().string(&artist.id.name);
let mb_response = self.client.search_artist(query)?;
Ok(mb_response
.artists
.into_iter()
.map(from_search_artist_response_artist)
.collect())
}
fn search_release_group(
&mut self,
arid: &Mbid,
album: &AlbumMeta,
) -> Result<Vec<Match<AlbumMeta>>, Error> {
// Some release groups may have a promotional early release messing up the search. Searching
// with just the year should be enough anyway.
let date = AlbumDate::new(album.date.year, None, None);
let query = SearchReleaseGroupRequest::new()
.arid(arid)
.and()
.first_release_date(&date)
.and()
.release_group(&album.id.title);
let mb_response = self.client.search_release_group(query)?;
Ok(mb_response
.release_groups
.into_iter()
.map(from_search_release_group_response_release_group)
.collect())
}
}
fn from_search_artist_response_artist(entity: SearchArtistResponseArtist) -> Match<ArtistMeta> {
Match {
score: entity.score,
item: ArtistMeta {
id: entity.name,
sort: entity.sort.map(Into::into),
musicbrainz: Some(entity.id.into()),
properties: HashMap::new(),
},
disambiguation: entity.disambiguation,
}
}
fn from_search_release_group_response_release_group(
entity: SearchReleaseGroupResponseReleaseGroup,
) -> Match<AlbumMeta> {
Match {
score: entity.score,
item: AlbumMeta {
id: entity.title,
date: entity.first_release_date,
seq: AlbumSeq::default(),
musicbrainz: Some(entity.id.into()),
primary_type: Some(entity.primary_type),
secondary_types: entity.secondary_types.unwrap_or_default(),
},
disambiguation: None,
}
}
// GRCOV_EXCL_STOP

View File

@ -0,0 +1,26 @@
//! Module for accessing MusicBrainz metadata.
#[cfg(test)]
use mockall::automock;
use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz::Mbid};
/// Trait for interacting with the MusicBrainz API.
#[cfg_attr(test, automock)]
pub trait IMusicBrainz {
fn search_artist(&mut self, name: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error>;
fn search_release_group(
&mut self,
arid: &Mbid,
album: &AlbumMeta,
) -> Result<Vec<Match<AlbumMeta>>, Error>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Match<T> {
pub score: u8,
pub item: T,
pub disambiguation: Option<String>,
}
pub type Error = musichoard::external::musicbrainz::api::Error;

View File

@ -1,26 +1 @@
//! Module for accessing MusicBrainz metadata. pub mod api;
#[cfg(test)]
use mockall::automock;
use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz::Mbid};
/// Trait for interacting with the MusicBrainz API.
#[cfg_attr(test, automock)]
pub trait IMusicBrainz {
fn search_artist(&mut self, name: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error>;
fn search_release_group(
&mut self,
arid: &Mbid,
album: &AlbumMeta,
) -> Result<Vec<Match<AlbumMeta>>, Error>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Match<T> {
pub score: u8,
pub item: T,
pub disambiguation: Option<String>,
}
pub type Error = musichoard::external::musicbrainz::api::Error;

View File

@ -8,7 +8,7 @@ mod ui;
pub use app::App; pub use app::App;
pub use event::EventChannel; pub use event::EventChannel;
pub use handler::EventHandler; pub use handler::EventHandler;
pub use lib::external::musicbrainz::MusicBrainz; pub use lib::external::musicbrainz::api::MusicBrainz;
pub use listener::EventListener; pub use listener::EventListener;
pub use ui::Ui; pub use ui::Ui;
@ -175,7 +175,7 @@ mod tests {
use std::{io, thread}; use std::{io, thread};
use event::EventSender; use event::EventSender;
use lib::interface::musicbrainz::MockIMusicBrainz; use lib::interface::musicbrainz::api::MockIMusicBrainz;
use ratatui::{backend::TestBackend, Terminal}; use ratatui::{backend::TestBackend, Terminal};
use musichoard::collection::Collection; use musichoard::collection::Collection;

View File

@ -207,7 +207,7 @@ mod tests {
use crate::tui::{ use crate::tui::{
app::{AppPublic, AppPublicInner, Delta, MatchOption, MatchStatePublic}, app::{AppPublic, AppPublicInner, Delta, MatchOption, MatchStatePublic},
lib::interface::musicbrainz::Match, lib::interface::musicbrainz::api::Match,
testmod::COLLECTION, testmod::COLLECTION,
tests::terminal, tests::terminal,
}; };