//! MusicHoard - a music collection manager. pub mod database; pub mod library; use std::{ cmp::Ordering, collections::{HashMap, HashSet}, fmt, iter::Peekable, mem, }; use database::IDatabase; use library::{ILibrary, Item, Query}; use serde::{Deserialize, Serialize}; use uuid::Uuid; /// [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID). pub type Mbid = Uuid; /// The track file format. #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] pub enum Format { Flac, Mp3, } /// The track quality. Combines format and bitrate information. #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct Quality { pub format: Format, 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 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, PartialOrd, Ord, Eq, Hash)] pub struct AlbumId { pub year: u32, pub title: String, } /// An album is a collection of tracks that were released together. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct Album { pub id: AlbumId, 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, PartialOrd, Ord, Hash)] pub struct ArtistId { pub name: String, } /// An artist. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct Artist { pub id: ArtistId, 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 { /// The [`MusicHoard`] failed to read/write from/to the library. LibraryError(String), /// The [`MusicHoard`] failed to read/write from/to the database. DatabaseError(String), } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { Self::LibraryError(ref s) => write!(f, "failed to read/write from/to the library: {s}"), Self::DatabaseError(ref s) => { write!(f, "failed to read/write from/to the database: {s}") } } } } impl From for Error { fn from(err: library::Error) -> Error { Error::LibraryError(err.to_string()) } } 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()) } } /// The Music Hoard. It is responsible for pulling information from both the library and the /// database, ensuring its consistent and writing back any changes. pub struct MusicHoard { library: LIB, database: DB, collection: Collection, } impl MusicHoard { /// Create a new [`MusicHoard`] with the provided [`ILibrary`] and [`IDatabase`]. pub fn new(library: LIB, database: DB) -> Self { MusicHoard { library, database, collection: vec![], } } pub fn rescan_library(&mut self) -> Result<(), Error> { 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.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)] #[macro_use] mod testlib; #[cfg(test)] mod tests { use mockall::predicate; use once_cell::sync::Lazy; use crate::{database::MockIDatabase, library::MockILibrary}; use super::*; 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 merge_track() { let left = Track { id: TrackId { number: 04, title: String::from("a title"), }, artist: vec![String::from("left artist")], quality: Quality { format: Format::Flac, bitrate: 1411, }, }; let right = Track { id: left.id.clone(), artist: vec![String::from("right artist")], quality: Quality { format: Format::Mp3, bitrate: 320, }, }; let merged = left.clone().merge(right); assert_eq!(left, merged); } #[test] fn merge_album_no_overlap() { let left = COLLECTION[0].albums[0].to_owned(); let mut right = COLLECTION[0].albums[1].to_owned(); right.id = left.id.clone(); let mut expected = left.clone(); expected.tracks.append(&mut right.tracks.clone()); expected.tracks.sort_unstable(); let merged = left.clone().merge(right); assert_eq!(expected, merged); } #[test] fn merge_album_overlap() { let mut left = COLLECTION[0].albums[0].to_owned(); let mut right = COLLECTION[0].albums[1].to_owned(); right.id = left.id.clone(); left.tracks.push(right.tracks[0].clone()); left.tracks.sort_unstable(); let mut expected = left.clone(); expected.tracks.append(&mut right.tracks.clone()); expected.tracks.sort_unstable(); expected.tracks.dedup(); let merged = left.clone().merge(right); assert_eq!(expected, merged); } #[test] fn merge_artist_no_overlap() { let left = COLLECTION[0].to_owned(); let mut right = COLLECTION[1].to_owned(); right.id = left.id.clone(); let mut expected = left.clone(); expected.albums.append(&mut right.albums.clone()); expected.albums.sort_unstable(); let merged = left.clone().merge(right); assert_eq!(expected, merged); } #[test] fn merge_artist_overlap() { let mut left = COLLECTION[0].to_owned(); let mut right = COLLECTION[1].to_owned(); right.id = left.id.clone(); left.albums.push(right.albums[0].clone()); left.albums.sort_unstable(); let mut expected = left.clone(); expected.albums.append(&mut right.albums.clone()); expected.albums.sort_unstable(); expected.albums.dedup(); let merged = left.clone().merge(right); assert_eq!(expected, merged); } #[test] fn merge_collection_no_overlap() { let half: usize = COLLECTION.len() / 2; let left = COLLECTION[..half].to_owned(); let right = COLLECTION[half..].to_owned(); let mut expected = COLLECTION.to_owned(); expected.sort_unstable(); let merged = MusicHoard::::merge(left.clone(), right); assert_eq!(expected, merged); } #[test] fn merge_collection_overlap() { let half: usize = COLLECTION.len() / 2; let left = COLLECTION[..(half + 1)].to_owned(); let right = COLLECTION[half..].to_owned(); let mut expected = COLLECTION.to_owned(); expected.sort_unstable(); let merged = MusicHoard::::merge(left.clone(), right); assert_eq!(expected, merged); } #[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 load_database() { let library = MockILibrary::new(); let mut database = MockIDatabase::new(); database .expect_load() .times(1) .return_once(|coll: &mut Collection| { *coll = COLLECTION.to_owned(); Ok(()) }); let mut music_hoard = MusicHoard::new(library, database); music_hoard.load_from_database().unwrap(); assert_eq!(music_hoard.get_collection(), &*COLLECTION); } #[test] fn rescan_get_save() { let mut library = MockILibrary::new(); let mut database = MockIDatabase::new(); let library_input = Query::new(); let library_result = Ok(artists_to_items(&COLLECTION)); let database_input = COLLECTION.to_owned(); let database_result = Ok(()); library .expect_list() .with(predicate::eq(library_input)) .times(1) .return_once(|_| library_result); database .expect_save() .with(predicate::eq(database_input)) .times(1) .return_once(|_: &Collection| database_result); let mut music_hoard = MusicHoard::new(library, database); music_hoard.rescan_library().unwrap(); assert_eq!(music_hoard.get_collection(), &*COLLECTION); music_hoard.save_to_database().unwrap(); } #[test] fn library_error() { let mut library = MockILibrary::new(); let database = MockIDatabase::new(); let library_result = Err(library::Error::Invalid(String::from("invalid data"))); library .expect_list() .times(1) .return_once(|_| library_result); let mut music_hoard = MusicHoard::new(library, database); let actual_err = music_hoard.rescan_library().unwrap_err(); let expected_err = Error::LibraryError(library::Error::Invalid(String::from("invalid data")).to_string()); assert_eq!(actual_err, expected_err); assert_eq!(actual_err.to_string(), expected_err.to_string()); } #[test] fn database_load_error() { let library = MockILibrary::new(); let mut database = MockIDatabase::new(); let database_result = Err(database::LoadError::IoError(String::from("I/O error"))); database .expect_load() .times(1) .return_once(|_: &mut Collection| database_result); let mut music_hoard = MusicHoard::new(library, database); let actual_err = music_hoard.load_from_database().unwrap_err(); let expected_err = Error::DatabaseError( database::LoadError::IoError(String::from("I/O error")).to_string(), ); assert_eq!(actual_err, expected_err); assert_eq!(actual_err.to_string(), expected_err.to_string()); } #[test] fn database_save_error() { let library = MockILibrary::new(); let mut database = MockIDatabase::new(); let database_result = Err(database::SaveError::IoError(String::from("I/O error"))); database .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::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()); } }