Support for artist musicbrainz search

This commit is contained in:
Wojciech Kozlowski 2024-08-30 11:40:12 +02:00
parent 5eef5f0f22
commit 096b8ed075
7 changed files with 513 additions and 373 deletions

5
Cargo.lock generated
View File

@ -645,6 +645,7 @@ dependencies = [
"mockall",
"once_cell",
"openssh",
"paste",
"ratatui",
"reqwest",
"serde",
@ -812,9 +813,9 @@ dependencies = [
[[package]]
name = "paste"
version = "1.0.14"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "percent-encoding"

View File

@ -10,6 +10,7 @@ aho-corasick = { version = "1.1.2", optional = true }
crossterm = { version = "0.27.0", optional = true}
once_cell = { version = "1.19.0", optional = true}
openssh = { version = "0.10.3", features = ["native-mux"], default-features = false, optional = true}
paste = { version = "1.0.15", optional = true }
ratatui = { version = "0.26.0", optional = true}
reqwest = { version = "0.11.25", features = ["blocking", "json"], optional = true }
serde = { version = "1.0.196", features = ["derive"], optional = true }
@ -33,7 +34,7 @@ bin = ["structopt"]
database-json = ["serde", "serde_json"]
library-beets = []
library-beets-ssh = ["openssh", "tokio"]
musicbrainz = ["reqwest", "serde", "serde_json"]
musicbrainz = ["paste", "reqwest", "serde", "serde_json"]
tui = ["aho-corasick", "crossterm", "once_cell", "ratatui"]
[[bin]]
@ -50,8 +51,8 @@ path = "examples/musicbrainz_api/lookup_artist.rs"
required-features = ["bin", "musicbrainz"]
[[example]]
name = "musicbrainz-api---search-release-group"
path = "examples/musicbrainz_api/search_release_group.rs"
name = "musicbrainz-api---search"
path = "examples/musicbrainz_api/search.rs"
required-features = ["bin", "musicbrainz"]
[package.metadata.docs.rs]

View File

@ -0,0 +1,150 @@
#![allow(non_snake_case)]
use std::{num::ParseIntError, str::FromStr};
use musichoard::{
collection::{album::AlbumDate, musicbrainz::Mbid},
external::musicbrainz::{
api::{
search::{SearchArtistRequest, SearchReleaseGroupRequest},
MusicBrainzClient,
},
http::MusicBrainzHttp,
},
};
use structopt::StructOpt;
use uuid::Uuid;
const USER_AGENT: &str = concat!(
"MusicHoard---examples---musicbrainz-api---search/",
env!("CARGO_PKG_VERSION"),
" ( musichoard@thenineworlds.net )"
);
#[derive(StructOpt)]
struct Opt {
#[structopt(subcommand)]
entity: OptEntity,
}
#[derive(StructOpt)]
enum OptEntity {
#[structopt(about = "Search artist")]
Artist(OptArtist),
#[structopt(about = "Search release group")]
ReleaseGroup(OptReleaseGroup),
}
#[derive(StructOpt)]
struct OptArtist {
#[structopt(help = "Artist search string")]
string: String,
}
#[derive(StructOpt)]
enum OptReleaseGroup {
#[structopt(about = "Search by artist MBID, title(, and date)")]
Title(OptReleaseGroupTitle),
#[structopt(about = "Search by release group MBID")]
Rgid(OptReleaseGroupRgid),
}
#[derive(StructOpt)]
struct OptReleaseGroupTitle {
#[structopt(help = "Release group's artist MBID")]
arid: Uuid,
#[structopt(help = "Release group title")]
title: String,
#[structopt(help = "Release group release date")]
date: Option<Date>,
}
#[derive(StructOpt)]
struct OptReleaseGroupRgid {
#[structopt(help = "Release group MBID")]
rgid: Uuid,
}
struct Date(AlbumDate);
impl FromStr for Date {
type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut elems = s.split('-');
let elem = elems.next();
let year = elem.map(|s| s.parse()).transpose()?;
let elem = elems.next();
let month = elem.map(|s| s.parse()).transpose()?;
let elem = elems.next();
let day = elem.map(|s| s.parse()).transpose()?;
Ok(Date(AlbumDate::new(year, month, day)))
}
}
impl From<Date> for AlbumDate {
fn from(value: Date) -> Self {
value.0
}
}
fn main() {
let opt = Opt::from_args();
println!("USER_AGENT: {USER_AGENT}");
let http = MusicBrainzHttp::new(USER_AGENT).expect("failed to create API client");
let mut client = MusicBrainzClient::new(http);
match opt.entity {
OptEntity::Artist(opt_artist) => {
let query = SearchArtistRequest::new().no_field(&opt_artist.string);
println!("Query: {query}");
let matches = client
.search_artist(query)
.expect("failed to make API call");
println!("{matches:#?}");
}
OptEntity::ReleaseGroup(opt_release_group) => {
let arid: Mbid;
let date: AlbumDate;
let title: String;
let rgid: Mbid;
let query = match opt_release_group {
OptReleaseGroup::Title(opt_title) => {
arid = opt_title.arid.into();
date = opt_title.date.map(Into::into).unwrap_or_default();
title = opt_title.title;
SearchReleaseGroupRequest::new()
.arid(&arid)
.and()
.release_group(&title)
.and()
.first_release_date(&date)
}
OptReleaseGroup::Rgid(opt_rgid) => {
rgid = opt_rgid.rgid.into();
SearchReleaseGroupRequest::new().rgid(&rgid)
}
};
println!("Query: {query}");
let matches = client
.search_release_group(query)
.expect("failed to make API call");
println!("{matches:#?}");
}
}
}

View File

@ -1,118 +0,0 @@
#![allow(non_snake_case)]
use std::{num::ParseIntError, str::FromStr};
use musichoard::{
collection::{album::AlbumDate, musicbrainz::Mbid},
external::musicbrainz::{
api::{search::SearchReleaseGroupRequest, MusicBrainzClient},
http::MusicBrainzHttp,
},
};
use structopt::StructOpt;
use uuid::Uuid;
const USER_AGENT: &str = concat!(
"MusicHoard---examples---musicbrainz-api---search-release-group/",
env!("CARGO_PKG_VERSION"),
" ( musichoard@thenineworlds.net )"
);
#[derive(StructOpt)]
struct Opt {
#[structopt(subcommand)]
command: OptCommand,
}
#[derive(StructOpt)]
enum OptCommand {
#[structopt(about = "Search by artist MBID, title(, and date)")]
Title(OptTitle),
#[structopt(about = "Search by release group MBID")]
Rgid(OptRgid),
}
#[derive(StructOpt)]
struct OptTitle {
#[structopt(help = "Release group's artist MBID")]
arid: Uuid,
#[structopt(help = "Release group title")]
title: String,
#[structopt(help = "Release group release date")]
date: Option<Date>,
}
#[derive(StructOpt)]
struct OptRgid {
#[structopt(help = "Release group MBID")]
rgid: Uuid,
}
struct Date(AlbumDate);
impl FromStr for Date {
type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut elems = s.split('-');
let elem = elems.next();
let year = elem.map(|s| s.parse()).transpose()?;
let elem = elems.next();
let month = elem.map(|s| s.parse()).transpose()?;
let elem = elems.next();
let day = elem.map(|s| s.parse()).transpose()?;
Ok(Date(AlbumDate::new(year, month, day)))
}
}
impl From<Date> for AlbumDate {
fn from(value: Date) -> Self {
value.0
}
}
fn main() {
let opt = Opt::from_args();
println!("USER_AGENT: {USER_AGENT}");
let http = MusicBrainzHttp::new(USER_AGENT).expect("failed to create API client");
let mut client = MusicBrainzClient::new(http);
let arid: Mbid;
let date: AlbumDate;
let title: String;
let rgid: Mbid;
let query = match opt.command {
OptCommand::Title(opt_title) => {
arid = opt_title.arid.into();
date = opt_title.date.map(Into::into).unwrap_or_default();
title = opt_title.title;
SearchReleaseGroupRequest::new()
.arid(&arid)
.and()
.release_group(&title)
.and()
.first_release_date(&date)
}
OptCommand::Rgid(opt_rgid) => {
rgid = opt_rgid.rgid.into();
SearchReleaseGroupRequest::new().rgid(&rgid)
}
};
println!("Query: {query}");
let matches = client
.search_release_group(query)
.expect("failed to make API call");
println!("{matches:#?}");
}

