Add a filtering tool to only show only certain release group types #252
@ -137,7 +137,27 @@ pub enum AlbumSecondaryType {
|
|||||||
FieldRecording,
|
FieldRecording,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AlbumSecondaryType {
|
||||||
|
pub fn all_variants() -> [AlbumSecondaryType; 12] {
|
||||||
|
[
|
||||||
|
AlbumSecondaryType::Compilation,
|
||||||
|
AlbumSecondaryType::Soundtrack,
|
||||||
|
AlbumSecondaryType::Spokenword,
|
||||||
|
AlbumSecondaryType::Interview,
|
||||||
|
AlbumSecondaryType::Audiobook,
|
||||||
|
AlbumSecondaryType::AudioDrama,
|
||||||
|
AlbumSecondaryType::Live,
|
||||||
|
AlbumSecondaryType::Remix,
|
||||||
|
AlbumSecondaryType::DjMix,
|
||||||
|
AlbumSecondaryType::MixtapeStreet,
|
||||||
|
AlbumSecondaryType::Demo,
|
||||||
|
AlbumSecondaryType::FieldRecording,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The album's ownership status.
|
/// The album's ownership status.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum AlbumOwnership {
|
pub enum AlbumOwnership {
|
||||||
None,
|
None,
|
||||||
Owned(TrackFormat),
|
Owned(TrackFormat),
|
||||||
@ -453,4 +473,12 @@ mod tests {
|
|||||||
let expected = meta.clone().with_date(right.date);
|
let expected = meta.clone().with_date(right.date);
|
||||||
assert_eq!(expected, left.merge(right));
|
assert_eq!(expected, left.merge(right));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn secondary_types_all_variants() {
|
||||||
|
let mut variants = AlbumSecondaryType::all_variants().to_vec();
|
||||||
|
variants.sort_unstable();
|
||||||
|
variants.dedup();
|
||||||
|
assert_eq!(variants, AlbumSecondaryType::all_variants().to_vec());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,14 +5,25 @@ use crate::core::{
|
|||||||
merge::{MergeCollections, NormalMap},
|
merge::{MergeCollections, NormalMap},
|
||||||
string, Collection,
|
string, Collection,
|
||||||
},
|
},
|
||||||
musichoard::{Error, MusicHoard},
|
musichoard::{filter::CollectionFilter, Error, MusicHoard},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait IMusicHoardBase {
|
pub trait IMusicHoardBase {
|
||||||
|
fn set_filter(&mut self, filter: CollectionFilter);
|
||||||
|
fn get_filtered(&self) -> &Collection;
|
||||||
fn get_collection(&self) -> &Collection;
|
fn get_collection(&self) -> &Collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Database, Library> IMusicHoardBase for MusicHoard<Database, Library> {
|
impl<Database, Library> IMusicHoardBase for MusicHoard<Database, Library> {
|
||||||
|
fn set_filter(&mut self, filter: CollectionFilter) {
|
||||||
|
self.filter = filter;
|
||||||
|
self.filtered = self.filter_collection();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_filtered(&self) -> &Collection {
|
||||||
|
&self.filtered
|
||||||
|
}
|
||||||
|
|
||||||
fn get_collection(&self) -> &Collection {
|
fn get_collection(&self) -> &Collection {
|
||||||
&self.collection
|
&self.collection
|
||||||
}
|
}
|
||||||
@ -23,6 +34,8 @@ pub trait IMusicHoardBasePrivate {
|
|||||||
fn sort_albums_and_tracks<'a, C: Iterator<Item = &'a mut Artist>>(collection: C);
|
fn sort_albums_and_tracks<'a, C: Iterator<Item = &'a mut Artist>>(collection: C);
|
||||||
|
|
||||||
fn merge_collections(&self) -> Collection;
|
fn merge_collections(&self) -> Collection;
|
||||||
|
fn filter_collection(&self) -> Collection;
|
||||||
|
fn filter_artist(&self, artist: &Artist) -> Option<Artist>;
|
||||||
|
|
||||||
fn get_artist<'a>(collection: &'a Collection, artist_id: &ArtistId) -> Option<&'a Artist>;
|
fn get_artist<'a>(collection: &'a Collection, artist_id: &ArtistId) -> Option<&'a Artist>;
|
||||||
fn get_artist_mut<'a>(
|
fn get_artist_mut<'a>(
|
||||||
@ -74,6 +87,24 @@ impl<Database, Library> IMusicHoardBasePrivate for MusicHoard<Database, Library>
|
|||||||
collection
|
collection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn filter_collection(&self) -> Collection {
|
||||||
|
let iter = self.collection.iter();
|
||||||
|
iter.flat_map(|a| self.filter_artist(a)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter_artist(&self, artist: &Artist) -> Option<Artist> {
|
||||||
|
let iter = artist.albums.iter();
|
||||||
|
let filtered = iter.filter(|a| self.filter.filter_album(a));
|
||||||
|
let albums: Vec<Album> = filtered.cloned().collect();
|
||||||
|
|
||||||
|
if albums.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let meta = artist.meta.clone();
|
||||||
|
Some(Artist { meta, albums })
|
||||||
|
}
|
||||||
|
|
||||||
fn get_artist<'a>(collection: &'a Collection, artist_id: &ArtistId) -> Option<&'a Artist> {
|
fn get_artist<'a>(collection: &'a Collection, artist_id: &ArtistId) -> Option<&'a Artist> {
|
||||||
collection.iter().find(|a| &a.meta.id == artist_id)
|
collection.iter().find(|a| &a.meta.id == artist_id)
|
||||||
}
|
}
|
||||||
@ -117,7 +148,11 @@ impl<Database, Library> IMusicHoardBasePrivate for MusicHoard<Database, Library>
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::core::testmod::FULL_COLLECTION;
|
use crate::{
|
||||||
|
collection::{album::AlbumPrimaryType, artist::ArtistMeta},
|
||||||
|
core::testmod::FULL_COLLECTION,
|
||||||
|
filter::AlbumField,
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@ -290,4 +325,31 @@ mod tests {
|
|||||||
mh.collection = mh.merge_collections();
|
mh.collection = mh.merge_collections();
|
||||||
assert_eq!(expected, mh.collection);
|
assert_eq!(expected, mh.collection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filtered() {
|
||||||
|
let mut mh = MusicHoard {
|
||||||
|
collection: vec![Artist {
|
||||||
|
meta: ArtistMeta::new(ArtistId::new("Artist")),
|
||||||
|
albums: vec![
|
||||||
|
Album::new(AlbumId::new("Album 1")),
|
||||||
|
Album::new(AlbumId::new("Album 2")),
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
mh.collection[0].albums[0].meta.info.primary_type = Some(AlbumPrimaryType::Ep);
|
||||||
|
mh.collection[0].albums[0].meta.info.primary_type = Some(AlbumPrimaryType::Album);
|
||||||
|
|
||||||
|
mh.set_filter(CollectionFilter {
|
||||||
|
include: vec![vec![AlbumField::PrimaryType(Some(AlbumPrimaryType::Album))]],
|
||||||
|
except: vec![],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(mh.get_collection().len(), 1);
|
||||||
|
assert_eq!(mh.get_filtered().len(), 1);
|
||||||
|
|
||||||
|
assert_eq!(mh.get_collection()[0].albums.len(), 2);
|
||||||
|
assert_eq!(mh.get_filtered()[0].albums.len(), 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use crate::core::{
|
use crate::core::{
|
||||||
interface::{database::IDatabase, library::ILibrary},
|
interface::{database::IDatabase, library::ILibrary},
|
||||||
musichoard::{database::IMusicHoardDatabase, Error, MusicHoard, NoDatabase, NoLibrary},
|
musichoard::{
|
||||||
|
database::IMusicHoardDatabase, CollectionFilter, Error, MusicHoard, NoDatabase, NoLibrary,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Builder for [`MusicHoard`]. Its purpose is to make it easier to set various combinations of
|
/// Builder for [`MusicHoard`]. Its purpose is to make it easier to set various combinations of
|
||||||
@ -62,6 +64,8 @@ impl MusicHoard<NoDatabase, NoLibrary> {
|
|||||||
/// Create a new [`MusicHoard`] without any library or database.
|
/// Create a new [`MusicHoard`] without any library or database.
|
||||||
pub fn empty() -> Self {
|
pub fn empty() -> Self {
|
||||||
MusicHoard {
|
MusicHoard {
|
||||||
|
filter: CollectionFilter::default(),
|
||||||
|
filtered: vec![],
|
||||||
collection: vec![],
|
collection: vec![],
|
||||||
pre_commit: vec![],
|
pre_commit: vec![],
|
||||||
database: NoDatabase,
|
database: NoDatabase,
|
||||||
@ -83,6 +87,8 @@ impl<Library: ILibrary> MusicHoard<NoDatabase, Library> {
|
|||||||
/// Create a new [`MusicHoard`] with the provided [`ILibrary`] and no database.
|
/// Create a new [`MusicHoard`] with the provided [`ILibrary`] and no database.
|
||||||
pub fn library(library: Library) -> Self {
|
pub fn library(library: Library) -> Self {
|
||||||
MusicHoard {
|
MusicHoard {
|
||||||
|
filter: CollectionFilter::default(),
|
||||||
|
filtered: vec![],
|
||||||
collection: vec![],
|
collection: vec![],
|
||||||
pre_commit: vec![],
|
pre_commit: vec![],
|
||||||
database: NoDatabase,
|
database: NoDatabase,
|
||||||
@ -104,6 +110,8 @@ impl<Database: IDatabase> MusicHoard<Database, NoLibrary> {
|
|||||||
/// Create a new [`MusicHoard`] with the provided [`IDatabase`] and no library.
|
/// Create a new [`MusicHoard`] with the provided [`IDatabase`] and no library.
|
||||||
pub fn database(database: Database) -> Result<Self, Error> {
|
pub fn database(database: Database) -> Result<Self, Error> {
|
||||||
let mut mh = MusicHoard {
|
let mut mh = MusicHoard {
|
||||||
|
filter: CollectionFilter::default(),
|
||||||
|
filtered: vec![],
|
||||||
collection: vec![],
|
collection: vec![],
|
||||||
pre_commit: vec![],
|
pre_commit: vec![],
|
||||||
database,
|
database,
|
||||||
@ -127,6 +135,8 @@ impl<Database: IDatabase, Library: ILibrary> MusicHoard<Database, Library> {
|
|||||||
/// Create a new [`MusicHoard`] with the provided [`ILibrary`] and [`IDatabase`].
|
/// Create a new [`MusicHoard`] with the provided [`ILibrary`] and [`IDatabase`].
|
||||||
pub fn new(database: Database, library: Library) -> Result<Self, Error> {
|
pub fn new(database: Database, library: Library) -> Result<Self, Error> {
|
||||||
let mut mh = MusicHoard {
|
let mut mh = MusicHoard {
|
||||||
|
filter: CollectionFilter::default(),
|
||||||
|
filtered: vec![],
|
||||||
collection: vec![],
|
collection: vec![],
|
||||||
pre_commit: vec![],
|
pre_commit: vec![],
|
||||||
database,
|
database,
|
||||||
|
@ -120,6 +120,8 @@ impl<Database: IDatabase, Library> IMusicHoardDatabase for MusicHoard<Database,
|
|||||||
Self::sort_albums_and_tracks(self.database_cache.iter_mut());
|
Self::sort_albums_and_tracks(self.database_cache.iter_mut());
|
||||||
|
|
||||||
self.collection = self.merge_collections();
|
self.collection = self.merge_collections();
|
||||||
|
self.filtered = self.filter_collection();
|
||||||
|
|
||||||
self.pre_commit = self.collection.clone();
|
self.pre_commit = self.collection.clone();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -364,6 +366,7 @@ pub trait IMusicHoardDatabasePrivate {
|
|||||||
impl<Library> IMusicHoardDatabasePrivate for MusicHoard<NoDatabase, Library> {
|
impl<Library> IMusicHoardDatabasePrivate for MusicHoard<NoDatabase, Library> {
|
||||||
fn commit(&mut self) -> Result<(), Error> {
|
fn commit(&mut self) -> Result<(), Error> {
|
||||||
self.collection = self.pre_commit.clone();
|
self.collection = self.pre_commit.clone();
|
||||||
|
self.filtered = self.filter_collection();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -376,6 +379,7 @@ impl<Database: IDatabase, Library> IMusicHoardDatabasePrivate for MusicHoard<Dat
|
|||||||
return Err(err.into());
|
return Err(err.into());
|
||||||
}
|
}
|
||||||
self.collection = self.pre_commit.clone();
|
self.collection = self.pre_commit.clone();
|
||||||
|
self.filtered = self.filter_collection();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
194
src/core/musichoard/filter.rs
Normal file
194
src/core/musichoard/filter.rs
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
use crate::core::collection::album::{Album, AlbumOwnership, AlbumPrimaryType, AlbumSecondaryType};
|
||||||
|
|
||||||
|
/// Filter for a specifying subsets of the entire collection (e.g., for display).
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct CollectionFilter {
|
||||||
|
pub include: Vec<Vec<AlbumField>>,
|
||||||
|
pub except: Vec<Vec<AlbumField>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AlbumField {
|
||||||
|
PrimaryType(Option<AlbumPrimaryType>),
|
||||||
|
SecondaryType(AlbumSecondaryType),
|
||||||
|
Ownership(AlbumOwnership),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CollectionFilter {
|
||||||
|
pub fn filter_album(&self, album: &Album) -> bool {
|
||||||
|
let include = Self::filter_and(true, &self.include, album);
|
||||||
|
let except = Self::filter_and(false, &self.except, album);
|
||||||
|
include && !except
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter_and(empty: bool, group: &[Vec<AlbumField>], album: &Album) -> bool {
|
||||||
|
let mut filter = !group.is_empty() || empty;
|
||||||
|
for field in group.iter() {
|
||||||
|
filter = filter && Self::filter_or(field, album);
|
||||||
|
}
|
||||||
|
filter
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter_or(group: &[AlbumField], album: &Album) -> bool {
|
||||||
|
let mut filter = false;
|
||||||
|
for field in group.iter() {
|
||||||
|
filter = filter || Self::filter_field(field, album);
|
||||||
|
}
|
||||||
|
filter
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter_field(field: &AlbumField, album: &Album) -> bool {
|
||||||
|
match field {
|
||||||
|
AlbumField::PrimaryType(filter) => *filter == album.meta.info.primary_type,
|
||||||
|
AlbumField::SecondaryType(filter) => {
|
||||||
|
let types = &album.meta.info.secondary_types;
|
||||||
|
types.iter().any(|st| st == filter)
|
||||||
|
}
|
||||||
|
AlbumField::Ownership(filter) => *filter == album.get_ownership(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::collection::{
|
||||||
|
album::AlbumId,
|
||||||
|
track::{Track, TrackFormat, TrackId, TrackNum, TrackQuality},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn test_filter() -> CollectionFilter {
|
||||||
|
CollectionFilter {
|
||||||
|
include: vec![vec![
|
||||||
|
AlbumField::PrimaryType(None),
|
||||||
|
AlbumField::PrimaryType(Some(AlbumPrimaryType::Ep)),
|
||||||
|
AlbumField::PrimaryType(Some(AlbumPrimaryType::Album)),
|
||||||
|
AlbumField::Ownership(AlbumOwnership::Owned(TrackFormat::Mp3)),
|
||||||
|
AlbumField::Ownership(AlbumOwnership::Owned(TrackFormat::Flac)),
|
||||||
|
]],
|
||||||
|
except: vec![
|
||||||
|
vec![AlbumField::Ownership(AlbumOwnership::None)],
|
||||||
|
vec![
|
||||||
|
AlbumField::SecondaryType(AlbumSecondaryType::Compilation),
|
||||||
|
AlbumField::SecondaryType(AlbumSecondaryType::Soundtrack),
|
||||||
|
AlbumField::SecondaryType(AlbumSecondaryType::Live),
|
||||||
|
AlbumField::SecondaryType(AlbumSecondaryType::Demo),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_track() -> Track {
|
||||||
|
Track {
|
||||||
|
id: TrackId {
|
||||||
|
title: String::from("Track"),
|
||||||
|
},
|
||||||
|
number: TrackNum(1),
|
||||||
|
artist: vec![String::from("Artist")],
|
||||||
|
quality: TrackQuality {
|
||||||
|
format: TrackFormat::Mp3,
|
||||||
|
bitrate: 320,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_album() -> Album {
|
||||||
|
let mut album = Album::new(AlbumId::new("An Album"));
|
||||||
|
album.meta.info.primary_type = Some(AlbumPrimaryType::Album);
|
||||||
|
album.tracks.push(test_track());
|
||||||
|
album
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filter_primary_type() {
|
||||||
|
let filter = test_filter();
|
||||||
|
let mut album = test_album();
|
||||||
|
|
||||||
|
// Drop ownership so that filtering is truly only on type.
|
||||||
|
album.tracks.clear();
|
||||||
|
assert_eq!(album.get_ownership(), AlbumOwnership::None);
|
||||||
|
|
||||||
|
album.meta.info.primary_type = None;
|
||||||
|
assert!(filter.filter_album(&album));
|
||||||
|
|
||||||
|
album.meta.info.primary_type = Some(AlbumPrimaryType::Ep);
|
||||||
|
assert!(filter.filter_album(&album));
|
||||||
|
|
||||||
|
album.meta.info.primary_type = Some(AlbumPrimaryType::Album);
|
||||||
|
assert!(filter.filter_album(&album));
|
||||||
|
|
||||||
|
album.meta.info.primary_type = Some(AlbumPrimaryType::Broadcast);
|
||||||
|
assert!(!filter.filter_album(&album));
|
||||||
|
|
||||||
|
album.meta.info.primary_type = Some(AlbumPrimaryType::Other);
|
||||||
|
assert!(!filter.filter_album(&album));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filter_secondary_type() {
|
||||||
|
let filter = test_filter();
|
||||||
|
let mut album = test_album();
|
||||||
|
|
||||||
|
assert!(filter.filter_album(&album));
|
||||||
|
|
||||||
|
// Non-filtered type.
|
||||||
|
let types = &mut album.meta.info.secondary_types;
|
||||||
|
types.push(AlbumSecondaryType::AudioDrama);
|
||||||
|
assert!(filter.filter_album(&album));
|
||||||
|
|
||||||
|
// Filtered type. But album is owned so it remains.
|
||||||
|
let types = &mut album.meta.info.secondary_types;
|
||||||
|
types.push(AlbumSecondaryType::Live);
|
||||||
|
assert_ne!(album.get_ownership(), AlbumOwnership::None);
|
||||||
|
assert!(filter.filter_album(&album));
|
||||||
|
|
||||||
|
// Add another filtered type.
|
||||||
|
let types = &mut album.meta.info.secondary_types;
|
||||||
|
types.push(AlbumSecondaryType::Soundtrack);
|
||||||
|
assert!(filter.filter_album(&album));
|
||||||
|
|
||||||
|
// Drop ownership and this should be now excluded.
|
||||||
|
album.tracks.clear();
|
||||||
|
assert_eq!(album.get_ownership(), AlbumOwnership::None);
|
||||||
|
assert!(!filter.filter_album(&album));
|
||||||
|
|
||||||
|
// Remove one of the two filtered types.
|
||||||
|
album.meta.info.secondary_types.pop();
|
||||||
|
assert!(!filter.filter_album(&album));
|
||||||
|
|
||||||
|
// Remove the second filtered type. Should now be included by primary type.
|
||||||
|
album.meta.info.secondary_types.pop();
|
||||||
|
assert!(filter.filter_album(&album));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filter_ownership() {
|
||||||
|
let filter = test_filter();
|
||||||
|
let mut album = test_album();
|
||||||
|
|
||||||
|
// It is an album though so it should remain included.
|
||||||
|
album.tracks.clear();
|
||||||
|
assert_eq!(album.get_ownership(), AlbumOwnership::None);
|
||||||
|
assert!(filter.filter_album(&album));
|
||||||
|
|
||||||
|
// Change to unincluded primary type.
|
||||||
|
album.meta.info.primary_type = Some(AlbumPrimaryType::Other);
|
||||||
|
assert!(!filter.filter_album(&album));
|
||||||
|
|
||||||
|
// Changing ownership should make it go back to being included.
|
||||||
|
album.tracks.push(test_track());
|
||||||
|
assert_eq!(
|
||||||
|
album.get_ownership(),
|
||||||
|
AlbumOwnership::Owned(TrackFormat::Mp3)
|
||||||
|
);
|
||||||
|
assert!(filter.filter_album(&album));
|
||||||
|
|
||||||
|
album.tracks[0].quality.format = TrackFormat::Flac;
|
||||||
|
assert_eq!(
|
||||||
|
album.get_ownership(),
|
||||||
|
AlbumOwnership::Owned(TrackFormat::Flac)
|
||||||
|
);
|
||||||
|
assert!(filter.filter_album(&album));
|
||||||
|
}
|
||||||
|
}
|
@ -5,18 +5,21 @@ mod database;
|
|||||||
mod library;
|
mod library;
|
||||||
|
|
||||||
pub mod builder;
|
pub mod builder;
|
||||||
|
pub mod filter;
|
||||||
|
|
||||||
pub use base::IMusicHoardBase;
|
pub use base::IMusicHoardBase;
|
||||||
pub use database::IMusicHoardDatabase;
|
pub use database::IMusicHoardDatabase;
|
||||||
|
pub use filter::CollectionFilter;
|
||||||
pub use library::IMusicHoardLibrary;
|
pub use library::IMusicHoardLibrary;
|
||||||
|
|
||||||
use std::fmt::{self, Display};
|
use std::fmt::{self, Display};
|
||||||
|
|
||||||
use crate::core::collection::Collection;
|
use crate::core::{
|
||||||
|
collection::Collection,
|
||||||
use crate::core::interface::{
|
interface::{
|
||||||
database::{LoadError as DatabaseLoadError, SaveError as DatabaseSaveError},
|
database::{LoadError as DatabaseLoadError, SaveError as DatabaseSaveError},
|
||||||
library::Error as LibraryError,
|
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
|
||||||
@ -24,6 +27,8 @@ use crate::core::interface::{
|
|||||||
// TODO: Split into inner and external/interfaces to facilitate building.
|
// TODO: Split into inner and external/interfaces to facilitate building.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct MusicHoard<Database, Library> {
|
pub struct MusicHoard<Database, Library> {
|
||||||
|
filter: CollectionFilter,
|
||||||
|
filtered: Collection,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
pre_commit: Collection,
|
pre_commit: Collection,
|
||||||
database: Database,
|
database: Database,
|
||||||
|
@ -10,8 +10,8 @@ pub use core::collection;
|
|||||||
pub use core::interface;
|
pub use core::interface;
|
||||||
|
|
||||||
pub use core::musichoard::{
|
pub use core::musichoard::{
|
||||||
builder::MusicHoardBuilder, Error, IMusicHoardBase, IMusicHoardDatabase, IMusicHoardLibrary,
|
builder::MusicHoardBuilder, filter, Error, IMusicHoardBase, IMusicHoardDatabase,
|
||||||
MusicHoard, NoDatabase, NoLibrary,
|
IMusicHoardLibrary, MusicHoard, NoDatabase, NoLibrary,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
30
src/main.rs
30
src/main.rs
@ -6,6 +6,10 @@ use ratatui::{backend::CrosstermBackend, Terminal};
|
|||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
use musichoard::{
|
use musichoard::{
|
||||||
|
collection::{
|
||||||
|
album::{AlbumOwnership, AlbumPrimaryType, AlbumSecondaryType},
|
||||||
|
track::TrackFormat,
|
||||||
|
},
|
||||||
external::{
|
external::{
|
||||||
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
||||||
library::beets::{
|
library::beets::{
|
||||||
@ -14,11 +18,12 @@ use musichoard::{
|
|||||||
},
|
},
|
||||||
musicbrainz::{api::MusicBrainzClient, http::MusicBrainzHttp},
|
musicbrainz::{api::MusicBrainzClient, http::MusicBrainzHttp},
|
||||||
},
|
},
|
||||||
|
filter::{AlbumField, CollectionFilter},
|
||||||
interface::{
|
interface::{
|
||||||
database::{IDatabase, NullDatabase},
|
database::{IDatabase, NullDatabase},
|
||||||
library::{ILibrary, NullLibrary},
|
library::{ILibrary, NullLibrary},
|
||||||
},
|
},
|
||||||
MusicHoardBuilder, NoDatabase, NoLibrary,
|
IMusicHoardBase, MusicHoardBuilder, NoDatabase, NoLibrary,
|
||||||
};
|
};
|
||||||
|
|
||||||
use tui::{
|
use tui::{
|
||||||
@ -69,10 +74,31 @@ struct DbOpt {
|
|||||||
no_database: bool,
|
no_database: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_filter() -> CollectionFilter {
|
||||||
|
CollectionFilter {
|
||||||
|
include: vec![vec![
|
||||||
|
AlbumField::PrimaryType(None),
|
||||||
|
AlbumField::PrimaryType(Some(AlbumPrimaryType::Ep)),
|
||||||
|
AlbumField::PrimaryType(Some(AlbumPrimaryType::Album)),
|
||||||
|
AlbumField::Ownership(AlbumOwnership::Owned(TrackFormat::Mp3)),
|
||||||
|
AlbumField::Ownership(AlbumOwnership::Owned(TrackFormat::Flac)),
|
||||||
|
]],
|
||||||
|
except: vec![
|
||||||
|
vec![AlbumField::Ownership(AlbumOwnership::None)],
|
||||||
|
AlbumSecondaryType::all_variants()
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(AlbumField::SecondaryType)
|
||||||
|
.collect(),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn with<Database: IDatabase + 'static, Library: ILibrary + 'static>(
|
fn with<Database: IDatabase + 'static, Library: ILibrary + 'static>(
|
||||||
builder: MusicHoardBuilder<Database, Library>,
|
builder: MusicHoardBuilder<Database, Library>,
|
||||||
) {
|
) {
|
||||||
let music_hoard = builder.build().expect("failed to initialise MusicHoard");
|
let mut music_hoard = builder.build().expect("failed to initialise MusicHoard");
|
||||||
|
music_hoard.set_filter(default_filter());
|
||||||
|
|
||||||
// Initialize the terminal user interface.
|
// Initialize the terminal user interface.
|
||||||
let backend = CrosstermBackend::new(io::stdout());
|
let backend = CrosstermBackend::new(io::stdout());
|
||||||
|
@ -45,14 +45,14 @@ impl IAppInteractBrowse for AppMachine<BrowseState> {
|
|||||||
fn increment_selection(mut self, delta: Delta) -> Self::APP {
|
fn increment_selection(mut self, delta: Delta) -> Self::APP {
|
||||||
self.inner
|
self.inner
|
||||||
.selection
|
.selection
|
||||||
.increment_selection(self.inner.music_hoard.get_collection(), delta);
|
.increment_selection(self.inner.music_hoard.get_filtered(), delta);
|
||||||
self.into()
|
self.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decrement_selection(mut self, delta: Delta) -> Self::APP {
|
fn decrement_selection(mut self, delta: Delta) -> Self::APP {
|
||||||
self.inner
|
self.inner
|
||||||
.selection
|
.selection
|
||||||
.decrement_selection(self.inner.music_hoard.get_collection(), delta);
|
.decrement_selection(self.inner.music_hoard.get_filtered(), delta);
|
||||||
self.into()
|
self.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ impl IAppInteractBrowse for AppMachine<BrowseState> {
|
|||||||
let orig = ListSelection::get(&self.inner.selection);
|
let orig = ListSelection::get(&self.inner.selection);
|
||||||
self.inner
|
self.inner
|
||||||
.selection
|
.selection
|
||||||
.reset(self.inner.music_hoard.get_collection());
|
.reset(self.inner.music_hoard.get_filtered());
|
||||||
AppMachine::search_state(self.inner, orig).into()
|
AppMachine::search_state(self.inner, orig).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ impl AppMachine<FetchState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_job(inner: &AppInner, rx: MbApiReceiver) -> Result<SubmitJob, &'static str> {
|
fn fetch_job(inner: &AppInner, rx: MbApiReceiver) -> Result<SubmitJob, &'static str> {
|
||||||
let coll = inner.music_hoard.get_collection();
|
let coll = inner.music_hoard.get_filtered();
|
||||||
|
|
||||||
let artist = match inner.selection.state_artist(coll) {
|
let artist = match inner.selection.state_artist(coll) {
|
||||||
Some(artist_state) => &coll[artist_state.index],
|
Some(artist_state) => &coll[artist_state.index],
|
||||||
@ -184,18 +184,22 @@ impl AppMachine<FetchState> {
|
|||||||
inner: &mut AppInner,
|
inner: &mut AppInner,
|
||||||
fetch_albums: Vec<AlbumMeta>,
|
fetch_albums: Vec<AlbumMeta>,
|
||||||
) -> Result<(), musichoard::Error> {
|
) -> Result<(), musichoard::Error> {
|
||||||
let coll = inner.music_hoard.get_collection();
|
let coll = inner.music_hoard.get_filtered();
|
||||||
|
|
||||||
let artist_state = inner.selection.state_artist(coll).unwrap();
|
let artist_state = inner.selection.state_artist(coll).unwrap();
|
||||||
let artist = &coll[artist_state.index];
|
let artist = &coll[artist_state.index];
|
||||||
let selection = KeySelection::get(coll, &inner.selection);
|
let selection = KeySelection::get(coll, &inner.selection);
|
||||||
|
|
||||||
let artist_id = &artist.meta.id.clone();
|
// Find the artist in the full collection to correctly identify already existing albums.
|
||||||
|
let artist_id = artist.meta.id.clone();
|
||||||
|
let coll = inner.music_hoard.get_collection();
|
||||||
|
let artist = coll.iter().find(|a| a.meta.id == artist_id).unwrap();
|
||||||
|
|
||||||
for new in Self::new_albums(fetch_albums, &artist.albums).into_iter() {
|
for new in Self::new_albums(fetch_albums, &artist.albums).into_iter() {
|
||||||
inner.music_hoard.add_album(artist_id, new)?;
|
inner.music_hoard.add_album(&artist_id, new)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let coll = inner.music_hoard.get_collection();
|
let coll = inner.music_hoard.get_filtered();
|
||||||
inner.selection.select_by_id(coll, selection);
|
inner.selection.select_by_id(coll, selection);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -361,7 +365,7 @@ impl IAppEventFetch for AppMachine<FetchState> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use mockall::predicate;
|
use mockall::{predicate, Sequence};
|
||||||
use musichoard::collection::{
|
use musichoard::collection::{
|
||||||
album::AlbumMeta,
|
album::AlbumMeta,
|
||||||
artist::{ArtistId, ArtistMeta},
|
artist::{ArtistId, ArtistMeta},
|
||||||
@ -735,11 +739,18 @@ mod tests {
|
|||||||
tx.send(fetch_result).unwrap();
|
tx.send(fetch_result).unwrap();
|
||||||
drop(tx);
|
drop(tx);
|
||||||
|
|
||||||
let mut music_hoard = music_hoard(collection);
|
let mut music_hoard = music_hoard(collection.clone());
|
||||||
|
let mut seq = Sequence::new();
|
||||||
|
music_hoard
|
||||||
|
.expect_get_collection()
|
||||||
|
.times(1)
|
||||||
|
.in_sequence(&mut seq)
|
||||||
|
.return_const(collection);
|
||||||
music_hoard
|
music_hoard
|
||||||
.expect_add_album()
|
.expect_add_album()
|
||||||
.with(predicate::eq(artist_id), predicate::eq(new_album))
|
.with(predicate::eq(artist_id), predicate::eq(new_album))
|
||||||
.times(1)
|
.times(1)
|
||||||
|
.in_sequence(&mut seq)
|
||||||
.return_once(|_, _| Ok(()));
|
.return_once(|_, _| Ok(()));
|
||||||
|
|
||||||
let app = AppMachine::app_fetch_next(inner(music_hoard), fetch);
|
let app = AppMachine::app_fetch_next(inner(music_hoard), fetch);
|
||||||
@ -762,11 +773,18 @@ mod tests {
|
|||||||
tx.send(fetch_result).unwrap();
|
tx.send(fetch_result).unwrap();
|
||||||
drop(tx);
|
drop(tx);
|
||||||
|
|
||||||
let mut music_hoard = music_hoard(collection);
|
let mut music_hoard = music_hoard(collection.clone());
|
||||||
|
let mut seq = Sequence::new();
|
||||||
|
music_hoard
|
||||||
|
.expect_get_collection()
|
||||||
|
.times(1)
|
||||||
|
.in_sequence(&mut seq)
|
||||||
|
.return_const(collection);
|
||||||
music_hoard
|
music_hoard
|
||||||
.expect_add_album()
|
.expect_add_album()
|
||||||
.with(predicate::eq(artist_id), predicate::eq(new_album))
|
.with(predicate::eq(artist_id), predicate::eq(new_album))
|
||||||
.times(1)
|
.times(1)
|
||||||
|
.in_sequence(&mut seq)
|
||||||
.return_once(|_, _| Err(musichoard::Error::CollectionError(String::from("get rekt"))));
|
.return_once(|_, _| Err(musichoard::Error::CollectionError(String::from("get rekt"))));
|
||||||
|
|
||||||
let app = AppMachine::app_fetch_next(inner(music_hoard), fetch);
|
let app = AppMachine::app_fetch_next(inner(music_hoard), fetch);
|
||||||
|
@ -447,7 +447,8 @@ mod tests {
|
|||||||
let (_tx, rx) = mpsc::channel();
|
let (_tx, rx) = mpsc::channel();
|
||||||
let app_matches = MatchState::new(matches_info.clone(), FetchState::search(rx));
|
let app_matches = MatchState::new(matches_info.clone(), FetchState::search(rx));
|
||||||
|
|
||||||
let mut music_hoard = music_hoard(vec![]);
|
let collection = vec![];
|
||||||
|
let mut music_hoard = music_hoard(collection.clone());
|
||||||
let artist_id = ArtistId::new("Artist");
|
let artist_id = ArtistId::new("Artist");
|
||||||
match matches_info {
|
match matches_info {
|
||||||
EntityMatches::Album(_) => {
|
EntityMatches::Album(_) => {
|
||||||
@ -456,6 +457,11 @@ mod tests {
|
|||||||
let info = AlbumInfo::default();
|
let info = AlbumInfo::default();
|
||||||
|
|
||||||
let mut seq = Sequence::new();
|
let mut seq = Sequence::new();
|
||||||
|
music_hoard
|
||||||
|
.expect_get_collection()
|
||||||
|
.times(1)
|
||||||
|
.in_sequence(&mut seq)
|
||||||
|
.return_const(collection);
|
||||||
music_hoard
|
music_hoard
|
||||||
.expect_merge_album_info()
|
.expect_merge_album_info()
|
||||||
.with(eq(artist_id.clone()), eq(album_id.clone()), eq(info))
|
.with(eq(artist_id.clone()), eq(album_id.clone()), eq(info))
|
||||||
@ -595,7 +601,8 @@ mod tests {
|
|||||||
let (_tx, rx) = mpsc::channel();
|
let (_tx, rx) = mpsc::channel();
|
||||||
let app_matches = MatchState::new(matches_info.clone(), FetchState::search(rx));
|
let app_matches = MatchState::new(matches_info.clone(), FetchState::search(rx));
|
||||||
|
|
||||||
let mut music_hoard = music_hoard(vec![]);
|
let collection = vec![];
|
||||||
|
let mut music_hoard = music_hoard(collection.clone());
|
||||||
match matches_info {
|
match matches_info {
|
||||||
EntityMatches::Artist(_) => panic!(),
|
EntityMatches::Artist(_) => panic!(),
|
||||||
EntityMatches::Album(matches) => {
|
EntityMatches::Album(matches) => {
|
||||||
@ -606,6 +613,11 @@ mod tests {
|
|||||||
let artist = matches.artist.clone();
|
let artist = matches.artist.clone();
|
||||||
|
|
||||||
let mut seq = Sequence::new();
|
let mut seq = Sequence::new();
|
||||||
|
music_hoard
|
||||||
|
.expect_get_collection()
|
||||||
|
.times(1)
|
||||||
|
.in_sequence(&mut seq)
|
||||||
|
.return_const(collection);
|
||||||
music_hoard
|
music_hoard
|
||||||
.expect_merge_album_info()
|
.expect_merge_album_info()
|
||||||
.with(eq(artist.clone()), eq(album_id.clone()), eq(meta.info))
|
.with(eq(artist.clone()), eq(album_id.clone()), eq(meta.info))
|
||||||
@ -647,13 +659,19 @@ mod tests {
|
|||||||
let (_tx, rx) = mpsc::channel();
|
let (_tx, rx) = mpsc::channel();
|
||||||
let app_matches = MatchState::new(matches_info.clone(), FetchState::search(rx));
|
let app_matches = MatchState::new(matches_info.clone(), FetchState::search(rx));
|
||||||
|
|
||||||
let mut music_hoard = music_hoard(vec![artist]);
|
let collection = vec![artist];
|
||||||
|
let mut music_hoard = music_hoard(collection.clone());
|
||||||
match matches_info {
|
match matches_info {
|
||||||
EntityMatches::Artist(_) => panic!(),
|
EntityMatches::Artist(_) => panic!(),
|
||||||
EntityMatches::Album(matches) => {
|
EntityMatches::Album(matches) => {
|
||||||
let artist = matches.artist.clone();
|
let artist = matches.artist.clone();
|
||||||
|
|
||||||
let mut seq = Sequence::new();
|
let mut seq = Sequence::new();
|
||||||
|
music_hoard
|
||||||
|
.expect_get_collection()
|
||||||
|
.times(1)
|
||||||
|
.in_sequence(&mut seq)
|
||||||
|
.return_const(collection);
|
||||||
music_hoard
|
music_hoard
|
||||||
.expect_remove_album()
|
.expect_remove_album()
|
||||||
.times(1)
|
.times(1)
|
||||||
|
@ -173,7 +173,7 @@ impl AppInner {
|
|||||||
music_hoard: MH,
|
music_hoard: MH,
|
||||||
musicbrainz: MB,
|
musicbrainz: MB,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let selection = Selection::new(music_hoard.get_collection());
|
let selection = Selection::new(music_hoard.get_filtered());
|
||||||
AppInner {
|
AppInner {
|
||||||
running: true,
|
running: true,
|
||||||
music_hoard: Box::new(music_hoard),
|
music_hoard: Box::new(music_hoard),
|
||||||
@ -186,7 +186,7 @@ impl AppInner {
|
|||||||
impl<'a> From<&'a mut AppInner> for AppPublicInner<'a> {
|
impl<'a> From<&'a mut AppInner> for AppPublicInner<'a> {
|
||||||
fn from(inner: &'a mut AppInner) -> Self {
|
fn from(inner: &'a mut AppInner) -> Self {
|
||||||
AppPublicInner {
|
AppPublicInner {
|
||||||
collection: inner.music_hoard.get_collection(),
|
collection: inner.music_hoard.get_filtered(),
|
||||||
selection: &mut inner.selection,
|
selection: &mut inner.selection,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -330,7 +330,7 @@ mod tests {
|
|||||||
|
|
||||||
pub fn music_hoard(collection: Collection) -> MockIMusicHoard {
|
pub fn music_hoard(collection: Collection) -> MockIMusicHoard {
|
||||||
let mut music_hoard = MockIMusicHoard::new();
|
let mut music_hoard = MockIMusicHoard::new();
|
||||||
music_hoard.expect_get_collection().return_const(collection);
|
music_hoard.expect_get_filtered().return_const(collection);
|
||||||
|
|
||||||
music_hoard
|
music_hoard
|
||||||
}
|
}
|
||||||
@ -594,7 +594,7 @@ mod tests {
|
|||||||
.expect_rescan_library()
|
.expect_rescan_library()
|
||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|| Err(musichoard::Error::LibraryError(String::from("get rekt"))));
|
.return_once(|| Err(musichoard::Error::LibraryError(String::from("get rekt"))));
|
||||||
music_hoard.expect_get_collection().return_const(vec![]);
|
music_hoard.expect_get_filtered().return_const(vec![]);
|
||||||
|
|
||||||
let app = App::new(music_hoard, mb_job_sender());
|
let app = App::new(music_hoard, mb_job_sender());
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
@ -28,19 +28,15 @@ impl IAppInteractReload for AppMachine<ReloadState> {
|
|||||||
type APP = App;
|
type APP = App;
|
||||||
|
|
||||||
fn reload_library(mut self) -> Self::APP {
|
fn reload_library(mut self) -> Self::APP {
|
||||||
let previous = KeySelection::get(
|
let previous =
|
||||||
self.inner.music_hoard.get_collection(),
|
KeySelection::get(self.inner.music_hoard.get_filtered(), &self.inner.selection);
|
||||||
&self.inner.selection,
|
|
||||||
);
|
|
||||||
let result = self.inner.music_hoard.rescan_library();
|
let result = self.inner.music_hoard.rescan_library();
|
||||||
self.refresh(previous, result)
|
self.refresh(previous, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reload_database(mut self) -> Self::APP {
|
fn reload_database(mut self) -> Self::APP {
|
||||||
let previous = KeySelection::get(
|
let previous =
|
||||||
self.inner.music_hoard.get_collection(),
|
KeySelection::get(self.inner.music_hoard.get_filtered(), &self.inner.selection);
|
||||||
&self.inner.selection,
|
|
||||||
);
|
|
||||||
let result = self.inner.music_hoard.reload_database();
|
let result = self.inner.music_hoard.reload_database();
|
||||||
self.refresh(previous, result)
|
self.refresh(previous, result)
|
||||||
}
|
}
|
||||||
@ -60,7 +56,7 @@ impl IAppInteractReloadPrivate for AppMachine<ReloadState> {
|
|||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.inner
|
self.inner
|
||||||
.selection
|
.selection
|
||||||
.select_by_id(self.inner.music_hoard.get_collection(), previous);
|
.select_by_id(self.inner.music_hoard.get_filtered(), previous);
|
||||||
AppMachine::browse_state(self.inner).into()
|
AppMachine::browse_state(self.inner).into()
|
||||||
}
|
}
|
||||||
Err(err) => AppMachine::error_state(self.inner, err.to_string()).into(),
|
Err(err) => AppMachine::error_state(self.inner, err.to_string()).into(),
|
||||||
|
@ -66,7 +66,7 @@ impl IAppInteractSearch for AppMachine<SearchState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn step_back(mut self) -> Self::APP {
|
fn step_back(mut self) -> Self::APP {
|
||||||
let collection = self.inner.music_hoard.get_collection();
|
let collection = self.inner.music_hoard.get_filtered();
|
||||||
if let Some(memo) = self.state.memo.pop() {
|
if let Some(memo) = self.state.memo.pop() {
|
||||||
if memo.char {
|
if memo.char {
|
||||||
self.state.string.pop();
|
self.state.string.pop();
|
||||||
@ -104,7 +104,7 @@ trait IAppInteractSearchPrivate {
|
|||||||
|
|
||||||
impl IAppInteractSearchPrivate for AppMachine<SearchState> {
|
impl IAppInteractSearchPrivate for AppMachine<SearchState> {
|
||||||
fn incremental_search(&mut self, next: bool) {
|
fn incremental_search(&mut self, next: bool) {
|
||||||
let collection = self.inner.music_hoard.get_collection();
|
let collection = self.inner.music_hoard.get_filtered();
|
||||||
let search = &self.state.string;
|
let search = &self.state.string;
|
||||||
|
|
||||||
let sel = &self.inner.selection;
|
let sel = &self.inner.selection;
|
||||||
@ -121,7 +121,7 @@ impl IAppInteractSearchPrivate for AppMachine<SearchState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if result.is_some() {
|
if result.is_some() {
|
||||||
let collection = self.inner.music_hoard.get_collection();
|
let collection = self.inner.music_hoard.get_filtered();
|
||||||
self.inner.selection.select(collection, result);
|
self.inner.selection.select(collection, result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,8 @@ use mockall::automock;
|
|||||||
pub trait IMusicHoard {
|
pub trait IMusicHoard {
|
||||||
fn rescan_library(&mut self) -> Result<(), musichoard::Error>;
|
fn rescan_library(&mut self) -> Result<(), musichoard::Error>;
|
||||||
fn reload_database(&mut self) -> Result<(), musichoard::Error>;
|
fn reload_database(&mut self) -> Result<(), musichoard::Error>;
|
||||||
|
|
||||||
|
fn get_filtered(&self) -> &Collection;
|
||||||
fn get_collection(&self) -> &Collection;
|
fn get_collection(&self) -> &Collection;
|
||||||
|
|
||||||
fn add_album(
|
fn add_album(
|
||||||
@ -65,6 +67,10 @@ impl<Database: IDatabase, Library: ILibrary> IMusicHoard for MusicHoard<Database
|
|||||||
<Self as IMusicHoardDatabase>::reload_database(self)
|
<Self as IMusicHoardDatabase>::reload_database(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_filtered(&self) -> &Collection {
|
||||||
|
<Self as IMusicHoardBase>::get_filtered(self)
|
||||||
|
}
|
||||||
|
|
||||||
fn get_collection(&self) -> &Collection {
|
fn get_collection(&self) -> &Collection {
|
||||||
<Self as IMusicHoardBase>::get_collection(self)
|
<Self as IMusicHoardBase>::get_collection(self)
|
||||||
}
|
}
|
||||||
|
@ -200,7 +200,7 @@ mod tests {
|
|||||||
|
|
||||||
music_hoard.expect_reload_database().returning(|| Ok(()));
|
music_hoard.expect_reload_database().returning(|| Ok(()));
|
||||||
music_hoard.expect_rescan_library().returning(|| Ok(()));
|
music_hoard.expect_rescan_library().returning(|| Ok(()));
|
||||||
music_hoard.expect_get_collection().return_const(collection);
|
music_hoard.expect_get_filtered().return_const(collection);
|
||||||
|
|
||||||
music_hoard
|
music_hoard
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user