Make fetch also fetch artist MBID if it is missing #201

Merged
wojtek merged 13 commits from 191---make-fetch-also-fetch-artist-mbid-if-it-is-missing into main 2024-08-30 17:58:44 +02:00
8 changed files with 78 additions and 34 deletions
Showing only changes of commit f24737d5fa - Show all commits

View File

@ -104,7 +104,7 @@ fn main() {
match opt.entity { match opt.entity {
OptEntity::Artist(opt_artist) => { OptEntity::Artist(opt_artist) => {
let query = SearchArtistRequest::new().no_field(&opt_artist.string); let query = SearchArtistRequest::new().string(&opt_artist.string);
println!("Query: {query}"); println!("Query: {query}");

View File

@ -10,20 +10,20 @@ use crate::{
use super::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin}; use super::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin};
pub enum SearchArtist<'a> { pub enum SearchArtist<'a> {
NoField(&'a str), String(&'a str),
} }
impl<'a> fmt::Display for SearchArtist<'a> { impl<'a> fmt::Display for SearchArtist<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::NoField(s) => write!(f, "\"{s}\""), Self::String(s) => write!(f, "\"{s}\""),
} }
} }
} }
pub type SearchArtistRequest<'a> = Query<SearchArtist<'a>>; pub type SearchArtistRequest<'a> = Query<SearchArtist<'a>>;
impl_term!(no_field, SearchArtist<'a>, NoField, &'a str); impl_term!(string, SearchArtist<'a>, String, &'a str);
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct SearchArtistResponse { pub struct SearchArtistResponse {
@ -116,7 +116,7 @@ mod tests {
} }
#[test] #[test]
fn search_no_field() { fn search_string() {
let mut http = MockIMusicBrainzHttp::new(); let mut http = MockIMusicBrainzHttp::new();
let url = format!( let url = format!(
"https://musicbrainz.org/ws/2\ "https://musicbrainz.org/ws/2\
@ -137,7 +137,7 @@ mod tests {
let name = "an artist"; let name = "an artist";
let query = SearchArtistRequest::new().no_field(name); let query = SearchArtistRequest::new().string(name);
let matches = client.search_artist(query).unwrap(); let matches = client.search_artist(query).unwrap();
assert_eq!(matches, response); assert_eq!(matches, response);

View File

@ -225,52 +225,52 @@ mod tests {
use super::*; use super::*;
pub enum TestEntity<'a> { pub enum TestEntity<'a> {
NoField(&'a str), String(&'a str),
} }
impl<'a> fmt::Display for TestEntity<'a> { impl<'a> fmt::Display for TestEntity<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::NoField(s) => write!(f, "\"{s}\""), Self::String(s) => write!(f, "\"{s}\""),
} }
} }
} }
type TestEntityRequest<'a> = Query<TestEntity<'a>>; type TestEntityRequest<'a> = Query<TestEntity<'a>>;
impl_term!(no_field, TestEntity<'a>, NoField, &'a str); impl_term!(string, TestEntity<'a>, String, &'a str);
#[test] #[test]
fn lucene_logical() { fn lucene_logical() {
let query = TestEntityRequest::new() let query = TestEntityRequest::new()
.no_field("jakarta apache") .string("jakarta apache")
.or() .or()
.no_field("jakarta"); .string("jakarta");
assert_eq!(format!("{query}"), "\"jakarta apache\" OR \"jakarta\""); assert_eq!(format!("{query}"), "\"jakarta apache\" OR \"jakarta\"");
let query = TestEntityRequest::new() let query = TestEntityRequest::new()
.no_field("jakarta apache") .string("jakarta apache")
.and() .and()
.no_field("jakarta"); .string("jakarta");
assert_eq!(format!("{query}"), "\"jakarta apache\" AND \"jakarta\""); assert_eq!(format!("{query}"), "\"jakarta apache\" AND \"jakarta\"");
let query = TestEntityRequest::new() let query = TestEntityRequest::new()
.require() .require()
.no_field("jakarta") .string("jakarta")
.or() .or()
.no_field("lucene"); .string("lucene");
assert_eq!(format!("{query}"), "+\"jakarta\" OR \"lucene\""); assert_eq!(format!("{query}"), "+\"jakarta\" OR \"lucene\"");
let query = TestEntityRequest::new() let query = TestEntityRequest::new()
.no_field("lucene") .string("lucene")
.require() .require()
.no_field("jakarta"); .string("jakarta");
assert_eq!(format!("{query}"), "\"lucene\" +\"jakarta\""); assert_eq!(format!("{query}"), "\"lucene\" +\"jakarta\"");
let query = TestEntityRequest::new() let query = TestEntityRequest::new()
.no_field("jakarta apache") .string("jakarta apache")
.not() .not()
.no_field("Apache Lucene"); .string("Apache Lucene");
assert_eq!( assert_eq!(
format!("{query}"), format!("{query}"),
"\"jakarta apache\" NOT \"Apache Lucene\"" "\"jakarta apache\" NOT \"Apache Lucene\""
@ -278,18 +278,18 @@ mod tests {
let query = TestEntityRequest::new() let query = TestEntityRequest::new()
.prohibit() .prohibit()
.no_field("Apache Lucene") .string("Apache Lucene")
.or() .or()
.no_field("jakarta apache"); .string("jakarta apache");
assert_eq!( assert_eq!(
format!("{query}"), format!("{query}"),
"-\"Apache Lucene\" OR \"jakarta apache\"" "-\"Apache Lucene\" OR \"jakarta apache\""
); );
let query = TestEntityRequest::new() let query = TestEntityRequest::new()
.no_field("jakarta apache") .string("jakarta apache")
.prohibit() .prohibit()
.no_field("Apache Lucene"); .string("Apache Lucene");
assert_eq!(format!("{query}"), "\"jakarta apache\" -\"Apache Lucene\""); assert_eq!(format!("{query}"), "\"jakarta apache\" -\"Apache Lucene\"");
} }
@ -298,12 +298,12 @@ mod tests {
let query = TestEntityRequest::new() let query = TestEntityRequest::new()
.expression( .expression(
TestEntityRequest::new() TestEntityRequest::new()
.no_field("jakarta") .string("jakarta")
.or() .or()
.no_field("apache"), .string("apache"),
) )
.and() .and()
.no_field("website"); .string("website");
assert_eq!( assert_eq!(
format!("{query}"), format!("{query}"),
"(\"jakarta\" OR \"apache\") AND \"website\"" "(\"jakarta\" OR \"apache\") AND \"website\""

View File

@ -15,7 +15,7 @@ use crate::{
use super::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin}; use super::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin};
pub enum SearchReleaseGroup<'a> { pub enum SearchReleaseGroup<'a> {
NoField(&'a str), String(&'a str),
Arid(&'a Mbid), Arid(&'a Mbid),
FirstReleaseDate(&'a AlbumDate), FirstReleaseDate(&'a AlbumDate),
ReleaseGroup(&'a str), ReleaseGroup(&'a str),
@ -25,7 +25,7 @@ pub enum SearchReleaseGroup<'a> {
impl<'a> fmt::Display for SearchReleaseGroup<'a> { impl<'a> fmt::Display for SearchReleaseGroup<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::NoField(s) => write!(f, "\"{s}\""), Self::String(s) => write!(f, "\"{s}\""),
Self::Arid(arid) => write!(f, "arid:{}", arid.uuid().as_hyphenated()), Self::Arid(arid) => write!(f, "arid:{}", arid.uuid().as_hyphenated()),
Self::FirstReleaseDate(date) => write!( Self::FirstReleaseDate(date) => write!(
f, f,
@ -40,7 +40,7 @@ impl<'a> fmt::Display for SearchReleaseGroup<'a> {
pub type SearchReleaseGroupRequest<'a> = Query<SearchReleaseGroup<'a>>; pub type SearchReleaseGroupRequest<'a> = Query<SearchReleaseGroup<'a>>;
impl_term!(no_field, SearchReleaseGroup<'a>, NoField, &'a str); impl_term!(string, SearchReleaseGroup<'a>, String, &'a str);
impl_term!(arid, SearchReleaseGroup<'a>, Arid, &'a Mbid); impl_term!(arid, SearchReleaseGroup<'a>, Arid, &'a Mbid);
impl_term!( impl_term!(
first_release_date, first_release_date,
@ -150,7 +150,7 @@ mod tests {
} }
#[test] #[test]
fn search_no_field() { fn search_string() {
let mut http = MockIMusicBrainzHttp::new(); let mut http = MockIMusicBrainzHttp::new();
let url = format!( let url = format!(
"https://musicbrainz.org/ws/2\ "https://musicbrainz.org/ws/2\
@ -171,7 +171,7 @@ mod tests {
let title = "an album"; let title = "an album";
let query = SearchReleaseGroupRequest::new().no_field(title); let query = SearchReleaseGroupRequest::new().string(title);
let matches = client.search_release_group(query).unwrap(); let matches = client.search_release_group(query).unwrap();
assert_eq!(matches, response); assert_eq!(matches, response);

View File

@ -146,6 +146,7 @@ mod tests {
let album_match_1_1 = Match { let album_match_1_1 = Match {
score: 100, score: 100,
item: album_1_1, item: album_1_1,
disambiguation: None,
}; };
let mut album_1_2 = album_1.clone(); let mut album_1_2 = album_1.clone();
@ -154,6 +155,7 @@ mod tests {
let album_match_1_2 = Match { let album_match_1_2 = Match {
score: 100, score: 100,
item: album_1_2, item: album_1_2,
disambiguation: None,
}; };
let matches_info_1 = AppMatchesInfo { let matches_info_1 = AppMatchesInfo {
@ -172,6 +174,7 @@ mod tests {
let album_match_2_1 = Match { let album_match_2_1 = Match {
score: 100, score: 100,
item: album_2_1, item: album_2_1,
disambiguation: None,
}; };
let matches_info_2 = AppMatchesInfo { let matches_info_2 = AppMatchesInfo {

View File

@ -3,11 +3,15 @@
use musichoard::{ use musichoard::{
collection::{ collection::{
album::{Album, AlbumDate}, album::{Album, AlbumDate},
artist::Artist,
musicbrainz::Mbid, musicbrainz::Mbid,
}, },
external::musicbrainz::{ external::musicbrainz::{
api::{ api::{
search::{SearchReleaseGroupRequest, SearchReleaseGroupResponseReleaseGroup}, search::{
SearchArtistRequest, SearchArtistResponseArtist, SearchReleaseGroupRequest,
SearchReleaseGroupResponseReleaseGroup,
},
MusicBrainzClient, MusicBrainzClient,
}, },
IMusicBrainzHttp, IMusicBrainzHttp,
@ -28,6 +32,21 @@ impl<Http> MusicBrainz<Http> {
} }
impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> { impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
fn search_artist(
&mut self,
name: &str,
) -> Result<Vec<Match<musichoard::collection::artist::Artist>>, Error> {
let query = SearchArtistRequest::new().string(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( fn search_release_group(
&mut self, &mut self,
arid: &Mbid, arid: &Mbid,
@ -54,6 +73,21 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
} }
} }
fn from_search_artist_response_artist(entity: SearchArtistResponseArtist) -> Match<Artist> {
let mut artist = Artist::new(entity.name);
if let Some(sort) = entity.sort {
artist.set_sort_key(sort);
}
artist.set_musicbrainz_ref(entity.id.into());
let mut artist_match = Match::new(entity.score, artist);
if let Some(disambiguation) = entity.disambiguation {
artist_match.set_disambiguation(disambiguation);
}
artist_match
}
fn from_search_release_group_response_release_group( fn from_search_release_group_response_release_group(
entity: SearchReleaseGroupResponseReleaseGroup, entity: SearchReleaseGroupResponseReleaseGroup,
) -> Match<Album> { ) -> Match<Album> {

View File

@ -3,11 +3,12 @@
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
use musichoard::collection::{album::Album, musicbrainz::Mbid}; use musichoard::collection::{album::Album, artist::Artist, musicbrainz::Mbid};
/// Trait for interacting with the MusicBrainz API. /// Trait for interacting with the MusicBrainz API.
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait IMusicBrainz { pub trait IMusicBrainz {
fn search_artist(&mut self, name: &str) -> Result<Vec<Match<Artist>>, Error>;
fn search_release_group( fn search_release_group(
&mut self, &mut self,
arid: &Mbid, arid: &Mbid,
@ -19,11 +20,16 @@ pub trait IMusicBrainz {
pub struct Match<T> { pub struct Match<T> {
pub score: u8, pub score: u8,
pub item: T, pub item: T,
pub disambiguation: Option<String>,
} }
impl<T> Match<T> { impl<T> Match<T> {
pub fn new(score: u8, item: T) -> Self { pub fn new(score: u8, item: T) -> Self {
Match { score, item } Match { score, item, disambiguation: None }
}
pub fn set_disambiguation<S: Into<String>>(&mut self, disambiguation: S) {
self.disambiguation = Some(disambiguation.into())
} }
} }

View File

@ -230,6 +230,7 @@ mod tests {
let album_match = Match { let album_match = Match {
score: 80, score: 80,
item: album.clone(), item: album.clone(),
disambiguation: None,
}; };
let mut app = AppPublic { let mut app = AppPublic {