View File

@ -0,0 +1,76 @@
use std::fmt;
use serde::Deserialize;
use crate::{collection::{artist::ArtistId, musicbrainz::Mbid}, external::musicbrainz::api::SerdeMbid};
use super::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin};
pub enum SearchArtist<'a> {
NoField(&'a str),
}
impl<'a> fmt::Display for SearchArtist<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoField(s) => write!(f, "\"{s}\""),
}
}
}
pub type SearchArtistRequest<'a> = Query<SearchArtist<'a>>;
impl_term!(no_field, SearchArtist<'a>, NoField, &'a str);
#[derive(Debug, PartialEq, Eq)]
pub struct SearchArtistResponse {
pub artists: Vec<SearchArtistResponseArtist>,
}
#[derive(Clone, Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))]
pub struct DeserializeSearchArtistResponse {
artists: Vec<DeserializeSearchArtistResponseArtist>,
}
impl From<DeserializeSearchArtistResponse> for SearchArtistResponse {
fn from(value: DeserializeSearchArtistResponse) -> Self {
SearchArtistResponse {
artists: value.artists.into_iter().map(Into::into).collect(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SearchArtistResponseArtist {
pub score: u8,
pub id: Mbid,
pub name: ArtistId,
pub sort: Option<ArtistId>,
pub disambiguation: Option<String>,
}
#[derive(Clone, Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))]
struct DeserializeSearchArtistResponseArtist {
score: u8,
id: SerdeMbid,
name: String,
sort_name: String,
disambiguation: Option<String>,
}
impl From<DeserializeSearchArtistResponseArtist> for SearchArtistResponseArtist {
fn from(value: DeserializeSearchArtistResponseArtist) -> Self {
let sort: Option<ArtistId> = Some(value.sort_name)
.filter(|s| s != &value.name)
.map(Into::into);
SearchArtistResponseArtist {
score: value.score,
id: value.id.into(),
name: value.name.into(),
sort,
disambiguation: value.disambiguation,
}
}
}

View File

@ -1,257 +1,46 @@
//! Module for interacting with the [MusicBrainz API](https://musicbrainz.org/doc/MusicBrainz_API).
mod artist;
mod query;
mod release_group;
use std::fmt;
use query::{impl_term, EmptyQuery, EmptyQueryJoin, QueryJoin};
use serde::Deserialize;
use url::form_urlencoded;
use crate::{
collection::{album::AlbumDate, musicbrainz::Mbid},
core::collection::album::{AlbumPrimaryType, AlbumSecondaryType},
external::musicbrainz::{
api::{
search::query::Query, ApiDisplay, Error, MusicBrainzClient, SerdeAlbumDate,
SerdeAlbumPrimaryType, SerdeAlbumSecondaryType, SerdeMbid, MB_BASE_URL,
},
IMusicBrainzHttp,
},
pub use artist::{SearchArtistRequest, SearchArtistResponse, SearchArtistResponseArtist};
pub use release_group::{
SearchReleaseGroupRequest, SearchReleaseGroupResponse, SearchReleaseGroupResponseReleaseGroup,
};
use paste::paste;
use url::form_urlencoded;
use crate::external::musicbrainz::{
api::{
search::{
artist::DeserializeSearchArtistResponse,
release_group::DeserializeSearchReleaseGroupResponse,
},
Error, MusicBrainzClient, MB_BASE_URL,
},
IMusicBrainzHttp,
};
macro_rules! impl_search_entity {
($name:ident, $entity:literal) => {
paste! {
pub fn [<search_ $name:snake>](
&mut self,
query: [<Search $name Request>]
) -> Result<[<Search $name Response>], Error> {
let query: String =
form_urlencoded::byte_serialize(format!("{query}").as_bytes()).collect();
let url = format!("{MB_BASE_URL}/{entity}?query={query}", entity = $entity);
let response: [<DeserializeSearch $name Response>] = self.http.get(&url)?;
Ok(response.into())
}
}
};
}
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
pub fn search_release_group(
&mut self,
query: SearchReleaseGroupRequest,
) -> Result<SearchReleaseGroupResponse, Error> {
let query: String =
form_urlencoded::byte_serialize(format!("{query}").as_bytes()).collect();
let url = format!("{MB_BASE_URL}/release-group?query={query}");
let response: DeserializeSearchReleaseGroupResponse = self.http.get(&url)?;
Ok(response.into())
}
}
pub enum SearchReleaseGroup<'a> {
NoField(&'a str),
Arid(&'a Mbid),
FirstReleaseDate(&'a AlbumDate),
ReleaseGroup(&'a str),
Rgid(&'a Mbid),
}
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>>;
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)]
pub struct SearchReleaseGroupResponse {
pub release_groups: Vec<SearchReleaseGroupResponseReleaseGroup>,
}
#[derive(Clone, Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))]
struct DeserializeSearchReleaseGroupResponse {
release_groups: Vec<DeserializeSearchReleaseGroupResponseReleaseGroup>,
}
impl From<DeserializeSearchReleaseGroupResponse> for SearchReleaseGroupResponse {
fn from(value: DeserializeSearchReleaseGroupResponse) -> Self {
SearchReleaseGroupResponse {
release_groups: value.release_groups.into_iter().map(Into::into).collect(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SearchReleaseGroupResponseReleaseGroup {
pub score: u8,
pub id: Mbid,
pub title: String,
pub first_release_date: AlbumDate,
pub primary_type: AlbumPrimaryType,
pub secondary_types: Option<Vec<AlbumSecondaryType>>,
}
#[derive(Clone, Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))]
struct DeserializeSearchReleaseGroupResponseReleaseGroup {
score: u8,
id: SerdeMbid,
title: String,
first_release_date: SerdeAlbumDate,
primary_type: SerdeAlbumPrimaryType,
secondary_types: Option<Vec<SerdeAlbumSecondaryType>>,
}
impl From<DeserializeSearchReleaseGroupResponseReleaseGroup>
for SearchReleaseGroupResponseReleaseGroup
{
fn from(value: DeserializeSearchReleaseGroupResponseReleaseGroup) -> Self {
SearchReleaseGroupResponseReleaseGroup {
score: value.score,
id: value.id.into(),
title: value.title,
first_release_date: value.first_release_date.into(),
primary_type: value.primary_type.into(),
secondary_types: value
.secondary_types
.map(|v| v.into_iter().map(Into::into).collect()),
}
}
}
#[cfg(test)]
mod tests {
use mockall::{predicate, Sequence};
use crate::{collection::album::AlbumId, external::musicbrainz::MockIMusicBrainzHttp};
use super::*;
#[test]
fn search_release_group() {
let mut http = MockIMusicBrainzHttp::new();
let url_title = format!(
"https://musicbrainz.org/ws/2\
/release-group\
?query=arid%3A{arid}+AND+firstreleasedate%3A{date}+AND+releasegroup%3A%22{title}%22",
arid = "00000000-0000-0000-0000-000000000000",
date = "1986-04",
title = "an+album",
);
let url_rgid = format!(
"https://musicbrainz.org/ws/2\
/release-group\
?query=rgid%3A{rgid}",
rgid = "11111111-1111-1111-1111-111111111111",
);
let de_release_group = DeserializeSearchReleaseGroupResponseReleaseGroup {
score: 67,
id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()),
title: String::from("an album"),
first_release_date: SerdeAlbumDate((1986, 4).into()),
primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album),
secondary_types: Some(vec![SerdeAlbumSecondaryType(AlbumSecondaryType::Live)]),
};
let de_response = DeserializeSearchReleaseGroupResponse {
release_groups: vec![de_release_group.clone()],
};
let release_group = SearchReleaseGroupResponseReleaseGroup {
score: 67,
id: de_release_group.id.0,
title: de_release_group.title,
first_release_date: de_release_group.first_release_date.0,
primary_type: de_release_group.primary_type.0,
secondary_types: de_release_group
.secondary_types
.map(|v| v.into_iter().map(|st| st.0).collect()),
};
let response = SearchReleaseGroupResponse {
release_groups: vec![release_group.clone()],
};
let mut seq = Sequence::new();
let title_response = de_response.clone();
http.expect_get()
.times(1)
.with(predicate::eq(url_title))
.return_once(|_| Ok(title_response))
.in_sequence(&mut seq);
let rgid_response = de_response;
http.expect_get()
.times(1)
.with(predicate::eq(url_rgid))
.return_once(|_| Ok(rgid_response))
.in_sequence(&mut seq);
let mut client = MusicBrainzClient::new(http);
let arid: Mbid = "00000000-0000-0000-0000-000000000000".try_into().unwrap();
let title: AlbumId = AlbumId::new("an album");
let date = (1986, 4).into();
let query = SearchReleaseGroupRequest::new()
.arid(&arid)
.and()
.release_group(&title.title)
.and()
.first_release_date(&date);
let matches = client.search_release_group(query).unwrap();
assert_eq!(matches, response);
let rgid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
let query = SearchReleaseGroupRequest::new().rgid(&rgid);
let matches = client.search_release_group(query).unwrap();
assert_eq!(matches, response);
}
#[test]
fn search_release_group_empty_date() {
let mut http = MockIMusicBrainzHttp::new();
let url = format!(
"https://musicbrainz.org/ws/2\
/release-group\
?query=arid%3A{arid}+AND+releasegroup%3A%22{title}%22",
arid = "00000000-0000-0000-0000-000000000000",
title = "an+album",
);
let de_response = DeserializeSearchReleaseGroupResponse {
release_groups: vec![],
};
http.expect_get()
.times(1)
.with(predicate::eq(url))
.return_once(|_| Ok(de_response));
let mut client = MusicBrainzClient::new(http);
let arid: Mbid = "00000000-0000-0000-0000-000000000000".try_into().unwrap();
let title: AlbumId = AlbumId::new("an album");
let date = AlbumDate::default();
let query = SearchReleaseGroupRequest::new()
.arid(&arid)
.and()
.release_group(&title.title)
.and()
.first_release_date(&date);
let _ = client.search_release_group(query).unwrap();
}
impl_search_entity!(Artist, "artist");
impl_search_entity!(ReleaseGroup, "release-group");
}

View File

@ -0,0 +1,241 @@
use std::fmt;
use serde::Deserialize;
use crate::{
collection::{
album::{AlbumDate, AlbumPrimaryType, AlbumSecondaryType},
musicbrainz::Mbid,
},
external::musicbrainz::api::{
ApiDisplay, SerdeAlbumDate, SerdeAlbumPrimaryType, SerdeAlbumSecondaryType, SerdeMbid,
},
};
use super::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin};
pub enum SearchReleaseGroup<'a> {
NoField(&'a str),
Arid(&'a Mbid),
FirstReleaseDate(&'a AlbumDate),
ReleaseGroup(&'a str),
Rgid(&'a Mbid),
}
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>>;
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)]
pub struct SearchReleaseGroupResponse {
pub release_groups: Vec<SearchReleaseGroupResponseReleaseGroup>,
}
#[derive(Clone, Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))]
pub struct DeserializeSearchReleaseGroupResponse {
release_groups: Vec<DeserializeSearchReleaseGroupResponseReleaseGroup>,
}
impl From<DeserializeSearchReleaseGroupResponse> for SearchReleaseGroupResponse {
fn from(value: DeserializeSearchReleaseGroupResponse) -> Self {
SearchReleaseGroupResponse {
release_groups: value.release_groups.into_iter().map(Into::into).collect(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SearchReleaseGroupResponseReleaseGroup {
pub score: u8,
pub id: Mbid,
pub title: String,
pub first_release_date: AlbumDate,
pub primary_type: AlbumPrimaryType,
pub secondary_types: Option<Vec<AlbumSecondaryType>>,
}
#[derive(Clone, Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))]
struct DeserializeSearchReleaseGroupResponseReleaseGroup {
score: u8,
id: SerdeMbid,
title: String,
first_release_date: SerdeAlbumDate,
primary_type: SerdeAlbumPrimaryType,
secondary_types: Option<Vec<SerdeAlbumSecondaryType>>,
}
impl From<DeserializeSearchReleaseGroupResponseReleaseGroup>
for SearchReleaseGroupResponseReleaseGroup
{
fn from(value: DeserializeSearchReleaseGroupResponseReleaseGroup) -> Self {
SearchReleaseGroupResponseReleaseGroup {
score: value.score,
id: value.id.into(),
title: value.title,
first_release_date: value.first_release_date.into(),
primary_type: value.primary_type.into(),
secondary_types: value
.secondary_types
.map(|v| v.into_iter().map(Into::into).collect()),
}
}
}
#[cfg(test)]
mod tests {
use mockall::{predicate, Sequence};
use crate::{
collection::album::AlbumId,
external::musicbrainz::{api::MusicBrainzClient, MockIMusicBrainzHttp},
};
use super::*;
#[test]
fn search_release_group() {
let mut http = MockIMusicBrainzHttp::new();
let url_title = format!(
"https://musicbrainz.org/ws/2\
/release-group\
?query=arid%3A{arid}+AND+firstreleasedate%3A{date}+AND+releasegroup%3A%22{title}%22",
arid = "00000000-0000-0000-0000-000000000000",
date = "1986-04",
title = "an+album",
);
let url_rgid = format!(
"https://musicbrainz.org/ws/2\
/release-group\
?query=rgid%3A{rgid}",
rgid = "11111111-1111-1111-1111-111111111111",
);
let de_release_group = DeserializeSearchReleaseGroupResponseReleaseGroup {
score: 67,
id: SerdeMbid("11111111-1111-1111-1111-111111111111".try_into().unwrap()),
title: String::from("an album"),
first_release_date: SerdeAlbumDate((1986, 4).into()),
primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album),
secondary_types: Some(vec![SerdeAlbumSecondaryType(AlbumSecondaryType::Live)]),
};
let de_response = DeserializeSearchReleaseGroupResponse {
release_groups: vec![de_release_group.clone()],
};
let release_group = SearchReleaseGroupResponseReleaseGroup {
score: 67,
id: de_release_group.id.0,
title: de_release_group.title,
first_release_date: de_release_group.first_release_date.0,
primary_type: de_release_group.primary_type.0,
secondary_types: de_release_group
.secondary_types
.map(|v| v.into_iter().map(|st| st.0).collect()),
};
let response = SearchReleaseGroupResponse {
release_groups: vec![release_group.clone()],
};
let mut seq = Sequence::new();
let title_response = de_response.clone();
http.expect_get()
.times(1)
.with(predicate::eq(url_title))
.return_once(|_| Ok(title_response))
.in_sequence(&mut seq);
let rgid_response = de_response;
http.expect_get()
.times(1)
.with(predicate::eq(url_rgid))
.return_once(|_| Ok(rgid_response))
.in_sequence(&mut seq);
let mut client = MusicBrainzClient::new(http);
let arid: Mbid = "00000000-0000-0000-0000-000000000000".try_into().unwrap();
let title: AlbumId = AlbumId::new("an album");
let date = (1986, 4).into();
let query = SearchReleaseGroupRequest::new()
.arid(&arid)
.and()
.release_group(&title.title)
.and()
.first_release_date(&date);
let matches = client.search_release_group(query).unwrap();
assert_eq!(matches, response);
let rgid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
let query = SearchReleaseGroupRequest::new().rgid(&rgid);
let matches = client.search_release_group(query).unwrap();
assert_eq!(matches, response);
}
#[test]
fn search_release_group_empty_date() {
let mut http = MockIMusicBrainzHttp::new();
let url = format!(
"https://musicbrainz.org/ws/2\
/release-group\
?query=arid%3A{arid}+AND+releasegroup%3A%22{title}%22",
arid = "00000000-0000-0000-0000-000000000000",
title = "an+album",
);
let de_response = DeserializeSearchReleaseGroupResponse {
release_groups: vec![],
};
http.expect_get()
.times(1)
.with(predicate::eq(url))
.return_once(|_| Ok(de_response));
let mut client = MusicBrainzClient::new(http);
let arid: Mbid = "00000000-0000-0000-0000-000000000000".try_into().unwrap();
let title: AlbumId = AlbumId::new("an album");
let date = AlbumDate::default();
let query = SearchReleaseGroupRequest::new()
.arid(&arid)
.and()
.release_group(&title.title)
.and()
.first_release_date(&date);
let _ = client.search_release_group(query).unwrap();
}
}