Decide carefully where external::musicbrainz belongs #196

Merged
wojtek merged 11 commits from 193---decide-carefully-where-external--musicbrainz-belongs into main 2024-08-28 18:21:13 +02:00
9 changed files with 487 additions and 447 deletions
Showing only changes of commit 041a91de4c - Show all commits

View File

@ -1,7 +1,10 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use musichoard::{ use musichoard::{
external::musicbrainz::{http::MusicBrainzHttp, LookupArtistRequest, MusicBrainzClient}, external::musicbrainz::{
api::{lookup::LookupArtistRequest, MusicBrainzClient},
http::MusicBrainzHttp,
},
interface::musicbrainz::Mbid, interface::musicbrainz::Mbid,
}; };
use structopt::StructOpt; use structopt::StructOpt;

View File

@ -4,7 +4,9 @@ use std::{num::ParseIntError, str::FromStr};
use musichoard::{ use musichoard::{
collection::album::AlbumDate, collection::album::AlbumDate,
external::musicbrainz::{http::MusicBrainzHttp, MusicBrainzClient, SearchReleaseGroupRequest}, external::musicbrainz::{
api::search::SearchReleaseGroupRequest, api::MusicBrainzClient, http::MusicBrainzHttp,
},
interface::musicbrainz::Mbid, interface::musicbrainz::Mbid,
}; };
use structopt::StructOpt; use structopt::StructOpt;

105
src/external/musicbrainz/api/lookup.rs vendored Normal file
View 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
View 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
View 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()),
}
}
}

View File

@ -1,25 +1,11 @@
//! 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).
pub mod api;
pub mod http; pub mod http;
use std::{fmt, num::ParseIntError};
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
use serde::{ use serde::de::DeserializeOwned;
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,
};
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait IMusicBrainzHttp { pub trait IMusicBrainzHttp {
@ -32,431 +18,6 @@ pub enum HttpError {
Status(u16), 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)] // #[cfg(test)]
// mod tests { // mod tests {
// use mockall::{predicate, Sequence}; // use mockall::{predicate, Sequence};

View File

@ -16,7 +16,7 @@ use musichoard::{
executor::{ssh::BeetsLibrarySshExecutor, BeetsLibraryProcessExecutor}, executor::{ssh::BeetsLibrarySshExecutor, BeetsLibraryProcessExecutor},
BeetsLibrary, BeetsLibrary,
}, },
musicbrainz::{http::MusicBrainzHttp, MusicBrainzClient}, musicbrainz::{api::MusicBrainzClient, http::MusicBrainzHttp},
}, },
interface::{ interface::{
database::{IDatabase, NullDatabase}, database::{IDatabase, NullDatabase},

View File

@ -3,8 +3,11 @@
use musichoard::{ use musichoard::{
collection::album::{Album, AlbumDate}, collection::album::{Album, AlbumDate},
external::musicbrainz::{ external::musicbrainz::{
IMusicBrainzHttp, MusicBrainzClient, SearchReleaseGroupRequest, api::{
SearchReleaseGroupResponseReleaseGroup, search::{SearchReleaseGroupRequest, SearchReleaseGroupResponseReleaseGroup},
MusicBrainzClient,
},
IMusicBrainzHttp,
}, },
interface::musicbrainz::Mbid, interface::musicbrainz::Mbid,
}; };

View File

@ -27,4 +27,4 @@ impl<T> Match<T> {
} }
} }
pub type Error = musichoard::external::musicbrainz::Error; pub type Error = musichoard::external::musicbrainz::api::Error;