parent
6e9249e265
commit
ba85505c9a
@ -1,10 +1,12 @@
|
|||||||
use paste::paste;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use paste::paste;
|
||||||
use structopt::{clap::AppSettings, StructOpt};
|
use structopt::{clap::AppSettings, StructOpt};
|
||||||
|
|
||||||
use musichoard::{
|
use musichoard::{
|
||||||
|
collection::artist::ArtistId,
|
||||||
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
||||||
ArtistId, MusicHoard, MusicHoardBuilder, NoLibrary,
|
MusicHoard, MusicHoardBuilder, NoLibrary,
|
||||||
};
|
};
|
||||||
|
|
||||||
type MH = MusicHoard<NoLibrary, JsonDatabase<JsonDatabaseFileBackend>>;
|
type MH = MusicHoard<NoLibrary, JsonDatabase<JsonDatabaseFileBackend>>;
|
||||||
|
84
src/core/collection/album.rs
Normal file
84
src/core/collection/album.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
use std::mem;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::core::collection::{
|
||||||
|
merge::{Merge, MergeSorted},
|
||||||
|
track::Track,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// An album is a collection of tracks that were released together.
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct Album {
|
||||||
|
pub id: AlbumId,
|
||||||
|
pub tracks: Vec<Track>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The album identifier.
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
||||||
|
pub struct AlbumId {
|
||||||
|
pub year: u32,
|
||||||
|
pub title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Album {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Album {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.id.cmp(&other.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Merge for Album {
|
||||||
|
fn merge_in_place(&mut self, other: Self) {
|
||||||
|
assert_eq!(self.id, other.id);
|
||||||
|
let tracks = mem::take(&mut self.tracks);
|
||||||
|
self.tracks = MergeSorted::new(tracks.into_iter(), other.tracks.into_iter()).collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::core::testmod::FULL_COLLECTION;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_album_no_overlap() {
|
||||||
|
let left = FULL_COLLECTION[0].albums[0].to_owned();
|
||||||
|
let mut right = FULL_COLLECTION[0].albums[1].to_owned();
|
||||||
|
right.id = left.id.clone();
|
||||||
|
|
||||||
|
let mut expected = left.clone();
|
||||||
|
expected.tracks.append(&mut right.tracks.clone());
|
||||||
|
expected.tracks.sort_unstable();
|
||||||
|
|
||||||
|
let merged = left.clone().merge(right.clone());
|
||||||
|
assert_eq!(expected, merged);
|
||||||
|
|
||||||
|
// Non-overlapping merge should be commutative.
|
||||||
|
let merged = right.clone().merge(left.clone());
|
||||||
|
assert_eq!(expected, merged);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_album_overlap() {
|
||||||
|
let mut left = FULL_COLLECTION[0].albums[0].to_owned();
|
||||||
|
let mut right = FULL_COLLECTION[0].albums[1].to_owned();
|
||||||
|
right.id = left.id.clone();
|
||||||
|
left.tracks.push(right.tracks[0].clone());
|
||||||
|
left.tracks.sort_unstable();
|
||||||
|
|
||||||
|
let mut expected = left.clone();
|
||||||
|
expected.tracks.append(&mut right.tracks.clone());
|
||||||
|
expected.tracks.sort_unstable();
|
||||||
|
expected.tracks.dedup();
|
||||||
|
|
||||||
|
let merged = left.clone().merge(right);
|
||||||
|
assert_eq!(expected, merged);
|
||||||
|
}
|
||||||
|
}
|
1014
src/core/collection/artist.rs
Normal file
1014
src/core/collection/artist.rs
Normal file
File diff suppressed because it is too large
Load Diff
67
src/core/collection/merge.rs
Normal file
67
src/core/collection/merge.rs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
use std::{cmp::Ordering, iter::Peekable};
|
||||||
|
|
||||||
|
/// A trait for merging two objects. The merge is asymmetric with the left argument considered to be
|
||||||
|
/// the primary whose properties are to be kept in case of collisions.
|
||||||
|
pub trait Merge {
|
||||||
|
fn merge_in_place(&mut self, other: Self);
|
||||||
|
|
||||||
|
fn merge(mut self, other: Self) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
self.merge_in_place(other);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_vecs<T: Ord + Eq>(this: &mut Vec<T>, mut other: Vec<T>) {
|
||||||
|
this.append(&mut other);
|
||||||
|
this.sort_unstable();
|
||||||
|
this.dedup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MergeSorted<L, R>
|
||||||
|
where
|
||||||
|
L: Iterator<Item = R::Item>,
|
||||||
|
R: Iterator,
|
||||||
|
{
|
||||||
|
left: Peekable<L>,
|
||||||
|
right: Peekable<R>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L, R> MergeSorted<L, R>
|
||||||
|
where
|
||||||
|
L: Iterator<Item = R::Item>,
|
||||||
|
R: Iterator,
|
||||||
|
{
|
||||||
|
pub fn new(left: L, right: R) -> MergeSorted<L, R> {
|
||||||
|
MergeSorted {
|
||||||
|
left: left.peekable(),
|
||||||
|
right: right.peekable(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L, R> Iterator for MergeSorted<L, R>
|
||||||
|
where
|
||||||
|
L: Iterator<Item = R::Item>,
|
||||||
|
R: Iterator,
|
||||||
|
L::Item: Ord + Merge,
|
||||||
|
{
|
||||||
|
type Item = L::Item;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<L::Item> {
|
||||||
|
let which = match (self.left.peek(), self.right.peek()) {
|
||||||
|
(Some(l), Some(r)) => l.cmp(r),
|
||||||
|
(Some(_), None) => Ordering::Less,
|
||||||
|
(None, Some(_)) => Ordering::Greater,
|
||||||
|
(None, None) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
match which {
|
||||||
|
Ordering::Less => self.left.next(),
|
||||||
|
Ordering::Equal => Some(self.left.next().unwrap().merge(self.right.next().unwrap())),
|
||||||
|
Ordering::Greater => self.right.next(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
src/core/collection/mod.rs
Normal file
40
src/core/collection/mod.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
//! The collection module defines the core data types and their relations.
|
||||||
|
|
||||||
|
pub mod album;
|
||||||
|
pub mod artist;
|
||||||
|
pub mod track;
|
||||||
|
|
||||||
|
mod merge;
|
||||||
|
pub use merge::Merge;
|
||||||
|
|
||||||
|
use std::fmt::{self, Display};
|
||||||
|
|
||||||
|
/// The [`Collection`] alias type for convenience.
|
||||||
|
pub type Collection = Vec<artist::Artist>;
|
||||||
|
|
||||||
|
/// Error type for the [`collection`] module.
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum Error {
|
||||||
|
/// An error occurred when processing a URL.
|
||||||
|
UrlError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Self::UrlError(ref s) => write!(f, "an error occurred when processing a URL: {s}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<url::ParseError> for Error {
|
||||||
|
fn from(err: url::ParseError) -> Error {
|
||||||
|
Error::UrlError(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<uuid::Error> for Error {
|
||||||
|
fn from(err: uuid::Error) -> Error {
|
||||||
|
Error::UrlError(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
81
src/core/collection/track.rs
Normal file
81
src/core/collection/track.rs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::core::collection::merge::Merge;
|
||||||
|
|
||||||
|
/// A single track on an album.
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct Track {
|
||||||
|
pub id: TrackId,
|
||||||
|
pub artist: Vec<String>,
|
||||||
|
pub quality: Quality,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The track identifier.
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct TrackId {
|
||||||
|
pub number: u32,
|
||||||
|
pub title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The track quality. Combines format and bitrate information.
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct Quality {
|
||||||
|
pub format: Format,
|
||||||
|
pub bitrate: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The track file format.
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Format {
|
||||||
|
Flac,
|
||||||
|
Mp3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Track {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Track {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.id.cmp(&other.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Merge for Track {
|
||||||
|
fn merge_in_place(&mut self, other: Self) {
|
||||||
|
assert_eq!(self.id, other.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_track() {
|
||||||
|
let left = Track {
|
||||||
|
id: TrackId {
|
||||||
|
number: 4,
|
||||||
|
title: String::from("a title"),
|
||||||
|
},
|
||||||
|
artist: vec![String::from("left artist")],
|
||||||
|
quality: Quality {
|
||||||
|
format: Format::Flac,
|
||||||
|
bitrate: 1411,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let right = Track {
|
||||||
|
id: left.id.clone(),
|
||||||
|
artist: vec![String::from("right artist")],
|
||||||
|
quality: Quality {
|
||||||
|
format: Format::Mp3,
|
||||||
|
bitrate: 320,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let merged = left.clone().merge(right);
|
||||||
|
assert_eq!(left, merged);
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use super::IJsonDatabaseBackend;
|
use crate::core::database::json::IJsonDatabaseBackend;
|
||||||
|
|
||||||
/// JSON database backend that uses a local file for persistent storage.
|
/// JSON database backend that uses a local file for persistent storage.
|
||||||
pub struct JsonDatabaseFileBackend {
|
pub struct JsonDatabaseFileBackend {
|
@ -1,13 +1,14 @@
|
|||||||
//! Module for storing MusicHoard data in a JSON file database.
|
//! Module for storing MusicHoard data in a JSON file database.
|
||||||
|
|
||||||
|
pub mod backend;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
|
|
||||||
use crate::Collection;
|
use crate::core::{
|
||||||
|
collection::Collection,
|
||||||
use super::{IDatabase, LoadError, SaveError};
|
database::{IDatabase, LoadError, SaveError},
|
||||||
|
};
|
||||||
pub mod backend;
|
|
||||||
|
|
||||||
impl From<serde_json::Error> for LoadError {
|
impl From<serde_json::Error> for LoadError {
|
||||||
fn from(err: serde_json::Error) -> LoadError {
|
fn from(err: serde_json::Error) -> LoadError {
|
||||||
@ -66,7 +67,13 @@ mod tests {
|
|||||||
|
|
||||||
use mockall::predicate;
|
use mockall::predicate;
|
||||||
|
|
||||||
use crate::{testlib::FULL_COLLECTION, Artist, ArtistId, Collection};
|
use crate::core::{
|
||||||
|
collection::{
|
||||||
|
artist::{Artist, ArtistId},
|
||||||
|
Collection,
|
||||||
|
},
|
||||||
|
testmod::FULL_COLLECTION,
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use testmod::DATABASE_JSON;
|
use testmod::DATABASE_JSON;
|
@ -1,14 +1,14 @@
|
|||||||
//! Module for storing MusicHoard data in a database.
|
//! Module for storing MusicHoard data in a database.
|
||||||
|
|
||||||
|
#[cfg(feature = "database-json")]
|
||||||
|
pub mod json;
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
|
|
||||||
use crate::Collection;
|
use crate::core::collection::Collection;
|
||||||
|
|
||||||
#[cfg(feature = "database-json")]
|
|
||||||
pub mod json;
|
|
||||||
|
|
||||||
/// Trait for interacting with the database.
|
/// Trait for interacting with the database.
|
||||||
#[cfg_attr(test, automock)]
|
#[cfg_attr(test, automock)]
|
@ -8,7 +8,7 @@ use std::{
|
|||||||
str,
|
str,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Error, IBeetsLibraryExecutor};
|
use crate::core::library::{beets::IBeetsLibraryExecutor, Error};
|
||||||
|
|
||||||
const BEET_DEFAULT: &str = "beet";
|
const BEET_DEFAULT: &str = "beet";
|
||||||
|
|
@ -1,14 +1,15 @@
|
|||||||
//! Module for interacting with the music library via
|
//! Module for interacting with the music library via
|
||||||
//! [beets](https://beets.readthedocs.io/en/stable/).
|
//! [beets](https://beets.readthedocs.io/en/stable/).
|
||||||
|
|
||||||
|
pub mod executor;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
|
|
||||||
use crate::Format;
|
use crate::core::{
|
||||||
|
collection::track::Format,
|
||||||
use super::{Error, Field, ILibrary, Item, Query};
|
library::{Error, Field, ILibrary, Item, Query},
|
||||||
|
};
|
||||||
pub mod executor;
|
|
||||||
|
|
||||||
macro_rules! list_format_separator {
|
macro_rules! list_format_separator {
|
||||||
() => {
|
() => {
|
||||||
@ -176,7 +177,7 @@ mod testmod;
|
|||||||
mod tests {
|
mod tests {
|
||||||
use mockall::predicate;
|
use mockall::predicate;
|
||||||
|
|
||||||
use crate::library::testmod::LIBRARY_ITEMS;
|
use crate::core::library::testmod::LIBRARY_ITEMS;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use testmod::LIBRARY_BEETS;
|
use testmod::LIBRARY_BEETS;
|
@ -1,14 +1,14 @@
|
|||||||
//! Module for interacting with the music library.
|
//! Module for interacting with the music library.
|
||||||
|
|
||||||
|
#[cfg(feature = "library-beets")]
|
||||||
|
pub mod beets;
|
||||||
|
|
||||||
use std::{collections::HashSet, fmt, num::ParseIntError, str::Utf8Error};
|
use std::{collections::HashSet, fmt, num::ParseIntError, str::Utf8Error};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
|
|
||||||
use crate::Format;
|
use crate::core::collection::track::Format;
|
||||||
|
|
||||||
#[cfg(feature = "library-beets")]
|
|
||||||
pub mod beets;
|
|
||||||
|
|
||||||
/// Trait for interacting with the music library.
|
/// Trait for interacting with the music library.
|
||||||
#[cfg_attr(test, automock)]
|
#[cfg_attr(test, automock)]
|
@ -1,6 +1,6 @@
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
use crate::{library::Item, Format};
|
use crate::core::{collection::track::Format, library::Item};
|
||||||
|
|
||||||
pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
|
pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
|
||||||
vec![
|
vec![
|
7
src/core/mod.rs
Normal file
7
src/core/mod.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
pub mod collection;
|
||||||
|
pub mod database;
|
||||||
|
pub mod library;
|
||||||
|
pub mod musichoard;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod testmod;
|
56
src/core/musichoard/mod.rs
Normal file
56
src/core/musichoard/mod.rs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
//! The core MusicHoard module. Serves as the main entry-point into the library.
|
||||||
|
|
||||||
|
#![allow(clippy::module_inception)]
|
||||||
|
pub mod musichoard;
|
||||||
|
pub mod musichoard_builder;
|
||||||
|
|
||||||
|
use std::fmt::{self, Display};
|
||||||
|
|
||||||
|
use crate::core::{collection, database, library};
|
||||||
|
|
||||||
|
/// Error type for `musichoard`.
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum Error {
|
||||||
|
/// The [`MusicHoard`] is not able to read/write its in-memory collection.
|
||||||
|
CollectionError(String),
|
||||||
|
/// The [`MusicHoard`] failed to read/write from/to the library.
|
||||||
|
LibraryError(String),
|
||||||
|
/// The [`MusicHoard`] failed to read/write from/to the database.
|
||||||
|
DatabaseError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Self::CollectionError(ref s) => write!(f, "failed to read/write the collection: {s}"),
|
||||||
|
Self::LibraryError(ref s) => write!(f, "failed to read/write from/to the library: {s}"),
|
||||||
|
Self::DatabaseError(ref s) => {
|
||||||
|
write!(f, "failed to read/write from/to the database: {s}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<collection::Error> for Error {
|
||||||
|
fn from(err: collection::Error) -> Self {
|
||||||
|
Error::CollectionError(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<library::Error> for Error {
|
||||||
|
fn from(err: library::Error) -> Error {
|
||||||
|
Error::LibraryError(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<database::LoadError> for Error {
|
||||||
|
fn from(err: database::LoadError) -> Error {
|
||||||
|
Error::DatabaseError(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<database::SaveError> for Error {
|
||||||
|
fn from(err: database::SaveError) -> Error {
|
||||||
|
Error::DatabaseError(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
1053
src/core/musichoard/musichoard.rs
Normal file
1053
src/core/musichoard/musichoard.rs
Normal file
File diff suppressed because it is too large
Load Diff
92
src/core/musichoard/musichoard_builder.rs
Normal file
92
src/core/musichoard/musichoard_builder.rs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
use crate::core::{
|
||||||
|
database::IDatabase,
|
||||||
|
library::ILibrary,
|
||||||
|
musichoard::musichoard::{MusicHoard, NoDatabase, NoLibrary},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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)]
|
||||||
|
mod tests {
|
||||||
|
use crate::core::{database::NullDatabase, library::NullLibrary};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_library_no_database() {
|
||||||
|
MusicHoardBuilder::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn with_library_no_database() {
|
||||||
|
let mut mh = MusicHoardBuilder::default()
|
||||||
|
.set_library(NullLibrary)
|
||||||
|
.build();
|
||||||
|
assert!(mh.rescan_library().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_library_with_database() {
|
||||||
|
let mut mh = MusicHoardBuilder::default()
|
||||||
|
.set_database(NullDatabase)
|
||||||
|
.build();
|
||||||
|
assert!(mh.load_from_database().is_ok());
|
||||||
|
assert!(mh.save_to_database().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn with_library_with_database() {
|
||||||
|
let mut mh = MusicHoardBuilder::default()
|
||||||
|
.set_library(NullLibrary)
|
||||||
|
.set_database(NullDatabase)
|
||||||
|
.build();
|
||||||
|
assert!(mh.rescan_library().is_ok());
|
||||||
|
assert!(mh.load_from_database().is_ok());
|
||||||
|
assert!(mh.save_to_database().is_ok());
|
||||||
|
}
|
||||||
|
}
|
11
src/core/testmod.rs
Normal file
11
src/core/testmod.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use crate::core::collection::{
|
||||||
|
album::{Album, AlbumId},
|
||||||
|
artist::{Artist, ArtistId, ArtistProperties, Bandcamp, MusicBrainz, MusicButler, Qobuz},
|
||||||
|
track::{Format, Quality, Track, TrackId},
|
||||||
|
};
|
||||||
|
use crate::tests::*;
|
||||||
|
|
||||||
|
pub static LIBRARY_COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| library_collection!());
|
||||||
|
pub static FULL_COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| full_collection!());
|
2296
src/lib.rs
2296
src/lib.rs
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,5 @@
|
|||||||
|
mod tui;
|
||||||
|
|
||||||
use std::{ffi::OsString, fs::OpenOptions, io, path::PathBuf};
|
use std::{ffi::OsString, fs::OpenOptions, io, path::PathBuf};
|
||||||
|
|
||||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||||
@ -18,7 +20,6 @@ use musichoard::{
|
|||||||
MusicHoardBuilder, NoDatabase, NoLibrary,
|
MusicHoardBuilder, NoDatabase, NoLibrary,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod tui;
|
|
||||||
use tui::{event::EventChannel, handler::EventHandler, listener::EventListener, ui::Ui, Tui};
|
use tui::{event::EventChannel, handler::EventHandler, listener::EventListener, ui::Ui, Tui};
|
||||||
|
|
||||||
#[derive(StructOpt)]
|
#[derive(StructOpt)]
|
||||||
@ -128,7 +129,4 @@ fn main() {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod testmacros;
|
mod tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod testbin;
|
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
use musichoard::{
|
|
||||||
Album, AlbumId, Artist, ArtistId, ArtistProperties, Bandcamp, Format, MusicBrainz, MusicButler,
|
|
||||||
Qobuz, Quality, Track, TrackId,
|
|
||||||
};
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
|
|
||||||
pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| full_collection!());
|
|
@ -1,8 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
Album, AlbumId, Artist, ArtistId, ArtistProperties, Bandcamp, Format, MusicBrainz, MusicButler,
|
|
||||||
Qobuz, Quality, Track, TrackId,
|
|
||||||
};
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
|
|
||||||
pub static LIBRARY_COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| library_collection!());
|
|
||||||
pub static FULL_COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| full_collection!());
|
|
@ -321,3 +321,6 @@ macro_rules! full_collection {
|
|||||||
collection
|
collection
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) use full_collection;
|
||||||
|
pub(crate) use library_collection;
|
@ -2,7 +2,7 @@ use crossterm::event::{KeyEvent, MouseEvent};
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
|
||||||
use super::ui::UiError;
|
use crate::tui::ui::UiError;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum EventError {
|
pub enum EventError {
|
||||||
@ -104,7 +104,7 @@ mod tests {
|
|||||||
|
|
||||||
use crate::tui::ui::UiError;
|
use crate::tui::ui::UiError;
|
||||||
|
|
||||||
use super::{Event, EventChannel, EventError};
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn event_sender() {
|
fn event_sender() {
|
||||||
|
@ -3,7 +3,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
|
|
||||||
use super::{
|
use crate::tui::{
|
||||||
event::{Event, EventError, EventReceiver},
|
event::{Event, EventError, EventReceiver},
|
||||||
ui::IUi,
|
ui::IUi,
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use musichoard::{database::IDatabase, library::ILibrary, Collection, MusicHoard};
|
use musichoard::{collection::Collection, database::IDatabase, library::ILibrary, MusicHoard};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
|
@ -157,24 +157,24 @@ impl<B: Backend, UI: IUi> Tui<B, UI> {
|
|||||||
// GRCOV_EXCL_STOP
|
// GRCOV_EXCL_STOP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod testmod;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{io, thread};
|
use std::{io, thread};
|
||||||
|
|
||||||
use musichoard::Collection;
|
|
||||||
use ratatui::{backend::TestBackend, Terminal};
|
use ratatui::{backend::TestBackend, Terminal};
|
||||||
|
|
||||||
use crate::testbin::COLLECTION;
|
use musichoard::collection::Collection;
|
||||||
|
|
||||||
use super::{
|
use crate::tui::{
|
||||||
event::EventError,
|
handler::MockIEventHandler, lib::MockIMusicHoard, listener::MockIEventListener, ui::Ui,
|
||||||
handler::MockIEventHandler,
|
|
||||||
lib::MockIMusicHoard,
|
|
||||||
listener::MockIEventListener,
|
|
||||||
ui::{IUi, Ui},
|
|
||||||
Error, Tui,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use testmod::COLLECTION;
|
||||||
|
|
||||||
pub fn terminal() -> Terminal<TestBackend> {
|
pub fn terminal() -> Terminal<TestBackend> {
|
||||||
let backend = TestBackend::new(150, 30);
|
let backend = TestBackend::new(150, 30);
|
||||||
Terminal::new(backend).unwrap()
|
Terminal::new(backend).unwrap()
|
||||||
|
10
src/tui/testmod.rs
Normal file
10
src/tui/testmod.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
use musichoard::collection::{
|
||||||
|
album::{Album, AlbumId},
|
||||||
|
artist::{Artist, ArtistId, ArtistProperties, Bandcamp, MusicBrainz, MusicButler, Qobuz},
|
||||||
|
track::{Format, Quality, Track, TrackId},
|
||||||
|
};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use crate::tests::*;
|
||||||
|
|
||||||
|
pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| full_collection!());
|
@ -1,6 +1,11 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use musichoard::{Album, Artist, Collection, Format, Track};
|
use musichoard::collection::{
|
||||||
|
album::Album,
|
||||||
|
artist::Artist,
|
||||||
|
track::{Format, Track},
|
||||||
|
Collection,
|
||||||
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
layout::{Alignment, Rect},
|
layout::{Alignment, Rect},
|
||||||
@ -9,7 +14,7 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{lib::IMusicHoard, Error};
|
use crate::tui::{lib::IMusicHoard, Error};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum UiError {
|
pub enum UiError {
|
||||||
@ -733,8 +738,8 @@ impl<MH: IMusicHoard> IUi for Ui<MH> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::testbin::COLLECTION;
|
|
||||||
use crate::tui::lib::MockIMusicHoard;
|
use crate::tui::lib::MockIMusicHoard;
|
||||||
|
use crate::tui::testmod::COLLECTION;
|
||||||
use crate::tui::tests::{terminal, ui};
|
use crate::tui::tests::{terminal, ui};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
use std::{fs, path::PathBuf};
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
use musichoard::{
|
use musichoard::{
|
||||||
|
collection::artist::Artist,
|
||||||
database::{
|
database::{
|
||||||
json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
||||||
IDatabase,
|
IDatabase,
|
||||||
},
|
},
|
||||||
Artist,
|
|
||||||
};
|
};
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use tempfile::NamedTempFile;
|
|
||||||
|
|
||||||
use crate::testlib::COLLECTION;
|
use crate::testlib::COLLECTION;
|
||||||
|
|
||||||
|
@ -5,14 +5,14 @@ mod testlib;
|
|||||||
|
|
||||||
use musichoard::MusicHoard;
|
use musichoard::MusicHoard;
|
||||||
|
|
||||||
use crate::testlib::COLLECTION;
|
|
||||||
|
|
||||||
#[cfg(feature = "database-json")]
|
#[cfg(feature = "database-json")]
|
||||||
use musichoard::database::json::{backend::JsonDatabaseFileBackend, JsonDatabase};
|
use musichoard::database::json::{backend::JsonDatabaseFileBackend, JsonDatabase};
|
||||||
|
|
||||||
#[cfg(feature = "library-beets")]
|
#[cfg(feature = "library-beets")]
|
||||||
use musichoard::library::beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary};
|
use musichoard::library::beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary};
|
||||||
|
|
||||||
|
use crate::testlib::COLLECTION;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "database-json")]
|
#[cfg(feature = "database-json")]
|
||||||
#[cfg(feature = "library-beets")]
|
#[cfg(feature = "library-beets")]
|
||||||
|
@ -12,7 +12,7 @@ use musichoard::library::{
|
|||||||
Field, ILibrary, Item, Query,
|
Field, ILibrary, Item, Query,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::testmod::LIBRARY_ITEMS;
|
use crate::library::testmod::LIBRARY_ITEMS;
|
||||||
|
|
||||||
pub static BEETS_TEST_CONFIG_PATH: Lazy<PathBuf> =
|
pub static BEETS_TEST_CONFIG_PATH: Lazy<PathBuf> =
|
||||||
Lazy::new(|| fs::canonicalize("./tests/files/library/config.yml").unwrap());
|
Lazy::new(|| fs::canonicalize("./tests/files/library/config.yml").unwrap());
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
mod testmod;
|
|
||||||
|
|
||||||
#[cfg(feature = "library-beets")]
|
#[cfg(feature = "library-beets")]
|
||||||
pub mod beets;
|
pub mod beets;
|
||||||
|
|
||||||
|
mod testmod;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
use musichoard::{library::Item, Format};
|
use musichoard::{collection::track::Format, library::Item};
|
||||||
|
|
||||||
pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
|
pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
|
||||||
vec![
|
vec![
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
use musichoard::{
|
|
||||||
Album, AlbumId, Artist, ArtistId, ArtistProperties, Bandcamp, Collection, Format, MusicBrainz,
|
|
||||||
MusicButler, Qobuz, Quality, Track, TrackId,
|
|
||||||
};
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use musichoard::collection::{
|
||||||
|
album::{Album, AlbumId},
|
||||||
|
artist::{Artist, ArtistId, ArtistProperties, Bandcamp, MusicBrainz, MusicButler, Qobuz},
|
||||||
|
track::{Format, Quality, Track, TrackId},
|
||||||
|
Collection,
|
||||||
|
};
|
||||||
|
|
||||||
pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
|
pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
|
||||||
vec![
|
vec![
|
||||||
Artist {
|
Artist {
|
||||||
|
Loading…
Reference in New Issue
Block a user