Connect release groups to musicbrainz id #157
@ -2,16 +2,12 @@ use std::{
|
||||
collections::HashMap,
|
||||
fmt::{self, Debug, Display},
|
||||
mem,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::core::collection::{
|
||||
album::Album,
|
||||
merge::{Merge, MergeCollections, WithId},
|
||||
Error,
|
||||
musicbrainz::MusicBrainz,
|
||||
};
|
||||
|
||||
/// An artist.
|
||||
@ -164,92 +160,6 @@ impl Display for ArtistId {
|
||||
}
|
||||
}
|
||||
|
||||
/// An object with the [`IMbid`] trait contains a [MusicBrainz
|
||||
/// Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID).
|
||||
pub trait IMbid {
|
||||
fn mbid(&self) -> &str;
|
||||
}
|
||||
|
||||
/// MusicBrainz reference.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct MusicBrainz(Url);
|
||||
|
||||
impl MusicBrainz {
|
||||
/// Validate and wrap a MusicBrainz URL.
|
||||
pub fn new_from_str<S: AsRef<str>>(url: S) -> Result<Self, Error> {
|
||||
let url = Url::parse(url.as_ref())?;
|
||||
Self::new_from_url(url)
|
||||
}
|
||||
|
||||
/// Validate and wrap a MusicBrainz URL.
|
||||
pub fn new_from_url(url: Url) -> Result<Self, Error> {
|
||||
if !url
|
||||
.domain()
|
||||
.map(|u| u.ends_with("musicbrainz.org"))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(Self::invalid_url_error(url));
|
||||
}
|
||||
|
||||
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)),
|
||||
};
|
||||
|
||||
Ok(MusicBrainz(url))
|
||||
}
|
||||
|
||||
fn invalid_url_error<U: Display>(url: U) -> Error {
|
||||
Error::UrlError(format!("invalid MusicBrainz URL: {url}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for MusicBrainz {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MusicBrainz {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
MusicBrainz::new_from_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
// A blanket TryFrom would be better, but https://stackoverflow.com/a/64407892
|
||||
macro_rules! impl_try_from_for_musicbrainz {
|
||||
($from:ty) => {
|
||||
impl TryFrom<$from> for MusicBrainz {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: $from) -> Result<Self, Self::Error> {
|
||||
MusicBrainz::new_from_str(value)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_try_from_for_musicbrainz!(&str);
|
||||
impl_try_from_for_musicbrainz!(&String);
|
||||
impl_try_from_for_musicbrainz!(String);
|
||||
|
||||
impl TryFrom<Url> for MusicBrainz {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: Url) -> Result<Self, Self::Error> {
|
||||
MusicBrainz::new_from_url(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl IMbid for MusicBrainz {
|
||||
fn mbid(&self) -> &str {
|
||||
// The URL is assumed to have been validated.
|
||||
self.0.path_segments().and_then(|mut ps| ps.nth(1)).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::core::testmod::FULL_COLLECTION;
|
||||
@ -263,40 +173,6 @@ mod tests {
|
||||
static MUSICBUTLER: &str = "https://www.musicbutler.io/artist-page/483340948";
|
||||
static MUSICBUTLER_2: &str = "https://www.musicbutler.io/artist-page/658903042/";
|
||||
|
||||
#[test]
|
||||
fn musicbrainz() {
|
||||
let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8";
|
||||
let url_str = format!("https://musicbrainz.org/artist/{uuid}");
|
||||
let url: Url = url_str.as_str().try_into().unwrap();
|
||||
let mb: MusicBrainz = url.try_into().unwrap();
|
||||
assert_eq!(url_str, mb.as_ref());
|
||||
assert_eq!(uuid, mb.mbid());
|
||||
|
||||
let url = "not a url at all".to_string();
|
||||
let expected_error: Error = url::ParseError::RelativeUrlWithoutBase.into();
|
||||
let actual_error = MusicBrainz::from_str(&url).unwrap_err();
|
||||
assert_eq!(actual_error, expected_error);
|
||||
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
||||
|
||||
let url = "https://musicbrainz.org/artist/i-am-not-a-uuid".to_string();
|
||||
let expected_error: Error = Uuid::try_parse("i-am-not-a-uuid").unwrap_err().into();
|
||||
let actual_error = MusicBrainz::from_str(&url).unwrap_err();
|
||||
assert_eq!(actual_error, expected_error);
|
||||
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
||||
|
||||
let url = "https://musicbrainz.org/artist".to_string();
|
||||
let expected_error = Error::UrlError(format!("invalid MusicBrainz URL: {url}"));
|
||||
let actual_error = MusicBrainz::from_str(&url).unwrap_err();
|
||||
assert_eq!(actual_error, expected_error);
|
||||
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn urls() {
|
||||
assert!(MusicBrainz::from_str(MUSICBRAINZ).is_ok());
|
||||
assert!(MusicBrainz::from_str(MUSICBUTLER).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artist_sort_set_clear() {
|
||||
let artist_id = ArtistId::new("an artist");
|
||||
@ -336,15 +212,6 @@ mod tests {
|
||||
assert!(artist < Artist::new(sort_id_2.clone()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn musicbrainz_url() {
|
||||
let result: Result<MusicBrainz, Error> = MUSICBUTLER.try_into();
|
||||
assert!(result.is_err());
|
||||
|
||||
let result: Result<MusicBrainz, Error> = MUSICBRAINZ.try_into();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_clear_musicbrainz_url() {
|
||||
let mut artist = Artist::new(ArtistId::new("an artist"));
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
pub mod album;
|
||||
pub mod artist;
|
||||
pub mod musicbrainz;
|
||||
pub mod track;
|
||||
|
||||
mod merge;
|
||||
|
147
src/core/collection/musicbrainz.rs
Normal file
147
src/core/collection/musicbrainz.rs
Normal file
@ -0,0 +1,147 @@
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::core::collection::Error;
|
||||
|
||||
/// An object with the [`IMbid`] trait contains a [MusicBrainz
|
||||
/// Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID).
|
||||
pub trait IMbid {
|
||||
fn mbid(&self) -> &str;
|
||||
}
|
||||
|
||||
/// MusicBrainz reference.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct MusicBrainz(Url);
|
||||
|
||||
impl MusicBrainz {
|
||||
/// Validate and wrap a MusicBrainz URL.
|
||||
pub fn new_from_str<S: AsRef<str>>(url: S) -> Result<Self, Error> {
|
||||
let url = Url::parse(url.as_ref())?;
|
||||
Self::new_from_url(url)
|
||||
}
|
||||
|
||||
/// Validate and wrap a MusicBrainz URL.
|
||||
pub fn new_from_url(url: Url) -> Result<Self, Error> {
|
||||
if !url
|
||||
.domain()
|
||||
.map(|u| u.ends_with("musicbrainz.org"))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(Self::invalid_url_error(url));
|
||||
}
|
||||
|
||||
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)),
|
||||
};
|
||||
|
||||
Ok(MusicBrainz(url))
|
||||
}
|
||||
|
||||
fn invalid_url_error<U: Display>(url: U) -> Error {
|
||||
Error::UrlError(format!("invalid MusicBrainz URL: {url}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for MusicBrainz {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MusicBrainz {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
MusicBrainz::new_from_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
// A blanket TryFrom would be better, but https://stackoverflow.com/a/64407892
|
||||
macro_rules! impl_try_from_for_musicbrainz {
|
||||
($from:ty) => {
|
||||
impl TryFrom<$from> for MusicBrainz {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: $from) -> Result<Self, Self::Error> {
|
||||
MusicBrainz::new_from_str(value)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_try_from_for_musicbrainz!(&str);
|
||||
impl_try_from_for_musicbrainz!(&String);
|
||||
impl_try_from_for_musicbrainz!(String);
|
||||
|
||||
impl TryFrom<Url> for MusicBrainz {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: Url) -> Result<Self, Self::Error> {
|
||||
MusicBrainz::new_from_url(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl IMbid for MusicBrainz {
|
||||
fn mbid(&self) -> &str {
|
||||
// The URL is assumed to have been validated.
|
||||
self.0.path_segments().and_then(|mut ps| ps.nth(1)).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
static MUSICBRAINZ: &str =
|
||||
"https://musicbrainz.org/artist/d368baa8-21ca-4759-9731-0b2753071ad8";
|
||||
static MUSICBUTLER: &str = "https://www.musicbutler.io/artist-page/483340948";
|
||||
|
||||
#[test]
|
||||
fn musicbrainz() {
|
||||
let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8";
|
||||
let url_str = format!("https://musicbrainz.org/artist/{uuid}");
|
||||
let url: Url = url_str.as_str().try_into().unwrap();
|
||||
let mb: MusicBrainz = url.try_into().unwrap();
|
||||
assert_eq!(url_str, mb.as_ref());
|
||||
assert_eq!(uuid, mb.mbid());
|
||||
|
||||
let url = "not a url at all".to_string();
|
||||
let expected_error: Error = url::ParseError::RelativeUrlWithoutBase.into();
|
||||
let actual_error = MusicBrainz::from_str(&url).unwrap_err();
|
||||
assert_eq!(actual_error, expected_error);
|
||||
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
||||
|
||||
let url = "https://musicbrainz.org/artist/i-am-not-a-uuid".to_string();
|
||||
let expected_error: Error = Uuid::try_parse("i-am-not-a-uuid").unwrap_err().into();
|
||||
let actual_error = MusicBrainz::from_str(&url).unwrap_err();
|
||||
assert_eq!(actual_error, expected_error);
|
||||
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
||||
|
||||
let url = "https://musicbrainz.org/artist".to_string();
|
||||
let expected_error = Error::UrlError(format!("invalid MusicBrainz URL: {url}"));
|
||||
let actual_error = MusicBrainz::from_str(&url).unwrap_err();
|
||||
assert_eq!(actual_error, expected_error);
|
||||
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert!(MusicBrainz::from_str(MUSICBRAINZ).is_ok());
|
||||
assert!(MusicBrainz::from_str(MUSICBUTLER).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_into() {
|
||||
let result: Result<MusicBrainz, Error> = MUSICBUTLER.try_into();
|
||||
assert!(result.is_err());
|
||||
|
||||
let result: Result<MusicBrainz, Error> = MUSICBRAINZ.try_into();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
@ -3,7 +3,8 @@ use std::collections::HashMap;
|
||||
use crate::core::{
|
||||
collection::{
|
||||
album::{Album, AlbumDate, AlbumId, AlbumSeq},
|
||||
artist::{Artist, ArtistId, MusicBrainz},
|
||||
artist::{Artist, ArtistId},
|
||||
musicbrainz::MusicBrainz,
|
||||
track::{Track, TrackId, TrackNum, TrackQuality},
|
||||
Collection, MergeCollections,
|
||||
},
|
||||
@ -470,7 +471,7 @@ mod tests {
|
||||
use mockall::{predicate, Sequence};
|
||||
|
||||
use crate::core::{
|
||||
collection::artist::{ArtistId, MusicBrainz},
|
||||
collection::{artist::ArtistId, musicbrainz::MusicBrainz},
|
||||
database::{self, MockIDatabase},
|
||||
library::{self, testmod::LIBRARY_ITEMS, MockILibrary},
|
||||
testmod::{FULL_COLLECTION, LIBRARY_COLLECTION},
|
||||
|
@ -3,7 +3,8 @@ use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use crate::core::collection::{
|
||||
album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq},
|
||||
artist::{Artist, ArtistId, MusicBrainz},
|
||||
artist::{Artist, ArtistId},
|
||||
musicbrainz::MusicBrainz,
|
||||
track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality},
|
||||
};
|
||||
use crate::tests::*;
|
||||
|
@ -2,7 +2,8 @@ use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use musichoard::collection::{
|
||||
album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq},
|
||||
artist::{Artist, ArtistId, MusicBrainz},
|
||||
artist::{Artist, ArtistId},
|
||||
musicbrainz::MusicBrainz,
|
||||
track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality},
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
|
@ -3,7 +3,8 @@ use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use musichoard::collection::{
|
||||
album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq},
|
||||
artist::{Artist, ArtistId, MusicBrainz},
|
||||
artist::{Artist, ArtistId},
|
||||
musicbrainz::MusicBrainz,
|
||||
track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality},
|
||||
Collection,
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user