musichoard/src/core/collection/musicbrainz.rs

166 lines
5.8 KiB
Rust
Raw Normal View History

use std::fmt::{Debug, Display};
use url::Url;
use uuid::Uuid;
use crate::core::collection::Error;
/// MusicBrainz reference.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct MusicBrainzUrl(Url);
impl MusicBrainzUrl {
pub fn mbid(&self) -> &str {
// The URL is assumed to have been validated.
self.0.path_segments().and_then(|mut ps| ps.nth(1)).unwrap()
}
pub fn artist_from_str<S: AsRef<str>>(url: S) -> Result<Self, Error> {
Self::artist_from_url(url.as_ref().try_into()?)
}
pub fn album_from_str<S: AsRef<str>>(url: S) -> Result<Self, Error> {
Self::album_from_url(url.as_ref().try_into()?)
}
pub fn artist_from_url(url: Url) -> Result<Self, Error> {
Self::new(url, "artist")
}
pub fn album_from_url(url: Url) -> Result<Self, Error> {
Self::new(url, "release-group")
}
fn new(url: Url, mb_type: &str) -> Result<Self, Error> {
if !url
.domain()
.map(|u| u.ends_with("musicbrainz.org"))
.unwrap_or(false)
{
return Err(Self::invalid_url_error(url, mb_type));
}
// 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() != mb_type {
return Err(Self::invalid_url_error(url, mb_type));
}
match url.path_segments().and_then(|mut ps| ps.nth(1)) {
Some(segment) => Uuid::try_parse(segment)?,
None => return Err(Self::invalid_url_error(url, mb_type)),
};
Ok(MusicBrainzUrl(url))
}
fn invalid_url_error<U: Display>(url: U, mb_type: &str) -> Error {
Error::UrlError(format!("invalid {mb_type} MusicBrainz URL: {url}"))
}
}
impl AsRef<str> for MusicBrainzUrl {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
#[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 = MusicBrainzUrl::artist_from_str(&url_str).unwrap();
assert_eq!(url_str, mb.as_ref());
assert_eq!(uuid, mb.mbid());
let url: Url = url_str.as_str().try_into().unwrap();
let mb = MusicBrainzUrl::artist_from_url(url).unwrap();
assert_eq!(url_str, mb.as_ref());
assert_eq!(uuid, mb.mbid());
}
#[test]
fn album() {
let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8";
let url_str = format!("https://musicbrainz.org/release-group/{uuid}");
let mb = MusicBrainzUrl::album_from_str(&url_str).unwrap();
assert_eq!(url_str, mb.as_ref());
assert_eq!(uuid, mb.mbid());
let url: Url = url_str.as_str().try_into().unwrap();
let mb = MusicBrainzUrl::album_from_url(url).unwrap();
assert_eq!(url_str, mb.as_ref());
assert_eq!(uuid, mb.mbid());
}
#[test]
fn not_a_url() {
let url = "not a url at all";
let expected_error: Error = url::ParseError::RelativeUrlWithoutBase.into();
let actual_error = MusicBrainzUrl::artist_from_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 = MusicBrainzUrl::artist_from_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 = MusicBrainzUrl::artist_from_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 = MusicBrainzUrl::album_from_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 = MusicBrainzUrl::artist_from_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 = MusicBrainzUrl::artist_from_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 = MusicBrainzUrl::artist_from_str(url).unwrap_err();
assert_eq!(actual_error, expected_error);
assert_eq!(actual_error.to_string(), expected_error.to_string());
}
}