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",
|
"mockall",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"openssh",
|
"openssh",
|
||||||
|
"paste",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
@ -812,9 +813,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.14"
|
version = "1.0.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
|
@ -10,6 +10,7 @@ aho-corasick = { version = "1.1.2", optional = true }
|
|||||||
crossterm = { version = "0.27.0", optional = true}
|
crossterm = { version = "0.27.0", optional = true}
|
||||||
once_cell = { version = "1.19.0", optional = true}
|
once_cell = { version = "1.19.0", optional = true}
|
||||||
openssh = { version = "0.10.3", features = ["native-mux"], default-features = false, 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}
|
ratatui = { version = "0.26.0", optional = true}
|
||||||
reqwest = { version = "0.11.25", features = ["blocking", "json"], optional = true }
|
reqwest = { version = "0.11.25", features = ["blocking", "json"], optional = true }
|
||||||
serde = { version = "1.0.196", features = ["derive"], optional = true }
|
serde = { version = "1.0.196", features = ["derive"], optional = true }
|
||||||
@ -33,7 +34,7 @@ bin = ["structopt"]
|
|||||||
database-json = ["serde", "serde_json"]
|
database-json = ["serde", "serde_json"]
|
||||||
library-beets = []
|
library-beets = []
|
||||||
library-beets-ssh = ["openssh", "tokio"]
|
library-beets-ssh = ["openssh", "tokio"]
|
||||||
musicbrainz = ["reqwest", "serde", "serde_json"]
|
musicbrainz = ["paste", "reqwest", "serde", "serde_json"]
|
||||||
tui = ["aho-corasick", "crossterm", "once_cell", "ratatui"]
|
tui = ["aho-corasick", "crossterm", "once_cell", "ratatui"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
@ -50,8 +51,8 @@ path = "examples/musicbrainz_api/lookup_artist.rs"
|
|||||||
required-features = ["bin", "musicbrainz"]
|
required-features = ["bin", "musicbrainz"]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "musicbrainz-api---search-release-group"
|
name = "musicbrainz-api---search"
|
||||||
path = "examples/musicbrainz_api/search_release_group.rs"
|
path = "examples/musicbrainz_api/search.rs"
|
||||||
required-features = ["bin", "musicbrainz"]
|
required-features = ["bin", "musicbrainz"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
271
src/external/musicbrainz/api/search/mod.rs
vendored
271
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).
|
//! Module for interacting with the [MusicBrainz API](https://musicbrainz.org/doc/MusicBrainz_API).
|
||||||
|
mod artist;
|
||||||
mod query;
|
mod query;
|
||||||
|
mod release_group;
|
||||||
|
|
||||||
use std::fmt;
|
pub use artist::{SearchArtistRequest, SearchArtistResponse, SearchArtistResponseArtist};
|
||||||
|
pub use release_group::{
|
||||||
use query::{impl_term, EmptyQuery, EmptyQueryJoin, QueryJoin};
|
SearchReleaseGroupRequest, SearchReleaseGroupResponse, SearchReleaseGroupResponseReleaseGroup,
|
||||||
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,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
|
use paste::paste;
|
||||||
pub fn search_release_group(
|
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,
|
&mut self,
|
||||||
query: SearchReleaseGroupRequest,
|
query: [<Search $name Request>]
|
||||||
) -> Result<SearchReleaseGroupResponse, Error> {
|
) -> Result<[<Search $name Response>], Error> {
|
||||||
let query: String =
|
let query: String =
|
||||||
form_urlencoded::byte_serialize(format!("{query}").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}/{entity}?query={query}", entity = $entity);
|
||||||
|
|
||||||
let response: DeserializeSearchReleaseGroupResponse = self.http.get(&url)?;
|
let response: [<DeserializeSearch $name Response>] = self.http.get(&url)?;
|
||||||
Ok(response.into())
|
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]
|
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
|
||||||
fn search_release_group_empty_date() {
|
impl_search_entity!(Artist, "artist");
|
||||||
let mut http = MockIMusicBrainzHttp::new();
|
impl_search_entity!(ReleaseGroup, "release-group");
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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