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>(url: S) -> Result { Self::artist_from_url(url.as_ref().try_into()?) } pub fn album_from_str>(url: S) -> Result { Self::album_from_url(url.as_ref().try_into()?) } pub fn artist_from_url(url: Url) -> Result { Self::new(url, "artist") } pub fn album_from_url(url: Url) -> Result { Self::new(url, "release-group") } fn new(url: Url, mb_type: &str) -> Result { 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(url: U, mb_type: &str) -> Error { Error::UrlError(format!("invalid {mb_type} MusicBrainz URL: {url}")) } } impl AsRef 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()); } }