More submodules
This commit is contained in:
parent
839193ce39
commit
9d4682b8bc
378
src/lib.rs
378
src/lib.rs
@ -1,19 +1,15 @@
|
|||||||
//! MusicHoard - a music collection manager.
|
//! MusicHoard - a music collection manager.
|
||||||
|
|
||||||
mod collection;
|
mod collection;
|
||||||
|
mod musichoard;
|
||||||
|
|
||||||
|
// FIXME: make private and fix via re-exports.
|
||||||
pub mod database;
|
pub mod database;
|
||||||
pub mod library;
|
pub mod library;
|
||||||
|
|
||||||
use std::{
|
use std::fmt::{self, Display};
|
||||||
collections::HashMap,
|
|
||||||
fmt::{self, Display},
|
|
||||||
mem,
|
|
||||||
};
|
|
||||||
|
|
||||||
use collection::{artist::InvalidUrlError, Merge};
|
use collection::artist::InvalidUrlError;
|
||||||
use database::IDatabase;
|
|
||||||
use library::{ILibrary, Item, Query};
|
|
||||||
use paste::paste;
|
|
||||||
|
|
||||||
// FIXME: validate the re-exports.
|
// FIXME: validate the re-exports.
|
||||||
pub use collection::{
|
pub use collection::{
|
||||||
@ -23,6 +19,9 @@ pub use collection::{
|
|||||||
Collection,
|
Collection,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// FIXME: validate the re-exports
|
||||||
|
pub use musichoard::{MusicHoard, MusicHoardBuilder, NoDatabase, NoLibrary};
|
||||||
|
|
||||||
/// Error type for `musichoard`.
|
/// Error type for `musichoard`.
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
@ -88,370 +87,9 @@ impl From<InvalidUrlError> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Music Hoard. It is responsible for pulling information from both the library and the
|
|
||||||
/// database, ensuring its consistent and writing back any changes.
|
|
||||||
pub struct MusicHoard<LIB, DB> {
|
|
||||||
collection: Collection,
|
|
||||||
library: LIB,
|
|
||||||
database: DB,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Phantom type for when a library implementation is not needed.
|
|
||||||
pub struct NoLibrary;
|
|
||||||
|
|
||||||
/// Phantom type for when a database implementation is not needed.
|
|
||||||
pub struct NoDatabase;
|
|
||||||
|
|
||||||
macro_rules! music_hoard_unique_url_dispatch {
|
|
||||||
($field:ident) => {
|
|
||||||
paste! {
|
|
||||||
pub fn [<add_ $field _url>]<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
|
||||||
&mut self,
|
|
||||||
artist_id: ID,
|
|
||||||
url: S,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
self.get_artist_mut_or_err(artist_id.as_ref())?.[<add_ $field _url>](url)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn [<remove_ $field _url>]<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
|
||||||
&mut self,
|
|
||||||
artist_id: ID,
|
|
||||||
url: S,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
self.get_artist_mut_or_err(artist_id.as_ref())?.[<remove_ $field _url>](url)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn [<set_ $field _url>]<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
|
||||||
&mut self,
|
|
||||||
artist_id: ID,
|
|
||||||
url: S,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
self.get_artist_mut_or_err(artist_id.as_ref())?.[<set_ $field _url>](url)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn [<clear_ $field _url>]<ID: AsRef<ArtistId>>(
|
|
||||||
&mut self,
|
|
||||||
artist_id: ID,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
self.get_artist_mut_or_err(artist_id.as_ref())?.[<clear_ $field _url>]();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! music_hoard_multi_url_dispatch {
|
|
||||||
($field:ident) => {
|
|
||||||
paste! {
|
|
||||||
pub fn [<add_ $field _urls>]<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
|
||||||
&mut self,
|
|
||||||
artist_id: ID,
|
|
||||||
urls: Vec<S>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
self.get_artist_mut_or_err(artist_id.as_ref())?.[<add_ $field _urls>](urls)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn [<remove_ $field _urls>]<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
|
||||||
&mut self,
|
|
||||||
artist_id: ID,
|
|
||||||
urls: Vec<S>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
self.get_artist_mut_or_err(artist_id.as_ref())?.[<remove_ $field _urls>](urls)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn [<set_ $field _urls>]<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
|
||||||
&mut self,
|
|
||||||
artist_id: ID,
|
|
||||||
urls: Vec<S>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
self.get_artist_mut_or_err(artist_id.as_ref())?.[<set_ $field _urls>](urls)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn [<clear_ $field _urls>]<ID: AsRef<ArtistId>>(
|
|
||||||
&mut self, artist_id: ID,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
self.get_artist_mut_or_err(artist_id.as_ref())?.[<clear_ $field _urls>]();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<LIB, DB> MusicHoard<LIB, DB> {
|
|
||||||
/// Create a new [`MusicHoard`] with the provided [`ILibrary`] and [`IDatabase`].
|
|
||||||
pub fn new(library: LIB, database: DB) -> Self {
|
|
||||||
MusicHoard {
|
|
||||||
collection: vec![],
|
|
||||||
library,
|
|
||||||
database,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieve the [`Collection`].
|
|
||||||
pub fn get_collection(&self) -> &Collection {
|
|
||||||
&self.collection
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_artist<ID: Into<ArtistId>>(&mut self, artist_id: ID) {
|
|
||||||
let artist_id: ArtistId = artist_id.into();
|
|
||||||
|
|
||||||
if self.get_artist(&artist_id).is_none() {
|
|
||||||
self.collection.push(Artist::new(artist_id));
|
|
||||||
Self::sort_artists(&mut self.collection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_artist<ID: AsRef<ArtistId>>(&mut self, artist_id: ID) {
|
|
||||||
let index_opt = self
|
|
||||||
.collection
|
|
||||||
.iter()
|
|
||||||
.position(|a| &a.id == artist_id.as_ref());
|
|
||||||
|
|
||||||
if let Some(index) = index_opt {
|
|
||||||
self.collection.remove(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_artist_sort<ID: AsRef<ArtistId>, SORT: Into<ArtistId>>(
|
|
||||||
&mut self,
|
|
||||||
artist_id: ID,
|
|
||||||
artist_sort: SORT,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
self.get_artist_mut_or_err(artist_id.as_ref())?
|
|
||||||
.set_sort_key(artist_sort);
|
|
||||||
Self::sort(&mut self.collection);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_artist_sort<ID: AsRef<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> {
|
|
||||||
self.get_artist_mut_or_err(artist_id.as_ref())?
|
|
||||||
.clear_sort_key();
|
|
||||||
Self::sort(&mut self.collection);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
music_hoard_unique_url_dispatch!(musicbrainz);
|
|
||||||
|
|
||||||
music_hoard_multi_url_dispatch!(musicbutler);
|
|
||||||
|
|
||||||
music_hoard_multi_url_dispatch!(bandcamp);
|
|
||||||
|
|
||||||
music_hoard_unique_url_dispatch!(qobuz);
|
|
||||||
|
|
||||||
fn sort(collection: &mut [Artist]) {
|
|
||||||
Self::sort_artists(collection);
|
|
||||||
Self::sort_albums_and_tracks(collection.iter_mut());
|
|
||||||
}
|
|
||||||
|
|
||||||
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_with_primary(&mut self, primary: HashMap<ArtistId, Artist>) {
|
|
||||||
let collection = mem::take(&mut self.collection);
|
|
||||||
self.collection = Self::merge_collections(primary, collection);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn merge_with_secondary<SEC: IntoIterator<Item = Artist>>(&mut self, secondary: SEC) {
|
|
||||||
let primary_map: HashMap<ArtistId, Artist> = self
|
|
||||||
.collection
|
|
||||||
.drain(..)
|
|
||||||
.map(|a| (a.id.clone(), a))
|
|
||||||
.collect();
|
|
||||||
self.collection = Self::merge_collections(primary_map, secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn merge_collections<SEC: IntoIterator<Item = Artist>>(
|
|
||||||
mut primary: HashMap<ArtistId, Artist>,
|
|
||||||
secondary: SEC,
|
|
||||||
) -> Collection {
|
|
||||||
for secondary_artist in secondary.into_iter() {
|
|
||||||
if let Some(ref mut primary_artist) = primary.get_mut(&secondary_artist.id) {
|
|
||||||
primary_artist.merge_in_place(secondary_artist);
|
|
||||||
} else {
|
|
||||||
primary.insert(secondary_artist.id.clone(), secondary_artist);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut collection: Collection = primary.into_values().collect();
|
|
||||||
Self::sort_artists(&mut collection);
|
|
||||||
collection
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
year: item.album_year,
|
|
||||||
title: item.album_title,
|
|
||||||
};
|
|
||||||
|
|
||||||
let track = Track {
|
|
||||||
id: TrackId {
|
|
||||||
number: item.track_number,
|
|
||||||
title: item.track_title,
|
|
||||||
},
|
|
||||||
artist: item.track_artist,
|
|
||||||
quality: Quality {
|
|
||||||
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 => artist.albums.push(Album {
|
|
||||||
id: album_id,
|
|
||||||
tracks: vec![track],
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(collection)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_artist(&self, artist_id: &ArtistId) -> Option<&Artist> {
|
|
||||||
self.collection.iter().find(|a| &a.id == artist_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_artist_mut(&mut self, artist_id: &ArtistId) -> Option<&mut Artist> {
|
|
||||||
self.collection.iter_mut().find(|a| &a.id == artist_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_artist_mut_or_err(&mut self, artist_id: &ArtistId) -> Result<&mut Artist, Error> {
|
|
||||||
self.get_artist_mut(artist_id).ok_or_else(|| {
|
|
||||||
Error::CollectionError(format!("artist '{}' is not in the collection", artist_id))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<LIB: ILibrary, DB> MusicHoard<LIB, DB> {
|
|
||||||
/// Rescan the library and merge with the in-memory collection.
|
|
||||||
pub fn rescan_library(&mut self) -> Result<(), Error> {
|
|
||||||
let items = self.library.list(&Query::new())?;
|
|
||||||
let mut library_collection = Self::items_to_artists(items)?;
|
|
||||||
Self::sort_albums_and_tracks(library_collection.values_mut());
|
|
||||||
|
|
||||||
self.merge_with_primary(library_collection);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
|
|
||||||
/// Load the database and merge with the in-memory collection.
|
|
||||||
pub fn load_from_database(&mut self) -> Result<(), Error> {
|
|
||||||
let mut database_collection = self.database.load()?;
|
|
||||||
Self::sort_albums_and_tracks(database_collection.iter_mut());
|
|
||||||
|
|
||||||
self.merge_with_secondary(database_collection);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save the in-memory collection to the database.
|
|
||||||
pub fn save_to_database(&mut self) -> Result<(), Error> {
|
|
||||||
self.database.save(&self.collection)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builder for [`MusicHoard`]. Its purpose is to make it easier to set various combinations of
|
|
||||||
/// library/database or their absence.
|
|
||||||
pub struct MusicHoardBuilder<LIB, DB> {
|
|
||||||
library: LIB,
|
|
||||||
database: DB,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for MusicHoardBuilder<NoLibrary, NoDatabase> {
|
|
||||||
/// Create a [`MusicHoardBuilder`].
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MusicHoardBuilder<NoLibrary, NoDatabase> {
|
|
||||||
/// Create a [`MusicHoardBuilder`].
|
|
||||||
pub fn new() -> Self {
|
|
||||||
MusicHoardBuilder {
|
|
||||||
library: NoLibrary,
|
|
||||||
database: NoDatabase,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<LIB, DB> MusicHoardBuilder<LIB, DB> {
|
|
||||||
/// Set a library for [`MusicHoard`].
|
|
||||||
pub fn set_library<NEWLIB: ILibrary>(self, library: NEWLIB) -> MusicHoardBuilder<NEWLIB, DB> {
|
|
||||||
MusicHoardBuilder {
|
|
||||||
library,
|
|
||||||
database: self.database,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set a database for [`MusicHoard`].
|
|
||||||
pub fn set_database<NEWDB: IDatabase>(self, database: NEWDB) -> MusicHoardBuilder<LIB, NEWDB> {
|
|
||||||
MusicHoardBuilder {
|
|
||||||
library: self.library,
|
|
||||||
database,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build [`MusicHoard`] with the currently set library and database.
|
|
||||||
pub fn build(self) -> MusicHoard<LIB, DB> {
|
|
||||||
MusicHoard::new(self.library, self.database)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod testmacros;
|
mod testmacros;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod testlib;
|
mod testlib;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
|
376
src/musichoard/mod.rs
Normal file
376
src/musichoard/mod.rs
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
use std::{collections::HashMap, mem};
|
||||||
|
|
||||||
|
use paste::paste;
|
||||||
|
|
||||||
|
use super::collection::{
|
||||||
|
album::{Album, AlbumId},
|
||||||
|
artist::{Artist, ArtistId},
|
||||||
|
track::{Quality, Track, TrackId},
|
||||||
|
Collection, Merge,
|
||||||
|
};
|
||||||
|
use super::database::IDatabase;
|
||||||
|
use super::library::{ILibrary, Item, Query};
|
||||||
|
|
||||||
|
// FIXME: should not be importing this error.
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
/// The Music Hoard. It is responsible for pulling information from both the library and the
|
||||||
|
/// database, ensuring its consistent and writing back any changes.
|
||||||
|
pub struct MusicHoard<LIB, DB> {
|
||||||
|
collection: Collection,
|
||||||
|
library: LIB,
|
||||||
|
database: DB,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! music_hoard_unique_url_dispatch {
|
||||||
|
($field:ident) => {
|
||||||
|
paste! {
|
||||||
|
pub fn [<add_ $field _url>]<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
||||||
|
&mut self,
|
||||||
|
artist_id: ID,
|
||||||
|
url: S,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.get_artist_mut_or_err(artist_id.as_ref())?.[<add_ $field _url>](url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn [<remove_ $field _url>]<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
||||||
|
&mut self,
|
||||||
|
artist_id: ID,
|
||||||
|
url: S,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.get_artist_mut_or_err(artist_id.as_ref())?.[<remove_ $field _url>](url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn [<set_ $field _url>]<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
||||||
|
&mut self,
|
||||||
|
artist_id: ID,
|
||||||
|
url: S,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.get_artist_mut_or_err(artist_id.as_ref())?.[<set_ $field _url>](url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn [<clear_ $field _url>]<ID: AsRef<ArtistId>>(
|
||||||
|
&mut self,
|
||||||
|
artist_id: ID,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.get_artist_mut_or_err(artist_id.as_ref())?.[<clear_ $field _url>]();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! music_hoard_multi_url_dispatch {
|
||||||
|
($field:ident) => {
|
||||||
|
paste! {
|
||||||
|
pub fn [<add_ $field _urls>]<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
||||||
|
&mut self,
|
||||||
|
artist_id: ID,
|
||||||
|
urls: Vec<S>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.get_artist_mut_or_err(artist_id.as_ref())?.[<add_ $field _urls>](urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn [<remove_ $field _urls>]<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
||||||
|
&mut self,
|
||||||
|
artist_id: ID,
|
||||||
|
urls: Vec<S>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.get_artist_mut_or_err(artist_id.as_ref())?.[<remove_ $field _urls>](urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn [<set_ $field _urls>]<ID: AsRef<ArtistId>, S: AsRef<str>>(
|
||||||
|
&mut self,
|
||||||
|
artist_id: ID,
|
||||||
|
urls: Vec<S>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.get_artist_mut_or_err(artist_id.as_ref())?.[<set_ $field _urls>](urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn [<clear_ $field _urls>]<ID: AsRef<ArtistId>>(
|
||||||
|
&mut self, artist_id: ID,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.get_artist_mut_or_err(artist_id.as_ref())?.[<clear_ $field _urls>]();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<LIB, DB> MusicHoard<LIB, DB> {
|
||||||
|
/// Create a new [`MusicHoard`] with the provided [`ILibrary`] and [`IDatabase`].
|
||||||
|
pub fn new(library: LIB, database: DB) -> Self {
|
||||||
|
MusicHoard {
|
||||||
|
collection: vec![],
|
||||||
|
library,
|
||||||
|
database,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve the [`Collection`].
|
||||||
|
pub fn get_collection(&self) -> &Collection {
|
||||||
|
&self.collection
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_artist<ID: Into<ArtistId>>(&mut self, artist_id: ID) {
|
||||||
|
let artist_id: ArtistId = artist_id.into();
|
||||||
|
|
||||||
|
if self.get_artist(&artist_id).is_none() {
|
||||||
|
self.collection.push(Artist::new(artist_id));
|
||||||
|
Self::sort_artists(&mut self.collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_artist<ID: AsRef<ArtistId>>(&mut self, artist_id: ID) {
|
||||||
|
let index_opt = self
|
||||||
|
.collection
|
||||||
|
.iter()
|
||||||
|
.position(|a| &a.id == artist_id.as_ref());
|
||||||
|
|
||||||
|
if let Some(index) = index_opt {
|
||||||
|
self.collection.remove(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_artist_sort<ID: AsRef<ArtistId>, SORT: Into<ArtistId>>(
|
||||||
|
&mut self,
|
||||||
|
artist_id: ID,
|
||||||
|
artist_sort: SORT,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.get_artist_mut_or_err(artist_id.as_ref())?
|
||||||
|
.set_sort_key(artist_sort);
|
||||||
|
Self::sort(&mut self.collection);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_artist_sort<ID: AsRef<ArtistId>>(&mut self, artist_id: ID) -> Result<(), Error> {
|
||||||
|
self.get_artist_mut_or_err(artist_id.as_ref())?
|
||||||
|
.clear_sort_key();
|
||||||
|
Self::sort(&mut self.collection);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
music_hoard_unique_url_dispatch!(musicbrainz);
|
||||||
|
|
||||||
|
music_hoard_multi_url_dispatch!(musicbutler);
|
||||||
|
|
||||||
|
music_hoard_multi_url_dispatch!(bandcamp);
|
||||||
|
|
||||||
|
music_hoard_unique_url_dispatch!(qobuz);
|
||||||
|
|
||||||
|
fn sort(collection: &mut [Artist]) {
|
||||||
|
Self::sort_artists(collection);
|
||||||
|
Self::sort_albums_and_tracks(collection.iter_mut());
|
||||||
|
}
|
||||||
|
|
||||||
|
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_with_primary(&mut self, primary: HashMap<ArtistId, Artist>) {
|
||||||
|
let collection = mem::take(&mut self.collection);
|
||||||
|
self.collection = Self::merge_collections(primary, collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_with_secondary<SEC: IntoIterator<Item = Artist>>(&mut self, secondary: SEC) {
|
||||||
|
let primary_map: HashMap<ArtistId, Artist> = self
|
||||||
|
.collection
|
||||||
|
.drain(..)
|
||||||
|
.map(|a| (a.id.clone(), a))
|
||||||
|
.collect();
|
||||||
|
self.collection = Self::merge_collections(primary_map, secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_collections<SEC: IntoIterator<Item = Artist>>(
|
||||||
|
mut primary: HashMap<ArtistId, Artist>,
|
||||||
|
secondary: SEC,
|
||||||
|
) -> Collection {
|
||||||
|
for secondary_artist in secondary.into_iter() {
|
||||||
|
if let Some(ref mut primary_artist) = primary.get_mut(&secondary_artist.id) {
|
||||||
|
primary_artist.merge_in_place(secondary_artist);
|
||||||
|
} else {
|
||||||
|
primary.insert(secondary_artist.id.clone(), secondary_artist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut collection: Collection = primary.into_values().collect();
|
||||||
|
Self::sort_artists(&mut collection);
|
||||||
|
collection
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
year: item.album_year,
|
||||||
|
title: item.album_title,
|
||||||
|
};
|
||||||
|
|
||||||
|
let track = Track {
|
||||||
|
id: TrackId {
|
||||||
|
number: item.track_number,
|
||||||
|
title: item.track_title,
|
||||||
|
},
|
||||||
|
artist: item.track_artist,
|
||||||
|
quality: Quality {
|
||||||
|
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 => artist.albums.push(Album {
|
||||||
|
id: album_id,
|
||||||
|
tracks: vec![track],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_artist(&self, artist_id: &ArtistId) -> Option<&Artist> {
|
||||||
|
self.collection.iter().find(|a| &a.id == artist_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_artist_mut(&mut self, artist_id: &ArtistId) -> Option<&mut Artist> {
|
||||||
|
self.collection.iter_mut().find(|a| &a.id == artist_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_artist_mut_or_err(&mut self, artist_id: &ArtistId) -> Result<&mut Artist, Error> {
|
||||||
|
self.get_artist_mut(artist_id).ok_or_else(|| {
|
||||||
|
Error::CollectionError(format!("artist '{}' is not in the collection", artist_id))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<LIB: ILibrary, DB> MusicHoard<LIB, DB> {
|
||||||
|
/// Rescan the library and merge with the in-memory collection.
|
||||||
|
pub fn rescan_library(&mut self) -> Result<(), Error> {
|
||||||
|
let items = self.library.list(&Query::new())?;
|
||||||
|
let mut library_collection = Self::items_to_artists(items)?;
|
||||||
|
Self::sort_albums_and_tracks(library_collection.values_mut());
|
||||||
|
|
||||||
|
self.merge_with_primary(library_collection);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<LIB, DB: IDatabase> MusicHoard<LIB, DB> {
|
||||||
|
/// Load the database and merge with the in-memory collection.
|
||||||
|
pub fn load_from_database(&mut self) -> Result<(), Error> {
|
||||||
|
let mut database_collection = self.database.load()?;
|
||||||
|
Self::sort_albums_and_tracks(database_collection.iter_mut());
|
||||||
|
|
||||||
|
self.merge_with_secondary(database_collection);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save the in-memory collection to the database.
|
||||||
|
pub fn save_to_database(&mut self) -> Result<(), Error> {
|
||||||
|
self.database.save(&self.collection)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder for [`MusicHoard`]. Its purpose is to make it easier to set various combinations of
|
||||||
|
/// library/database or their absence.
|
||||||
|
pub struct MusicHoardBuilder<LIB, DB> {
|
||||||
|
library: LIB,
|
||||||
|
database: DB,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phantom type for when a library implementation is not needed.
|
||||||
|
pub struct NoLibrary;
|
||||||
|
|
||||||
|
/// Phantom type for when a database implementation is not needed.
|
||||||
|
pub struct NoDatabase;
|
||||||
|
|
||||||
|
impl Default for MusicHoardBuilder<NoLibrary, NoDatabase> {
|
||||||
|
/// Create a [`MusicHoardBuilder`].
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MusicHoardBuilder<NoLibrary, NoDatabase> {
|
||||||
|
/// Create a [`MusicHoardBuilder`].
|
||||||
|
pub fn new() -> Self {
|
||||||
|
MusicHoardBuilder {
|
||||||
|
library: NoLibrary,
|
||||||
|
database: NoDatabase,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<LIB, DB> MusicHoardBuilder<LIB, DB> {
|
||||||
|
/// Set a library for [`MusicHoard`].
|
||||||
|
pub fn set_library<NEWLIB: ILibrary>(self, library: NEWLIB) -> MusicHoardBuilder<NEWLIB, DB> {
|
||||||
|
MusicHoardBuilder {
|
||||||
|
library,
|
||||||
|
database: self.database,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a database for [`MusicHoard`].
|
||||||
|
pub fn set_database<NEWDB: IDatabase>(self, database: NEWDB) -> MusicHoardBuilder<LIB, NEWDB> {
|
||||||
|
MusicHoardBuilder {
|
||||||
|
library: self.library,
|
||||||
|
database,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build [`MusicHoard`] with the currently set library and database.
|
||||||
|
pub fn build(self) -> MusicHoard<LIB, DB> {
|
||||||
|
MusicHoard::new(self.library, self.database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
@ -1,14 +1,16 @@
|
|||||||
use mockall::predicate;
|
use mockall::predicate;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use collection::{
|
|
||||||
artist::{Bandcamp, MusicBrainz, MusicButler, Qobuz},
|
// FIXME: check all crate::* imports - are the tests where they should be?
|
||||||
|
use crate::collection::{
|
||||||
|
artist::{ArtistProperties, Bandcamp, MusicBrainz, MusicButler, Qobuz},
|
||||||
track::Format,
|
track::Format,
|
||||||
Merge,
|
Merge,
|
||||||
};
|
};
|
||||||
use database::MockIDatabase;
|
use crate::database::{self, MockIDatabase};
|
||||||
use library::{testmod::LIBRARY_ITEMS, MockILibrary};
|
use crate::library::{self, testmod::LIBRARY_ITEMS, MockILibrary};
|
||||||
use testlib::{FULL_COLLECTION, LIBRARY_COLLECTION};
|
use crate::testlib::{FULL_COLLECTION, LIBRARY_COLLECTION};
|
||||||
|
|
||||||
static MUSICBRAINZ: &str = "https://musicbrainz.org/artist/d368baa8-21ca-4759-9731-0b2753071ad8";
|
static MUSICBRAINZ: &str = "https://musicbrainz.org/artist/d368baa8-21ca-4759-9731-0b2753071ad8";
|
||||||
static MUSICBRAINZ_2: &str = "https://musicbrainz.org/artist/823869a5-5ded-4f6b-9fb7-2a9344d83c6b";
|
static MUSICBRAINZ_2: &str = "https://musicbrainz.org/artist/823869a5-5ded-4f6b-9fb7-2a9344d83c6b";
|
Loading…
Reference in New Issue
Block a user