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 249 additions and 89 deletions
Showing only changes of commit a236052b4b - Show all commits

View File

@ -5,7 +5,10 @@ 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::{search::SearchReleaseGroupRequest, MusicBrainzClient}, api::{
search::{Expression, Query, SearchReleaseGroup},
MusicBrainzClient,
},
http::MusicBrainzHttp, http::MusicBrainzHttp,
}, },
}; };
@ -85,29 +88,30 @@ fn main() {
let http = MusicBrainzHttp::new(USER_AGENT).expect("failed to create API client"); let http = MusicBrainzHttp::new(USER_AGENT).expect("failed to create API client");
let mut client = MusicBrainzClient::new(http); let mut client = MusicBrainzClient::new(http);
let mut request = SearchReleaseGroupRequest::default();
let arid: Mbid; let arid: Mbid;
let date: AlbumDate; let date: AlbumDate;
let title: String; let title: String;
let rgid: Mbid; let rgid: Mbid;
match opt.command {
let query: Query<SearchReleaseGroup> = 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;
request Query::expression(Expression::arid(&arid))
.arid(&arid) .and(Expression::release_group(&title))
.first_release_date(&date) .and(Expression::first_release_date(&date))
.release_group(&title);
} }
OptCommand::Rgid(opt_rgid) => { OptCommand::Rgid(opt_rgid) => {
rgid = opt_rgid.rgid.into(); rgid = opt_rgid.rgid.into();
request.rgid(&rgid); Query::expression(Expression::rgid(&rgid))
} }
}; };
println!("Query: {query}");
let matches = client let matches = client
.search_release_group(request) .search_release_group(query)
.expect("failed to make API call"); .expect("failed to make API call");
println!("{matches:#?}"); println!("{matches:#?}");

View File

