Break down the musichoard files #165
@ -5,10 +5,10 @@ use structopt::{clap::AppSettings, StructOpt};
|
||||
use musichoard::{
|
||||
collection::{album::AlbumId, artist::ArtistId},
|
||||
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
||||
MusicHoard, MusicHoardBuilder, NoLibrary,
|
||||
IMusicHoardDatabase, MusicHoard, MusicHoardBuilder, NoLibrary,
|
||||
};
|
||||
|
||||
type MH = MusicHoard<NoLibrary, JsonDatabase<JsonDatabaseFileBackend>>;
|
||||
type MH = MusicHoard<JsonDatabase<JsonDatabaseFileBackend>, NoLibrary>;
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(about = "musichoard-edit: edit the MusicHoard database",
|
||||
|
@ -5,7 +5,7 @@ use std::{
|
||||
|
||||
use crate::core::collection::{
|
||||
merge::{Merge, MergeSorted, WithId},
|
||||
musicbrainz::MusicBrainz,
|
||||
musicbrainz::MusicBrainzUrl,
|
||||
track::{Track, TrackFormat},
|
||||
};
|
||||
|
||||
@ -15,7 +15,7 @@ pub struct Album {
|
||||
pub id: AlbumId,
|
||||
pub date: AlbumDate,
|
||||
pub seq: AlbumSeq,
|
||||
pub musicbrainz: Option<MusicBrainz>,
|
||||
pub musicbrainz: Option<MusicBrainzUrl>,
|
||||
pub tracks: Vec<Track>,
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ use std::{
|
||||
use crate::core::collection::{
|
||||
album::Album,
|
||||
merge::{Merge, MergeCollections, WithId},
|
||||
musicbrainz::MusicBrainz,
|
||||
musicbrainz::MusicBrainzUrl,
|
||||
};
|
||||
|
||||
/// An artist.
|
||||
@ -15,7 +15,7 @@ use crate::core::collection::{
|
||||
pub struct Artist {
|
||||
pub id: ArtistId,
|
||||
pub sort: Option<ArtistId>,
|
||||
pub musicbrainz: Option<MusicBrainz>,
|
||||
pub musicbrainz: Option<MusicBrainzUrl>,
|
||||
pub properties: HashMap<String, Vec<String>>,
|
||||
pub albums: Vec<Album>,
|
||||
}
|
||||
@ -58,7 +58,7 @@ impl Artist {
|
||||
_ = self.sort.take();
|
||||
}
|
||||
|
||||
pub fn set_musicbrainz_url(&mut self, url: MusicBrainz) {
|
||||
pub fn set_musicbrainz_url(&mut self, url: MusicBrainzUrl) {
|
||||
_ = self.musicbrainz.insert(url);
|
||||
}
|
||||
|
||||
@ -216,19 +216,19 @@ mod tests {
|
||||
fn set_clear_musicbrainz_url() {
|
||||
let mut artist = Artist::new(ArtistId::new("an artist"));
|
||||
|
||||
let mut expected: Option<MusicBrainz> = None;
|
||||
let mut expected: Option<MusicBrainzUrl> = None;
|
||||
assert_eq!(artist.musicbrainz, expected);
|
||||
|
||||
// Setting a URL on an artist.
|
||||
artist.set_musicbrainz_url(MusicBrainz::artist_from_str(MUSICBRAINZ).unwrap());
|
||||
_ = expected.insert(MusicBrainz::artist_from_str(MUSICBRAINZ).unwrap());
|
||||
artist.set_musicbrainz_url(MusicBrainzUrl::artist_from_str(MUSICBRAINZ).unwrap());
|
||||
_ = expected.insert(MusicBrainzUrl::artist_from_str(MUSICBRAINZ).unwrap());
|
||||
assert_eq!(artist.musicbrainz, expected);
|
||||
|
||||
artist.set_musicbrainz_url(MusicBrainz::artist_from_str(MUSICBRAINZ).unwrap());
|
||||
artist.set_musicbrainz_url(MusicBrainzUrl::artist_from_str(MUSICBRAINZ).unwrap());
|
||||
assert_eq!(artist.musicbrainz, expected);
|
||||
|
||||
artist.set_musicbrainz_url(MusicBrainz::artist_from_str(MUSICBRAINZ_2).unwrap());
|
||||
_ = expected.insert(MusicBrainz::artist_from_str(MUSICBRAINZ_2).unwrap());
|
||||
artist.set_musicbrainz_url(MusicBrainzUrl::artist_from_str(MUSICBRAINZ_2).unwrap());
|
||||
_ = expected.insert(MusicBrainzUrl::artist_from_str(MUSICBRAINZ_2).unwrap());
|
||||
assert_eq!(artist.musicbrainz, expected);
|
||||
|
||||
// Clearing URLs.
|
||||
|
@ -5,17 +5,16 @@ use uuid::Uuid;
|
||||
|
||||
use crate::core::collection::Error;
|
||||
|
||||
/// An object with the [`IMbid`] trait contains a [MusicBrainz
|
||||
/// Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID).
|
||||
pub trait IMbid {
|
||||
fn mbid(&self) -> &str;
|
||||
}
|
||||
|
||||
/// MusicBrainz reference.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct MusicBrainz(Url);
|
||||
pub struct MusicBrainzUrl(Url);
|
||||
|
||||
impl MusicBrainzUrl {
|
||||
pub fn mbid(&self) -> &str {
|
||||
// The URL is assumed to have been validated.
|
||||
self.0.path_segments().and_then(|mut ps| ps.nth(1)).unwrap()
|
||||
}
|
||||
|
||||
impl MusicBrainz {
|
||||
pub fn artist_from_str<S: AsRef<str>>(url: S) -> Result<Self, Error> {
|
||||
Self::artist_from_url(url.as_ref().try_into()?)
|
||||
}
|
||||
@ -52,7 +51,7 @@ impl MusicBrainz {
|
||||
None => return Err(Self::invalid_url_error(url, mb_type)),
|
||||
};
|
||||
|
||||
Ok(MusicBrainz(url))
|
||||
Ok(MusicBrainzUrl(url))
|
||||
}
|
||||
|
||||
fn invalid_url_error<U: Display>(url: U, mb_type: &str) -> Error {
|
||||
@ -60,19 +59,12 @@ impl MusicBrainz {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for MusicBrainz {
|
||||
impl AsRef<str> for MusicBrainzUrl {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl IMbid for MusicBrainz {
|
||||
fn mbid(&self) -> &str {
|
||||
// The URL is assumed to have been validated.
|
||||
self.0.path_segments().and_then(|mut ps| ps.nth(1)).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@ -82,12 +74,12 @@ mod tests {
|
||||
let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8";
|
||||
let url_str = format!("https://musicbrainz.org/artist/{uuid}");
|
||||
|
||||
let mb = MusicBrainz::artist_from_str(&url_str).unwrap();
|
||||
let mb = MusicBrainzUrl::artist_from_str(&url_str).unwrap();
|
||||
assert_eq!(url_str, mb.as_ref());
|
||||
assert_eq!(uuid, mb.mbid());
|
||||
|
||||
let url: Url = url_str.as_str().try_into().unwrap();
|
||||
let mb = MusicBrainz::artist_from_url(url).unwrap();
|
||||
let mb = MusicBrainzUrl::artist_from_url(url).unwrap();
|
||||
assert_eq!(url_str, mb.as_ref());
|
||||
assert_eq!(uuid, mb.mbid());
|
||||
}
|
||||
@ -97,12 +89,12 @@ mod tests {
|
||||
let uuid = "d368baa8-21ca-4759-9731-0b2753071ad8";
|
||||
let url_str = format!("https://musicbrainz.org/release-group/{uuid}");
|
||||
|
||||
let mb = MusicBrainz::album_from_str(&url_str).unwrap();
|
||||
let mb = MusicBrainzUrl::album_from_str(&url_str).unwrap();
|
||||
assert_eq!(url_str, mb.as_ref());
|
||||
assert_eq!(uuid, mb.mbid());
|
||||
|
||||
let url: Url = url_str.as_str().try_into().unwrap();
|
||||
let mb = MusicBrainz::album_from_url(url).unwrap();
|
||||
let mb = MusicBrainzUrl::album_from_url(url).unwrap();
|
||||
assert_eq!(url_str, mb.as_ref());
|
||||
assert_eq!(uuid, mb.mbid());
|
||||
}
|
||||
@ -111,7 +103,7 @@ mod tests {
|
||||
fn not_a_url() {
|
||||
let url = "not a url at all";
|
||||
let expected_error: Error = url::ParseError::RelativeUrlWithoutBase.into();
|
||||
let actual_error = MusicBrainz::artist_from_str(url).unwrap_err();
|
||||
let actual_error = MusicBrainzUrl::artist_from_str(url).unwrap_err();
|
||||
assert_eq!(actual_error, expected_error);
|
||||
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
||||
}
|
||||
@ -120,7 +112,7 @@ mod tests {
|
||||
fn invalid_url() {
|
||||
let url = "https://www.musicbutler.io/artist-page/483340948";
|
||||
let expected_error = Error::UrlError(format!("invalid artist MusicBrainz URL: {url}"));
|
||||
let actual_error = MusicBrainz::artist_from_str(url).unwrap_err();
|
||||
let actual_error = MusicBrainzUrl::artist_from_str(url).unwrap_err();
|
||||
assert_eq!(actual_error, expected_error);
|
||||
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
||||
}
|
||||
@ -129,7 +121,7 @@ mod tests {
|
||||
fn artist_invalid_type() {
|
||||
let url = "https://musicbrainz.org/release-group/i-am-not-a-uuid";
|
||||
let expected_error = Error::UrlError(format!("invalid artist MusicBrainz URL: {url}"));
|
||||
let actual_error = MusicBrainz::artist_from_str(url).unwrap_err();
|
||||
let actual_error = MusicBrainzUrl::artist_from_str(url).unwrap_err();
|
||||
assert_eq!(actual_error, expected_error);
|
||||
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
||||
}
|
||||
@ -139,7 +131,7 @@ mod tests {
|
||||
let url = "https://musicbrainz.org/artist/i-am-not-a-uuid";
|
||||
let expected_error =
|
||||
Error::UrlError(format!("invalid release-group MusicBrainz URL: {url}"));
|
||||
let actual_error = MusicBrainz::album_from_str(url).unwrap_err();
|
||||
let actual_error = MusicBrainzUrl::album_from_str(url).unwrap_err();
|
||||
assert_eq!(actual_error, expected_error);
|
||||
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
||||
}
|
||||
@ -148,7 +140,7 @@ mod tests {
|
||||
fn invalid_uuid() {
|
||||
let url = "https://musicbrainz.org/artist/i-am-not-a-uuid";
|
||||
let expected_error: Error = Uuid::try_parse("i-am-not-a-uuid").unwrap_err().into();
|
||||
let actual_error = MusicBrainz::artist_from_str(url).unwrap_err();
|
||||
let actual_error = MusicBrainzUrl::artist_from_str(url).unwrap_err();
|
||||
assert_eq!(actual_error, expected_error);
|
||||
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
||||
}
|
||||
@ -157,7 +149,7 @@ mod tests {
|
||||
fn missing_type() {
|
||||
let url = "https://musicbrainz.org";
|
||||
let expected_error = Error::UrlError(format!("invalid artist MusicBrainz URL: {url}/"));
|
||||
let actual_error = MusicBrainz::artist_from_str(url).unwrap_err();
|
||||
let actual_error = MusicBrainzUrl::artist_from_str(url).unwrap_err();
|
||||
assert_eq!(actual_error, expected_error);
|
||||
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
||||
}
|
||||
@ -166,7 +158,7 @@ mod tests {
|
||||
fn missing_uuid() {
|
||||
let url = "https://musicbrainz.org/artist";
|
||||
let expected_error = Error::UrlError(format!("invalid artist MusicBrainz URL: {url}"));
|
||||
let actual_error = MusicBrainz::artist_from_str(url).unwrap_err();
|
||||
let actual_error = MusicBrainzUrl::artist_from_str(url).unwrap_err();
|
||||
assert_eq!(actual_error, expected_error);
|
||||
assert_eq!(actual_error.to_string(), expected_error.to_string());
|
||||
}
|
||||
|
230
src/core/musichoard/base.rs
Normal file
230
src/core/musichoard/base.rs
Normal file
@ -0,0 +1,230 @@
|
||||
use crate::core::{
|
||||
collection::{
|
||||
album::{Album, AlbumId},
|
||||
artist::{Artist, ArtistId},
|
||||
Collection, MergeCollections,
|
||||
},
|
||||
musichoard::{Error, MusicHoard},
|
||||
};
|
||||
|
||||
pub trait IMusicHoardBase {
|
||||
fn get_collection(&self) -> &Collection;
|
||||
}
|
||||
|
||||
impl<Database, Library> IMusicHoardBase for MusicHoard<Database, Library> {
|
||||
fn get_collection(&self) -> &Collection {
|
||||
&self.collection
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IMusicHoardBasePrivate {
|
||||
fn sort_artists(collection: &mut [Artist]);
|
||||
fn sort_albums_and_tracks<'a, C: Iterator<Item = &'a mut Artist>>(collection: C);
|
||||
|
||||
fn merge_collections(&self) -> Collection;
|
||||
|
||||
fn get_artist<'a>(collection: &'a Collection, artist_id: &ArtistId) -> Option<&'a Artist>;
|
||||
fn get_artist_mut<'a>(
|
||||
collection: &'a mut Collection,
|
||||
artist_id: &ArtistId,
|
||||
) -> Option<&'a mut Artist>;
|
||||
fn get_artist_mut_or_err<'a>(
|
||||
collection: &'a mut Collection,
|
||||
artist_id: &ArtistId,
|
||||
) -> Result<&'a mut Artist, Error>;
|
||||
|
||||
fn get_album_mut<'a>(artist: &'a mut Artist, album_id: &AlbumId) -> Option<&'a mut Album>;
|
||||
fn get_album_mut_or_err<'a>(
|
||||
artist: &'a mut Artist,
|
||||
album_id: &AlbumId,
|
||||
) -> Result<&'a mut Album, Error>;
|
||||
}
|
||||
|
||||
impl<Database, Library> IMusicHoardBasePrivate for MusicHoard<Database, Library> {
|
||||
fn sort_artists(collection: &mut [Artist]) {
|
||||
collection.sort_unstable();
|
||||
}
|
||||
|
||||
fn sort_albums_and_tracks<'a, COL: Iterator<Item = &'a mut Artist>>(collection: COL) {
|
||||
for artist in collection {
|
||||
artist.albums.sort_unstable();
|
||||
for album in artist.albums.iter_mut() {
|
||||
album.tracks.sort_unstable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_collections(&self) -> Collection {
|
||||
MergeCollections::merge(self.library_cache.clone(), self.database_cache.clone())
|
||||
}
|
||||
|
||||
fn get_artist<'a>(collection: &'a Collection, artist_id: &ArtistId) -> Option<&'a Artist> {
|
||||
collection.iter().find(|a| &a.id == artist_id)
|
||||
}
|
||||
|
||||
fn get_artist_mut<'a>(
|
||||
collection: &'a mut Collection,
|
||||
artist_id: &ArtistId,
|
||||
) -> Option<&'a mut Artist> {
|
||||
collection.iter_mut().find(|a| &a.id == artist_id)
|
||||
}
|
||||
|
||||
fn get_artist_mut_or_err<'a>(
|
||||
collection: &'a mut Collection,
|
||||
artist_id: &ArtistId,
|
||||
) -> Result<&'a mut Artist, Error> {
|
||||
Self::get_artist_mut(collection, artist_id).ok_or_else(|| {
|
||||
Error::CollectionError(format!("artist '{}' is not in the collection", artist_id))
|
||||
})
|
||||
}
|
||||
|
||||
fn get_album_mut<'a>(artist: &'a mut Artist, album_id: &AlbumId) -> Option<&'a mut Album> {
|
||||
artist.albums.iter_mut().find(|a| &a.id == album_id)
|
||||
}
|
||||
|
||||
fn get_album_mut_or_err<'a>(
|
||||
artist: &'a mut Artist,
|
||||
album_id: &AlbumId,
|
||||
) -> Result<&'a mut Album, Error> {
|
||||
Self::get_album_mut(artist, album_id).ok_or_else(|| {
|
||||
Error::CollectionError(format!(
|
||||
"album '{}' does not belong to the artist",
|
||||
album_id
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::core::{collection::artist::ArtistId, testmod::FULL_COLLECTION};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn merge_collection_no_overlap() {
|
||||
let half: usize = FULL_COLLECTION.len() / 2;
|
||||
|
||||
let left = FULL_COLLECTION[..half].to_owned();
|
||||
let right = FULL_COLLECTION[half..].to_owned();
|
||||
|
||||
let mut expected = FULL_COLLECTION.to_owned();
|
||||
expected.sort_unstable();
|
||||
|
||||
let mut mh = MusicHoard {
|
||||
library_cache: left
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|a| (a.id.clone(), a))
|
||||
.collect(),
|
||||
database_cache: right.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
mh.collection = mh.merge_collections();
|
||||
assert_eq!(expected, mh.collection);
|
||||
|
||||
// The merge is completely non-overlapping so it should be commutative.
|
||||
let mut mh = MusicHoard {
|
||||
library_cache: right
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|a| (a.id.clone(), a))
|
||||
.collect(),
|
||||
database_cache: left.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
mh.collection = mh.merge_collections();
|
||||
assert_eq!(expected, mh.collection);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_collection_overlap() {
|
||||
let half: usize = FULL_COLLECTION.len() / 2;
|
||||
|
||||
let left = FULL_COLLECTION[..(half + 1)].to_owned();
|
||||
let right = FULL_COLLECTION[half..].to_owned();
|
||||
|
||||
let mut expected = FULL_COLLECTION.to_owned();
|
||||
expected.sort_unstable();
|
||||
|
||||
let mut mh = MusicHoard {
|
||||
library_cache: left
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|a| (a.id.clone(), a))
|
||||
.collect(),
|
||||
database_cache: right.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
mh.collection = mh.merge_collections();
|
||||
assert_eq!(expected, mh.collection);
|
||||
|
||||
// The merge does not overwrite any data so it should be commutative.
|
||||
let mut mh = MusicHoard {
|
||||
library_cache: right
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|a| (a.id.clone(), a))
|
||||
.collect(),
|
||||
database_cache: left.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
mh.collection = mh.merge_collections();
|
||||
assert_eq!(expected, mh.collection);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_collection_incompatible_sorting() {
|
||||
// It may be that the same artist in one collection has a "sort" field defined while the
|
||||
// same artist in the other collection does not. This means that the two collections are not
|
||||
// sorted consistently. If the merge assumes they are sorted consistently this will lead to
|
||||
// the same artist appearing twice in the final list. This should not be the case.
|
||||
|
||||
// We will mimic this situation by taking the last artist from FULL_COLLECTION and giving it
|
||||
// a sorting name that would place it in the beginning.
|
||||
let left = FULL_COLLECTION.to_owned();
|
||||
let mut right: Vec<Artist> = vec![left.last().unwrap().clone()];
|
||||
|
||||
assert!(right.first().unwrap() > left.first().unwrap());
|
||||
let artist_sort = Some(ArtistId::new("Album_Artist 0"));
|
||||
right[0].sort = artist_sort.clone();
|
||||
assert!(right.first().unwrap() < left.first().unwrap());
|
||||
|
||||
// The result of the merge should be the same list of artists, but with the last artist now
|
||||
// in first place.
|
||||
let mut expected = left.to_owned();
|
||||
expected.last_mut().as_mut().unwrap().sort = artist_sort.clone();
|
||||
expected.rotate_right(1);
|
||||
|
||||
let mut mh = MusicHoard {
|
||||
library_cache: left
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|a| (a.id.clone(), a))
|
||||
.collect(),
|
||||
database_cache: right.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
mh.collection = mh.merge_collections();
|
||||
assert_eq!(expected, mh.collection);
|
||||
|
||||
// The merge overwrites the sort data, but no data is erased so it should be commutative.
|
||||
let mut mh = MusicHoard {
|
||||
library_cache: right
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|a| (a.id.clone(), a))
|
||||
.collect(),
|
||||
database_cache: left.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
mh.collection = mh.merge_collections();
|
||||
assert_eq!(expected, mh.collection);
|
||||
}
|
||||
}
|
194
src/core/musichoard/builder.rs
Normal file
194
src/core/musichoard/builder.rs
Normal file
@ -0,0 +1,194 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
core::{
|
||||
interface::{database::IDatabase, library::ILibrary},
|
||||
musichoard::{database::IMusicHoardDatabase, MusicHoard, NoDatabase, NoLibrary},
|
||||
},
|
||||
Error,
|
||||
};
|
||||
|
||||
/// Builder for [`MusicHoard`]. Its purpose is to make it easier to set various combinations of
|
||||
/// library/database or their absence.
|
||||
pub struct MusicHoardBuilder<Database, Library> {
|
||||
database: Database,
|
||||
library: Library,
|
||||
}
|
||||
|
||||
impl Default for MusicHoardBuilder<NoDatabase, NoLibrary> {
|
||||
/// Create a [`MusicHoardBuilder`].
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MusicHoardBuilder<NoDatabase, NoLibrary> {
|
||||
/// Create a [`MusicHoardBuilder`].
|
||||
pub fn new() -> Self {
|
||||
MusicHoardBuilder {
|
||||
database: NoDatabase,
|
||||
library: NoLibrary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Database, Library> MusicHoardBuilder<Database, Library> {
|
||||
/// Set a library for [`MusicHoard`].
|
||||
pub fn set_library<NewLibrary: ILibrary>(
|
||||
self,
|
||||
library: NewLibrary,
|
||||
) -> MusicHoardBuilder<Database, NewLibrary> {
|
||||
MusicHoardBuilder {
|
||||
database: self.database,
|
||||
library,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a database for [`MusicHoard`].
|
||||
pub fn set_database<NewDatabase: IDatabase>(
|
||||
self,
|
||||
database: NewDatabase,
|
||||
) -> MusicHoardBuilder<NewDatabase, Library> {
|
||||
MusicHoardBuilder {
|
||||
database,
|
||||
library: self.library,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MusicHoardBuilder<NoDatabase, NoLibrary> {
|
||||
/// Build [`MusicHoard`] with the currently set library and database.
|
||||
pub fn build(self) -> MusicHoard<NoDatabase, NoLibrary> {
|
||||
MusicHoard::empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl MusicHoard<NoDatabase, NoLibrary> {
|
||||
/// Create a new [`MusicHoard`] without any library or database.
|
||||
pub fn empty() -> Self {
|
||||
MusicHoard {
|
||||
collection: vec![],
|
||||
pre_commit: vec![],
|
||||
database: NoDatabase,
|
||||
database_cache: vec![],
|
||||
library: NoLibrary,
|
||||
library_cache: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Library: ILibrary> MusicHoardBuilder<NoDatabase, Library> {
|
||||
/// Build [`MusicHoard`] with the currently set library and database.
|
||||
pub fn build(self) -> MusicHoard<NoDatabase, Library> {
|
||||
MusicHoard::library(self.library)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Library: ILibrary> MusicHoard<NoDatabase, Library> {
|
||||
/// Create a new [`MusicHoard`] with the provided [`ILibrary`] and no database.
|
||||
pub fn library(library: Library) -> Self {
|
||||
MusicHoard {
|
||||
collection: vec![],
|
||||
pre_commit: vec![],
|
||||
database: NoDatabase,
|
||||
database_cache: vec![],
|
||||
library,
|
||||
library_cache: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Database: IDatabase> MusicHoardBuilder<Database, NoLibrary> {
|
||||
/// Build [`MusicHoard`] with the currently set library and database.
|
||||
pub fn build(self) -> Result<MusicHoard<Database, NoLibrary>, Error> {
|
||||
MusicHoard::database(self.database)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Database: IDatabase> MusicHoard<Database, NoLibrary> {
|
||||
/// Create a new [`MusicHoard`] with the provided [`IDatabase`] and no library.
|
||||
pub fn database(database: Database) -> Result<Self, Error> {
|
||||
let mut mh = MusicHoard {
|
||||
collection: vec![],
|
||||
pre_commit: vec![],
|
||||
database,
|
||||
database_cache: vec![],
|
||||
library: NoLibrary,
|
||||
library_cache: HashMap::new(),
|
||||
};
|
||||
mh.reload_database()?;
|
||||
Ok(mh)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Database: IDatabase, Library: ILibrary> MusicHoardBuilder<Database, Library> {
|
||||
/// Build [`MusicHoard`] with the currently set library and database.
|
||||
pub fn build(self) -> Result<MusicHoard<Database, Library>, Error> {
|
||||
MusicHoard::new(self.database, self.library)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Database: IDatabase, Library: ILibrary> MusicHoard<Database, Library> {
|
||||
/// Create a new [`MusicHoard`] with the provided [`ILibrary`] and [`IDatabase`].
|
||||
pub fn new(database: Database, library: Library) -> Result<Self, Error> {
|
||||
let mut mh = MusicHoard {
|
||||
collection: vec![],
|
||||
pre_commit: vec![],
|
||||
database,
|
||||
database_cache: vec![],
|
||||
library,
|
||||
library_cache: HashMap::new(),
|
||||
};
|
||||
mh.reload_database()?;
|
||||
Ok(mh)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::core::{
|
||||
interface::{database::NullDatabase, library::NullLibrary},
|
||||
musichoard::library::IMusicHoardLibrary,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn no_library_no_database() {
|
||||
MusicHoardBuilder::default().build();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let music_hoard = MusicHoard::empty();
|
||||
assert!(!format!("{music_hoard:?}").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_library_no_database() {
|
||||
let mut mh = MusicHoardBuilder::default()
|
||||
.set_library(NullLibrary)
|
||||
.build();
|
||||
assert!(mh.rescan_library().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_library_with_database() {
|
||||
let mut mh = MusicHoardBuilder::default()
|
||||
.set_database(NullDatabase)
|
||||
.build()
|
||||
.unwrap();
|
||||
assert!(mh.reload_database().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_library_with_database() {
|
||||
let mut mh = MusicHoardBuilder::default()
|
||||
.set_library(NullLibrary)
|
||||
.set_database(NullDatabase)
|
||||
.build()
|
||||
.unwrap();
|
||||
assert!(mh.rescan_library().is_ok());
|
||||
assert!(mh.reload_database().is_ok());
|
||||
}
|
||||
}
|
661
src/core/musichoard/database.rs
Normal file
661
src/core/musichoard/database.rs
Normal file
@ -0,0 +1,661 @@
|
||||
use crate::core::{
|
||||
collection::{
|
||||
album::{Album, AlbumId, AlbumSeq},
|
||||
artist::{Artist, ArtistId},
|
||||
musicbrainz::MusicBrainzUrl,
|
||||
Collection,
|
||||
},
|
||||
interface::database::IDatabase,
|
||||
musichoard::{base::IMusicHoardBasePrivate, Error, MusicHoard, NoDatabase},
|
||||
};
|
||||
|
||||
pub trait IMusicHoardDatabase {
|
||||
fn reload_database(&mut self) -> Result<(), Error>;
|
||||
|
||||
fn add_artist<IntoId: Into<ArtistId>>(&mut self, artist_id: IntoId) -> Result<(), Error>;
|
||||
fn remove_artist<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error>;
|
||||
|
||||
fn set_artist_sort<Id: AsRef<ArtistId>, IntoId: Into<ArtistId>>(
|
||||
&mut self,
|
||||
artist_id: Id,
|
||||
artist_sort: IntoId,
|
||||
) -> Result<(), Error>;
|
||||
fn clear_artist_sort<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error>;
|
||||
|
||||
fn set_artist_musicbrainz<Id: AsRef<ArtistId>, S: AsRef<str>>(
|
||||
&mut self,
|
||||
artist_id: Id,
|
||||
url: S,
|
||||
) -> Result<(), Error>;
|
||||
fn clear_artist_musicbrainz<Id: AsRef<ArtistId>>(&mut self, artist_id: Id)
|
||||
-> Result<(), Error>;
|
||||
|
||||
fn add_to_artist_property<Id: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
|
||||
&mut self,
|
||||
artist_id: Id,
|
||||
property: S,
|
||||
values: Vec<S>,
|
||||
) -> Result<(), Error>;
|
||||
fn remove_from_artist_property<Id: AsRef<ArtistId>, S: AsRef<str>>(
|
||||
&mut self,
|
||||
artist_id: Id,
|
||||
property: S,
|
||||
values: Vec<S>,
|
||||
) -> Result<(), Error>;
|
||||
fn set_artist_property<Id: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
|
||||
&mut self,
|
||||
artist_id: Id,
|
||||
property: S,
|
||||
values: Vec<S>,
|
||||
) -> Result<(), Error>;
|
||||
fn clear_artist_property<Id: AsRef<ArtistId>, S: AsRef<str>>(
|
||||
&mut self,
|
||||
artist_id: Id,
|
||||
property: S,
|
||||
) -> Result<(), Error>;
|
||||
|
||||
fn set_album_seq<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
|
||||
&mut self,
|
||||
artist_id: ArtistIdRef,
|
||||
album_id: AlbumIdRef,
|
||||
seq: u8,
|
||||
) -> Result<(), Error>;
|
||||
fn clear_album_seq<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
|
||||
&mut self,
|
||||
artist_id: ArtistIdRef,
|
||||
album_id: AlbumIdRef,
|
||||
) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database, Library> {
|
||||
fn reload_database(&mut self) -> Result<(), Error> {
|
||||
self.database_cache = self.database.load()?;
|
||||
Self::sort_albums_and_tracks(self.database_cache.iter_mut());
|
||||
|
||||
self.collection = self.merge_collections();
|
||||
self.pre_commit = self.collection.clone();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_artist<IntoId: Into<ArtistId>>(&mut self, artist_id: IntoId) -> Result<(), Error> {
|
||||
let artist_id: ArtistId = artist_id.into();
|
||||
|
||||
self.update_collection(|collection| {
|
||||
if Self::get_artist(collection, &artist_id).is_none() {
|
||||
collection.push(Artist::new(artist_id));
|
||||
Self::sort_artists(collection);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_artist<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error> {
|
||||
self.update_collection(|collection| {
|
||||
let index_opt = collection.iter().position(|a| &a.id == artist_id.as_ref());
|
||||
if let Some(index) = index_opt {
|
||||
collection.remove(index);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn set_artist_sort<Id: AsRef<ArtistId>, IntoId: Into<ArtistId>>(
|
||||
&mut self,
|
||||
artist_id: Id,
|
||||
artist_sort: IntoId,
|
||||
) -> Result<(), Error> {
|
||||
self.update_artist_and(
|
||||
artist_id.as_ref(),
|
||||
|artist| artist.set_sort_key(artist_sort),
|
||||
|collection| Self::sort_artists(collection),
|
||||
)
|
||||
}
|
||||
|
||||
fn clear_artist_sort<Id: AsRef<ArtistId>>(&mut self, artist_id: Id) -> Result<(), Error> {
|
||||
self.update_artist_and(
|
||||
artist_id.as_ref(),
|
||||
|artist| artist.clear_sort_key(),
|
||||
|collection| Self::sort_artists(collection),
|
||||
)
|
||||
}
|
||||
|
||||
fn set_artist_musicbrainz<Id: AsRef<ArtistId>, S: AsRef<str>>(
|
||||
&mut self,
|
||||
artist_id: Id,
|
||||
url: S,
|
||||
) -> Result<(), Error> {
|
||||
let mb = MusicBrainzUrl::artist_from_str(url)?;
|
||||
self.update_artist(artist_id.as_ref(), |artist| artist.set_musicbrainz_url(mb))
|
||||
}
|
||||
|
||||
fn clear_artist_musicbrainz<Id: AsRef<ArtistId>>(
|
||||
&mut self,
|
||||
artist_id: Id,
|
||||
) -> Result<(), Error> {
|
||||
self.update_artist(artist_id.as_ref(), |artist| artist.clear_musicbrainz_url())
|
||||
}
|
||||
|
||||
fn add_to_artist_property<Id: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
|
||||
&mut self,
|
||||
artist_id: Id,
|
||||
property: S,
|
||||
values: Vec<S>,
|
||||
) -> Result<(), Error> {
|
||||
self.update_artist(artist_id.as_ref(), |artist| {
|
||||
artist.add_to_property(property, values)
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_from_artist_property<Id: AsRef<ArtistId>, S: AsRef<str>>(
|
||||
&mut self,
|
||||
artist_id: Id,
|
||||
property: S,
|
||||
values: Vec<S>,
|
||||
) -> Result<(), Error> {
|
||||
self.update_artist(artist_id.as_ref(), |artist| {
|
||||
artist.remove_from_property(property, values)
|
||||
})
|
||||
}
|
||||
|
||||
fn set_artist_property<Id: AsRef<ArtistId>, S: AsRef<str> + Into<String>>(
|
||||
&mut self,
|
||||
artist_id: Id,
|
||||
property: S,
|
||||
values: Vec<S>,
|
||||
) -> Result<(), Error> {
|
||||
self.update_artist(artist_id.as_ref(), |artist| {
|
||||
artist.set_property(property, values)
|
||||
})
|
||||
}
|
||||
|
||||
fn clear_artist_property<Id: AsRef<ArtistId>, S: AsRef<str>>(
|
||||
&mut self,
|
||||
artist_id: Id,
|
||||
property: S,
|
||||
) -> Result<(), Error> {
|
||||
self.update_artist(artist_id.as_ref(), |artist| artist.clear_property(property))
|
||||
}
|
||||
|
||||
fn set_album_seq<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
|
||||
&mut self,
|
||||
artist_id: ArtistIdRef,
|
||||
album_id: AlbumIdRef,
|
||||
seq: u8,
|
||||
) -> Result<(), Error> {
|
||||
self.update_album_and(
|
||||
artist_id.as_ref(),
|
||||
album_id.as_ref(),
|
||||
|album| album.set_seq(AlbumSeq(seq)),
|
||||
|artist| artist.albums.sort_unstable(),
|
||||
|_| {},
|
||||
)
|
||||
}
|
||||
|
||||
fn clear_album_seq<ArtistIdRef: AsRef<ArtistId>, AlbumIdRef: AsRef<AlbumId>>(
|
||||
&mut self,
|
||||
artist_id: ArtistIdRef,
|
||||
album_id: AlbumIdRef,
|
||||
) -> Result<(), Error> {
|
||||
self.update_album_and(
|
||||
artist_id.as_ref(),
|
||||
album_id.as_ref(),
|
||||
|album| album.clear_seq(),
|
||||
|artist| artist.albums.sort_unstable(),
|
||||
|_| {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IMusicHoardDatabasePrivate {
|
||||
fn commit(&mut self) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
impl<Library> IMusicHoardDatabasePrivate for MusicHoard<NoDatabase, Library> {
|
||||
fn commit(&mut self) -> Result<(), Error> {
|
||||
self.collection = self.pre_commit.clone();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Database: IDatabase, Library> IMusicHoardDatabasePrivate for MusicHoard<Database, Library> {
|
||||
fn commit(&mut self) -> Result<(), Error> {
|
||||
if self.collection != self.pre_commit {
|
||||
if let Err(err) = self.database.save(&self.pre_commit) {
|
||||
self.pre_commit = self.collection.clone();
|
||||
return Err(err.into());
|
||||
}
|
||||
self.collection = self.pre_commit.clone();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Database: IDatabase, Library> MusicHoard<Database, Library> {
|
||||
fn update_collection<FnColl>(&mut self, fn_coll: FnColl) -> Result<(), Error>
|
||||
where
|
||||
FnColl: FnOnce(&mut Collection),
|
||||
{
|
||||
fn_coll(&mut self.pre_commit);
|
||||
self.commit()
|
||||
}
|
||||
|
||||
fn update_artist_and<FnArtist, FnColl>(
|
||||
&mut self,
|
||||
artist_id: &ArtistId,
|
||||
fn_artist: FnArtist,
|
||||
fn_coll: FnColl,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
FnArtist: FnOnce(&mut Artist),
|
||||
FnColl: FnOnce(&mut Collection),
|
||||
{
|
||||
let artist = Self::get_artist_mut_or_err(&mut self.pre_commit, artist_id)?;
|
||||
fn_artist(artist);
|
||||
self.update_collection(fn_coll)
|
||||
}
|
||||
|
||||
fn update_artist<FnArtist>(
|
||||
&mut self,
|
||||
artist_id: &ArtistId,
|
||||
fn_artist: FnArtist,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
FnArtist: FnOnce(&mut Artist),
|
||||
{
|
||||
self.update_artist_and(artist_id, fn_artist, |_| {})
|
||||
}
|
||||
|
||||
fn update_album_and<FnAlbum, FnArtist, FnColl>(
|
||||
&mut self,
|
||||
artist_id: &ArtistId,
|
||||
album_id: &AlbumId,
|
||||
fn_album: FnAlbum,
|
||||
fn_artist: FnArtist,
|
||||
fn_coll: FnColl,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
FnAlbum: FnOnce(&mut Album),
|
||||
FnArtist: FnOnce(&mut Artist),
|
||||
FnColl: FnOnce(&mut Collection),
|
||||
{
|
||||
let artist = Self::get_artist_mut_or_err(&mut self.pre_commit, artist_id)?;
|
||||
let album = Self::get_album_mut_or_err(artist, album_id)?;
|
||||
fn_album(album);
|
||||
fn_artist(artist);
|
||||
self.update_collection(fn_coll)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mockall::{predicate, Sequence};
|
||||
|
||||
use crate::core::{
|
||||
collection::{album::AlbumDate, artist::ArtistId, musicbrainz::MusicBrainzUrl},
|
||||
interface::database::{self, MockIDatabase},
|
||||
musichoard::{base::IMusicHoardBase, NoLibrary},
|
||||
testmod::FULL_COLLECTION,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
static MUSICBRAINZ: &str =
|
||||
"https://musicbrainz.org/artist/d368baa8-21ca-4759-9731-0b2753071ad8";
|
||||
static MUSICBUTLER: &str = "https://www.musicbutler.io/artist-page/483340948";
|
||||
static MUSICBUTLER_2: &str = "https://www.musicbutler.io/artist-page/658903042/";
|
||||
|
||||
#[test]
|
||||
fn artist_new_delete() {
|
||||
let artist_id = ArtistId::new("an artist");
|
||||
let artist_id_2 = ArtistId::new("another artist");
|
||||
|
||||
let collection = FULL_COLLECTION.to_owned();
|
||||
let mut with_artist = collection.clone();
|
||||
with_artist.push(Artist::new(artist_id.clone()));
|
||||
|
||||
let mut database = MockIDatabase::new();
|
||||
let mut seq = Sequence::new();
|
||||
database
|
||||
.expect_load()
|
||||
.times(1)
|
||||
.times(1)
|
||||
.in_sequence(&mut seq)
|
||||
.returning(|| Ok(FULL_COLLECTION.to_owned()));
|
||||
database
|
||||
.expect_save()
|
||||
.times(1)
|
||||
.in_sequence(&mut seq)
|
||||
.with(predicate::eq(with_artist.clone()))
|
||||
.returning(|_| Ok(()));
|
||||
database
|
||||
.expect_save()
|
||||
.times(1)
|
||||
.in_sequence(&mut seq)
|
||||
.with(predicate::eq(collection.clone()))
|
||||
.returning(|_| Ok(()));
|
||||
|
||||
let mut music_hoard = MusicHoard::database(database).unwrap();
|
||||
assert_eq!(music_hoard.collection, collection);
|
||||
|
||||
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
|
||||
assert_eq!(music_hoard.collection, with_artist);
|
||||
|
||||
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
|
||||
assert_eq!(music_hoard.collection, with_artist);
|
||||
|
||||
assert!(music_hoard.remove_artist(&artist_id_2).is_ok());
|
||||
assert_eq!(music_hoard.collection, with_artist);
|
||||
|
||||
assert!(music_hoard.remove_artist(&artist_id).is_ok());
|
||||
assert_eq!(music_hoard.collection, collection);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artist_sort_set_clear() {
|
||||
let mut database = MockIDatabase::new();
|
||||
database.expect_load().times(1).returning(|| Ok(vec![]));
|
||||
database.expect_save().times(4).returning(|_| Ok(()));
|
||||
|
||||
type MH = MusicHoard<MockIDatabase, NoLibrary>;
|
||||
let mut music_hoard: MH = MusicHoard::database(database).unwrap();
|
||||
|
||||
let artist_1_id = ArtistId::new("the artist");
|
||||
let artist_1_sort = ArtistId::new("artist, the");
|
||||
|
||||
// Must be after "artist, the", but before "the artist"
|
||||
let artist_2_id = ArtistId::new("b-artist");
|
||||
|
||||
assert!(artist_1_sort < artist_2_id);
|
||||
assert!(artist_2_id < artist_1_id);
|
||||
|
||||
assert!(music_hoard.add_artist(artist_1_id.clone()).is_ok());
|
||||
assert!(music_hoard.add_artist(artist_2_id.clone()).is_ok());
|
||||
|
||||
let artist_1: &Artist = MH::get_artist(&music_hoard.collection, &artist_1_id).unwrap();
|
||||
let artist_2: &Artist = MH::get_artist(&music_hoard.collection, &artist_2_id).unwrap();
|
||||
|
||||
assert!(artist_2 < artist_1);
|
||||
|
||||
assert_eq!(artist_1, &music_hoard.collection[1]);
|
||||
assert_eq!(artist_2, &music_hoard.collection[0]);
|
||||
|
||||
music_hoard
|
||||
.set_artist_sort(artist_1_id.as_ref(), artist_1_sort.clone())
|
||||
.unwrap();
|
||||
|
||||
let artist_1: &Artist = MH::get_artist(&music_hoard.collection, &artist_1_id).unwrap();
|
||||
let artist_2: &Artist = MH::get_artist(&music_hoard.collection, &artist_2_id).unwrap();
|
||||
|
||||
assert!(artist_1 < artist_2);
|
||||
|
||||
assert_eq!(artist_1, &music_hoard.collection[0]);
|
||||
assert_eq!(artist_2, &music_hoard.collection[1]);
|
||||
|
||||
music_hoard.clear_artist_sort(artist_1_id.as_ref()).unwrap();
|
||||
|
||||
let artist_1: &Artist = MH::get_artist(&music_hoard.collection, &artist_1_id).unwrap();
|
||||
let artist_2: &Artist = MH::get_artist(&music_hoard.collection, &artist_2_id).unwrap();
|
||||
|
||||
assert!(artist_2 < artist_1);
|
||||
|
||||
assert_eq!(artist_1, &music_hoard.collection[1]);
|
||||
assert_eq!(artist_2, &music_hoard.collection[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collection_error() {
|
||||
let mut database = MockIDatabase::new();
|
||||
database.expect_load().times(1).returning(|| Ok(vec![]));
|
||||
database.expect_save().times(1).returning(|_| Ok(()));
|
||||
|
||||
let artist_id = ArtistId::new("an artist");
|
||||
let mut music_hoard = MusicHoard::database(database).unwrap();
|
||||
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
|
||||
|
||||
let actual_err = music_hoard
|
||||
.set_artist_musicbrainz(&artist_id, MUSICBUTLER)
|
||||
.unwrap_err();
|
||||
let expected_err = Error::CollectionError(format!(
|
||||
"an error occurred when processing a URL: invalid artist MusicBrainz URL: {MUSICBUTLER}"
|
||||
));
|
||||
assert_eq!(actual_err, expected_err);
|
||||
assert_eq!(actual_err.to_string(), expected_err.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_clear_musicbrainz_url() {
|
||||
let mut database = MockIDatabase::new();
|
||||
database.expect_load().times(1).returning(|| Ok(vec![]));
|
||||
database.expect_save().times(3).returning(|_| Ok(()));
|
||||
|
||||
let artist_id = ArtistId::new("an artist");
|
||||
let artist_id_2 = ArtistId::new("another artist");
|
||||
let mut music_hoard = MusicHoard::database(database).unwrap();
|
||||
|
||||
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
|
||||
|
||||
let mut expected: Option<MusicBrainzUrl> = None;
|
||||
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
||||
|
||||
// Setting a URL on an artist not in the collection is an error.
|
||||
assert!(music_hoard
|
||||
.set_artist_musicbrainz(&artist_id_2, MUSICBRAINZ)
|
||||
.is_err());
|
||||
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
||||
|
||||
// Setting a URL on an artist.
|
||||
assert!(music_hoard
|
||||
.set_artist_musicbrainz(&artist_id, MUSICBRAINZ)
|
||||
.is_ok());
|
||||
_ = expected.insert(MusicBrainzUrl::artist_from_str(MUSICBRAINZ).unwrap());
|
||||
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
||||
|
||||
// Clearing URLs on an artist that does not exist is an error.
|
||||
assert!(music_hoard.clear_artist_musicbrainz(&artist_id_2).is_err());
|
||||
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
||||
|
||||
// Clearing URLs.
|
||||
assert!(music_hoard.clear_artist_musicbrainz(&artist_id).is_ok());
|
||||
_ = expected.take();
|
||||
assert_eq!(music_hoard.collection[0].musicbrainz, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_to_remove_from_property() {
|
||||
let mut database = MockIDatabase::new();
|
||||
database.expect_load().times(1).returning(|| Ok(vec![]));
|
||||
database.expect_save().times(3).returning(|_| Ok(()));
|
||||
|
||||
let artist_id = ArtistId::new("an artist");
|
||||
let artist_id_2 = ArtistId::new("another artist");
|
||||
let mut music_hoard = MusicHoard::database(database).unwrap();
|
||||
|
||||
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
|
||||
|
||||
let mut expected: Vec<String> = vec![];
|
||||
assert!(music_hoard.collection[0].properties.is_empty());
|
||||
|
||||
// Adding URLs to an artist not in the collection is an error.
|
||||
assert!(music_hoard
|
||||
.add_to_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
||||
.is_err());
|
||||
assert!(music_hoard.collection[0].properties.is_empty());
|
||||
|
||||
// Adding mutliple URLs without clashes.
|
||||
assert!(music_hoard
|
||||
.add_to_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
|
||||
.is_ok());
|
||||
expected.push(MUSICBUTLER.to_owned());
|
||||
expected.push(MUSICBUTLER_2.to_owned());
|
||||
assert_eq!(
|
||||
music_hoard.collection[0].properties.get("MusicButler"),
|
||||
Some(&expected)
|
||||
);
|
||||
|
||||
// Removing URLs from an artist not in the collection is an error.
|
||||
assert!(music_hoard
|
||||
.remove_from_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
||||
.is_err());
|
||||
assert_eq!(
|
||||
music_hoard.collection[0].properties.get("MusicButler"),
|
||||
Some(&expected)
|
||||
);
|
||||
|
||||
// Removing multiple URLs without clashes.
|
||||
assert!(music_hoard
|
||||
.remove_from_artist_property(
|
||||
&artist_id,
|
||||
"MusicButler",
|
||||
vec![MUSICBUTLER, MUSICBUTLER_2]
|
||||
)
|
||||
.is_ok());
|
||||
expected.clear();
|
||||
assert!(music_hoard.collection[0].properties.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_clear_property() {
|
||||
let mut database = MockIDatabase::new();
|
||||
database.expect_load().times(1).returning(|| Ok(vec![]));
|
||||
database.expect_save().times(3).returning(|_| Ok(()));
|
||||
|
||||
let artist_id = ArtistId::new("an artist");
|
||||
let artist_id_2 = ArtistId::new("another artist");
|
||||
let mut music_hoard = MusicHoard::database(database).unwrap();
|
||||
|
||||
assert!(music_hoard.add_artist(artist_id.clone()).is_ok());
|
||||
|
||||
let mut expected: Vec<String> = vec![];
|
||||
assert!(music_hoard.collection[0].properties.is_empty());
|
||||
|
||||
// Seting URL on an artist not in the collection is an error.
|
||||
assert!(music_hoard
|
||||
.set_artist_property(&artist_id_2, "MusicButler", vec![MUSICBUTLER])
|
||||
.is_err());
|
||||
assert!(music_hoard.collection[0].properties.is_empty());
|
||||
|
||||
// Set URLs.
|
||||
assert!(music_hoard
|
||||
.set_artist_property(&artist_id, "MusicButler", vec![MUSICBUTLER, MUSICBUTLER_2])
|
||||
.is_ok());
|
||||
expected.clear();
|
||||
expected.push(MUSICBUTLER.to_owned());
|
||||
expected.push(MUSICBUTLER_2.to_owned());
|
||||
assert_eq!(
|
||||
music_hoard.collection[0].properties.get("MusicButler"),
|
||||
Some(&expected)
|
||||
);
|
||||
|
||||
// Clearing URLs on an artist that does not exist is an error.
|
||||
assert!(music_hoard
|
||||
.clear_artist_property(&artist_id_2, "MusicButler")
|
||||
.is_err());
|
||||
|
||||
// Clear URLs.
|
||||
assert!(music_hoard
|
||||
.clear_artist_property(&artist_id, "MusicButler")
|
||||
.is_ok());
|
||||
expected.clear();
|
||||
assert!(music_hoard.collection[0].properties.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_clear_album_seq() {
|
||||
let mut database = MockIDatabase::new();
|
||||
|
||||
let artist_id = ArtistId::new("an artist");
|
||||
let album_id = AlbumId::new("an album");
|
||||
let album_id_2 = AlbumId::new("another album");
|
||||
|
||||
let mut database_result = vec![Artist::new(artist_id.clone())];
|
||||
database_result[0]
|
||||
.albums
|
||||
.push(Album::new(album_id.clone(), AlbumDate::default()));
|
||||
|
||||
database
|
||||
.expect_load()
|
||||
.times(1)
|
||||
.return_once(|| Ok(database_result));
|
||||
database.expect_save().times(2).returning(|_| Ok(()));
|
||||
|
||||
let mut music_hoard = MusicHoard::database(database).unwrap();
|
||||
assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(0));
|
||||
|
||||
// Seting seq on an album not belonging to the artist is an error.
|
||||
assert!(music_hoard
|
||||
.set_album_seq(&artist_id, &album_id_2, 6)
|
||||
.is_err());
|
||||
assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(0));
|
||||
|
||||
// Set seq.
|
||||
assert!(music_hoard.set_album_seq(&artist_id, &album_id, 6).is_ok());
|
||||
assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(6));
|
||||
|
||||
// Clearing seq on an album that does not exist is an error.
|
||||
assert!(music_hoard
|
||||
.clear_album_seq(&artist_id, &album_id_2)
|
||||
.is_err());
|
||||
|
||||
// Clear seq.
|
||||
assert!(music_hoard.clear_album_seq(&artist_id, &album_id).is_ok());
|
||||
assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_database() {
|
||||
let mut database = MockIDatabase::new();
|
||||
|
||||
database
|
||||
.expect_load()
|
||||
.times(1)
|
||||
.return_once(|| Ok(FULL_COLLECTION.to_owned()));
|
||||
|
||||
let music_hoard = MusicHoard::database(database).unwrap();
|
||||
|
||||
assert_eq!(music_hoard.get_collection(), &*FULL_COLLECTION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn database_load_error() {
|
||||
let mut database = MockIDatabase::new();
|
||||
|
||||
let database_result = Err(database::LoadError::IoError(String::from("I/O error")));
|
||||
|
||||
database
|
||||
.expect_load()
|
||||
.times(1)
|
||||
.return_once(|| database_result);
|
||||
|
||||
let actual_err = MusicHoard::database(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 mut database = MockIDatabase::new();
|
||||
|
||||
let database_result = Err(database::SaveError::IoError(String::from("I/O error")));
|
||||
|
||||
database.expect_load().return_once(|| Ok(vec![]));
|
||||
database
|
||||
.expect_save()
|
||||
.times(1)
|
||||
.return_once(|_: &Collection| database_result);
|
||||
|
||||
let mut music_hoard = MusicHoard::database(database).unwrap();
|
||||
|
||||
let actual_err = music_hoard
|
||||
.add_artist(ArtistId::new("an artist"))
|
||||
.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());
|
||||
}
|
||||
}
|
317
src/core/musichoard/library.rs
Normal file
317
src/core/musichoard/library.rs
Normal file
@ -0,0 +1,317 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::core::{
|
||||
collection::{
|
||||
album::{Album, AlbumDate, AlbumId},
|
||||
artist::{Artist, ArtistId},
|
||||
track::{Track, TrackId, TrackNum, TrackQuality},
|
||||
Collection,
|
||||
},
|
||||
interface::{
|
||||
database::IDatabase,
|
||||
library::{ILibrary, Item, Query},
|
||||
},
|
||||
musichoard::{
|
||||
base::IMusicHoardBasePrivate, database::IMusicHoardDatabasePrivate, Error, MusicHoard,
|
||||
NoDatabase,
|
||||
},
|
||||
};
|
||||
|
||||
pub trait IMusicHoardLibrary {
|
||||
fn rescan_library(&mut self) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
impl<Library: ILibrary> IMusicHoardLibrary for MusicHoard<NoDatabase, Library> {
|
||||
fn rescan_library(&mut self) -> Result<(), Error> {
|
||||
self.pre_commit = self.rescan_library_inner()?;
|
||||
self.commit()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Database: IDatabase, Library: ILibrary> IMusicHoardLibrary for MusicHoard<Database, Library> {
|
||||
fn rescan_library(&mut self) -> Result<(), Error> {
|
||||
self.pre_commit = self.rescan_library_inner()?;
|
||||
self.commit()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Database, Library: ILibrary> MusicHoard<Database, Library> {
|
||||
fn rescan_library_inner(&mut self) -> Result<Collection, Error> {
|
||||
let items = self.library.list(&Query::new())?;
|
||||
self.library_cache = Self::items_to_artists(items)?;
|
||||
Self::sort_albums_and_tracks(self.library_cache.values_mut());
|
||||
|
||||
Ok(self.merge_collections())
|
||||
}
|
||||
|
||||
fn items_to_artists(items: Vec<Item>) -> Result<HashMap<ArtistId, Artist>, Error> {
|
||||
let mut collection = HashMap::<ArtistId, Artist>::new();
|
||||
|
||||
for item in items.into_iter() {
|
||||
let artist_id = ArtistId {
|
||||
name: item.album_artist,
|
||||
};
|
||||
|
||||
let artist_sort = item.album_artist_sort.map(|s| ArtistId { name: s });
|
||||
|
||||
let album_id = AlbumId {
|
||||
title: item.album_title,
|
||||
};
|
||||
|
||||
let album_date = AlbumDate {
|
||||
year: item.album_year,
|
||||
month: item.album_month,
|
||||
day: item.album_day,
|
||||
};
|
||||
|
||||
let track = Track {
|
||||
id: TrackId {
|
||||
title: item.track_title,
|
||||
},
|
||||
number: TrackNum(item.track_number),
|
||||
artist: item.track_artist,
|
||||
quality: TrackQuality {
|
||||
format: item.track_format,
|
||||
bitrate: item.track_bitrate,
|
||||
},
|
||||
};
|
||||
|
||||
// There are usually many entries per artist. Therefore, we avoid simply calling
|
||||
// .entry(artist_id.clone()).or_insert_with(..), because of the clone. The flipside is
|
||||
// that insertions will thus do an additional lookup.
|
||||
let artist = match collection.get_mut(&artist_id) {
|
||||
Some(artist) => artist,
|
||||
None => collection
|
||||
.entry(artist_id.clone())
|
||||
.or_insert_with(|| Artist::new(artist_id)),
|
||||
};
|
||||
|
||||
if artist.sort.is_some() {
|
||||
if artist_sort.is_some() && (artist.sort != artist_sort) {
|
||||
return Err(Error::CollectionError(format!(
|
||||
"multiple album_artist_sort found for artist '{}': '{}' != '{}'",
|
||||
artist.id,
|
||||
artist.sort.as_ref().unwrap(),
|
||||
artist_sort.as_ref().unwrap()
|
||||
)));
|
||||
}
|
||||
} else if artist_sort.is_some() {
|
||||
artist.sort = artist_sort;
|
||||
}
|
||||
|
||||
// Do a linear search as few artists have more than a handful of albums. Search from the
|
||||
// back as the original items vector is usually already sorted.
|
||||
match artist
|
||||
.albums
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.find(|album| album.id == album_id)
|
||||
{
|
||||
Some(album) => album.tracks.push(track),
|
||||
None => {
|
||||
let mut album = Album::new(album_id, album_date);
|
||||
album.tracks.push(track);
|
||||
artist.albums.push(album);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(collection)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mockall::{predicate, Sequence};
|
||||
|
||||
use crate::core::{
|
||||
interface::{
|
||||
database::MockIDatabase,
|
||||
library::{self, testmod::LIBRARY_ITEMS, MockILibrary},
|
||||
},
|
||||
musichoard::base::IMusicHoardBase,
|
||||
testmod::LIBRARY_COLLECTION,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rescan_library_ordered() {
|
||||
let mut library = MockILibrary::new();
|
||||
let mut database = MockIDatabase::new();
|
||||
|
||||
let library_input = Query::new();
|
||||
let library_result = Ok(LIBRARY_ITEMS.to_owned());
|
||||
|
||||
library
|
||||
.expect_list()
|
||||
.with(predicate::eq(library_input))
|
||||
.times(1)
|
||||
.return_once(|_| library_result);
|
||||
|
||||
database.expect_load().times(1).returning(|| Ok(vec![]));
|
||||
database
|
||||
.expect_save()
|
||||
.with(predicate::eq(&*LIBRARY_COLLECTION))
|
||||
.times(1)
|
||||
.return_once(|_| Ok(()));
|
||||
|
||||
let mut music_hoard = MusicHoard::new(database, library).unwrap();
|
||||
|
||||
music_hoard.rescan_library().unwrap();
|
||||
assert_eq!(music_hoard.get_collection(), &*LIBRARY_COLLECTION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rescan_library_changed() {
|
||||
let mut library = MockILibrary::new();
|
||||
let mut seq = Sequence::new();
|
||||
|
||||
let library_input = Query::new();
|
||||
let library_result = Ok(LIBRARY_ITEMS.to_owned());
|
||||
|
||||
library
|
||||
.expect_list()
|
||||
.with(predicate::eq(library_input))
|
||||
.times(1)
|
||||
.in_sequence(&mut seq)
|
||||
.return_once(|_| library_result);
|
||||
|
||||
let library_input = Query::new();
|
||||
let library_result = Ok(LIBRARY_ITEMS
|
||||
.iter()
|
||||
.filter(|item| item.album_title != "album_title a.a")
|
||||
.cloned()
|
||||
.collect());
|
||||
|
||||
library
|
||||
.expect_list()
|
||||
.with(predicate::eq(library_input))
|
||||
.times(1)
|
||||
.in_sequence(&mut seq)
|
||||
.return_once(|_| library_result);
|
||||
|
||||
let mut music_hoard = MusicHoard::library(library);
|
||||
|
||||
music_hoard.rescan_library().unwrap();
|
||||
assert!(music_hoard.get_collection()[0]
|
||||
.albums
|
||||
.iter()
|
||||
.any(|album| album.id.title == "album_title a.a"));
|
||||
|
||||
music_hoard.rescan_library().unwrap();
|
||||
assert!(!music_hoard.get_collection()[0]
|
||||
.albums
|
||||
.iter()
|
||||
.any(|album| album.id.title == "album_title a.a"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rescan_library_unordered() {
|
||||
let mut library = MockILibrary::new();
|
||||
|
||||
let library_input = Query::new();
|
||||
let mut library_result = Ok(LIBRARY_ITEMS.to_owned());
|
||||
|
||||
// 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::library(library);
|
||||
|
||||
music_hoard.rescan_library().unwrap();
|
||||
assert_eq!(music_hoard.get_collection(), &*LIBRARY_COLLECTION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rescan_library_album_id_clash() {
|
||||
let mut library = MockILibrary::new();
|
||||
|
||||
let mut expected = LIBRARY_COLLECTION.to_owned();
|
||||
let removed_album_id = expected[0].albums[0].id.clone();
|
||||
let clashed_album_id = &expected[1].albums[0].id;
|
||||
|
||||
let mut items = LIBRARY_ITEMS.to_owned();
|
||||
for item in items
|
||||
.iter_mut()
|
||||
.filter(|it| it.album_title == removed_album_id.title)
|
||||
{
|
||||
item.album_title = clashed_album_id.title.clone();
|
||||
}
|
||||
|
||||
expected[0].albums[0].id = clashed_album_id.clone();
|
||||
|
||||
let library_input = Query::new();
|
||||
let library_result = Ok(items);
|
||||
|
||||
library
|
||||
.expect_list()
|
||||
.with(predicate::eq(library_input))
|
||||
.times(1)
|
||||
.return_once(|_| library_result);
|
||||
|
||||
let mut music_hoard = MusicHoard::library(library);
|
||||
|
||||
music_hoard.rescan_library().unwrap();
|
||||
assert_eq!(music_hoard.get_collection()[0], expected[0]);
|
||||
assert_eq!(music_hoard.get_collection(), &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rescan_library_album_artist_sort_clash() {
|
||||
let mut library = MockILibrary::new();
|
||||
|
||||
let library_input = Query::new();
|
||||
let mut library_items = LIBRARY_ITEMS.to_owned();
|
||||
|
||||
assert_eq!(library_items[0].album_artist, library_items[1].album_artist);
|
||||
library_items[0].album_artist_sort = Some(library_items[0].album_artist.clone());
|
||||
library_items[1].album_artist_sort = Some(
|
||||
library_items[1]
|
||||
.album_artist
|
||||
.clone()
|
||||
.chars()
|
||||
.rev()
|
||||
.collect(),
|
||||
);
|
||||
|
||||
let library_result = Ok(library_items);
|
||||
|
||||
library
|
||||
.expect_list()
|
||||
.with(predicate::eq(library_input))
|
||||
.times(1)
|
||||
.return_once(|_| library_result);
|
||||
|
||||
let mut music_hoard = MusicHoard::library(library);
|
||||
|
||||
assert!(music_hoard.rescan_library().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn library_error() {
|
||||
let mut library = MockILibrary::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::library(library);
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
@ -1,16 +1,61 @@
|
||||
//! The core MusicHoard module. Serves as the main entry-point into the library.
|
||||
|
||||
#![allow(clippy::module_inception)]
|
||||
pub mod musichoard;
|
||||
pub mod musichoard_builder;
|
||||
mod base;
|
||||
mod database;
|
||||
mod library;
|
||||
|
||||
use std::fmt::{self, Display};
|
||||
pub mod builder;
|
||||
|
||||
pub use base::IMusicHoardBase;
|
||||
pub use database::IMusicHoardDatabase;
|
||||
pub use library::IMusicHoardLibrary;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{self, Display},
|
||||
};
|
||||
|
||||
use crate::core::collection::{
|
||||
artist::{Artist, ArtistId},
|
||||
Collection,
|
||||
};
|
||||
|
||||
use crate::core::{
|
||||
collection,
|
||||
interface::{database, library},
|
||||
interface::{
|
||||
database::{LoadError as DatabaseLoadError, SaveError as DatabaseSaveError},
|
||||
library::Error as LibraryError,
|
||||
},
|
||||
};
|
||||
|
||||
/// The Music Hoard. It is responsible for pulling information from both the library and the
|
||||
/// database, ensuring its consistent and writing back any changes.
|
||||
// TODO: Split into inner and external/interfaces to facilitate building.
|
||||
#[derive(Debug)]
|
||||
pub struct MusicHoard<Database, Library> {
|
||||
collection: Collection,
|
||||
pre_commit: Collection,
|
||||
database: Database,
|
||||
database_cache: Collection,
|
||||
library: Library,
|
||||
library_cache: HashMap<ArtistId, Artist>,
|
||||
}
|
||||
|
||||
/// Phantom type for when a library implementation is not needed.
|
||||
#[derive(Debug)]
|
||||
pub struct NoLibrary;
|
||||
|
||||
/// Phantom type for when a database implementation is not needed.
|
||||
#[derive(Debug)]
|
||||
pub struct NoDatabase;
|
||||
|
||||
impl Default for MusicHoard<NoDatabase, NoLibrary> {
|
||||
/// Create a new [`MusicHoard`] without any library or database.
|
||||
fn default() -> Self {
|
||||
MusicHoard::empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for `musichoard`.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
@ -40,20 +85,20 @@ impl From<collection::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<library::Error> for Error {
|
||||
fn from(err: library::Error) -> Error {
|
||||
impl From<LibraryError> for Error {
|
||||
fn from(err: LibraryError) -> Error {
|
||||
Error::LibraryError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<database::LoadError> for Error {
|
||||
fn from(err: database::LoadError) -> Error {
|
||||
impl From<DatabaseLoadError> for Error {
|
||||
fn from(err: DatabaseLoadError) -> Error {
|
||||
Error::DatabaseError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<database::SaveError> for Error {
|
||||
fn from(err: database::SaveError) -> Error {
|
||||
impl From<DatabaseSaveError> for Error {
|
||||
fn from(err: DatabaseSaveError) -> Error {
|
||||
Error::DatabaseError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,117 +0,0 @@
|
||||
use crate::{
|
||||
core::{
|
||||
interface::{database::IDatabase, library::ILibrary},
|
||||
musichoard::musichoard::{MusicHoard, NoDatabase, NoLibrary},
|
||||
},
|
||||
Error,
|
||||
};
|
||||
|
||||
/// Builder for [`MusicHoard`]. Its purpose is to make it easier to set various combinations of
|
||||
/// library/database or their absence.
|
||||
pub struct MusicHoardBuilder<LIB, DB> {
|
||||
library: LIB,
|
||||
database: DB,
|
||||
}
|
||||
|
||||
impl Default for MusicHoardBuilder<NoLibrary, NoDatabase> {
|
||||
/// Create a [`MusicHoardBuilder`].
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MusicHoardBuilder<NoLibrary, NoDatabase> {
|
||||
/// Create a [`MusicHoardBuilder`].
|
||||
pub fn new() -> Self {
|
||||
MusicHoardBuilder {
|
||||
library: NoLibrary,
|
||||
database: NoDatabase,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<LIB, DB> MusicHoardBuilder<LIB, DB> {
|
||||
/// Set a library for [`MusicHoard`].
|
||||
pub fn set_library<NEWLIB: ILibrary>(self, library: NEWLIB) -> MusicHoardBuilder<NEWLIB, DB> {
|
||||
MusicHoardBuilder {
|
||||
library,
|
||||
database: self.database,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a database for [`MusicHoard`].
|
||||
pub fn set_database<NEWDB: IDatabase>(self, database: NEWDB) -> MusicHoardBuilder<LIB, NEWDB> {
|
||||
MusicHoardBuilder {
|
||||
library: self.library,
|
||||
database,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MusicHoardBuilder<NoLibrary, NoDatabase> {
|
||||
/// Build [`MusicHoard`] with the currently set library and database.
|
||||
pub fn build(self) -> MusicHoard<NoLibrary, NoDatabase> {
|
||||
MusicHoard::empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<LIB: ILibrary> MusicHoardBuilder<LIB, NoDatabase> {
|
||||
/// Build [`MusicHoard`] with the currently set library and database.
|
||||
pub fn build(self) -> MusicHoard<LIB, NoDatabase> {
|
||||
MusicHoard::library(self.library)
|
||||
}
|
||||
}
|
||||
|
||||
impl<DB: IDatabase> MusicHoardBuilder<NoLibrary, DB> {
|
||||
/// Build [`MusicHoard`] with the currently set library and database.
|
||||
pub fn build(self) -> Result<MusicHoard<NoLibrary, DB>, Error> {
|
||||
MusicHoard::database(self.database)
|
||||
}
|
||||
}
|
||||
|
||||
impl<LIB: ILibrary, DB: IDatabase> MusicHoardBuilder<LIB, DB> {
|
||||
/// Build [`MusicHoard`] with the currently set library and database.
|
||||
pub fn build(self) -> Result<MusicHoard<LIB, DB>, Error> {
|
||||
MusicHoard::new(self.library, self.database)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::core::interface::{database::NullDatabase, library::NullLibrary};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn no_library_no_database() {
|
||||
MusicHoardBuilder::default().build();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_library_no_database() {
|
||||
let mut mh = MusicHoardBuilder::default()
|
||||
.set_library(NullLibrary)
|
||||
.build();
|
||||
assert!(mh.rescan_library().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_library_with_database() {
|
||||
let mut mh = MusicHoardBuilder::default()
|
||||
.set_database(NullDatabase)
|
||||
.build()
|
||||
.unwrap();
|
||||
assert!(mh.reload_database().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_library_with_database() {
|
||||
let mut mh = MusicHoardBuilder::default()
|
||||
.set_library(NullLibrary)
|
||||
.set_database(NullDatabase)
|
||||
.build()
|
||||
.unwrap();
|
||||
assert!(mh.rescan_library().is_ok());
|
||||
assert!(mh.reload_database().is_ok());
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ use std::collections::HashMap;
|
||||
use crate::core::collection::{
|
||||
album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq},
|
||||
artist::{Artist, ArtistId},
|
||||
musicbrainz::MusicBrainz,
|
||||
musicbrainz::MusicBrainzUrl,
|
||||
track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality},
|
||||
};
|
||||
use crate::tests::*;
|
||||
|
@ -6,7 +6,7 @@ use crate::core::{
|
||||
collection::{
|
||||
album::{Album, AlbumDate, AlbumId, AlbumSeq},
|
||||
artist::{Artist, ArtistId},
|
||||
musicbrainz::MusicBrainz,
|
||||
musicbrainz::MusicBrainzUrl,
|
||||
Collection,
|
||||
},
|
||||
interface::database::LoadError,
|
||||
@ -55,7 +55,7 @@ impl TryFrom<DeserializeArtist> for Artist {
|
||||
sort: artist.sort.map(ArtistId::new),
|
||||
musicbrainz: artist
|
||||
.musicbrainz
|
||||
.map(MusicBrainz::artist_from_str)
|
||||
.map(MusicBrainzUrl::artist_from_str)
|
||||
.transpose()?,
|
||||
properties: artist.properties,
|
||||
albums: artist
|
||||
@ -77,7 +77,7 @@ impl TryFrom<DeserializeAlbum> for Album {
|
||||
seq: AlbumSeq(album.seq),
|
||||
musicbrainz: album
|
||||
.musicbrainz
|
||||
.map(MusicBrainz::album_from_str)
|
||||
.map(MusicBrainzUrl::album_from_str)
|
||||
.transpose()?,
|
||||
tracks: vec![],
|
||||
})
|
||||
|
@ -8,9 +8,8 @@ pub use core::collection;
|
||||
pub use core::interface;
|
||||
|
||||
pub use core::musichoard::{
|
||||
musichoard::{MusicHoard, NoDatabase, NoLibrary},
|
||||
musichoard_builder::MusicHoardBuilder,
|
||||
Error,
|
||||
builder::MusicHoardBuilder, Error, IMusicHoardBase, IMusicHoardDatabase, IMusicHoardLibrary,
|
||||
MusicHoard, NoDatabase, NoLibrary,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -58,7 +58,7 @@ struct DbOpt {
|
||||
no_database: bool,
|
||||
}
|
||||
|
||||
fn with<LIB: ILibrary, DB: IDatabase>(builder: MusicHoardBuilder<LIB, DB>) {
|
||||
fn with<Database: IDatabase, Library: ILibrary>(builder: MusicHoardBuilder<Database, Library>) {
|
||||
let music_hoard = builder.build().expect("failed to initialise MusicHoard");
|
||||
|
||||
// Initialize the terminal user interface.
|
||||
@ -76,7 +76,10 @@ fn with<LIB: ILibrary, DB: IDatabase>(builder: MusicHoardBuilder<LIB, DB>) {
|
||||
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui");
|
||||
}
|
||||
|
||||
fn with_database<LIB: ILibrary>(db_opt: DbOpt, builder: MusicHoardBuilder<LIB, NoDatabase>) {
|
||||
fn with_database<Library: ILibrary>(
|
||||
db_opt: DbOpt,
|
||||
builder: MusicHoardBuilder<NoDatabase, Library>,
|
||||
) {
|
||||
if db_opt.no_database {
|
||||
with(builder.set_database(NullDatabase));
|
||||
} else {
|
||||
@ -103,7 +106,7 @@ fn with_database<LIB: ILibrary>(db_opt: DbOpt, builder: MusicHoardBuilder<LIB, N
|
||||
};
|
||||
}
|
||||
|
||||
fn with_library(lib_opt: LibOpt, db_opt: DbOpt, builder: MusicHoardBuilder<NoLibrary, NoDatabase>) {
|
||||
fn with_library(lib_opt: LibOpt, db_opt: DbOpt, builder: MusicHoardBuilder<NoDatabase, NoLibrary>) {
|
||||
if lib_opt.no_library {
|
||||
with_database(db_opt, builder.set_library(NullLibrary));
|
||||
} else if let Some(uri) = lib_opt.beets_ssh_uri {
|
||||
|
12
src/tests.rs
12
src/tests.rs
@ -464,7 +464,7 @@ macro_rules! full_collection {
|
||||
let artist_a = iter.next().unwrap();
|
||||
assert_eq!(artist_a.id.name, "Album_Artist ‘A’");
|
||||
|
||||
artist_a.musicbrainz = Some(MusicBrainz::artist_from_str(
|
||||
artist_a.musicbrainz = Some(MusicBrainzUrl::artist_from_str(
|
||||
"https://musicbrainz.org/artist/00000000-0000-0000-0000-000000000000",
|
||||
).unwrap());
|
||||
|
||||
@ -482,14 +482,14 @@ macro_rules! full_collection {
|
||||
artist_a.albums[0].seq = AlbumSeq(1);
|
||||
artist_a.albums[1].seq = AlbumSeq(1);
|
||||
|
||||
artist_a.albums[0].musicbrainz = Some(MusicBrainz::album_from_str(
|
||||
artist_a.albums[0].musicbrainz = Some(MusicBrainzUrl::album_from_str(
|
||||
"https://musicbrainz.org/release-group/00000000-0000-0000-0000-000000000000"
|
||||
).unwrap());
|
||||
|
||||
let artist_b = iter.next().unwrap();
|
||||
assert_eq!(artist_b.id.name, "Album_Artist ‘B’");
|
||||
|
||||
artist_b.musicbrainz = Some(MusicBrainz::artist_from_str(
|
||||
artist_b.musicbrainz = Some(MusicBrainzUrl::artist_from_str(
|
||||
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111",
|
||||
).unwrap());
|
||||
|
||||
@ -511,18 +511,18 @@ macro_rules! full_collection {
|
||||
artist_b.albums[2].seq = AlbumSeq(2);
|
||||
artist_b.albums[3].seq = AlbumSeq(4);
|
||||
|
||||
artist_b.albums[1].musicbrainz = Some(MusicBrainz::album_from_str(
|
||||
artist_b.albums[1].musicbrainz = Some(MusicBrainzUrl::album_from_str(
|
||||
"https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111111"
|
||||
).unwrap());
|
||||
|
||||
artist_b.albums[2].musicbrainz = Some(MusicBrainz::album_from_str(
|
||||
artist_b.albums[2].musicbrainz = Some(MusicBrainzUrl::album_from_str(
|
||||
"https://musicbrainz.org/release-group/11111111-1111-1111-1111-111111111112"
|
||||
).unwrap());
|
||||
|
||||
let artist_c = iter.next().unwrap();
|
||||
assert_eq!(artist_c.id.name, "The Album_Artist ‘C’");
|
||||
|
||||
artist_c.musicbrainz = Some(MusicBrainz::artist_from_str(
|
||||
artist_c.musicbrainz = Some(MusicBrainzUrl::artist_from_str(
|
||||
"https://musicbrainz.org/artist/11111111-1111-1111-1111-111111111111",
|
||||
).unwrap());
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
use musichoard::{
|
||||
collection::Collection, interface::database::IDatabase, interface::library::ILibrary,
|
||||
MusicHoard,
|
||||
IMusicHoardBase, IMusicHoardDatabase, IMusicHoardLibrary, MusicHoard,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
@ -14,17 +14,17 @@ pub trait IMusicHoard {
|
||||
}
|
||||
|
||||
// GRCOV_EXCL_START
|
||||
impl<LIB: ILibrary, DB: IDatabase> IMusicHoard for MusicHoard<LIB, DB> {
|
||||
impl<Database: IDatabase, Library: ILibrary> IMusicHoard for MusicHoard<Database, Library> {
|
||||
fn rescan_library(&mut self) -> Result<(), musichoard::Error> {
|
||||
MusicHoard::<LIB, DB>::rescan_library(self)
|
||||
<Self as IMusicHoardLibrary>::rescan_library(self)
|
||||
}
|
||||
|
||||
fn reload_database(&mut self) -> Result<(), musichoard::Error> {
|
||||
MusicHoard::reload_database(self)
|
||||
<Self as IMusicHoardDatabase>::reload_database(self)
|
||||
}
|
||||
|
||||
fn get_collection(&self) -> &Collection {
|
||||
MusicHoard::get_collection(self)
|
||||
<Self as IMusicHoardBase>::get_collection(self)
|
||||
}
|
||||
}
|
||||
// GRCOV_EXCL_STOP
|
||||
|
@ -3,7 +3,7 @@ use std::collections::HashMap;
|
||||
use musichoard::collection::{
|
||||
album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq},
|
||||
artist::{Artist, ArtistId},
|
||||
musicbrainz::MusicBrainz,
|
||||
musicbrainz::MusicBrainzUrl,
|
||||
track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality},
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
|
@ -9,7 +9,7 @@ mod testlib;
|
||||
use musichoard::{
|
||||
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
||||
library::beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary},
|
||||
MusicHoard,
|
||||
IMusicHoardBase, IMusicHoardDatabase, IMusicHoardLibrary, MusicHoard,
|
||||
};
|
||||
|
||||
use crate::testlib::COLLECTION;
|
||||
@ -29,7 +29,7 @@ fn merge_library_then_database() {
|
||||
let backend = JsonDatabaseFileBackend::new(&*database::json::DATABASE_TEST_FILE);
|
||||
let database = JsonDatabase::new(backend);
|
||||
|
||||
let mut music_hoard = MusicHoard::new(library, database).unwrap();
|
||||
let mut music_hoard = MusicHoard::new(database, library).unwrap();
|
||||
|
||||
music_hoard.rescan_library().unwrap();
|
||||
music_hoard.reload_database().unwrap();
|
||||
@ -52,7 +52,7 @@ fn merge_database_then_library() {
|
||||
let backend = JsonDatabaseFileBackend::new(&*database::json::DATABASE_TEST_FILE);
|
||||
let database = JsonDatabase::new(backend);
|
||||
|
||||
let mut music_hoard = MusicHoard::new(library, database).unwrap();
|
||||
let mut music_hoard = MusicHoard::new(database, library).unwrap();
|
||||
|
||||
music_hoard.reload_database().unwrap();
|
||||
music_hoard.rescan_library().unwrap();
|
||||
|
@ -4,7 +4,7 @@ use std::collections::HashMap;
|
||||
use musichoard::collection::{
|
||||
album::{Album, AlbumDate, AlbumId, AlbumMonth, AlbumSeq},
|
||||
artist::{Artist, ArtistId},
|
||||
musicbrainz::MusicBrainz,
|
||||
musicbrainz::MusicBrainzUrl,
|
||||
track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality},
|
||||
Collection,
|
||||
};
|
||||
@ -18,7 +18,7 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
|
||||
sort: Some(ArtistId{
|
||||
name: String::from("Arkona")
|
||||
}),
|
||||
musicbrainz: Some(MusicBrainz::artist_from_str(
|
||||
musicbrainz: Some(MusicBrainzUrl::artist_from_str(
|
||||
"https://musicbrainz.org/artist/baad262d-55ef-427a-83c7-f7530964f212"
|
||||
).unwrap()),
|
||||
properties: HashMap::from([
|
||||
@ -206,7 +206,7 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
|
||||
name: String::from("Eluveitie"),
|
||||
},
|
||||
sort: None,
|
||||
musicbrainz: Some(MusicBrainz::artist_from_str(
|
||||
musicbrainz: Some(MusicBrainzUrl::artist_from_str(
|
||||
"https://musicbrainz.org/artist/8000598a-5edb-401c-8e6d-36b167feaf38",
|
||||
).unwrap()),
|
||||
properties: HashMap::from([
|
||||
@ -451,7 +451,7 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
|
||||
name: String::from("Frontside"),
|
||||
},
|
||||
sort: None,
|
||||
musicbrainz: Some(MusicBrainz::artist_from_str(
|
||||
musicbrainz: Some(MusicBrainzUrl::artist_from_str(
|
||||
"https://musicbrainz.org/artist/3a901353-fccd-4afd-ad01-9c03f451b490",
|
||||
).unwrap()),
|
||||
properties: HashMap::from([
|
||||
@ -605,7 +605,7 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
|
||||
sort: Some(ArtistId {
|
||||
name: String::from("Heaven’s Basement"),
|
||||
}),
|
||||
musicbrainz: Some(MusicBrainz::artist_from_str(
|
||||
musicbrainz: Some(MusicBrainzUrl::artist_from_str(
|
||||
"https://musicbrainz.org/artist/c2c4d56a-d599-4a18-bd2f-ae644e2198cc",
|
||||
).unwrap()),
|
||||
properties: HashMap::from([
|
||||
@ -737,7 +737,7 @@ pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
|
||||
name: String::from("Metallica"),
|
||||
},
|
||||
sort: None,
|
||||
musicbrainz: Some(MusicBrainz::artist_from_str(
|
||||
musicbrainz: Some(MusicBrainzUrl::artist_from_str(
|
||||
"https://musicbrainz.org/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab",
|
||||
).unwrap()),
|
||||
properties: HashMap::from([
|
||||
|
Loading…
Reference in New Issue
Block a user