musichoard/src/core/collection/musicbrainz.rs

219 lines
7.3 KiB
Rust
Raw Normal View History

use std::fmt::{Debug, Display};
use url::Url;
use uuid::Uuid;
use crate::{core::collection::Error, interface::musicbrainz::Mbid};
const MB_DOMAIN: &str = "musicbrainz.org";
#[derive(Clone, Debug, PartialEq, Eq)]
struct MusicBrainzRef {
mbid: Mbid,
url: Url,
}
pub trait IMusicBrainzRef {
fn mbid(&self) -> &Mbid;
fn url(&self) -> &Url;
fn entity() -> &'static str;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MbArtistRef(MusicBrainzRef);
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MbAlbumRef(MusicBrainzRef);
macro_rules! impl_imusicbrainzref {
($mbref:ident, $entity:literal) => {
impl IMusicBrainzRef for $mbref {
fn mbid(&self) -> &Mbid {
&self.0.mbid
}
fn url(&self) -> &Url {
&self.0.url
}
fn entity() -> &'static str {
$entity
}
}
impl TryFrom<Url> for $mbref {
type Error = Error;
fn try_from(url: Url) -> Result<Self, Self::Error> {
Ok($mbref(MusicBrainzRef::from_url(url, $mbref::entity())?))
}
}
impl From<Uuid> for $mbref {
fn from(uuid: Uuid) -> Self {
$mbref(MusicBrainzRef::from_uuid(uuid, $mbref::entity()))
}
}
impl $mbref {
pub fn from_url_str<S: AsRef<str>>(url: S) -> Result<Self, Error> {
let url: Url = url.as_ref().try_into()?;
url.try_into()
}
}
impl $mbref {
pub fn from_uuid_str<S: AsRef<str>>(uuid: S) -> Result<Self, Error> {
let uuid: Uuid = uuid.as_ref().try_into()?;
Ok(uuid.into())
}
}
};
}
impl_imusicbrainzref!(MbArtistRef, "artist");
impl_imusicbrainzref!(MbAlbumRef, "release-group");
impl MusicBrainzRef {
fn from_url(url: Url, entity: &'static str) -> Result<Self, Error> {
if !url
.domain()
.map(|u| u.ends_with(MB_DOMAIN))
.unwrap_or(false)
{
return Err(Self::invalid_url_error(url, entity));
}
// path_segments only returns an empty iterator if the URL cannot-be-a-base. However, if the
// URL cannot-be-a-base then it will fail the check above already as it won't have a domain.
if url.path_segments().and_then(|mut ps| ps.nth(0)).unwrap() != entity {
return Err(Self::invalid_url_error(url, entity));
}
let mbid = match url.path_segments().and_then(|mut ps| ps.nth(1)) {
Some(segment) => Uuid::try_parse(segment)?.into(),
None => return Err(Self::invalid_url_error(url, entity)),
};
Ok(MusicBrainzRef { mbid, url })
}
fn from_uuid(uuid: Uuid, entity: &'static str) -> Self {
let uuid_str = uuid.to_string();
let mbid = uuid.into();
let url = Url::parse(&format!("https://{MB_DOMAIN}/{entity}/{uuid_str}")).unwrap();
MusicBrainzRef { mbid, url }
}
fn invalid_url_error<U: Display>(url: U, entity: &'static str) -> Error {
Error::UrlError(format!("invalid {entity} MusicBrainz URL: {url}"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn artist() {
let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8";
let url_str = format!("https://musicbrainz.org/artist/{uuid}");
let mb = MbArtistRef::from_url_str(&url_str).unwrap();
assert_eq!(url_str, mb.url().as_ref());
assert_eq!(uuid, mb.mbid().uuid().to_string());
let mb = MbArtistRef::from_uuid_str(uuid).unwrap();
assert_eq!(url_str, mb.url().as_ref());
assert_eq!(uuid, mb.mbid().uuid().to_string());
let url: Url = url_str.as_str().try_into().unwrap();
let mb: MbArtistRef = url.try_into().unwrap();
assert_eq!(url_str, mb.url().as_ref());
assert_eq!(uuid, mb.mbid().uuid().to_string());
}
#[test]
fn album() {
let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8";
let url_str = format!("https://musicbrainz.org/release-group/{uuid}");
let mb = MbAlbumRef::from_url_str(&url_str).unwrap();
assert_eq!(url_str, mb.url().as_ref());
assert_eq!(uuid, mb.mbid().uuid().to_string());
let mb = MbAlbumRef::from_uuid_str(uuid).unwrap();
assert_eq!(url_str, mb.url().as_ref());
assert_eq!(uuid, mb.mbid().uuid().to_string());
let url: Url = url_str.as_str().try_into().unwrap();
let mb: MbAlbumRef = url.try_into().unwrap();
assert_eq!(url_str, mb.url().as_ref());
assert_eq!(uuid, mb.mbid().uuid().to_string());
}
#[test]
fn not_a_url() {
let url = "not a url at all";
let expected_error: Error = url::ParseError::RelativeUrlWithoutBase.into();
let actual_error = MbArtistRef::from_url_str(url).unwrap_err();
assert_eq!(actual_error, expected_error);
assert_eq!(actual_error.to_string(), expected_error.to_string());
}
#[test]
fn invalid_url() {
let url = "https://www.musicbutler.io/artist-page/483340948";
let expected_error = Error::UrlError(format!("invalid artist MusicBrainz URL: {url}"));
let actual_error = MbArtistRef::from_url_str(url).unwrap_err();
assert_eq!(actual_error, expected_error);
assert_eq!(actual_error.to_string(), expected_error.to_string());
}
#[test]
fn artist_invalid_type() {
let url = "https://musicbrainz.org/release-group/i-am-not-a-uuid";
let expected_error = Error::UrlError(format!("invalid artist MusicBrainz URL: {url}"));
let actual_error = MbArtistRef::from_url_str(url).unwrap_err();
assert_eq!(actual_error, expected_error);
assert_eq!(actual_error.to_string(), expected_error.to_string());
}
#[test]
fn album_invalid_type() {
let url = "https://musicbrainz.org/artist/i-am-not-a-uuid";
let expected_error =
Error::UrlError(format!("invalid release-group MusicBrainz URL: {url}"));
let actual_error = MbAlbumRef::from_url_str(url).unwrap_err();
assert_eq!(actual_error, expected_error);
assert_eq!(actual_error.to_string(), expected_error.to_string());
}
#[test]
fn invalid_uuid() {
let url = "https://musicbrainz.org/artist/i-am-not-a-uuid";
let expected_error: Error = Uuid::try_parse("i-am-not-a-uuid").unwrap_err().into();
let actual_error = MbArtistRef::from_url_str(url).unwrap_err();
assert_eq!(actual_error, expected_error);
assert_eq!(actual_error.to_string(), expected_error.to_string());
}
#[test]
fn missing_type() {
let url = "https://musicbrainz.org";
let expected_error = Error::UrlError(format!("invalid artist MusicBrainz URL: {url}/"));
let actual_error = MbArtistRef::from_url_str(url).unwrap_err();
assert_eq!(actual_error, expected_error);
assert_eq!(actual_error.to_string(), expected_error.to_string());
}
#[test]
fn missing_uuid() {
let url = "https://musicbrainz.org/artist";
let expected_error = Error::UrlError(format!("invalid artist MusicBrainz URL: {url}"));
let actual_error = MbArtistRef::from_url_str(url).unwrap_err();
assert_eq!(actual_error, expected_error);
assert_eq!(actual_error.to_string(), expected_error.to_string());
}
}