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
4 changed files with 200 additions and 100 deletions
Showing only changes of commit 5eef5f0f22 - Show all commits

View File

@ -5,10 +5,7 @@ use std::{num::ParseIntError, str::FromStr};
use musichoard::{ use musichoard::{
collection::{album::AlbumDate, musicbrainz::Mbid}, collection::{album::AlbumDate, musicbrainz::Mbid},
external::musicbrainz::{ external::musicbrainz::{
api::{ api::{search::SearchReleaseGroupRequest, MusicBrainzClient},
search::{Expression, Query, SearchReleaseGroup},
MusicBrainzClient,
},
http::MusicBrainzHttp, http::MusicBrainzHttp,
}, },
}; };
@ -93,18 +90,21 @@ fn main() {
let title: String; let title: String;
let rgid: Mbid; let rgid: Mbid;
let query: Query<SearchReleaseGroup> = match opt.command { let query = match opt.command {
OptCommand::Title(opt_title) => { OptCommand::Title(opt_title) => {
arid = opt_title.arid.into(); arid = opt_title.arid.into();
date = opt_title.date.map(Into::into).unwrap_or_default(); date = opt_title.date.map(Into::into).unwrap_or_default();
title = opt_title.title; title = opt_title.title;
Query::expression(Expression::arid(&arid)) SearchReleaseGroupRequest::new()
.and(Expression::release_group(&title)) .arid(&arid)
.and(Expression::first_release_date(&date)) .and()
.release_group(&title)
.and()
.first_release_date(&date)
} }
OptCommand::Rgid(opt_rgid) => { OptCommand::Rgid(opt_rgid) => {
rgid = opt_rgid.rgid.into(); rgid = opt_rgid.rgid.into();
Query::expression(Expression::rgid(&rgid)) SearchReleaseGroupRequest::new().rgid(&rgid)
} }
}; };

View File

@ -3,7 +3,7 @@ mod query;
use std::fmt; use std::fmt;
pub use query::{Expression, Query}; use query::{impl_term, EmptyQuery, EmptyQueryJoin, QueryJoin};
use serde::Deserialize; use serde::Deserialize;
use url::form_urlencoded; use url::form_urlencoded;
@ -12,8 +12,8 @@ use crate::{
core::collection::album::{AlbumPrimaryType, AlbumSecondaryType}, core::collection::album::{AlbumPrimaryType, AlbumSecondaryType},
external::musicbrainz::{ external::musicbrainz::{
api::{ api::{
ApiDisplay, Error, MusicBrainzClient, SerdeAlbumDate, SerdeAlbumPrimaryType, search::query::Query, ApiDisplay, Error, MusicBrainzClient, SerdeAlbumDate,
SerdeAlbumSecondaryType, SerdeMbid, MB_BASE_URL, SerdeAlbumPrimaryType, SerdeAlbumSecondaryType, SerdeMbid, MB_BASE_URL,
}, },
IMusicBrainzHttp, IMusicBrainzHttp,
}, },
@ -41,28 +41,6 @@ pub enum SearchReleaseGroup<'a> {
Rgid(&'a Mbid), Rgid(&'a Mbid),
} }
impl<'a> Expression<SearchReleaseGroup<'a>> {
pub fn no_field(string: &'a str) -> Self {
Expression::Term(SearchReleaseGroup::NoField(string))
}
pub fn arid(arid: &'a Mbid) -> Self {
Expression::Term(SearchReleaseGroup::Arid(arid))
}
pub fn first_release_date(first_release_date: &'a AlbumDate) -> Self {
Expression::Term(SearchReleaseGroup::FirstReleaseDate(first_release_date))
}
pub fn release_group(release_group: &'a str) -> Self {
Expression::Term(SearchReleaseGroup::ReleaseGroup(release_group))
}
pub fn rgid(rgid: &'a Mbid) -> Self {
Expression::Term(SearchReleaseGroup::Rgid(rgid))
}
}
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 {
@ -81,6 +59,17 @@ 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!(arid, SearchReleaseGroup<'a>, Arid, &'a Mbid);
impl_term!(
first_release_date,
SearchReleaseGroup<'a>,
FirstReleaseDate,
&'a AlbumDate
);
impl_term!(release_group, SearchReleaseGroup<'a>, ReleaseGroup, &'a str);
impl_term!(rgid, SearchReleaseGroup<'a>, Rgid, &'a Mbid);
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct SearchReleaseGroupResponse { pub struct SearchReleaseGroupResponse {
pub release_groups: Vec<SearchReleaseGroupResponseReleaseGroup>, pub release_groups: Vec<SearchReleaseGroupResponseReleaseGroup>,
@ -212,16 +201,19 @@ mod tests {
let title: AlbumId = AlbumId::new("an album"); let title: AlbumId = AlbumId::new("an album");
let date = (1986, 4).into(); let date = (1986, 4).into();
let query = Query::expression(Expression::arid(&arid)) let query = SearchReleaseGroupRequest::new()
.and(Expression::release_group(&title.title)) .arid(&arid)
.and(Expression::first_release_date(&date)); .and()
.release_group(&title.title)
.and()
.first_release_date(&date);
let matches = client.search_release_group(query).unwrap(); let matches = client.search_release_group(query).unwrap();
assert_eq!(matches, response); assert_eq!(matches, response);
let rgid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap(); let rgid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
let query = Query::expression(Expression::rgid(&rgid)); let query = SearchReleaseGroupRequest::new().rgid(&rgid);
let matches = client.search_release_group(query).unwrap(); let matches = client.search_release_group(query).unwrap();
assert_eq!(matches, response); assert_eq!(matches, response);
@ -253,9 +245,12 @@ mod tests {
let title: AlbumId = AlbumId::new("an album"); let title: AlbumId = AlbumId::new("an album");
let date = AlbumDate::default(); let date = AlbumDate::default();
let query = Query::expression(Expression::arid(&arid)) let query = SearchReleaseGroupRequest::new()
.and(Expression::release_group(&title.title)) .arid(&arid)
.and(Expression::first_release_date(&date)); .and()
.release_group(&title.title)
.and()
.first_release_date(&date);
let _ = client.search_release_group(query).unwrap(); let _ = client.search_release_group(query).unwrap();
} }

View File

@ -1,4 +1,4 @@
use std::fmt; use std::{fmt, marker::PhantomData};
pub enum Logical { pub enum Logical {
Unary(Unary), Unary(Unary),
@ -49,6 +49,18 @@ pub enum Expression<Entity> {
Expr(Query<Entity>), Expr(Query<Entity>),
} }
impl<Entity> From<Entity> for Expression<Entity> {
fn from(value: Entity) -> Self {
Expression::Term(value)
}
}
impl<Entity> From<Query<Entity>> for Expression<Entity> {
fn from(value: Query<Entity>) -> Self {
Expression::Expr(value)
}
}
impl<Entity: fmt::Display> fmt::Display for Expression<Entity> { impl<Entity: fmt::Display> fmt::Display for Expression<Entity> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
@ -58,6 +70,55 @@ impl<Entity: fmt::Display> fmt::Display for Expression<Entity> {
} }
} }
pub struct EmptyQuery<Entity> {
_marker: PhantomData<Entity>,
}
impl<Entity> Default for EmptyQuery<Entity> {
fn default() -> Self {
EmptyQuery {
_marker: PhantomData,
}
}
}
impl<Entity> EmptyQuery<Entity> {
pub fn expression<Expr: Into<Expression<Entity>>>(self, expr: Expr) -> Query<Entity> {
Query {
left: (None, Box::new(expr.into())),
right: vec![],
}
}
pub fn require(self) -> EmptyQueryJoin<Entity> {
EmptyQueryJoin {
unary: Unary::Require,
_marker: PhantomData,
}
}
pub fn prohibit(self) -> EmptyQueryJoin<Entity> {
EmptyQueryJoin {
unary: Unary::Prohibit,
_marker: PhantomData,
}
}
}
pub struct EmptyQueryJoin<Entity> {
unary: Unary,
_marker: PhantomData<Entity>,
}
impl<Entity> EmptyQueryJoin<Entity> {
pub fn expression<Expr: Into<Expression<Entity>>>(self, expr: Expr) -> Query<Entity> {
Query {
left: (Some(self.unary), Box::new(expr.into())),
right: vec![],
}
}
}
pub struct Query<Entity> { pub struct Query<Entity> {
left: (Option<Unary>, Box<Expression<Entity>>), left: (Option<Unary>, Box<Expression<Entity>>),
right: Vec<(Logical, Box<Expression<Entity>>)>, right: Vec<(Logical, Box<Expression<Entity>>)>,
@ -79,94 +140,135 @@ impl<Entity: fmt::Display> fmt::Display for Query<Entity> {
} }
} }
impl<'a, Entity> Query<Entity> { impl<Entity> Query<Entity> {
pub fn expression(expr: Expression<Entity>) -> Self { pub fn new() -> EmptyQuery<Entity> {
Query { EmptyQuery::default()
left: (None, Box::new(expr)), }
right: vec![],
pub fn require(self) -> QueryJoin<Entity> {
QueryJoin {
logical: Logical::Unary(Unary::Require),
query: self,
} }
} }
pub fn require(expr: Expression<Entity>) -> Self { pub fn prohibit(self) -> QueryJoin<Entity> {
Query { QueryJoin {
left: (Some(Unary::Require), Box::new(expr)), logical: Logical::Unary(Unary::Prohibit),
right: vec![], query: self,
} }
} }
pub fn and_require(mut self, expr: Expression<Entity>) -> Self { pub fn and(self) -> QueryJoin<Entity> {
self.right QueryJoin {
.push((Logical::Unary(Unary::Require), Box::new(expr))); logical: Logical::Binary(Boolean::And),
self query: self,
}
pub fn prohibit(expr: Expression<Entity>) -> Self {
Query {
left: (Some(Unary::Prohibit), Box::new(expr)),
right: vec![],
} }
} }
pub fn and_prohibit(mut self, expr: Expression<Entity>) -> Self { pub fn or(self) -> QueryJoin<Entity> {
self.right QueryJoin {
.push((Logical::Unary(Unary::Prohibit), Box::new(expr))); logical: Logical::Binary(Boolean::Or),
self query: self,
}
} }
pub fn and(mut self, expr: Expression<Entity>) -> Self { pub fn not(self) -> QueryJoin<Entity> {
self.right QueryJoin {
.push((Logical::Binary(Boolean::And), Box::new(expr))); logical: Logical::Binary(Boolean::Not),
self query: self,
}
}
} }
pub fn or(mut self, expr: Expression<Entity>) -> Self { pub struct QueryJoin<Entity> {
self.right logical: Logical,
.push((Logical::Binary(Boolean::Or), Box::new(expr))); query: Query<Entity>,
self
} }
pub fn not(mut self, expr: Expression<Entity>) -> Self { impl<Entity> QueryJoin<Entity> {
self.right pub fn expression<Expr: Into<Expression<Entity>>>(mut self, expr: Expr) -> Query<Entity> {
.push((Logical::Binary(Boolean::Not), Box::new(expr))); self.query.right.push((self.logical, Box::new(expr.into())));
self self.query
} }
} }
macro_rules! impl_term {
($name:ident, $enum:ty, $variant:ident, $type:ty) => {
impl<'a> EmptyQuery<$enum> {
pub fn $name(self, $name: $type) -> Query<$enum> {
self.expression(<$enum>::$variant($name))
}
}
impl<'a> EmptyQueryJoin<$enum> {
pub fn $name(self, $name: $type) -> Query<$enum> {
self.expression(<$enum>::$variant($name))
}
}
impl<'a> QueryJoin<$enum> {
pub fn $name(self, $name: $type) -> Query<$enum> {
self.expression(<$enum>::$variant($name))
}
}
};
}
pub(crate) use impl_term;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use crate::external::musicbrainz::api::search::SearchReleaseGroupRequest;
#[test] #[test]
fn lucene_logical() { fn lucene_logical() {
let query = Query::expression(Expression::no_field("jakarta apache")) let query = SearchReleaseGroupRequest::new()
.or(Expression::no_field("jakarta")); .no_field("jakarta apache")
.or()
.no_field("jakarta");
assert_eq!(format!("{query}"), "\"jakarta apache\" OR \"jakarta\""); assert_eq!(format!("{query}"), "\"jakarta apache\" OR \"jakarta\"");
let query = Query::expression(Expression::no_field("jakarta apache")) let query = SearchReleaseGroupRequest::new()
.and(Expression::no_field("jakarta")); .no_field("jakarta apache")
.and()
.no_field("jakarta");
assert_eq!(format!("{query}"), "\"jakarta apache\" AND \"jakarta\""); assert_eq!(format!("{query}"), "\"jakarta apache\" AND \"jakarta\"");
let query = let query = SearchReleaseGroupRequest::new()
Query::require(Expression::no_field("jakarta")).or(Expression::no_field("lucene")); .require()
.no_field("jakarta")
.or()
.no_field("lucene");
assert_eq!(format!("{query}"), "+\"jakarta\" OR \"lucene\""); assert_eq!(format!("{query}"), "+\"jakarta\" OR \"lucene\"");
let query = Query::expression(Expression::no_field("jakarta apache")) let query = SearchReleaseGroupRequest::new()
.not(Expression::no_field("Apache Lucene")); .no_field("jakarta apache")
.not()
.no_field("Apache Lucene");
assert_eq!( assert_eq!(
format!("{query}"), format!("{query}"),
"\"jakarta apache\" NOT \"Apache Lucene\"" "\"jakarta apache\" NOT \"Apache Lucene\""
); );
let query = Query::expression(Expression::no_field("jakarta apache")) let query = SearchReleaseGroupRequest::new()
.and_prohibit(Expression::no_field("Apache Lucene")); .no_field("jakarta apache")
.prohibit()
.no_field("Apache Lucene");
assert_eq!(format!("{query}"), "\"jakarta apache\" -\"Apache Lucene\""); assert_eq!(format!("{query}"), "\"jakarta apache\" -\"Apache Lucene\"");
} }
#[test] #[test]
fn lucene_grouping() { fn lucene_grouping() {
let query = Query::expression(Expression::Expr( let query = SearchReleaseGroupRequest::new()
Query::expression(Expression::no_field("jakarta")).or(Expression::no_field("apache")), .expression(
)) SearchReleaseGroupRequest::new()
.and(Expression::no_field("website")); .no_field("jakarta")
.or()
.no_field("apache"),
)
.and()
.no_field("website");
assert_eq!( assert_eq!(
format!("{query}"), format!("{query}"),
"(\"jakarta\" OR \"apache\") AND \"website\"" "(\"jakarta\" OR \"apache\") AND \"website\""

View File

@ -7,7 +7,7 @@ use musichoard::{
}, },
external::musicbrainz::{ external::musicbrainz::{
api::{ api::{
search::{Expression, Query, SearchReleaseGroupResponseReleaseGroup}, search::{SearchReleaseGroupRequest, SearchReleaseGroupResponseReleaseGroup},
MusicBrainzClient, MusicBrainzClient,
}, },
IMusicBrainzHttp, IMusicBrainzHttp,
@ -37,9 +37,12 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
// with just the year should be enough anyway. // with just the year should be enough anyway.
let date = AlbumDate::new(album.date.year, None, None); let date = AlbumDate::new(album.date.year, None, None);
let query = Query::expression(Expression::arid(arid)) let query = SearchReleaseGroupRequest::new()
.and(Expression::first_release_date(&date)) .arid(arid)
.and(Expression::release_group(&album.id.title)); .and()
.first_release_date(&date)
.and()
.release_group(&album.id.title);
let mb_response = self.client.search_release_group(query)?; let mb_response = self.client.search_release_group(query)?;