Decide carefully where external::musicbrainz
belongs
#196
@ -1,7 +1,10 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use musichoard::{
|
||||
external::musicbrainz::{http::MusicBrainzHttp, LookupArtistRequest, MusicBrainzClient},
|
||||
external::musicbrainz::{
|
||||
api::{lookup::LookupArtistRequest, MusicBrainzClient},
|
||||
http::MusicBrainzHttp,
|
||||
},
|
||||
interface::musicbrainz::Mbid,
|
||||
};
|
||||
use structopt::StructOpt;
|
||||
|
@ -4,7 +4,9 @@ use std::{num::ParseIntError, str::FromStr};
|
||||
|
||||
use musichoard::{
|
||||
collection::album::AlbumDate,
|
||||
external::musicbrainz::{http::MusicBrainzHttp, MusicBrainzClient, SearchReleaseGroupRequest},
|
||||
external::musicbrainz::{
|
||||
api::search::SearchReleaseGroupRequest, api::MusicBrainzClient, http::MusicBrainzHttp,
|
||||
},
|
||||
interface::musicbrainz::Mbid,
|
||||
};
|
||||
use structopt::StructOpt;
|
||||
|
105
src/external/musicbrainz/api/lookup.rs
vendored
Normal file
105
src/external/musicbrainz/api/lookup.rs
vendored
Normal file
@ -0,0 +1,105 @@
|
||||
use serde::Deserialize;
|
||||
use url::form_urlencoded;
|
||||
|
||||
use crate::{
|
||||
collection::album::{AlbumDate, AlbumPrimaryType, AlbumSecondaryType},
|
||||
external::musicbrainz::{
|
||||
api::{
|
||||
Error, MusicBrainzClient, SerdeAlbumDate, SerdeAlbumPrimaryType,
|
||||
SerdeAlbumSecondaryType, SerdeMbid, MB_BASE_URL,
|
||||
},
|
||||
IMusicBrainzHttp,
|
||||
},
|
||||
interface::musicbrainz::Mbid,
|
||||
};
|
||||
|
||||
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
|
||||
pub fn lookup_artist(
|
||||
&mut self,
|
||||
request: LookupArtistRequest,
|
||||
) -> Result<LookupArtistResponse, Error> {
|
||||
let mut include: Vec<String> = vec![];
|
||||
|
||||
let mbid: String = request.mbid.uuid().as_hyphenated().to_string();
|
||||
|
||||
if request.release_groups {
|
||||
include.push(String::from("release-groups"));
|
||||
}
|
||||
|
||||
let include: String =
|
||||
form_urlencoded::byte_serialize(include.join("+").as_bytes()).collect();
|
||||
let url = format!("{MB_BASE_URL}/artist/{mbid}?inc={include}");
|
||||
|
||||
let response: DeserializeLookupArtistResponse = self.http.get(&url)?;
|
||||
Ok(response.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LookupArtistRequest<'a> {
|
||||
mbid: &'a Mbid,
|
||||
release_groups: bool,
|
||||
}
|
||||
|
||||
impl<'a> LookupArtistRequest<'a> {
|
||||
pub fn new(mbid: &'a Mbid) -> Self {
|
||||
LookupArtistRequest {
|
||||
mbid,
|
||||
release_groups: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn include_release_groups(&mut self) -> &mut Self {
|
||||
self.release_groups = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LookupArtistResponse {
|
||||
pub release_groups: Vec<LookupArtistResponseReleaseGroup>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||
struct DeserializeLookupArtistResponse {
|
||||
release_groups: Vec<DeserializeLookupArtistResponseReleaseGroup>,
|
||||
}
|
||||
|
||||
impl From<DeserializeLookupArtistResponse> for LookupArtistResponse {
|
||||
fn from(value: DeserializeLookupArtistResponse) -> Self {
|
||||
LookupArtistResponse {
|
||||
release_groups: value.release_groups.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LookupArtistResponseReleaseGroup {
|
||||
pub id: Mbid,
|
||||
pub title: String,
|
||||
pub first_release_date: AlbumDate,
|
||||
pub primary_type: AlbumPrimaryType,
|
||||
pub secondary_types: Vec<AlbumSecondaryType>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||
struct DeserializeLookupArtistResponseReleaseGroup {
|
||||
id: SerdeMbid,
|
||||
title: String,
|
||||
first_release_date: SerdeAlbumDate,
|
||||
primary_type: SerdeAlbumPrimaryType,
|
||||
secondary_types: Vec<SerdeAlbumSecondaryType>,
|
||||
}
|
||||
|
||||
impl From<DeserializeLookupArtistResponseReleaseGroup> for LookupArtistResponseReleaseGroup {
|
||||
fn from(value: DeserializeLookupArtistResponseReleaseGroup) -> Self {
|
||||
LookupArtistResponseReleaseGroup {
|
||||
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.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
222
src/external/musicbrainz/api/mod.rs
vendored
Normal file
222
src/external/musicbrainz/api/mod.rs
vendored
Normal file
@ -0,0 +1,222 @@
|
||||
use std::{fmt, num};
|
||||
|
||||
use serde::{de::Visitor, Deserialize, Deserializer};
|
||||
|
||||
use crate::{
|
||||
collection::album::{AlbumDate, AlbumPrimaryType, AlbumSecondaryType},
|
||||
external::musicbrainz::HttpError,
|
||||
interface::musicbrainz::{Mbid, MbidError},
|
||||
};
|
||||
|
||||
pub mod lookup;
|
||||
pub mod search;
|
||||
|
||||
const MB_BASE_URL: &str = "https://musicbrainz.org/ws/2";
|
||||
const MB_RATE_LIMIT_CODE: u16 = 503;
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
/// The HTTP client failed.
|
||||
Http(String),
|
||||
/// The client reached the API rate limit.
|
||||
RateLimit,
|
||||
/// The API response could not be understood.
|
||||
Unknown(u16),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Error::Http(s) => write!(f, "the HTTP client failed: {s}"),
|
||||
Error::RateLimit => write!(f, "the API rate limit has been reached"),
|
||||
Error::Unknown(u) => write!(f, "the API response could not be understood: status {u}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HttpError> for Error {
|
||||
fn from(err: HttpError) -> Self {
|
||||
match err {
|
||||
HttpError::Client(s) => Error::Http(s),
|
||||
HttpError::Status(status) => match status {
|
||||
MB_RATE_LIMIT_CODE => Error::RateLimit,
|
||||
_ => Error::Unknown(status),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MusicBrainzClient<Http> {
|
||||
http: Http,
|
||||
}
|
||||
|
||||
impl<Http> MusicBrainzClient<Http> {
|
||||
pub fn new(http: Http) -> Self {
|
||||
MusicBrainzClient { http }
|
||||
}
|
||||
|
||||
fn format_album_date(date: &AlbumDate) -> Option<String> {
|
||||
match date.year {
|
||||
Some(year) => match date.month {
|
||||
Some(month) => match date.day {
|
||||
Some(day) => Some(format!("{year}-{month:02}-{day:02}")),
|
||||
None => Some(format!("{year}-{month:02}")),
|
||||
},
|
||||
None => Some(format!("{year}")),
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SerdeMbid(Mbid);
|
||||
|
||||
impl From<SerdeMbid> for Mbid {
|
||||
fn from(value: SerdeMbid) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
struct SerdeMbidVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for SerdeMbidVisitor {
|
||||
type Value = SerdeMbid;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a valid MusicBrainz identifier")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(SerdeMbid(
|
||||
v.try_into()
|
||||
.map_err(|e: MbidError| E::custom(e.to_string()))?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SerdeMbid {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(SerdeMbidVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SerdeAlbumDate(AlbumDate);
|
||||
|
||||
impl From<SerdeAlbumDate> for AlbumDate {
|
||||
fn from(value: SerdeAlbumDate) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
struct SerdeAlbumDateVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for SerdeAlbumDateVisitor {
|
||||
type Value = SerdeAlbumDate;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a valid YYYY(-MM-(-DD)) date")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
let mut elems = v.split('-');
|
||||
|
||||
let elem = elems.next();
|
||||
let year = elem
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s.parse()) })
|
||||
.transpose()
|
||||
.map_err(|e: num::ParseIntError| E::custom(e.to_string()))?;
|
||||
|
||||
let elem = elems.next();
|
||||
let month = elem
|
||||
.map(|s| s.parse())
|
||||
.transpose()
|
||||
.map_err(|e: num::ParseIntError| E::custom(e.to_string()))?;
|
||||
|
||||
let elem = elems.next();
|
||||
let day = elem
|
||||
.map(|s| s.parse())
|
||||
.transpose()
|
||||
.map_err(|e: num::ParseIntError| E::custom(e.to_string()))?;
|
||||
|
||||
Ok(SerdeAlbumDate(AlbumDate::new(year, month, day)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SerdeAlbumDate {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(SerdeAlbumDateVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(remote = "AlbumPrimaryType")]
|
||||
pub enum AlbumPrimaryTypeDef {
|
||||
Album,
|
||||
Single,
|
||||
#[serde(rename = "EP")]
|
||||
Ep,
|
||||
Broadcast,
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SerdeAlbumPrimaryType(#[serde(with = "AlbumPrimaryTypeDef")] AlbumPrimaryType);
|
||||
|
||||
impl From<SerdeAlbumPrimaryType> for AlbumPrimaryType {
|
||||
fn from(value: SerdeAlbumPrimaryType) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AlbumPrimaryType> for SerdeAlbumPrimaryType {
|
||||
fn from(value: AlbumPrimaryType) -> Self {
|
||||
SerdeAlbumPrimaryType(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(remote = "AlbumSecondaryType")]
|
||||
pub enum AlbumSecondaryTypeDef {
|
||||
Compilation,
|
||||
Soundtrack,
|
||||
Spokenword,
|
||||
Interview,
|
||||
Audiobook,
|
||||
#[serde(rename = "Audio drama")]
|
||||
AudioDrama,
|
||||
Live,
|
||||
Remix,
|
||||
#[serde(rename = "DJ-mix")]
|
||||
DjMix,
|
||||
#[serde(rename = "Mixtape/Street")]
|
||||
MixtapeStreet,
|
||||
Demo,
|
||||
#[serde(rename = "Field recording")]
|
||||
FieldRecording,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SerdeAlbumSecondaryType(#[serde(with = "AlbumSecondaryTypeDef")] AlbumSecondaryType);
|
||||
|
||||
impl From<SerdeAlbumSecondaryType> for AlbumSecondaryType {
|
||||
fn from(value: SerdeAlbumSecondaryType) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AlbumSecondaryType> for SerdeAlbumSecondaryType {
|
||||
fn from(value: AlbumSecondaryType) -> Self {
|
||||
SerdeAlbumSecondaryType(value)
|
||||
}
|
||||
}
|
144
src/external/musicbrainz/api/search.rs
vendored
Normal file
144
src/external/musicbrainz/api/search.rs
vendored
Normal file
@ -0,0 +1,144 @@
|
||||
//! Module for interacting with the [MusicBrainz API](https://musicbrainz.org/doc/MusicBrainz_API).
|
||||
|
||||
use serde::Deserialize;
|
||||
use url::form_urlencoded;
|
||||
|
||||
use crate::{
|
||||
collection::album::AlbumDate,
|
||||
core::{
|
||||
collection::album::{AlbumPrimaryType, AlbumSecondaryType},
|
||||
interface::musicbrainz::Mbid,
|
||||
},
|
||||
external::musicbrainz::{
|
||||
api::{
|
||||
Error, MusicBrainzClient, SerdeAlbumDate, SerdeAlbumPrimaryType,
|
||||
SerdeAlbumSecondaryType, SerdeMbid, MB_BASE_URL,
|
||||
},
|
||||
IMusicBrainzHttp,
|
||||
},
|
||||
};
|
||||
|
||||
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
|
||||
pub fn search_release_group(
|
||||
&mut self,
|
||||
request: SearchReleaseGroupRequest,
|
||||
) -> Result<SearchReleaseGroupResponse, Error> {
|
||||
let mut query: Vec<String> = vec![];
|
||||
|
||||
if let Some(arid) = request.arid {
|
||||
query.push(format!("arid:{}", arid.uuid().as_hyphenated().to_string()));
|
||||
}
|
||||
|
||||
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().to_string()));
|
||||
}
|
||||
|
||||
let query: String =
|
||||
form_urlencoded::byte_serialize(query.join(" AND ").as_bytes()).collect();
|
||||
let url = format!("{MB_BASE_URL}/release-group?query={query}");
|
||||
|
||||
let response: DeserializeSearchReleaseGroupResponse = self.http.get(&url)?;
|
||||
Ok(response.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SearchReleaseGroupRequest<'a> {
|
||||
arid: Option<&'a Mbid>,
|
||||
first_release_date: Option<&'a AlbumDate>,
|
||||
release_group: Option<&'a str>,
|
||||
rgid: Option<&'a Mbid>,
|
||||
}
|
||||
|
||||
impl<'a> SearchReleaseGroupRequest<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn arid(&mut self, arid: &'a Mbid) -> &mut Self {
|
||||
self.arid = Some(arid);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn first_release_date(&mut self, first_release_date: &'a AlbumDate) -> &mut Self {
|
||||
self.first_release_date = Some(first_release_date);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn release_group(&mut self, release_group: &'a str) -> &mut Self {
|
||||
self.release_group = Some(release_group);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn rgid(&mut self, rgid: &'a Mbid) -> &mut Self {
|
||||
self.rgid = Some(rgid);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SearchReleaseGroupResponse {
|
||||
pub release_groups: Vec<SearchReleaseGroupResponseReleaseGroup>,
|
||||
}
|
||||
|
||||
#[derive(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(Debug)]
|
||||
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(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()),
|
||||
}
|
||||
}
|
||||
}
|
443
src/external/musicbrainz/mod.rs
vendored
443
src/external/musicbrainz/mod.rs
vendored
@ -1,25 +1,11 @@
|
||||
//! Module for interacting with the [MusicBrainz API](https://musicbrainz.org/doc/MusicBrainz_API).
|
||||
|
||||
pub mod api;
|
||||
pub mod http;
|
||||
|
||||
use std::{fmt, num::ParseIntError};
|
||||
|
||||
#[cfg(test)]
|
||||
use mockall::automock;
|
||||
use serde::{
|
||||
de::{DeserializeOwned, Visitor},
|
||||
Deserialize, Deserializer,
|
||||
};
|
||||
use url::form_urlencoded;
|
||||
|
||||
use crate::{
|
||||
collection::album::AlbumDate,
|
||||
core::{
|
||||
collection::album::{AlbumPrimaryType, AlbumSecondaryType},
|
||||
interface::musicbrainz::Mbid,
|
||||
},
|
||||
interface::musicbrainz::MbidError,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
#[cfg_attr(test, automock)]
|
||||
pub trait IMusicBrainzHttp {
|
||||
@ -32,431 +18,6 @@ pub enum HttpError {
|
||||
Status(u16),
|
||||
}
|
||||
|
||||
const MB_BASE_URL: &str = "https://musicbrainz.org/ws/2";
|
||||
const MB_RATE_LIMIT_CODE: u16 = 503;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
/// The HTTP client failed.
|
||||
Http(String),
|
||||
/// The client reached the API rate limit.
|
||||
RateLimit,
|
||||
/// The API response could not be understood.
|
||||
Unknown(u16),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Error::Http(s) => write!(f, "the HTTP client failed: {s}"),
|
||||
Error::RateLimit => write!(f, "the API rate limit has been reached"),
|
||||
Error::Unknown(u) => write!(f, "the API response could not be understood: status {u}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HttpError> for Error {
|
||||
fn from(err: HttpError) -> Self {
|
||||
match err {
|
||||
HttpError::Client(s) => Error::Http(s),
|
||||
HttpError::Status(status) => match status {
|
||||
MB_RATE_LIMIT_CODE => Error::RateLimit,
|
||||
_ => Error::Unknown(status),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MusicBrainzClient<Http> {
|
||||
http: Http,
|
||||
}
|
||||
|
||||
impl<Http> MusicBrainzClient<Http> {
|
||||
pub fn new(http: Http) -> Self {
|
||||
MusicBrainzClient { http }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
|
||||
fn format_album_date(date: &AlbumDate) -> Option<String> {
|
||||
match date.year {
|
||||
Some(year) => match date.month {
|
||||
Some(month) => match date.day {
|
||||
Some(day) => Some(format!("{year}-{month:02}-{day:02}")),
|
||||
None => Some(format!("{year}-{month:02}")),
|
||||
},
|
||||
None => Some(format!("{year}")),
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lookup_artist(
|
||||
&mut self,
|
||||
request: LookupArtistRequest,
|
||||
) -> Result<LookupArtistResponse, Error> {
|
||||
let mut include: Vec<String> = vec![];
|
||||
|
||||
let mbid: String = request.mbid.uuid().as_hyphenated().to_string();
|
||||
|
||||
if request.release_groups {
|
||||
include.push(String::from("release-groups"));
|
||||
}
|
||||
|
||||
let include: String =
|
||||
form_urlencoded::byte_serialize(include.join("+").as_bytes()).collect();
|
||||
let url = format!("{MB_BASE_URL}/artist/{mbid}?inc={include}");
|
||||
|
||||
let response: DeserializeLookupArtistResponse = self.http.get(&url)?;
|
||||
Ok(response.into())
|
||||
}
|
||||
|
||||
pub fn search_release_group(
|
||||
&mut self,
|
||||
request: SearchReleaseGroupRequest,
|
||||
) -> Result<SearchReleaseGroupResponse, Error> {
|
||||
let mut query: Vec<String> = vec![];
|
||||
|
||||
if let Some(arid) = request.arid {
|
||||
query.push(format!("arid:{}", arid.uuid().as_hyphenated().to_string()));
|
||||
}
|
||||
|
||||
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().to_string()));
|
||||
}
|
||||
|
||||
let query: String =
|
||||
form_urlencoded::byte_serialize(query.join(" AND ").as_bytes()).collect();
|
||||
let url = format!("{MB_BASE_URL}/release-group?query={query}");
|
||||
|
||||
let response: DeserializeSearchReleaseGroupResponse = self.http.get(&url)?;
|
||||
Ok(response.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LookupArtistRequest<'a> {
|
||||
mbid: &'a Mbid,
|
||||
release_groups: bool,
|
||||
}
|
||||
|
||||
impl<'a> LookupArtistRequest<'a> {
|
||||
pub fn new(mbid: &'a Mbid) -> Self {
|
||||
LookupArtistRequest {
|
||||
mbid,
|
||||
release_groups: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn include_release_groups(&mut self) -> &mut Self {
|
||||
self.release_groups = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LookupArtistResponse {
|
||||
pub release_groups: Vec<LookupArtistResponseReleaseGroup>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||
struct DeserializeLookupArtistResponse {
|
||||
release_groups: Vec<DeserializeLookupArtistResponseReleaseGroup>,
|
||||
}
|
||||
|
||||
impl From<DeserializeLookupArtistResponse> for LookupArtistResponse {
|
||||
fn from(value: DeserializeLookupArtistResponse) -> Self {
|
||||
LookupArtistResponse {
|
||||
release_groups: value.release_groups.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LookupArtistResponseReleaseGroup {
|
||||
pub id: Mbid,
|
||||
pub title: String,
|
||||
pub first_release_date: AlbumDate,
|
||||
pub primary_type: AlbumPrimaryType,
|
||||
pub secondary_types: Vec<AlbumSecondaryType>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||
struct DeserializeLookupArtistResponseReleaseGroup {
|
||||
id: SerdeMbid,
|
||||
title: String,
|
||||
first_release_date: SerdeAlbumDate,
|
||||
primary_type: SerdeAlbumPrimaryType,
|
||||
secondary_types: Vec<SerdeAlbumSecondaryType>,
|
||||
}
|
||||
|
||||
impl From<DeserializeLookupArtistResponseReleaseGroup> for LookupArtistResponseReleaseGroup {
|
||||
fn from(value: DeserializeLookupArtistResponseReleaseGroup) -> Self {
|
||||
LookupArtistResponseReleaseGroup {
|
||||
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.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SearchReleaseGroupRequest<'a> {
|
||||
arid: Option<&'a Mbid>,
|
||||
first_release_date: Option<&'a AlbumDate>,
|
||||
release_group: Option<&'a str>,
|
||||
rgid: Option<&'a Mbid>,
|
||||
}
|
||||
|
||||
impl<'a> SearchReleaseGroupRequest<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn arid(&mut self, arid: &'a Mbid) -> &mut Self {
|
||||
self.arid = Some(arid);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn first_release_date(&mut self, first_release_date: &'a AlbumDate) -> &mut Self {
|
||||
self.first_release_date = Some(first_release_date);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn release_group(&mut self, release_group: &'a str) -> &mut Self {
|
||||
self.release_group = Some(release_group);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn rgid(&mut self, rgid: &'a Mbid) -> &mut Self {
|
||||
self.rgid = Some(rgid);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SearchReleaseGroupResponse {
|
||||
pub release_groups: Vec<SearchReleaseGroupResponseReleaseGroup>,
|
||||
}
|
||||
|
||||
#[derive(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(Debug)]
|
||||
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(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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SerdeMbid(Mbid);
|
||||
|
||||
impl From<SerdeMbid> for Mbid {
|
||||
fn from(value: SerdeMbid) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
struct SerdeMbidVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for SerdeMbidVisitor {
|
||||
type Value = SerdeMbid;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a valid MusicBrainz identifier")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(SerdeMbid(
|
||||
v.try_into()
|
||||
.map_err(|e: MbidError| E::custom(e.to_string()))?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SerdeMbid {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(SerdeMbidVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SerdeAlbumDate(AlbumDate);
|
||||
|
||||
impl From<SerdeAlbumDate> for AlbumDate {
|
||||
fn from(value: SerdeAlbumDate) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
struct SerdeAlbumDateVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for SerdeAlbumDateVisitor {
|
||||
type Value = SerdeAlbumDate;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a valid YYYY(-MM-(-DD)) date")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
let mut elems = v.split('-');
|
||||
|
||||
let elem = elems.next();
|
||||
let year = elem
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s.parse()) })
|
||||
.transpose()
|
||||
.map_err(|e: ParseIntError| E::custom(e.to_string()))?;
|
||||
|
||||
let elem = elems.next();
|
||||
let month = elem
|
||||
.map(|s| s.parse())
|
||||
.transpose()
|
||||
.map_err(|e: ParseIntError| E::custom(e.to_string()))?;
|
||||
|
||||
let elem = elems.next();
|
||||
let day = elem
|
||||
.map(|s| s.parse())
|
||||
.transpose()
|
||||
.map_err(|e: ParseIntError| E::custom(e.to_string()))?;
|
||||
|
||||
Ok(SerdeAlbumDate(AlbumDate::new(year, month, day)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SerdeAlbumDate {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(SerdeAlbumDateVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(remote = "AlbumPrimaryType")]
|
||||
pub enum AlbumPrimaryTypeDef {
|
||||
Album,
|
||||
Single,
|
||||
#[serde(rename = "EP")]
|
||||
Ep,
|
||||
Broadcast,
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SerdeAlbumPrimaryType(#[serde(with = "AlbumPrimaryTypeDef")] AlbumPrimaryType);
|
||||
|
||||
impl From<SerdeAlbumPrimaryType> for AlbumPrimaryType {
|
||||
fn from(value: SerdeAlbumPrimaryType) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AlbumPrimaryType> for SerdeAlbumPrimaryType {
|
||||
fn from(value: AlbumPrimaryType) -> Self {
|
||||
SerdeAlbumPrimaryType(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(remote = "AlbumSecondaryType")]
|
||||
pub enum AlbumSecondaryTypeDef {
|
||||
Compilation,
|
||||
Soundtrack,
|
||||
Spokenword,
|
||||
Interview,
|
||||
Audiobook,
|
||||
#[serde(rename = "Audio drama")]
|
||||
AudioDrama,
|
||||
Live,
|
||||
Remix,
|
||||
#[serde(rename = "DJ-mix")]
|
||||
DjMix,
|
||||
#[serde(rename = "Mixtape/Street")]
|
||||
MixtapeStreet,
|
||||
Demo,
|
||||
#[serde(rename = "Field recording")]
|
||||
FieldRecording,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SerdeAlbumSecondaryType(#[serde(with = "AlbumSecondaryTypeDef")] AlbumSecondaryType);
|
||||
|
||||
impl From<SerdeAlbumSecondaryType> for AlbumSecondaryType {
|
||||
fn from(value: SerdeAlbumSecondaryType) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AlbumSecondaryType> for SerdeAlbumSecondaryType {
|
||||
fn from(value: AlbumSecondaryType) -> Self {
|
||||
SerdeAlbumSecondaryType(value)
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use mockall::{predicate, Sequence};
|
||||
|
@ -16,7 +16,7 @@ use musichoard::{
|
||||
executor::{ssh::BeetsLibrarySshExecutor, BeetsLibraryProcessExecutor},
|
||||
BeetsLibrary,
|
||||
},
|
||||
musicbrainz::{http::MusicBrainzHttp, MusicBrainzClient},
|
||||
musicbrainz::{api::MusicBrainzClient, http::MusicBrainzHttp},
|
||||
},
|
||||
interface::{
|
||||
database::{IDatabase, NullDatabase},
|
||||
|
7
src/tui/lib/external/musicbrainz/mod.rs
vendored
7
src/tui/lib/external/musicbrainz/mod.rs
vendored
@ -3,8 +3,11 @@
|
||||
use musichoard::{
|
||||
collection::album::{Album, AlbumDate},
|
||||
external::musicbrainz::{
|
||||
IMusicBrainzHttp, MusicBrainzClient, SearchReleaseGroupRequest,
|
||||
SearchReleaseGroupResponseReleaseGroup,
|
||||
api::{
|
||||
search::{SearchReleaseGroupRequest, SearchReleaseGroupResponseReleaseGroup},
|
||||
MusicBrainzClient,
|
||||
},
|
||||
IMusicBrainzHttp,
|
||||
},
|
||||
interface::musicbrainz::Mbid,
|
||||
};
|
||||
|
@ -27,4 +27,4 @@ impl<T> Match<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub type Error = musichoard::external::musicbrainz::Error;
|
||||
pub type Error = musichoard::external::musicbrainz::api::Error;
|
||||
|
Loading…
Reference in New Issue
Block a user