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
6 changed files with 120 additions and 41 deletions
Showing only changes of commit eac27d3b17 - Show all commits

View File

@ -138,7 +138,7 @@ pub enum AlbumSecondaryType {
} }
/// The album's ownership status. /// The album's ownership status.
#[derive(Debug)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AlbumOwnership { pub enum AlbumOwnership {
None, None,
Owned(TrackFormat), Owned(TrackFormat),

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.map(|a| self.filter_artist(a)).flatten().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)
} }

View File

@ -0,0 +1,50 @@
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 exclude: 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(&self.include, album);
let exclude = Self::filter_and(&self.exclude, album);
include && !exclude
}
fn filter_and(group: &Vec<Vec<AlbumField>>, album: &Album) -> bool {
let mut filter = !group.is_empty();
for group in group.iter() {
filter = filter && Self::filter_or(group, album);
}
filter
}
fn filter_or(group: &Vec<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().find(|st| *st == filter).is_some()
}
AlbumField::Ownership(filter) => *filter == album.get_ownership(),
}
}
}

View File

@ -5,19 +5,17 @@ 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::{ use crate::core::{
collection::{ collection::Collection,
album::{AlbumOwnership, AlbumPrimaryType, AlbumSecondaryType},
track::TrackFormat,
Collection,
},
interface::{ interface::{
database::{LoadError as DatabaseLoadError, SaveError as DatabaseSaveError}, database::{LoadError as DatabaseLoadError, SaveError as DatabaseSaveError},
library::Error as LibraryError, library::Error as LibraryError,
@ -39,32 +37,6 @@ pub struct MusicHoard<Database, Library> {
library_cache: Collection, library_cache: Collection,
} }
/// Filter for a specifying subsets of the entire collection (e.g., for display).
// Filter is inclusive, not exclusive. To include something in the filtered collection, the filter
// must select it. It also means that the options below are additive - the more options are set, the
// more items will be included.
#[derive(Debug)]
pub struct CollectionFilter {
primary_types: Vec<AlbumPrimaryType>,
secondary_types: Vec<AlbumSecondaryType>,
include_untyped: bool,
ownership: Vec<AlbumOwnership>,
}
impl Default for CollectionFilter {
fn default() -> Self {
CollectionFilter {
primary_types: vec![AlbumPrimaryType::Ep, AlbumPrimaryType::Album],
secondary_types: vec![],
include_untyped: true,
ownership: vec![
AlbumOwnership::Owned(TrackFormat::Mp3),
AlbumOwnership::Owned(TrackFormat::Flac),
],
}
}
}
/// Phantom type for when a library implementation is not needed. /// Phantom type for when a library implementation is not needed.
#[derive(Debug)] #[derive(Debug)]
pub struct NoLibrary; pub struct NoLibrary;

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,19 +6,17 @@ use ratatui::{backend::CrosstermBackend, Terminal};
use structopt::StructOpt; use structopt::StructOpt;
use musichoard::{ use musichoard::{
external::{ collection::album::{AlbumOwnership, AlbumPrimaryType, AlbumSecondaryType}, external::{
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase}, database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
library::beets::{ library::beets::{
executor::{ssh::BeetsLibrarySshExecutor, BeetsLibraryProcessExecutor}, executor::{ssh::BeetsLibrarySshExecutor, BeetsLibraryProcessExecutor},
BeetsLibrary, BeetsLibrary,
}, },
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},
}, }, IMusicHoardBase, MusicHoardBuilder, NoDatabase, NoLibrary
MusicHoardBuilder, NoDatabase, NoLibrary,
}; };
use tui::{ use tui::{
@ -69,10 +67,38 @@ 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)),
]],
exclude: vec![
vec![AlbumField::Ownership(AlbumOwnership::None)],
vec![
AlbumField::SecondaryType(AlbumSecondaryType::Compilation),
AlbumField::SecondaryType(AlbumSecondaryType::Soundtrack),
AlbumField::SecondaryType(AlbumSecondaryType::Spokenword),
AlbumField::SecondaryType(AlbumSecondaryType::Interview),
AlbumField::SecondaryType(AlbumSecondaryType::Audiobook),
AlbumField::SecondaryType(AlbumSecondaryType::AudioDrama),
AlbumField::SecondaryType(AlbumSecondaryType::Live),
AlbumField::SecondaryType(AlbumSecondaryType::Remix),
AlbumField::SecondaryType(AlbumSecondaryType::DjMix),
AlbumField::SecondaryType(AlbumSecondaryType::MixtapeStreet),
AlbumField::SecondaryType(AlbumSecondaryType::Demo),
AlbumField::SecondaryType(AlbumSecondaryType::FieldRecording),
],
],
}
}
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());