Separate metadata from collections (#209)
All checks were successful
Cargo CI / Build and Test (push) Successful in 1m59s
Cargo CI / Lint (push) Successful in 1m6s

Part 1 of #187

Reviewed-on: #209
This commit is contained in:
Wojciech Kozlowski 2024-08-31 22:55:25 +02:00
parent ebd63cc80b
commit fd9d3677ec
27 changed files with 887 additions and 658 deletions

View File

@ -12,20 +12,26 @@ use crate::core::collection::{
/// An album is a collection of tracks that were released together. /// An album is a collection of tracks that were released together.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Album { pub struct Album {
pub meta: AlbumMeta,
pub tracks: Vec<Track>,
}
/// Album metadata.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AlbumMeta {
pub id: AlbumId, pub id: AlbumId,
pub date: AlbumDate, pub date: AlbumDate,
pub seq: AlbumSeq, pub seq: AlbumSeq,
pub musicbrainz: Option<MbAlbumRef>, pub musicbrainz: Option<MbAlbumRef>,
pub primary_type: Option<AlbumPrimaryType>, pub primary_type: Option<AlbumPrimaryType>,
pub secondary_types: Vec<AlbumSecondaryType>, pub secondary_types: Vec<AlbumSecondaryType>,
pub tracks: Vec<Track>,
} }
impl WithId for Album { impl WithId for Album {
type Id = AlbumId; type Id = AlbumId;
fn id(&self) -> &Self::Id { fn id(&self) -> &Self::Id {
&self.id &self.meta.id
} }
} }
@ -139,13 +145,50 @@ impl Album {
secondary_types: Vec<AlbumSecondaryType>, secondary_types: Vec<AlbumSecondaryType>,
) -> Self { ) -> Self {
Album { Album {
meta: AlbumMeta::new(id, date, primary_type, secondary_types),
tracks: vec![],
}
}
pub fn get_status(&self) -> AlbumStatus {
AlbumStatus::from_tracks(&self.tracks)
}
}
impl PartialOrd for Album {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Album {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.meta.cmp(&other.meta)
}
}
impl Merge for Album {
fn merge_in_place(&mut self, other: Self) {
self.meta.merge_in_place(other.meta);
let tracks = mem::take(&mut self.tracks);
self.tracks = MergeSorted::new(tracks.into_iter(), other.tracks.into_iter()).collect();
}
}
impl AlbumMeta {
pub fn new<Id: Into<AlbumId>, Date: Into<AlbumDate>>(
id: Id,
date: Date,
primary_type: Option<AlbumPrimaryType>,
secondary_types: Vec<AlbumSecondaryType>,
) -> Self {
AlbumMeta {
id: id.into(), id: id.into(),
date: date.into(), date: date.into(),
seq: AlbumSeq::default(), seq: AlbumSeq::default(),
musicbrainz: None, musicbrainz: None,
primary_type, primary_type,
secondary_types, secondary_types,
tracks: vec![],
} }
} }
@ -153,10 +196,6 @@ impl Album {
(&self.date, &self.seq, &self.id) (&self.date, &self.seq, &self.id)
} }
pub fn get_status(&self) -> AlbumStatus {
AlbumStatus::from_tracks(&self.tracks)
}
pub fn set_seq(&mut self, seq: AlbumSeq) { pub fn set_seq(&mut self, seq: AlbumSeq) {
self.seq = seq; self.seq = seq;
} }
@ -174,19 +213,19 @@ impl Album {
} }
} }
impl PartialOrd for Album { impl PartialOrd for AlbumMeta {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other)) Some(self.cmp(other))
} }
} }
impl Ord for Album { impl Ord for AlbumMeta {
fn cmp(&self, other: &Self) -> std::cmp::Ordering { fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.get_sort_key().cmp(&other.get_sort_key()) self.get_sort_key().cmp(&other.get_sort_key())
} }
} }
impl Merge for Album { impl Merge for AlbumMeta {
fn merge_in_place(&mut self, other: Self) { fn merge_in_place(&mut self, other: Self) {
assert_eq!(self.id, other.id); assert_eq!(self.id, other.id);
self.seq = std::cmp::max(self.seq, other.seq); self.seq = std::cmp::max(self.seq, other.seq);
@ -194,9 +233,6 @@ impl Merge for Album {
self.musicbrainz = self.musicbrainz.take().or(other.musicbrainz); self.musicbrainz = self.musicbrainz.take().or(other.musicbrainz);
self.primary_type = self.primary_type.take().or(other.primary_type); self.primary_type = self.primary_type.take().or(other.primary_type);
self.secondary_types.merge_in_place(other.secondary_types); self.secondary_types.merge_in_place(other.secondary_types);
let tracks = mem::take(&mut self.tracks);
self.tracks = MergeSorted::new(tracks.into_iter(), other.tracks.into_iter()).collect();
} }
} }
@ -250,44 +286,45 @@ mod tests {
title: String::from("album z"), title: String::from("album z"),
}; };
let mut album_1 = Album::new(album_id_1, date.clone(), None, vec![]); let mut album_1 = Album::new(album_id_1, date.clone(), None, vec![]);
album_1.set_seq(AlbumSeq(1)); album_1.meta.set_seq(AlbumSeq(1));
let album_id_2 = AlbumId { let album_id_2 = AlbumId {
title: String::from("album a"), title: String::from("album a"),
}; };
let mut album_2 = Album::new(album_id_2, date.clone(), None, vec![]); let mut album_2 = Album::new(album_id_2, date.clone(), None, vec![]);
album_2.set_seq(AlbumSeq(2)); album_2.meta.set_seq(AlbumSeq(2));
assert_ne!(album_1, album_2); assert_ne!(album_1, album_2);
assert!(album_1 < album_2); assert!(album_1 < album_2);
assert!(album_1.meta < album_2.meta);
} }
#[test] #[test]
fn set_clear_seq() { fn set_clear_seq() {
let mut album = Album::new("An album", AlbumDate::default(), None, vec![]); let mut album = Album::new("An album", AlbumDate::default(), None, vec![]);
assert_eq!(album.seq, AlbumSeq(0)); assert_eq!(album.meta.seq, AlbumSeq(0));
// Setting a seq on an album. // Setting a seq on an album.
album.set_seq(AlbumSeq(6)); album.meta.set_seq(AlbumSeq(6));
assert_eq!(album.seq, AlbumSeq(6)); assert_eq!(album.meta.seq, AlbumSeq(6));
album.set_seq(AlbumSeq(6)); album.meta.set_seq(AlbumSeq(6));
assert_eq!(album.seq, AlbumSeq(6)); assert_eq!(album.meta.seq, AlbumSeq(6));
album.set_seq(AlbumSeq(8)); album.meta.set_seq(AlbumSeq(8));
assert_eq!(album.seq, AlbumSeq(8)); assert_eq!(album.meta.seq, AlbumSeq(8));
// Clearing seq. // Clearing seq.
album.clear_seq(); album.meta.clear_seq();
assert_eq!(album.seq, AlbumSeq(0)); assert_eq!(album.meta.seq, AlbumSeq(0));
} }
#[test] #[test]
fn merge_album_no_overlap() { fn merge_album_no_overlap() {
let left = FULL_COLLECTION[0].albums[0].to_owned(); let left = FULL_COLLECTION[0].albums[0].to_owned();
let mut right = FULL_COLLECTION[0].albums[1].to_owned(); let mut right = FULL_COLLECTION[0].albums[1].to_owned();
right.id = left.id.clone(); right.meta.id = left.meta.id.clone();
let mut expected = left.clone(); let mut expected = left.clone();
expected.tracks.append(&mut right.tracks.clone()); expected.tracks.append(&mut right.tracks.clone());
@ -305,7 +342,7 @@ mod tests {
fn merge_album_overlap() { fn merge_album_overlap() {
let mut left = FULL_COLLECTION[0].albums[0].to_owned(); let mut left = FULL_COLLECTION[0].albums[0].to_owned();
let mut right = FULL_COLLECTION[0].albums[1].to_owned(); let mut right = FULL_COLLECTION[0].albums[1].to_owned();
right.id = left.id.clone(); right.meta.id = left.meta.id.clone();
left.tracks.push(right.tracks[0].clone()); left.tracks.push(right.tracks[0].clone());
left.tracks.sort_unstable(); left.tracks.sort_unstable();
@ -328,23 +365,29 @@ mod tests {
let mut album = Album::new(AlbumId::new("an album"), AlbumDate::default(), None, vec![]); let mut album = Album::new(AlbumId::new("an album"), AlbumDate::default(), None, vec![]);
let mut expected: Option<MbAlbumRef> = None; let mut expected: Option<MbAlbumRef> = None;
assert_eq!(album.musicbrainz, expected); assert_eq!(album.meta.musicbrainz, expected);
// Setting a URL on an album. // Setting a URL on an album.
album.set_musicbrainz_ref(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap()); album
.meta
.set_musicbrainz_ref(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap());
_ = expected.insert(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap()); _ = expected.insert(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap());
assert_eq!(album.musicbrainz, expected); assert_eq!(album.meta.musicbrainz, expected);
album.set_musicbrainz_ref(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap()); album
assert_eq!(album.musicbrainz, expected); .meta
.set_musicbrainz_ref(MbAlbumRef::from_url_str(MUSICBRAINZ).unwrap());
assert_eq!(album.meta.musicbrainz, expected);
album.set_musicbrainz_ref(MbAlbumRef::from_url_str(MUSICBRAINZ_2).unwrap()); album
.meta
.set_musicbrainz_ref(MbAlbumRef::from_url_str(MUSICBRAINZ_2).unwrap());
_ = expected.insert(MbAlbumRef::from_url_str(MUSICBRAINZ_2).unwrap()); _ = expected.insert(MbAlbumRef::from_url_str(MUSICBRAINZ_2).unwrap());
assert_eq!(album.musicbrainz, expected); assert_eq!(album.meta.musicbrainz, expected);
// Clearing URLs. // Clearing URLs.
album.clear_musicbrainz_ref(); album.meta.clear_musicbrainz_ref();
_ = expected.take(); _ = expected.take();
assert_eq!(album.musicbrainz, expected); assert_eq!(album.meta.musicbrainz, expected);
} }
} }

View File

@ -13,18 +13,24 @@ use crate::core::collection::{
/// An artist. /// An artist.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Artist { pub struct Artist {
pub meta: ArtistMeta,
pub albums: Vec<Album>,
}
/// Artist metadata.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ArtistMeta {
pub id: ArtistId, pub id: ArtistId,
pub sort: Option<ArtistId>, pub sort: Option<ArtistId>,
pub musicbrainz: Option<MbArtistRef>, pub musicbrainz: Option<MbArtistRef>,
pub properties: HashMap<String, Vec<String>>, pub properties: HashMap<String, Vec<String>>,
pub albums: Vec<Album>,
} }
impl WithId for Artist { impl WithId for Artist {
type Id = ArtistId; type Id = ArtistId;
fn id(&self) -> &Self::Id { fn id(&self) -> &Self::Id {
&self.id &self.meta.id
} }
} }
@ -38,11 +44,39 @@ impl Artist {
/// Create new [`Artist`] with the given [`ArtistId`]. /// Create new [`Artist`] with the given [`ArtistId`].
pub fn new<Id: Into<ArtistId>>(id: Id) -> Self { pub fn new<Id: Into<ArtistId>>(id: Id) -> Self {
Artist { Artist {
meta: ArtistMeta::new(id),
albums: vec![],
}
}
}
impl PartialOrd for Artist {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Artist {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.meta.cmp(&other.meta)
}
}
impl Merge for Artist {
fn merge_in_place(&mut self, other: Self) {
self.meta.merge_in_place(other.meta);
let albums = mem::take(&mut self.albums);
self.albums = MergeCollections::merge_iter(albums, other.albums);
}
}
impl ArtistMeta {
pub fn new<Id: Into<ArtistId>>(id: Id) -> Self {
ArtistMeta {
id: id.into(), id: id.into(),
sort: None, sort: None,
musicbrainz: None, musicbrainz: None,
properties: HashMap::new(), properties: HashMap::new(),
albums: vec![],
} }
} }
@ -111,28 +145,25 @@ impl Artist {
} }
} }
impl PartialOrd for Artist { impl PartialOrd for ArtistMeta {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other)) Some(self.cmp(other))
} }
} }
impl Ord for Artist { impl Ord for ArtistMeta {
fn cmp(&self, other: &Self) -> std::cmp::Ordering { fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.get_sort_key().cmp(&other.get_sort_key()) self.get_sort_key().cmp(&other.get_sort_key())
} }
} }
impl Merge for Artist { impl Merge for ArtistMeta {
fn merge_in_place(&mut self, other: Self) { fn merge_in_place(&mut self, other: Self) {
assert_eq!(self.id, other.id); assert_eq!(self.id, other.id);
self.sort = self.sort.take().or(other.sort); self.sort = self.sort.take().or(other.sort);
self.musicbrainz = self.musicbrainz.take().or(other.musicbrainz); self.musicbrainz = self.musicbrainz.take().or(other.musicbrainz);
self.properties.merge_in_place(other.properties); self.properties.merge_in_place(other.properties);
let albums = mem::take(&mut self.albums);
self.albums = MergeCollections::merge_iter(albums, other.albums);
} }
} }
@ -181,33 +212,41 @@ mod tests {
let mut artist = Artist::new(&artist_id.name); let mut artist = Artist::new(&artist_id.name);
assert_eq!(artist.id, artist_id); assert_eq!(artist.meta.id, artist_id);
assert_eq!(artist.sort, None); assert_eq!(artist.meta.sort, None);
assert_eq!(artist.get_sort_key(), (&artist_id,)); assert_eq!(artist.meta.get_sort_key(), (&artist_id,));
assert!(artist.meta < ArtistMeta::new(sort_id_1.clone()));
assert!(artist.meta < ArtistMeta::new(sort_id_2.clone()));
assert!(artist < Artist::new(sort_id_1.clone())); assert!(artist < Artist::new(sort_id_1.clone()));
assert!(artist < Artist::new(sort_id_2.clone())); assert!(artist < Artist::new(sort_id_2.clone()));
artist.set_sort_key(sort_id_1.clone()); artist.meta.set_sort_key(sort_id_1.clone());
assert_eq!(artist.id, artist_id); assert_eq!(artist.meta.id, artist_id);
assert_eq!(artist.sort.as_ref(), Some(&sort_id_1)); assert_eq!(artist.meta.sort.as_ref(), Some(&sort_id_1));
assert_eq!(artist.get_sort_key(), (&sort_id_1,)); assert_eq!(artist.meta.get_sort_key(), (&sort_id_1,));
assert!(artist.meta > ArtistMeta::new(artist_id.clone()));
assert!(artist.meta < ArtistMeta::new(sort_id_2.clone()));
assert!(artist > Artist::new(artist_id.clone())); assert!(artist > Artist::new(artist_id.clone()));
assert!(artist < Artist::new(sort_id_2.clone())); assert!(artist < Artist::new(sort_id_2.clone()));
artist.set_sort_key(sort_id_2.clone()); artist.meta.set_sort_key(sort_id_2.clone());
assert_eq!(artist.id, artist_id); assert_eq!(artist.meta.id, artist_id);
assert_eq!(artist.sort.as_ref(), Some(&sort_id_2)); assert_eq!(artist.meta.sort.as_ref(), Some(&sort_id_2));
assert_eq!(artist.get_sort_key(), (&sort_id_2,)); assert_eq!(artist.meta.get_sort_key(), (&sort_id_2,));
assert!(artist.meta > ArtistMeta::new(artist_id.clone()));
assert!(artist.meta > ArtistMeta::new(sort_id_1.clone()));
assert!(artist > Artist::new(artist_id.clone())); assert!(artist > Artist::new(artist_id.clone()));
assert!(artist > Artist::new(sort_id_1.clone())); assert!(artist > Artist::new(sort_id_1.clone()));
artist.clear_sort_key(); artist.meta.clear_sort_key();
assert_eq!(artist.id, artist_id); assert_eq!(artist.meta.id, artist_id);
assert_eq!(artist.sort, None); assert_eq!(artist.meta.sort, None);
assert_eq!(artist.get_sort_key(), (&artist_id,)); assert_eq!(artist.meta.get_sort_key(), (&artist_id,));
assert!(artist.meta < ArtistMeta::new(sort_id_1.clone()));
assert!(artist.meta < ArtistMeta::new(sort_id_2.clone()));
assert!(artist < Artist::new(sort_id_1.clone())); assert!(artist < Artist::new(sort_id_1.clone()));
assert!(artist < Artist::new(sort_id_2.clone())); assert!(artist < Artist::new(sort_id_2.clone()));
} }
@ -217,24 +256,30 @@ mod tests {
let mut artist = Artist::new(ArtistId::new("an artist")); let mut artist = Artist::new(ArtistId::new("an artist"));
let mut expected: Option<MbArtistRef> = None; let mut expected: Option<MbArtistRef> = None;
assert_eq!(artist.musicbrainz, expected); assert_eq!(artist.meta.musicbrainz, expected);
// Setting a URL on an artist. // Setting a URL on an artist.
artist.set_musicbrainz_ref(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap()); artist
.meta
.set_musicbrainz_ref(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap());
_ = expected.insert(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap()); _ = expected.insert(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap());
assert_eq!(artist.musicbrainz, expected); assert_eq!(artist.meta.musicbrainz, expected);
artist.set_musicbrainz_ref(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap()); artist
assert_eq!(artist.musicbrainz, expected); .meta
.set_musicbrainz_ref(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap());
assert_eq!(artist.meta.musicbrainz, expected);
artist.set_musicbrainz_ref(MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap()); artist
.meta
.set_musicbrainz_ref(MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap());
_ = expected.insert(MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap()); _ = expected.insert(MbArtistRef::from_url_str(MUSICBRAINZ_2).unwrap());
assert_eq!(artist.musicbrainz, expected); assert_eq!(artist.meta.musicbrainz, expected);
// Clearing URLs. // Clearing URLs.
artist.clear_musicbrainz_ref(); artist.meta.clear_musicbrainz_ref();
_ = expected.take(); _ = expected.take();
assert_eq!(artist.musicbrainz, expected); assert_eq!(artist.meta.musicbrainz, expected);
} }
#[test] #[test]
@ -242,70 +287,98 @@ mod tests {
let mut artist = Artist::new(ArtistId::new("an artist")); let mut artist = Artist::new(ArtistId::new("an artist"));
let mut expected: Vec<String> = vec![]; let mut expected: Vec<String> = vec![];
assert!(artist.properties.is_empty()); assert!(artist.meta.properties.is_empty());
// Adding a single URL. // Adding a single URL.
artist.add_to_property("MusicButler", vec![MUSICBUTLER]); artist
.meta
.add_to_property("MusicButler", vec![MUSICBUTLER]);
expected.push(MUSICBUTLER.to_owned()); expected.push(MUSICBUTLER.to_owned());
assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected));
// Adding a URL that already exists is ok, but does not do anything. // Adding a URL that already exists is ok, but does not do anything.
artist.add_to_property("MusicButler", vec![MUSICBUTLER]); artist
assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); .meta
.add_to_property("MusicButler", vec![MUSICBUTLER]);
assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected));
// Adding another single URL. // Adding another single URL.
artist.add_to_property("MusicButler", vec![MUSICBUTLER_2]); artist
.meta
.add_to_property("MusicButler", vec![MUSICBUTLER_2]);
expected.push(MUSICBUTLER_2.to_owned()); expected.push(MUSICBUTLER_2.to_owned());
assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected));
artist.add_to_property("MusicButler", vec![MUSICBUTLER_2]); artist
assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); .meta
.add_to_property("MusicButler", vec![MUSICBUTLER_2]);
assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected));
// Removing a URL. // Removing a URL.
artist.remove_from_property("MusicButler", vec![MUSICBUTLER]); artist
.meta
.remove_from_property("MusicButler", vec![MUSICBUTLER]);
expected.retain(|url| url != MUSICBUTLER); expected.retain(|url| url != MUSICBUTLER);
assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected));
// Removing URls that do not exist is okay, they will be ignored. // Removing URls that do not exist is okay, they will be ignored.
artist.remove_from_property("MusicButler", vec![MUSICBUTLER]); artist
assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); .meta
.remove_from_property("MusicButler", vec![MUSICBUTLER]);
assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected));
// Removing a URL. // Removing a URL.
artist.remove_from_property("MusicButler", vec![MUSICBUTLER_2]); artist
.meta
.remove_from_property("MusicButler", vec![MUSICBUTLER_2]);
expected.retain(|url| url.as_str() != MUSICBUTLER_2); expected.retain(|url| url.as_str() != MUSICBUTLER_2);
assert!(artist.properties.is_empty()); assert!(artist.meta.properties.is_empty());
artist.remove_from_property("MusicButler", vec![MUSICBUTLER_2]); artist
assert!(artist.properties.is_empty()); .meta
.remove_from_property("MusicButler", vec![MUSICBUTLER_2]);
assert!(artist.meta.properties.is_empty());
// Adding URLs if some exist is okay, they will be ignored. // Adding URLs if some exist is okay, they will be ignored.
artist.add_to_property("MusicButler", vec![MUSICBUTLER]); artist
.meta
.add_to_property("MusicButler", vec![MUSICBUTLER]);
expected.push(MUSICBUTLER.to_owned()); expected.push(MUSICBUTLER.to_owned());
assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected));
artist.add_to_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); artist
.meta
.add_to_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]);
expected.push(MUSICBUTLER_2.to_owned()); expected.push(MUSICBUTLER_2.to_owned());
assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected));
// Removing URLs if some do not exist is okay, they will be ignored. // Removing URLs if some do not exist is okay, they will be ignored.
artist.remove_from_property("MusicButler", vec![MUSICBUTLER]); artist
.meta
.remove_from_property("MusicButler", vec![MUSICBUTLER]);
expected.retain(|url| url.as_str() != MUSICBUTLER); expected.retain(|url| url.as_str() != MUSICBUTLER);
assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected));
artist.remove_from_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); artist
.meta
.remove_from_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]);
expected.retain(|url| url.as_str() != MUSICBUTLER_2); expected.retain(|url| url.as_str() != MUSICBUTLER_2);
assert!(artist.properties.is_empty()); assert!(artist.meta.properties.is_empty());
// Adding mutliple URLs without clashes. // Adding mutliple URLs without clashes.
artist.add_to_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); artist
.meta
.add_to_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]);
expected.push(MUSICBUTLER.to_owned()); expected.push(MUSICBUTLER.to_owned());
expected.push(MUSICBUTLER_2.to_owned()); expected.push(MUSICBUTLER_2.to_owned());
assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected));
// Removing multiple URLs without clashes. // Removing multiple URLs without clashes.
artist.remove_from_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); artist
.meta
.remove_from_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]);
expected.clear(); expected.clear();
assert!(artist.properties.is_empty()); assert!(artist.meta.properties.is_empty());
} }
#[test] #[test]
@ -313,40 +386,45 @@ mod tests {
let mut artist = Artist::new(ArtistId::new("an artist")); let mut artist = Artist::new(ArtistId::new("an artist"));
let mut expected: Vec<String> = vec![]; let mut expected: Vec<String> = vec![];
assert!(artist.properties.is_empty()); assert!(artist.meta.properties.is_empty());
// Set URLs. // Set URLs.
artist.set_property("MusicButler", vec![MUSICBUTLER]); artist.meta.set_property("MusicButler", vec![MUSICBUTLER]);
expected.push(MUSICBUTLER.to_owned()); expected.push(MUSICBUTLER.to_owned());
assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected));
artist.set_property("MusicButler", vec![MUSICBUTLER_2]); artist.meta.set_property("MusicButler", vec![MUSICBUTLER_2]);
expected.clear(); expected.clear();
expected.push(MUSICBUTLER_2.to_owned()); expected.push(MUSICBUTLER_2.to_owned());
assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected));
artist.set_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]); artist
.meta
.set_property("MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]);
expected.clear(); expected.clear();
expected.push(MUSICBUTLER.to_owned()); expected.push(MUSICBUTLER.to_owned());
expected.push(MUSICBUTLER_2.to_owned()); expected.push(MUSICBUTLER_2.to_owned());
assert_eq!(artist.properties.get("MusicButler"), Some(&expected)); assert_eq!(artist.meta.properties.get("MusicButler"), Some(&expected));
// Clear URLs. // Clear URLs.
artist.clear_property("MusicButler"); artist.meta.clear_property("MusicButler");
expected.clear(); expected.clear();
assert!(artist.properties.is_empty()); assert!(artist.meta.properties.is_empty());
} }
#[test] #[test]
fn merge_artist_no_overlap() { fn merge_artist_no_overlap() {
let left = FULL_COLLECTION[0].to_owned(); let left = FULL_COLLECTION[0].to_owned();
let mut right = FULL_COLLECTION[1].to_owned(); let mut right = FULL_COLLECTION[1].to_owned();
right.id = left.id.clone(); right.meta.id = left.meta.id.clone();
right.musicbrainz = None; right.meta.musicbrainz = None;
right.properties = HashMap::new(); right.meta.properties = HashMap::new();
let mut expected = left.clone(); let mut expected = left.clone();
expected.properties = expected.properties.merge(right.clone().properties); expected.meta.properties = expected
.meta
.properties
.merge(right.clone().meta.properties);
expected.albums.append(&mut right.albums.clone()); expected.albums.append(&mut right.albums.clone());
expected.albums.sort_unstable(); expected.albums.sort_unstable();
@ -362,12 +440,15 @@ mod tests {
fn merge_artist_overlap() { fn merge_artist_overlap() {
let mut left = FULL_COLLECTION[0].to_owned(); let mut left = FULL_COLLECTION[0].to_owned();
let mut right = FULL_COLLECTION[1].to_owned(); let mut right = FULL_COLLECTION[1].to_owned();
right.id = left.id.clone(); right.meta.id = left.meta.id.clone();
left.albums.push(right.albums[0].clone()); left.albums.push(right.albums[0].clone());
left.albums.sort_unstable(); left.albums.sort_unstable();
let mut expected = left.clone(); let mut expected = left.clone();
expected.properties = expected.properties.merge(right.clone().properties); expected.meta.properties = expected
.meta
.properties
.merge(right.clone().meta.properties);
expected.albums.append(&mut right.albums.clone()); expected.albums.append(&mut right.albums.clone());
expected.albums.sort_unstable(); expected.albums.sort_unstable();
expected.albums.dedup(); expected.albums.dedup();

View File

@ -59,14 +59,14 @@ impl<Database, Library> IMusicHoardBasePrivate for MusicHoard<Database, Library>
} }
fn get_artist<'a>(collection: &'a Collection, artist_id: &ArtistId) -> Option<&'a Artist> { fn get_artist<'a>(collection: &'a Collection, artist_id: &ArtistId) -> Option<&'a Artist> {
collection.iter().find(|a| &a.id == artist_id) collection.iter().find(|a| &a.meta.id == artist_id)
} }
fn get_artist_mut<'a>( fn get_artist_mut<'a>(
collection: &'a mut Collection, collection: &'a mut Collection,
artist_id: &ArtistId, artist_id: &ArtistId,
) -> Option<&'a mut Artist> { ) -> Option<&'a mut Artist> {
collection.iter_mut().find(|a| &a.id == artist_id) collection.iter_mut().find(|a| &a.meta.id == artist_id)
} }
fn get_artist_mut_or_err<'a>( fn get_artist_mut_or_err<'a>(
@ -79,7 +79,7 @@ impl<Database, Library> IMusicHoardBasePrivate for MusicHoard<Database, Library>
} }
fn get_album_mut<'a>(artist: &'a mut Artist, album_id: &AlbumId) -> Option<&'a mut Album> { fn get_album_mut<'a>(artist: &'a mut Artist, album_id: &AlbumId) -> Option<&'a mut Album> {
artist.albums.iter_mut().find(|a| &a.id == album_id) artist.albums.iter_mut().find(|a| &a.meta.id == album_id)
} }
fn get_album_mut_or_err<'a>( fn get_album_mut_or_err<'a>(
@ -115,7 +115,7 @@ mod tests {
library_cache: left library_cache: left
.clone() .clone()
.into_iter() .into_iter()
.map(|a| (a.id.clone(), a)) .map(|a| (a.meta.id.clone(), a))
.collect(), .collect(),
database_cache: right.clone(), database_cache: right.clone(),
..Default::default() ..Default::default()
@ -129,7 +129,7 @@ mod tests {
library_cache: right library_cache: right
.clone() .clone()
.into_iter() .into_iter()
.map(|a| (a.id.clone(), a)) .map(|a| (a.meta.id.clone(), a))
.collect(), .collect(),
database_cache: left.clone(), database_cache: left.clone(),
..Default::default() ..Default::default()
@ -153,7 +153,7 @@ mod tests {
library_cache: left library_cache: left
.clone() .clone()
.into_iter() .into_iter()
.map(|a| (a.id.clone(), a)) .map(|a| (a.meta.id.clone(), a))
.collect(), .collect(),
database_cache: right.clone(), database_cache: right.clone(),
..Default::default() ..Default::default()
@ -167,7 +167,7 @@ mod tests {
library_cache: right library_cache: right
.clone() .clone()
.into_iter() .into_iter()
.map(|a| (a.id.clone(), a)) .map(|a| (a.meta.id.clone(), a))
.collect(), .collect(),
database_cache: left.clone(), database_cache: left.clone(),
..Default::default() ..Default::default()
@ -191,20 +191,20 @@ mod tests {
assert!(right.first().unwrap() > left.first().unwrap()); assert!(right.first().unwrap() > left.first().unwrap());
let artist_sort = Some(ArtistId::new("Album_Artist 0")); let artist_sort = Some(ArtistId::new("Album_Artist 0"));
right[0].sort = artist_sort.clone(); right[0].meta.sort = artist_sort.clone();
assert!(right.first().unwrap() < left.first().unwrap()); assert!(right.first().unwrap() < left.first().unwrap());
// The result of the merge should be the same list of artists, but with the last artist now // The result of the merge should be the same list of artists, but with the last artist now
// in first place. // in first place.
let mut expected = left.to_owned(); let mut expected = left.to_owned();
expected.last_mut().as_mut().unwrap().sort = artist_sort.clone(); expected.last_mut().as_mut().unwrap().meta.sort = artist_sort.clone();
expected.rotate_right(1); expected.rotate_right(1);
let mut mh = MusicHoard { let mut mh = MusicHoard {
library_cache: left library_cache: left
.clone() .clone()
.into_iter() .into_iter()
.map(|a| (a.id.clone(), a)) .map(|a| (a.meta.id.clone(), a))
.collect(), .collect(),
database_cache: right.clone(), database_cache: right.clone(),
..Default::default() ..Default::default()
@ -218,7 +218,7 @@ mod tests {
library_cache: right library_cache: right
.clone() .clone()
.into_iter() .into_iter()
.map(|a| (a.id.clone(), a)) .map(|a| (a.meta.id.clone(), a))
.collect(), .collect(),
database_cache: left.clone(), database_cache: left.clone(),
..Default::default() ..Default::default()

View File

@ -91,7 +91,9 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
fn remove_artist<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error> { fn remove_artist<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error> {
self.update_collection(|collection| { self.update_collection(|collection| {
let index_opt = collection.iter().position(|a| &a.id == artist_id.as_ref()); let index_opt = collection
.iter()
.position(|a| &a.meta.id == artist_id.as_ref());
if let Some(index) = index_opt { if let Some(index) = index_opt {
collection.remove(index); collection.remove(index);
} }
@ -105,7 +107,7 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.update_artist_and( self.update_artist_and(
artist_id.as_ref(), artist_id.as_ref(),
|artist| artist.set_sort_key(artist_sort), |artist| artist.meta.set_sort_key(artist_sort),
|collection| Self::sort_artists(collection), |collection| Self::sort_artists(collection),
) )
} }
@ -113,7 +115,7 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
fn clear_artist_sort<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error> { fn clear_artist_sort<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error> {
self.update_artist_and( self.update_artist_and(
artist_id.as_ref(), artist_id.as_ref(),
|artist| artist.clear_sort_key(), |artist| artist.meta.clear_sort_key(),
|collection| Self::sort_artists(collection), |collection| Self::sort_artists(collection),
) )
} }
@ -124,14 +126,18 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
url: S, url: S,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mb = MbArtistRef::from_url_str(url)?; let mb = MbArtistRef::from_url_str(url)?;
self.update_artist(artist_id.as_ref(), |artist| artist.set_musicbrainz_ref(mb)) self.update_artist(artist_id.as_ref(), |artist| {
artist.meta.set_musicbrainz_ref(mb)
})
} }
fn clear_artist_musicbrainz<Id: AsRef<ArtistId>>( fn clear_artist_musicbrainz<Id: AsRef<ArtistId>>(
&mut self, &mut self,
artist_id: Id, artist_id: Id,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.update_artist(artist_id.as_ref(), |artist| artist.clear_musicbrainz_ref()) self.update_artist(artist_id.as_ref(), |artist| {
artist.meta.clear_musicbrainz_ref()
})
} }
fn add_to_artist_property<Id: AsRef<ArtistId>, S: AsRef<str> + Into<String>>( fn add_to_artist_property<Id: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
@ -141,7 +147,7 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
values: Vec<S>, values: Vec<S>,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.update_artist(artist_id.as_ref(), |artist| { self.update_artist(artist_id.as_ref(), |artist| {
artist.add_to_property(property, values) artist.meta.add_to_property(property, values)
}) })
} }
@ -152,7 +158,7 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
values: Vec<S>, values: Vec<S>,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.update_artist(artist_id.as_ref(), |artist| { self.update_artist(artist_id.as_ref(), |artist| {
artist.remove_from_property(property, values) artist.meta.remove_from_property(property, values)
}) })
} }
@ -163,7 +169,7 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
values: Vec<S>, values: Vec<S>,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.update_artist(artist_id.as_ref(), |artist| { self.update_artist(artist_id.as_ref(), |artist| {
artist.set_property(property, values) artist.meta.set_property(property, values)
}) })
} }
@ -172,7 +178,9 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
artist_id: Id, artist_id: Id,
property: S, property: S,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.update_artist(artist_id.as_ref(), |artist| artist.clear_property(property)) self.update_artist(artist_id.as_ref(), |artist| {
artist.meta.clear_property(property)
})
} }
fn set_album_seq<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>( fn set_album_seq<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
@ -184,7 +192,7 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
self.update_album_and( self.update_album_and(
artist_id.as_ref(), artist_id.as_ref(),
album_id.as_ref(), album_id.as_ref(),
|album| album.set_seq(AlbumSeq(seq)), |album| album.meta.set_seq(AlbumSeq(seq)),
|artist| artist.albums.sort_unstable(), |artist| artist.albums.sort_unstable(),
|_| {}, |_| {},
) )
@ -198,7 +206,7 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
self.update_album_and( self.update_album_and(
artist_id.as_ref(), artist_id.as_ref(),
album_id.as_ref(), album_id.as_ref(),
|album| album.clear_seq(), |album| album.meta.clear_seq(),
|artist| artist.albums.sort_unstable(), |artist| artist.albums.sort_unstable(),
|_| {}, |_| {},
) )
@ -434,29 +442,29 @@ mod tests {
assert!(music_hoard.add_artist(artist_id.clone()).is_ok()); assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
let mut expected: Option<MbArtistRef> = None; let mut expected: Option<MbArtistRef> = None;
assert_eq!(music_hoard.collection[0].musicbrainz, expected); assert_eq!(music_hoard.collection[0].meta.musicbrainz, expected);
// Setting a URL on an artist not in the collection is an error. // Setting a URL on an artist not in the collection is an error.
assert!(music_hoard assert!(music_hoard
.set_artist_musicbrainz(&artist_id_2, MUSICBRAINZ) .set_artist_musicbrainz(&artist_id_2, MUSICBRAINZ)
.is_err()); .is_err());
assert_eq!(music_hoard.collection[0].musicbrainz, expected); assert_eq!(music_hoard.collection[0].meta.musicbrainz, expected);
// Setting a URL on an artist. // Setting a URL on an artist.
assert!(music_hoard assert!(music_hoard
.set_artist_musicbrainz(&artist_id, MUSICBRAINZ) .set_artist_musicbrainz(&artist_id, MUSICBRAINZ)
.is_ok()); .is_ok());
_ = expected.insert(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap()); _ = expected.insert(MbArtistRef::from_url_str(MUSICBRAINZ).unwrap());
assert_eq!(music_hoard.collection[0].musicbrainz, expected); assert_eq!(music_hoard.collection[0].meta.musicbrainz, expected);
// Clearing URLs on an artist that does not exist is an error. // Clearing URLs on an artist that does not exist is an error.
assert!(music_hoard.clear_artist_musicbrainz(&artist_id_2).is_err()); assert!(music_hoard.clear_artist_musicbrainz(&artist_id_2).is_err());
assert_eq!(music_hoard.collection[0].musicbrainz, expected); assert_eq!(music_hoard.collection[0].meta.musicbrainz, expected);
// Clearing URLs. // Clearing URLs.
assert!(music_hoard.clear_artist_musicbrainz(&artist_id).is_ok()); assert!(music_hoard.clear_artist_musicbrainz(&artist_id).is_ok());
_ = expected.take(); _ = expected.take();
assert_eq!(music_hoard.collection[0].musicbrainz, expected); assert_eq!(music_hoard.collection[0].meta.musicbrainz, expected);
} }
#[test] #[test]
@ -472,13 +480,13 @@ mod tests {
assert!(music_hoard.add_artist(artist_id.clone()).is_ok()); assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
let mut expected: Vec<String> = vec![]; let mut expected: Vec<String> = vec![];
assert!(music_hoard.collection[0].properties.is_empty()); assert!(music_hoard.collection[0].meta.properties.is_empty());
// Adding URLs to an artist not in the collection is an error. // Adding URLs to an artist not in the collection is an error.
assert!(music_hoard assert!(music_hoard
.add_to_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) .add_to_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.is_err()); .is_err());
assert!(music_hoard.collection[0].properties.is_empty()); assert!(music_hoard.collection[0].meta.properties.is_empty());
// Adding mutliple URLs without clashes. // Adding mutliple URLs without clashes.
assert!(music_hoard assert!(music_hoard
@ -487,7 +495,7 @@ mod tests {
expected.push(MUSICBUTLER.to_owned()); expected.push(MUSICBUTLER.to_owned());
expected.push(MUSICBUTLER_2.to_owned()); expected.push(MUSICBUTLER_2.to_owned());
assert_eq!( assert_eq!(
music_hoard.collection[0].properties.get("MusicButler"), music_hoard.collection[0].meta.properties.get("MusicButler"),
Some(&expected) Some(&expected)
); );
@ -496,7 +504,7 @@ mod tests {
.remove_from_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) .remove_from_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.is_err()); .is_err());
assert_eq!( assert_eq!(
music_hoard.collection[0].properties.get("MusicButler"), music_hoard.collection[0].meta.properties.get("MusicButler"),
Some(&expected) Some(&expected)
); );
@ -509,7 +517,7 @@ mod tests {
) )
.is_ok()); .is_ok());
expected.clear(); expected.clear();
assert!(music_hoard.collection[0].properties.is_empty()); assert!(music_hoard.collection[0].meta.properties.is_empty());
} }
#[test] #[test]
@ -525,13 +533,13 @@ mod tests {
assert!(music_hoard.add_artist(artist_id.clone()).is_ok()); assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
let mut expected: Vec<String> = vec![]; let mut expected: Vec<String> = vec![];
assert!(music_hoard.collection[0].properties.is_empty()); assert!(music_hoard.collection[0].meta.properties.is_empty());
// Seting URL on an artist not in the collection is an error. // Seting URL on an artist not in the collection is an error.
assert!(music_hoard assert!(music_hoard
.set_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) .set_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
.is_err()); .is_err());
assert!(music_hoard.collection[0].properties.is_empty()); assert!(music_hoard.collection[0].meta.properties.is_empty());
// Set URLs. // Set URLs.
assert!(music_hoard assert!(music_hoard
@ -541,7 +549,7 @@ mod tests {
expected.push(MUSICBUTLER.to_owned()); expected.push(MUSICBUTLER.to_owned());
expected.push(MUSICBUTLER_2.to_owned()); expected.push(MUSICBUTLER_2.to_owned());
assert_eq!( assert_eq!(
music_hoard.collection[0].properties.get("MusicButler"), music_hoard.collection[0].meta.properties.get("MusicButler"),
Some(&expected) Some(&expected)
); );
@ -555,7 +563,7 @@ mod tests {
.clear_artist_property(&artist_id, "MusicButler") .clear_artist_property(&artist_id, "MusicButler")
.is_ok()); .is_ok());
expected.clear(); expected.clear();
assert!(music_hoard.collection[0].properties.is_empty()); assert!(music_hoard.collection[0].meta.properties.is_empty());
} }
#[test] #[test]
@ -581,17 +589,17 @@ mod tests {
database.expect_save().times(2).returning(|_| Ok(())); database.expect_save().times(2).returning(|_| Ok(()));
let mut music_hoard = MusicHoard::database(database).unwrap(); let mut music_hoard = MusicHoard::database(database).unwrap();
assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(0)); assert_eq!(music_hoard.collection[0].albums[0].meta.seq, AlbumSeq(0));
// Seting seq on an album not belonging to the artist is an error. // Seting seq on an album not belonging to the artist is an error.
assert!(music_hoard assert!(music_hoard
.set_album_seq(&artist_id, &album_id_2, 6) .set_album_seq(&artist_id, &album_id_2, 6)
.is_err()); .is_err());
assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(0)); assert_eq!(music_hoard.collection[0].albums[0].meta.seq, AlbumSeq(0));
// Set seq. // Set seq.
assert!(music_hoard.set_album_seq(&artist_id, &album_id, 6).is_ok()); assert!(music_hoard.set_album_seq(&artist_id, &album_id, 6).is_ok());
assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(6)); assert_eq!(music_hoard.collection[0].albums[0].meta.seq, AlbumSeq(6));
// Clearing seq on an album that does not exist is an error. // Clearing seq on an album that does not exist is an error.
assert!(music_hoard assert!(music_hoard
@ -600,7 +608,7 @@ mod tests {
// Clear seq. // Clear seq.
assert!(music_hoard.clear_album_seq(&artist_id, &album_id).is_ok()); assert!(music_hoard.clear_album_seq(&artist_id, &album_id).is_ok());
assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(0)); assert_eq!(music_hoard.collection[0].albums[0].meta.seq, AlbumSeq(0));
} }
#[test] #[test]

