Support for artist musicbrainz search
This commit is contained in:
parent
5eef5f0f22
commit
096b8ed075
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -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"
|
||||
|
@ -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]
|
||||
|
150
examples/musicbrainz_api/search.rs
Normal file
150
examples/musicbrainz_api/search.rs
Normal 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:#?}");
|
||||
}
|
||||
}
|
||||
}
|
@ -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:#?}");
|
||||
}
|
76
src/external/musicbrainz/api/search/artist.rs
vendored
Normal file
76
src/external/musicbrainz/api/search/artist.rs
vendored
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
275
src/external/musicbrainz/api/search/mod.rs
vendored
275
src/external/musicbrainz/api/search/mod.rs
vendored
@ -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,
|
||||
};
|
||||
|
||||
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
|
||||
pub fn search_release_group(
|
||||
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: SearchReleaseGroupRequest,
|
||||
) -> Result<SearchReleaseGroupResponse, Error> {
|
||||
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}/release-group?query={query}");
|
||||
let url = format!("{MB_BASE_URL}/{entity}?query={query}", entity = $entity);
|
||||
|
||||
let response: DeserializeSearchReleaseGroupResponse = self.http.get(&url)?;
|
||||
let response: [<DeserializeSearch $name Response>] = 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<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
|
||||
impl_search_entity!(Artist, "artist");
|
||||
impl_search_entity!(ReleaseGroup, "release-group");
|
||||
}
|
||||
|
241
src/external/musicbrainz/api/search/release_group.rs
vendored
Normal file
241
src/external/musicbrainz/api/search/release_group.rs
vendored
Normal 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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user