//! MusicHoard - a music collection manager. pub mod database; pub mod library; use std::fmt; use database::IDatabase; use library::{ILibrary, 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, } /// A single track on an album. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct Track { pub number: u32, pub title: String, pub artist: Vec, pub quality: Quality, } /// The album identifier. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, 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, } /// The artist identifier. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, 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, } /// The collection type. Currently, a collection is a list of artists. pub type Collection = Vec; /// 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::Error) -> 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> { self.collection = self.library.list(&Query::new())?; Ok(()) } pub fn save_to_database(&mut self) -> Result<(), Error> { self.database.write(&self.collection)?; Ok(()) } pub fn get_collection(&self) -> &Collection { &self.collection } } #[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!()); #[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 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_write() .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_error() { let library = MockILibrary::new(); let mut database = MockIDatabase::new(); let database_result = Err(database::Error::IoError(String::from("I/O error"))); database .expect_write() .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()); assert_eq!(actual_err, expected_err); assert_eq!(actual_err.to_string(), expected_err.to_string()); } }