From c015f4c11217c49f78df2e648c923fcb9f2f3f1f Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Tue, 5 Mar 2024 23:24:18 +0100 Subject: [PATCH] Sort albums by month if two releases of the same artist happen in the same year (#155) Closes #106 Reviewed-on: https://git.thenineworlds.net/wojtek/musichoard/pulls/155 --- src/bin/musichoard-edit.rs | 191 ++++++--- src/core/collection/album.rs | 222 +++++++++- src/core/collection/artist.rs | 95 +++-- src/core/collection/merge.rs | 44 +- src/core/collection/mod.rs | 2 +- src/core/collection/track.rs | 31 +- src/core/database/json/mod.rs | 7 +- src/core/database/json/testmod.rs | 28 +- src/core/database/serde/deserialize.rs | 40 +- src/core/database/serde/mod.rs | 7 - src/core/database/serde/serialize.rs | 29 +- src/core/library/beets/mod.rs | 68 ++- src/core/library/beets/testmod.rs | 132 +++++- src/core/library/mod.rs | 9 +- src/core/library/testmod.rs | 93 ++++- src/core/musichoard/musichoard.rs | 289 +++++++++---- src/core/testmod.rs | 6 +- src/tests.rs | 235 +++++++---- src/tui/app/machine/reload.rs | 10 +- src/tui/app/selection/album.rs | 35 +- src/tui/app/selection/artist.rs | 33 +- src/tui/app/selection/mod.rs | 14 +- src/tui/app/selection/track.rs | 27 +- src/tui/mod.rs | 25 +- src/tui/testmod.rs | 7 +- src/tui/ui.rs | 16 +- tests/database/json.rs | 7 +- tests/files/database/database.json | 2 +- tests/library/testmod.rs | 325 +++++++++++---- tests/testlib.rs | 554 +++++++++++++------------ 30 files changed, 1786 insertions(+), 797 deletions(-) diff --git a/src/bin/musichoard-edit.rs b/src/bin/musichoard-edit.rs index 00de904..94f7f0c 100644 --- a/src/bin/musichoard-edit.rs +++ b/src/bin/musichoard-edit.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use structopt::{clap::AppSettings, StructOpt}; use musichoard::{ - collection::artist::ArtistId, + collection::{album::AlbumId, artist::ArtistId}, database::json::{backend::JsonDatabaseFileBackend, JsonDatabase}, MusicHoard, MusicHoardBuilder, NoLibrary, }; @@ -22,56 +22,54 @@ struct Opt { database_file_path: PathBuf, #[structopt(subcommand)] - category: Category, + command: Command, } #[derive(StructOpt, Debug)] -enum Category { - #[structopt(about = "Edit artist information")] - Artist(ArtistCommand), +enum Command { + #[structopt(about = "Modify artist information")] + Artist(ArtistOpt), } -impl Category { - fn handle(self, music_hoard: &mut MH) { - match self { - Category::Artist(artist_command) => artist_command.handle(music_hoard), - } - } +#[derive(StructOpt, Debug)] +struct ArtistOpt { + // For some reason, not specyfing the artist name with the `long` name makes StructOpt failed + // for inexplicable reason. For example, it won't recognise `Abadde` or `Abadden` as a name and + // will insteady try to process it as a command. + #[structopt(long, help = "The name of the artist")] + name: String, + + #[structopt(subcommand)] + command: ArtistCommand, } #[derive(StructOpt, Debug)] enum ArtistCommand { #[structopt(about = "Add a new artist to the collection")] - Add(ArtistValue), + Add, #[structopt(about = "Remove an artist from the collection")] - Remove(ArtistValue), + Remove, #[structopt(about = "Edit the artist's sort name")] Sort(SortCommand), #[structopt(name = "musicbrainz", about = "Edit the MusicBrainz URL of an artist")] MusicBrainz(MusicBrainzCommand), - #[structopt(name = "property", about = "Edit a property of an artist")] + #[structopt(about = "Edit a property of an artist")] Property(PropertyCommand), + #[structopt(about = "Modify the artist's album information")] + Album(AlbumOpt), } #[derive(StructOpt, Debug)] enum SortCommand { #[structopt(about = "Set the provided name as the artist's sort name")] - Set(ArtistSortValue), + Set(SortValue), #[structopt(about = "Clear the artist's sort name")] - Clear(ArtistValue), + Clear, } #[derive(StructOpt, Debug)] -struct ArtistValue { - #[structopt(help = "The name of the artist")] - artist: String, -} - -#[derive(StructOpt, Debug)] -struct ArtistSortValue { - #[structopt(help = "The name of the artist")] - artist: String, - #[structopt(help = "The sort name of the artist")] +struct SortValue { + #[structopt(help = "The sort name")] sort: String, } @@ -80,13 +78,11 @@ enum MusicBrainzCommand { #[structopt(about = "Set the MusicBrainz URL overwriting any existing value")] Set(MusicBrainzValue), #[structopt(about = "Clear the MusicBrainz URL)")] - Clear(ArtistValue), + Clear, } #[derive(StructOpt, Debug)] struct MusicBrainzValue { - #[structopt(help = "The name of the artist")] - artist: String, #[structopt(help = "The MusicBrainz URL")] url: String, } @@ -105,8 +101,6 @@ enum PropertyCommand { #[derive(StructOpt, Debug)] struct PropertyValue { - #[structopt(help = "The name of the artist")] - artist: String, #[structopt(help = "The name of the property")] property: String, #[structopt(help = "The list of values")] @@ -115,101 +109,176 @@ struct PropertyValue { #[derive(StructOpt, Debug)] struct PropertyName { - #[structopt(help = "The name of the artist")] - artist: String, #[structopt(help = "The name of the property")] property: String, } -impl ArtistCommand { +#[derive(StructOpt, Debug)] +struct AlbumOpt { + // Using `long` for consistency with `ArtistOpt`. + #[structopt(long, help = "The title of the album")] + title: String, + + #[structopt(subcommand)] + command: AlbumCommand, +} + +#[derive(StructOpt, Debug)] +enum AlbumCommand { + #[structopt(about = "Edit the album's sequence value")] + Seq(AlbumSeqCommand), +} + +#[derive(StructOpt, Debug)] +enum AlbumSeqCommand { + #[structopt(about = "Set the sequence value overwriting any existing value")] + Set(AlbumSeqValue), + #[structopt(about = "Clear the sequence value")] + Clear, +} + +#[derive(StructOpt, Debug)] +struct AlbumSeqValue { + #[structopt(help = "The new sequence value")] + value: u8, +} + +impl Command { fn handle(self, music_hoard: &mut MH) { match self { - ArtistCommand::Add(artist_value) => { + Command::Artist(artist_opt) => artist_opt.handle(music_hoard), + } + } +} + +impl ArtistOpt { + fn handle(self, music_hoard: &mut MH) { + self.command.handle(music_hoard, &self.name) + } +} + +impl ArtistCommand { + fn handle(self, music_hoard: &mut MH, artist_name: &str) { + match self { + ArtistCommand::Add => { music_hoard - .add_artist(ArtistId::new(artist_value.artist)) + .add_artist(ArtistId::new(artist_name)) .expect("failed to add artist"); } - ArtistCommand::Remove(artist_value) => { + ArtistCommand::Remove => { music_hoard - .remove_artist(ArtistId::new(artist_value.artist)) + .remove_artist(ArtistId::new(artist_name)) .expect("failed to remove artist"); } ArtistCommand::Sort(sort_command) => { - sort_command.handle(music_hoard); + sort_command.handle(music_hoard, artist_name); } ArtistCommand::MusicBrainz(musicbrainz_command) => { - musicbrainz_command.handle(music_hoard) + musicbrainz_command.handle(music_hoard, artist_name) } ArtistCommand::Property(property_command) => { - property_command.handle(music_hoard); + property_command.handle(music_hoard, artist_name); + } + ArtistCommand::Album(album_opt) => { + album_opt.handle(music_hoard, artist_name); } } } } impl SortCommand { - fn handle(self, music_hoard: &mut MH) { + fn handle(self, music_hoard: &mut MH, artist_name: &str) { match self { SortCommand::Set(artist_sort_value) => music_hoard .set_artist_sort( - ArtistId::new(artist_sort_value.artist), + ArtistId::new(artist_name), ArtistId::new(artist_sort_value.sort), ) .expect("faild to set artist sort name"), - SortCommand::Clear(artist_value) => music_hoard - .clear_artist_sort(ArtistId::new(artist_value.artist)) + SortCommand::Clear => music_hoard + .clear_artist_sort(ArtistId::new(artist_name)) .expect("failed to clear artist sort name"), } } } impl MusicBrainzCommand { - fn handle(self, music_hoard: &mut MH) { + fn handle(self, music_hoard: &mut MH, artist_name: &str) { match self { MusicBrainzCommand::Set(musicbrainz_value) => music_hoard - .set_musicbrainz_url( - ArtistId::new(musicbrainz_value.artist), - musicbrainz_value.url, - ) + .set_artist_musicbrainz(ArtistId::new(artist_name), musicbrainz_value.url) .expect("failed to set MusicBrainz URL"), - MusicBrainzCommand::Clear(artist_value) => music_hoard - .clear_musicbrainz_url(ArtistId::new(artist_value.artist)) + MusicBrainzCommand::Clear => music_hoard + .clear_artist_musicbrainz(ArtistId::new(artist_name)) .expect("failed to clear MusicBrainz URL"), } } } impl PropertyCommand { - fn handle(self, music_hoard: &mut MH) { + fn handle(self, music_hoard: &mut MH, artist_name: &str) { match self { PropertyCommand::Add(property_value) => music_hoard - .add_to_property( - ArtistId::new(property_value.artist), + .add_to_artist_property( + ArtistId::new(artist_name), property_value.property, property_value.values, ) .expect("failed to add values to property"), PropertyCommand::Remove(property_value) => music_hoard - .remove_from_property( - ArtistId::new(property_value.artist), + .remove_from_artist_property( + ArtistId::new(artist_name), property_value.property, property_value.values, ) .expect("failed to remove values from property"), PropertyCommand::Set(property_value) => music_hoard - .set_property( - ArtistId::new(property_value.artist), + .set_artist_property( + ArtistId::new(artist_name), property_value.property, property_value.values, ) .expect("failed to set property"), PropertyCommand::Clear(property_name) => music_hoard - .clear_property(ArtistId::new(property_name.artist), property_name.property) + .clear_artist_property(ArtistId::new(artist_name), property_name.property) .expect("failed to clear property"), } } } +impl AlbumOpt { + fn handle(self, music_hoard: &mut MH, artist_name: &str) { + self.command.handle(music_hoard, artist_name, &self.title) + } +} + +impl AlbumCommand { + fn handle(self, music_hoard: &mut MH, artist_name: &str, album_name: &str) { + match self { + AlbumCommand::Seq(seq_command) => { + seq_command.handle(music_hoard, artist_name, album_name); + } + } + } +} + +impl AlbumSeqCommand { + fn handle(self, music_hoard: &mut MH, artist_name: &str, album_name: &str) { + match self { + AlbumSeqCommand::Set(seq_value) => music_hoard + .set_album_seq( + ArtistId::new(artist_name), + AlbumId::new(album_name), + seq_value.value, + ) + .expect("failed to set sequence value"), + AlbumSeqCommand::Clear => music_hoard + .clear_album_seq(ArtistId::new(artist_name), AlbumId::new(album_name)) + .expect("failed to clear sequence value"), + } + } +} + fn main() { let opt = Opt::from_args(); @@ -219,5 +288,5 @@ fn main() { .set_database(db) .build() .expect("failed to initialise MusicHoard"); - opt.category.handle(&mut music_hoard); + opt.command.handle(&mut music_hoard); } diff --git a/src/core/collection/album.rs b/src/core/collection/album.rs index 7a35591..9de9d3b 100644 --- a/src/core/collection/album.rs +++ b/src/core/collection/album.rs @@ -1,7 +1,10 @@ -use std::mem; +use std::{ + fmt::{self, Display}, + mem, +}; use crate::core::collection::{ - merge::{Merge, MergeSorted}, + merge::{Merge, MergeSorted, WithId}, track::Track, }; @@ -9,19 +12,106 @@ use crate::core::collection::{ #[derive(Clone, Debug, PartialEq, Eq)] pub struct Album { pub id: AlbumId, + pub date: AlbumDate, + pub seq: AlbumSeq, pub tracks: Vec, } +impl WithId for Album { + type Id = AlbumId; + + fn id(&self) -> &Self::Id { + &self.id + } +} + /// The album identifier. #[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)] pub struct AlbumId { - pub year: u32, pub title: String, } +// There are crates for handling dates, but we don't need much complexity beyond year-month-day. +/// The album's release date. +#[derive(Clone, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)] +pub struct AlbumDate { + pub year: u32, + pub month: AlbumMonth, + pub day: u8, +} + +impl Display for AlbumDate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.month.is_none() { + write!(f, "{}", self.year) + } else if self.day == 0 { + write!(f, "{}‐{:02}", self.year, self.month as u8) + } else { + write!(f, "{}‐{:02}‐{:02}", self.year, self.month as u8, self.day) + } + } +} + +/// The album's sequence to determine order when two or more albums have the same release date. +#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)] +pub struct AlbumSeq(pub u8); + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)] +pub enum AlbumMonth { + #[default] + None = 0, + January = 1, + February = 2, + March = 3, + April = 4, + May = 5, + June = 6, + July = 7, + August = 8, + September = 9, + October = 10, + November = 11, + December = 12, +} + +impl From for AlbumMonth { + fn from(value: u8) -> Self { + match value { + 1 => AlbumMonth::January, + 2 => AlbumMonth::February, + 3 => AlbumMonth::March, + 4 => AlbumMonth::April, + 5 => AlbumMonth::May, + 6 => AlbumMonth::June, + 7 => AlbumMonth::July, + 8 => AlbumMonth::August, + 9 => AlbumMonth::September, + 10 => AlbumMonth::October, + 11 => AlbumMonth::November, + 12 => AlbumMonth::December, + _ => AlbumMonth::None, + } + } +} + +impl AlbumMonth { + fn is_none(&self) -> bool { + matches!(self, AlbumMonth::None) + } +} + impl Album { - pub fn get_sort_key(&self) -> &AlbumId { - &self.id + pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &AlbumId) { + (&self.date, &self.seq, &self.id) + } + + pub fn set_seq(&mut self, seq: AlbumSeq) { + self.seq = seq; + } + + pub fn clear_seq(&mut self) { + self.seq = AlbumSeq::default(); } } @@ -33,24 +123,140 @@ impl PartialOrd for Album { impl Ord for Album { fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.id.cmp(&other.id) + self.get_sort_key().cmp(&other.get_sort_key()) } } impl Merge for Album { fn merge_in_place(&mut self, other: Self) { assert_eq!(self.id, other.id); + self.seq = std::cmp::max(self.seq, other.seq); let tracks = mem::take(&mut self.tracks); self.tracks = MergeSorted::new(tracks.into_iter(), other.tracks.into_iter()).collect(); } } +impl> From for AlbumId { + fn from(value: S) -> Self { + AlbumId::new(value) + } +} + +impl AsRef for AlbumId { + fn as_ref(&self) -> &AlbumId { + self + } +} + +impl AlbumId { + pub fn new>(name: S) -> AlbumId { + AlbumId { title: name.into() } + } +} + +impl Display for AlbumId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.title) + } +} + #[cfg(test)] mod tests { use crate::core::testmod::FULL_COLLECTION; use super::*; + impl AlbumDate { + fn new>(year: u32, month: M, day: u8) -> Self { + AlbumDate { + year, + month: month.into(), + day, + } + } + } + + #[test] + fn album_month() { + assert_eq!(>::into(0), AlbumMonth::None); + assert_eq!(>::into(1), AlbumMonth::January); + assert_eq!(>::into(2), AlbumMonth::February); + assert_eq!(>::into(3), AlbumMonth::March); + assert_eq!(>::into(4), AlbumMonth::April); + assert_eq!(>::into(5), AlbumMonth::May); + assert_eq!(>::into(6), AlbumMonth::June); + assert_eq!(>::into(7), AlbumMonth::July); + assert_eq!(>::into(8), AlbumMonth::August); + assert_eq!(>::into(9), AlbumMonth::September); + assert_eq!(>::into(10), AlbumMonth::October); + assert_eq!(>::into(11), AlbumMonth::November); + assert_eq!(>::into(12), AlbumMonth::December); + assert_eq!(>::into(13), AlbumMonth::None); + assert_eq!(>::into(255), AlbumMonth::None); + } + + #[test] + fn album_display() { + assert_eq!(AlbumDate::default().to_string(), "0"); + assert_eq!(AlbumDate::new(1990, 0, 0).to_string(), "1990"); + assert_eq!(AlbumDate::new(1990, 5, 0).to_string(), "1990‐05"); + assert_eq!(AlbumDate::new(1990, 5, 6).to_string(), "1990‐05‐06"); + } + + #[test] + fn same_date_seq_cmp() { + let date = AlbumDate::new(2024, 3, 2); + + let album_id_1 = AlbumId { + title: String::from("album z"), + }; + let album_1 = Album { + id: album_id_1, + date: date.clone(), + seq: AlbumSeq(1), + tracks: vec![], + }; + + let album_id_2 = AlbumId { + title: String::from("album a"), + }; + let album_2 = Album { + id: album_id_2, + date: date.clone(), + seq: AlbumSeq(2), + tracks: vec![], + }; + + assert_ne!(album_1, album_2); + assert!(album_1 < album_2); + } + + #[test] + fn set_clear_seq() { + let mut album = Album { + id: "an album".into(), + date: AlbumDate::default(), + seq: AlbumSeq::default(), + tracks: vec![], + }; + + assert_eq!(album.seq, AlbumSeq(0)); + + // Setting a seq on an album. + album.set_seq(AlbumSeq(6)); + assert_eq!(album.seq, AlbumSeq(6)); + + album.set_seq(AlbumSeq(6)); + assert_eq!(album.seq, AlbumSeq(6)); + + album.set_seq(AlbumSeq(8)); + assert_eq!(album.seq, AlbumSeq(8)); + + // Clearing seq. + album.clear_seq(); + assert_eq!(album.seq, AlbumSeq(0)); + } + #[test] fn merge_album_no_overlap() { let left = FULL_COLLECTION[0].albums[0].to_owned(); @@ -64,9 +270,9 @@ mod tests { let merged = left.clone().merge(right.clone()); assert_eq!(expected, merged); - // Non-overlapping merge should be commutative. + // Non-overlapping merge should be commutative in the tracks. let merged = right.clone().merge(left.clone()); - assert_eq!(expected, merged); + assert_eq!(expected.tracks, merged.tracks); } #[test] diff --git a/src/core/collection/artist.rs b/src/core/collection/artist.rs index c2a09fa..372dc20 100644 --- a/src/core/collection/artist.rs +++ b/src/core/collection/artist.rs @@ -2,6 +2,7 @@ use std::{ collections::HashMap, fmt::{self, Debug, Display}, mem, + str::FromStr, }; use url::Url; @@ -9,7 +10,7 @@ use uuid::Uuid; use crate::core::collection::{ album::Album, - merge::{Merge, MergeSorted}, + merge::{Merge, MergeCollections, WithId}, Error, }; @@ -23,6 +24,14 @@ pub struct Artist { pub albums: Vec, } +impl WithId for Artist { + type Id = ArtistId; + + fn id(&self) -> &Self::Id { + &self.id + } +} + /// The artist identifier. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ArtistId { @@ -41,8 +50,8 @@ impl Artist { } } - pub fn get_sort_key(&self) -> &ArtistId { - self.sort.as_ref().unwrap_or(&self.id) + pub fn get_sort_key(&self) -> (&ArtistId,) { + (self.sort.as_ref().unwrap_or(&self.id),) } pub fn set_sort_key>(&mut self, sort: SORT) { @@ -114,18 +123,26 @@ impl PartialOrd for Artist { impl Ord for Artist { 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 { fn merge_in_place(&mut self, other: Self) { assert_eq!(self.id, other.id); + self.sort = self.sort.take().or(other.sort); self.musicbrainz = self.musicbrainz.take().or(other.musicbrainz); self.properties.merge_in_place(other.properties); + let albums = mem::take(&mut self.albums); - self.albums = MergeSorted::new(albums.into_iter(), other.albums.into_iter()).collect(); + self.albums = MergeCollections::merge_iter(albums, other.albums); + } +} + +impl> From for ArtistId { + fn from(value: S) -> Self { + ArtistId::new(value) } } @@ -159,9 +176,13 @@ pub struct MusicBrainz(Url); impl MusicBrainz { /// Validate and wrap a MusicBrainz URL. - pub fn new>(url: S) -> Result { + pub fn new_from_str>(url: S) -> Result { let url = Url::parse(url.as_ref())?; + Self::new_from_url(url) + } + /// Validate and wrap a MusicBrainz URL. + pub fn new_from_url(url: Url) -> Result { if !url .domain() .map(|u| u.ends_with("musicbrainz.org")) @@ -189,11 +210,36 @@ impl AsRef for MusicBrainz { } } -impl TryFrom<&str> for MusicBrainz { +impl FromStr for MusicBrainz { + type Err = Error; + + fn from_str(s: &str) -> Result { + MusicBrainz::new_from_str(s) + } +} + +// A blanket TryFrom would be better, but https://stackoverflow.com/a/64407892 +macro_rules! impl_try_from_for_musicbrainz { + ($from:ty) => { + impl TryFrom<$from> for MusicBrainz { + type Error = Error; + + fn try_from(value: $from) -> Result { + MusicBrainz::new_from_str(value) + } + } + }; +} + +impl_try_from_for_musicbrainz!(&str); +impl_try_from_for_musicbrainz!(&String); +impl_try_from_for_musicbrainz!(String); + +impl TryFrom for MusicBrainz { type Error = Error; - fn try_from(value: &str) -> Result { - MusicBrainz::new(value) + fn try_from(value: Url) -> Result { + MusicBrainz::new_from_url(value) } } @@ -220,34 +266,35 @@ mod tests { #[test] fn musicbrainz() { let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8"; - let url = format!("https://musicbrainz.org/artist/{uuid}"); - let mb = MusicBrainz::new(&url).unwrap(); - assert_eq!(url, mb.as_ref()); + let url_str = format!("https://musicbrainz.org/artist/{uuid}"); + let url: Url = url_str.as_str().try_into().unwrap(); + let mb: MusicBrainz = url.try_into().unwrap(); + assert_eq!(url_str, mb.as_ref()); assert_eq!(uuid, mb.mbid()); let url = "not a url at all".to_string(); let expected_error: Error = url::ParseError::RelativeUrlWithoutBase.into(); - let actual_error = MusicBrainz::new(url).unwrap_err(); + let actual_error = MusicBrainz::from_str(&url).unwrap_err(); assert_eq!(actual_error, expected_error); assert_eq!(actual_error.to_string(), expected_error.to_string()); let url = "https://musicbrainz.org/artist/i-am-not-a-uuid".to_string(); let expected_error: Error = Uuid::try_parse("i-am-not-a-uuid").unwrap_err().into(); - let actual_error = MusicBrainz::new(url).unwrap_err(); + let actual_error = MusicBrainz::from_str(&url).unwrap_err(); assert_eq!(actual_error, expected_error); assert_eq!(actual_error.to_string(), expected_error.to_string()); let url = "https://musicbrainz.org/artist".to_string(); let expected_error = Error::UrlError(format!("invalid MusicBrainz URL: {url}")); - let actual_error = MusicBrainz::new(&url).unwrap_err(); + let actual_error = MusicBrainz::from_str(&url).unwrap_err(); assert_eq!(actual_error, expected_error); assert_eq!(actual_error.to_string(), expected_error.to_string()); } #[test] fn urls() { - assert!(MusicBrainz::new(MUSICBRAINZ).is_ok()); - assert!(MusicBrainz::new(MUSICBUTLER).is_err()); + assert!(MusicBrainz::from_str(MUSICBRAINZ).is_ok()); + assert!(MusicBrainz::from_str(MUSICBUTLER).is_err()); } #[test] @@ -256,11 +303,11 @@ mod tests { let sort_id_1 = ArtistId::new("sort id 1"); let sort_id_2 = ArtistId::new("sort id 2"); - let mut artist = Artist::new(artist_id.clone()); + let mut artist = Artist::new(&artist_id.name); assert_eq!(artist.id, artist_id); assert_eq!(artist.sort, None); - assert_eq!(artist.get_sort_key(), &artist_id); + assert_eq!(artist.get_sort_key(), (&artist_id,)); assert!(artist < Artist::new(sort_id_1.clone())); assert!(artist < Artist::new(sort_id_2.clone())); @@ -268,7 +315,7 @@ mod tests { assert_eq!(artist.id, artist_id); assert_eq!(artist.sort.as_ref(), Some(&sort_id_1)); - assert_eq!(artist.get_sort_key(), &sort_id_1); + assert_eq!(artist.get_sort_key(), (&sort_id_1,)); assert!(artist > Artist::new(artist_id.clone())); assert!(artist < Artist::new(sort_id_2.clone())); @@ -276,7 +323,7 @@ mod tests { assert_eq!(artist.id, artist_id); assert_eq!(artist.sort.as_ref(), Some(&sort_id_2)); - assert_eq!(artist.get_sort_key(), &sort_id_2); + assert_eq!(artist.get_sort_key(), (&sort_id_2,)); assert!(artist > Artist::new(artist_id.clone())); assert!(artist > Artist::new(sort_id_1.clone())); @@ -284,7 +331,7 @@ mod tests { assert_eq!(artist.id, artist_id); assert_eq!(artist.sort, None); - assert_eq!(artist.get_sort_key(), &artist_id); + assert_eq!(artist.get_sort_key(), (&artist_id,)); assert!(artist < Artist::new(sort_id_1.clone())); assert!(artist < Artist::new(sort_id_2.clone())); } @@ -307,14 +354,14 @@ mod tests { // Setting a URL on an artist. artist.set_musicbrainz_url(MUSICBRAINZ.try_into().unwrap()); - _ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap()); + _ = expected.insert(MUSICBRAINZ.try_into().unwrap()); assert_eq!(artist.musicbrainz, expected); artist.set_musicbrainz_url(MUSICBRAINZ.try_into().unwrap()); assert_eq!(artist.musicbrainz, expected); artist.set_musicbrainz_url(MUSICBRAINZ_2.try_into().unwrap()); - _ = expected.insert(MusicBrainz::new(MUSICBRAINZ_2).unwrap()); + _ = expected.insert(MUSICBRAINZ_2.try_into().unwrap()); assert_eq!(artist.musicbrainz, expected); // Clearing URLs. diff --git a/src/core/collection/merge.rs b/src/core/collection/merge.rs index e2a5fc5..2173801 100644 --- a/src/core/collection/merge.rs +++ b/src/core/collection/merge.rs @@ -1,4 +1,4 @@ -use std::{cmp::Ordering, collections::HashMap, hash::Hash, iter::Peekable}; +use std::{cmp::Ordering, collections::HashMap, hash::Hash, iter::Peekable, marker::PhantomData}; /// A trait for merging two objects. The merge is asymmetric with the left argument considered to be /// the primary whose properties are to be kept in case of collisions. @@ -79,3 +79,45 @@ where } } } + +pub trait WithId { + type Id; + + fn id(&self) -> &Self::Id; +} + +pub struct MergeCollections { + _id: PhantomData, + _t: PhantomData, + _it: PhantomData, +} + +impl MergeCollections +where + ID: Eq + Hash + Clone, + T: WithId + Merge + Ord, + IT: IntoIterator, +{ + pub fn merge_iter(primary: IT, secondary: IT) -> Vec { + let primary = primary + .into_iter() + .map(|item| (item.id().clone(), item)) + .collect(); + Self::merge(primary, secondary) + } + + pub fn merge(mut primary: HashMap, secondary: IT) -> Vec { + for secondary_item in secondary { + if let Some(ref mut primary_item) = primary.get_mut(secondary_item.id()) { + primary_item.merge_in_place(secondary_item); + } else { + primary.insert(secondary_item.id().clone(), secondary_item); + } + } + + let mut collection: Vec = primary.into_values().collect(); + collection.sort_unstable(); + + collection + } +} diff --git a/src/core/collection/mod.rs b/src/core/collection/mod.rs index ea42e15..ef4386d 100644 --- a/src/core/collection/mod.rs +++ b/src/core/collection/mod.rs @@ -5,7 +5,7 @@ pub mod artist; pub mod track; mod merge; -pub use merge::Merge; +pub use merge::MergeCollections; use std::fmt::{self, Display}; diff --git a/src/core/collection/track.rs b/src/core/collection/track.rs index 5853de1..dc4bc4f 100644 --- a/src/core/collection/track.rs +++ b/src/core/collection/track.rs @@ -4,33 +4,37 @@ use crate::core::collection::merge::Merge; #[derive(Clone, Debug, PartialEq, Eq)] pub struct Track { pub id: TrackId, + pub number: TrackNum, pub artist: Vec, - pub quality: Quality, + pub quality: TrackQuality, } /// The track identifier. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct TrackId { - pub number: u32, pub title: String, } +/// The track number. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct TrackNum(pub u32); + /// The track quality. Combines format and bitrate information. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Quality { - pub format: Format, +pub struct TrackQuality { + pub format: TrackFormat, pub bitrate: u32, } impl Track { - pub fn get_sort_key(&self) -> &TrackId { - &self.id + pub fn get_sort_key(&self) -> (&TrackNum, &TrackId) { + (&self.number, &self.id) } } /// The track file format. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum Format { +pub enum TrackFormat { Flac, Mp3, } @@ -43,7 +47,7 @@ impl PartialOrd for Track { impl Ord for Track { fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.id.cmp(&other.id) + self.get_sort_key().cmp(&other.get_sort_key()) } } @@ -61,20 +65,21 @@ mod tests { fn merge_track() { let left = Track { id: TrackId { - number: 4, title: String::from("a title"), }, + number: TrackNum(4), artist: vec![String::from("left artist")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1411, }, }; let right = Track { id: left.id.clone(), + number: left.number, artist: vec![String::from("right artist")], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 320, }, }; diff --git a/src/core/database/json/mod.rs b/src/core/database/json/mod.rs index a525dac..5eec609 100644 --- a/src/core/database/json/mod.rs +++ b/src/core/database/json/mod.rs @@ -72,7 +72,7 @@ mod tests { use mockall::predicate; use crate::core::{ - collection::{artist::Artist, Collection}, + collection::{album::AlbumDate, artist::Artist, Collection}, testmod::FULL_COLLECTION, }; @@ -82,7 +82,10 @@ mod tests { fn expected() -> Collection { let mut expected = FULL_COLLECTION.to_owned(); for artist in expected.iter_mut() { - artist.albums.clear(); + for album in artist.albums.iter_mut() { + album.date = AlbumDate::default(); + album.tracks.clear(); + } } expected } diff --git a/src/core/database/json/testmod.rs b/src/core/database/json/testmod.rs index c492aed..d328f8e 100644 --- a/src/core/database/json/testmod.rs +++ b/src/core/database/json/testmod.rs @@ -1,5 +1,5 @@ pub static DATABASE_JSON: &str = "{\ - \"V20240210\":\ + \"V20240302\":\ [\ {\ \"name\":\"Album_Artist ‘A’\",\ @@ -8,7 +8,11 @@ pub static DATABASE_JSON: &str = "{\ \"properties\":{\ \"MusicButler\":[\"https://www.musicbutler.io/artist-page/000000000\"],\ \"Qobuz\":[\"https://www.qobuz.com/nl-nl/interpreter/artist-a/download-streaming-albums\"]\ - }\ + },\ + \"albums\":[\ + {\"title\":\"album_title a.a\",\"seq\":1},\ + {\"title\":\"album_title a.b\",\"seq\":1}\ + ]\ },\ {\ \"name\":\"Album_Artist ‘B’\",\ @@ -21,19 +25,33 @@ pub static DATABASE_JSON: &str = "{\ \"https://www.musicbutler.io/artist-page/111111112\"\ ],\ \"Qobuz\":[\"https://www.qobuz.com/nl-nl/interpreter/artist-b/download-streaming-albums\"]\ - }\ + },\ + \"albums\":[\ + {\"title\":\"album_title b.a\",\"seq\":1},\ + {\"title\":\"album_title b.b\",\"seq\":3},\ + {\"title\":\"album_title b.c\",\"seq\":2},\ + {\"title\":\"album_title b.d\",\"seq\":4}\ + ]\ },\ {\ \"name\":\"The Album_Artist ‘C’\",\ \"sort\":\"Album_Artist ‘C’, The\",\ \"musicbrainz\":\"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111\",\ - \"properties\":{}\ + \"properties\":{},\ + \"albums\":[\ + {\"title\":\"album_title c.a\",\"seq\":0},\ + {\"title\":\"album_title c.b\",\"seq\":0}\ + ]\ },\ {\ \"name\":\"Album_Artist ‘D’\",\ \"sort\":null,\ \"musicbrainz\":null,\ - \"properties\":{}\ + \"properties\":{},\ + \"albums\":[\ + {\"title\":\"album_title d.a\",\"seq\":0},\ + {\"title\":\"album_title d.b\",\"seq\":0}\ + ]\ }\ ]\ }"; diff --git a/src/core/database/serde/deserialize.rs b/src/core/database/serde/deserialize.rs index a4f0e16..cc4a64a 100644 --- a/src/core/database/serde/deserialize.rs +++ b/src/core/database/serde/deserialize.rs @@ -2,15 +2,19 @@ use std::collections::HashMap; use serde::Deserialize; -use crate::{ - collection::artist::{ArtistId, MusicBrainz}, - core::{ - collection::{artist::Artist, Collection}, - database::{serde::Database, LoadError}, +use crate::core::{ + collection::{ + album::{Album, AlbumDate, AlbumId, AlbumSeq}, + artist::{Artist, ArtistId}, + Collection, }, + database::LoadError, }; -pub type DeserializeDatabase = Database; +#[derive(Debug, Deserialize)] +pub enum DeserializeDatabase { + V20240302(Vec), +} #[derive(Debug, Deserialize)] pub struct DeserializeArtist { @@ -18,6 +22,13 @@ pub struct DeserializeArtist { sort: Option, musicbrainz: Option, properties: HashMap>, + albums: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct DeserializeAlbum { + title: String, + seq: u8, } impl TryFrom for Collection { @@ -25,7 +36,7 @@ impl TryFrom for Collection { fn try_from(database: DeserializeDatabase) -> Result { match database { - Database::V20240210(collection) => collection + DeserializeDatabase::V20240302(collection) => collection .into_iter() .map(|artist| artist.try_into()) .collect(), @@ -40,9 +51,20 @@ impl TryFrom for Artist { Ok(Artist { id: ArtistId::new(artist.name), sort: artist.sort.map(ArtistId::new), - musicbrainz: artist.musicbrainz.map(MusicBrainz::new).transpose()?, + musicbrainz: artist.musicbrainz.map(TryInto::try_into).transpose()?, properties: artist.properties, - albums: vec![], + albums: artist.albums.into_iter().map(Into::into).collect(), }) } } + +impl From for Album { + fn from(album: DeserializeAlbum) -> Self { + Album { + id: AlbumId { title: album.title }, + date: AlbumDate::default(), + seq: AlbumSeq(album.seq), + tracks: vec![], + } + } +} diff --git a/src/core/database/serde/mod.rs b/src/core/database/serde/mod.rs index 453b017..d0a878f 100644 --- a/src/core/database/serde/mod.rs +++ b/src/core/database/serde/mod.rs @@ -2,10 +2,3 @@ pub mod deserialize; pub mod serialize; - -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize)] -pub enum Database { - V20240210(Vec), -} diff --git a/src/core/database/serde/serialize.rs b/src/core/database/serde/serialize.rs index 6043ee2..5276b54 100644 --- a/src/core/database/serde/serialize.rs +++ b/src/core/database/serde/serialize.rs @@ -2,12 +2,12 @@ use std::collections::BTreeMap; use serde::Serialize; -use crate::core::{ - collection::{artist::Artist, Collection}, - database::serde::Database, -}; +use crate::core::collection::{album::Album, artist::Artist, Collection}; -pub type SerializeDatabase<'a> = Database>; +#[derive(Debug, Serialize)] +pub enum SerializeDatabase<'a> { + V20240302(Vec>), +} #[derive(Debug, Serialize)] pub struct SerializeArtist<'a> { @@ -15,11 +15,18 @@ pub struct SerializeArtist<'a> { sort: Option<&'a str>, musicbrainz: Option<&'a str>, properties: BTreeMap<&'a str, &'a Vec>, + albums: Vec>, +} + +#[derive(Debug, Serialize)] +pub struct SerializeAlbum<'a> { + title: &'a str, + seq: u8, } impl<'a> From<&'a Collection> for SerializeDatabase<'a> { fn from(collection: &'a Collection) -> Self { - Database::V20240210(collection.iter().map(|artist| artist.into()).collect()) + SerializeDatabase::V20240302(collection.iter().map(Into::into).collect()) } } @@ -34,6 +41,16 @@ impl<'a> From<&'a Artist> for SerializeArtist<'a> { .iter() .map(|(k, v)| (k.as_ref(), v)) .collect(), + albums: artist.albums.iter().map(Into::into).collect(), + } + } +} + +impl<'a> From<&'a Album> for SerializeAlbum<'a> { + fn from(album: &'a Album) -> Self { + SerializeAlbum { + title: &album.id.title, + seq: album.seq.0, } } } diff --git a/src/core/library/beets/mod.rs b/src/core/library/beets/mod.rs index 748a3c6..d67d90d 100644 --- a/src/core/library/beets/mod.rs +++ b/src/core/library/beets/mod.rs @@ -7,7 +7,7 @@ pub mod executor; use mockall::automock; use crate::core::{ - collection::track::Format, + collection::track::TrackFormat, library::{Error, Field, ILibrary, Item, Query}, }; @@ -27,6 +27,10 @@ const LIST_FORMAT_ARG: &str = concat!( list_format_separator!(), "$year", list_format_separator!(), + "$month", + list_format_separator!(), + "$day", + list_format_separator!(), "$album", list_format_separator!(), "$track", @@ -42,6 +46,21 @@ const LIST_FORMAT_ARG: &str = concat!( const TRACK_FORMAT_FLAC: &str = "FLAC"; const TRACK_FORMAT_MP3: &str = "MP3"; +fn format_to_str(format: &TrackFormat) -> &'static str { + match format { + TrackFormat::Flac => TRACK_FORMAT_FLAC, + TrackFormat::Mp3 => TRACK_FORMAT_MP3, + } +} + +fn str_to_format(format: &str) -> Option { + match format { + TRACK_FORMAT_FLAC => Some(TrackFormat::Flac), + TRACK_FORMAT_MP3 => Some(TrackFormat::Mp3), + _ => None, + } +} + trait ToBeetsArg { fn to_arg(&self, include: bool) -> String; } @@ -57,10 +76,13 @@ impl ToBeetsArg for Field { Field::AlbumArtist(ref s) => format!("{negate}albumartist:{s}"), Field::AlbumArtistSort(ref s) => format!("{negate}albumartist_sort:{s}"), Field::AlbumYear(ref u) => format!("{negate}year:{u}"), + Field::AlbumMonth(ref e) => format!("{negate}month:{}", *e as u8), + Field::AlbumDay(ref u) => format!("{negate}day:{u}"), Field::AlbumTitle(ref s) => format!("{negate}album:{s}"), Field::TrackNumber(ref u) => format!("{negate}track:{u}"), Field::TrackTitle(ref s) => format!("{negate}title:{s}"), Field::TrackArtist(ref v) => format!("{negate}artist:{}", v.join("; ")), + Field::TrackFormat(ref f) => format!("{negate}format:{}", format_to_str(f)), Field::All(ref s) => format!("{negate}{s}"), } } @@ -127,36 +149,38 @@ impl ILibraryPrivate for BeetsLibrary { } let split: Vec<&str> = line.split(LIST_FORMAT_SEPARATOR).collect(); - if split.len() != 9 { + if split.len() != 11 { return Err(Error::Invalid(line.to_string())); } let album_artist = split[0].to_string(); - let album_artist_sort = if !split[1].is_empty() { - Some(split[1].to_string()) - } else { - None + let album_artist_sort = match !split[1].is_empty() { + true => Some(split[1].to_string()), + false => None, }; let album_year = split[2].parse::()?; - let album_title = split[3].to_string(); - let track_number = split[4].parse::()?; - let track_title = split[5].to_string(); - let track_artist = split[6] + let album_month = split[3].parse::()?.into(); + let album_day = split[4].parse::()?; + let album_title = split[5].to_string(); + let track_number = split[6].parse::()?; + let track_title = split[7].to_string(); + let track_artist = split[8] .to_string() .split("; ") .map(|s| s.to_owned()) .collect(); - let track_format = match split[7].to_string().as_str() { - TRACK_FORMAT_FLAC => Format::Flac, - TRACK_FORMAT_MP3 => Format::Mp3, - _ => return Err(Error::Invalid(line.to_string())), + let track_format = match str_to_format(split[9].to_string().as_str()) { + Some(format) => format, + None => return Err(Error::Invalid(line.to_string())), }; - let track_bitrate = split[8].trim_end_matches("kbps").parse::()?; + let track_bitrate = split[10].trim_end_matches("kbps").parse::()?; items.push(Item { album_artist, album_artist_sort, album_year, + album_month, + album_day, album_title, track_number, track_title, @@ -177,7 +201,7 @@ mod testmod; mod tests { use mockall::predicate; - use crate::core::library::testmod::LIBRARY_ITEMS; + use crate::{collection::album::AlbumMonth, core::library::testmod::LIBRARY_ITEMS}; use super::*; use testmod::LIBRARY_BEETS; @@ -191,6 +215,7 @@ mod tests { String::from("some.artist.1"), String::from("some.artist.2"), ])) + .exclude(Field::TrackFormat(TrackFormat::Mp3)) .exclude(Field::All(String::from("some.all"))) .to_args(); query.sort(); @@ -199,6 +224,7 @@ mod tests { query, vec![ String::from("^album:some.album"), + String::from("^format:MP3"), String::from("^some.all"), String::from("artist:some.artist.1; some.artist.2"), String::from("track:5"), @@ -209,7 +235,10 @@ mod tests { .exclude(Field::AlbumArtist(String::from("some.albumartist"))) .exclude(Field::AlbumArtistSort(String::from("some.albumartist"))) .include(Field::AlbumYear(3030)) + .include(Field::AlbumMonth(AlbumMonth::April)) + .include(Field::AlbumDay(6)) .include(Field::TrackTitle(String::from("some.track"))) + .include(Field::TrackFormat(TrackFormat::Flac)) .exclude(Field::TrackArtist(vec![ String::from("some.artist.1"), String::from("some.artist.2"), @@ -223,6 +252,9 @@ mod tests { String::from("^albumartist:some.albumartist"), String::from("^albumartist_sort:some.albumartist"), String::from("^artist:some.artist.1; some.artist.2"), + String::from("day:6"), + String::from("format:FLAC"), + String::from("month:4"), String::from("title:some.track"), String::from("year:3030"), ] @@ -335,8 +367,8 @@ mod tests { .split(LIST_FORMAT_SEPARATOR) .map(|s| s.to_owned()) .collect::>(); - invalid_string[7].clear(); - invalid_string[7].push_str("invalid format"); + invalid_string[9].clear(); + invalid_string[9].push_str("invalid format"); let invalid_string = invalid_string.join(LIST_FORMAT_SEPARATOR); output[2] = invalid_string.clone(); let result = Ok(output); diff --git a/src/core/library/beets/testmod.rs b/src/core/library/beets/testmod.rs index e4f30f9..b0fc10a 100644 --- a/src/core/library/beets/testmod.rs +++ b/src/core/library/beets/testmod.rs @@ -2,27 +2,115 @@ use once_cell::sync::Lazy; pub static LIBRARY_BEETS: Lazy> = Lazy::new(|| -> Vec { vec![ - String::from("Album_Artist ‘A’ -*^- -*^- 1998 -*^- album_title a.a -*^- 1 -*^- track a.a.1 -*^- artist a.a.1 -*^- FLAC -*^- 992"), - String::from("Album_Artist ‘A’ -*^- -*^- 1998 -*^- album_title a.a -*^- 2 -*^- track a.a.2 -*^- artist a.a.2.1; artist a.a.2.2 -*^- MP3 -*^- 320"), - String::from("Album_Artist ‘A’ -*^- -*^- 1998 -*^- album_title a.a -*^- 3 -*^- track a.a.3 -*^- artist a.a.3 -*^- FLAC -*^- 1061"), - String::from("Album_Artist ‘A’ -*^- -*^- 1998 -*^- album_title a.a -*^- 4 -*^- track a.a.4 -*^- artist a.a.4 -*^- FLAC -*^- 1042"), - String::from("Album_Artist ‘A’ -*^- -*^- 2015 -*^- album_title a.b -*^- 1 -*^- track a.b.1 -*^- artist a.b.1 -*^- FLAC -*^- 1004"), - String::from("Album_Artist ‘A’ -*^- -*^- 2015 -*^- album_title a.b -*^- 2 -*^- track a.b.2 -*^- artist a.b.2 -*^- FLAC -*^- 1077"), - String::from("Album_Artist ‘B’ -*^- -*^- 2003 -*^- album_title b.a -*^- 1 -*^- track b.a.1 -*^- artist b.a.1 -*^- MP3 -*^- 190"), - String::from("Album_Artist ‘B’ -*^- -*^- 2003 -*^- album_title b.a -*^- 2 -*^- track b.a.2 -*^- artist b.a.2.1; artist b.a.2.2 -*^- MP3 -*^- 120"), - String::from("Album_Artist ‘B’ -*^- -*^- 2008 -*^- album_title b.b -*^- 1 -*^- track b.b.1 -*^- artist b.b.1 -*^- FLAC -*^- 1077"), - String::from("Album_Artist ‘B’ -*^- -*^- 2008 -*^- album_title b.b -*^- 2 -*^- track b.b.2 -*^- artist b.b.2.1; artist b.b.2.2 -*^- MP3 -*^- 320"), - String::from("Album_Artist ‘B’ -*^- -*^- 2009 -*^- album_title b.c -*^- 1 -*^- track b.c.1 -*^- artist b.c.1 -*^- MP3 -*^- 190"), - String::from("Album_Artist ‘B’ -*^- -*^- 2009 -*^- album_title b.c -*^- 2 -*^- track b.c.2 -*^- artist b.c.2.1; artist b.c.2.2 -*^- MP3 -*^- 120"), - String::from("Album_Artist ‘B’ -*^- -*^- 2015 -*^- album_title b.d -*^- 1 -*^- track b.d.1 -*^- artist b.d.1 -*^- MP3 -*^- 190"), - String::from("Album_Artist ‘B’ -*^- -*^- 2015 -*^- album_title b.d -*^- 2 -*^- track b.d.2 -*^- artist b.d.2.1; artist b.d.2.2 -*^- MP3 -*^- 120"), - String::from("The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- 1985 -*^- album_title c.a -*^- 1 -*^- track c.a.1 -*^- artist c.a.1 -*^- MP3 -*^- 320"), - String::from("The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- 1985 -*^- album_title c.a -*^- 2 -*^- track c.a.2 -*^- artist c.a.2.1; artist c.a.2.2 -*^- MP3 -*^- 120"), - String::from("The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- 2018 -*^- album_title c.b -*^- 1 -*^- track c.b.1 -*^- artist c.b.1 -*^- FLAC -*^- 1041"), - String::from("The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- 2018 -*^- album_title c.b -*^- 2 -*^- track c.b.2 -*^- artist c.b.2.1; artist c.b.2.2 -*^- FLAC -*^- 756"), - String::from("Album_Artist ‘D’ -*^- -*^- 1995 -*^- album_title d.a -*^- 1 -*^- track d.a.1 -*^- artist d.a.1 -*^- MP3 -*^- 120"), - String::from("Album_Artist ‘D’ -*^- -*^- 1995 -*^- album_title d.a -*^- 2 -*^- track d.a.2 -*^- artist d.a.2.1; artist d.a.2.2 -*^- MP3 -*^- 120"), - String::from("Album_Artist ‘D’ -*^- -*^- 2028 -*^- album_title d.b -*^- 1 -*^- track d.b.1 -*^- artist d.b.1 -*^- FLAC -*^- 841"), - String::from("Album_Artist ‘D’ -*^- -*^- 2028 -*^- album_title d.b -*^- 2 -*^- track d.b.2 -*^- artist d.b.2.1; artist d.b.2.2 -*^- FLAC -*^- 756") + String::from( + "Album_Artist ‘A’ -*^- -*^- \ + 1998 -*^- 00 -*^- 00 -*^- album_title a.a -*^- \ + 1 -*^- track a.a.1 -*^- artist a.a.1 -*^- FLAC -*^- 992", + ), + String::from( + "Album_Artist ‘A’ -*^- -*^- \ + 1998 -*^- 00 -*^- 00 -*^- album_title a.a -*^- \ + 2 -*^- track a.a.2 -*^- artist a.a.2.1; artist a.a.2.2 -*^- MP3 -*^- 320", + ), + String::from( + "Album_Artist ‘A’ -*^- -*^- \ + 1998 -*^- 00 -*^- 00 -*^- album_title a.a -*^- \ + 3 -*^- track a.a.3 -*^- artist a.a.3 -*^- FLAC -*^- 1061", + ), + String::from( + "Album_Artist ‘A’ -*^- -*^- \ + 1998 -*^- 00 -*^- 00 -*^- album_title a.a -*^- \ + 4 -*^- track a.a.4 -*^- artist a.a.4 -*^- FLAC -*^- 1042", + ), + String::from( + "Album_Artist ‘A’ -*^- -*^- \ + 2015 -*^- 04 -*^- 00 -*^- album_title a.b -*^- \ + 1 -*^- track a.b.1 -*^- artist a.b.1 -*^- FLAC -*^- 1004", + ), + String::from( + "Album_Artist ‘A’ -*^- -*^- \ + 2015 -*^- 04 -*^- 00 -*^- album_title a.b -*^- \ + 2 -*^- track a.b.2 -*^- artist a.b.2 -*^- FLAC -*^- 1077", + ), + String::from( + "Album_Artist ‘B’ -*^- -*^- \ + 2003 -*^- 06 -*^- 06 -*^- album_title b.a -*^- \ + 1 -*^- track b.a.1 -*^- artist b.a.1 -*^- MP3 -*^- 190", + ), + String::from( + "Album_Artist ‘B’ -*^- -*^- \ + 2003 -*^- 06 -*^- 06 -*^- album_title b.a -*^- \ + 2 -*^- track b.a.2 -*^- artist b.a.2.1; artist b.a.2.2 -*^- MP3 -*^- 120", + ), + String::from( + "Album_Artist ‘B’ -*^- -*^- \ + 2008 -*^- 00 -*^- 00 -*^- album_title b.b -*^- \ + 1 -*^- track b.b.1 -*^- artist b.b.1 -*^- FLAC -*^- 1077", + ), + String::from( + "Album_Artist ‘B’ -*^- -*^- \ + 2008 -*^- 00 -*^- 00 -*^- album_title b.b -*^- \ + 2 -*^- track b.b.2 -*^- artist b.b.2.1; artist b.b.2.2 -*^- MP3 -*^- 320", + ), + String::from( + "Album_Artist ‘B’ -*^- -*^- \ + 2009 -*^- 00 -*^- 00 -*^- album_title b.c -*^- \ + 1 -*^- track b.c.1 -*^- artist b.c.1 -*^- MP3 -*^- 190", + ), + String::from( + "Album_Artist ‘B’ -*^- -*^- \ + 2009 -*^- 00 -*^- 00 -*^- album_title b.c -*^- \ + 2 -*^- track b.c.2 -*^- artist b.c.2.1; artist b.c.2.2 -*^- MP3 -*^- 120", + ), + String::from( + "Album_Artist ‘B’ -*^- -*^- \ + 2015 -*^- 00 -*^- 00 -*^- album_title b.d -*^- \ + 1 -*^- track b.d.1 -*^- artist b.d.1 -*^- MP3 -*^- 190", + ), + String::from( + "Album_Artist ‘B’ -*^- -*^- \ + 2015 -*^- 00 -*^- 00 -*^- album_title b.d -*^- \ + 2 -*^- track b.d.2 -*^- artist b.d.2.1; artist b.d.2.2 -*^- MP3 -*^- 120", + ), + String::from( + "The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- \ + 1985 -*^- 00 -*^- 00 -*^- album_title c.a -*^- \ + 1 -*^- track c.a.1 -*^- artist c.a.1 -*^- MP3 -*^- 320", + ), + String::from( + "The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- \ + 1985 -*^- 00 -*^- 00 -*^- album_title c.a -*^- \ + 2 -*^- track c.a.2 -*^- artist c.a.2.1; artist c.a.2.2 -*^- MP3 -*^- 120", + ), + String::from( + "The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- \ + 2018 -*^- 00 -*^- 00 -*^- album_title c.b -*^- \ + 1 -*^- track c.b.1 -*^- artist c.b.1 -*^- FLAC -*^- 1041", + ), + String::from( + "The Album_Artist ‘C’ -*^- Album_Artist ‘C’, The -*^- \ + 2018 -*^- 00 -*^- 00 -*^- album_title c.b -*^- \ + 2 -*^- track c.b.2 -*^- artist c.b.2.1; artist c.b.2.2 -*^- FLAC -*^- 756", + ), + String::from( + "Album_Artist ‘D’ -*^- -*^- \ + 1995 -*^- 00 -*^- 00 -*^- album_title d.a -*^- \ + 1 -*^- track d.a.1 -*^- artist d.a.1 -*^- MP3 -*^- 120", + ), + String::from( + "Album_Artist ‘D’ -*^- -*^- \ + 1995 -*^- 00 -*^- 00 -*^- album_title d.a -*^- \ + 2 -*^- track d.a.2 -*^- artist d.a.2.1; artist d.a.2.2 -*^- MP3 -*^- 120", + ), + String::from( + "Album_Artist ‘D’ -*^- -*^- \ + 2028 -*^- 00 -*^- 00 -*^- album_title d.b -*^- \ + 1 -*^- track d.b.1 -*^- artist d.b.1 -*^- FLAC -*^- 841", + ), + String::from( + "Album_Artist ‘D’ -*^- -*^- \ + 2028 -*^- 00 -*^- 00 -*^- album_title d.b -*^- \ + 2 -*^- track d.b.2 -*^- artist d.b.2.1; artist d.b.2.2 -*^- FLAC -*^- 756", + ), ] }); diff --git a/src/core/library/mod.rs b/src/core/library/mod.rs index 6e0778a..7797c07 100644 --- a/src/core/library/mod.rs +++ b/src/core/library/mod.rs @@ -8,7 +8,7 @@ use std::{collections::HashSet, fmt, num::ParseIntError, str::Utf8Error}; #[cfg(test)] use mockall::automock; -use crate::core::collection::track::Format; +use crate::core::collection::{album::AlbumMonth, track::TrackFormat}; /// Trait for interacting with the music library. #[cfg_attr(test, automock)] @@ -32,11 +32,13 @@ pub struct Item { pub album_artist: String, pub album_artist_sort: Option, pub album_year: u32, + pub album_month: AlbumMonth, + pub album_day: u8, pub album_title: String, pub track_number: u32, pub track_title: String, pub track_artist: Vec, - pub track_format: Format, + pub track_format: TrackFormat, pub track_bitrate: u32, } @@ -46,10 +48,13 @@ pub enum Field { AlbumArtist(String), AlbumArtistSort(String), AlbumYear(u32), + AlbumMonth(AlbumMonth), + AlbumDay(u8), AlbumTitle(String), TrackNumber(u32), TrackTitle(String), TrackArtist(Vec), + TrackFormat(TrackFormat), All(String), } diff --git a/src/core/library/testmod.rs b/src/core/library/testmod.rs index af2ed68..09e56b8 100644 --- a/src/core/library/testmod.rs +++ b/src/core/library/testmod.rs @@ -1,6 +1,9 @@ use once_cell::sync::Lazy; -use crate::core::{collection::track::Format, library::Item}; +use crate::core::{ + collection::{album::AlbumMonth, track::TrackFormat}, + library::Item, +}; pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { vec![ @@ -8,17 +11,21 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Album_Artist ‘A’"), album_artist_sort: None, album_year: 1998, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("album_title a.a"), track_number: 1, track_title: String::from("track a.a.1"), track_artist: vec![String::from("artist a.a.1")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 992, }, Item { album_artist: String::from("Album_Artist ‘A’"), album_artist_sort: None, album_year: 1998, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("album_title a.a"), track_number: 2, track_title: String::from("track a.a.2"), @@ -26,68 +33,80 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { String::from("artist a.a.2.1"), String::from("artist a.a.2.2"), ], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 320, }, Item { album_artist: String::from("Album_Artist ‘A’"), album_artist_sort: None, album_year: 1998, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("album_title a.a"), track_number: 3, track_title: String::from("track a.a.3"), track_artist: vec![String::from("artist a.a.3")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1061, }, Item { album_artist: String::from("Album_Artist ‘A’"), album_artist_sort: None, album_year: 1998, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("album_title a.a"), track_number: 4, track_title: String::from("track a.a.4"), track_artist: vec![String::from("artist a.a.4")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1042, }, Item { album_artist: String::from("Album_Artist ‘A’"), album_artist_sort: None, album_year: 2015, + album_month: AlbumMonth::April, + album_day: 0, album_title: String::from("album_title a.b"), track_number: 1, track_title: String::from("track a.b.1"), track_artist: vec![String::from("artist a.b.1")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1004, }, Item { album_artist: String::from("Album_Artist ‘A’"), album_artist_sort: None, album_year: 2015, + album_month: AlbumMonth::April, + album_day: 0, album_title: String::from("album_title a.b"), track_number: 2, track_title: String::from("track a.b.2"), track_artist: vec![String::from("artist a.b.2")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1077, }, Item { album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2003, + album_month: AlbumMonth::June, + album_day: 6, album_title: String::from("album_title b.a"), track_number: 1, track_title: String::from("track b.a.1"), track_artist: vec![String::from("artist b.a.1")], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 190, }, Item { album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2003, + album_month: AlbumMonth::June, + album_day: 6, album_title: String::from("album_title b.a"), track_number: 2, track_title: String::from("track b.a.2"), @@ -95,24 +114,28 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { String::from("artist b.a.2.1"), String::from("artist b.a.2.2"), ], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 120, }, Item { album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2008, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("album_title b.b"), track_number: 1, track_title: String::from("track b.b.1"), track_artist: vec![String::from("artist b.b.1")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1077, }, Item { album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2008, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("album_title b.b"), track_number: 2, track_title: String::from("track b.b.2"), @@ -120,24 +143,28 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { String::from("artist b.b.2.1"), String::from("artist b.b.2.2"), ], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 320, }, Item { album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2009, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("album_title b.c"), track_number: 1, track_title: String::from("track b.c.1"), track_artist: vec![String::from("artist b.c.1")], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 190, }, Item { album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2009, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("album_title b.c"), track_number: 2, track_title: String::from("track b.c.2"), @@ -145,24 +172,28 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { String::from("artist b.c.2.1"), String::from("artist b.c.2.2"), ], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 120, }, Item { album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2015, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("album_title b.d"), track_number: 1, track_title: String::from("track b.d.1"), track_artist: vec![String::from("artist b.d.1")], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 190, }, Item { album_artist: String::from("Album_Artist ‘B’"), album_artist_sort: None, album_year: 2015, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("album_title b.d"), track_number: 2, track_title: String::from("track b.d.2"), @@ -170,24 +201,28 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { String::from("artist b.d.2.1"), String::from("artist b.d.2.2"), ], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 120, }, Item { album_artist: String::from("The Album_Artist ‘C’"), album_artist_sort: Some(String::from("Album_Artist ‘C’, The")), album_year: 1985, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("album_title c.a"), track_number: 1, track_title: String::from("track c.a.1"), track_artist: vec![String::from("artist c.a.1")], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 320, }, Item { album_artist: String::from("The Album_Artist ‘C’"), album_artist_sort: Some(String::from("Album_Artist ‘C’, The")), album_year: 1985, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("album_title c.a"), track_number: 2, track_title: String::from("track c.a.2"), @@ -195,24 +230,28 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { String::from("artist c.a.2.1"), String::from("artist c.a.2.2"), ], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 120, }, Item { album_artist: String::from("The Album_Artist ‘C’"), album_artist_sort: Some(String::from("Album_Artist ‘C’, The")), album_year: 2018, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("album_title c.b"), track_number: 1, track_title: String::from("track c.b.1"), track_artist: vec![String::from("artist c.b.1")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1041, }, Item { album_artist: String::from("The Album_Artist ‘C’"), album_artist_sort: Some(String::from("Album_Artist ‘C’, The")), album_year: 2018, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("album_title c.b"), track_number: 2, track_title: String::from("track c.b.2"), @@ -220,24 +259,28 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { String::from("artist c.b.2.1"), String::from("artist c.b.2.2"), ], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 756, }, Item { album_artist: String::from("Album_Artist ‘D’"), album_artist_sort: None, album_year: 1995, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("album_title d.a"), track_number: 1, track_title: String::from("track d.a.1"), track_artist: vec![String::from("artist d.a.1")], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 120, }, Item { album_artist: String::from("Album_Artist ‘D’"), album_artist_sort: None, album_year: 1995, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("album_title d.a"), track_number: 2, track_title: String::from("track d.a.2"), @@ -245,24 +288,28 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { String::from("artist d.a.2.1"), String::from("artist d.a.2.2"), ], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 120, }, Item { album_artist: String::from("Album_Artist ‘D’"), album_artist_sort: None, album_year: 2028, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("album_title d.b"), track_number: 1, track_title: String::from("track d.b.1"), track_artist: vec![String::from("artist d.b.1")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 841, }, Item { album_artist: String::from("Album_Artist ‘D’"), album_artist_sort: None, album_year: 2028, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("album_title d.b"), track_number: 2, track_title: String::from("track d.b.2"), @@ -270,7 +317,7 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { String::from("artist d.b.2.1"), String::from("artist d.b.2.2"), ], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 756, }, ] diff --git a/src/core/musichoard/musichoard.rs b/src/core/musichoard/musichoard.rs index 266502a..cad9ec3 100644 --- a/src/core/musichoard/musichoard.rs +++ b/src/core/musichoard/musichoard.rs @@ -2,10 +2,10 @@ use std::collections::HashMap; use crate::core::{ collection::{ - album::{Album, AlbumId}, - artist::{Artist, ArtistId}, - track::{Quality, Track, TrackId}, - Collection, Merge, + album::{Album, AlbumDate, AlbumId, AlbumSeq}, + artist::{Artist, ArtistId, MusicBrainz}, + track::{Track, TrackId, TrackNum, TrackQuality}, + Collection, MergeCollections, }, database::IDatabase, library::{ILibrary, Item, Query}, @@ -73,19 +73,7 @@ impl MusicHoard { } fn merge_collections(&self) -> Collection { - let mut primary = self.library_cache.clone(); - for secondary_artist in self.database_cache.iter().cloned() { - if let Some(ref mut primary_artist) = primary.get_mut(&secondary_artist.id) { - primary_artist.merge_in_place(secondary_artist); - } else { - primary.insert(secondary_artist.id.clone(), secondary_artist); - } - } - - let mut collection: Collection = primary.into_values().collect(); - Self::sort_artists(&mut collection); - - collection + MergeCollections::merge(self.library_cache.clone(), self.database_cache.clone()) } fn items_to_artists(items: Vec) -> Result, Error> { @@ -99,17 +87,22 @@ impl MusicHoard { let artist_sort = item.album_artist_sort.map(|s| ArtistId { name: s }); let album_id = AlbumId { - year: item.album_year, title: item.album_title, }; + let album_date = AlbumDate { + year: item.album_year, + month: item.album_month, + day: item.album_day, + }; + let track = Track { id: TrackId { - number: item.track_number, title: item.track_title, }, + number: TrackNum(item.track_number), artist: item.track_artist, - quality: Quality { + quality: TrackQuality { format: item.track_format, bitrate: item.track_bitrate, }, @@ -149,6 +142,8 @@ impl MusicHoard { Some(album) => album.tracks.push(track), None => artist.albums.push(Album { id: album_id, + date: album_date, + seq: AlbumSeq(0), tracks: vec![track], }), } @@ -176,6 +171,22 @@ impl MusicHoard { Error::CollectionError(format!("artist '{}' is not in the collection", artist_id)) }) } + + 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) + } + + fn get_album_mut_or_err<'a>( + artist: &'a mut Artist, + album_id: &AlbumId, + ) -> Result<&'a mut Album, Error> { + Self::get_album_mut(artist, album_id).ok_or_else(|| { + Error::CollectionError(format!( + "album '{}' does not belong to the artist", + album_id + )) + }) + } } impl MusicHoard { @@ -248,39 +259,61 @@ impl MusicHoard { Ok(()) } - fn update_collection(&mut self, func: F) -> Result<(), Error> + fn update_collection(&mut self, fn_coll: FnColl) -> Result<(), Error> where - F: FnOnce(&mut Collection), + FnColl: FnOnce(&mut Collection), { - func(&mut self.pre_commit); + fn_coll(&mut self.pre_commit); self.commit() } - fn update_artist_and, F1, F2>( + fn update_artist_and( &mut self, - artist_id: ID, - f1: F1, - f2: F2, + artist_id: &ArtistId, + fn_artist: FnArtist, + fn_coll: FnColl, ) -> Result<(), Error> where - F1: FnOnce(&mut Artist), - F2: FnOnce(&mut Collection), + FnArtist: FnOnce(&mut Artist), + FnColl: FnOnce(&mut Collection), { - f1(Self::get_artist_mut_or_err( - &mut self.pre_commit, - artist_id.as_ref(), - )?); - self.update_collection(f2) + let artist = Self::get_artist_mut_or_err(&mut self.pre_commit, artist_id)?; + fn_artist(artist); + self.update_collection(fn_coll) } - fn update_artist, F>(&mut self, artist_id: ID, func: F) -> Result<(), Error> + fn update_artist( + &mut self, + artist_id: &ArtistId, + fn_artist: FnArtist, + ) -> Result<(), Error> where - F: FnOnce(&mut Artist), + FnArtist: FnOnce(&mut Artist), { - self.update_artist_and(artist_id, func, |_| {}) + self.update_artist_and(artist_id, fn_artist, |_| {}) } - pub fn add_artist>(&mut self, artist_id: ID) -> Result<(), Error> { + fn update_album_and( + &mut self, + artist_id: &ArtistId, + album_id: &AlbumId, + fn_album: FnAlbum, + fn_artist: FnArtist, + fn_coll: FnColl, + ) -> Result<(), Error> + where + FnAlbum: FnOnce(&mut Album), + FnArtist: FnOnce(&mut Artist), + FnColl: FnOnce(&mut Collection), + { + let artist = Self::get_artist_mut_or_err(&mut self.pre_commit, artist_id)?; + let album = Self::get_album_mut_or_err(artist, album_id)?; + fn_album(album); + fn_artist(artist); + self.update_collection(fn_coll) + } + + pub fn add_artist>(&mut self, artist_id: IntoId) -> Result<(), Error> { let artist_id: ArtistId = artist_id.into(); self.update_collection(|collection| { @@ -291,7 +324,7 @@ impl MusicHoard { }) } - pub fn remove_artist>(&mut self, artist_id: ID) -> Result<(), Error> { + pub fn remove_artist>(&mut self, artist_id: Id) -> Result<(), Error> { self.update_collection(|collection| { let index_opt = collection.iter().position(|a| &a.id == artist_id.as_ref()); if let Some(index) = index_opt { @@ -300,77 +333,113 @@ impl MusicHoard { }) } - pub fn set_artist_sort, SORT: Into>( + pub fn set_artist_sort, IntoId: Into>( &mut self, - artist_id: ID, - artist_sort: SORT, + artist_id: Id, + artist_sort: IntoId, ) -> Result<(), Error> { self.update_artist_and( - artist_id, + artist_id.as_ref(), |artist| artist.set_sort_key(artist_sort), |collection| Self::sort_artists(collection), ) } - pub fn clear_artist_sort>(&mut self, artist_id: ID) -> Result<(), Error> { + pub fn clear_artist_sort>(&mut self, artist_id: Id) -> Result<(), Error> { self.update_artist_and( - artist_id, + artist_id.as_ref(), |artist| artist.clear_sort_key(), |collection| Self::sort_artists(collection), ) } - pub fn set_musicbrainz_url, S: AsRef>( + pub fn set_artist_musicbrainz, Mb: TryInto, E>( &mut self, - artist_id: ID, - url: S, - ) -> Result<(), Error> { - let url = url.as_ref().try_into()?; - self.update_artist(artist_id, |artist| artist.set_musicbrainz_url(url)) + artist_id: Id, + url: Mb, + ) -> Result<(), Error> + where + Error: From, + { + let mb = url.try_into()?; + self.update_artist(artist_id.as_ref(), |artist| artist.set_musicbrainz_url(mb)) } - pub fn clear_musicbrainz_url>( + pub fn clear_artist_musicbrainz>( &mut self, - artist_id: ID, + artist_id: Id, ) -> Result<(), Error> { - self.update_artist(artist_id, |artist| artist.clear_musicbrainz_url()) + self.update_artist(artist_id.as_ref(), |artist| artist.clear_musicbrainz_url()) } - pub fn add_to_property, S: AsRef + Into>( + pub fn add_to_artist_property, S: AsRef + Into>( &mut self, - artist_id: ID, + artist_id: Id, property: S, values: Vec, ) -> Result<(), Error> { - self.update_artist(artist_id, |artist| artist.add_to_property(property, values)) + self.update_artist(artist_id.as_ref(), |artist| { + artist.add_to_property(property, values) + }) } - pub fn remove_from_property, S: AsRef>( + pub fn remove_from_artist_property, S: AsRef>( &mut self, - artist_id: ID, + artist_id: Id, property: S, values: Vec, ) -> Result<(), Error> { - self.update_artist(artist_id, |artist| { + self.update_artist(artist_id.as_ref(), |artist| { artist.remove_from_property(property, values) }) } - pub fn set_property, S: AsRef + Into>( + pub fn set_artist_property, S: AsRef + Into>( &mut self, - artist_id: ID, + artist_id: Id, property: S, values: Vec, ) -> Result<(), Error> { - self.update_artist(artist_id, |artist| artist.set_property(property, values)) + self.update_artist(artist_id.as_ref(), |artist| { + artist.set_property(property, values) + }) } - pub fn clear_property, S: AsRef>( + pub fn clear_artist_property, S: AsRef>( &mut self, - artist_id: ID, + artist_id: Id, property: S, ) -> Result<(), Error> { - self.update_artist(artist_id, |artist| artist.clear_property(property)) + self.update_artist(artist_id.as_ref(), |artist| artist.clear_property(property)) + } + + pub fn set_album_seq, AlbumIdRef: AsRef>( + &mut self, + artist_id: ArtistIdRef, + album_id: AlbumIdRef, + seq: u8, + ) -> Result<(), Error> { + self.update_album_and( + artist_id.as_ref(), + album_id.as_ref(), + |album| album.set_seq(AlbumSeq(seq)), + |artist| artist.albums.sort_unstable(), + |_| {}, + ) + } + + pub fn clear_album_seq, AlbumIdRef: AsRef>( + &mut self, + artist_id: ArtistIdRef, + album_id: AlbumIdRef, + ) -> Result<(), Error> { + self.update_album_and( + artist_id.as_ref(), + album_id.as_ref(), + |album| album.clear_seq(), + |artist| artist.albums.sort_unstable(), + |_| {}, + ) } } @@ -523,7 +592,7 @@ mod tests { assert!(music_hoard.add_artist(artist_id.clone()).is_ok()); let actual_err = music_hoard - .set_musicbrainz_url(&artist_id, MUSICBUTLER) + .set_artist_musicbrainz(&artist_id, MUSICBUTLER) .unwrap_err(); let expected_err = Error::CollectionError(format!( "an error occurred when processing a URL: invalid MusicBrainz URL: {MUSICBUTLER}" @@ -549,23 +618,23 @@ mod tests { // Setting a URL on an artist not in the collection is an error. assert!(music_hoard - .set_musicbrainz_url(&artist_id_2, MUSICBRAINZ) + .set_artist_musicbrainz(&artist_id_2, MUSICBRAINZ) .is_err()); assert_eq!(music_hoard.collection[0].musicbrainz, expected); // Setting a URL on an artist. assert!(music_hoard - .set_musicbrainz_url(&artist_id, MUSICBRAINZ) + .set_artist_musicbrainz(&artist_id, MUSICBRAINZ) .is_ok()); - _ = expected.insert(MusicBrainz::new(MUSICBRAINZ).unwrap()); + _ = expected.insert(MUSICBRAINZ.try_into().unwrap()); assert_eq!(music_hoard.collection[0].musicbrainz, expected); // Clearing URLs on an artist that does not exist is an error. - assert!(music_hoard.clear_musicbrainz_url(&artist_id_2).is_err()); + assert!(music_hoard.clear_artist_musicbrainz(&artist_id_2).is_err()); assert_eq!(music_hoard.collection[0].musicbrainz, expected); // Clearing URLs. - assert!(music_hoard.clear_musicbrainz_url(&artist_id).is_ok()); + assert!(music_hoard.clear_artist_musicbrainz(&artist_id).is_ok()); _ = expected.take(); assert_eq!(music_hoard.collection[0].musicbrainz, expected); } @@ -587,13 +656,13 @@ mod tests { // Adding URLs to an artist not in the collection is an error. assert!(music_hoard - .add_to_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) + .add_to_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) .is_err()); assert!(music_hoard.collection[0].properties.is_empty()); // Adding mutliple URLs without clashes. assert!(music_hoard - .add_to_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]) + .add_to_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]) .is_ok()); expected.push(MUSICBUTLER.to_owned()); expected.push(MUSICBUTLER_2.to_owned()); @@ -604,7 +673,7 @@ mod tests { // Removing URLs from an artist not in the collection is an error. assert!(music_hoard - .remove_from_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) + .remove_from_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) .is_err()); assert_eq!( music_hoard.collection[0].properties.get("MusicButler"), @@ -613,7 +682,11 @@ mod tests { // Removing multiple URLs without clashes. assert!(music_hoard - .remove_from_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]) + .remove_from_artist_property( + &artist_id, + "MusicButler", + vec![MUSICBUTLER, MUSICBUTLER_2] + ) .is_ok()); expected.clear(); assert!(music_hoard.collection[0].properties.is_empty()); @@ -636,13 +709,13 @@ mod tests { // Seting URL on an artist not in the collection is an error. assert!(music_hoard - .set_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) + .set_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER]) .is_err()); assert!(music_hoard.collection[0].properties.is_empty()); // Set URLs. assert!(music_hoard - .set_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]) + .set_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2]) .is_ok()); expected.clear(); expected.push(MUSICBUTLER.to_owned()); @@ -654,17 +727,62 @@ mod tests { // Clearing URLs on an artist that does not exist is an error. assert!(music_hoard - .clear_property(&artist_id_2, "MusicButler") + .clear_artist_property(&artist_id_2, "MusicButler") .is_err()); // Clear URLs. assert!(music_hoard - .clear_property(&artist_id, "MusicButler") + .clear_artist_property(&artist_id, "MusicButler") .is_ok()); expected.clear(); assert!(music_hoard.collection[0].properties.is_empty()); } + #[test] + fn set_clear_album_seq() { + let mut database = MockIDatabase::new(); + + let artist_id = ArtistId::new("an artist"); + let album_id = AlbumId::new("an album"); + let album_id_2 = AlbumId::new("another album"); + + let mut database_result = vec![Artist::new(artist_id.clone())]; + database_result[0].albums.push(Album { + id: album_id.clone(), + date: AlbumDate::default(), + seq: AlbumSeq::default(), + tracks: vec![], + }); + + database + .expect_load() + .times(1) + .return_once(|| Ok(database_result)); + database.expect_save().times(2).returning(|_| Ok(())); + + let mut music_hoard = MusicHoard::database(database).unwrap(); + assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(0)); + + // Seting seq on an album not belonging to the artist is an error. + assert!(music_hoard + .set_album_seq(&artist_id, &album_id_2, 6) + .is_err()); + assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(0)); + + // Set seq. + assert!(music_hoard.set_album_seq(&artist_id, &album_id, 6).is_ok()); + assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(6)); + + // Clearing seq on an album that does not exist is an error. + assert!(music_hoard + .clear_album_seq(&artist_id, &album_id_2) + .is_err()); + + // Clear seq. + assert!(music_hoard.clear_album_seq(&artist_id, &album_id).is_ok()); + assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(0)); + } + #[test] fn merge_collection_no_overlap() { let half: usize = FULL_COLLECTION.len() / 2; @@ -887,7 +1005,7 @@ mod tests { } #[test] - fn rescan_library_album_title_year_clash() { + fn rescan_library_album_id_clash() { let mut library = MockILibrary::new(); let mut expected = LIBRARY_COLLECTION.to_owned(); @@ -895,10 +1013,10 @@ mod tests { let clashed_album_id = &expected[1].albums[0].id; let mut items = LIBRARY_ITEMS.to_owned(); - for item in items.iter_mut().filter(|it| { - (it.album_year == removed_album_id.year) && (it.album_title == removed_album_id.title) - }) { - item.album_year = clashed_album_id.year; + for item in items + .iter_mut() + .filter(|it| it.album_title == removed_album_id.title) + { item.album_title = clashed_album_id.title.clone(); } @@ -916,6 +1034,7 @@ mod tests { let mut music_hoard = MusicHoard::library(library); music_hoard.rescan_library().unwrap(); + assert_eq!(music_hoard.get_collection()[0], expected[0]); assert_eq!(music_hoard.get_collection(), &expected); } diff --git a/src/core/testmod.rs b/src/core/testmod.rs index 2f10f36..a0620fe 100644 --- a/src/core/testmod.rs +++ b/src/core/testmod.rs @@ -1,10 +1,10 @@ use once_cell::sync::Lazy; -use std::collections::HashMap; +use std::{collections::HashMap, str::FromStr}; use crate::core::collection::{ - album::{Album, AlbumId}, + album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq}, artist::{Artist, ArtistId, MusicBrainz}, - track::{Format, Quality, Track, TrackId}, + track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality}, }; use crate::tests::*; diff --git a/src/tests.rs b/src/tests.rs index bbb4de1..c54bb6e 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -11,54 +11,59 @@ macro_rules! library_collection { albums: vec![ Album { id: AlbumId { - year: 1998, title: "album_title a.a".to_string(), }, + date: AlbumDate { + year: 1998, + month: AlbumMonth::None, + day: 0, + }, + seq: AlbumSeq(0), tracks: vec![ Track { id: TrackId { - number: 1, title: "track a.a.1".to_string(), }, + number: TrackNum(1), artist: vec!["artist a.a.1".to_string()], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 992, }, }, Track { id: TrackId { - number: 2, title: "track a.a.2".to_string(), }, + number: TrackNum(2), artist: vec![ "artist a.a.2.1".to_string(), "artist a.a.2.2".to_string(), ], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 320, }, }, Track { id: TrackId { - number: 3, title: "track a.a.3".to_string(), }, + number: TrackNum(3), artist: vec!["artist a.a.3".to_string()], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1061, }, }, Track { id: TrackId { - number: 4, title: "track a.a.4".to_string(), }, + number: TrackNum(4), artist: vec!["artist a.a.4".to_string()], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1042, }, }, @@ -66,29 +71,34 @@ macro_rules! library_collection { }, Album { id: AlbumId { - year: 2015, title: "album_title a.b".to_string(), }, + date: AlbumDate { + year: 2015, + month: AlbumMonth::April, + day: 0, + }, + seq: AlbumSeq(0), tracks: vec![ Track { id: TrackId { - number: 1, title: "track a.b.1".to_string(), }, + number: TrackNum(1), artist: vec!["artist a.b.1".to_string()], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1004, }, }, Track { id: TrackId { - number: 2, title: "track a.b.2".to_string(), }, + number: TrackNum(2), artist: vec!["artist a.b.2".to_string()], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1077, }, }, @@ -106,32 +116,37 @@ macro_rules! library_collection { albums: vec![ Album { id: AlbumId { - year: 2003, title: "album_title b.a".to_string(), }, + date: AlbumDate { + year: 2003, + month: AlbumMonth::June, + day: 6, + }, + seq: AlbumSeq(0), tracks: vec![ Track { id: TrackId { - number: 1, title: "track b.a.1".to_string(), }, + number: TrackNum(1), artist: vec!["artist b.a.1".to_string()], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 190, }, }, Track { id: TrackId { - number: 2, title: "track b.a.2".to_string(), }, + number: TrackNum(2), artist: vec![ "artist b.a.2.1".to_string(), "artist b.a.2.2".to_string(), ], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 120, }, }, @@ -139,32 +154,37 @@ macro_rules! library_collection { }, Album { id: AlbumId { - year: 2008, title: "album_title b.b".to_string(), }, + date: AlbumDate { + year: 2008, + month: AlbumMonth::None, + day: 0, + }, + seq: AlbumSeq(0), tracks: vec![ Track { id: TrackId { - number: 1, title: "track b.b.1".to_string(), }, + number: TrackNum(1), artist: vec!["artist b.b.1".to_string()], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1077, }, }, Track { id: TrackId { - number: 2, title: "track b.b.2".to_string(), }, + number: TrackNum(2), artist: vec![ "artist b.b.2.1".to_string(), "artist b.b.2.2".to_string(), ], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 320, }, }, @@ -172,32 +192,37 @@ macro_rules! library_collection { }, Album { id: AlbumId { - year: 2009, title: "album_title b.c".to_string(), }, + date: AlbumDate { + year: 2009, + month: AlbumMonth::None, + day: 0, + }, + seq: AlbumSeq(0), tracks: vec![ Track { id: TrackId { - number: 1, title: "track b.c.1".to_string(), }, + number: TrackNum(1), artist: vec!["artist b.c.1".to_string()], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 190, }, }, Track { id: TrackId { - number: 2, title: "track b.c.2".to_string(), }, + number: TrackNum(2), artist: vec![ "artist b.c.2.1".to_string(), "artist b.c.2.2".to_string(), ], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 120, }, }, @@ -205,32 +230,37 @@ macro_rules! library_collection { }, Album { id: AlbumId { - year: 2015, title: "album_title b.d".to_string(), }, + date: AlbumDate { + year: 2015, + month: AlbumMonth::None, + day: 0, + }, + seq: AlbumSeq(0), tracks: vec![ Track { id: TrackId { - number: 1, title: "track b.d.1".to_string(), }, + number: TrackNum(1), artist: vec!["artist b.d.1".to_string()], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 190, }, }, Track { id: TrackId { - number: 2, title: "track b.d.2".to_string(), }, + number: TrackNum(2), artist: vec![ "artist b.d.2.1".to_string(), "artist b.d.2.2".to_string(), ], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 120, }, }, @@ -250,32 +280,37 @@ macro_rules! library_collection { albums: vec![ Album { id: AlbumId { - year: 1985, title: "album_title c.a".to_string(), }, + date: AlbumDate { + year: 1985, + month: AlbumMonth::None, + day: 0, + }, + seq: AlbumSeq(0), tracks: vec![ Track { id: TrackId { - number: 1, title: "track c.a.1".to_string(), }, + number: TrackNum(1), artist: vec!["artist c.a.1".to_string()], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 320, }, }, Track { id: TrackId { - number: 2, title: "track c.a.2".to_string(), }, + number: TrackNum(2), artist: vec![ "artist c.a.2.1".to_string(), "artist c.a.2.2".to_string(), ], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 120, }, }, @@ -283,32 +318,37 @@ macro_rules! library_collection { }, Album { id: AlbumId { - year: 2018, title: "album_title c.b".to_string(), }, + date: AlbumDate { + year: 2018, + month: AlbumMonth::None, + day: 0, + }, + seq: AlbumSeq(0), tracks: vec![ Track { id: TrackId { - number: 1, title: "track c.b.1".to_string(), }, + number: TrackNum(1), artist: vec!["artist c.b.1".to_string()], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1041, }, }, Track { id: TrackId { - number: 2, title: "track c.b.2".to_string(), }, + number: TrackNum(2), artist: vec![ "artist c.b.2.1".to_string(), "artist c.b.2.2".to_string(), ], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 756, }, }, @@ -326,32 +366,37 @@ macro_rules! library_collection { albums: vec![ Album { id: AlbumId { - year: 1995, title: "album_title d.a".to_string(), }, + date: AlbumDate { + year: 1995, + month: AlbumMonth::None, + day: 0, + }, + seq: AlbumSeq(0), tracks: vec![ Track { id: TrackId { - number: 1, title: "track d.a.1".to_string(), }, + number: TrackNum(1), artist: vec!["artist d.a.1".to_string()], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 120, }, }, Track { id: TrackId { - number: 2, title: "track d.a.2".to_string(), }, + number: TrackNum(2), artist: vec![ "artist d.a.2.1".to_string(), "artist d.a.2.2".to_string(), ], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 120, }, }, @@ -359,32 +404,37 @@ macro_rules! library_collection { }, Album { id: AlbumId { - year: 2028, title: "album_title d.b".to_string(), }, + date: AlbumDate { + year: 2028, + month: AlbumMonth::None, + day: 0, + }, + seq: AlbumSeq(0), tracks: vec![ Track { id: TrackId { - number: 1, title: "track d.b.1".to_string(), }, + number: TrackNum(1), artist: vec!["artist d.b.1".to_string()], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 841, }, }, Track { id: TrackId { - number: 2, title: "track d.b.2".to_string(), }, + number: TrackNum(2), artist: vec![ "artist d.b.2.1".to_string(), "artist d.b.2.2".to_string(), ], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 756, }, }, @@ -404,12 +454,9 @@ macro_rules! full_collection { let artist_a = iter.next().unwrap(); assert_eq!(artist_a.id.name, "Album_Artist ‘A’"); - artist_a.musicbrainz = Some( - MusicBrainz::new( - "https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000", - ) - .unwrap(), - ); + artist_a.musicbrainz = Some(MusicBrainz::from_str( + "https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000", + ).unwrap()); artist_a.properties = HashMap::from([ (String::from("MusicButler"), vec![ @@ -422,14 +469,15 @@ macro_rules! full_collection { ]), ]); + artist_a.albums[0].seq = AlbumSeq(1); + artist_a.albums[1].seq = AlbumSeq(1); + let artist_b = iter.next().unwrap(); assert_eq!(artist_b.id.name, "Album_Artist ‘B’"); - artist_b.musicbrainz = Some( - MusicBrainz::new( - "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111", - ).unwrap(), - ); + artist_b.musicbrainz = Some(MusicBrainz::from_str( + "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111", + ).unwrap()); artist_b.properties = HashMap::from([ (String::from("MusicButler"), vec![ @@ -444,14 +492,17 @@ macro_rules! full_collection { ]), ]); + artist_b.albums[0].seq = AlbumSeq(1); + artist_b.albums[1].seq = AlbumSeq(3); + artist_b.albums[2].seq = AlbumSeq(2); + artist_b.albums[3].seq = AlbumSeq(4); + let artist_c = iter.next().unwrap(); assert_eq!(artist_c.id.name, "The Album_Artist ‘C’"); - artist_c.musicbrainz = Some( - MusicBrainz::new( - "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111", - ).unwrap(), - ); + artist_c.musicbrainz = Some(MusicBrainz::from_str( + "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111", + ).unwrap()); // Nothing for artist_d diff --git a/src/tui/app/machine/reload.rs b/src/tui/app/machine/reload.rs index ee6a57f..cc1638e 100644 --- a/src/tui/app/machine/reload.rs +++ b/src/tui/app/machine/reload.rs @@ -1,7 +1,7 @@ use crate::tui::{ app::{ machine::{App, AppInner, AppMachine}, - selection::IdSelection, + selection::KeySelection, AppPublic, AppState, IAppInteractReload, }, lib::IMusicHoard, @@ -36,7 +36,7 @@ impl IAppInteractReload for AppMachine { type APP = App; fn reload_library(mut self) -> Self::APP { - let previous = IdSelection::get( + let previous = KeySelection::get( self.inner.music_hoard.get_collection(), &self.inner.selection, ); @@ -45,7 +45,7 @@ impl IAppInteractReload for AppMachine { } fn reload_database(mut self) -> Self::APP { - let previous = IdSelection::get( + let previous = KeySelection::get( self.inner.music_hoard.get_collection(), &self.inner.selection, ); @@ -63,11 +63,11 @@ impl IAppInteractReload for AppMachine { } trait IAppInteractReloadPrivate { - fn refresh(self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App; + fn refresh(self, previous: KeySelection, result: Result<(), musichoard::Error>) -> App; } impl IAppInteractReloadPrivate for AppMachine { - fn refresh(mut self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App { + fn refresh(mut self, previous: KeySelection, result: Result<(), musichoard::Error>) -> App { match result { Ok(()) => { self.inner diff --git a/src/tui/app/selection/album.rs b/src/tui/app/selection/album.rs index 1329d95..cce9b5a 100644 --- a/src/tui/app/selection/album.rs +++ b/src/tui/app/selection/album.rs @@ -1,12 +1,12 @@ use std::cmp; use musichoard::collection::{ - album::{Album, AlbumId}, + album::{Album, AlbumDate, AlbumId, AlbumSeq}, track::Track, }; use crate::tui::app::selection::{ - track::{IdSelectTrack, TrackSelection}, + track::{KeySelectTrack, TrackSelection}, Delta, SelectionState, WidgetState, }; @@ -26,9 +26,9 @@ impl AlbumSelection { selection } - pub fn reinitialise(&mut self, albums: &[Album], album: Option) { + pub fn reinitialise(&mut self, albums: &[Album], album: Option) { if let Some(album) = album { - let result = albums.binary_search_by(|a| a.get_sort_key().cmp(&album.album_id)); + let result = albums.binary_search_by(|a| a.get_sort_key().cmp(&album.get_sort_key())); match result { Ok(index) => self.reinitialise_with_index(albums, index, album.track), Err(index) => self.reinitialise_with_index(albums, index, None), @@ -42,7 +42,7 @@ impl AlbumSelection { &mut self, albums: &[Album], index: usize, - active_track: Option, + active_track: Option, ) { if albums.is_empty() { self.state.list.select(None); @@ -160,21 +160,26 @@ impl AlbumSelection { } } -pub struct IdSelectAlbum { - album_id: AlbumId, - track: Option, +pub struct KeySelectAlbum { + key: (AlbumDate, AlbumSeq, AlbumId), + track: Option, } -impl IdSelectAlbum { +impl KeySelectAlbum { pub fn get(albums: &[Album], selection: &AlbumSelection) -> Option { selection.state.list.selected().map(|index| { let album = &albums[index]; - IdSelectAlbum { - album_id: album.get_sort_key().clone(), - track: IdSelectTrack::get(&album.tracks, &selection.track), + let key = album.get_sort_key(); + KeySelectAlbum { + key: (key.0.to_owned(), key.1.to_owned(), key.2.to_owned()), + track: KeySelectTrack::get(&album.tracks, &selection.track), } }) } + + pub fn get_sort_key(&self) -> (&AlbumDate, &AlbumSeq, &AlbumId) { + (&self.key.0, &self.key.1, &self.key.2) + } } #[cfg(test)] @@ -330,20 +335,20 @@ mod tests { // Re-initialise. let expected = sel.clone(); - let active_album = IdSelectAlbum::get(albums, &sel); + let active_album = KeySelectAlbum::get(albums, &sel); sel.reinitialise(albums, active_album); assert_eq!(sel, expected); // Re-initialise out-of-bounds. let mut expected = sel.clone(); expected.decrement(albums, Delta::Line); - let active_album = IdSelectAlbum::get(albums, &sel); + let active_album = KeySelectAlbum::get(albums, &sel); sel.reinitialise(&albums[..(albums.len() - 1)], active_album); assert_eq!(sel, expected); // Re-initialise empty. let expected = AlbumSelection::initialise(&[]); - let active_album = IdSelectAlbum::get(albums, &sel); + let active_album = KeySelectAlbum::get(albums, &sel); sel.reinitialise(&[], active_album); assert_eq!(sel, expected); } diff --git a/src/tui/app/selection/artist.rs b/src/tui/app/selection/artist.rs index 8b76d5a..aa96d25 100644 --- a/src/tui/app/selection/artist.rs +++ b/src/tui/app/selection/artist.rs @@ -7,7 +7,7 @@ use musichoard::collection::{ }; use crate::tui::app::selection::{ - album::{AlbumSelection, IdSelectAlbum}, + album::{AlbumSelection, KeySelectAlbum}, Delta, SelectionState, WidgetState, }; @@ -27,9 +27,9 @@ impl ArtistSelection { selection } - pub fn reinitialise(&mut self, artists: &[Artist], active: Option) { + pub fn reinitialise(&mut self, artists: &[Artist], active: Option) { if let Some(active) = active { - let result = artists.binary_search_by(|a| a.get_sort_key().cmp(&active.artist_id)); + let result = artists.binary_search_by(|a| a.get_sort_key().cmp(&active.get_sort_key())); match result { Ok(index) => self.reinitialise_with_index(artists, index, active.album), Err(index) => self.reinitialise_with_index(artists, index, None), @@ -43,7 +43,7 @@ impl ArtistSelection { &mut self, artists: &[Artist], index: usize, - active_album: Option, + active_album: Option, ) { if artists.is_empty() { self.state.list.select(None); @@ -193,21 +193,26 @@ impl ArtistSelection { } } -pub struct IdSelectArtist { - artist_id: ArtistId, - album: Option, +pub struct KeySelectArtist { + key: (ArtistId,), + album: Option, } -impl IdSelectArtist { +impl KeySelectArtist { pub fn get(artists: &[Artist], selection: &ArtistSelection) -> Option { selection.state.list.selected().map(|index| { let artist = &artists[index]; - IdSelectArtist { - artist_id: artist.get_sort_key().clone(), - album: IdSelectAlbum::get(&artist.albums, &selection.album), + let key = artist.get_sort_key(); + KeySelectArtist { + key: (key.0.to_owned(),), + album: KeySelectAlbum::get(&artist.albums, &selection.album), } }) } + + pub fn get_sort_key(&self) -> (&ArtistId,) { + (&self.key.0,) + } } #[cfg(test)] @@ -385,20 +390,20 @@ mod tests { // Re-initialise. let expected = sel.clone(); - let active_artist = IdSelectArtist::get(artists, &sel); + let active_artist = KeySelectArtist::get(artists, &sel); sel.reinitialise(artists, active_artist); assert_eq!(sel, expected); // Re-initialise out-of-bounds. let mut expected = sel.clone(); expected.decrement(artists, Delta::Line); - let active_artist = IdSelectArtist::get(artists, &sel); + let active_artist = KeySelectArtist::get(artists, &sel); sel.reinitialise(&artists[..(artists.len() - 1)], active_artist); assert_eq!(sel, expected); // Re-initialise empty. let expected = ArtistSelection::initialise(&[]); - let active_artist = IdSelectArtist::get(artists, &sel); + let active_artist = KeySelectArtist::get(artists, &sel); sel.reinitialise(&[], active_artist); assert_eq!(sel, expected); } diff --git a/src/tui/app/selection/mod.rs b/src/tui/app/selection/mod.rs index 8cddcad..74cfb9c 100644 --- a/src/tui/app/selection/mod.rs +++ b/src/tui/app/selection/mod.rs @@ -5,7 +5,7 @@ mod track; use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection}; use ratatui::widgets::ListState; -use artist::{ArtistSelection, IdSelectArtist}; +use artist::{ArtistSelection, KeySelectArtist}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Category { @@ -64,7 +64,7 @@ impl Selection { self.artist.album.track.state.list = selected.track; } - pub fn select_by_id(&mut self, artists: &[Artist], selected: IdSelection) { + pub fn select_by_id(&mut self, artists: &[Artist], selected: KeySelection) { self.artist.reinitialise(artists, selected.artist); } @@ -229,14 +229,14 @@ impl ListSelection { } } -pub struct IdSelection { - artist: Option, +pub struct KeySelection { + artist: Option, } -impl IdSelection { +impl KeySelection { pub fn get(collection: &Collection, selection: &Selection) -> Self { - IdSelection { - artist: IdSelectArtist::get(collection, &selection.artist), + KeySelection { + artist: KeySelectArtist::get(collection, &selection.artist), } } } diff --git a/src/tui/app/selection/track.rs b/src/tui/app/selection/track.rs index feedac5..adec55b 100644 --- a/src/tui/app/selection/track.rs +++ b/src/tui/app/selection/track.rs @@ -1,6 +1,6 @@ use std::cmp; -use musichoard::collection::track::{Track, TrackId}; +use musichoard::collection::track::{Track, TrackId, TrackNum}; use crate::tui::app::selection::{Delta, SelectionState, WidgetState}; @@ -18,9 +18,9 @@ impl TrackSelection { selection } - pub fn reinitialise(&mut self, tracks: &[Track], track: Option) { + pub fn reinitialise(&mut self, tracks: &[Track], track: Option) { if let Some(track) = track { - let result = tracks.binary_search_by(|t| t.get_sort_key().cmp(&track.track_id)); + let result = tracks.binary_search_by(|t| t.get_sort_key().cmp(&track.get_sort_key())); match result { Ok(index) | Err(index) => self.reinitialise_with_index(tracks, index), } @@ -100,19 +100,24 @@ impl TrackSelection { } } -pub struct IdSelectTrack { - track_id: TrackId, +pub struct KeySelectTrack { + key: (TrackNum, TrackId), } -impl IdSelectTrack { +impl KeySelectTrack { pub fn get(tracks: &[Track], selection: &TrackSelection) -> Option { selection.state.list.selected().map(|index| { let track = &tracks[index]; - IdSelectTrack { - track_id: track.get_sort_key().clone(), + let key = track.get_sort_key(); + KeySelectTrack { + key: (key.0.to_owned(), key.1.to_owned()), } }) } + + pub fn get_sort_key(&self) -> (&TrackNum, &TrackId) { + (&self.key.0, &self.key.1) + } } #[cfg(test)] @@ -210,20 +215,20 @@ mod tests { // Re-initialise. let expected = sel.clone(); - let active_track = IdSelectTrack::get(tracks, &sel); + let active_track = KeySelectTrack::get(tracks, &sel); sel.reinitialise(tracks, active_track); assert_eq!(sel, expected); // Re-initialise out-of-bounds. let mut expected = sel.clone(); expected.decrement(tracks, Delta::Line); - let active_track = IdSelectTrack::get(tracks, &sel); + let active_track = KeySelectTrack::get(tracks, &sel); sel.reinitialise(&tracks[..(tracks.len() - 1)], active_track); assert_eq!(sel, expected); // Re-initialise empty. let expected = TrackSelection::initialise(&[]); - let active_track = IdSelectTrack::get(tracks, &sel); + let active_track = KeySelectTrack::get(tracks, &sel); sel.reinitialise(&[], active_track); assert_eq!(sel, expected); } diff --git a/src/tui/mod.rs b/src/tui/mod.rs index f442ce6..0601ba1 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -11,12 +11,12 @@ pub use handler::EventHandler; pub use listener::EventListener; pub use ui::Ui; -use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; -use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; -use ratatui::backend::Backend; -use ratatui::Terminal; -use std::io; -use std::marker::PhantomData; +use crossterm::{ + event::{DisableMouseCapture, EnableMouseCapture}, + terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::{backend::Backend, Terminal}; +use std::{io, marker::PhantomData}; use crate::tui::{ app::{IAppAccess, IAppInteract}, @@ -26,7 +26,7 @@ use crate::tui::{ ui::IUi, }; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Eq, PartialEq)] pub enum Error { Io(String), Event(String), @@ -112,8 +112,8 @@ impl Tui { match listener_handle.join() { Ok(err) => return Err(err.into()), // Calling std::panic::resume_unwind(err) as recommended by the Rust docs - // will not produce an error message. The panic error message is printed at - // the location of the panic which at the time is hidden by the TUI. + // will not produce an error message. This may be due to the panic simply + // causing the process to abort in which case there is nothing to unwind. Err(_) => return Err(Error::ListenerPanic), } } @@ -251,10 +251,9 @@ mod tests { let result = Tui::main(terminal, app, ui, handler, listener); assert!(result.is_err()); - assert_eq!( - result.unwrap_err(), - Error::Event(EventError::Recv.to_string()) - ); + + let error = EventError::Recv; + assert_eq!(result.unwrap_err(), Error::Event(error.to_string())); } #[test] diff --git a/src/tui/testmod.rs b/src/tui/testmod.rs index 7054f71..1d7e506 100644 --- a/src/tui/testmod.rs +++ b/src/tui/testmod.rs @@ -1,10 +1,11 @@ +use std::{collections::HashMap, str::FromStr}; + use musichoard::collection::{ - album::{Album, AlbumId}, + album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq}, artist::{Artist, ArtistId, MusicBrainz}, - track::{Format, Quality, Track, TrackId}, + track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality}, }; use once_cell::sync::Lazy; -use std::collections::HashMap; use crate::tests::*; diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 5f6b467..2d71e98 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use musichoard::collection::{ album::Album, artist::Artist, - track::{Format, Track}, + track::{Track, TrackFormat}, Collection, }; use ratatui::{ @@ -287,9 +287,13 @@ impl<'a, 'b> AlbumState<'a, 'b> { let album = state.list.selected().map(|i| &albums[i]); let info = Paragraph::new(format!( "Title: {}\n\ - Year: {}", + Date: {}{}", album.map(|a| a.id.title.as_str()).unwrap_or(""), - album.map(|a| a.id.year.to_string()).unwrap_or_default(), + album.map(|a| a.date.to_string()).unwrap_or_default(), + album + .filter(|a| a.seq.0 > 0) + .map(|a| format!(" ({})", a.seq.0)) + .unwrap_or_default() )); AlbumState { @@ -323,13 +327,13 @@ impl<'a, 'b> TrackState<'a, 'b> { Title: {}\n\ Artist: {}\n\ Quality: {}", - track.map(|t| t.id.number.to_string()).unwrap_or_default(), + track.map(|t| t.number.0.to_string()).unwrap_or_default(), track.map(|t| t.id.title.as_str()).unwrap_or(""), track.map(|t| t.artist.join("; ")).unwrap_or_default(), track .map(|t| match t.quality.format { - Format::Flac => "FLAC".to_string(), - Format::Mp3 => format!("MP3 {}kbps", t.quality.bitrate), + TrackFormat::Flac => "FLAC".to_string(), + TrackFormat::Mp3 => format!("MP3 {}kbps", t.quality.bitrate), }) .unwrap_or_default(), )); diff --git a/tests/database/json.rs b/tests/database/json.rs index 0f9d6b2..f2c6c52 100644 --- a/tests/database/json.rs +++ b/tests/database/json.rs @@ -4,7 +4,7 @@ use once_cell::sync::Lazy; use tempfile::NamedTempFile; use musichoard::{ - collection::{artist::Artist, Collection}, + collection::{album::AlbumDate, artist::Artist, Collection}, database::{ json::{backend::JsonDatabaseFileBackend, JsonDatabase}, IDatabase, @@ -19,7 +19,10 @@ pub static DATABASE_TEST_FILE: Lazy = fn expected() -> Collection { let mut expected = COLLECTION.to_owned(); for artist in expected.iter_mut() { - artist.albums.clear(); + for album in artist.albums.iter_mut() { + album.date = AlbumDate::default(); + album.tracks.clear(); + } } expected } diff --git a/tests/files/database/database.json b/tests/files/database/database.json index 465e2b9..3515a9b 100644 --- a/tests/files/database/database.json +++ b/tests/files/database/database.json @@ -1 +1 @@ -{"V20240210":[{"name":"Аркона","sort":"Arkona","musicbrainz":"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212","properties":{"Bandcamp":["https://arkonamoscow.bandcamp.com/"],"MusicButler":["https://www.musicbutler.io/artist-page/283448581"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums"]}},{"name":"Eluveitie","sort":null,"musicbrainz":"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/269358403"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums"]}},{"name":"Frontside","sort":null,"musicbrainz":"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/826588800"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums"]}},{"name":"Heaven’s Basement","sort":"Heaven’s Basement","musicbrainz":"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/291158685"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums"]}},{"name":"Metallica","sort":null,"musicbrainz":"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/3996865"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums"]}}]} \ No newline at end of file +{"V20240302":[{"name":"Аркона","sort":"Arkona","musicbrainz":"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212","properties":{"Bandcamp":["https://arkonamoscow.bandcamp.com/"],"MusicButler":["https://www.musicbutler.io/artist-page/283448581"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums"]},"albums":[{"title":"Slovo","seq":0}]},{"name":"Eluveitie","sort":null,"musicbrainz":"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/269358403"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums"]},"albums":[{"title":"Vên [re‐recorded]","seq":0},{"title":"Slania","seq":0}]},{"name":"Frontside","sort":null,"musicbrainz":"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/826588800"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums"]},"albums":[{"title":"…nasze jest królestwo, potęga i chwała na wieki…","seq":0}]},{"name":"Heaven’s Basement","sort":"Heaven’s Basement","musicbrainz":"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/291158685"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums"]},"albums":[{"title":"Paper Plague","seq":0},{"title":"Unbreakable","seq":0}]},{"name":"Metallica","sort":null,"musicbrainz":"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab","properties":{"MusicButler":["https://www.musicbutler.io/artist-page/3996865"],"Qobuz":["https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums"]},"albums":[{"title":"Ride the Lightning","seq":0},{"title":"S&M","seq":0}]}]} \ No newline at end of file diff --git a/tests/library/testmod.rs b/tests/library/testmod.rs index bfd1658..cb85c50 100644 --- a/tests/library/testmod.rs +++ b/tests/library/testmod.rs @@ -1,6 +1,9 @@ use once_cell::sync::Lazy; -use musichoard::{collection::track::Format, library::Item}; +use musichoard::{ + collection::{album::AlbumMonth, track::TrackFormat}, + library::Item, +}; pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { vec![ @@ -8,880 +11,1040 @@ pub static LIBRARY_ITEMS: Lazy> = Lazy::new(|| -> Vec { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slovo"), track_number: 1, track_title: String::from("Az’"), track_artist: vec![String::from("Аркона")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 992, }, Item { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slovo"), track_number: 2, track_title: String::from("Arkaim"), track_artist: vec![String::from("Аркона")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1061, }, Item { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slovo"), track_number: 3, track_title: String::from("Bol’no mne"), track_artist: vec![String::from("Аркона")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1004, }, Item { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slovo"), track_number: 4, track_title: String::from("Leshiy"), track_artist: vec![String::from("Аркона")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1077, }, Item { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slovo"), track_number: 5, track_title: String::from("Zakliatie"), track_artist: vec![String::from("Аркона")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1041, }, Item { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slovo"), track_number: 6, track_title: String::from("Predok"), track_artist: vec![String::from("Аркона")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 756, }, Item { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slovo"), track_number: 7, track_title: String::from("Nikogda"), track_artist: vec![String::from("Аркона")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1059, }, Item { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slovo"), track_number: 8, track_title: String::from("Tam za tumanami"), track_artist: vec![String::from("Аркона")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1023, }, Item { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slovo"), track_number: 9, track_title: String::from("Potomok"), track_artist: vec![String::from("Аркона")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 838, }, Item { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slovo"), track_number: 10, track_title: String::from("Slovo"), track_artist: vec![String::from("Аркона")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1028, }, Item { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slovo"), track_number: 11, track_title: String::from("Odna"), track_artist: vec![String::from("Аркона")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 991, }, Item { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slovo"), track_number: 12, track_title: String::from("Vo moiom sadochke…"), track_artist: vec![String::from("Аркона")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 919, }, Item { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slovo"), track_number: 13, track_title: String::from("Stenka na stenku"), track_artist: vec![String::from("Аркона")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1039, }, Item { album_artist: String::from("Аркона"), album_artist_sort: Some(String::from("Arkona")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slovo"), track_number: 14, track_title: String::from("Zimushka"), track_artist: vec![String::from("Аркона")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 974, }, Item { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2004, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Vên [re‐recorded]"), track_number: 1, track_title: String::from("Verja Urit an Bitus"), track_artist: vec![String::from("Eluveitie")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 961, }, Item { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2004, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Vên [re‐recorded]"), track_number: 2, track_title: String::from("Uis Elveti"), track_artist: vec![String::from("Eluveitie")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1067, }, Item { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2004, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Vên [re‐recorded]"), track_number: 3, track_title: String::from("Ôrô"), track_artist: vec![String::from("Eluveitie")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 933, }, Item { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2004, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Vên [re‐recorded]"), track_number: 4, track_title: String::from("Lament"), track_artist: vec![String::from("Eluveitie")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1083, }, Item { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2004, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Vên [re‐recorded]"), track_number: 5, track_title: String::from("Druid"), track_artist: vec![String::from("Eluveitie")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1073, }, Item { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2004, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Vên [re‐recorded]"), track_number: 6, track_title: String::from("Jêzaïg"), track_artist: vec![String::from("Eluveitie")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1002, }, Item { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slania"), track_number: 1, track_title: String::from("Samon"), track_artist: vec![String::from("Eluveitie")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 953, }, Item { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slania"), track_number: 2, track_title: String::from("Primordial Breath"), track_artist: vec![String::from("Eluveitie")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1103, }, Item { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slania"), track_number: 3, track_title: String::from("Inis Mona"), track_artist: vec![String::from("Eluveitie")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1117, }, Item { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slania"), track_number: 4, track_title: String::from("Gray Sublime Archon"), track_artist: vec![String::from("Eluveitie")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1092, }, Item { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slania"), track_number: 5, track_title: String::from("Anagantios"), track_artist: vec![String::from("Eluveitie")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 923, }, Item { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slania"), track_number: 6, track_title: String::from("Bloodstained Ground"), track_artist: vec![String::from("Eluveitie")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1098, }, Item { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slania"), track_number: 7, track_title: String::from("The Somber Lay"), track_artist: vec![String::from("Eluveitie")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1068, }, Item { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slania"), track_number: 8, track_title: String::from("Slanias Song"), track_artist: vec![String::from("Eluveitie")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1098, }, Item { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slania"), track_number: 9, track_title: String::from("Giamonios"), track_artist: vec![String::from("Eluveitie")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 825, }, Item { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slania"), track_number: 10, track_title: String::from("Tarvos"), track_artist: vec![String::from("Eluveitie")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1115, }, Item { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slania"), track_number: 11, track_title: String::from("Calling the Rain"), track_artist: vec![String::from("Eluveitie")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1096, }, Item { album_artist: String::from("Eluveitie"), album_artist_sort: None, album_year: 2008, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Slania"), track_number: 12, track_title: String::from("Elembivos"), track_artist: vec![String::from("Eluveitie")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1059, }, Item { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 1, track_title: String::from("Intro = Chaos"), track_artist: vec![String::from("Frontside")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1024, }, Item { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 2, track_title: String::from("Modlitwa"), track_artist: vec![String::from("Frontside")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1073, }, Item { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 3, track_title: String::from("Długa droga z piekła"), track_artist: vec![String::from("Frontside")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1058, }, Item { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 4, track_title: String::from("Synowie ognia"), track_artist: vec![String::from("Frontside")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1066, }, Item { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 5, track_title: String::from("1902"), track_artist: vec![String::from("Frontside")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1074, }, Item { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 6, track_title: String::from("Krew za krew"), track_artist: vec![String::from("Frontside")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1080, }, Item { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 7, track_title: String::from("Kulminacja"), track_artist: vec![String::from("Frontside")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 992, }, Item { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 8, track_title: String::from("Judasz"), track_artist: vec![String::from("Frontside")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1018, }, Item { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 9, track_title: String::from("Więzy"), track_artist: vec![String::from("Frontside")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1077, }, Item { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 10, track_title: String::from("Zagubione dusze"), track_artist: vec![String::from("Frontside")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1033, }, Item { album_artist: String::from("Frontside"), album_artist_sort: None, album_year: 2001, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), track_number: 11, track_title: String::from("Linia życia"), track_artist: vec![String::from("Frontside")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 987, }, Item { album_artist: String::from("Heaven’s Basement"), album_artist_sort: None, album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Paper Plague"), track_number: 0, track_title: String::from("Paper Plague"), track_artist: vec![String::from("Heaven’s Basement")], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 320, }, Item { album_artist: String::from("Heaven’s Basement"), album_artist_sort: Some(String::from("Heaven’s Basement")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Unbreakable"), track_number: 1, track_title: String::from("Unbreakable"), track_artist: vec![String::from("Heaven’s Basement")], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 208, }, Item { album_artist: String::from("Heaven’s Basement"), album_artist_sort: Some(String::from("Heaven’s Basement")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Unbreakable"), track_number: 2, track_title: String::from("Guilt Trips and Sins"), track_artist: vec![String::from("Heaven’s Basement")], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 205, }, Item { album_artist: String::from("Heaven’s Basement"), album_artist_sort: Some(String::from("Heaven’s Basement")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Unbreakable"), track_number: 3, track_title: String::from("The Long Goodbye"), track_artist: vec![String::from("Heaven’s Basement")], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 227, }, Item { album_artist: String::from("Heaven’s Basement"), album_artist_sort: Some(String::from("Heaven’s Basement")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Unbreakable"), track_number: 4, track_title: String::from("Close Encounters"), track_artist: vec![String::from("Heaven’s Basement")], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 213, }, Item { album_artist: String::from("Heaven’s Basement"), album_artist_sort: Some(String::from("Heaven’s Basement")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Unbreakable"), track_number: 5, track_title: String::from("Paranoia"), track_artist: vec![String::from("Heaven’s Basement")], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 218, }, Item { album_artist: String::from("Heaven’s Basement"), album_artist_sort: Some(String::from("Heaven’s Basement")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Unbreakable"), track_number: 6, track_title: String::from("Let Me Out of Here"), track_artist: vec![String::from("Heaven’s Basement")], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 207, }, Item { album_artist: String::from("Heaven’s Basement"), album_artist_sort: Some(String::from("Heaven’s Basement")), album_year: 2011, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Unbreakable"), track_number: 7, track_title: String::from("Leeches"), track_artist: vec![String::from("Heaven’s Basement")], - track_format: Format::Mp3, + track_format: TrackFormat::Mp3, track_bitrate: 225, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1984, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Ride the Lightning"), track_number: 1, track_title: String::from("Fight Fire with Fire"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 954, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1984, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Ride the Lightning"), track_number: 2, track_title: String::from("Ride the Lightning"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 951, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1984, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Ride the Lightning"), track_number: 3, track_title: String::from("For Whom the Bell Tolls"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 889, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1984, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Ride the Lightning"), track_number: 4, track_title: String::from("Fade to Black"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 939, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1984, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Ride the Lightning"), track_number: 5, track_title: String::from("Trapped under Ice"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 955, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1984, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Ride the Lightning"), track_number: 6, track_title: String::from("Escape"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 941, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1984, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Ride the Lightning"), track_number: 7, track_title: String::from("Creeping Death"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 958, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1984, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("Ride the Lightning"), track_number: 8, track_title: String::from("The Call of Ktulu"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 888, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 1, track_title: String::from("The Ecstasy of Gold"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 875, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 2, track_title: String::from("The Call of Ktulu"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1030, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 3, track_title: String::from("Master of Puppets"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1082, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 4, track_title: String::from("Of Wolf and Man"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1115, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 5, track_title: String::from("The Thing That Should Not Be"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1029, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 6, track_title: String::from("Fuel"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1057, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 7, track_title: String::from("The Memory Remains"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1080, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 8, track_title: String::from("No Leaf Clover"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1004, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 9, track_title: String::from("Hero of the Day"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 962, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 10, track_title: String::from("Devil’s Dance"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1076, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 11, track_title: String::from("Bleeding Me"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 993, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 12, track_title: String::from("Nothing Else Matters"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 875, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 13, track_title: String::from("Until It Sleeps"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1038, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 14, track_title: String::from("For Whom the Bell Tolls"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1072, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 15, track_title: String::from("−Human"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1029, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 16, track_title: String::from("Wherever I May Roam"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1035, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 17, track_title: String::from("Outlaw Torn"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1042, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 18, track_title: String::from("Sad but True"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1082, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 19, track_title: String::from("One"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 1017, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 20, track_title: String::from("Enter Sandman"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 993, }, Item { album_artist: String::from("Metallica"), album_artist_sort: None, album_year: 1999, + album_month: AlbumMonth::None, + album_day: 0, album_title: String::from("S&M"), track_number: 21, track_title: String::from("Battery"), track_artist: vec![String::from("Metallica")], - track_format: Format::Flac, + track_format: TrackFormat::Flac, track_bitrate: 967, }, ] diff --git a/tests/testlib.rs b/tests/testlib.rs index ef4821e..d2bc4d3 100644 --- a/tests/testlib.rs +++ b/tests/testlib.rs @@ -1,10 +1,10 @@ use once_cell::sync::Lazy; -use std::collections::HashMap; +use std::{collections::HashMap, str::FromStr}; use musichoard::collection::{ - album::{Album, AlbumId}, + album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq}, artist::{Artist, ArtistId, MusicBrainz}, - track::{Format, Quality, Track, TrackId}, + track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality}, Collection, }; @@ -17,8 +17,8 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { sort: Some(ArtistId{ name: String::from("Arkona") }), - musicbrainz: Some(MusicBrainz::new( - "https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212", + musicbrainz: Some(MusicBrainz::from_str( + "https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212" ).unwrap()), properties: HashMap::from([ (String::from("MusicButler"), vec![ @@ -33,161 +33,166 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { ]), albums: vec![Album { id: AlbumId { - year: 2011, title: String::from("Slovo"), }, + date: AlbumDate { + year: 2011, + month: AlbumMonth::None, + day: 0, + }, + seq: AlbumSeq(0), tracks: vec![ Track { id: TrackId { - number: 1, title: String::from("Az’"), }, + number: TrackNum(1), artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 992, }, }, Track { id: TrackId { - number: 2, title: String::from("Arkaim"), }, + number: TrackNum(2), artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1061, }, }, Track { id: TrackId { - number: 3, title: String::from("Bol’no mne"), }, + number: TrackNum(3), artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1004, }, }, Track { id: TrackId { - number: 4, title: String::from("Leshiy"), }, + number: TrackNum(4), artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1077, }, }, Track { id: TrackId { - number: 5, title: String::from("Zakliatie"), }, + number: TrackNum(5), artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1041, }, }, Track { id: TrackId { - number: 6, title: String::from("Predok"), }, + number: TrackNum(6), artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 756, }, }, Track { id: TrackId { - number: 7, title: String::from("Nikogda"), }, + number: TrackNum(7), artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1059, }, }, Track { id: TrackId { - number: 8, title: String::from("Tam za tumanami"), }, + number: TrackNum(8), artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1023, }, }, Track { id: TrackId { - number: 9, title: String::from("Potomok"), }, + number: TrackNum(9), artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 838, }, }, Track { id: TrackId { - number: 10, title: String::from("Slovo"), }, + number: TrackNum(10), artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1028, }, }, Track { id: TrackId { - number: 11, title: String::from("Odna"), }, + number: TrackNum(11), artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 991, }, }, Track { id: TrackId { - number: 12, title: String::from("Vo moiom sadochke…"), }, + number: TrackNum(12), artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 919, }, }, Track { id: TrackId { - number: 13, title: String::from("Stenka na stenku"), }, + number: TrackNum(13), artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1039, }, }, Track { id: TrackId { - number: 14, title: String::from("Zimushka"), }, + number: TrackNum(14), artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 974, }, }, @@ -199,7 +204,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { name: String::from("Eluveitie"), }, sort: None, - musicbrainz: Some(MusicBrainz::new( + musicbrainz: Some(MusicBrainz::from_str( "https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38", ).unwrap()), properties: HashMap::from([ @@ -213,73 +218,78 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { albums: vec![ Album { id: AlbumId { - year: 2004, title: String::from("Vên [re‐recorded]"), }, + date: AlbumDate { + year: 2004, + month: AlbumMonth::None, + day: 0, + }, + seq: AlbumSeq(0), tracks: vec![ Track { id: TrackId { - number: 1, title: String::from("Verja Urit an Bitus"), }, + number: TrackNum(1), artist: vec![String::from("Eluveitie")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 961, }, }, Track { id: TrackId { - number: 2, title: String::from("Uis Elveti"), }, + number: TrackNum(2), artist: vec![String::from("Eluveitie")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1067, }, }, Track { id: TrackId { - number: 3, title: String::from("Ôrô"), }, + number: TrackNum(3), artist: vec![String::from("Eluveitie")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 933, }, }, Track { id: TrackId { - number: 4, title: String::from("Lament"), }, + number: TrackNum(4), artist: vec![String::from("Eluveitie")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1083, }, }, Track { id: TrackId { - number: 5, title: String::from("Druid"), }, + number: TrackNum(5), artist: vec![String::from("Eluveitie")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1073, }, }, Track { id: TrackId { - number: 6, title: String::from("Jêzaïg"), }, + number: TrackNum(6), artist: vec![String::from("Eluveitie")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1002, }, }, @@ -287,139 +297,144 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { }, Album { id: AlbumId { - year: 2008, title: String::from("Slania"), }, + date: AlbumDate { + year: 2008, + month: AlbumMonth::None, + day: 0, + }, + seq: AlbumSeq(0), tracks: vec![ Track { id: TrackId { - number: 1, title: String::from("Samon"), }, + number: TrackNum(1), artist: vec![String::from("Eluveitie")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 953, }, }, Track { id: TrackId { - number: 2, title: String::from("Primordial Breath"), }, + number: TrackNum(2), artist: vec![String::from("Eluveitie")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1103, }, }, Track { id: TrackId { - number: 3, title: String::from("Inis Mona"), }, + number: TrackNum(3), artist: vec![String::from("Eluveitie")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1117, }, }, Track { id: TrackId { - number: 4, title: String::from("Gray Sublime Archon"), }, + number: TrackNum(4), artist: vec![String::from("Eluveitie")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1092, }, }, Track { id: TrackId { - number: 5, title: String::from("Anagantios"), }, + number: TrackNum(5), artist: vec![String::from("Eluveitie")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 923, }, }, Track { id: TrackId { - number: 6, title: String::from("Bloodstained Ground"), }, + number: TrackNum(6), artist: vec![String::from("Eluveitie")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1098, }, }, Track { id: TrackId { - number: 7, title: String::from("The Somber Lay"), }, + number: TrackNum(7), artist: vec![String::from("Eluveitie")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1068, }, }, Track { id: TrackId { - number: 8, title: String::from("Slanias Song"), }, + number: TrackNum(8), artist: vec![String::from("Eluveitie")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1098, }, }, Track { id: TrackId { - number: 9, title: String::from("Giamonios"), }, + number: TrackNum(9), artist: vec![String::from("Eluveitie")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 825, }, }, Track { id: TrackId { - number: 10, title: String::from("Tarvos"), }, + number: TrackNum(10), artist: vec![String::from("Eluveitie")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1115, }, }, Track { id: TrackId { - number: 11, title: String::from("Calling the Rain"), }, + number: TrackNum(11), artist: vec![String::from("Eluveitie")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1096, }, }, Track { id: TrackId { - number: 12, title: String::from("Elembivos"), }, + number: TrackNum(12), artist: vec![String::from("Eluveitie")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1059, }, }, @@ -432,7 +447,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { name: String::from("Frontside"), }, sort: None, - musicbrainz: Some(MusicBrainz::new( + musicbrainz: Some(MusicBrainz::from_str( "https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490", ).unwrap()), properties: HashMap::from([ @@ -445,128 +460,133 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { ]), albums: vec![Album { id: AlbumId { - year: 2001, title: String::from("…nasze jest królestwo, potęga i chwała na wieki…"), }, + date: AlbumDate { + year: 2001, + month: AlbumMonth::None, + day: 0, + }, + seq: AlbumSeq(0), tracks: vec![ Track { id: TrackId { - number: 1, title: String::from("Intro = Chaos"), }, + number: TrackNum(1), artist: vec![String::from("Frontside")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1024, }, }, Track { id: TrackId { - number: 2, title: String::from("Modlitwa"), }, + number: TrackNum(2), artist: vec![String::from("Frontside")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1073, }, }, Track { id: TrackId { - number: 3, title: String::from("Długa droga z piekła"), }, + number: TrackNum(3), artist: vec![String::from("Frontside")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1058, }, }, Track { id: TrackId { - number: 4, title: String::from("Synowie ognia"), }, + number: TrackNum(4), artist: vec![String::from("Frontside")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1066, }, }, Track { id: TrackId { - number: 5, title: String::from("1902"), }, + number: TrackNum(5), artist: vec![String::from("Frontside")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1074, }, }, Track { id: TrackId { - number: 6, title: String::from("Krew za krew"), }, + number: TrackNum(6), artist: vec![String::from("Frontside")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1080, }, }, Track { id: TrackId { - number: 7, title: String::from("Kulminacja"), }, + number: TrackNum(7), artist: vec![String::from("Frontside")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 992, }, }, Track { id: TrackId { - number: 8, title: String::from("Judasz"), }, + number: TrackNum(8), artist: vec![String::from("Frontside")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1018, }, }, Track { id: TrackId { - number: 9, title: String::from("Więzy"), }, + number: TrackNum(9), artist: vec![String::from("Frontside")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1077, }, }, Track { id: TrackId { - number: 10, title: String::from("Zagubione dusze"), }, + number: TrackNum(10), artist: vec![String::from("Frontside")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1033, }, }, Track { id: TrackId { - number: 11, title: String::from("Linia życia"), }, + number: TrackNum(11), artist: vec![String::from("Frontside")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 987, }, }, @@ -580,7 +600,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { sort: Some(ArtistId { name: String::from("Heaven’s Basement"), }), - musicbrainz: Some(MusicBrainz::new( + musicbrainz: Some(MusicBrainz::from_str( "https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc", ).unwrap()), properties: HashMap::from([ @@ -593,102 +613,112 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { ]), albums: vec![Album { id: AlbumId { - year: 2011, title: String::from("Paper Plague"), }, + date: AlbumDate { + year: 2011, + month: AlbumMonth::None, + day: 0, + }, + seq: AlbumSeq(0), tracks: vec![ Track { id: TrackId { - number: 0, title: String::from("Paper Plague"), }, + number: TrackNum(0), artist: vec![String::from("Heaven’s Basement")], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 320, }, }, ], }, Album { id: AlbumId { - year: 2011, title: String::from("Unbreakable"), }, + date: AlbumDate { + year: 2011, + month: AlbumMonth::None, + day: 0, + }, + seq: AlbumSeq(0), tracks: vec![ Track { id: TrackId { - number: 1, title: String::from("Unbreakable"), }, + number: TrackNum(1), artist: vec![String::from("Heaven’s Basement")], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 208, }, }, Track { id: TrackId { - number: 2, title: String::from("Guilt Trips and Sins"), }, + number: TrackNum(2), artist: vec![String::from("Heaven’s Basement")], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 205, }, }, Track { id: TrackId { - number: 3, title: String::from("The Long Goodbye"), }, + number: TrackNum(3), artist: vec![String::from("Heaven’s Basement")], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 227, }, }, Track { id: TrackId { - number: 4, title: String::from("Close Encounters"), }, + number: TrackNum(4), artist: vec![String::from("Heaven’s Basement")], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 213, }, }, Track { id: TrackId { - number: 5, title: String::from("Paranoia"), }, + number: TrackNum(5), artist: vec![String::from("Heaven’s Basement")], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 218, }, }, Track { id: TrackId { - number: 6, title: String::from("Let Me Out of Here"), }, + number: TrackNum(6), artist: vec![String::from("Heaven’s Basement")], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 207, }, }, Track { id: TrackId { - number: 7, title: String::from("Leeches"), }, + number: TrackNum(7), artist: vec![String::from("Heaven’s Basement")], - quality: Quality { - format: Format::Mp3, + quality: TrackQuality { + format: TrackFormat::Mp3, bitrate: 225, }, }, @@ -700,7 +730,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { name: String::from("Metallica"), }, sort: None, - musicbrainz: Some(MusicBrainz::new( + musicbrainz: Some(MusicBrainz::from_str( "https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab", ).unwrap()), properties: HashMap::from([ @@ -714,95 +744,100 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { albums: vec![ Album { id: AlbumId { - year: 1984, title: String::from("Ride the Lightning"), }, + date: AlbumDate { + year: 1984, + month: AlbumMonth::None, + day: 0, + }, + seq: AlbumSeq(0), tracks: vec![ Track { id: TrackId { - number: 1, title: String::from("Fight Fire with Fire"), }, + number: TrackNum(1), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 954, }, }, Track { id: TrackId { - number: 2, title: String::from("Ride the Lightning"), }, + number: TrackNum(2), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 951, }, }, Track { id: TrackId { - number: 3, title: String::from("For Whom the Bell Tolls"), }, + number: TrackNum(3), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 889, }, }, Track { id: TrackId { - number: 4, title: String::from("Fade to Black"), }, + number: TrackNum(4), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 939, }, }, Track { id: TrackId { - number: 5, title: String::from("Trapped under Ice"), }, + number: TrackNum(5), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 955, }, }, Track { id: TrackId { - number: 6, title: String::from("Escape"), }, + number: TrackNum(6), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 941, }, }, Track { id: TrackId { - number: 7, title: String::from("Creeping Death"), }, + number: TrackNum(7), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 958, }, }, Track { id: TrackId { - number: 8, title: String::from("The Call of Ktulu"), }, + number: TrackNum(8), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 888, }, }, @@ -810,238 +845,243 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { }, Album { id: AlbumId { - year: 1999, title: String::from("S&M"), }, + date: AlbumDate { + year: 1999, + month: AlbumMonth::None, + day: 0, + }, + seq: AlbumSeq(0), tracks: vec![ Track { id: TrackId { - number: 1, title: String::from("The Ecstasy of Gold"), }, + number: TrackNum(1), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 875, }, }, Track { id: TrackId { - number: 2, title: String::from("The Call of Ktulu"), }, + number: TrackNum(2), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1030, }, }, Track { id: TrackId { - number: 3, title: String::from("Master of Puppets"), }, + number: TrackNum(3), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1082, }, }, Track { id: TrackId { - number: 4, title: String::from("Of Wolf and Man"), }, + number: TrackNum(4), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1115, }, }, Track { id: TrackId { - number: 5, title: String::from("The Thing That Should Not Be"), }, + number: TrackNum(5), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1029, }, }, Track { id: TrackId { - number: 6, title: String::from("Fuel"), }, + number: TrackNum(6), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1057, }, }, Track { id: TrackId { - number: 7, title: String::from("The Memory Remains"), }, + number: TrackNum(7), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1080, }, }, Track { id: TrackId { - number: 8, title: String::from("No Leaf Clover"), }, + number: TrackNum(8), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1004, }, }, Track { id: TrackId { - number: 9, title: String::from("Hero of the Day"), }, + number: TrackNum(9), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 962, }, }, Track { id: TrackId { - number: 10, title: String::from("Devil’s Dance"), }, + number: TrackNum(10), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1076, }, }, Track { id: TrackId { - number: 11, title: String::from("Bleeding Me"), }, + number: TrackNum(11), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 993, }, }, Track { id: TrackId { - number: 12, title: String::from("Nothing Else Matters"), }, + number: TrackNum(12), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 875, }, }, Track { id: TrackId { - number: 13, title: String::from("Until It Sleeps"), }, + number: TrackNum(13), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1038, }, }, Track { id: TrackId { - number: 14, title: String::from("For Whom the Bell Tolls"), }, + number: TrackNum(14), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1072, }, }, Track { id: TrackId { - number: 15, title: String::from("−Human"), }, + number: TrackNum(15), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1029, }, }, Track { id: TrackId { - number: 16, title: String::from("Wherever I May Roam"), }, + number: TrackNum(16), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1035, }, }, Track { id: TrackId { - number: 17, title: String::from("Outlaw Torn"), }, + number: TrackNum(17), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1042, }, }, Track { id: TrackId { - number: 18, title: String::from("Sad but True"), }, + number: TrackNum(18), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1082, }, }, Track { id: TrackId { - number: 19, title: String::from("One"), }, + number: TrackNum(19), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 1017, }, }, Track { id: TrackId { - number: 20, title: String::from("Enter Sandman"), }, + number: TrackNum(20), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 993, }, }, Track { id: TrackId { - number: 21, title: String::from("Battery"), }, + number: TrackNum(21), artist: vec![String::from("Metallica")], - quality: Quality { - format: Format::Flac, + quality: TrackQuality { + format: TrackFormat::Flac, bitrate: 967, }, },