Split musichoard file
This commit is contained in:
parent
3ade35e3fa
commit
355446a28b
202
src/core/musichoard/base.rs
Normal file
202
src/core/musichoard/base.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -155,6 +155,12 @@ mod tests {
|
|||||||
MusicHoardBuilder::default().build();
|
MusicHoardBuilder::default().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty() {
|
||||||
|
let music_hoard = MusicHoard::empty();
|
||||||
|
assert!(!format!("{music_hoard:?}").is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn with_library_no_database() {
|
fn with_library_no_database() {
|
||||||
let mut mh = MusicHoardBuilder::default()
|
let mut mh = MusicHoardBuilder::default()
|
||||||
|
@ -1,175 +1,16 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use crate::core::{
|
use crate::core::{
|
||||||
collection::{
|
collection::{
|
||||||
album::{Album, AlbumDate, AlbumId, AlbumSeq},
|
album::{Album, AlbumId, AlbumSeq},
|
||||||
artist::{Artist, ArtistId},
|
artist::{Artist, ArtistId},
|
||||||
musicbrainz::MusicBrainzUrl,
|
musicbrainz::MusicBrainzUrl,
|
||||||
track::{Track, TrackId, TrackNum, TrackQuality},
|
Collection,
|
||||||
Collection, MergeCollections,
|
|
||||||
},
|
|
||||||
interface::{
|
|
||||||
database::IDatabase,
|
|
||||||
library::{ILibrary, Item, Query},
|
|
||||||
},
|
},
|
||||||
|
interface::database::IDatabase,
|
||||||
musichoard::{Error, MusicHoard, NoDatabase},
|
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> {
|
impl<Library> MusicHoard<NoDatabase, Library> {
|
||||||
fn commit(&mut self) -> Result<(), Error> {
|
pub fn commit(&mut self) -> Result<(), Error> {
|
||||||
self.collection = self.pre_commit.clone();
|
self.collection = self.pre_commit.clone();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -187,7 +28,7 @@ impl<Database: IDatabase, Library> MusicHoard<Database, Library> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit(&mut self) -> Result<(), Error> {
|
pub fn commit(&mut self) -> Result<(), Error> {
|
||||||
if self.collection != self.pre_commit {
|
if self.collection != self.pre_commit {
|
||||||
if let Err(err) = self.database.save(&self.pre_commit) {
|
if let Err(err) = self.database.save(&self.pre_commit) {
|
||||||
self.pre_commit = self.collection.clone();
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use mockall::{predicate, Sequence};
|
use mockall::{predicate, Sequence};
|
||||||
|
|
||||||
use crate::core::{
|
use crate::core::{
|
||||||
collection::{artist::ArtistId, musicbrainz::MusicBrainzUrl},
|
collection::{album::AlbumDate, artist::ArtistId, musicbrainz::MusicBrainzUrl},
|
||||||
interface::{
|
interface::database::{self, MockIDatabase},
|
||||||
database::{self, MockIDatabase},
|
|
||||||
library::{self, testmod::LIBRARY_ITEMS, MockILibrary},
|
|
||||||
},
|
|
||||||
musichoard::NoLibrary,
|
musichoard::NoLibrary,
|
||||||
testmod::{FULL_COLLECTION, LIBRARY_COLLECTION},
|
testmod::FULL_COLLECTION,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -705,295 +535,8 @@ mod tests {
|
|||||||
assert_eq!(music_hoard.collection[0].albums[0].seq, AlbumSeq(0));
|
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]
|
#[test]
|
||||||
fn load_database() {
|
fn load_database() {
|
||||||
let library = MockILibrary::new();
|
|
||||||
let mut database = MockIDatabase::new();
|
let mut database = MockIDatabase::new();
|
||||||
|
|
||||||
database
|
database
|
||||||
@ -1001,32 +544,11 @@ mod tests {
|
|||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|| Ok(FULL_COLLECTION.to_owned()));
|
.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);
|
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]
|
#[test]
|
||||||
fn database_load_error() {
|
fn database_load_error() {
|
||||||
let mut database = MockIDatabase::new();
|
let mut database = MockIDatabase::new();
|
||||||
@ -1071,10 +593,4 @@ mod tests {
|
|||||||
assert_eq!(actual_err, expected_err);
|
assert_eq!(actual_err, expected_err);
|
||||||
assert_eq!(actual_err.to_string(), expected_err.to_string());
|
assert_eq!(actual_err.to_string(), expected_err.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty() {
|
|
||||||
let music_hoard = MusicHoard::empty();
|
|
||||||
assert!(!format!("{music_hoard:?}").is_empty());
|
|
||||||
}
|
|
||||||
}
|
}
|
311
src/core/musichoard/library.rs
Normal file
311
src/core/musichoard/library.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,10 @@
|
|||||||
//! The core MusicHoard module. Serves as the main entry-point into the library.
|
//! 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 builder;
|
||||||
pub mod musichoard;
|
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
@ -16,7 +18,10 @@ use crate::core::collection::{
|
|||||||
|
|
||||||
use crate::core::{
|
use crate::core::{
|
||||||
collection,
|
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
|
/// 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 {
|
impl From<LibraryError> for Error {
|
||||||
fn from(err: library::Error) -> Error {
|
fn from(err: LibraryError) -> Error {
|
||||||
Error::LibraryError(err.to_string())
|
Error::LibraryError(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<database::LoadError> for Error {
|
impl From<DatabaseLoadError> for Error {
|
||||||
fn from(err: database::LoadError) -> Error {
|
fn from(err: DatabaseLoadError) -> Error {
|
||||||
Error::DatabaseError(err.to_string())
|
Error::DatabaseError(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<database::SaveError> for Error {
|
impl From<DatabaseSaveError> for Error {
|
||||||
fn from(err: database::SaveError) -> Error {
|
fn from(err: DatabaseSaveError) -> Error {
|
||||||
Error::DatabaseError(err.to_string())
|
Error::DatabaseError(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user