View File

@ -86,17 +86,17 @@ impl<Database, Library: ILibrary> MusicHoard<Database, Library> {
.or_insert_with(|| Artist::new(artist_id)), .or_insert_with(|| Artist::new(artist_id)),
}; };
if artist.sort.is_some() { if artist.meta.sort.is_some() {
if artist_sort.is_some() && (artist.sort != artist_sort) { if artist_sort.is_some() && (artist.meta.sort != artist_sort) {
return Err(Error::CollectionError(format!( return Err(Error::CollectionError(format!(
"multiple album_artist_sort found for artist '{}': '{}' != '{}'", "multiple album_artist_sort found for artist '{}': '{}' != '{}'",
artist.id, artist.meta.id,
artist.sort.as_ref().unwrap(), artist.meta.sort.as_ref().unwrap(),
artist_sort.as_ref().unwrap() artist_sort.as_ref().unwrap()
))); )));
} }
} else if artist_sort.is_some() { } else if artist_sort.is_some() {
artist.sort = artist_sort; artist.meta.sort = artist_sort;
} }
// Do a linear search as few artists have more than a handful of albums. Search from the // Do a linear search as few artists have more than a handful of albums. Search from the
@ -105,7 +105,7 @@ impl<Database, Library: ILibrary> MusicHoard<Database, Library> {
.albums .albums
.iter_mut() .iter_mut()
.rev() .rev()
.find(|album| album.id == album_id) .find(|album| album.meta.id == album_id)
{ {
Some(album) => album.tracks.push(track), Some(album) => album.tracks.push(track),
None => { None => {
@ -197,13 +197,13 @@ mod tests {
assert!(music_hoard.get_collection()[0] assert!(music_hoard.get_collection()[0]
.albums .albums
.iter() .iter()
.any(|album| album.id.title == "album_title a.a")); .any(|album| album.meta.id.title == "album_title a.a"));
music_hoard.rescan_library().unwrap(); music_hoard.rescan_library().unwrap();
assert!(!music_hoard.get_collection()[0] assert!(!music_hoard.get_collection()[0]
.albums .albums
.iter() .iter()
.any(|album| album.id.title == "album_title a.a")); .any(|album| album.meta.id.title == "album_title a.a"));
} }
#[test] #[test]
@ -234,8 +234,8 @@ mod tests {
let mut library = MockILibrary::new(); let mut library = MockILibrary::new();
let mut expected = LIBRARY_COLLECTION.to_owned(); let mut expected = LIBRARY_COLLECTION.to_owned();
let removed_album_id = expected[0].albums[0].id.clone(); let removed_album_id = expected[0].albums[0].meta.id.clone();
let clashed_album_id = &expected[1].albums[0].id; let clashed_album_id = &expected[1].albums[0].meta.id;
let mut items = LIBRARY_ITEMS.to_owned(); let mut items = LIBRARY_ITEMS.to_owned();
for item in items for item in items
@ -245,7 +245,7 @@ mod tests {
item.album_title = clashed_album_id.title.clone(); item.album_title = clashed_album_id.title.clone();
} }
expected[0].albums[0].id = clashed_album_id.clone(); expected[0].albums[0].meta.id = clashed_album_id.clone();
let library_input = Query::new(); let library_input = Query::new();
let library_result = Ok(items); let library_result = Ok(items);

View File

@ -2,8 +2,8 @@ use once_cell::sync::Lazy;
use std::collections::HashMap; use std::collections::HashMap;
use crate::core::collection::{ use crate::core::collection::{
album::{Album, AlbumId, AlbumPrimaryType, AlbumSeq}, album::{Album, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSeq},
artist::{Artist, ArtistId}, artist::{Artist, ArtistId, ArtistMeta},
musicbrainz::{MbAlbumRef, MbArtistRef}, musicbrainz::{MbAlbumRef, MbArtistRef},
track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality}, track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality},
}; };

View File

@ -83,7 +83,7 @@ mod tests {
let mut expected = FULL_COLLECTION.to_owned(); let mut expected = FULL_COLLECTION.to_owned();
for artist in expected.iter_mut() { for artist in expected.iter_mut() {
for album in artist.albums.iter_mut() { for album in artist.albums.iter_mut() {
album.date = AlbumDate::default(); album.meta.date = AlbumDate::default();
album.tracks.clear(); album.tracks.clear();
} }
} }

View File

@ -3,7 +3,7 @@ use std::{collections::HashMap, fmt};
use serde::{de::Visitor, Deserialize, Deserializer}; use serde::{de::Visitor, Deserialize, Deserializer};
use crate::{ use crate::{
collection::musicbrainz::Mbid, collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz::Mbid},
core::collection::{ core::collection::{
album::{Album, AlbumDate, AlbumId, AlbumSeq}, album::{Album, AlbumDate, AlbumId, AlbumSeq},
artist::{Artist, ArtistId}, artist::{Artist, ArtistId},
@ -86,10 +86,12 @@ impl<'de> Deserialize<'de> for DeserializeMbid {
impl From<DeserializeArtist> for Artist { impl From<DeserializeArtist> for Artist {
fn from(artist: DeserializeArtist) -> Self { fn from(artist: DeserializeArtist) -> Self {
Artist { Artist {
id: ArtistId::new(artist.name), meta: ArtistMeta {
sort: artist.sort.map(ArtistId::new), id: ArtistId::new(artist.name),
musicbrainz: artist.musicbrainz.map(Into::<Mbid>::into).map(Into::into), sort: artist.sort.map(ArtistId::new),
properties: artist.properties, musicbrainz: artist.musicbrainz.map(Into::<Mbid>::into).map(Into::into),
properties: artist.properties,
},
albums: artist.albums.into_iter().map(Into::into).collect(), albums: artist.albums.into_iter().map(Into::into).collect(),
} }
} }
@ -98,12 +100,14 @@ impl From<DeserializeArtist> for Artist {
impl From<DeserializeAlbum> for Album { impl From<DeserializeAlbum> for Album {
fn from(album: DeserializeAlbum) -> Self { fn from(album: DeserializeAlbum) -> Self {
Album { Album {
id: AlbumId { title: album.title }, meta: AlbumMeta {
date: AlbumDate::default(), id: AlbumId { title: album.title },
seq: AlbumSeq(album.seq), date: AlbumDate::default(),
musicbrainz: album.musicbrainz.map(Into::<Mbid>::into).map(Into::into), seq: AlbumSeq(album.seq),
primary_type: album.primary_type.map(Into::into), musicbrainz: album.musicbrainz.map(Into::<Mbid>::into).map(Into::into),
secondary_types: album.secondary_types.into_iter().map(Into::into).collect(), primary_type: album.primary_type.map(Into::into),
secondary_types: album.secondary_types.into_iter().map(Into::into).collect(),
},
tracks: vec![], tracks: vec![],
} }
} }

View File

@ -52,13 +52,15 @@ impl<'a> Serialize for SerializeMbid<'a> {
impl<'a> From<&'a Artist> for SerializeArtist<'a> { impl<'a> From<&'a Artist> for SerializeArtist<'a> {
fn from(artist: &'a Artist) -> Self { fn from(artist: &'a Artist) -> Self {
SerializeArtist { SerializeArtist {
name: &artist.id.name, name: &artist.meta.id.name,
sort: artist.sort.as_ref().map(|id| id.name.as_ref()), sort: artist.meta.sort.as_ref().map(|id| id.name.as_ref()),
musicbrainz: artist musicbrainz: artist
.meta
.musicbrainz .musicbrainz
.as_ref() .as_ref()
.map(|mbref| SerializeMbid(mbref.mbid())), .map(|mbref| SerializeMbid(mbref.mbid())),
properties: artist properties: artist
.meta
.properties .properties
.iter() .iter()
.map(|(k, v)| (k.as_ref(), v)) .map(|(k, v)| (k.as_ref(), v))
@ -71,14 +73,16 @@ impl<'a> From<&'a Artist> for SerializeArtist<'a> {
impl<'a> From<&'a Album> for SerializeAlbum<'a> { impl<'a> From<&'a Album> for SerializeAlbum<'a> {
fn from(album: &'a Album) -> Self { fn from(album: &'a Album) -> Self {
SerializeAlbum { SerializeAlbum {
title: &album.id.title, title: &album.meta.id.title,
seq: album.seq.0, seq: album.meta.seq.0,
musicbrainz: album musicbrainz: album
.meta
.musicbrainz .musicbrainz
.as_ref() .as_ref()
.map(|mbref| SerializeMbid(mbref.mbid())), .map(|mbref| SerializeMbid(mbref.mbid())),
primary_type: album.primary_type.map(Into::into), primary_type: album.meta.primary_type.map(Into::into),
secondary_types: album secondary_types: album
.meta
.secondary_types .secondary_types
.iter() .iter()
.copied() .copied()

View File

@ -2,35 +2,39 @@ macro_rules! full_collection {
() => { () => {
vec![ vec![
Artist { Artist {
id: ArtistId { meta: ArtistMeta {
name: "Album_Artist A".to_string(), id: ArtistId {
name: "Album_Artist A".to_string(),
},
sort: None,
musicbrainz: Some(MbArtistRef::from_url_str(
"https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000"
).unwrap()),
properties: HashMap::from([
(String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/000000000"),
]),
(String::from("Qobuz"), vec![
String::from(
"https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums",
)
]),
]),
}, },
sort: None,
musicbrainz: Some(MbArtistRef::from_url_str(
"https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000"
).unwrap()),
properties: HashMap::from([
(String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/000000000"),
]),
(String::from("Qobuz"), vec![
String::from(
"https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums",
)
]),
]),
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title a.a".to_string(), id: AlbumId {
title: "album_title a.a".to_string(),
},
date: 1998.into(),
seq: AlbumSeq(1),
musicbrainz: Some(MbAlbumRef::from_url_str(
"https://musicbrainz.org/release-group/00000000-0000-0000-0000-000000000000"
).unwrap()),
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
}, },
date: 1998.into(),
seq: AlbumSeq(1),
musicbrainz: Some(MbAlbumRef::from_url_str(
"https://musicbrainz.org/release-group/00000000-0000-0000-0000-000000000000"
).unwrap()),
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -82,14 +86,16 @@ macro_rules! full_collection {
], ],
}, },
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title a.b".to_string(), id: AlbumId {
title: "album_title a.b".to_string(),
},
date: (2015, 4).into(),
seq: AlbumSeq(1),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
}, },
date: (2015, 4).into(),
seq: AlbumSeq(1),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -118,37 +124,41 @@ macro_rules! full_collection {
], ],
}, },
Artist { Artist {
id: ArtistId { meta: ArtistMeta {
name: "Album_Artist B".to_string(), id: ArtistId {
name: "Album_Artist B".to_string(),
},
sort: None,
musicbrainz: Some(MbArtistRef::from_url_str(
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111"
).unwrap()),
properties: HashMap::from([
(String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/111111111"),
String::from("https://www.musicbutler.io/artist-page/111111112"),
]),
(String::from("Bandcamp"), vec![
String::from("https://artist-b.bandcamp.com/")
]),
(String::from("Qobuz"), vec![
String::from(
"https://www.qobuz.com/nl-nl/interpreter/artist-b/download-streaming-albums",
)
]),
]),
}, },
sort: None,
musicbrainz: Some(MbArtistRef::from_url_str(
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111"
).unwrap()),
properties: HashMap::from([
(String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/111111111"),
String::from("https://www.musicbutler.io/artist-page/111111112"),
]),
(String::from("Bandcamp"), vec![
String::from("https://artist-b.bandcamp.com/")
]),
(String::from("Qobuz"), vec![
String::from(
"https://www.qobuz.com/nl-nl/interpreter/artist-b/download-streaming-albums",
)
]),
]),
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title b.a".to_string(), id: AlbumId {
title: "album_title b.a".to_string(),
},
date: (2003, 6, 6).into(),
seq: AlbumSeq(1),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
}, },
date: (2003, 6, 6).into(),
seq: AlbumSeq(1),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -178,16 +188,18 @@ macro_rules! full_collection {
], ],
}, },
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title b.b".to_string(), id: AlbumId {
title: "album_title b.b".to_string(),
},
date: 2008.into(),
seq: AlbumSeq(3),
musicbrainz: Some(MbAlbumRef::from_url_str(
"https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111111"
).unwrap()),
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
}, },
date: 2008.into(),
seq: AlbumSeq(3),
musicbrainz: Some(MbAlbumRef::from_url_str(
"https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111111"
).unwrap()),
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -217,16 +229,18 @@ macro_rules! full_collection {
], ],
}, },
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title b.c".to_string(), id: AlbumId {
title: "album_title b.c".to_string(),
},
date: 2009.into(),
seq: AlbumSeq(2),
musicbrainz: Some(MbAlbumRef::from_url_str(
"https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111112"
).unwrap()),
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
}, },
date: 2009.into(),
seq: AlbumSeq(2),
musicbrainz: Some(MbAlbumRef::from_url_str(
"https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111112"
).unwrap()),
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -256,14 +270,16 @@ macro_rules! full_collection {
], ],
}, },
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title b.d".to_string(), id: AlbumId {
title: "album_title b.d".to_string(),
},
date: 2015.into(),
seq: AlbumSeq(4),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
}, },
date: 2015.into(),
seq: AlbumSeq(4),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -295,26 +311,30 @@ macro_rules! full_collection {
], ],
}, },
Artist { Artist {
id: ArtistId { meta: ArtistMeta {
name: "The Album_Artist C".to_string(), id: ArtistId {
name: "The Album_Artist C".to_string(),
},
sort: Some(ArtistId {
name: "Album_Artist C, The".to_string(),
}),
musicbrainz: Some(MbArtistRef::from_url_str(
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111"
).unwrap()),
properties: HashMap::new(),
}, },
sort: Some(ArtistId {
name: "Album_Artist C, The".to_string(),
}),
musicbrainz: Some(MbArtistRef::from_url_str(
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111"
).unwrap()),
properties: HashMap::new(),
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title c.a".to_string(), id: AlbumId {
title: "album_title c.a".to_string(),
},
date: 1985.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
}, },
date: 1985.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -344,14 +364,16 @@ macro_rules! full_collection {
], ],
}, },
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title c.b".to_string(), id: AlbumId {
title: "album_title c.b".to_string(),
},
date: 2018.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
}, },
date: 2018.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -383,22 +405,26 @@ macro_rules! full_collection {
], ],
}, },
Artist { Artist {
id: ArtistId { meta: ArtistMeta {
name: "Album_Artist D".to_string(), id: ArtistId {
name: "Album_Artist D".to_string(),
},
sort: None,
musicbrainz: None,
properties: HashMap::new(),
}, },
sort: None,
musicbrainz: None,
properties: HashMap::new(),
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title d.a".to_string(), id: AlbumId {
title: "album_title d.a".to_string(),
},
date: 1995.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
}, },
date: 1995.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -428,14 +454,16 @@ macro_rules! full_collection {
], ],
}, },
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title d.b".to_string(), id: AlbumId {
title: "album_title d.b".to_string(),
},
date: 2028.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
}, },
date: 2028.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {

View File

@ -3,22 +3,26 @@ macro_rules! library_collection {
() => { () => {
vec![ vec![
Artist { Artist {
id: ArtistId { meta: ArtistMeta {
name: "Album_Artist A".to_string(), id: ArtistId {
name: "Album_Artist A".to_string(),
},
sort: None,
musicbrainz: None,
properties: HashMap::new(),
}, },
sort: None,
musicbrainz: None,
properties: HashMap::new(),
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title a.a".to_string(), id: AlbumId {
title: "album_title a.a".to_string(),
},
date: 1998.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
}, },
date: 1998.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -70,14 +74,16 @@ macro_rules! library_collection {
], ],
}, },
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title a.b".to_string(), id: AlbumId {
title: "album_title a.b".to_string(),
},
date: (2015, 4).into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
}, },
date: (2015, 4).into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -106,22 +112,26 @@ macro_rules! library_collection {
], ],
}, },
Artist { Artist {
id: ArtistId { meta: ArtistMeta {
name: "Album_Artist B".to_string(), id: ArtistId {
name: "Album_Artist B".to_string(),
},
sort: None,
musicbrainz: None,
properties: HashMap::new(),
}, },
sort: None,
musicbrainz: None,
properties: HashMap::new(),
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title b.a".to_string(), id: AlbumId {
title: "album_title b.a".to_string(),
},
date: (2003, 6, 6).into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
}, },
date: (2003, 6, 6).into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -151,14 +161,16 @@ macro_rules! library_collection {
], ],
}, },
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title b.b".to_string(), id: AlbumId {
title: "album_title b.b".to_string(),
},
date: 2008.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
}, },
date: 2008.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -188,14 +200,16 @@ macro_rules! library_collection {
], ],
}, },
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title b.c".to_string(), id: AlbumId {
title: "album_title b.c".to_string(),
},
date: 2009.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
}, },
date: 2009.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -225,14 +239,16 @@ macro_rules! library_collection {
], ],
}, },
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title b.d".to_string(), id: AlbumId {
title: "album_title b.d".to_string(),
},
date: 2015.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
}, },
date: 2015.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -264,24 +280,28 @@ macro_rules! library_collection {
], ],
}, },
Artist { Artist {
id: ArtistId { meta: ArtistMeta {
name: "The Album_Artist C".to_string(), id: ArtistId {
name: "The Album_Artist C".to_string(),
},
sort: Some(ArtistId {
name: "Album_Artist C, The".to_string(),
}),
musicbrainz: None,
properties: HashMap::new(),
}, },
sort: Some(ArtistId {
name: "Album_Artist C, The".to_string(),
}),
musicbrainz: None,
properties: HashMap::new(),
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title c.a".to_string(), id: AlbumId {
title: "album_title c.a".to_string(),
},
date: 1985.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
}, },
date: 1985.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -311,14 +331,16 @@ macro_rules! library_collection {
], ],
}, },
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title c.b".to_string(), id: AlbumId {
title: "album_title c.b".to_string(),
},
date: 2018.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
}, },
date: 2018.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -350,22 +372,26 @@ macro_rules! library_collection {
], ],
}, },
Artist { Artist {
id: ArtistId { meta: ArtistMeta {
name: "Album_Artist D".to_string(), id: ArtistId {
name: "Album_Artist D".to_string(),
},
sort: None,
musicbrainz: None,
properties: HashMap::new(),
}, },
sort: None,
musicbrainz: None,
properties: HashMap::new(),
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title d.a".to_string(), id: AlbumId {
title: "album_title d.a".to_string(),
},
date: 1995.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
}, },
date: 1995.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -395,14 +421,16 @@ macro_rules! library_collection {
], ],
}, },
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: "album_title d.b".to_string(), id: AlbumId {
title: "album_title d.b".to_string(),
},
date: 2028.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
}, },
date: 2028.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {

View File

@ -93,19 +93,23 @@ impl IAppInteractBrowse for AppMachine<AppBrowse> {
let (matches_tx, matches_rx) = mpsc::channel::<AppMatchesInfo>(); let (matches_tx, matches_rx) = mpsc::channel::<AppMatchesInfo>();
match artist.musicbrainz { match artist.meta.musicbrainz {
Some(ref mbid) => { Some(ref mbid) => {
let arid = mbid.mbid(); let arid = mbid.mbid();
let mut album_iter = artist.albums.iter().peekable(); let mut album_iter = artist.albums.iter().peekable();
while let Some(album) = album_iter.next() { while let Some(album) = album_iter.next() {
if album.musicbrainz.is_some() { if album.meta.musicbrainz.is_some() {
continue; continue;
} }
match self.inner.musicbrainz.search_release_group(arid, album) { match self
.inner
.musicbrainz
.search_release_group(arid, &album.meta)
{
Ok(list) => matches_tx Ok(list) => matches_tx
.send(AppMatchesInfo::album(album.clone(), list)) .send(AppMatchesInfo::album(album.meta.clone(), list))
.expect("send fails only if receiver is disconnected"), .expect("send fails only if receiver is disconnected"),
Err(err) => return AppMachine::error(self.inner, err.to_string()).into(), Err(err) => return AppMachine::error(self.inner, err.to_string()).into(),
} }
@ -115,9 +119,9 @@ impl IAppInteractBrowse for AppMachine<AppBrowse> {
} }
} }
} }
None => match self.inner.musicbrainz.search_artist(artist) { None => match self.inner.musicbrainz.search_artist(&artist.meta) {
Ok(list) => matches_tx Ok(list) => matches_tx
.send(AppMatchesInfo::artist(artist.clone(), list)) .send(AppMatchesInfo::artist(artist.meta.clone(), list))
.expect("send fails only if receiver is disconnected"), .expect("send fails only if receiver is disconnected"),
Err(err) => return AppMachine::error(self.inner, err.to_string()).into(), Err(err) => return AppMachine::error(self.inner, err.to_string()).into(),
}, },
@ -134,7 +138,7 @@ impl IAppInteractBrowse for AppMachine<AppBrowse> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use mockall::{predicate, Sequence}; use mockall::{predicate, Sequence};
use musichoard::collection::{album::Album, artist::Artist, musicbrainz::Mbid}; use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz::Mbid};
use crate::tui::{ use crate::tui::{
app::{ app::{
@ -223,8 +227,8 @@ mod tests {
let mut mb_api = Box::new(MockIMusicBrainz::new()); let mut mb_api = Box::new(MockIMusicBrainz::new());
let arid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap(); let arid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
let album_1 = COLLECTION[1].albums[0].clone(); let album_1 = COLLECTION[1].albums[0].meta.clone();
let album_4 = COLLECTION[1].albums[3].clone(); let album_4 = COLLECTION[1].albums[3].meta.clone();
let album_match_1_1 = Match::new(100, album_1.clone()); let album_match_1_1 = Match::new(100, album_1.clone());
let album_match_1_2 = Match::new(50, album_4.clone()); let album_match_1_2 = Match::new(50, album_4.clone());
@ -233,8 +237,8 @@ mod tests {
let matches_1 = vec![album_match_1_1.clone(), album_match_1_2.clone()]; let matches_1 = vec![album_match_1_1.clone(), album_match_1_2.clone()];
let matches_4 = vec![album_match_4_1.clone(), album_match_4_2.clone()]; let matches_4 = vec![album_match_4_1.clone(), album_match_4_2.clone()];
let result_1: Result<Vec<Match<Album>>, musicbrainz::Error> = Ok(matches_1.clone()); let result_1: Result<Vec<Match<AlbumMeta>>, musicbrainz::Error> = Ok(matches_1.clone());
let result_4: Result<Vec<Match<Album>>, musicbrainz::Error> = Ok(matches_4.clone()); let result_4: Result<Vec<Match<AlbumMeta>>, musicbrainz::Error> = Ok(matches_4.clone());
// Other albums have an MBID and so they will be skipped. // Other albums have an MBID and so they will be skipped.
let mut seq = Sequence::new(); let mut seq = Sequence::new();
@ -263,7 +267,7 @@ mod tests {
let public_matches = public.state.unwrap_matches(); let public_matches = public.state.unwrap_matches();
let mut matches_1: Vec<MatchOption<Album>> = let mut matches_1: Vec<MatchOption<AlbumMeta>> =
matches_1.into_iter().map(Into::into).collect(); matches_1.into_iter().map(Into::into).collect();
matches_1.push(MatchOption::CannotHaveMbid); matches_1.push(MatchOption::CannotHaveMbid);
let expected = Some(AppMatchesInfo::Album(AppAlbumMatches { let expected = Some(AppMatchesInfo::Album(AppAlbumMatches {
@ -279,7 +283,7 @@ mod tests {
let public_matches = public.state.unwrap_matches(); let public_matches = public.state.unwrap_matches();
let mut matches_4: Vec<MatchOption<Album>> = let mut matches_4: Vec<MatchOption<AlbumMeta>> =
matches_4.into_iter().map(Into::into).collect(); matches_4.into_iter().map(Into::into).collect();
matches_4.push(MatchOption::CannotHaveMbid); matches_4.push(MatchOption::CannotHaveMbid);
let expected = Some(AppMatchesInfo::Album(AppAlbumMatches { let expected = Some(AppMatchesInfo::Album(AppAlbumMatches {
@ -303,13 +307,13 @@ mod tests {
fn fetch_musicbrainz_no_artist_mbid() { fn fetch_musicbrainz_no_artist_mbid() {
let mut mb_api = Box::new(MockIMusicBrainz::new()); let mut mb_api = Box::new(MockIMusicBrainz::new());
let artist = COLLECTION[3].clone(); let artist = COLLECTION[3].meta.clone();
let artist_match_1 = Match::new(100, artist.clone()); let artist_match_1 = Match::new(100, artist.clone());
let artist_match_2 = Match::new(50, artist.clone()); let artist_match_2 = Match::new(50, artist.clone());
let matches = vec![artist_match_1.clone(), artist_match_2.clone()]; let matches = vec![artist_match_1.clone(), artist_match_2.clone()];
let result: Result<Vec<Match<Artist>>, musicbrainz::Error> = Ok(matches.clone()); let result: Result<Vec<Match<ArtistMeta>>, musicbrainz::Error> = Ok(matches.clone());
mb_api mb_api
.expect_search_artist() .expect_search_artist()
@ -330,7 +334,8 @@ mod tests {
let public_matches = public.state.unwrap_matches(); let public_matches = public.state.unwrap_matches();
let mut matches: Vec<MatchOption<Artist>> = matches.into_iter().map(Into::into).collect(); let mut matches: Vec<MatchOption<ArtistMeta>> =
matches.into_iter().map(Into::into).collect();
matches.push(MatchOption::CannotHaveMbid); matches.push(MatchOption::CannotHaveMbid);
let expected = Some(AppMatchesInfo::Artist(AppArtistMatches { let expected = Some(AppMatchesInfo::Artist(AppArtistMatches {
matching: artist.clone(), matching: artist.clone(),

View File

@ -144,8 +144,8 @@ impl IAppInteractMatchesPrivate for AppMatches {
mod tests { mod tests {
use mpsc::Receiver; use mpsc::Receiver;
use musichoard::collection::{ use musichoard::collection::{
album::{Album, AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType}, album::{AlbumDate, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType},
artist::{Artist, ArtistId}, artist::{ArtistId, ArtistMeta},
}; };
use crate::tui::{ use crate::tui::{
@ -158,20 +158,30 @@ mod tests {
use super::*; use super::*;
impl<T> Match<T> {
pub fn new(score: u8, item: T) -> Self {
Match {
score,
item,
disambiguation: None,
}
}
}
fn artist_matches_info_vec() -> Vec<AppMatchesInfo> { fn artist_matches_info_vec() -> Vec<AppMatchesInfo> {
let artist_1 = Artist::new(ArtistId::new("Artist 1")); let artist_1 = ArtistMeta::new(ArtistId::new("Artist 1"));
let artist_1_1 = artist_1.clone(); let artist_1_1 = artist_1.clone();
let artist_match_1_1 = Match::new(100, artist_1_1); let artist_match_1_1 = Match::new(100, artist_1_1);
let artist_1_2 = artist_1.clone(); let artist_1_2 = artist_1.clone();
let mut artist_match_1_2 = Match::new(100, artist_1_2); let mut artist_match_1_2 = Match::new(100, artist_1_2);
artist_match_1_2.set_disambiguation("some disambiguation"); artist_match_1_2.disambiguation = Some(String::from("some disambiguation"));
let list = vec![artist_match_1_1.clone(), artist_match_1_2.clone()]; let list = vec![artist_match_1_1.clone(), artist_match_1_2.clone()];
let matches_info_1 = AppMatchesInfo::artist(artist_1.clone(), list); let matches_info_1 = AppMatchesInfo::artist(artist_1.clone(), list);
let artist_2 = Artist::new(ArtistId::new("Artist 2")); let artist_2 = ArtistMeta::new(ArtistId::new("Artist 2"));
let artist_2_1 = artist_1.clone(); let artist_2_1 = artist_1.clone();
let album_match_2_1 = Match::new(100, artist_2_1); let album_match_2_1 = Match::new(100, artist_2_1);
@ -183,7 +193,7 @@ mod tests {
} }
fn album_matches_info_vec() -> Vec<AppMatchesInfo> { fn album_matches_info_vec() -> Vec<AppMatchesInfo> {
let album_1 = Album::new( let album_1 = AlbumMeta::new(
AlbumId::new("Album 1"), AlbumId::new("Album 1"),
AlbumDate::new(Some(1990), Some(5), None), AlbumDate::new(Some(1990), Some(5), None),
Some(AlbumPrimaryType::Album), Some(AlbumPrimaryType::Album),
@ -201,7 +211,7 @@ mod tests {
let list = vec![album_match_1_1.clone(), album_match_1_2.clone()]; let list = vec![album_match_1_1.clone(), album_match_1_2.clone()];
let matches_info_1 = AppMatchesInfo::album(album_1.clone(), list); let matches_info_1 = AppMatchesInfo::album(album_1.clone(), list);
let album_2 = Album::new( let album_2 = AlbumMeta::new(
AlbumId::new("Album 2"), AlbumId::new("Album 2"),
AlbumDate::new(Some(2001), None, None), AlbumDate::new(Some(2001), None, None),
Some(AlbumPrimaryType::Album), Some(AlbumPrimaryType::Album),

View File

@ -181,10 +181,10 @@ impl IAppInteractSearchPrivate for AppMachine<AppSearch> {
} }
fn predicate_artists(case_sens: bool, char_sens: bool, search: &str, probe: &Artist) -> bool { fn predicate_artists(case_sens: bool, char_sens: bool, search: &str, probe: &Artist) -> bool {
let name = Self::normalize_search(&probe.id.name, !case_sens, !char_sens); let name = Self::normalize_search(&probe.meta.id.name, !case_sens, !char_sens);
let mut result = name.starts_with(search); let mut result = name.starts_with(search);
if let Some(ref probe_sort) = probe.sort { if let Some(ref probe_sort) = probe.meta.sort {
if !result { if !result {
let name = Self::normalize_search(&probe_sort.name, !case_sens, !char_sens); let name = Self::normalize_search(&probe_sort.name, !case_sens, !char_sens);
result = name.starts_with(search); result = name.starts_with(search);
@ -195,7 +195,7 @@ impl IAppInteractSearchPrivate for AppMachine<AppSearch> {
} }
fn predicate_albums(case_sens: bool, char_sens: bool, search: &str, probe: &Album) -> bool { fn predicate_albums(case_sens: bool, char_sens: bool, search: &str, probe: &Album) -> bool {
Self::predicate_title(case_sens, char_sens, search, &probe.id.title) Self::predicate_title(case_sens, char_sens, search, &probe.meta.id.title)
} }
fn predicate_tracks(case_sens: bool, char_sens: bool, search: &str, probe: &Track) -> bool { fn predicate_tracks(case_sens: bool, char_sens: bool, search: &str, probe: &Track) -> bool {

View File

@ -4,7 +4,7 @@ mod selection;
pub use machine::App; pub use machine::App;
pub use selection::{Category, Delta, Selection, WidgetState}; pub use selection::{Category, Delta, Selection, WidgetState};
use musichoard::collection::{album::Album, artist::Artist, Collection}; use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, Collection};
use crate::tui::lib::interface::musicbrainz::Match; use crate::tui::lib::interface::musicbrainz::Match;
@ -143,14 +143,14 @@ impl<T> From<Match<T>> for MatchOption<T> {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct AppArtistMatches { pub struct AppArtistMatches {
pub matching: Artist, pub matching: ArtistMeta,
pub list: Vec<MatchOption<Artist>>, pub list: Vec<MatchOption<ArtistMeta>>,
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct AppAlbumMatches { pub struct AppAlbumMatches {
pub matching: Album, pub matching: AlbumMeta,
pub list: Vec<MatchOption<Album>>, pub list: Vec<MatchOption<AlbumMeta>>,
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -160,13 +160,13 @@ pub enum AppMatchesInfo {
} }
impl AppMatchesInfo { impl AppMatchesInfo {
pub fn artist<M: Into<MatchOption<Artist>>>(matching: Artist, list: Vec<M>) -> Self { pub fn artist<M: Into<MatchOption<ArtistMeta>>>(matching: ArtistMeta, list: Vec<M>) -> Self {
let list: Vec<MatchOption<Artist>> = list.into_iter().map(Into::into).collect(); let list: Vec<MatchOption<ArtistMeta>> = list.into_iter().map(Into::into).collect();
AppMatchesInfo::Artist(AppArtistMatches { matching, list }) AppMatchesInfo::Artist(AppArtistMatches { matching, list })
} }
pub fn album<M: Into<MatchOption<Album>>>(matching: Album, list: Vec<M>) -> Self { pub fn album<M: Into<MatchOption<AlbumMeta>>>(matching: AlbumMeta, list: Vec<M>) -> Self {
let list: Vec<MatchOption<Album>> = list.into_iter().map(Into::into).collect(); let list: Vec<MatchOption<AlbumMeta>> = list.into_iter().map(Into::into).collect();
AppMatchesInfo::Album(AppAlbumMatches { matching, list }) AppMatchesInfo::Album(AppAlbumMatches { matching, list })
} }
} }

View File

@ -28,7 +28,8 @@ impl AlbumSelection {
pub fn reinitialise(&mut self, albums: &[Album], album: Option<KeySelectAlbum>) { pub fn reinitialise(&mut self, albums: &[Album], album: Option<KeySelectAlbum>) {
if let Some(album) = album { if let Some(album) = album {
let result = albums.binary_search_by(|a| a.get_sort_key().cmp(&album.get_sort_key())); let result =
albums.binary_search_by(|a| a.meta.get_sort_key().cmp(&album.get_sort_key()));
match result { match result {
Ok(index) => self.reinitialise_with_index(albums, index, album.track), Ok(index) => self.reinitialise_with_index(albums, index, album.track),
Err(index) => self.reinitialise_with_index(albums, index, None), Err(index) => self.reinitialise_with_index(albums, index, None),
@ -169,7 +170,7 @@ impl KeySelectAlbum {
pub fn get(albums: &[Album], selection: &AlbumSelection) -> Option<Self> { pub fn get(albums: &[Album], selection: &AlbumSelection) -> Option<Self> {
selection.state.list.selected().map(|index| { selection.state.list.selected().map(|index| {
let album = &albums[index]; let album = &albums[index];
let key = album.get_sort_key(); let key = album.meta.get_sort_key();
KeySelectAlbum { KeySelectAlbum {
key: (key.0.to_owned(), key.1.to_owned(), key.2.to_owned()), key: (key.0.to_owned(), key.1.to_owned(), key.2.to_owned()),
track: KeySelectTrack::get(&album.tracks, &selection.track), track: KeySelectTrack::get(&album.tracks, &selection.track),

View File

@ -29,7 +29,8 @@ impl ArtistSelection {
pub fn reinitialise(&mut self, artists: &[Artist], active: Option<KeySelectArtist>) { pub fn reinitialise(&mut self, artists: &[Artist], active: Option<KeySelectArtist>) {
if let Some(active) = active { if let Some(active) = active {
let result = artists.binary_search_by(|a| a.get_sort_key().cmp(&active.get_sort_key())); let result =
artists.binary_search_by(|a| a.meta.get_sort_key().cmp(&active.get_sort_key()));
match result { match result {
Ok(index) => self.reinitialise_with_index(artists, index, active.album), Ok(index) => self.reinitialise_with_index(artists, index, active.album),
Err(index) => self.reinitialise_with_index(artists, index, None), Err(index) => self.reinitialise_with_index(artists, index, None),
@ -202,7 +203,7 @@ impl KeySelectArtist {
pub fn get(artists: &[Artist], selection: &ArtistSelection) -> Option<Self> { pub fn get(artists: &[Artist], selection: &ArtistSelection) -> Option<Self> {
selection.state.list.selected().map(|index| { selection.state.list.selected().map(|index| {
let artist = &artists[index]; let artist = &artists[index];
let key = artist.get_sort_key(); let key = artist.meta.get_sort_key();
KeySelectArtist { KeySelectArtist {
key: (key.0.to_owned(),), key: (key.0.to_owned(),),
album: KeySelectAlbum::get(&artist.albums, &selection.album), album: KeySelectAlbum::get(&artist.albums, &selection.album),

View File

@ -1,9 +1,11 @@
//! Module for interacting with the [MusicBrainz API](https://musicbrainz.org/doc/MusicBrainz_API). //! Module for interacting with the [MusicBrainz API](https://musicbrainz.org/doc/MusicBrainz_API).
use std::collections::HashMap;
use musichoard::{ use musichoard::{
collection::{ collection::{
album::{Album, AlbumDate}, album::{AlbumDate, AlbumMeta, AlbumSeq},
artist::Artist, artist::ArtistMeta,
musicbrainz::Mbid, musicbrainz::Mbid,
}, },
external::musicbrainz::{ external::musicbrainz::{
@ -32,7 +34,7 @@ impl<Http> MusicBrainz<Http> {
} }
impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> { impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
fn search_artist(&mut self, artist: &Artist) -> Result<Vec<Match<Artist>>, Error> { fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error> {
let query = SearchArtistRequest::new().string(&artist.id.name); let query = SearchArtistRequest::new().string(&artist.id.name);
let mb_response = self.client.search_artist(query)?; let mb_response = self.client.search_artist(query)?;
@ -47,8 +49,8 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
fn search_release_group( fn search_release_group(
&mut self, &mut self,
arid: &Mbid, arid: &Mbid,
album: &Album, album: &AlbumMeta,
) -> Result<Vec<Match<Album>>, Error> { ) -> Result<Vec<Match<AlbumMeta>>, Error> {
// Some release groups may have a promotional early release messing up the search. Searching // Some release groups may have a promotional early release messing up the search. Searching
// with just the year should be enough anyway. // with just the year should be enough anyway.
let date = AlbumDate::new(album.date.year, None, None); let date = AlbumDate::new(album.date.year, None, None);
@ -70,31 +72,33 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
} }
} }
fn from_search_artist_response_artist(entity: SearchArtistResponseArtist) -> Match<Artist> { fn from_search_artist_response_artist(entity: SearchArtistResponseArtist) -> Match<ArtistMeta> {
let mut artist = Artist::new(entity.name); Match {
if let Some(sort) = entity.sort { score: entity.score,
artist.set_sort_key(sort); item: ArtistMeta {
id: entity.name,
sort: entity.sort.map(Into::into),
musicbrainz: Some(entity.id.into()),
properties: HashMap::new(),
},
disambiguation: entity.disambiguation,
} }
artist.set_musicbrainz_ref(entity.id.into());
let mut artist_match = Match::new(entity.score, artist);
if let Some(disambiguation) = entity.disambiguation {
artist_match.set_disambiguation(disambiguation);
}
artist_match
} }
fn from_search_release_group_response_release_group( fn from_search_release_group_response_release_group(
entity: SearchReleaseGroupResponseReleaseGroup, entity: SearchReleaseGroupResponseReleaseGroup,
) -> Match<Album> { ) -> Match<AlbumMeta> {
let mut album = Album::new( Match {
entity.title, score: entity.score,
entity.first_release_date, item: AlbumMeta {
Some(entity.primary_type), id: entity.title,
entity.secondary_types.unwrap_or_default(), date: entity.first_release_date,
); seq: AlbumSeq::default(),
album.set_musicbrainz_ref(entity.id.into()); musicbrainz: Some(entity.id.into()),
Match::new(entity.score, album) primary_type: Some(entity.primary_type),
secondary_types: entity.secondary_types.unwrap_or_default(),
},
disambiguation: None,
}
} }
// GRCOV_EXCL_STOP // GRCOV_EXCL_STOP

View File

@ -3,17 +3,17 @@
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
use musichoard::collection::{album::Album, artist::Artist, musicbrainz::Mbid}; use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta, musicbrainz::Mbid};
/// Trait for interacting with the MusicBrainz API. /// Trait for interacting with the MusicBrainz API.
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait IMusicBrainz { pub trait IMusicBrainz {
fn search_artist(&mut self, name: &Artist) -> Result<Vec<Match<Artist>>, Error>; fn search_artist(&mut self, name: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error>;
fn search_release_group( fn search_release_group(
&mut self, &mut self,
arid: &Mbid, arid: &Mbid,
album: &Album, album: &AlbumMeta,
) -> Result<Vec<Match<Album>>, Error>; ) -> Result<Vec<Match<AlbumMeta>>, Error>;
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -23,18 +23,4 @@ pub struct Match<T> {
pub disambiguation: Option<String>, pub disambiguation: Option<String>,
} }
impl<T> Match<T> {
pub fn new(score: u8, item: T) -> Self {
Match {
score,
item,
disambiguation: None,
}
}
pub fn set_disambiguation<S: Into<String>>(&mut self, disambiguation: S) {
self.disambiguation = Some(disambiguation.into())
}
}
pub type Error = musichoard::external::musicbrainz::api::Error; pub type Error = musichoard::external::musicbrainz::api::Error;

View File

@ -1,8 +1,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use musichoard::collection::{ use musichoard::collection::{
album::{Album, AlbumId, AlbumPrimaryType, AlbumSeq}, album::{Album, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSeq},
artist::{Artist, ArtistId}, artist::{Artist, ArtistId, ArtistMeta},
musicbrainz::{MbAlbumRef, MbArtistRef}, musicbrainz::{MbAlbumRef, MbArtistRef},
track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality}, track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality},
}; };

View File

@ -120,7 +120,7 @@ impl<'a, 'b> ArtistState<'a, 'b> {
let list = List::new( let list = List::new(
artists artists
.iter() .iter()
.map(|a| ListItem::new(a.id.name.as_str())) .map(|a| ListItem::new(a.meta.id.name.as_str()))
.collect::<Vec<ListItem>>(), .collect::<Vec<ListItem>>(),
); );
@ -158,12 +158,12 @@ impl<'a, 'b> AlbumState<'a, 'b> {
Date: {}\n\ Date: {}\n\
Type: {}\n\ Type: {}\n\
Status: {}", Status: {}",
album.map(|a| a.id.title.as_str()).unwrap_or(""), album.map(|a| a.meta.id.title.as_str()).unwrap_or(""),
album album
.map(|a| UiDisplay::display_date(&a.date, &a.seq)) .map(|a| UiDisplay::display_date(&a.meta.date, &a.meta.seq))
.unwrap_or_default(), .unwrap_or_default(),
album album
.map(|a| UiDisplay::display_type(&a.primary_type, &a.secondary_types)) .map(|a| UiDisplay::display_type(&a.meta.primary_type, &a.meta.secondary_types))
.unwrap_or_default(), .unwrap_or_default(),
album album
.map(|a| UiDisplay::display_album_status(&a.get_status())) .map(|a| UiDisplay::display_album_status(&a.get_status()))
@ -180,10 +180,10 @@ impl<'a, 'b> AlbumState<'a, 'b> {
fn to_list_item(album: &Album) -> ListItem { fn to_list_item(album: &Album) -> ListItem {
let line = match album.get_status() { let line = match album.get_status() {
AlbumStatus::None => Line::raw(album.id.title.as_str()), AlbumStatus::None => Line::raw(album.meta.id.title.as_str()),
AlbumStatus::Owned(format) => match format { AlbumStatus::Owned(format) => match format {
TrackFormat::Mp3 => Line::styled(album.id.title.as_str(), UiColor::FG_WARN), TrackFormat::Mp3 => Line::styled(album.meta.id.title.as_str(), UiColor::FG_WARN),
TrackFormat::Flac => Line::styled(album.id.title.as_str(), UiColor::FG_GOOD), TrackFormat::Flac => Line::styled(album.meta.id.title.as_str(), UiColor::FG_GOOD),
}, },
}; };
ListItem::new(line) ListItem::new(line)

View File

@ -1,6 +1,6 @@
use musichoard::collection::{ use musichoard::collection::{
album::{Album, AlbumDate, AlbumPrimaryType, AlbumSecondaryType, AlbumSeq, AlbumStatus}, album::{AlbumDate, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType, AlbumSeq, AlbumStatus},
artist::Artist, artist::ArtistMeta,
track::{TrackFormat, TrackQuality}, track::{TrackFormat, TrackQuality},
}; };
@ -98,11 +98,11 @@ impl UiDisplay {
} }
} }
pub fn display_artist_matching(artist: &Artist) -> String { pub fn display_artist_matching(artist: &ArtistMeta) -> String {
format!("Matching artist: {}", &artist.id.name) format!("Matching artist: {}", &artist.id.name)
} }
pub fn display_album_matching(album: &Album) -> String { pub fn display_album_matching(album: &AlbumMeta) -> String {
format!( format!(
"Matching album: {} | {}", "Matching album: {} | {}",
UiDisplay::display_album_date(&album.date), UiDisplay::display_album_date(&album.date),
@ -124,7 +124,7 @@ impl UiDisplay {
} }
} }
pub fn display_artist_match(match_option: &MatchOption<Artist>) -> String { pub fn display_artist_match(match_option: &MatchOption<ArtistMeta>) -> String {
match match_option { match match_option {
MatchOption::Match(match_artist) => format!( MatchOption::Match(match_artist) => format!(
"{}{} ({}%)", "{}{} ({}%)",
@ -140,7 +140,7 @@ impl UiDisplay {
} }
} }
pub fn display_album_match(match_option: &MatchOption<Album>) -> String { pub fn display_album_match(match_option: &MatchOption<AlbumMeta>) -> String {
match match_option { match match_option {
MatchOption::Match(match_album) => format!( MatchOption::Match(match_album) => format!(
"{:010} | {} [{}] ({}%)", "{:010} | {} [{}] ({}%)",

View File

@ -72,12 +72,12 @@ impl<'a> ArtistOverlay<'a> {
"Artist: {}\n\n{item_indent}\ "Artist: {}\n\n{item_indent}\
MusicBrainz: {}\n{item_indent}\ MusicBrainz: {}\n{item_indent}\
Properties: {}", Properties: {}",
artist.map(|a| a.id.name.as_str()).unwrap_or(""), artist.map(|a| a.meta.id.name.as_str()).unwrap_or(""),
artist artist
.and_then(|a| a.musicbrainz.as_ref().map(|mb| mb.url().as_str())) .and_then(|a| a.meta.musicbrainz.as_ref().map(|mb| mb.url().as_str()))
.unwrap_or(""), .unwrap_or(""),
Self::opt_hashmap_to_string( Self::opt_hashmap_to_string(
artist.map(|a| &a.properties), artist.map(|a| &a.meta.properties),
&double_item_indent, &double_item_indent,
&double_list_indent &double_list_indent
), ),
@ -100,9 +100,9 @@ impl<'a> AlbumOverlay<'a> {
let properties = Paragraph::new(format!( let properties = Paragraph::new(format!(
"Album: {}\n\n{item_indent}\ "Album: {}\n\n{item_indent}\
MusicBrainz: {}", MusicBrainz: {}",
album.map(|a| a.id.title.as_str()).unwrap_or(""), album.map(|a| a.meta.id.title.as_str()).unwrap_or(""),
album album
.and_then(|a| a.musicbrainz.as_ref().map(|mb| mb.url().as_str())) .and_then(|a| a.meta.musicbrainz.as_ref().map(|mb| mb.url().as_str()))
.unwrap_or(""), .unwrap_or(""),
)); ));

View File

@ -1,4 +1,4 @@
use musichoard::collection::{album::Album, artist::Artist}; use musichoard::collection::{album::AlbumMeta, artist::ArtistMeta};
use ratatui::widgets::{List, ListItem}; use ratatui::widgets::{List, ListItem};
use crate::tui::{ use crate::tui::{
@ -32,8 +32,8 @@ impl<'a, 'b> MatchesState<'a, 'b> {
} }
fn artists( fn artists(
matching: &Artist, matching: &ArtistMeta,
matches: &[MatchOption<Artist>], matches: &[MatchOption<ArtistMeta>],
state: &'b mut WidgetState, state: &'b mut WidgetState,
) -> Self { ) -> Self {
let matching = UiDisplay::display_artist_matching(matching); let matching = UiDisplay::display_artist_matching(matching);
@ -54,8 +54,8 @@ impl<'a, 'b> MatchesState<'a, 'b> {
} }
fn albums( fn albums(
matching: &Album, matching: &AlbumMeta,
matches: &[MatchOption<Album>], matches: &[MatchOption<AlbumMeta>],
state: &'b mut WidgetState, state: &'b mut WidgetState,
) -> Self { ) -> Self {
let matching = UiDisplay::display_album_matching(matching); let matching = UiDisplay::display_album_matching(matching);

View File

@ -177,8 +177,8 @@ impl IUi for Ui {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use musichoard::collection::{ use musichoard::collection::{
album::{AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType}, album::{AlbumDate, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType},
artist::{Artist, ArtistId}, artist::{Artist, ArtistId, ArtistMeta},
}; };
use crate::tui::{ use crate::tui::{
@ -224,14 +224,14 @@ mod tests {
} }
} }
fn artist_matches(matching: Artist, list: Vec<Match<Artist>>) -> AppMatchesInfo { fn artist_matches(matching: ArtistMeta, list: Vec<Match<ArtistMeta>>) -> AppMatchesInfo {
let mut list: Vec<MatchOption<Artist>> = list.into_iter().map(Into::into).collect(); let mut list: Vec<MatchOption<ArtistMeta>> = list.into_iter().map(Into::into).collect();
list.push(MatchOption::CannotHaveMbid); list.push(MatchOption::CannotHaveMbid);
AppMatchesInfo::artist(matching, list) AppMatchesInfo::artist(matching, list)
} }
fn album_matches(matching: Album, list: Vec<Match<Album>>) -> AppMatchesInfo { fn album_matches(matching: AlbumMeta, list: Vec<Match<AlbumMeta>>) -> AppMatchesInfo {
let mut list: Vec<MatchOption<Album>> = list.into_iter().map(Into::into).collect(); let mut list: Vec<MatchOption<AlbumMeta>> = list.into_iter().map(Into::into).collect();
list.push(MatchOption::CannotHaveMbid); list.push(MatchOption::CannotHaveMbid);
AppMatchesInfo::album(matching, list) AppMatchesInfo::album(matching, list)
} }
@ -328,7 +328,7 @@ mod tests {
let mut terminal = terminal(); let mut terminal = terminal();
let artist = Artist::new(ArtistId::new("an artist")); let artist = ArtistMeta::new(ArtistId::new("an artist"));
let artist_match = Match { let artist_match = Match {
score: 80, score: 80,
item: artist.clone(), item: artist.clone(),
@ -357,7 +357,7 @@ mod tests {
let mut terminal = terminal(); let mut terminal = terminal();
let album = Album::new( let album = AlbumMeta::new(
AlbumId::new("An Album"), AlbumId::new("An Album"),
AlbumDate::new(Some(1990), Some(5), None), AlbumDate::new(Some(1990), Some(5), None),
Some(AlbumPrimaryType::Album), Some(AlbumPrimaryType::Album),

View File

@ -18,7 +18,7 @@ fn expected() -> Collection {
let mut expected = COLLECTION.to_owned(); let mut expected = COLLECTION.to_owned();
for artist in expected.iter_mut() { for artist in expected.iter_mut() {
for album in artist.albums.iter_mut() { for album in artist.albums.iter_mut() {
album.date = AlbumDate::default(); album.meta.date = AlbumDate::default();
album.tracks.clear(); album.tracks.clear();
} }
} }

View File

@ -2,8 +2,8 @@ use once_cell::sync::Lazy;
use std::collections::HashMap; use std::collections::HashMap;
use musichoard::collection::{ use musichoard::collection::{
album::{Album, AlbumId, AlbumPrimaryType, AlbumSecondaryType, AlbumSeq}, album::{Album, AlbumId, AlbumMeta, AlbumPrimaryType, AlbumSecondaryType, AlbumSeq},
artist::{Artist, ArtistId}, artist::{Artist, ArtistId, ArtistMeta},
musicbrainz::MbArtistRef, musicbrainz::MbArtistRef,
track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality}, track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality},
Collection, Collection,
@ -12,35 +12,39 @@ use musichoard::collection::{
pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection { pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
vec![ vec![
Artist { Artist {
id: ArtistId { meta: ArtistMeta {
name: String::from("Аркона"), id: ArtistId {
}, name: String::from("Аркона"),
sort: Some(ArtistId{ },
name: String::from("Arkona") sort: Some(ArtistId{
}), name: String::from("Arkona")
musicbrainz: Some(MbArtistRef::from_url_str( }),
"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212" musicbrainz: Some(MbArtistRef::from_url_str(
).unwrap()), "https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212"
properties: HashMap::from([ ).unwrap()),
(String::from("MusicButler"), vec![ properties: HashMap::from([
String::from("https://www.musicbutler.io/artist-page/283448581"), (String::from("MusicButler"), vec![
]), String::from("https://www.musicbutler.io/artist-page/283448581"),
(String::from("Bandcamp"), vec![ ]),
String::from("https://arkonamoscow.bandcamp.com/"), (String::from("Bandcamp"), vec![
]), String::from("https://arkonamoscow.bandcamp.com/"),
(String::from("Qobuz"), vec![String::from( ]),
"https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums", (String::from("Qobuz"), vec![String::from(
)]), "https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums",
]), )]),
albums: vec![Album { ]),
id: AlbumId { },
title: String::from("Slovo"), albums: vec![Album {
meta: AlbumMeta {
id: AlbumId {
title: String::from("Slovo"),
},
date: 2011.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
}, },
date: 2011.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -200,31 +204,35 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
}], }],
}, },
Artist { Artist {
id: ArtistId { meta: ArtistMeta {
name: String::from("Eluveitie"), id: ArtistId {
}, name: String::from("Eluveitie"),
sort: None, },
musicbrainz: Some(MbArtistRef::from_url_str( sort: None,
"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38" musicbrainz: Some(MbArtistRef::from_url_str(
).unwrap()), "https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38"
properties: HashMap::from([ ).unwrap()),
(String::from("MusicButler"), vec![ properties: HashMap::from([
String::from("https://www.musicbutler.io/artist-page/269358403"), (String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/269358403"),
]),
(String::from("Qobuz"), vec![String::from(
"https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums",
)]),
]), ]),
(String::from("Qobuz"), vec![String::from( },
"https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums",
)]),
]),
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: String::from("Vên [rerecorded]"), id: AlbumId {
title: String::from("Vên [rerecorded]"),
},
date: 2004.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Ep),
secondary_types: vec![],
}, },
date: 2004.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Ep),
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -295,14 +303,16 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
], ],
}, },
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: String::from("Slania"), id: AlbumId {
title: String::from("Slania"),
},
date: 2008.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
}, },
date: 2008.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -441,30 +451,34 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
], ],
}, },
Artist { Artist {
id: ArtistId { meta: ArtistMeta {
name: String::from("Frontside"), id: ArtistId {
}, name: String::from("Frontside"),
sort: None, },
musicbrainz: Some(MbArtistRef::from_url_str( sort: None,
"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490" musicbrainz: Some(MbArtistRef::from_url_str(
).unwrap()), "https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490"
properties: HashMap::from([ ).unwrap()),
(String::from("MusicButler"), vec![ properties: HashMap::from([
String::from("https://www.musicbutler.io/artist-page/826588800"), (String::from("MusicButler"), vec![
]), String::from("https://www.musicbutler.io/artist-page/826588800"),
(String::from("Qobuz"), vec![String::from( ]),
"https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums", (String::from("Qobuz"), vec![String::from(
)]), "https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums",
]), )]),
albums: vec![Album { ]),
id: AlbumId { },
title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), albums: vec![Album {
meta: AlbumMeta {
id: AlbumId {
title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"),
},
date: 2001.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
}, },
date: 2001.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -591,32 +605,36 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
}], }],
}, },
Artist { Artist {
id: ArtistId { meta: ArtistMeta {
name: String::from("Heavens Basement"), id: ArtistId {
}, name: String::from("Heavens Basement"),
sort: Some(ArtistId { },
name: String::from("Heavens Basement"), sort: Some(ArtistId {
}), name: String::from("Heavens Basement"),
musicbrainz: Some(MbArtistRef::from_url_str( }),
"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc" musicbrainz: Some(MbArtistRef::from_url_str(
).unwrap()), "https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc"
properties: HashMap::from([ ).unwrap()),
(String::from("MusicButler"), vec![ properties: HashMap::from([
String::from("https://www.musicbutler.io/artist-page/291158685"), (String::from("MusicButler"), vec![
]), String::from("https://www.musicbutler.io/artist-page/291158685"),
(String::from("Qobuz"), vec![String::from( ]),
"https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums", (String::from("Qobuz"), vec![String::from(
)]), "https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums",
]), )]),
albums: vec![Album { ]),
id: AlbumId { },
title: String::from("Paper Plague"), albums: vec![Album {
meta: AlbumMeta {
id: AlbumId {
title: String::from("Paper Plague"),
},
date: 2011.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
}, },
date: 2011.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: None,
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -631,14 +649,16 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
}, },
], ],
}, Album { }, Album {
id: AlbumId { meta: AlbumMeta {
title: String::from("Unbreakable"), id: AlbumId {
title: String::from("Unbreakable"),
},
date: 2011.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
}, },
date: 2011.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -721,31 +741,35 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
}], }],
}, },
Artist { Artist {
id: ArtistId { meta: ArtistMeta {
name: String::from("Metallica"), id: ArtistId {
}, name: String::from("Metallica"),
sort: None, },
musicbrainz: Some(MbArtistRef::from_url_str( sort: None,
"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab" musicbrainz: Some(MbArtistRef::from_url_str(
).unwrap()), "https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab"
properties: HashMap::from([ ).unwrap()),
(String::from("MusicButler"), vec![ properties: HashMap::from([
String::from("https://www.musicbutler.io/artist-page/3996865"), (String::from("MusicButler"), vec![
String::from("https://www.musicbutler.io/artist-page/3996865"),
]),
(String::from("Qobuz"), vec![String::from(
"https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums",
)]),
]), ]),
(String::from("Qobuz"), vec![String::from( },
"https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums",
)]),
]),
albums: vec![ albums: vec![
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: String::from("Ride the Lightning"), id: AlbumId {
title: String::from("Ride the Lightning"),
},
date: 1984.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
}, },
date: 1984.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {
@ -838,14 +862,16 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
], ],
}, },
Album { Album {
id: AlbumId { meta: AlbumMeta {
title: String::from("S&M"), id: AlbumId {
title: String::from("S&M"),
},
date: 1999.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![AlbumSecondaryType::Live],
}, },
date: 1999.into(),
seq: AlbumSeq(0),
musicbrainz: None,
primary_type: Some(AlbumPrimaryType::Album),
secondary_types: vec![AlbumSecondaryType::Live],
tracks: vec![ tracks: vec![
Track { Track {
id: TrackId { id: TrackId {