diff --git a/.gitignore b/.gitignore index eadf8f4..34d5d2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target /codecov +database.json diff --git a/src/database/json/mod.rs b/src/database/json/mod.rs index 38b6aec..a1fd666 100644 --- a/src/database/json/mod.rs +++ b/src/database/json/mod.rs @@ -6,13 +6,19 @@ use serde::Serialize; #[cfg(test)] use mockall::automock; -use super::{Error, IDatabase}; +use super::{IDatabase, LoadError, SaveError}; pub mod backend; -impl From for Error { - fn from(err: serde_json::Error) -> Error { - Error::SerDeError(err.to_string()) +impl From for LoadError { + fn from(err: serde_json::Error) -> LoadError { + LoadError::SerDeError(err.to_string()) + } +} + +impl From for SaveError { + fn from(err: serde_json::Error) -> SaveError { + SaveError::SerDeError(err.to_string()) } } @@ -40,13 +46,13 @@ impl JsonDatabase { } impl IDatabase for JsonDatabase { - fn read(&self, collection: &mut D) -> Result<(), Error> { + fn load(&self, collection: &mut D) -> Result<(), LoadError> { let serialized = self.backend.read()?; *collection = serde_json::from_str(&serialized)?; Ok(()) } - fn write(&mut self, collection: &S) -> Result<(), Error> { + fn save(&mut self, collection: &S) -> Result<(), SaveError> { let serialized = serde_json::to_string(&collection)?; self.backend.write(&serialized)?; Ok(()) @@ -73,8 +79,8 @@ mod tests { let mut tracks: Vec = vec![]; for track in album.tracks.iter() { - let track_number = track.number; - let track_title = &track.title; + let track_number = track.id.number; + let track_title = &track.id.title; let mut track_artist: Vec = vec![]; for artist in track.artist.iter() { @@ -89,8 +95,7 @@ mod tests { tracks.push(format!( "{{\ - \"number\":{track_number},\ - \"title\":\"{track_title}\",\ + \"id\":{{\"number\":{track_number},\"title\":\"{track_title}\"}},\ \"artist\":[{track_artist}],\ \"quality\":{{\"format\":\"{track_format}\",\"bitrate\":{track_bitrate}}}\ }}" @@ -128,7 +133,7 @@ mod tests { } #[test] - fn write() { + fn save() { let write_data = COLLECTION.to_owned(); let input = artists_to_json(&write_data); @@ -139,11 +144,11 @@ mod tests { .times(1) .return_once(|_| Ok(())); - JsonDatabase::new(backend).write(&write_data).unwrap(); + JsonDatabase::new(backend).save(&write_data).unwrap(); } #[test] - fn read() { + fn load() { let expected = COLLECTION.to_owned(); let result = Ok(artists_to_json(&expected)); @@ -151,7 +156,7 @@ mod tests { backend.expect_read().times(1).return_once(|| result); let mut read_data: Vec = vec![]; - JsonDatabase::new(backend).read(&mut read_data).unwrap(); + JsonDatabase::new(backend).load(&mut read_data).unwrap(); assert_eq!(read_data, expected); } @@ -174,8 +179,8 @@ mod tests { let write_data = COLLECTION.to_owned(); let mut read_data: Vec = vec![]; - database.write(&write_data).unwrap(); - database.read(&mut read_data).unwrap(); + database.save(&write_data).unwrap(); + database.load(&mut read_data).unwrap(); assert_eq!(write_data, read_data); } @@ -192,7 +197,7 @@ mod tests { let serde_err = serde_json::to_string(&object); assert!(serde_err.is_err()); - let serde_err: Error = serde_err.unwrap_err().into(); + let serde_err: SaveError = serde_err.unwrap_err().into(); assert!(!serde_err.to_string().is_empty()); assert!(!format!("{:?}", serde_err).is_empty()); } diff --git a/src/database/mod.rs b/src/database/mod.rs index 91bc687..3846314 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -13,36 +13,62 @@ pub mod json; /// Trait for interacting with the database. #[cfg_attr(test, automock)] pub trait IDatabase { - /// Read collection from the database. - fn read(&self, collection: &mut D) -> Result<(), Error>; + /// Load collection from the database. + fn load(&self, collection: &mut D) -> Result<(), LoadError>; - /// Write collection to the database. - fn write(&mut self, collection: &S) -> Result<(), Error>; + /// Save collection to the database. + fn save(&mut self, collection: &S) -> Result<(), SaveError>; } /// Error type for database calls. #[derive(Debug)] -pub enum Error { - /// The database experienced an I/O error. +pub enum LoadError { + /// The database experienced an I/O read error. IoError(String), - /// The database experienced a (de)serialisation error. + /// The database experienced a deserialisation error. SerDeError(String), } -impl fmt::Display for Error { +impl fmt::Display for LoadError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { - Self::IoError(ref s) => write!(f, "the database experienced an I/O error: {s}"), + Self::IoError(ref s) => write!(f, "the database experienced an I/O read error: {s}"), Self::SerDeError(ref s) => { - write!(f, "the database experienced a (de)serialisation error: {s}") + write!(f, "the database experienced a deserialisation error: {s}") } } } } -impl From for Error { - fn from(err: std::io::Error) -> Error { - Error::IoError(err.to_string()) +impl From for LoadError { + fn from(err: std::io::Error) -> LoadError { + LoadError::IoError(err.to_string()) + } +} + +/// Error type for database calls. +#[derive(Debug)] +pub enum SaveError { + /// The database experienced an I/O write error. + IoError(String), + /// The database experienced a serialisation error. + SerDeError(String), +} + +impl fmt::Display for SaveError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Self::IoError(ref s) => write!(f, "the database experienced an I/O write error: {s}"), + Self::SerDeError(ref s) => { + write!(f, "the database experienced a serialisation error: {s}") + } + } + } +} + +impl From for SaveError { + fn from(err: std::io::Error) -> SaveError { + SaveError::IoError(err.to_string()) } } @@ -50,11 +76,15 @@ impl From for Error { mod tests { use std::io; - use super::Error; + use super::{LoadError, SaveError}; #[test] fn errors() { - let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into(); + let io_err: LoadError = io::Error::new(io::ErrorKind::Interrupted, "error").into(); + assert!(!io_err.to_string().is_empty()); + assert!(!format!("{:?}", io_err).is_empty()); + + let io_err: SaveError = io::Error::new(io::ErrorKind::Interrupted, "error").into(); assert!(!io_err.to_string().is_empty()); assert!(!format!("{:?}", io_err).is_empty()); } diff --git a/src/lib.rs b/src/lib.rs index d372e05..90c040b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,10 +3,16 @@ pub mod database; pub mod library; -use std::fmt; +use std::{ + cmp::Ordering, + collections::{HashMap, HashSet}, + fmt, + iter::Peekable, + mem, +}; use database::IDatabase; -use library::{ILibrary, Query}; +use library::{ILibrary, Item, Query}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -27,17 +33,42 @@ pub struct Quality { pub bitrate: u32, } +/// The track identifier. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct TrackId { + pub number: u32, + pub title: String, +} + /// A single track on an album. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct Track { - pub number: u32, - pub title: String, + pub id: TrackId, pub artist: Vec, pub quality: Quality, } +impl PartialOrd for Track { + fn partial_cmp(&self, other: &Self) -> Option { + self.id.partial_cmp(&other.id) + } +} + +impl Ord for Track { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.id.cmp(&other.id) + } +} + +impl Merge for Track { + fn merge(self, other: Self) -> Self { + assert_eq!(self.id, other.id); + self + } +} + /// The album identifier. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Hash)] pub struct AlbumId { pub year: u32, pub title: String, @@ -50,8 +81,28 @@ pub struct Album { pub tracks: Vec, } +impl PartialOrd for Album { + fn partial_cmp(&self, other: &Self) -> Option { + self.id.partial_cmp(&other.id) + } +} + +impl Ord for Album { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.id.cmp(&other.id) + } +} + +impl Merge for Album { + fn merge(mut self, other: Self) -> Self { + assert_eq!(self.id, other.id); + self.tracks = MergeSorted::new(self.tracks.into_iter(), other.tracks.into_iter()).collect(); + self + } +} + /// The artist identifier. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ArtistId { pub name: String, } @@ -63,9 +114,79 @@ pub struct Artist { pub albums: Vec, } +impl PartialOrd for Artist { + fn partial_cmp(&self, other: &Self) -> Option { + self.id.partial_cmp(&other.id) + } +} + +impl Ord for Artist { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.id.cmp(&other.id) + } +} + +impl Merge for Artist { + fn merge(mut self, other: Self) -> Self { + assert_eq!(self.id, other.id); + self.albums = MergeSorted::new(self.albums.into_iter(), other.albums.into_iter()).collect(); + self + } +} + /// The collection type. Currently, a collection is a list of artists. pub type Collection = Vec; +trait Merge { + fn merge(self, other: Self) -> Self; +} + +struct MergeSorted +where + L: Iterator, + R: Iterator, +{ + left: Peekable, + right: Peekable, +} + +impl MergeSorted +where + L: Iterator, + R: Iterator, +{ + fn new(left: L, right: R) -> MergeSorted { + MergeSorted { + left: left.peekable(), + right: right.peekable(), + } + } +} + +impl Iterator for MergeSorted +where + L: Iterator, + R: Iterator, + L::Item: Ord + Merge, +{ + type Item = L::Item; + + fn next(&mut self) -> Option { + let which = match (self.left.peek(), self.right.peek()) { + (Some(l), Some(r)) => l.cmp(r), + (Some(_), None) => Ordering::Less, + (None, Some(_)) => Ordering::Greater, + (None, None) => return None, + }; + + match which { + Ordering::Less => self.left.next(), + Ordering::Equal => Some(self.left.next().unwrap().merge(self.right.next().unwrap())), + Ordering::Greater => self.right.next(), + } + } +} + /// Error type for `musichoard`. #[derive(Debug, PartialEq, Eq)] pub enum Error { @@ -92,8 +213,14 @@ impl From for Error { } } -impl From for Error { - fn from(err: database::Error) -> Error { +impl From for Error { + fn from(err: database::LoadError) -> Error { + Error::DatabaseError(err.to_string()) + } +} + +impl From for Error { + fn from(err: database::SaveError) -> Error { Error::DatabaseError(err.to_string()) } } @@ -117,18 +244,119 @@ impl MusicHoard { } pub fn rescan_library(&mut self) -> Result<(), Error> { - self.collection = self.library.list(&Query::new())?; + let items = self.library.list(&Query::new())?; + let mut library = Self::items_to_artists(items); + Self::sort(&mut library); + + let collection = mem::take(&mut self.collection); + self.collection = Self::merge(library, collection); + + Ok(()) + } + + pub fn load_from_database(&mut self) -> Result<(), Error> { + let mut database: Collection = vec![]; + self.database.load(&mut database)?; + Self::sort(&mut database); + + let collection = mem::take(&mut self.collection); + self.collection = Self::merge(collection, database); + Ok(()) } pub fn save_to_database(&mut self) -> Result<(), Error> { - self.database.write(&self.collection)?; + self.database.save(&self.collection)?; Ok(()) } pub fn get_collection(&self) -> &Collection { &self.collection } + + fn sort(collection: &mut [Artist]) { + collection.sort_unstable(); + for artist in collection.iter_mut() { + artist.albums.sort_unstable(); + for album in artist.albums.iter_mut() { + album.tracks.sort_unstable(); + } + } + } + + fn merge(primary: Vec, secondary: Vec) -> Vec { + MergeSorted::new(primary.into_iter(), secondary.into_iter()).collect() + } + + fn items_to_artists(items: Vec) -> Vec { + let mut artists: Vec = vec![]; + let mut album_ids = HashMap::>::new(); + + for item in items.into_iter() { + let artist_id = ArtistId { + name: item.album_artist, + }; + + let album_id = AlbumId { + year: item.album_year, + title: item.album_title, + }; + + let track = Track { + id: TrackId { + number: item.track_number, + title: item.track_title, + }, + artist: item.track_artist, + quality: Quality { + format: item.track_format, + bitrate: item.track_bitrate, + }, + }; + + let artist = if album_ids.contains_key(&artist_id) { + // Assume results are in some order which means they will likely be grouped by + // artist. Therefore, we look from the back since the last inserted artist is most + // likely the one we are looking for. + artists + .iter_mut() + .rev() + .find(|a| a.id == artist_id) + .unwrap() + } else { + album_ids.insert(artist_id.clone(), HashSet::::new()); + artists.push(Artist { + id: artist_id.clone(), + albums: vec![], + }); + artists.last_mut().unwrap() + }; + + 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 + // likely the one we are looking for. + let album = artist + .albums + .iter_mut() + .rev() + .find(|a| a.id == album_id) + .unwrap(); + album.tracks.push(track); + } else { + album_ids + .get_mut(&artist_id) + .unwrap() + .insert(album_id.clone()); + artist.albums.push(Album { + id: album_id, + tracks: vec![track], + }); + } + } + + artists + } } #[cfg(test)] @@ -146,13 +374,110 @@ mod tests { pub static COLLECTION: Lazy> = Lazy::new(|| collection!()); + pub fn artist_to_items(artist: &Artist) -> Vec { + let mut items = vec![]; + + for album in artist.albums.iter() { + for track in album.tracks.iter() { + items.push(Item { + album_artist: artist.id.name.clone(), + album_year: album.id.year, + album_title: album.id.title.clone(), + track_number: track.id.number, + track_title: track.id.title.clone(), + track_artist: track.artist.clone(), + track_format: track.quality.format, + track_bitrate: track.quality.bitrate, + }); + } + } + + items + } + + pub fn artists_to_items(artists: &[Artist]) -> Vec { + let mut items = vec![]; + for artist in artists.iter() { + items.append(&mut artist_to_items(artist)); + } + items + } + + #[test] + fn rescan_library_ordered() { + let mut library = MockILibrary::new(); + let database = MockIDatabase::new(); + + let library_input = Query::new(); + let library_result = Ok(artists_to_items(&COLLECTION)); + + library + .expect_list() + .with(predicate::eq(library_input)) + .times(1) + .return_once(|_| library_result); + + let mut music_hoard = MusicHoard::new(library, database); + + music_hoard.rescan_library().unwrap(); + assert_eq!(music_hoard.get_collection(), &*COLLECTION); + } + + #[test] + fn rescan_library_unordered() { + let mut library = MockILibrary::new(); + let database = MockIDatabase::new(); + + let library_input = Query::new(); + let mut library_result = Ok(artists_to_items(&COLLECTION)); + + // Swap the last item with the first. + let last = library_result.as_ref().unwrap().len() - 1; + library_result.as_mut().unwrap().swap(0, last); + + library + .expect_list() + .with(predicate::eq(library_input)) + .times(1) + .return_once(|_| library_result); + + let mut music_hoard = MusicHoard::new(library, database); + + music_hoard.rescan_library().unwrap(); + assert_eq!(music_hoard.get_collection(), &*COLLECTION); + } + + #[test] + fn rescan_library_album_title_year_clash() { + let mut library = MockILibrary::new(); + let database = MockIDatabase::new(); + + let mut expected = COLLECTION.to_owned(); + expected[0].albums[0].id.year = expected[1].albums[0].id.year; + expected[0].albums[0].id.title = expected[1].albums[0].id.title.clone(); + + let library_input = Query::new(); + let library_result = Ok(artists_to_items(&expected)); + + library + .expect_list() + .with(predicate::eq(library_input)) + .times(1) + .return_once(|_| library_result); + + let mut music_hoard = MusicHoard::new(library, database); + + music_hoard.rescan_library().unwrap(); + assert_eq!(music_hoard.get_collection(), &expected); + } + #[test] fn read_get_write() { let mut library = MockILibrary::new(); let mut database = MockIDatabase::new(); let library_input = Query::new(); - let library_result = Ok(COLLECTION.to_owned()); + let library_result = Ok(artists_to_items(&COLLECTION)); let database_input = COLLECTION.to_owned(); let database_result = Ok(()); @@ -164,7 +489,7 @@ mod tests { .return_once(|_| library_result); database - .expect_write() + .expect_save() .with(predicate::eq(database_input)) .times(1) .return_once(|_: &Collection| database_result); @@ -203,18 +528,19 @@ mod tests { let library = MockILibrary::new(); let mut database = MockIDatabase::new(); - let database_result = Err(database::Error::IoError(String::from("I/O error"))); + let database_result = Err(database::SaveError::IoError(String::from("I/O error"))); database - .expect_write() + .expect_save() .times(1) .return_once(|_: &Collection| database_result); let mut music_hoard = MusicHoard::new(library, database); let actual_err = music_hoard.save_to_database().unwrap_err(); - let expected_err = - Error::DatabaseError(database::Error::IoError(String::from("I/O error")).to_string()); + let expected_err = Error::DatabaseError( + database::SaveError::IoError(String::from("I/O error")).to_string(), + ); assert_eq!(actual_err, expected_err); assert_eq!(actual_err.to_string(), expected_err.to_string()); diff --git a/src/library/beets/mod.rs b/src/library/beets/mod.rs index 761e974..9253016 100644 --- a/src/library/beets/mod.rs +++ b/src/library/beets/mod.rs @@ -1,17 +1,12 @@ //! Module for interacting with the music library via //! [beets](https://beets.readthedocs.io/en/stable/). -use std::{ - collections::{HashMap, HashSet}, - str, -}; - #[cfg(test)] use mockall::automock; -use crate::{Album, AlbumId, Artist, ArtistId, Format, Quality, Track}; +use crate::Format; -use super::{Error, Field, ILibrary, Query}; +use super::{Error, Field, ILibrary, Item, Query}; pub mod executor; @@ -92,7 +87,7 @@ pub struct BeetsLibrary { trait ILibraryPrivate { fn list_cmd_and_args(query: &Query) -> Vec; - fn list_to_artists>(list_output: &[S]) -> Result, Error>; + fn list_to_items>(list_output: &[S]) -> Result, Error>; } impl BeetsLibrary { @@ -104,10 +99,10 @@ impl BeetsLibrary { } impl ILibrary for BeetsLibrary { - fn list(&mut self, query: &Query) -> Result, Error> { + fn list(&mut self, query: &Query) -> Result, Error> { let cmd = Self::list_cmd_and_args(query); let output = self.executor.exec(&cmd)?; - Self::list_to_artists(&output) + Self::list_to_items(&output) } } @@ -119,9 +114,8 @@ impl ILibraryPrivate for BeetsLibrary { cmd } - fn list_to_artists>(list_output: &[S]) -> Result, Error> { - let mut artists: Vec = vec![]; - let mut album_ids = HashMap::>::new(); + fn list_to_items>(list_output: &[S]) -> Result, Error> { + let mut items: Vec = vec![]; for line in list_output.iter().map(|s| s.as_ref()) { if line.is_empty() { @@ -138,69 +132,31 @@ impl ILibraryPrivate for BeetsLibrary { 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].to_string(); - let track_format = split[6].to_string(); + let track_artist = split[5] + .to_string() + .split("; ") + .map(|s| s.to_owned()) + .collect(); + let track_format = match split[6].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 artist_id = ArtistId { name: album_artist }; - - let album_id = AlbumId { - year: album_year, - title: album_title, - }; - - let track = Track { - number: track_number, - title: track_title, - artist: track_artist.split("; ").map(|s| s.to_owned()).collect(), - quality: Quality { - format: match track_format.as_ref() { - TRACK_FORMAT_FLAC => Format::Flac, - TRACK_FORMAT_MP3 => Format::Mp3, - _ => return Err(Error::Invalid(line.to_string())), - }, - bitrate: track_bitrate, - }, - }; - - let artist = if album_ids.contains_key(&artist_id) { - // Beets returns results in order so we look from the back. - artists - .iter_mut() - .rev() - .find(|a| a.id == artist_id) - .unwrap() - } else { - album_ids.insert(artist_id.clone(), HashSet::::new()); - artists.push(Artist { - id: artist_id.clone(), - albums: vec![], - }); - artists.last_mut().unwrap() - }; - - if album_ids[&artist_id].contains(&album_id) { - // Beets returns results in order so we look from the back. - let album = artist - .albums - .iter_mut() - .rev() - .find(|a| a.id == album_id) - .unwrap(); - album.tracks.push(track); - } else { - album_ids - .get_mut(&artist_id) - .unwrap() - .insert(album_id.clone()); - artist.albums.push(Album { - id: album_id, - tracks: vec![track], - }); - } + items.push(Item { + album_artist, + album_year, + album_title, + track_number, + track_title, + track_artist, + track_format, + track_bitrate, + }); } - Ok(artists) + Ok(items) } } @@ -208,45 +164,34 @@ impl ILibraryPrivate for BeetsLibrary { mod tests { use mockall::predicate; - use crate::tests::COLLECTION; + use crate::tests::{artists_to_items, COLLECTION}; use super::*; - fn artist_to_beets_string(artist: &Artist) -> Vec { - let mut strings = vec![]; - - let album_artist = &artist.id.name; - - for album in artist.albums.iter() { - let album_year = &album.id.year; - let album_title = &album.id.title; - - for track in album.tracks.iter() { - let track_number = &track.number; - let track_title = &track.title; - let track_artist = &track.artist.join("; "); - let track_format = match track.quality.format { - Format::Flac => TRACK_FORMAT_FLAC, - Format::Mp3 => TRACK_FORMAT_MP3, - }; - let track_bitrate = track.quality.bitrate; - - strings.push(format!( - "{album_artist}{0}{album_year}{0}{album_title}{0}\ - {track_number}{0}{track_title}{0}\ - {track_artist}{0}{track_format}{0}{track_bitrate}kbps", - LIST_FORMAT_SEPARATOR, - )); - } - } - - strings + fn item_to_beets_string(item: &Item) -> String { + format!( + "{album_artist}{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_year = item.album_year, + album_title = item.album_title, + track_number = item.track_number, + track_title = item.track_title, + track_artist = item.track_artist.join("; "), + track_format = match item.track_format { + Format::Flac => TRACK_FORMAT_FLAC, + Format::Mp3 => TRACK_FORMAT_MP3, + }, + track_bitrate = item.track_bitrate, + sep = LIST_FORMAT_SEPARATOR, + ) } - fn artists_to_beets_string(artists: &[Artist]) -> Vec { + fn items_to_beets_strings(items: &[Item]) -> Vec { let mut strings = vec![]; - for artist in artists.iter() { - strings.append(&mut artist_to_beets_string(artist)); + for item in items.iter() { + strings.push(item_to_beets_string(item)); } strings } @@ -311,72 +256,15 @@ mod tests { let mut beets = BeetsLibrary::new(executor); let output = beets.list(&Query::new()).unwrap(); - let expected: Vec = vec![]; + let expected: Vec = vec![]; assert_eq!(output, expected); } #[test] - fn test_list_ordered() { + fn test_list() { let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()]; - let expected = COLLECTION.to_owned(); - let result = Ok(artists_to_beets_string(&expected)); - - let mut executor = MockIBeetsLibraryExecutor::new(); - executor - .expect_exec() - .with(predicate::eq(arguments)) - .times(1) - .return_once(|_| result); - - let mut beets = BeetsLibrary::new(executor); - let output = beets.list(&Query::new()).unwrap(); - - assert_eq!(output, expected); - } - - #[test] - fn test_list_unordered() { - let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()]; - let mut expected = COLLECTION.to_owned(); - let mut output = artists_to_beets_string(&expected); - let last = output.len() - 1; - output.swap(0, last); - let result = Ok(output); - - // Putting the last track first will make the entire artist come first in the output. - expected.rotate_right(1); - - // Same applies to that artists' albums. - expected[0].albums.rotate_right(1); - - // Same applies to that album's tracks. - expected[0].albums[0].tracks.rotate_right(1); - - // And the original first album's (now the first album of the second artist) tracks first - // track comes last. - expected[1].albums[0].tracks.rotate_left(1); - - let mut executor = MockIBeetsLibraryExecutor::new(); - executor - .expect_exec() - .with(predicate::eq(arguments)) - .times(1) - .return_once(|_| result); - - let mut beets = BeetsLibrary::new(executor); - let output = beets.list(&Query::new()).unwrap(); - - assert_eq!(output, expected); - } - - #[test] - fn test_list_album_title_year_clash() { - let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()]; - let mut expected = COLLECTION.to_owned(); - expected[0].albums[0].id.year = expected[1].albums[0].id.year; - expected[0].albums[0].id.title = expected[1].albums[0].id.title.clone(); - let output = artists_to_beets_string(&expected); - let result = Ok(output); + let expected = artists_to_items(&COLLECTION); + let result = Ok(items_to_beets_strings(&expected)); let mut executor = MockIBeetsLibraryExecutor::new(); executor @@ -422,15 +310,15 @@ mod tests { let mut beets = BeetsLibrary::new(executor); let output = beets.list(&query).unwrap(); - let expected: Vec = vec![]; + let expected: Vec = vec![]; assert_eq!(output, expected); } #[test] fn invalid_data_split() { let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()]; - let expected = COLLECTION.to_owned(); - let mut output = artists_to_beets_string(&expected); + let expected = artists_to_items(&COLLECTION); + let mut output = items_to_beets_strings(&expected); let invalid_string = output[2] .split(LIST_FORMAT_SEPARATOR) .map(|s| s.to_owned()) @@ -455,8 +343,8 @@ mod tests { #[test] fn invalid_data_format() { let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()]; - let expected = COLLECTION.to_owned(); - let mut output = artists_to_beets_string(&expected); + let expected = artists_to_items(&COLLECTION); + let mut output = items_to_beets_strings(&expected); let mut invalid_string = output[2] .split(LIST_FORMAT_SEPARATOR) .map(|s| s.to_owned()) diff --git a/src/library/mod.rs b/src/library/mod.rs index 506e54e..98905c7 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -5,7 +5,7 @@ use std::{collections::HashSet, fmt, num::ParseIntError, str::Utf8Error}; #[cfg(test)] use mockall::automock; -use crate::Artist; +use crate::Format; #[cfg(feature = "library-beets")] pub mod beets; @@ -14,7 +14,20 @@ pub mod beets; #[cfg_attr(test, automock)] pub trait ILibrary { /// List lirbary items that match the a specific query. - fn list(&mut self, query: &Query) -> Result, Error>; + fn list(&mut self, query: &Query) -> Result, Error>; +} + +/// An item from the library. An item corresponds to an individual file (usually a single track). +#[derive(Debug, PartialEq, Eq)] +pub struct Item { + pub album_artist: String, + pub album_year: u32, + pub album_title: String, + pub track_number: u32, + pub track_title: String, + pub track_artist: Vec, + pub track_format: Format, + pub track_bitrate: u32, } /// Individual fields that can be queried on. diff --git a/src/main.rs b/src/main.rs index 143d78c..c0be878 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ +use std::fs::OpenOptions; use std::path::PathBuf; use std::{ffi::OsString, io}; +use musichoard::Collection; use ratatui::{backend::CrosstermBackend, Terminal}; use structopt::StructOpt; @@ -57,9 +59,28 @@ fn with(lib: LIB, db: DB) { } fn main() { - // Create the application. let opt = Opt::from_args(); + // Create an empty database file if it does not exist. + match OpenOptions::new() + .write(true) + .create_new(true) + .open(&opt.database_file_path) + { + Ok(f) => { + drop(f); + JsonDatabase::new(JsonDatabaseFileBackend::new(&opt.database_file_path)) + .save::(&vec![]) + .expect("failed to create empty database"); + } + Err(e) => match e.kind() { + io::ErrorKind::AlreadyExists => {} + _ => panic!("failed to access database file"), + }, + } + + // Create the application. + let db_exec = JsonDatabaseFileBackend::new(&opt.database_file_path); if let Some(uri) = opt.beets_ssh_uri { let uri = uri.into_string().expect("invalid SSH URI"); let beets_config_file_path = opt @@ -70,11 +91,9 @@ fn main() { let lib_exec = BeetsLibrarySshExecutor::new(uri) .expect("failed to initialise beets") .config(beets_config_file_path); - let db_exec = JsonDatabaseFileBackend::new(&opt.database_file_path); with(BeetsLibrary::new(lib_exec), JsonDatabase::new(db_exec)); } else { let lib_exec = BeetsLibraryProcessExecutor::default().config(opt.beets_config_file_path); - let db_exec = JsonDatabaseFileBackend::new(&opt.database_file_path); with(BeetsLibrary::new(lib_exec), JsonDatabase::new(db_exec)); } } diff --git a/src/testlib.rs b/src/testlib.rs index 95703ca..a790b10 100644 --- a/src/testlib.rs +++ b/src/testlib.rs @@ -13,8 +13,10 @@ macro_rules! collection { }, tracks: vec![ Track { - number: 1, - title: "track a.a.1".to_string(), + id: TrackId { + number: 1, + title: "track a.a.1".to_string(), + }, artist: vec!["artist a.a.1".to_string()], quality: Quality { format: Format::Flac, @@ -22,8 +24,10 @@ macro_rules! collection { }, }, Track { - number: 2, - title: "track a.a.2".to_string(), + id: TrackId { + number: 2, + title: "track a.a.2".to_string(), + }, artist: vec![ "artist a.a.2.1".to_string(), "artist a.a.2.2".to_string(), @@ -34,8 +38,10 @@ macro_rules! collection { }, }, Track { - number: 3, - title: "track a.a.3".to_string(), + id: TrackId { + number: 3, + title: "track a.a.3".to_string(), + }, artist: vec!["artist a.a.3".to_string()], quality: Quality { format: Format::Flac, @@ -51,8 +57,10 @@ macro_rules! collection { }, tracks: vec![ Track { - number: 1, - title: "track a.b.1".to_string(), + id: TrackId { + number: 1, + title: "track a.b.1".to_string(), + }, artist: vec!["artist a.b.1".to_string()], quality: Quality { format: Format::Flac, @@ -60,8 +68,10 @@ macro_rules! collection { }, }, Track { - number: 2, - title: "track a.b.2".to_string(), + id: TrackId { + number: 2, + title: "track a.b.2".to_string(), + }, artist: vec!["artist a.b.2".to_string()], quality: Quality { format: Format::Flac, @@ -84,8 +94,10 @@ macro_rules! collection { }, tracks: vec![ Track { - number: 1, - title: "track b.a.1".to_string(), + id: TrackId { + number: 1, + title: "track b.a.1".to_string(), + }, artist: vec!["artist b.a.1".to_string()], quality: Quality { format: Format::Mp3, @@ -93,8 +105,10 @@ macro_rules! collection { }, }, Track { - number: 2, - title: "track b.a.2".to_string(), + id: TrackId { + number: 2, + title: "track b.a.2".to_string(), + }, artist: vec![ "artist b.a.2.1".to_string(), "artist b.a.2.2".to_string(), @@ -113,8 +127,10 @@ macro_rules! collection { }, tracks: vec![ Track { - number: 1, - title: "track b.b.1".to_string(), + id: TrackId { + number: 1, + title: "track b.b.1".to_string(), + }, artist: vec!["artist b.b.1".to_string()], quality: Quality { format: Format::Flac, @@ -122,8 +138,10 @@ macro_rules! collection { }, }, Track { - number: 2, - title: "track b.b.2".to_string(), + id: TrackId { + number: 2, + title: "track b.b.2".to_string(), + }, artist: vec![ "artist b.b.2.1".to_string(), "artist b.b.2.2".to_string(), @@ -149,8 +167,10 @@ macro_rules! collection { }, tracks: vec![ Track { - number: 1, - title: "track c.a.1".to_string(), + id: TrackId { + number: 1, + title: "track c.a.1".to_string(), + }, artist: vec!["artist c.a.1".to_string()], quality: Quality { format: Format::Mp3, @@ -158,8 +178,10 @@ macro_rules! collection { }, }, Track { - number: 2, - title: "track c.a.2".to_string(), + id: TrackId { + number: 2, + title: "track c.a.2".to_string(), + }, artist: vec![ "artist c.a.2.1".to_string(), "artist c.a.2.2".to_string(), @@ -178,8 +200,10 @@ macro_rules! collection { }, tracks: vec![ Track { - number: 1, - title: "track c.b.1".to_string(), + id: TrackId { + number: 1, + title: "track c.b.1".to_string(), + }, artist: vec!["artist c.b.1".to_string()], quality: Quality { format: Format::Flac, @@ -187,8 +211,10 @@ macro_rules! collection { }, }, Track { - number: 2, - title: "track c.b.2".to_string(), + id: TrackId { + number: 2, + title: "track c.b.2".to_string(), + }, artist: vec![ "artist c.b.2.1".to_string(), "artist c.b.2.2".to_string(), diff --git a/src/tui/event.rs b/src/tui/event.rs index 0cb188f..bae2541 100644 --- a/src/tui/event.rs +++ b/src/tui/event.rs @@ -2,11 +2,14 @@ use crossterm::event::{KeyEvent, MouseEvent}; use std::fmt; use std::sync::mpsc; +use super::ui::UiError; + #[derive(Debug)] pub enum EventError { Send(Event), Recv, Io(std::io::Error), + Ui(String), } impl fmt::Display for EventError { @@ -17,6 +20,9 @@ impl fmt::Display for EventError { Self::Io(ref e) => { write!(f, "an I/O error was triggered during event handling: {e}") } + Self::Ui(ref s) => { + write!(f, "the UI returned an error during event handling: {s}") + } } } } @@ -33,6 +39,12 @@ impl From for EventError { } } +impl From for EventError { + fn from(err: UiError) -> EventError { + EventError::Ui(err.to_string()) + } +} + #[derive(Clone, Copy, Debug)] pub enum Event { Key(KeyEvent), diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 41de726..5da457f 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -14,7 +14,8 @@ pub trait IEventHandler { } trait IEventHandlerPrivate { - fn handle_key_event(ui: &mut UI, key_event: KeyEvent); + fn handle_key_event(ui: &mut UI, key_event: KeyEvent) -> Result<(), EventError>; + fn quit(ui: &mut UI) -> Result<(), EventError>; } pub struct EventHandler { @@ -31,7 +32,7 @@ impl EventHandler { impl IEventHandler for EventHandler { fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError> { match self.events.recv()? { - Event::Key(key_event) => Self::handle_key_event(ui, key_event), + Event::Key(key_event) => Self::handle_key_event(ui, key_event)?, Event::Mouse(_) => {} Event::Resize(_, _) => {} }; @@ -40,16 +41,16 @@ impl IEventHandler for EventHandler { } impl IEventHandlerPrivate for EventHandler { - fn handle_key_event(ui: &mut UI, key_event: KeyEvent) { + fn handle_key_event(ui: &mut UI, key_event: KeyEvent) -> Result<(), EventError> { match key_event.code { // Exit application on `ESC` or `q`. KeyCode::Esc | KeyCode::Char('q') => { - ui.quit(); + Self::quit(ui)?; } // Exit application on `Ctrl-C`. KeyCode::Char('c') | KeyCode::Char('C') => { if key_event.modifiers == KeyModifiers::CONTROL { - ui.quit(); + Self::quit(ui)?; } } // Category change. @@ -69,6 +70,14 @@ impl IEventHandlerPrivate for EventHandler { // Other keys. _ => {} } + + Ok(()) + } + + fn quit(ui: &mut UI) -> Result<(), EventError> { + ui.quit(); + ui.save()?; + Ok(()) } } // GRCOV_EXCL_STOP diff --git a/src/tui/lib.rs b/src/tui/lib.rs index 89a6242..6e0facd 100644 --- a/src/tui/lib.rs +++ b/src/tui/lib.rs @@ -3,24 +3,26 @@ use musichoard::{database::IDatabase, library::ILibrary, Collection, MusicHoard} #[cfg(test)] use mockall::automock; -use super::Error; - #[cfg_attr(test, automock)] pub trait IMusicHoard { - fn rescan_library(&mut self) -> Result<(), Error>; + fn rescan_library(&mut self) -> Result<(), musichoard::Error>; + fn load_from_database(&mut self) -> Result<(), musichoard::Error>; + fn save_to_database(&mut self) -> Result<(), musichoard::Error>; fn get_collection(&self) -> &Collection; } -impl From for Error { - fn from(err: musichoard::Error) -> Error { - Error::Lib(err.to_string()) - } -} - // GRCOV_EXCL_START impl IMusicHoard for MusicHoard { - fn rescan_library(&mut self) -> Result<(), Error> { - Ok(MusicHoard::rescan_library(self)?) + fn rescan_library(&mut self) -> Result<(), musichoard::Error> { + MusicHoard::rescan_library(self) + } + + fn load_from_database(&mut self) -> Result<(), musichoard::Error> { + MusicHoard::load_from_database(self) + } + + fn save_to_database(&mut self) -> Result<(), musichoard::Error> { + MusicHoard::save_to_database(self) } fn get_collection(&self) -> &Collection { diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 8e99637..400bf18 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -25,6 +25,12 @@ pub enum Error { ListenerPanic, } +impl From for Error { + fn from(err: musichoard::Error) -> Error { + Error::Lib(err.to_string()) + } +} + impl From for Error { fn from(err: io::Error) -> Error { Error::Io(err.to_string()) @@ -174,13 +180,18 @@ mod tests { Terminal::new(backend).unwrap() } - pub fn ui(collection: Collection) -> Ui { + pub fn music_hoard(collection: Collection) -> MockIMusicHoard { let mut music_hoard = MockIMusicHoard::new(); + music_hoard.expect_load_from_database().returning(|| Ok(())); music_hoard.expect_rescan_library().returning(|| Ok(())); music_hoard.expect_get_collection().return_const(collection); - Ui::new(music_hoard).unwrap() + music_hoard + } + + pub fn ui(collection: Collection) -> Ui { + Ui::new(music_hoard(collection)).unwrap() } fn listener() -> MockIEventListener { diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 4d0207e..6cd7536 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -1,3 +1,5 @@ +use std::fmt; + use musichoard::{Album, Artist, Collection, Format, Track}; use ratatui::{ backend::Backend, @@ -9,10 +11,31 @@ use ratatui::{ use super::{lib::IMusicHoard, Error}; +#[derive(Debug)] +pub enum UiError { + Lib(String), +} + +impl fmt::Display for UiError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Self::Lib(ref s) => write!(f, "the musichoard library returned an error: {s}"), + } + } +} + +impl From for UiError { + fn from(err: musichoard::Error) -> UiError { + UiError::Lib(err.to_string()) + } +} + pub trait IUi { fn is_running(&self) -> bool; fn quit(&mut self); + fn save(&mut self) -> Result<(), UiError>; + fn increment_category(&mut self); fn decrement_category(&mut self); @@ -404,7 +427,7 @@ impl<'a, 'b> TrackState<'a, 'b> { let list = List::new( tracks .iter() - .map(|id| ListItem::new(id.title.as_str())) + .map(|tr| ListItem::new(tr.id.title.as_str())) .collect::>(), ); @@ -415,9 +438,9 @@ impl<'a, 'b> TrackState<'a, 'b> { Artist: {}\n\ Quality: {}", track - .map(|t| t.number.to_string()) + .map(|t| t.id.number.to_string()) .unwrap_or_else(|| "".to_string()), - track.map(|t| t.title.as_str()).unwrap_or(""), + track.map(|t| t.id.title.as_str()).unwrap_or(""), track .map(|t| t.artist.join("; ")) .unwrap_or_else(|| "".to_string()), @@ -440,6 +463,7 @@ impl<'a, 'b> TrackState<'a, 'b> { impl Ui { pub fn new(mut music_hoard: MH) -> Result { + music_hoard.load_from_database()?; music_hoard.rescan_library()?; let selection = Selection::new(Some(music_hoard.get_collection())); Ok(Ui { @@ -531,6 +555,11 @@ impl IUi for Ui { self.running = false; } + fn save(&mut self) -> Result<(), UiError> { + self.music_hoard.save_to_database()?; + Ok(()) + } + fn increment_category(&mut self) { self.selection.increment_category(); } @@ -603,6 +632,22 @@ mod tests { use super::*; + pub fn music_hoard(collection: Collection) -> MockIMusicHoard { + let mut music_hoard = MockIMusicHoard::new(); + + music_hoard + .expect_load_from_database() + .times(1) + .return_once(|| Ok(())); + music_hoard + .expect_rescan_library() + .times(1) + .return_once(|| Ok(())); + music_hoard.expect_get_collection().return_const(collection); + + music_hoard + } + #[test] fn test_track_selection() { let tracks = &COLLECTION[0].albums[0].tracks; @@ -757,17 +802,7 @@ mod tests { #[test] fn ui_running() { - let mut music_hoard = MockIMusicHoard::new(); - - music_hoard - .expect_rescan_library() - .times(1) - .return_once(|| Ok(())); - music_hoard - .expect_get_collection() - .return_const(COLLECTION.to_owned()); - - let mut ui = Ui::new(music_hoard).unwrap(); + let mut ui = Ui::new(music_hoard(COLLECTION.to_owned())).unwrap(); assert!(ui.is_running()); ui.quit(); @@ -776,17 +811,7 @@ mod tests { #[test] fn ui_modifiers() { - let mut music_hoard = MockIMusicHoard::new(); - - music_hoard - .expect_rescan_library() - .times(1) - .return_once(|| Ok(())); - music_hoard - .expect_get_collection() - .return_const(COLLECTION.to_owned()); - - let mut ui = Ui::new(music_hoard).unwrap(); + let mut ui = Ui::new(music_hoard(COLLECTION.to_owned())).unwrap(); assert!(ui.is_running()); assert_eq!(ui.selection.active, Category::Artist); @@ -875,17 +900,10 @@ mod tests { #[test] fn app_no_tracks() { - let mut music_hoard = MockIMusicHoard::new(); let mut collection = COLLECTION.to_owned(); collection[0].albums[0].tracks = vec![]; - music_hoard - .expect_rescan_library() - .times(1) - .return_once(|| Ok(())); - music_hoard.expect_get_collection().return_const(collection); - - let mut app = Ui::new(music_hoard).unwrap(); + let mut app = Ui::new(music_hoard(collection)).unwrap(); assert!(app.is_running()); assert_eq!(app.selection.active, Category::Artist); @@ -911,17 +929,10 @@ mod tests { #[test] fn app_no_albums() { - let mut music_hoard = MockIMusicHoard::new(); let mut collection = COLLECTION.to_owned(); collection[0].albums = vec![]; - music_hoard - .expect_rescan_library() - .times(1) - .return_once(|| Ok(())); - music_hoard.expect_get_collection().return_const(collection); - - let mut app = Ui::new(music_hoard).unwrap(); + let mut app = Ui::new(music_hoard(collection)).unwrap(); assert!(app.is_running()); assert_eq!(app.selection.active, Category::Artist); @@ -960,16 +971,7 @@ mod tests { #[test] fn app_no_artists() { - let mut music_hoard = MockIMusicHoard::new(); - let collection = vec![]; - - music_hoard - .expect_rescan_library() - .times(1) - .return_once(|| Ok(())); - music_hoard.expect_get_collection().return_const(collection); - - let mut app = Ui::new(music_hoard).unwrap(); + let mut app = Ui::new(music_hoard(vec![])).unwrap(); assert!(app.is_running()); assert_eq!(app.selection.active, Category::Artist); diff --git a/tests/database/json.rs b/tests/database/json.rs index 07fbdf6..2761ecb 100644 --- a/tests/database/json.rs +++ b/tests/database/json.rs @@ -16,14 +16,14 @@ static DATABASE_TEST_FILE: Lazy = Lazy::new(|| fs::canonicalize("./tests/files/database/database.json").unwrap()); #[test] -fn write() { +fn save() { let file = NamedTempFile::new().unwrap(); let backend = JsonDatabaseFileBackend::new(file.path()); let mut database = JsonDatabase::new(backend); let write_data = COLLECTION.to_owned(); - database.write(&write_data).unwrap(); + database.save(&write_data).unwrap(); let expected = fs::read_to_string(&*DATABASE_TEST_FILE).unwrap(); let actual = fs::read_to_string(file.path()).unwrap(); @@ -32,12 +32,12 @@ fn write() { } #[test] -fn read() { +fn load() { let backend = JsonDatabaseFileBackend::new(&*DATABASE_TEST_FILE); let database = JsonDatabase::new(backend); let mut read_data: Vec = vec![]; - database.read(&mut read_data).unwrap(); + database.load(&mut read_data).unwrap(); let expected = COLLECTION.to_owned(); assert_eq!(read_data, expected); @@ -51,10 +51,10 @@ fn reverse() { let mut database = JsonDatabase::new(backend); let write_data = COLLECTION.to_owned(); - database.write(&write_data).unwrap(); + database.save(&write_data).unwrap(); let mut read_data: Vec = vec![]; - database.read(&mut read_data).unwrap(); + database.load(&mut read_data).unwrap(); assert_eq!(write_data, read_data); } diff --git a/tests/files/database/database.json b/tests/files/database/database.json index 42cb67a..1c2d62c 100644 --- a/tests/files/database/database.json +++ b/tests/files/database/database.json @@ -1 +1 @@ -[{"id":{"name":"Аркона"},"albums":[{"id":{"year":2011,"title":"Slovo"},"tracks":[{"number":1,"title":"Az’","artist":["Аркона"],"quality":{"format":"Flac","bitrate":992}},{"number":2,"title":"Arkaim","artist":["Аркона"],"quality":{"format":"Flac","bitrate":1061}},{"number":3,"title":"Bol’no mne","artist":["Аркона"],"quality":{"format":"Flac","bitrate":1004}},{"number":4,"title":"Leshiy","artist":["Аркона"],"quality":{"format":"Flac","bitrate":1077}},{"number":5,"title":"Zakliatie","artist":["Аркона"],"quality":{"format":"Flac","bitrate":1041}},{"number":6,"title":"Predok","artist":["Аркона"],"quality":{"format":"Flac","bitrate":756}},{"number":7,"title":"Nikogda","artist":["Аркона"],"quality":{"format":"Flac","bitrate":1059}},{"number":8,"title":"Tam za tumanami","artist":["Аркона"],"quality":{"format":"Flac","bitrate":1023}},{"number":9,"title":"Potomok","artist":["Аркона"],"quality":{"format":"Flac","bitrate":838}},{"number":10,"title":"Slovo","artist":["Аркона"],"quality":{"format":"Flac","bitrate":1028}},{"number":11,"title":"Odna","artist":["Аркона"],"quality":{"format":"Flac","bitrate":991}},{"number":12,"title":"Vo moiom sadochke…","artist":["Аркона"],"quality":{"format":"Flac","bitrate":919}},{"number":13,"title":"Stenka na stenku","artist":["Аркона"],"quality":{"format":"Flac","bitrate":1039}},{"number":14,"title":"Zimushka","artist":["Аркона"],"quality":{"format":"Flac","bitrate":974}}]}]},{"id":{"name":"Eluveitie"},"albums":[{"id":{"year":2008,"title":"Slania"},"tracks":[{"number":1,"title":"Samon","artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":953}},{"number":2,"title":"Primordial Breath","artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1103}},{"number":3,"title":"Inis Mona","artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1117}},{"number":4,"title":"Gray Sublime Archon","artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1092}},{"number":5,"title":"Anagantios","artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":923}},{"number":6,"title":"Bloodstained Ground","artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1098}},{"number":7,"title":"The Somber Lay","artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1068}},{"number":8,"title":"Slanias Song","artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1098}},{"number":9,"title":"Giamonios","artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":825}},{"number":10,"title":"Tarvos","artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1115}},{"number":11,"title":"Calling the Rain","artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1096}},{"number":12,"title":"Elembivos","artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1059}}]},{"id":{"year":2004,"title":"Vên [re‐recorded]"},"tracks":[{"number":1,"title":"Verja Urit an Bitus","artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":961}},{"number":2,"title":"Uis Elveti","artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1067}},{"number":3,"title":"Ôrô","artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":933}},{"number":4,"title":"Lament","artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1083}},{"number":5,"title":"Druid","artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1073}},{"number":6,"title":"Jêzaïg","artist":["Eluveitie"],"quality":{"format":"Flac","bitrate":1002}}]}]},{"id":{"name":"Frontside"},"albums":[{"id":{"year":2001,"title":"…nasze jest królestwo, potęga i chwała na wieki…"},"tracks":[{"number":1,"title":"Intro = Chaos","artist":["Frontside"],"quality":{"format":"Flac","bitrate":1024}},{"number":2,"title":"Modlitwa","artist":["Frontside"],"quality":{"format":"Flac","bitrate":1073}},{"number":3,"title":"Długa droga z piekła","artist":["Frontside"],"quality":{"format":"Flac","bitrate":1058}},{"number":4,"title":"Synowie ognia","artist":["Frontside"],"quality":{"format":"Flac","bitrate":1066}},{"number":5,"title":"1902","artist":["Frontside"],"quality":{"format":"Flac","bitrate":1074}},{"number":6,"title":"Krew za krew","artist":["Frontside"],"quality":{"format":"Flac","bitrate":1080}},{"number":7,"title":"Kulminacja","artist":["Frontside"],"quality":{"format":"Flac","bitrate":992}},{"number":8,"title":"Judasz","artist":["Frontside"],"quality":{"format":"Flac","bitrate":1018}},{"number":9,"title":"Więzy","artist":["Frontside"],"quality":{"format":"Flac","bitrate":1077}},{"number":10,"title":"Zagubione dusze","artist":["Frontside"],"quality":{"format":"Flac","bitrate":1033}},{"number":11,"title":"Linia życia","artist":["Frontside"],"quality":{"format":"Flac","bitrate":987}}]}]},{"id":{"name":"Heaven’s Basement"},"albums":[{"id":{"year":2011,"title":"Unbreakable"},"tracks":[{"number":1,"title":"Unbreakable","artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":208}},{"number":2,"title":"Guilt Trips and Sins","artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":205}},{"number":3,"title":"The Long Goodbye","artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":227}},{"number":4,"title":"Close Encounters","artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":213}},{"number":5,"title":"Paranoia","artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":218}},{"number":6,"title":"Let Me Out of Here","artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":207}},{"number":7,"title":"Leeches","artist":["Heaven’s Basement"],"quality":{"format":"Mp3","bitrate":225}}]}]},{"id":{"name":"Metallica"},"albums":[{"id":{"year":1984,"title":"Ride the Lightning"},"tracks":[{"number":1,"title":"Fight Fire with Fire","artist":["Metallica"],"quality":{"format":"Flac","bitrate":954}},{"number":2,"title":"Ride the Lightning","artist":["Metallica"],"quality":{"format":"Flac","bitrate":951}},{"number":3,"title":"For Whom the Bell Tolls","artist":["Metallica"],"quality":{"format":"Flac","bitrate":889}},{"number":4,"title":"Fade to Black","artist":["Metallica"],"quality":{"format":"Flac","bitrate":939}},{"number":5,"title":"Trapped under Ice","artist":["Metallica"],"quality":{"format":"Flac","bitrate":955}},{"number":6,"title":"Escape","artist":["Metallica"],"quality":{"format":"Flac","bitrate":941}},{"number":7,"title":"Creeping Death","artist":["Metallica"],"quality":{"format":"Flac","bitrate":958}},{"number":8,"title":"The Call of Ktulu","artist":["Metallica"],"quality":{"format":"Flac","bitrate":888}}]},{"id":{"year":1999,"title":"S&M"},"tracks":[{"number":1,"title":"The Ecstasy of Gold","artist":["Metallica"],"quality":{"format":"Flac","bitrate":875}},{"number":2,"title":"The Call of Ktulu","artist":["Metallica"],"quality":{"format":"Flac","bitrate":1030}},{"number":3,"title":"Master of Puppets","artist":["Metallica"],"quality":{"format":"Flac","bitrate":1082}},{"number":4,"title":"Of Wolf and Man","artist":["Metallica"],"quality":{"format":"Flac","bitrate":1115}},{"number":5,"title":"The Thing That Should Not Be","artist":["Metallica"],"quality":{"format":"Flac","bitrate":1029}},{"number":6,"title":"Fuel","artist":["Metallica"],"quality":{"format":"Flac","bitrate":1057}},{"number":7,"title":"The Memory Remains","artist":["Metallica"],"quality":{"format":"Flac","bitrate":1080}},{"number":8,"title":"No Leaf Clover","artist":["Metallica"],"quality":{"format":"Flac","bitrate":1004}},{"number":9,"title":"Hero of the Day","artist":["Metallica"],"quality":{"format":"Flac","bitrate":962}},{"number":10,"title":"Devil’s Dance","artist":["Metallica"],"quality":{"format":"Flac","bitrate":1076}},{"number":11,"title":"Bleeding Me","artist":["Metallica"],"quality":{"format":"Flac","bitrate":993}},{"number":12,"title":"Nothing Else Matters","artist":["Metallica"],"quality":{"format":"Flac","bitrate":875}},{"number":13,"title":"Until It Sleeps","artist":["Metallica"],"quality":{"format":"Flac","bitrate":1038}},{"number":14,"title":"For Whom the Bell Tolls","artist":["Metallica"],"quality":{"format":"Flac","bitrate":1072}},{"number":15,"title":"−Human","artist":["Metallica"],"quality":{"format":"Flac","bitrate":1029}},{"number":16,"title":"Wherever I May Roam","artist":["Metallica"],"quality":{"format":"Flac","bitrate":1035}},{"number":17,"title":"Outlaw Torn","artist":["Metallica"],"quality":{"format":"Flac","bitrate":1042}},{"number":18,"title":"Sad but True","artist":["Metallica"],"quality":{"format":"Flac","bitrate":1082}},{"number":19,"title":"One","artist":["Metallica"],"quality":{"format":"Flac","bitrate":1017}},{"number":20,"title":"Enter Sandman","artist":["Metallica"],"quality":{"format":"Flac","bitrate":993}},{"number":21,"title":"Battery","artist":["Metallica"],"quality":{"format":"Flac","bitrate":967}}]}]}] \ No newline at end of file +[{"id":{"name":"Аркона"},"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"},"albums":[{"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":{"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":{"name":"Frontside"},"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"},"albums":[{"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"},"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/lib.rs b/tests/lib.rs index b92eb2b..4291501 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,7 +1,7 @@ mod database; mod library; -use musichoard::{Album, AlbumId, Artist, ArtistId, Format, Quality, Track}; +use musichoard::{Album, AlbumId, Artist, ArtistId, Format, Quality, Track, TrackId}; use once_cell::sync::Lazy; static COLLECTION: Lazy> = Lazy::new(|| { @@ -17,8 +17,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, tracks: vec![ Track { - number: 01, - title: String::from("Az’"), + id: TrackId { + number: 01, + title: String::from("Az’"), + }, artist: vec![String::from("Аркона")], quality: Quality { format: Format::Flac, @@ -26,8 +28,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 02, - title: String::from("Arkaim"), + id: TrackId { + number: 02, + title: String::from("Arkaim"), + }, artist: vec![String::from("Аркона")], quality: Quality { format: Format::Flac, @@ -35,8 +39,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 03, - title: String::from("Bol’no mne"), + id: TrackId { + number: 03, + title: String::from("Bol’no mne"), + }, artist: vec![String::from("Аркона")], quality: Quality { format: Format::Flac, @@ -44,8 +50,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 04, - title: String::from("Leshiy"), + id: TrackId { + number: 04, + title: String::from("Leshiy"), + }, artist: vec![String::from("Аркона")], quality: Quality { format: Format::Flac, @@ -53,8 +61,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 05, - title: String::from("Zakliatie"), + id: TrackId { + number: 05, + title: String::from("Zakliatie"), + }, artist: vec![String::from("Аркона")], quality: Quality { format: Format::Flac, @@ -62,8 +72,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 06, - title: String::from("Predok"), + id: TrackId { + number: 06, + title: String::from("Predok"), + }, artist: vec![String::from("Аркона")], quality: Quality { format: Format::Flac, @@ -71,8 +83,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 07, - title: String::from("Nikogda"), + id: TrackId { + number: 07, + title: String::from("Nikogda"), + }, artist: vec![String::from("Аркона")], quality: Quality { format: Format::Flac, @@ -80,8 +94,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 08, - title: String::from("Tam za tumanami"), + id: TrackId { + number: 08, + title: String::from("Tam za tumanami"), + }, artist: vec![String::from("Аркона")], quality: Quality { format: Format::Flac, @@ -89,8 +105,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 09, - title: String::from("Potomok"), + id: TrackId { + number: 09, + title: String::from("Potomok"), + }, artist: vec![String::from("Аркона")], quality: Quality { format: Format::Flac, @@ -98,8 +116,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 10, - title: String::from("Slovo"), + id: TrackId { + number: 10, + title: String::from("Slovo"), + }, artist: vec![String::from("Аркона")], quality: Quality { format: Format::Flac, @@ -107,8 +127,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 11, - title: String::from("Odna"), + id: TrackId { + number: 11, + title: String::from("Odna"), + }, artist: vec![String::from("Аркона")], quality: Quality { format: Format::Flac, @@ -116,8 +138,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 12, - title: String::from("Vo moiom sadochke…"), + id: TrackId { + number: 12, + title: String::from("Vo moiom sadochke…"), + }, artist: vec![String::from("Аркона")], quality: Quality { format: Format::Flac, @@ -125,8 +149,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 13, - title: String::from("Stenka na stenku"), + id: TrackId { + number: 13, + title: String::from("Stenka na stenku"), + }, artist: vec![String::from("Аркона")], quality: Quality { format: Format::Flac, @@ -134,8 +160,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 14, - title: String::from("Zimushka"), + id: TrackId { + number: 14, + title: String::from("Zimushka"), + }, artist: vec![String::from("Аркона")], quality: Quality { format: Format::Flac, @@ -157,8 +185,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, tracks: vec![ Track { - number: 01, - title: String::from("Samon"), + id: TrackId { + number: 01, + title: String::from("Samon"), + }, artist: vec![String::from("Eluveitie")], quality: Quality { format: Format::Flac, @@ -166,8 +196,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 02, - title: String::from("Primordial Breath"), + id: TrackId { + number: 02, + title: String::from("Primordial Breath"), + }, artist: vec![String::from("Eluveitie")], quality: Quality { format: Format::Flac, @@ -175,8 +207,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 03, - title: String::from("Inis Mona"), + id: TrackId { + number: 03, + title: String::from("Inis Mona"), + }, artist: vec![String::from("Eluveitie")], quality: Quality { format: Format::Flac, @@ -184,8 +218,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 04, - title: String::from("Gray Sublime Archon"), + id: TrackId { + number: 04, + title: String::from("Gray Sublime Archon"), + }, artist: vec![String::from("Eluveitie")], quality: Quality { format: Format::Flac, @@ -193,8 +229,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 05, - title: String::from("Anagantios"), + id: TrackId { + number: 05, + title: String::from("Anagantios"), + }, artist: vec![String::from("Eluveitie")], quality: Quality { format: Format::Flac, @@ -202,8 +240,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 06, - title: String::from("Bloodstained Ground"), + id: TrackId { + number: 06, + title: String::from("Bloodstained Ground"), + }, artist: vec![String::from("Eluveitie")], quality: Quality { format: Format::Flac, @@ -211,8 +251,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 07, - title: String::from("The Somber Lay"), + id: TrackId { + number: 07, + title: String::from("The Somber Lay"), + }, artist: vec![String::from("Eluveitie")], quality: Quality { format: Format::Flac, @@ -220,8 +262,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 08, - title: String::from("Slanias Song"), + id: TrackId { + number: 08, + title: String::from("Slanias Song"), + }, artist: vec![String::from("Eluveitie")], quality: Quality { format: Format::Flac, @@ -229,8 +273,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 09, - title: String::from("Giamonios"), + id: TrackId { + number: 09, + title: String::from("Giamonios"), + }, artist: vec![String::from("Eluveitie")], quality: Quality { format: Format::Flac, @@ -238,8 +284,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 10, - title: String::from("Tarvos"), + id: TrackId { + number: 10, + title: String::from("Tarvos"), + }, artist: vec![String::from("Eluveitie")], quality: Quality { format: Format::Flac, @@ -247,8 +295,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 11, - title: String::from("Calling the Rain"), + id: TrackId { + number: 11, + title: String::from("Calling the Rain"), + }, artist: vec![String::from("Eluveitie")], quality: Quality { format: Format::Flac, @@ -256,8 +306,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 12, - title: String::from("Elembivos"), + id: TrackId { + number: 12, + title: String::from("Elembivos"), + }, artist: vec![String::from("Eluveitie")], quality: Quality { format: Format::Flac, @@ -273,8 +325,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, tracks: vec![ Track { - number: 01, - title: String::from("Verja Urit an Bitus"), + id: TrackId { + number: 01, + title: String::from("Verja Urit an Bitus"), + }, artist: vec![String::from("Eluveitie")], quality: Quality { format: Format::Flac, @@ -282,8 +336,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 02, - title: String::from("Uis Elveti"), + id: TrackId { + number: 02, + title: String::from("Uis Elveti"), + }, artist: vec![String::from("Eluveitie")], quality: Quality { format: Format::Flac, @@ -291,8 +347,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 03, - title: String::from("Ôrô"), + id: TrackId { + number: 03, + title: String::from("Ôrô"), + }, artist: vec![String::from("Eluveitie")], quality: Quality { format: Format::Flac, @@ -300,8 +358,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 04, - title: String::from("Lament"), + id: TrackId { + number: 04, + title: String::from("Lament"), + }, artist: vec![String::from("Eluveitie")], quality: Quality { format: Format::Flac, @@ -309,8 +369,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 05, - title: String::from("Druid"), + id: TrackId { + number: 05, + title: String::from("Druid"), + }, artist: vec![String::from("Eluveitie")], quality: Quality { format: Format::Flac, @@ -318,8 +380,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 06, - title: String::from("Jêzaïg"), + id: TrackId { + number: 06, + title: String::from("Jêzaïg"), + }, artist: vec![String::from("Eluveitie")], quality: Quality { format: Format::Flac, @@ -341,8 +405,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, tracks: vec![ Track { - number: 01, - title: String::from("Intro = Chaos"), + id: TrackId { + number: 01, + title: String::from("Intro = Chaos"), + }, artist: vec![String::from("Frontside")], quality: Quality { format: Format::Flac, @@ -350,8 +416,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 02, - title: String::from("Modlitwa"), + id: TrackId { + number: 02, + title: String::from("Modlitwa"), + }, artist: vec![String::from("Frontside")], quality: Quality { format: Format::Flac, @@ -359,8 +427,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 03, - title: String::from("Długa droga z piekła"), + id: TrackId { + number: 03, + title: String::from("Długa droga z piekła"), + }, artist: vec![String::from("Frontside")], quality: Quality { format: Format::Flac, @@ -368,8 +438,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 04, - title: String::from("Synowie ognia"), + id: TrackId { + number: 04, + title: String::from("Synowie ognia"), + }, artist: vec![String::from("Frontside")], quality: Quality { format: Format::Flac, @@ -377,8 +449,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 05, - title: String::from("1902"), + id: TrackId { + number: 05, + title: String::from("1902"), + }, artist: vec![String::from("Frontside")], quality: Quality { format: Format::Flac, @@ -386,8 +460,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 06, - title: String::from("Krew za krew"), + id: TrackId { + number: 06, + title: String::from("Krew za krew"), + }, artist: vec![String::from("Frontside")], quality: Quality { format: Format::Flac, @@ -395,8 +471,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 07, - title: String::from("Kulminacja"), + id: TrackId { + number: 07, + title: String::from("Kulminacja"), + }, artist: vec![String::from("Frontside")], quality: Quality { format: Format::Flac, @@ -404,8 +482,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 08, - title: String::from("Judasz"), + id: TrackId { + number: 08, + title: String::from("Judasz"), + }, artist: vec![String::from("Frontside")], quality: Quality { format: Format::Flac, @@ -413,8 +493,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 09, - title: String::from("Więzy"), + id: TrackId { + number: 09, + title: String::from("Więzy"), + }, artist: vec![String::from("Frontside")], quality: Quality { format: Format::Flac, @@ -422,8 +504,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 10, - title: String::from("Zagubione dusze"), + id: TrackId { + number: 10, + title: String::from("Zagubione dusze"), + }, artist: vec![String::from("Frontside")], quality: Quality { format: Format::Flac, @@ -431,8 +515,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 11, - title: String::from("Linia życia"), + id: TrackId { + number: 11, + title: String::from("Linia życia"), + }, artist: vec![String::from("Frontside")], quality: Quality { format: Format::Flac, @@ -453,8 +539,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, tracks: vec![ Track { - number: 01, - title: String::from("Unbreakable"), + id: TrackId { + number: 01, + title: String::from("Unbreakable"), + }, artist: vec![String::from("Heaven’s Basement")], quality: Quality { format: Format::Mp3, @@ -462,8 +550,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 02, - title: String::from("Guilt Trips and Sins"), + id: TrackId { + number: 02, + title: String::from("Guilt Trips and Sins"), + }, artist: vec![String::from("Heaven’s Basement")], quality: Quality { format: Format::Mp3, @@ -471,8 +561,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 03, - title: String::from("The Long Goodbye"), + id: TrackId { + number: 03, + title: String::from("The Long Goodbye"), + }, artist: vec![String::from("Heaven’s Basement")], quality: Quality { format: Format::Mp3, @@ -480,8 +572,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 04, - title: String::from("Close Encounters"), + id: TrackId { + number: 04, + title: String::from("Close Encounters"), + }, artist: vec![String::from("Heaven’s Basement")], quality: Quality { format: Format::Mp3, @@ -489,8 +583,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 05, - title: String::from("Paranoia"), + id: TrackId { + number: 05, + title: String::from("Paranoia"), + }, artist: vec![String::from("Heaven’s Basement")], quality: Quality { format: Format::Mp3, @@ -498,8 +594,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 06, - title: String::from("Let Me Out of Here"), + id: TrackId { + number: 06, + title: String::from("Let Me Out of Here"), + }, artist: vec![String::from("Heaven’s Basement")], quality: Quality { format: Format::Mp3, @@ -507,8 +605,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 07, - title: String::from("Leeches"), + id: TrackId { + number: 07, + title: String::from("Leeches"), + }, artist: vec![String::from("Heaven’s Basement")], quality: Quality { format: Format::Mp3, @@ -530,8 +630,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, tracks: vec![ Track { - number: 01, - title: String::from("Fight Fire with Fire"), + id: TrackId { + number: 01, + title: String::from("Fight Fire with Fire"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -539,8 +641,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 02, - title: String::from("Ride the Lightning"), + id: TrackId { + number: 02, + title: String::from("Ride the Lightning"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -548,8 +652,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 03, - title: String::from("For Whom the Bell Tolls"), + id: TrackId { + number: 03, + title: String::from("For Whom the Bell Tolls"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -557,8 +663,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 04, - title: String::from("Fade to Black"), + id: TrackId { + number: 04, + title: String::from("Fade to Black"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -566,8 +674,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 05, - title: String::from("Trapped under Ice"), + id: TrackId { + number: 05, + title: String::from("Trapped under Ice"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -575,8 +685,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 06, - title: String::from("Escape"), + id: TrackId { + number: 06, + title: String::from("Escape"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -584,8 +696,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 07, - title: String::from("Creeping Death"), + id: TrackId { + number: 07, + title: String::from("Creeping Death"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -593,8 +707,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 08, - title: String::from("The Call of Ktulu"), + id: TrackId { + number: 08, + title: String::from("The Call of Ktulu"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -610,8 +726,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, tracks: vec![ Track { - number: 01, - title: String::from("The Ecstasy of Gold"), + id: TrackId { + number: 01, + title: String::from("The Ecstasy of Gold"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -619,8 +737,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 02, - title: String::from("The Call of Ktulu"), + id: TrackId { + number: 02, + title: String::from("The Call of Ktulu"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -628,8 +748,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 03, - title: String::from("Master of Puppets"), + id: TrackId { + number: 03, + title: String::from("Master of Puppets"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -637,8 +759,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 04, - title: String::from("Of Wolf and Man"), + id: TrackId { + number: 04, + title: String::from("Of Wolf and Man"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -646,8 +770,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 05, - title: String::from("The Thing That Should Not Be"), + id: TrackId { + number: 05, + title: String::from("The Thing That Should Not Be"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -655,8 +781,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 06, - title: String::from("Fuel"), + id: TrackId { + number: 06, + title: String::from("Fuel"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -664,8 +792,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 07, - title: String::from("The Memory Remains"), + id: TrackId { + number: 07, + title: String::from("The Memory Remains"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -673,8 +803,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 08, - title: String::from("No Leaf Clover"), + id: TrackId { + number: 08, + title: String::from("No Leaf Clover"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -682,8 +814,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 09, - title: String::from("Hero of the Day"), + id: TrackId { + number: 09, + title: String::from("Hero of the Day"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -691,8 +825,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 10, - title: String::from("Devil’s Dance"), + id: TrackId { + number: 10, + title: String::from("Devil’s Dance"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -700,8 +836,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 11, - title: String::from("Bleeding Me"), + id: TrackId { + number: 11, + title: String::from("Bleeding Me"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -709,8 +847,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 12, - title: String::from("Nothing Else Matters"), + id: TrackId { + number: 12, + title: String::from("Nothing Else Matters"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -718,8 +858,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 13, - title: String::from("Until It Sleeps"), + id: TrackId { + number: 13, + title: String::from("Until It Sleeps"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -727,8 +869,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 14, - title: String::from("For Whom the Bell Tolls"), + id: TrackId { + number: 14, + title: String::from("For Whom the Bell Tolls"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -736,8 +880,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 15, - title: String::from("−Human"), + id: TrackId { + number: 15, + title: String::from("−Human"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -745,8 +891,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 16, - title: String::from("Wherever I May Roam"), + id: TrackId { + number: 16, + title: String::from("Wherever I May Roam"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -754,8 +902,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 17, - title: String::from("Outlaw Torn"), + id: TrackId { + number: 17, + title: String::from("Outlaw Torn"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -763,8 +913,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 18, - title: String::from("Sad but True"), + id: TrackId { + number: 18, + title: String::from("Sad but True"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -772,8 +924,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 19, - title: String::from("One"), + id: TrackId { + number: 19, + title: String::from("One"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -781,8 +935,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 20, - title: String::from("Enter Sandman"), + id: TrackId { + number: 20, + title: String::from("Enter Sandman"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, @@ -790,8 +946,10 @@ static COLLECTION: Lazy> = Lazy::new(|| { }, }, Track { - number: 21, - title: String::from("Battery"), + id: TrackId { + number: 21, + title: String::from("Battery"), + }, artist: vec![String::from("Metallica")], quality: Quality { format: Format::Flac, diff --git a/tests/library/beets.rs b/tests/library/beets.rs index eb8f5f6..6cd0f5c 100644 --- a/tests/library/beets.rs +++ b/tests/library/beets.rs @@ -9,7 +9,7 @@ use once_cell::sync::Lazy; use musichoard::{ library::{ beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary}, - Field, ILibrary, Query, + Field, ILibrary, Item, Query, }, Artist, }; @@ -32,6 +32,35 @@ static BEETS_TEST_CONFIG: Lazy Vec { + let mut items = vec![]; + + for album in artist.albums.iter() { + for track in album.tracks.iter() { + items.push(Item { + album_artist: artist.id.name.clone(), + album_year: album.id.year, + album_title: album.id.title.clone(), + track_number: track.id.number, + track_title: track.id.title.clone(), + track_artist: track.artist.clone(), + track_format: track.quality.format, + track_bitrate: track.quality.bitrate, + }); + } + } + + items +} + +fn artists_to_items(artists: &[Artist]) -> Vec { + let mut items = vec![]; + for artist in artists.iter() { + items.append(&mut artist_to_items(artist)); + } + items +} + #[test] fn test_no_config_list() { let beets_arc = BEETS_EMPTY_CONFIG.clone(); @@ -39,7 +68,7 @@ fn test_no_config_list() { let output = beets.list(&Query::new()).unwrap(); - let expected: Vec = vec![]; + let expected: Vec = vec![]; assert_eq!(output, expected); } @@ -61,7 +90,7 @@ fn test_full_list() { let output = beets.list(&Query::new()).unwrap(); - let expected: Vec = COLLECTION.to_owned(); + let expected: Vec = artists_to_items(&COLLECTION); assert_eq!(output, expected); } @@ -74,7 +103,7 @@ fn test_album_artist_query() { .list(Query::new().include(Field::AlbumArtist(String::from("Аркона")))) .unwrap(); - let expected: Vec = COLLECTION[0..1].to_owned(); + let expected: Vec = artists_to_items(&COLLECTION[0..1]); assert_eq!(output, expected); } @@ -87,7 +116,7 @@ fn test_album_title_query() { .list(&Query::new().include(Field::AlbumTitle(String::from("Slovo")))) .unwrap(); - let expected: Vec = COLLECTION[0..1].to_owned(); + let expected: Vec = artists_to_items(&COLLECTION[0..1]); assert_eq!(output, expected); } @@ -100,6 +129,6 @@ fn test_exclude_query() { .list(&Query::new().exclude(Field::AlbumArtist(String::from("Аркона")))) .unwrap(); - let expected: Vec = COLLECTION[1..].to_owned(); + let expected: Vec = artists_to_items(&COLLECTION[1..]); assert_eq!(output, expected); }