diff --git a/src/bin/musichoard-edit.rs b/src/bin/musichoard-edit.rs index 58fc8f8..7c180e7 100644 --- a/src/bin/musichoard-edit.rs +++ b/src/bin/musichoard-edit.rs @@ -44,6 +44,8 @@ enum ArtistCommand { Add(ArtistValue), #[structopt(about = "Remove an artist from the collection")] Remove(ArtistValue), + #[structopt(about = "Edit the artist's sort name")] + Sort(SortCommand), #[structopt(name = "musicbrainz", about = "Edit the MusicBrainz URL of an artist")] MusicBrainz(UrlCommand), #[structopt( @@ -57,12 +59,28 @@ enum ArtistCommand { Qobuz(UrlCommand), } +#[derive(StructOpt, Debug)] +enum SortCommand { + #[structopt(help = "Set the provided name as the artist's sort name")] + Set(ArtistSortValue), + #[structopt(help = "Clear the artist's sort name")] + Clear(ArtistValue), +} + #[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")] + sort: String, +} + #[derive(StructOpt, Debug)] enum UrlCommand { #[structopt(about = "Add the provided URL(s) without overwriting existing values")] @@ -137,6 +155,9 @@ impl ArtistCommand { ArtistCommand::Remove(artist_value) => { music_hoard.remove_artist(ArtistId::new(artist_value.artist)); } + ArtistCommand::Sort(sort_command) => { + sort_command.handle(music_hoard); + } ArtistCommand::MusicBrainz(url_command) => { single_url_command_dispatch!(url_command, music_hoard, musicbrainz) } @@ -153,6 +174,22 @@ impl ArtistCommand { } } +impl SortCommand { + fn handle(self, music_hoard: &mut MH) { + match self { + SortCommand::Set(artist_sort_value) => music_hoard + .set_artist_sort( + ArtistId::new(artist_sort_value.artist), + 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)) + .expect("failed to clear artist sort name"), + } + } +} + fn main() { let opt = Opt::from_args(); diff --git a/src/database/json/mod.rs b/src/database/json/mod.rs index 5acce3c..477d08d 100644 --- a/src/database/json/mod.rs +++ b/src/database/json/mod.rs @@ -65,26 +65,28 @@ mod tests { use super::*; - use crate::{tests::COLLECTION, Artist, ArtistId, Collection, Format, IUrl}; + use crate::{tests::COLLECTION, Artist, ArtistId, Collection, Format}; - fn opt_to_url(opt: &Option) -> String { + fn opt_to_str>(opt: &Option) -> String { match opt { - Some(mb) => format!("\"{}\"", mb.url()), + Some(val) => format!("\"{}\"", val.as_ref()), None => String::from("null"), } } - fn vec_to_urls(vec: &[U]) -> String { + fn vec_to_str>(vec: &[S]) -> String { let mut urls: Vec = vec![]; for item in vec.iter() { - urls.push(format!("\"{}\"", item.url())); + urls.push(format!("\"{}\"", item.as_ref())); } format!("[{}]", urls.join(",")) } - fn artist_to_json(artist: &Artist) -> String { - let album_artist = &artist.id.name; + fn artist_id_to_str(id: &ArtistId) -> String { + format!("{{\"name\":\"{}\"}}", id.name) + } + fn artist_to_json(artist: &Artist) -> String { let mut albums: Vec = vec![]; for album in artist.albums.iter() { let album_year = album.id.year; @@ -127,10 +129,10 @@ mod tests { } let albums = albums.join(","); - let musicbrainz = opt_to_url(&artist.properties.musicbrainz); - let musicbutler = vec_to_urls(&artist.properties.musicbutler); - let bandcamp = vec_to_urls(&artist.properties.bandcamp); - let qobuz = opt_to_url(&artist.properties.qobuz); + let musicbrainz = opt_to_str(&artist.properties.musicbrainz); + let musicbutler = vec_to_str(&artist.properties.musicbutler); + let bandcamp = vec_to_str(&artist.properties.bandcamp); + let qobuz = opt_to_str(&artist.properties.qobuz); let properties = format!( "{{\ @@ -141,9 +143,17 @@ mod tests { }}" ); + let album_artist = artist_id_to_str(&artist.id); + let album_artist_sort = artist + .sort + .as_ref() + .map(artist_id_to_str) + .unwrap_or_else(|| "null".to_string()); + format!( "{{\ - \"id\":{{\"name\":\"{album_artist}\"}},\ + \"id\":{album_artist},\ + \"sort\":{album_artist_sort},\ \"properties\":{properties},\ \"albums\":[{albums}]\ }}" @@ -225,9 +235,7 @@ mod tests { fn save_errors() { let mut object = HashMap::::new(); object.insert( - ArtistId { - name: String::from("artist"), - }, + ArtistId::new(String::from("artist")), String::from("string"), ); let serde_err = serde_json::to_string(&object); diff --git a/src/lib.rs b/src/lib.rs index 535fa40..a782173 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,11 +18,6 @@ use serde::{Deserialize, Serialize}; use url::Url; use uuid::Uuid; -/// An object with the [`IUrl`] trait contains a valid URL. -pub trait IUrl { - fn url(&self) -> &str; -} - /// An object with the [`IMbid`] trait contains a [MusicBrainz /// Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID). pub trait IMbid { @@ -83,6 +78,12 @@ impl MusicBrainz { } } +impl AsRef for MusicBrainz { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + impl TryFrom<&str> for MusicBrainz { type Error = Error; @@ -97,12 +98,6 @@ impl Display for MusicBrainz { } } -impl IUrl for MusicBrainz { - fn url(&self) -> &str { - self.0.as_str() - } -} - impl IMbid for MusicBrainz { fn mbid(&self) -> &str { // The URL is assumed to have been validated. @@ -142,6 +137,12 @@ impl MusicButler { } } +impl AsRef for MusicButler { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + impl TryFrom<&str> for MusicButler { type Error = Error; @@ -150,12 +151,6 @@ impl TryFrom<&str> for MusicButler { } } -impl IUrl for MusicButler { - fn url(&self) -> &str { - self.0.as_str() - } -} - /// Bandcamp reference. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] pub struct Bandcamp(Url); @@ -188,6 +183,12 @@ impl Bandcamp { } } +impl AsRef for Bandcamp { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + impl TryFrom<&str> for Bandcamp { type Error = Error; @@ -196,12 +197,6 @@ impl TryFrom<&str> for Bandcamp { } } -impl IUrl for Bandcamp { - fn url(&self) -> &str { - self.0.as_str() - } -} - /// Qobuz reference. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] pub struct Qobuz(Url); @@ -230,6 +225,12 @@ impl Qobuz { } } +impl AsRef for Qobuz { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + impl TryFrom<&str> for Qobuz { type Error = Error; @@ -244,12 +245,6 @@ impl Display for Qobuz { } } -impl IUrl for Qobuz { - fn url(&self) -> &str { - self.0.as_str() - } -} - /// The track file format. #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] pub enum Format { @@ -380,6 +375,7 @@ impl Merge for ArtistProperties { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct Artist { pub id: ArtistId, + pub sort: Option, pub properties: ArtistProperties, pub albums: Vec, } @@ -433,11 +429,24 @@ impl Artist { pub fn new>(id: ID) -> Self { Artist { id: id.into(), + sort: None, properties: ArtistProperties::default(), albums: vec![], } } + fn get_sort_key(&self) -> &ArtistId { + self.sort.as_ref().unwrap_or(&self.id) + } + + fn set_sort_key>(&mut self, sort: SORT) { + self.sort = Some(sort.into()); + } + + fn clear_sort_key(&mut self) { + _ = self.sort.take(); + } + fn add_unique_url, T: for<'a> TryFrom<&'a str, Error = Error> + Eq + Display>( container: &mut Option, url: S, @@ -552,13 +561,14 @@ impl PartialOrd for Artist { impl Ord for Artist { 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 Artist { fn merge(mut self, other: Self) -> Self { assert_eq!(self.id, other.id); + self.sort = Self::merge_opts(self.sort, other.sort); self.properties = self.properties.merge(other.properties); self.albums = MergeSorted::new(self.albums.into_iter(), other.albums.into_iter()).collect(); self @@ -719,7 +729,7 @@ macro_rules! music_hoard_unique_url_dispatch { artist_id: ID, url: S, ) -> Result<(), Error> { - self.get_artist_or_err(artist_id.as_ref())?.[](url) + self.get_artist_mut_or_err(artist_id.as_ref())?.[](url) } pub fn [], S: AsRef>( @@ -727,7 +737,7 @@ macro_rules! music_hoard_unique_url_dispatch { artist_id: ID, url: S, ) -> Result<(), Error> { - self.get_artist_or_err(artist_id.as_ref())?.[](url) + self.get_artist_mut_or_err(artist_id.as_ref())?.[](url) } pub fn [], S: AsRef>( @@ -735,14 +745,14 @@ macro_rules! music_hoard_unique_url_dispatch { artist_id: ID, url: S, ) -> Result<(), Error> { - self.get_artist_or_err(artist_id.as_ref())?.[](url) + self.get_artist_mut_or_err(artist_id.as_ref())?.[](url) } pub fn []>( &mut self, artist_id: ID, ) -> Result<(), Error> { - self.get_artist_or_err(artist_id.as_ref())?.[](); + self.get_artist_mut_or_err(artist_id.as_ref())?.[](); Ok(()) } } @@ -757,7 +767,7 @@ macro_rules! music_hoard_multi_url_dispatch { artist_id: ID, urls: Vec, ) -> Result<(), Error> { - self.get_artist_or_err(artist_id.as_ref())?.[](urls) + self.get_artist_mut_or_err(artist_id.as_ref())?.[](urls) } pub fn [], S: AsRef>( @@ -765,7 +775,7 @@ macro_rules! music_hoard_multi_url_dispatch { artist_id: ID, urls: Vec, ) -> Result<(), Error> { - self.get_artist_or_err(artist_id.as_ref())?.[](urls) + self.get_artist_mut_or_err(artist_id.as_ref())?.[](urls) } pub fn [], S: AsRef>( @@ -773,13 +783,13 @@ macro_rules! music_hoard_multi_url_dispatch { artist_id: ID, urls: Vec, ) -> Result<(), Error> { - self.get_artist_or_err(artist_id.as_ref())?.[](urls) + self.get_artist_mut_or_err(artist_id.as_ref())?.[](urls) } pub fn []>( &mut self, artist_id: ID, ) -> Result<(), Error> { - self.get_artist_or_err(artist_id.as_ref())?.[](); + self.get_artist_mut_or_err(artist_id.as_ref())?.[](); Ok(()) } } @@ -823,6 +833,24 @@ impl MusicHoard { } } + pub fn set_artist_sort, SORT: Into>( + &mut self, + artist_id: ID, + artist_sort: SORT, + ) -> Result<(), Error> { + self.get_artist_mut_or_err(artist_id.as_ref())? + .set_sort_key(artist_sort); + Self::sort(&mut self.collection); + Ok(()) + } + + pub fn clear_artist_sort>(&mut self, artist_id: ID) -> Result<(), Error> { + self.get_artist_mut_or_err(artist_id.as_ref())? + .clear_sort_key(); + Self::sort(&mut self.collection); + Ok(()) + } + music_hoard_unique_url_dispatch!(musicbrainz); music_hoard_multi_url_dispatch!(musicbutler); @@ -845,7 +873,7 @@ impl MusicHoard { MergeSorted::new(primary.into_iter(), secondary.into_iter()).collect() } - fn items_to_artists(items: Vec) -> Vec { + fn items_to_artists(items: Vec) -> Result, Error> { let mut artists: Vec = vec![]; let mut album_ids = HashMap::>::new(); @@ -854,6 +882,8 @@ impl MusicHoard { name: item.album_artist, }; + let artist_sort = item.album_artist_sort.map(|s| ArtistId { name: s }); + let album_id = AlbumId { year: item.album_year, title: item.album_title, @@ -886,6 +916,19 @@ impl MusicHoard { artists.last_mut().unwrap() }; + if artist.sort.is_some() { + if artist_sort.is_some() && (artist.sort != artist_sort) { + return Err(Error::CollectionError(format!( + "multiple album_artist_sort found for artist '{}': '{}' != '{}'", + artist.id, + artist.sort.as_ref().unwrap(), + artist_sort.as_ref().unwrap() + ))); + } + } else if artist_sort.is_some() { + artist.sort = artist_sort; + } + if album_ids[&artist_id].contains(&album_id) { // Assume results are in some order which means they will likely be grouped by // album. Therefore, we look from the back since the last inserted album is most @@ -909,15 +952,19 @@ impl MusicHoard { } } - artists + Ok(artists) } - fn get_artist(&mut self, artist_id: &ArtistId) -> Option<&mut Artist> { + fn get_artist(&self, artist_id: &ArtistId) -> Option<&Artist> { + self.collection.iter().find(|a| &a.id == artist_id) + } + + fn get_artist_mut(&mut self, artist_id: &ArtistId) -> Option<&mut Artist> { self.collection.iter_mut().find(|a| &a.id == artist_id) } - fn get_artist_or_err(&mut self, artist_id: &ArtistId) -> Result<&mut Artist, Error> { - self.get_artist(artist_id).ok_or_else(|| { + fn get_artist_mut_or_err(&mut self, artist_id: &ArtistId) -> Result<&mut Artist, Error> { + self.get_artist_mut(artist_id).ok_or_else(|| { Error::CollectionError(format!("artist '{}' is not in the collection", artist_id)) }) } @@ -927,7 +974,7 @@ impl MusicHoard { /// Rescan the library and merge with the in-memory collection. pub fn rescan_library(&mut self) -> Result<(), Error> { let items = self.library.list(&Query::new())?; - let mut library_collection = Self::items_to_artists(items); + let mut library_collection = Self::items_to_artists(items)?; Self::sort(&mut library_collection); let collection = mem::take(&mut self.collection); @@ -1036,6 +1083,7 @@ mod tests { for track in album.tracks.iter() { items.push(Item { album_artist: artist.id.name.clone(), + album_artist_sort: artist.sort.as_ref().map(|s| s.name.clone()), album_year: album.id.year, album_title: album.id.title.clone(), track_number: track.id.number, @@ -1070,7 +1118,7 @@ mod tests { 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.url()); + assert_eq!(url, mb.as_ref()); assert_eq!(uuid, mb.mbid()); let url = "not a url at all".to_string(); @@ -1142,6 +1190,53 @@ mod tests { assert_eq!(music_hoard.collection, expected); } + #[test] + fn artist_sort_set_clear() { + let mut music_hoard = MusicHoardBuilder::default().build(); + + let artist_1_id = ArtistId::new("the artist"); + let artist_1_sort = ArtistId::new("artist, the"); + + // Must be after "artist, the", but before "the artist" + let artist_2_id = ArtistId::new("b-artist"); + + assert!(artist_1_sort < artist_2_id); + assert!(artist_2_id < artist_1_id); + + music_hoard.add_artist(artist_1_id.clone()); + music_hoard.add_artist(artist_2_id.clone()); + + let artist_1: &Artist = music_hoard.get_artist(&artist_1_id).unwrap(); + let artist_2: &Artist = music_hoard.get_artist(&artist_2_id).unwrap(); + + assert!(artist_2 < artist_1); + + assert_eq!(artist_1, &music_hoard.collection[1]); + assert_eq!(artist_2, &music_hoard.collection[0]); + + music_hoard + .set_artist_sort(artist_1_id.as_ref(), artist_1_sort.clone()) + .unwrap(); + + let artist_1: &Artist = music_hoard.get_artist(&artist_1_id).unwrap(); + let artist_2: &Artist = music_hoard.get_artist(&artist_2_id).unwrap(); + + assert!(artist_1 < artist_2); + + assert_eq!(artist_1, &music_hoard.collection[0]); + assert_eq!(artist_2, &music_hoard.collection[1]); + + music_hoard.clear_artist_sort(artist_1_id.as_ref()).unwrap(); + + let artist_1: &Artist = music_hoard.get_artist(&artist_1_id).unwrap(); + let artist_2: &Artist = music_hoard.get_artist(&artist_2_id).unwrap(); + + assert!(artist_2 < artist_1); + + assert_eq!(artist_1, &music_hoard.collection[1]); + assert_eq!(artist_2, &music_hoard.collection[0]); + } + #[test] fn collection_error() { let artist_id = ArtistId::new("an artist"); @@ -1963,6 +2058,42 @@ mod tests { assert_eq!(music_hoard.get_collection(), &expected); } + #[test] + fn rescan_library_album_artist_sort_clash() { + let mut library = MockILibrary::new(); + let database = MockIDatabase::new(); + + let expected = clean_collection(COLLECTION.to_owned()); + let library_input = Query::new(); + let mut library_items = artists_to_items(&expected); + + assert_eq!(library_items[0].album_artist, library_items[1].album_artist); + library_items[0].album_artist_sort = Some(library_items[0].album_artist.clone()); + library_items[1].album_artist_sort = Some( + library_items[1] + .album_artist + .clone() + .chars() + .rev() + .collect(), + ); + + let library_result = Ok(library_items); + + library + .expect_list() + .with(predicate::eq(library_input)) + .times(1) + .return_once(|_| library_result); + + let mut music_hoard = MusicHoardBuilder::default() + .set_library(library) + .set_database(database) + .build(); + + assert!(music_hoard.rescan_library().is_err()); + } + #[test] fn load_database() { let library = MockILibrary::new(); diff --git a/src/library/beets/mod.rs b/src/library/beets/mod.rs index 9253016..bd30db0 100644 --- a/src/library/beets/mod.rs +++ b/src/library/beets/mod.rs @@ -22,6 +22,8 @@ const LIST_FORMAT_ARG: &str = concat!( "--format=", "$albumartist", list_format_separator!(), + "$albumartist_sort", + list_format_separator!(), "$year", list_format_separator!(), "$album", @@ -52,6 +54,7 @@ impl ToBeetsArg for Field { let negate = if include { "" } else { "^" }; match self { 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::AlbumTitle(ref s) => format!("{negate}album:{s}"), Field::TrackNumber(ref u) => format!("{negate}track:{u}"), @@ -123,29 +126,35 @@ impl ILibraryPrivate for BeetsLibrary { } let split: Vec<&str> = line.split(LIST_FORMAT_SEPARATOR).collect(); - if split.len() != 8 { + if split.len() != 9 { return Err(Error::Invalid(line.to_string())); } let album_artist = split[0].to_string(); - let album_year = split[1].parse::()?; - let album_title = split[2].to_string(); - let track_number = split[3].parse::()?; - let track_title = split[4].to_string(); - let track_artist = split[5] + let album_artist_sort = if !split[1].is_empty() { + Some(split[1].to_string()) + } else { + 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] .to_string() .split("; ") .map(|s| s.to_owned()) .collect(); - let track_format = match split[6].to_string().as_str() { + 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_bitrate = split[7].trim_end_matches("kbps").parse::()?; + let track_bitrate = split[8].trim_end_matches("kbps").parse::()?; items.push(Item { album_artist, + album_artist_sort, album_year, album_title, track_number, @@ -170,10 +179,15 @@ mod tests { fn item_to_beets_string(item: &Item) -> String { format!( - "{album_artist}{sep}{album_year}{sep}{album_title}{sep}\ + "{album_artist}{sep}{album_artist_sort}{sep}\ + {album_year}{sep}{album_title}{sep}\ {track_number}{sep}{track_title}{sep}\ {track_artist}{sep}{track_format}{sep}{track_bitrate}kbps", album_artist = item.album_artist, + album_artist_sort = match item.album_artist_sort { + Some(ref album_artist_sort) => album_artist_sort, + None => "", + }, album_year = item.album_year, album_title = item.album_title, track_number = item.track_number, @@ -221,6 +235,7 @@ mod tests { let mut query = Query::default() .exclude(Field::AlbumArtist(String::from("some.albumartist"))) + .exclude(Field::AlbumArtistSort(String::from("some.albumartist"))) .include(Field::AlbumYear(3030)) .include(Field::TrackTitle(String::from("some.track"))) .exclude(Field::TrackArtist(vec![ @@ -234,6 +249,7 @@ mod tests { query, vec![ String::from("^albumartist:some.albumartist"), + String::from("^albumartist_sort:some.albumartist"), String::from("^artist:some.artist.1; some.artist.2"), String::from("title:some.track"), String::from("year:3030"), @@ -349,8 +365,8 @@ mod tests { .split(LIST_FORMAT_SEPARATOR) .map(|s| s.to_owned()) .collect::>(); - invalid_string[6].clear(); - invalid_string[6].push_str("invalid format"); + invalid_string[7].clear(); + invalid_string[7].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/library/mod.rs b/src/library/mod.rs index bc476ed..53b53d3 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -30,6 +30,7 @@ impl ILibrary for NullLibrary { #[derive(Debug, PartialEq, Eq, Hash)] pub struct Item { pub album_artist: String, + pub album_artist_sort: Option, pub album_year: u32, pub album_title: String, pub track_number: u32, @@ -43,6 +44,7 @@ pub struct Item { #[derive(Debug, Hash, PartialEq, Eq)] pub enum Field { AlbumArtist(String), + AlbumArtistSort(String), AlbumYear(u32), AlbumTitle(String), TrackNumber(u32), diff --git a/src/testlib.rs b/src/testlib.rs index 0a4c306..f1de5d0 100644 --- a/src/testlib.rs +++ b/src/testlib.rs @@ -5,6 +5,7 @@ macro_rules! collection { id: ArtistId { name: "album_artist a".to_string(), }, + sort: None, properties: ArtistProperties { musicbrainz: Some(MusicBrainz::new( "https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000", @@ -100,6 +101,7 @@ macro_rules! collection { id: ArtistId { name: "album_artist b".to_string(), }, + sort: None, properties: ArtistProperties { musicbrainz: Some(MusicBrainz::new( "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111", @@ -192,6 +194,7 @@ macro_rules! collection { id: ArtistId { name: "album_artist c".to_string(), }, + sort: None, properties: ArtistProperties { musicbrainz: Some(MusicBrainz::new( "https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111", diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 8ca4b23..f9f0382 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -1,6 +1,6 @@ use std::fmt; -use musichoard::{Album, Artist, Collection, Format, IUrl, Track}; +use musichoard::{Album, Artist, Collection, Format, Track}; use ratatui::{ backend::Backend, layout::{Alignment, Rect}, @@ -409,20 +409,23 @@ struct ArtistOverlay<'a> { } impl<'a> ArtistOverlay<'a> { - fn opt_opt_to_str(opt: Option>) -> &str { - opt.flatten().map(|item| item.url()).unwrap_or("") + fn opt_opt_to_str>(opt: Option>) -> &str { + opt.flatten().map(|item| item.as_ref()).unwrap_or("") } - fn opt_vec_to_string(opt_vec: Option<&Vec>, indent: &str) -> String { + fn opt_vec_to_string>(opt_vec: Option<&Vec>, indent: &str) -> String { opt_vec .map(|vec| { if vec.len() < 2 { - vec.first().map(|item| item.url()).unwrap_or("").to_string() + vec.first() + .map(|item| item.as_ref()) + .unwrap_or("") + .to_string() } else { let indent = format!("\n{indent}"); let list = vec .iter() - .map(|item| item.url()) + .map(|item| item.as_ref()) .collect::>() .join(&indent); format!("{indent}{list}") diff --git a/tests/files/database/database.json b/tests/files/database/database.json index b633728..9c51a67 100644 --- a/tests/files/database/database.json +++ b/tests/files/database/database.json @@ -1 +1 @@ -[{"id":{"name":"Eluveitie"},"properties":{"musicbrainz":"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38","musicbutler":["https://www.musicbutler.io/artist-page/269358403"],"bandcamp":[],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums"},"albums":[{"id":{"year":2004,"title":"Vên [re‐recorded]"},"tracks":[{"id":{"number":1,"title":"Verja Urit an Bitus"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":961}},{"id":{"number":2,"title":"Uis Elveti"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1067}},{"id":{"number":3,"title":"Ôrô"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":933}},{"id":{"number":4,"title":"Lament"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1083}},{"id":{"number":5,"title":"Druid"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1073}},{"id":{"number":6,"title":"Jêzaïg"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1002}}]},{"id":{"year":2008,"title":"Slania"},"tracks":[{"id":{"number":1,"title":"Samon"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":953}},{"id":{"number":2,"title":"Primordial Breath"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1103}},{"id":{"number":3,"title":"Inis Mona"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1117}},{"id":{"number":4,"title":"Gray Sublime Archon"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1092}},{"id":{"number":5,"title":"Anagantios"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":923}},{"id":{"number":6,"title":"Bloodstained Ground"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1098}},{"id":{"number":7,"title":"The Somber Lay"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1068}},{"id":{"number":8,"title":"Slanias Song"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1098}},{"id":{"number":9,"title":"Giamonios"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":825}},{"id":{"number":10,"title":"Tarvos"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1115}},{"id":{"number":11,"title":"Calling the Rain"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1096}},{"id":{"number":12,"title":"Elembivos"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1059}}]}]},{"id":{"name":"Frontside"},"properties":{"musicbrainz":"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490","musicbutler":["https://www.musicbutler.io/artist-page/826588800"],"bandcamp":[],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums"},"albums":[{"id":{"year":2001,"title":"…nasze jest królestwo, potęga i chwała na wieki…"},"tracks":[{"id":{"number":1,"title":"Intro = Chaos"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1024}},{"id":{"number":2,"title":"Modlitwa"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1073}},{"id":{"number":3,"title":"Długa droga z piekła"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1058}},{"id":{"number":4,"title":"Synowie ognia"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1066}},{"id":{"number":5,"title":"1902"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1074}},{"id":{"number":6,"title":"Krew za krew"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1080}},{"id":{"number":7,"title":"Kulminacja"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":992}},{"id":{"number":8,"title":"Judasz"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1018}},{"id":{"number":9,"title":"Więzy"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1077}},{"id":{"number":10,"title":"Zagubione dusze"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1033}},{"id":{"number":11,"title":"Linia życia"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":987}}]}]},{"id":{"name":"Heaven’s Basement"},"properties":{"musicbrainz":"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc","musicbutler":["https://www.musicbutler.io/artist-page/291158685"],"bandcamp":[],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums"},"albums":[{"id":{"year":2011,"title":"Paper Plague"},"tracks":[{"id":{"number":0,"title":"Paper Plague"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":320}}]},{"id":{"year":2011,"title":"Unbreakable"},"tracks":[{"id":{"number":1,"title":"Unbreakable"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":208}},{"id":{"number":2,"title":"Guilt Trips and Sins"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":205}},{"id":{"number":3,"title":"The Long Goodbye"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":227}},{"id":{"number":4,"title":"Close Encounters"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":213}},{"id":{"number":5,"title":"Paranoia"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":218}},{"id":{"number":6,"title":"Let Me Out of Here"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":207}},{"id":{"number":7,"title":"Leeches"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":225}}]}]},{"id":{"name":"Metallica"},"properties":{"musicbrainz":"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab","musicbutler":["https://www.musicbutler.io/artist-page/3996865"],"bandcamp":[],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums"},"albums":[{"id":{"year":1984,"title":"Ride the Lightning"},"tracks":[{"id":{"number":1,"title":"Fight Fire with Fire"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":954}},{"id":{"number":2,"title":"Ride the Lightning"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":951}},{"id":{"number":3,"title":"For Whom the Bell Tolls"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":889}},{"id":{"number":4,"title":"Fade to Black"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":939}},{"id":{"number":5,"title":"Trapped under Ice"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":955}},{"id":{"number":6,"title":"Escape"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":941}},{"id":{"number":7,"title":"Creeping Death"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":958}},{"id":{"number":8,"title":"The Call of Ktulu"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":888}}]},{"id":{"year":1999,"title":"S&M"},"tracks":[{"id":{"number":1,"title":"The Ecstasy of Gold"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":875}},{"id":{"number":2,"title":"The Call of Ktulu"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1030}},{"id":{"number":3,"title":"Master of Puppets"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1082}},{"id":{"number":4,"title":"Of Wolf and Man"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1115}},{"id":{"number":5,"title":"The Thing That Should Not Be"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1029}},{"id":{"number":6,"title":"Fuel"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1057}},{"id":{"number":7,"title":"The Memory Remains"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1080}},{"id":{"number":8,"title":"No Leaf Clover"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1004}},{"id":{"number":9,"title":"Hero of the Day"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":962}},{"id":{"number":10,"title":"Devil’s Dance"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1076}},{"id":{"number":11,"title":"Bleeding Me"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":993}},{"id":{"number":12,"title":"Nothing Else Matters"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":875}},{"id":{"number":13,"title":"Until It Sleeps"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1038}},{"id":{"number":14,"title":"For Whom the Bell Tolls"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1072}},{"id":{"number":15,"title":"−Human"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1029}},{"id":{"number":16,"title":"Wherever I May Roam"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1035}},{"id":{"number":17,"title":"Outlaw Torn"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1042}},{"id":{"number":18,"title":"Sad but True"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1082}},{"id":{"number":19,"title":"One"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1017}},{"id":{"number":20,"title":"Enter Sandman"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":993}},{"id":{"number":21,"title":"Battery"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":967}}]}]},{"id":{"name":"Аркона"},"properties":{"musicbrainz":"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212","musicbutler":["https://www.musicbutler.io/artist-page/283448581"],"bandcamp":["https://arkonamoscow.bandcamp.com/"],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums"},"albums":[{"id":{"year":2011,"title":"Slovo"},"tracks":[{"id":{"number":1,"title":"Az’"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":992}},{"id":{"number":2,"title":"Arkaim"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1061}},{"id":{"number":3,"title":"Bol’no mne"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1004}},{"id":{"number":4,"title":"Leshiy"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1077}},{"id":{"number":5,"title":"Zakliatie"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1041}},{"id":{"number":6,"title":"Predok"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":756}},{"id":{"number":7,"title":"Nikogda"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1059}},{"id":{"number":8,"title":"Tam za tumanami"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1023}},{"id":{"number":9,"title":"Potomok"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":838}},{"id":{"number":10,"title":"Slovo"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1028}},{"id":{"number":11,"title":"Odna"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":991}},{"id":{"number":12,"title":"Vo moiom sadochke…"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":919}},{"id":{"number":13,"title":"Stenka na stenku"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1039}},{"id":{"number":14,"title":"Zimushka"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":974}}]}]}] \ No newline at end of file +[{"id":{"name":"Аркона"},"sort":{"name":"Arkona"},"properties":{"musicbrainz":"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212","musicbutler":["https://www.musicbutler.io/artist-page/283448581"],"bandcamp":["https://arkonamoscow.bandcamp.com/"],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums"},"albums":[{"id":{"year":2011,"title":"Slovo"},"tracks":[{"id":{"number":1,"title":"Az’"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":992}},{"id":{"number":2,"title":"Arkaim"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1061}},{"id":{"number":3,"title":"Bol’no mne"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1004}},{"id":{"number":4,"title":"Leshiy"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1077}},{"id":{"number":5,"title":"Zakliatie"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1041}},{"id":{"number":6,"title":"Predok"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":756}},{"id":{"number":7,"title":"Nikogda"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1059}},{"id":{"number":8,"title":"Tam za tumanami"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1023}},{"id":{"number":9,"title":"Potomok"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":838}},{"id":{"number":10,"title":"Slovo"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1028}},{"id":{"number":11,"title":"Odna"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":991}},{"id":{"number":12,"title":"Vo moiom sadochke…"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":919}},{"id":{"number":13,"title":"Stenka na stenku"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":1039}},{"id":{"number":14,"title":"Zimushka"},"artist":["Аркона"],"quality":{"format":"Flac","bitrate":974}}]}]},{"id":{"name":"Eluveitie"},"sort":null,"properties":{"musicbrainz":"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38","musicbutler":["https://www.musicbutler.io/artist-page/269358403"],"bandcamp":[],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/eluveitie/download-streaming-albums"},"albums":[{"id":{"year":2004,"title":"Vên [re‐recorded]"},"tracks":[{"id":{"number":1,"title":"Verja Urit an Bitus"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":961}},{"id":{"number":2,"title":"Uis Elveti"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1067}},{"id":{"number":3,"title":"Ôrô"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":933}},{"id":{"number":4,"title":"Lament"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1083}},{"id":{"number":5,"title":"Druid"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1073}},{"id":{"number":6,"title":"Jêzaïg"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1002}}]},{"id":{"year":2008,"title":"Slania"},"tracks":[{"id":{"number":1,"title":"Samon"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":953}},{"id":{"number":2,"title":"Primordial Breath"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1103}},{"id":{"number":3,"title":"Inis Mona"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1117}},{"id":{"number":4,"title":"Gray Sublime Archon"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1092}},{"id":{"number":5,"title":"Anagantios"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":923}},{"id":{"number":6,"title":"Bloodstained Ground"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1098}},{"id":{"number":7,"title":"The Somber Lay"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1068}},{"id":{"number":8,"title":"Slanias Song"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1098}},{"id":{"number":9,"title":"Giamonios"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":825}},{"id":{"number":10,"title":"Tarvos"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1115}},{"id":{"number":11,"title":"Calling the Rain"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1096}},{"id":{"number":12,"title":"Elembivos"},"artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1059}}]}]},{"id":{"name":"Frontside"},"sort":null,"properties":{"musicbrainz":"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490","musicbutler":["https://www.musicbutler.io/artist-page/826588800"],"bandcamp":[],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/frontside/download-streaming-albums"},"albums":[{"id":{"year":2001,"title":"…nasze jest królestwo, potęga i chwała na wieki…"},"tracks":[{"id":{"number":1,"title":"Intro = Chaos"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1024}},{"id":{"number":2,"title":"Modlitwa"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1073}},{"id":{"number":3,"title":"Długa droga z piekła"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1058}},{"id":{"number":4,"title":"Synowie ognia"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1066}},{"id":{"number":5,"title":"1902"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1074}},{"id":{"number":6,"title":"Krew za krew"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1080}},{"id":{"number":7,"title":"Kulminacja"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":992}},{"id":{"number":8,"title":"Judasz"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1018}},{"id":{"number":9,"title":"Więzy"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1077}},{"id":{"number":10,"title":"Zagubione dusze"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":1033}},{"id":{"number":11,"title":"Linia życia"},"artist":["Frontside"],"quality":{"format":"Flac","bitrate":987}}]}]},{"id":{"name":"Heaven’s Basement"},"sort":{"name":"Heaven’s Basement"},"properties":{"musicbrainz":"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc","musicbutler":["https://www.musicbutler.io/artist-page/291158685"],"bandcamp":[],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/heaven-s-basement/download-streaming-albums"},"albums":[{"id":{"year":2011,"title":"Paper Plague"},"tracks":[{"id":{"number":0,"title":"Paper Plague"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":320}}]},{"id":{"year":2011,"title":"Unbreakable"},"tracks":[{"id":{"number":1,"title":"Unbreakable"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":208}},{"id":{"number":2,"title":"Guilt Trips and Sins"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":205}},{"id":{"number":3,"title":"The Long Goodbye"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":227}},{"id":{"number":4,"title":"Close Encounters"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":213}},{"id":{"number":5,"title":"Paranoia"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":218}},{"id":{"number":6,"title":"Let Me Out of Here"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":207}},{"id":{"number":7,"title":"Leeches"},"artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":225}}]}]},{"id":{"name":"Metallica"},"sort":null,"properties":{"musicbrainz":"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab","musicbutler":["https://www.musicbutler.io/artist-page/3996865"],"bandcamp":[],"qobuz":"https://www.qobuz.com/nl-nl/interpreter/metallica/download-streaming-albums"},"albums":[{"id":{"year":1984,"title":"Ride the Lightning"},"tracks":[{"id":{"number":1,"title":"Fight Fire with Fire"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":954}},{"id":{"number":2,"title":"Ride the Lightning"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":951}},{"id":{"number":3,"title":"For Whom the Bell Tolls"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":889}},{"id":{"number":4,"title":"Fade to Black"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":939}},{"id":{"number":5,"title":"Trapped under Ice"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":955}},{"id":{"number":6,"title":"Escape"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":941}},{"id":{"number":7,"title":"Creeping Death"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":958}},{"id":{"number":8,"title":"The Call of Ktulu"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":888}}]},{"id":{"year":1999,"title":"S&M"},"tracks":[{"id":{"number":1,"title":"The Ecstasy of Gold"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":875}},{"id":{"number":2,"title":"The Call of Ktulu"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1030}},{"id":{"number":3,"title":"Master of Puppets"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1082}},{"id":{"number":4,"title":"Of Wolf and Man"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1115}},{"id":{"number":5,"title":"The Thing That Should Not Be"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1029}},{"id":{"number":6,"title":"Fuel"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1057}},{"id":{"number":7,"title":"The Memory Remains"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1080}},{"id":{"number":8,"title":"No Leaf Clover"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1004}},{"id":{"number":9,"title":"Hero of the Day"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":962}},{"id":{"number":10,"title":"Devil’s Dance"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1076}},{"id":{"number":11,"title":"Bleeding Me"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":993}},{"id":{"number":12,"title":"Nothing Else Matters"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":875}},{"id":{"number":13,"title":"Until It Sleeps"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1038}},{"id":{"number":14,"title":"For Whom the Bell Tolls"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1072}},{"id":{"number":15,"title":"−Human"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1029}},{"id":{"number":16,"title":"Wherever I May Roam"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1035}},{"id":{"number":17,"title":"Outlaw Torn"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1042}},{"id":{"number":18,"title":"Sad but True"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1082}},{"id":{"number":19,"title":"One"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":1017}},{"id":{"number":20,"title":"Enter Sandman"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":993}},{"id":{"number":21,"title":"Battery"},"artist":["Metallica"],"quality":{"format":"Flac","bitrate":967}}]}]}] \ No newline at end of file diff --git a/tests/files/library/library.db b/tests/files/library/library.db index c24c541..54665c5 100644 Binary files a/tests/files/library/library.db and b/tests/files/library/library.db differ diff --git a/tests/library/beets.rs b/tests/library/beets.rs index 96d1889..656485b 100644 --- a/tests/library/beets.rs +++ b/tests/library/beets.rs @@ -41,6 +41,7 @@ fn artist_to_items(artist: &Artist) -> Vec { for track in album.tracks.iter() { items.push(Item { album_artist: artist.id.name.clone(), + album_artist_sort: artist.sort.as_ref().map(|s| s.name.clone()), album_year: album.id.year, album_title: album.id.title.clone(), track_number: track.id.number, @@ -107,7 +108,7 @@ fn test_album_artist_query() { .list(Query::new().include(Field::AlbumArtist(String::from("Аркона")))) .unwrap(); - let expected: Vec = artists_to_items(&COLLECTION[4..5]); + let expected: Vec = artists_to_items(&COLLECTION[0..1]); assert_eq!(output, expected); } @@ -120,7 +121,7 @@ fn test_album_title_query() { .list(Query::new().include(Field::AlbumTitle(String::from("Slovo")))) .unwrap(); - let expected: Vec = artists_to_items(&COLLECTION[4..5]); + let expected: Vec = artists_to_items(&COLLECTION[0..1]); assert_eq!(output, expected); } @@ -132,7 +133,7 @@ fn test_exclude_query() { let output = beets .list(Query::new().exclude(Field::AlbumArtist(String::from("Аркона")))) .unwrap(); - let expected: Vec = artists_to_items(&COLLECTION[..4]); + let expected: Vec = artists_to_items(&COLLECTION[1..]); let output: HashSet<_> = output.iter().collect(); let expected: HashSet<_> = expected.iter().collect(); diff --git a/tests/testlib.rs b/tests/testlib.rs index 83de7c9..8df1677 100644 --- a/tests/testlib.rs +++ b/tests/testlib.rs @@ -6,10 +6,195 @@ use once_cell::sync::Lazy; pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { vec![ + Artist { + id: ArtistId { + name: String::from("Аркона"), + }, + sort: Some(ArtistId{ + name: String::from("Arkona") + }), + properties: ArtistProperties { + musicbrainz: Some(MusicBrainz::new( + "https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212", + ).unwrap()), + musicbutler: vec![ + MusicButler::new("https://www.musicbutler.io/artist-page/283448581").unwrap(), + ], + bandcamp: vec![ + Bandcamp::new("https://arkonamoscow.bandcamp.com/").unwrap(), + ], + qobuz: Some(Qobuz::new( + "https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums", + ).unwrap()), + }, + albums: vec![Album { + id: AlbumId { + year: 2011, + title: String::from("Slovo"), + }, + tracks: vec![ + Track { + id: TrackId { + number: 1, + title: String::from("Az’"), + }, + artist: vec![String::from("Аркона")], + quality: Quality { + format: Format::Flac, + bitrate: 992, + }, + }, + Track { + id: TrackId { + number: 2, + title: String::from("Arkaim"), + }, + artist: vec![String::from("Аркона")], + quality: Quality { + format: Format::Flac, + bitrate: 1061, + }, + }, + Track { + id: TrackId { + number: 3, + title: String::from("Bol’no mne"), + }, + artist: vec![String::from("Аркона")], + quality: Quality { + format: Format::Flac, + bitrate: 1004, + }, + }, + Track { + id: TrackId { + number: 4, + title: String::from("Leshiy"), + }, + artist: vec![String::from("Аркона")], + quality: Quality { + format: Format::Flac, + bitrate: 1077, + }, + }, + Track { + id: TrackId { + number: 5, + title: String::from("Zakliatie"), + }, + artist: vec![String::from("Аркона")], + quality: Quality { + format: Format::Flac, + bitrate: 1041, + }, + }, + Track { + id: TrackId { + number: 6, + title: String::from("Predok"), + }, + artist: vec![String::from("Аркона")], + quality: Quality { + format: Format::Flac, + bitrate: 756, + }, + }, + Track { + id: TrackId { + number: 7, + title: String::from("Nikogda"), + }, + artist: vec![String::from("Аркона")], + quality: Quality { + format: Format::Flac, + bitrate: 1059, + }, + }, + Track { + id: TrackId { + number: 8, + title: String::from("Tam za tumanami"), + }, + artist: vec![String::from("Аркона")], + quality: Quality { + format: Format::Flac, + bitrate: 1023, + }, + }, + Track { + id: TrackId { + number: 9, + title: String::from("Potomok"), + }, + artist: vec![String::from("Аркона")], + quality: Quality { + format: Format::Flac, + bitrate: 838, + }, + }, + Track { + id: TrackId { + number: 10, + title: String::from("Slovo"), + }, + artist: vec![String::from("Аркона")], + quality: Quality { + format: Format::Flac, + bitrate: 1028, + }, + }, + Track { + id: TrackId { + number: 11, + title: String::from("Odna"), + }, + artist: vec![String::from("Аркона")], + quality: Quality { + format: Format::Flac, + bitrate: 991, + }, + }, + Track { + id: TrackId { + number: 12, + title: String::from("Vo moiom sadochke…"), + }, + artist: vec![String::from("Аркона")], + quality: Quality { + format: Format::Flac, + bitrate: 919, + }, + }, + Track { + id: TrackId { + number: 13, + title: String::from("Stenka na stenku"), + }, + artist: vec![String::from("Аркона")], + quality: Quality { + format: Format::Flac, + bitrate: 1039, + }, + }, + Track { + id: TrackId { + number: 14, + title: String::from("Zimushka"), + }, + artist: vec![String::from("Аркона")], + quality: Quality { + format: Format::Flac, + bitrate: 974, + }, + }, + ], + }], + }, Artist { id: ArtistId { name: String::from("Eluveitie"), }, + sort: None, properties: ArtistProperties { musicbrainz: Some(MusicBrainz::new( "https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38", @@ -243,6 +428,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { id: ArtistId { name: String::from("Frontside"), }, + sort: None, properties: ArtistProperties { musicbrainz: Some(MusicBrainz::new( "https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490", @@ -389,6 +575,9 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { id: ArtistId { name: String::from("Heaven’s Basement"), }, + sort: Some(ArtistId { + name: String::from("Heaven’s Basement"), + }), properties: ArtistProperties { musicbrainz: Some(MusicBrainz::new( "https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc", @@ -509,6 +698,7 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { id: ArtistId { name: String::from("Metallica"), }, + sort: None, properties: ArtistProperties { musicbrainz: Some(MusicBrainz::new( "https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab", @@ -859,186 +1049,5 @@ pub static COLLECTION: Lazy> = Lazy::new(|| -> Collection { }, ], }, - Artist { - id: ArtistId { - name: String::from("Аркона"), - }, - properties: ArtistProperties { - musicbrainz: Some(MusicBrainz::new( - "https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212", - ).unwrap()), - musicbutler: vec![ - MusicButler::new("https://www.musicbutler.io/artist-page/283448581").unwrap(), - ], - bandcamp: vec![ - Bandcamp::new("https://arkonamoscow.bandcamp.com/").unwrap(), - ], - qobuz: Some(Qobuz::new( - "https://www.qobuz.com/nl-nl/interpreter/arkona/download-streaming-albums", - ).unwrap()), - }, - albums: vec![Album { - id: AlbumId { - year: 2011, - title: String::from("Slovo"), - }, - tracks: vec![ - Track { - id: TrackId { - number: 1, - title: String::from("Az’"), - }, - artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, - bitrate: 992, - }, - }, - Track { - id: TrackId { - number: 2, - title: String::from("Arkaim"), - }, - artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, - bitrate: 1061, - }, - }, - Track { - id: TrackId { - number: 3, - title: String::from("Bol’no mne"), - }, - artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, - bitrate: 1004, - }, - }, - Track { - id: TrackId { - number: 4, - title: String::from("Leshiy"), - }, - artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, - bitrate: 1077, - }, - }, - Track { - id: TrackId { - number: 5, - title: String::from("Zakliatie"), - }, - artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, - bitrate: 1041, - }, - }, - Track { - id: TrackId { - number: 6, - title: String::from("Predok"), - }, - artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, - bitrate: 756, - }, - }, - Track { - id: TrackId { - number: 7, - title: String::from("Nikogda"), - }, - artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, - bitrate: 1059, - }, - }, - Track { - id: TrackId { - number: 8, - title: String::from("Tam za tumanami"), - }, - artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, - bitrate: 1023, - }, - }, - Track { - id: TrackId { - number: 9, - title: String::from("Potomok"), - }, - artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, - bitrate: 838, - }, - }, - Track { - id: TrackId { - number: 10, - title: String::from("Slovo"), - }, - artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, - bitrate: 1028, - }, - }, - Track { - id: TrackId { - number: 11, - title: String::from("Odna"), - }, - artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, - bitrate: 991, - }, - }, - Track { - id: TrackId { - number: 12, - title: String::from("Vo moiom sadochke…"), - }, - artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, - bitrate: 919, - }, - }, - Track { - id: TrackId { - number: 13, - title: String::from("Stenka na stenku"), - }, - artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, - bitrate: 1039, - }, - }, - Track { - id: TrackId { - number: 14, - title: String::from("Zimushka"), - }, - artist: vec![String::from("Аркона")], - quality: Quality { - format: Format::Flac, - bitrate: 974, - }, - }, - ], - }], - }, ] });