Add a filtering tool to only show only certain release group types #252

Merged
wojtek merged 14 commits from 161---add-a-filtering-tool-to-only-show-only-certain-release-group-types into main 2025-01-04 22:42:27 +01:00
16 changed files with 410 additions and 43 deletions

View File

@ -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());
}
} }

View File

@ -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);
}
} }

View File

@ -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,

View File

@ -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(())
} }

View 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));
}
}

View File

@ -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,

View File

@ -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)]

View File

@ -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());

View File

@ -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()
} }

View File

@ -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);

View File

@ -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)

View File

@ -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());

View File

@ -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(),

View File

@ -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);
} }
} }

View File

@ -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)
} }

View File

@ -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
} }