Split musichoard file

This commit is contained in:
Wojciech Kozlowski 2024-03-09 21:41:14 +01:00
parent 3ade35e3fa
commit 355446a28b
5 changed files with 542 additions and 502 deletions

202
src/core/musichoard/base.rs Normal file
View File

@ -0,0 +1,202 @@
use crate::core::{
collection::{
album::{Album, AlbumId},
artist::{Artist, ArtistId},
Collection, MergeCollections,
},
musichoard::{Error, MusicHoard},
};
impl<Database, Library> MusicHoard<Database, Library> {
/// Retrieve the [`Collection`].
pub fn get_collection(&self) -> &Collection {
&self.collection
}
pub fn sort_artists(collection: &mut [Artist]) {
collection.sort_unstable();
}
pub 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();
}
}
}
pub fn merge_collections(&self) -> Collection {
MergeCollections::merge(self.library_cache.clone(), self.database_cache.clone())
}
pub fn get_artist<'a>(collection: &'a Collection, artist_id: &ArtistId) -> Option<&'a Artist> {
collection.iter().find(|a| &a.id == artist_id)
}
pub 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)
}
pub 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))
})
}
pub 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)
}
pub 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);
}
}

View File

@ -155,6 +155,12 @@ mod tests {
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()

View File

@ -1,175 +1,16 @@
use std::collections::HashMap;
use crate::core::{
collection::{
album::{Album, AlbumDate, AlbumId, AlbumSeq},
album::{Album, AlbumId, AlbumSeq},
artist::{Artist, ArtistId},
musicbrainz::MusicBrainzUrl,
track::{Track, TrackId, TrackNum, TrackQuality},
Collection, MergeCollections,
},
interface::{
database::IDatabase,
library::{ILibrary, Item, Query},
Collection,
},
interface::database::IDatabase,
musichoard::{Error, MusicHoard, NoDatabase},
};
impl<Database, Library> MusicHoard<Database, Library> {
/// Retrieve the [`Collection`].
pub fn get_collection(&self) -> &Collection {
&self.collection
}
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 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)
}
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
))
})
}
}
impl<Library: ILibrary> MusicHoard<NoDatabase, Library> {
/// Rescan the library and merge with the in-memory collection.
pub 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())
}
}
impl<Library> MusicHoard<NoDatabase, Library> {
fn commit(&mut self) -> Result<(), Error> {
pub fn commit(&mut self) -> Result<(), Error> {
self.collection = self.pre_commit.clone();
Ok(())
}
@ -187,7 +28,7 @@ impl<Database: IDatabase, Library> MusicHoard<Database, Library> {
Ok(())
}
fn commit(&mut self) -> Result<(), Error> {
pub 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();
@ -379,26 +220,15 @@ impl<Database: IDatabase, Library> MusicHoard<Database, Library> {
}
}
impl<Database: IDatabase, Library: ILibrary> MusicHoard<Database, Library> {
/// Rescan the library and merge with the in-memory collection.
pub fn rescan_library(&mut self) -> Result<(), Error> {
self.pre_commit = self.rescan_library_inner()?;
self.commit()
}
}
#[cfg(test)]
mod tests {
use mockall::{predicate, Sequence};
use crate::core::{
collection::{artist::ArtistId, musicbrainz::MusicBrainzUrl},
interface::{
database::{self, MockIDatabase},
library::{self, testmod::LIBRARY_ITEMS, MockILibrary},
},
collection::{album::AlbumDate, artist::ArtistId, musicbrainz::MusicBrainzUrl},
interface::database::{self, MockIDatabase},
musichoard::NoLibrary,
testmod::{FULL_COLLECTION, LIBRARY_COLLECTION},
testmod::FULL_COLLECTION,
};
use super::*;
@ -705,295 +535,8 @@ mod tests {
assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(0));
}
#[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);
}
#[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 load_database() {
let library = MockILibrary::new();
let mut database = MockIDatabase::new();
database
@ -1001,32 +544,11 @@ mod tests {
.times(1)
.return_once(|| Ok(FULL_COLLECTION.to_owned()));
let music_hoard = MusicHoard::new(database, library).unwrap();
let music_hoard = MusicHoard::database(database).unwrap();
assert_eq!(music_hoard.get_collection(), &*FULL_COLLECTION);
}
#[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());
}
#[test]
fn database_load_error() {
let mut database = MockIDatabase::new();
@ -1071,10 +593,4 @@ mod tests {
assert_eq!(actual_err, expected_err);
assert_eq!(actual_err.to_string(), expected_err.to_string());
}
#[test]
fn empty() {
let music_hoard = MusicHoard::empty();
assert!(!format!("{music_hoard:?}").is_empty());
}
}

View File

@ -0,0 +1,311 @@
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::{Error, MusicHoard, NoDatabase},
};
impl<Library: ILibrary> MusicHoard<NoDatabase, Library> {
/// Rescan the library and merge with the in-memory collection.
pub fn rescan_library(&mut self) -> Result<(), Error> {
self.pre_commit = self.rescan_library_inner()?;
self.commit()
}
}
impl<Database: IDatabase, Library: ILibrary> MusicHoard<Database, Library> {
/// Rescan the library and merge with the in-memory collection.
pub 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},
},
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());
}
}

View File

@ -1,8 +1,10 @@
//! The core MusicHoard module. Serves as the main entry-point into the library.
#![allow(clippy::module_inception)]
mod base;
mod database;
mod library;
pub mod builder;
pub mod musichoard;
use std::{
collections::HashMap,
@ -16,7 +18,10 @@ use crate::core::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
@ -76,20 +81,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())
}
}