Decide carefully where external::musicbrainz
belongs
#196
@ -51,7 +51,13 @@ macro_rules! impl_imusicbrainzref {
|
||||
|
||||
impl From<Uuid> for $mbref {
|
||||
fn from(uuid: Uuid) -> Self {
|
||||
$mbref(MusicBrainzRef::from_uuid(uuid, $mbref::entity()))
|
||||
$mbref(MusicBrainzRef::from_mbid(uuid, $mbref::entity()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Mbid> for $mbref {
|
||||
fn from(mbid: Mbid) -> Self {
|
||||
$mbref(MusicBrainzRef::from_mbid(mbid, $mbref::entity()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,9 +104,9 @@ impl MusicBrainzRef {
|
||||
Ok(MusicBrainzRef { mbid, url })
|
||||
}
|
||||
|
||||
fn from_uuid(uuid: Uuid, entity: &'static str) -> Self {
|
||||
let uuid_str = uuid.to_string();
|
||||
let mbid = uuid.into();
|
||||
fn from_mbid<ID: Into<Mbid>>(id: ID, entity: &'static str) -> Self {
|
||||
let mbid = id.into();
|
||||
let uuid_str = mbid.uuid().to_string();
|
||||
let url = Url::parse(&format!("https://{MB_DOMAIN}/{entity}/{uuid_str}")).unwrap();
|
||||
MusicBrainzRef { mbid, url }
|
||||
}
|
||||
|
205
src/external/musicbrainz/mod.rs
vendored
205
src/external/musicbrainz/mod.rs
vendored
@ -2,19 +2,26 @@
|
||||
|
||||
pub mod http;
|
||||
|
||||
use std::fmt;
|
||||
use std::{fmt, num::ParseIntError};
|
||||
|
||||
#[cfg(test)]
|
||||
use mockall::automock;
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use serde::{
|
||||
de::{DeserializeOwned, Visitor},
|
||||
Deserialize, Deserializer,
|
||||
};
|
||||
use url::form_urlencoded;
|
||||
|
||||
use crate::core::{
|
||||
collection::{
|
||||
album::{Album, AlbumPrimaryType, AlbumSecondaryType},
|
||||
musicbrainz::IMusicBrainzRef,
|
||||
use crate::{
|
||||
collection::album::AlbumDate,
|
||||
core::{
|
||||
collection::{
|
||||
album::{Album, AlbumPrimaryType, AlbumSecondaryType},
|
||||
musicbrainz::IMusicBrainzRef,
|
||||
},
|
||||
interface::musicbrainz::Mbid,
|
||||
},
|
||||
interface::musicbrainz::Mbid,
|
||||
interface::musicbrainz::MbidError,
|
||||
};
|
||||
|
||||
#[cfg_attr(test, automock)]
|
||||
@ -74,15 +81,6 @@ impl<Http> MusicBrainzClient<Http> {
|
||||
}
|
||||
|
||||
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
|
||||
pub fn lookup_artist_release_groups(
|
||||
&mut self,
|
||||
mbid: &Mbid,
|
||||
) -> Result<ResponseLookupArtist, Error> {
|
||||
let mbid = mbid.uuid().as_hyphenated().to_string();
|
||||
let url = format!("{MB_BASE_URL}/artist/{mbid}?inc=release-groups");
|
||||
Ok(self.http.get(&url)?)
|
||||
}
|
||||
|
||||
pub fn search_release_group(
|
||||
&mut self,
|
||||
arid: &Mbid,
|
||||
@ -112,42 +110,100 @@ impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||
pub struct ResponseLookupArtist {
|
||||
pub release_groups: Vec<LookupReleaseGroup>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||
pub struct LookupReleaseGroup {
|
||||
pub id: String, // TODO: Change to MBID
|
||||
pub title: String,
|
||||
pub first_release_date: String, // TODO: Change to AlbumDate
|
||||
pub primary_type: SerdeAlbumPrimaryType, // TODO: Change to AlbumPrimaryType
|
||||
pub secondary_types: Vec<SerdeAlbumSecondaryType>, // TODO: Change to Vec<AlbumSecondaryType>
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||
pub struct ResponseSearchReleaseGroup {
|
||||
pub release_groups: Vec<SearchReleaseGroup>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all(deserialize = "kebab-case"))]
|
||||
pub struct SearchReleaseGroup {
|
||||
pub score: u8,
|
||||
pub id: String, // TODO: Change to MBID
|
||||
pub id: Mbid,
|
||||
pub title: String,
|
||||
pub first_release_date: String, // TODO: Change to AlbumDate
|
||||
pub primary_type: SerdeAlbumPrimaryType, // TODO: Change to AlbumDate
|
||||
pub secondary_types: Option<Vec<SerdeAlbumSecondaryType>>, // TODO: Change to Vec<AlbumSecondaryType>
|
||||
pub first_release_date: AlbumDate,
|
||||
#[serde(with = "AlbumPrimaryTypeDef")]
|
||||
pub primary_type: AlbumPrimaryType,
|
||||
pub secondary_types: Option<Vec<AlbumSecondaryType>>,
|
||||
}
|
||||
|
||||
struct MbidVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for MbidVisitor {
|
||||
type Value = Mbid;
|
||||
|
||||
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(v.try_into()
|
||||
.map_err(|e: MbidError| E::custom(e.to_string()))?)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Mbid {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(MbidVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct AlbumDateVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for AlbumDateVisitor {
|
||||
type Value = AlbumDate;
|
||||
|
||||
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(AlbumDate::new(year, month, day))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AlbumDate {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(AlbumDateVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(remote = "AlbumPrimaryType")]
|
||||
pub enum SerdeAlbumPrimaryTypeDef {
|
||||
pub enum AlbumPrimaryTypeDef {
|
||||
Album,
|
||||
Single,
|
||||
#[serde(rename = "EP")]
|
||||
@ -156,44 +212,47 @@ pub enum SerdeAlbumPrimaryTypeDef {
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct SerdeAlbumPrimaryType(#[serde(with = "SerdeAlbumPrimaryTypeDef")] AlbumPrimaryType);
|
||||
// AlbumSecondaryType is implemented manually because deserializing to a remote type is not (yet)
|
||||
// supported for Option/Vec/Map by serde: https://github.com/serde-rs/serde/issues/723.
|
||||
struct AlbumSecondaryTypeVisitor;
|
||||
|
||||
impl From<SerdeAlbumPrimaryType> for AlbumPrimaryType {
|
||||
fn from(value: SerdeAlbumPrimaryType) -> Self {
|
||||
value.0
|
||||
impl<'de> Visitor<'de> for AlbumSecondaryTypeVisitor {
|
||||
type Value = AlbumSecondaryType;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a valid MusicBrainz album secondary type")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
let variant = match v {
|
||||
"Compilation" => AlbumSecondaryType::Compilation,
|
||||
"Soundtrack" => AlbumSecondaryType::Soundtrack,
|
||||
"Spokenword" => AlbumSecondaryType::Spokenword,
|
||||
"Interview" => AlbumSecondaryType::Interview,
|
||||
"Audiobook" => AlbumSecondaryType::Audiobook,
|
||||
"Audio drama" => AlbumSecondaryType::AudioDrama,
|
||||
"Live" => AlbumSecondaryType::Live,
|
||||
"Remix" => AlbumSecondaryType::Remix,
|
||||
"DJ-mix" => AlbumSecondaryType::DjMix,
|
||||
"Mixtape/Street" => AlbumSecondaryType::MixtapeStreet,
|
||||
"Demo" => AlbumSecondaryType::Demo,
|
||||
"Field recording" => AlbumSecondaryType::FieldRecording,
|
||||
_ => return Err(E::custom(format!("unknown album secondary type: {v}"))),
|
||||
};
|
||||
|
||||
Ok(variant)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(remote = "AlbumSecondaryType")]
|
||||
pub enum SerdeAlbumSecondaryTypeDef {
|
||||
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(Clone, Debug, Deserialize)]
|
||||
pub struct SerdeAlbumSecondaryType(
|
||||
#[serde(with = "SerdeAlbumSecondaryTypeDef")] AlbumSecondaryType,
|
||||
);
|
||||
|
||||
impl From<SerdeAlbumSecondaryType> for AlbumSecondaryType {
|
||||
fn from(value: SerdeAlbumSecondaryType) -> Self {
|
||||
value.0
|
||||
impl<'de> Deserialize<'de> for AlbumSecondaryType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(AlbumSecondaryTypeVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
|
63
src/tui/lib/external/musicbrainz/mod.rs
vendored
63
src/tui/lib/external/musicbrainz/mod.rs
vendored
@ -1,12 +1,7 @@
|
||||
//! Module for interacting with the [MusicBrainz API](https://musicbrainz.org/doc/MusicBrainz_API).
|
||||
|
||||
use std::num;
|
||||
|
||||
use musichoard::{
|
||||
collection::{
|
||||
album::{Album, AlbumDate},
|
||||
musicbrainz::MbAlbumRef,
|
||||
},
|
||||
collection::album::Album,
|
||||
external::musicbrainz::{IMusicBrainzHttp, MusicBrainzClient, SearchReleaseGroup},
|
||||
interface::musicbrainz::Mbid,
|
||||
};
|
||||
@ -29,62 +24,24 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
|
||||
arid: &Mbid,
|
||||
album: &Album,
|
||||
) -> Result<Vec<Match<Album>>, Error> {
|
||||
self.client
|
||||
.search_release_group(arid, album)?
|
||||
let mb_response = self.client.search_release_group(arid, album)?;
|
||||
Ok(mb_response
|
||||
.release_groups
|
||||
.into_iter()
|
||||
.map(from_search_release_group)
|
||||
.collect()
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<musichoard::external::musicbrainz::Error> for Error {
|
||||
fn from(value: musichoard::external::musicbrainz::Error) -> Self {
|
||||
match value {
|
||||
musichoard::external::musicbrainz::Error::Http(s) => Error::Client(s),
|
||||
musichoard::external::musicbrainz::Error::RateLimit => Error::RateLimit,
|
||||
musichoard::external::musicbrainz::Error::Unknown(u) => Error::Unknown(u),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn from_search_release_group(entity: SearchReleaseGroup) -> Result<Match<Album>, Error> {
|
||||
fn from_search_release_group(entity: SearchReleaseGroup) -> Match<Album> {
|
||||
let mut album = Album::new(
|
||||
entity.title,
|
||||
from_mb_date(&entity.first_release_date)?,
|
||||
Some(entity.primary_type.into()),
|
||||
entity
|
||||
.secondary_types
|
||||
.map(|v| v.into_iter().map(|st| st.into()).collect())
|
||||
.unwrap_or_default(),
|
||||
entity.first_release_date,
|
||||
Some(entity.primary_type),
|
||||
entity.secondary_types.unwrap_or_default(),
|
||||
);
|
||||
let mbref =
|
||||
MbAlbumRef::from_uuid_str(entity.id).map_err(|err| Error::MbidParse(err.to_string()))?;
|
||||
album.set_musicbrainz_ref(mbref);
|
||||
Ok(Match::new(entity.score, album))
|
||||
}
|
||||
|
||||
impl From<num::ParseIntError> for Error {
|
||||
fn from(err: num::ParseIntError) -> Error {
|
||||
Error::Parse(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn from_mb_date(mb_date: &str) -> Result<AlbumDate, Error> {
|
||||
let mut elems = mb_date.split('-');
|
||||
|
||||
let elem = elems.next();
|
||||
let year = elem
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(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(AlbumDate::new(year, month, day))
|
||||
album.set_musicbrainz_ref(entity.id.into());
|
||||
Match::new(entity.score, album)
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
|
@ -1,7 +1,5 @@
|
||||
//! Module for accessing MusicBrainz metadata.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
#[cfg(test)]
|
||||
use mockall::automock;
|
||||
|
||||
@ -29,28 +27,4 @@ impl<T> Match<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
/// Failed to parse input into an MBID.
|
||||
MbidParse(String),
|
||||
/// The API client failed.
|
||||
Client(String),
|
||||
/// The client reached the API rate limit.
|
||||
RateLimit,
|
||||
/// The API response could not be understood.
|
||||
Unknown(u16),
|
||||
/// Part of the response could not be parsed.
|
||||
Parse(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Error::MbidParse(s) => write!(f, "failed to parse input into an MBID: {s}"),
|
||||
Error::Client(s) => write!(f, "the API client failed: {s}"),
|
||||
Error::RateLimit => write!(f, "the API client reached the rate limit"),
|
||||
Error::Unknown(u) => write!(f, "the API response could not be understood: status {u}"),
|
||||
Error::Parse(s) => write!(f, "part of the response could not be parsed: {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub type Error = musichoard::external::musicbrainz::Error;
|
||||
|
Loading…
Reference in New Issue
Block a user