musichoard/src/lib.rs

223 lines
6.0 KiB
Rust
Raw Normal View History

//! 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<String>,
pub quality: Quality,
2023-03-28 22:49:59 +02:00
}
/// The album identifier.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
pub struct AlbumId {
pub year: u32,
pub title: String,
}
2023-03-28 22:49:59 +02:00
/// 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<Track>,
2023-03-28 22:49:59 +02:00
}
/// 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<Album>,
}
/// The collection type. Currently, a collection is a list of artists.
pub type Collection = Vec<Artist>;
/// 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<library::Error> for Error {
fn from(err: library::Error) -> Error {
Error::LibraryError(err.to_string())
}
}
impl From<database::Error> 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<LIB, DB> {
library: LIB,
database: DB,
collection: Collection,
}
impl<LIB: ILibrary, DB: IDatabase> MusicHoard<LIB, DB> {
/// 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<Vec<Artist>> = 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());
}
}