@ -56,17 +56,21 @@ impl<Http> MusicBrainzClient<Http> {
pub fn new(http: Http) -> Self { pub fn new(http: Http) -> Self {
MusicBrainzClient { http } MusicBrainzClient { http }
} }
}
fn format_album_date(date: &AlbumDate) -> Option<String> { pub struct ApiDisplay;
impl ApiDisplay {
fn format_album_date(date: &AlbumDate) -> String {
match date.year { match date.year {
Some(year) => match date.month { Some(year) => match date.month {
Some(month) => match date.day { Some(month) => match date.day {
Some(day) => Some(format!("{year}-{month:02}-{day:02}")), Some(day) => format!("{year}-{month:02}-{day:02}"),
None => Some(format!("{year}-{month:02}")), None => format!("{year}-{month:02}"),
}, },
None => Some(format!("{year}")), None => format!("{year}"),
}, },
None => None, None => format!("*"),
} }
} }
} }
@ -241,22 +245,15 @@ mod tests {
#[test] #[test]
fn format_album_date() { fn format_album_date() {
struct Null;
assert_eq!( assert_eq!(
MusicBrainzClient::<Null>::format_album_date(&AlbumDate::new(None, None, None)), ApiDisplay::format_album_date(&AlbumDate::new(None, None, None)),
None "*"
); );
assert_eq!(ApiDisplay::format_album_date(&(1986).into()), "1986");
assert_eq!(ApiDisplay::format_album_date(&(1986, 4).into()), "1986-04");
assert_eq!( assert_eq!(
MusicBrainzClient::<Null>::format_album_date(&(1986).into()), ApiDisplay::format_album_date(&(1986, 4, 21).into()),
Some(String::from("1986")) "1986-04-21"
);
assert_eq!(
MusicBrainzClient::<Null>::format_album_date(&(1986, 4).into()),
Some(String::from("1986-04"))
);
assert_eq!(
MusicBrainzClient::<Null>::format_album_date(&(1986, 4, 21).into()),
Some(String::from("1986-04-21"))
); );
} }

View File

@ -1,5 +1,7 @@
//! Module for interacting with the [MusicBrainz API](https://musicbrainz.org/doc/MusicBrainz_API). //! Module for interacting with the [MusicBrainz API](https://musicbrainz.org/doc/MusicBrainz_API).
use std::fmt;
use serde::Deserialize; use serde::Deserialize;
use url::form_urlencoded; use url::form_urlencoded;
@ -8,7 +10,7 @@ use crate::{
core::collection::album::{AlbumPrimaryType, AlbumSecondaryType}, core::collection::album::{AlbumPrimaryType, AlbumSecondaryType},
external::musicbrainz::{ external::musicbrainz::{
api::{ api::{
Error, MusicBrainzClient, SerdeAlbumDate, SerdeAlbumPrimaryType, ApiDisplay, Error, MusicBrainzClient, SerdeAlbumDate, SerdeAlbumPrimaryType,
SerdeAlbumSecondaryType, SerdeMbid, MB_BASE_URL, SerdeAlbumSecondaryType, SerdeMbid, MB_BASE_URL,
}, },
IMusicBrainzHttp, IMusicBrainzHttp,
@ -18,30 +20,10 @@ use crate::{
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> { impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
pub fn search_release_group( pub fn search_release_group(
&mut self, &mut self,
request: SearchReleaseGroupRequest, query: SearchReleaseGroupRequest,
) -> Result<SearchReleaseGroupResponse, Error> { ) -> Result<SearchReleaseGroupResponse, Error> {
let mut query: Vec<String> = vec![];
if let Some(arid) = request.arid {
query.push(format!("arid:{}", arid.uuid().as_hyphenated()));
}
if let Some(date) = request.first_release_date {
if let Some(date_string) = Self::format_album_date(date) {
query.push(format!("firstreleasedate:{date_string}"))
}
}
if let Some(release_group) = request.release_group {
query.push(format!("releasegroup:\"{release_group}\""));
}
if let Some(rgid) = request.rgid {
query.push(format!("rgid:{}", rgid.uuid().as_hyphenated()));
}
let query: String = let query: String =
form_urlencoded::byte_serialize(query.join(" AND ").as_bytes()).collect(); form_urlencoded::byte_serialize(format!("{query}").as_bytes()).collect();
let url = format!("{MB_BASE_URL}/release-group?query={query}"); let url = format!("{MB_BASE_URL}/release-group?query={query}");
let response: DeserializeSearchReleaseGroupResponse = self.http.get(&url)?; let response: DeserializeSearchReleaseGroupResponse = self.http.get(&url)?;
@ -49,40 +31,186 @@ impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
} }
} }
#[derive(Default)] pub enum Logical {
pub struct SearchReleaseGroupRequest<'a> { Unary(Unary),
arid: Option<&'a Mbid>, Binary(Boolean),
first_release_date: Option<&'a AlbumDate>,
release_group: Option<&'a str>,
rgid: Option<&'a Mbid>,
} }
impl<'a> SearchReleaseGroupRequest<'a> { impl fmt::Display for Logical {
pub fn new() -> Self { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Self::default() match self {
Logical::Unary(u) => write!(f, "{u}"),
Logical::Binary(b) => write!(f, "{b}"),
}
}
}
pub enum Unary {
Require,
Prohibit,
}
impl fmt::Display for Unary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Unary::Require => write!(f, "+"),
Unary::Prohibit => write!(f, "-"),
}
}
}
pub enum Boolean {
And,
Or,
Not,
}
impl fmt::Display for Boolean {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Boolean::And => write!(f, "AND "),
Boolean::Or => write!(f, "OR "),
Boolean::Not => write!(f, "NOT "),
}
}
}
pub enum Expression<Entity> {
Term(Entity),
Expr(Query<Entity>),
}
impl<Entity: fmt::Display> fmt::Display for Expression<Entity> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Expression::Term(t) => write!(f, "{t}"),
Expression::Expr(q) => write!(f, "({q})"),
}
}
}
pub struct Query<Entity> {
left: (Option<Unary>, Box<Expression<Entity>>),
right: Vec<(Logical, Box<Expression<Entity>>)>,
}
impl<Entity: fmt::Display> fmt::Display for Query<Entity> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(u) = &self.left.0 {
write!(f, "{u}")?;
}
write!(f, "{}", self.left.1)?;
for (logical, expr) in self.right.iter() {
write!(f, " {logical}{expr}")?;
}
Ok(())
}
}
impl<'a, Entity> Query<Entity> {
pub fn expression(expr: Expression<Entity>) -> Self {
Query {
left: (None, Box::new(expr)),
right: vec![],
}
} }
pub fn arid(&mut self, arid: &'a Mbid) -> &mut Self { pub fn require(expr: Expression<Entity>) -> Self {
self.arid = Some(arid); Query {
left: (Some(Unary::Require), Box::new(expr)),
right: vec![],
}
}
pub fn and_require(mut self, expr: Expression<Entity>) -> Self {
self.right
.push((Logical::Unary(Unary::Require), Box::new(expr)));
self self
} }
pub fn first_release_date(&mut self, first_release_date: &'a AlbumDate) -> &mut Self { pub fn prohibit(expr: Expression<Entity>) -> Self {
self.first_release_date = Some(first_release_date); Query {
left: (Some(Unary::Prohibit), Box::new(expr)),
right: vec![],
}
}
pub fn and_prohibit(mut self, expr: Expression<Entity>) -> Self {
self.right
.push((Logical::Unary(Unary::Prohibit), Box::new(expr)));
self self
} }
pub fn release_group(&mut self, release_group: &'a str) -> &mut Self { pub fn and(mut self, expr: Expression<Entity>) -> Self {
self.release_group = Some(release_group); self.right
.push((Logical::Binary(Boolean::And), Box::new(expr)));
self self
} }
pub fn rgid(&mut self, rgid: &'a Mbid) -> &mut Self { pub fn or(mut self, expr: Expression<Entity>) -> Self {
self.rgid = Some(rgid); self.right
.push((Logical::Binary(Boolean::Or), Box::new(expr)));
self
}
pub fn not(mut self, expr: Expression<Entity>) -> Self {
self.right
.push((Logical::Binary(Boolean::Not), Box::new(expr)));
self self
} }
} }
pub enum SearchReleaseGroup<'a> {
NoField(&'a str),
Arid(&'a Mbid),
FirstReleaseDate(&'a AlbumDate),
ReleaseGroup(&'a str),
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> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoField(s) => write!(f, "\"{s}\""),
Self::Arid(arid) => write!(f, "arid:{}", arid.uuid().as_hyphenated()),
Self::FirstReleaseDate(date) => write!(
f,
"firstreleasedate:{}",
ApiDisplay::format_album_date(date)
),
Self::ReleaseGroup(release_group) => write!(f, "releasegroup:\"{release_group}\""),
Self::Rgid(rgid) => write!(f, "rgid:{}", rgid.uuid().as_hyphenated()),
}
}
}
pub type SearchReleaseGroupRequest<'a> = Query<SearchReleaseGroup<'a>>;
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct SearchReleaseGroupResponse { pub struct SearchReleaseGroupResponse {
pub release_groups: Vec<SearchReleaseGroupResponseReleaseGroup>, pub release_groups: Vec<SearchReleaseGroupResponseReleaseGroup>,
@ -148,6 +276,44 @@ mod tests {
use super::*; use super::*;
#[test]
fn lucene_logical() {
let query = Query::expression(Expression::no_field("jakarta apache"))
.or(Expression::no_field("jakarta"));
assert_eq!(format!("{query}"), "\"jakarta apache\" OR \"jakarta\"");
let query = Query::expression(Expression::no_field("jakarta apache"))
.and(Expression::no_field("jakarta"));
assert_eq!(format!("{query}"), "\"jakarta apache\" AND \"jakarta\"");
let query =
Query::require(Expression::no_field("jakarta")).or(Expression::no_field("lucene"));
assert_eq!(format!("{query}"), "+\"jakarta\" OR \"lucene\"");
let query = Query::expression(Expression::no_field("jakarta apache"))
.not(Expression::no_field("Apache Lucene"));
assert_eq!(
format!("{query}"),
"\"jakarta apache\" NOT \"Apache Lucene\""
);
let query = Query::expression(Expression::no_field("jakarta apache"))
.and_prohibit(Expression::no_field("Apache Lucene"));
assert_eq!(format!("{query}"), "\"jakarta apache\" -\"Apache Lucene\"");
}
#[test]
fn lucene_grouping() {
let query = Query::expression(Expression::Expr(
Query::expression(Expression::no_field("jakarta")).or(Expression::no_field("apache")),
))
.and(Expression::no_field("website"));
assert_eq!(
format!("{query}"),
"(\"jakarta\" OR \"apache\") AND \"website\""
);
}
#[test] #[test]
fn search_release_group() { fn search_release_group() {
let mut http = MockIMusicBrainzHttp::new(); let mut http = MockIMusicBrainzHttp::new();
@ -214,21 +380,18 @@ 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 mut request = SearchReleaseGroupRequest::new(); let query = Query::expression(Expression::arid(&arid))
request .and(Expression::release_group(&title.title))
.arid(&arid) .and(Expression::first_release_date(&date));
.release_group(&title.title)
.first_release_date(&date);
let matches = client.search_release_group(request).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 mut request = SearchReleaseGroupRequest::new(); let query = Query::expression(Expression::rgid(&rgid));
request.rgid(&rgid);
let matches = client.search_release_group(request).unwrap(); let matches = client.search_release_group(query).unwrap();
assert_eq!(matches, response); assert_eq!(matches, response);
} }
@ -258,12 +421,10 @@ 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 mut request = SearchReleaseGroupRequest::new(); let query = Query::expression(Expression::arid(&arid))
request .and(Expression::release_group(&title.title))
.arid(&arid) .and(Expression::first_release_date(&date));
.release_group(&title.title)
.first_release_date(&date);
let _ = client.search_release_group(request).unwrap(); let _ = client.search_release_group(query).unwrap();
} }
} }

View File

@ -7,7 +7,7 @@ use musichoard::{
}, },
external::musicbrainz::{ external::musicbrainz::{
api::{ api::{
search::{SearchReleaseGroupRequest, SearchReleaseGroupResponseReleaseGroup}, search::{Expression, Query, SearchReleaseGroupResponseReleaseGroup},
MusicBrainzClient, MusicBrainzClient,
}, },
IMusicBrainzHttp, IMusicBrainzHttp,
@ -37,13 +37,11 @@ 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 mut request = SearchReleaseGroupRequest::default(); let query = Query::expression(Expression::arid(arid))
request .and(Expression::first_release_date(&date))
.arid(arid) .and(Expression::release_group(&album.id.title));
.first_release_date(&date)
.release_group(&album.id.title);
let mb_response = self.client.search_release_group(request)?; let mb_response = self.client.search_release_group(query)?;
Ok(mb_response Ok(mb_response
.release_groups .release_groups