290 lines
9.1 KiB
Rust
290 lines
9.1 KiB
Rust
use std::{fmt, mem};
|
|
|
|
use url::Url;
|
|
use uuid::Uuid;
|
|
|
|
use crate::core::collection::Error;
|
|
|
|
const MB_DOMAIN: &str = "musicbrainz.org";
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct Mbid(Uuid);
|
|
|
|
impl Mbid {
|
|
pub fn uuid(&self) -> &Uuid {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl From<Uuid> for Mbid {
|
|
fn from(value: Uuid) -> Self {
|
|
Mbid(value)
|
|
}
|
|
}
|
|
|
|
macro_rules! try_from_impl_for_mbid {
|
|
($from:ty) => {
|
|
impl TryFrom<$from> for Mbid {
|
|
type Error = Error;
|
|
|
|
fn try_from(value: $from) -> Result<Self, Self::Error> {
|
|
Ok(Uuid::parse_str(value.as_ref())?.into())
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
try_from_impl_for_mbid!(&str);
|
|
try_from_impl_for_mbid!(&String);
|
|
try_from_impl_for_mbid!(String);
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum MbRefOption<T> {
|
|
Some(T),
|
|
CannotHaveMbid,
|
|
None,
|
|
}
|
|
|
|
impl<T> MbRefOption<T> {
|
|
pub fn or(self, optb: MbRefOption<T>) -> MbRefOption<T> {
|
|
match (&self, &optb) {
|
|
(MbRefOption::Some(_), _) | (MbRefOption::CannotHaveMbid, MbRefOption::None) => self,
|
|
_ => optb,
|
|
}
|
|
}
|
|
|
|
pub fn replace(&mut self, value: T) -> MbRefOption<T> {
|
|
mem::replace(self, MbRefOption::Some(value))
|
|
}
|
|
|
|
pub fn take(&mut self) -> MbRefOption<T> {
|
|
mem::replace(self, MbRefOption::None)
|
|
}
|
|
}
|
|
|
|
#[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_mbid(uuid, $mbref::entity()))
|
|
}
|
|
}
|
|
|
|
impl From<Mbid> for $mbref {
|
|
fn from(mbid: Mbid) -> Self {
|
|
$mbref(MusicBrainzRef::from_mbid(mbid, $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_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 }
|
|
}
|
|
|
|
fn invalid_url_error<U: fmt::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 mbid: Mbid = TryInto::<Uuid>::try_into(uuid).unwrap().into();
|
|
let mb: MbArtistRef = mbid.into();
|
|
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 mbid: Mbid = TryInto::<Uuid>::try_into(uuid).unwrap().into();
|
|
let mb: MbAlbumRef = mbid.into();
|
|
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());
|
|
}
|
|
}
|