Clean up interfaces #62
@ -17,7 +17,7 @@ env CARGO_TARGET_DIR=codecov \
|
|||||||
env RUSTFLAGS="-C instrument-coverage" \
|
env RUSTFLAGS="-C instrument-coverage" \
|
||||||
LLVM_PROFILE_FILE="codecov/debug/profraw/musichoard-%p-%m.profraw" \
|
LLVM_PROFILE_FILE="codecov/debug/profraw/musichoard-%p-%m.profraw" \
|
||||||
CARGO_TARGET_DIR=codecov \
|
CARGO_TARGET_DIR=codecov \
|
||||||
cargo test --all-features
|
cargo test --all-features --all-targets
|
||||||
grcov codecov/debug/profraw \
|
grcov codecov/debug/profraw \
|
||||||
--binary-path ./codecov/debug/ \
|
--binary-path ./codecov/debug/ \
|
||||||
--output-types html \
|
--output-types html \
|
||||||
|
@ -1,178 +0,0 @@
|
|||||||
//! Module for managing the music collection, i.e. "The Music Hoard".
|
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
database::{self, Database},
|
|
||||||
library::{self, Library, Query},
|
|
||||||
Artist,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// The collection type.
|
|
||||||
pub type Collection = Vec<Artist>;
|
|
||||||
|
|
||||||
/// Error type for collection manager.
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub enum Error {
|
|
||||||
/// The [`CollectionManager`] failed to read/write from/to the library.
|
|
||||||
LibraryError(String),
|
|
||||||
/// The [`CollectionManager`] failed to read/write from/to the database.
|
|
||||||
DatabaseError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
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<library::Error> for Error {
|
|
||||||
fn from(err: library::Error) -> Error {
|
|
||||||
Error::LibraryError(err.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<database::Error> for Error {
|
|
||||||
fn from(err: database::Error) -> Error {
|
|
||||||
Error::DatabaseError(err.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait CollectionManager {
|
|
||||||
/// Rescan the library and integrate any updates into the collection.
|
|
||||||
fn rescan_library(&mut self) -> Result<(), Error>;
|
|
||||||
|
|
||||||
/// Save the collection state to the database.
|
|
||||||
fn save_to_database(&mut self) -> Result<(), Error>;
|
|
||||||
|
|
||||||
/// Get the current collection.
|
|
||||||
fn get_collection(&self) -> &Collection;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The collection manager. It is responsible for pulling information from both the library and the
|
|
||||||
/// database, ensuring its consistent and writing back any changes.
|
|
||||||
pub struct MhCollectionManager<LIB, DB> {
|
|
||||||
library: LIB,
|
|
||||||
database: DB,
|
|
||||||
collection: Collection,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<LIB: Library, DB: Database> MhCollectionManager<LIB, DB> {
|
|
||||||
/// Create a new [`CollectionManager`] with the provided [`Library`] and [`Database`].
|
|
||||||
pub fn new(library: LIB, database: DB) -> Self {
|
|
||||||
MhCollectionManager {
|
|
||||||
library,
|
|
||||||
database,
|
|
||||||
collection: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<LIB: Library, DB: Database> CollectionManager for MhCollectionManager<LIB, DB> {
|
|
||||||
fn rescan_library(&mut self) -> Result<(), Error> {
|
|
||||||
self.collection = self.library.list(&Query::new())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save_to_database(&mut self) -> Result<(), Error> {
|
|
||||||
self.database.write(&self.collection)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_collection(&self) -> &Collection {
|
|
||||||
&self.collection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use mockall::predicate;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
collection::Collection,
|
|
||||||
database::{self, MockDatabase},
|
|
||||||
library::{self, MockLibrary, Query},
|
|
||||||
tests::COLLECTION,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{CollectionManager, Error, MhCollectionManager};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn read_get_write() {
|
|
||||||
let mut library = MockLibrary::new();
|
|
||||||
let mut database = MockDatabase::new();
|
|
||||||
|
|
||||||
let library_input = Query::new();
|
|
||||||
let library_result = Ok(COLLECTION.to_owned());
|
|
||||||
|
|
||||||
let database_input = COLLECTION.to_owned();
|
|
||||||
let database_result = Ok(());
|
|
||||||
|
|
||||||
library
|
|
||||||
.expect_list()
|
|
||||||
.with(predicate::eq(library_input))
|
|
||||||
.times(1)
|
|
||||||
.return_once(|_| library_result);
|
|
||||||
|
|
||||||
database
|
|
||||||
.expect_write()
|
|
||||||
.with(predicate::eq(database_input))
|
|
||||||
.times(1)
|
|
||||||
.return_once(|_: &Collection| database_result);
|
|
||||||
|
|
||||||
let mut collection_manager = MhCollectionManager::new(library, database);
|
|
||||||
|
|
||||||
collection_manager.rescan_library().unwrap();
|
|
||||||
assert_eq!(collection_manager.get_collection(), &*COLLECTION);
|
|
||||||
collection_manager.save_to_database().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn library_error() {
|
|
||||||
let mut library = MockLibrary::new();
|
|
||||||
let database = MockDatabase::new();
|
|
||||||
|
|
||||||
let library_result = Err(library::Error::Invalid(String::from("invalid data")));
|
|
||||||
|
|
||||||
library
|
|
||||||
.expect_list()
|
|
||||||
.times(1)
|
|
||||||
.return_once(|_| library_result);
|
|
||||||
|
|
||||||
let mut collection_manager = MhCollectionManager::new(library, database);
|
|
||||||
|
|
||||||
let actual_err = collection_manager.rescan_library().unwrap_err();
|
|
||||||
let expected_err =
|
|
||||||
Error::LibraryError(library::Error::Invalid(String::from("invalid data")).to_string());
|
|
||||||
|
|
||||||
assert_eq!(actual_err, expected_err);
|
|
||||||
assert_eq!(actual_err.to_string(), expected_err.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn database_error() {
|
|
||||||
let library = MockLibrary::new();
|
|
||||||
let mut database = MockDatabase::new();
|
|
||||||
|
|
||||||
let database_result = Err(database::Error::IoError(String::from("I/O error")));
|
|
||||||
|
|
||||||
database
|
|
||||||
.expect_write()
|
|
||||||
.times(1)
|
|
||||||
.return_once(|_: &Collection| database_result);
|
|
||||||
|
|
||||||
let mut collection_manager = MhCollectionManager::new(library, database);
|
|
||||||
|
|
||||||
let actual_err = collection_manager.save_to_database().unwrap_err();
|
|
||||||
let expected_err =
|
|
||||||
Error::DatabaseError(database::Error::IoError(String::from("I/O error")).to_string());
|
|
||||||
|
|
||||||
assert_eq!(actual_err, expected_err);
|
|
||||||
assert_eq!(actual_err.to_string(), expected_err.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,7 +3,7 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use super::JsonDatabaseBackend;
|
use super::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 {
|
||||||
@ -17,7 +17,7 @@ impl JsonDatabaseFileBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JsonDatabaseBackend for JsonDatabaseFileBackend {
|
impl IJsonDatabaseBackend for JsonDatabaseFileBackend {
|
||||||
fn read(&self) -> Result<String, std::io::Error> {
|
fn read(&self) -> Result<String, std::io::Error> {
|
||||||
// Read entire file to memory as for now this is faster than a buffered read from disk:
|
// Read entire file to memory as for now this is faster than a buffered read from disk:
|
||||||
// https://github.com/serde-rs/json/issues/160
|
// https://github.com/serde-rs/json/issues/160
|
||||||
|
@ -6,7 +6,7 @@ use serde::Serialize;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
|
|
||||||
use super::{Database, Error};
|
use super::{Error, IDatabase};
|
||||||
|
|
||||||
pub mod backend;
|
pub mod backend;
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ impl From<serde_json::Error> for Error {
|
|||||||
|
|
||||||
/// Trait for the JSON database backend.
|
/// Trait for the JSON database backend.
|
||||||
#[cfg_attr(test, automock)]
|
#[cfg_attr(test, automock)]
|
||||||
pub trait JsonDatabaseBackend {
|
pub trait IJsonDatabaseBackend {
|
||||||
/// Read the JSON string from the backend.
|
/// Read the JSON string from the backend.
|
||||||
fn read(&self) -> Result<String, std::io::Error>;
|
fn read(&self) -> Result<String, std::io::Error>;
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ pub struct JsonDatabase<JDB> {
|
|||||||
backend: JDB,
|
backend: JDB,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<JDB: JsonDatabaseBackend> JsonDatabase<JDB> {
|
impl<JDB: IJsonDatabaseBackend> JsonDatabase<JDB> {
|
||||||
/// Create a new JSON database with the provided backend, e.g.
|
/// Create a new JSON database with the provided backend, e.g.
|
||||||
/// [`backend::JsonDatabaseFileBackend`].
|
/// [`backend::JsonDatabaseFileBackend`].
|
||||||
pub fn new(backend: JDB) -> Self {
|
pub fn new(backend: JDB) -> Self {
|
||||||
@ -39,7 +39,7 @@ impl<JDB: JsonDatabaseBackend> JsonDatabase<JDB> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<JDB: JsonDatabaseBackend> Database for JsonDatabase<JDB> {
|
impl<JDB: IJsonDatabaseBackend> IDatabase for JsonDatabase<JDB> {
|
||||||
fn read<D: DeserializeOwned>(&self, collection: &mut D) -> Result<(), Error> {
|
fn read<D: DeserializeOwned>(&self, collection: &mut D) -> Result<(), Error> {
|
||||||
let serialized = self.backend.read()?;
|
let serialized = self.backend.read()?;
|
||||||
*collection = serde_json::from_str(&serialized)?;
|
*collection = serde_json::from_str(&serialized)?;
|
||||||
@ -132,7 +132,7 @@ mod tests {
|
|||||||
let write_data = COLLECTION.to_owned();
|
let write_data = COLLECTION.to_owned();
|
||||||
let input = artists_to_json(&write_data);
|
let input = artists_to_json(&write_data);
|
||||||
|
|
||||||
let mut backend = MockJsonDatabaseBackend::new();
|
let mut backend = MockIJsonDatabaseBackend::new();
|
||||||
backend
|
backend
|
||||||
.expect_write()
|
.expect_write()
|
||||||
.with(predicate::eq(input))
|
.with(predicate::eq(input))
|
||||||
@ -147,7 +147,7 @@ mod tests {
|
|||||||
let expected = COLLECTION.to_owned();
|
let expected = COLLECTION.to_owned();
|
||||||
let result = Ok(artists_to_json(&expected));
|
let result = Ok(artists_to_json(&expected));
|
||||||
|
|
||||||
let mut backend = MockJsonDatabaseBackend::new();
|
let mut backend = MockIJsonDatabaseBackend::new();
|
||||||
backend.expect_read().times(1).return_once(|| result);
|
backend.expect_read().times(1).return_once(|| result);
|
||||||
|
|
||||||
let mut read_data: Vec<Artist> = vec![];
|
let mut read_data: Vec<Artist> = vec![];
|
||||||
@ -162,7 +162,7 @@ mod tests {
|
|||||||
let input = artists_to_json(&expected);
|
let input = artists_to_json(&expected);
|
||||||
let result = Ok(input.clone());
|
let result = Ok(input.clone());
|
||||||
|
|
||||||
let mut backend = MockJsonDatabaseBackend::new();
|
let mut backend = MockIJsonDatabaseBackend::new();
|
||||||
backend
|
backend
|
||||||
.expect_write()
|
.expect_write()
|
||||||
.with(predicate::eq(input))
|
.with(predicate::eq(input))
|
||||||
|
@ -10,6 +10,16 @@ use mockall::automock;
|
|||||||
#[cfg(feature = "database-json")]
|
#[cfg(feature = "database-json")]
|
||||||
pub mod json;
|
pub mod json;
|
||||||
|
|
||||||
|
/// Trait for interacting with the database.
|
||||||
|
#[cfg_attr(test, automock)]
|
||||||
|
pub trait IDatabase {
|
||||||
|
/// Read collection from the database.
|
||||||
|
fn read<D: DeserializeOwned + 'static>(&self, collection: &mut D) -> Result<(), Error>;
|
||||||
|
|
||||||
|
/// Write collection to the database.
|
||||||
|
fn write<S: Serialize + 'static>(&mut self, collection: &S) -> Result<(), Error>;
|
||||||
|
}
|
||||||
|
|
||||||
/// Error type for database calls.
|
/// Error type for database calls.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
@ -36,16 +46,6 @@ impl From<std::io::Error> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait for interacting with the database.
|
|
||||||
#[cfg_attr(test, automock)]
|
|
||||||
pub trait Database {
|
|
||||||
/// Read collection from the database.
|
|
||||||
fn read<D: DeserializeOwned + 'static>(&self, collection: &mut D) -> Result<(), Error>;
|
|
||||||
|
|
||||||
/// Write collection to the database.
|
|
||||||
fn write<S: Serialize + 'static>(&mut self, collection: &S) -> Result<(), Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::io;
|
use std::io;
|
||||||
|
156
src/lib.rs
156
src/lib.rs
@ -1,12 +1,15 @@
|
|||||||
//! MusicHoard - a music collection manager.
|
//! MusicHoard - a music collection manager.
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
pub mod collection;
|
|
||||||
pub mod database;
|
pub mod database;
|
||||||
pub mod library;
|
pub mod library;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use database::IDatabase;
|
||||||
|
use library::{ILibrary, Query};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID).
|
/// [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID).
|
||||||
pub type Mbid = Uuid;
|
pub type Mbid = Uuid;
|
||||||
|
|
||||||
@ -53,15 +56,160 @@ pub struct Artist {
|
|||||||
pub albums: Vec<Album>,
|
pub albums: Vec<Album>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The collection type. Currently, a collection is a list of artists.
|
||||||
|
pub type Collection = Vec<Artist>;
|
||||||
|
|
||||||
|
/// Error type for `musichoard`.
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum Error {
|
||||||
|
/// 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 fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
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<library::Error> for Error {
|
||||||
|
fn from(err: library::Error) -> Error {
|
||||||
|
Error::LibraryError(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<database::Error> for Error {
|
||||||
|
fn from(err: database::Error) -> Error {
|
||||||
|
Error::DatabaseError(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
|
library: LIB,
|
||||||
|
database: DB,
|
||||||
|
collection: Collection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<LIB: ILibrary, DB: IDatabase> MusicHoard<LIB, DB> {
|
||||||
|
/// Create a new [`MusicHoard`] with the provided [`ILibrary`] and [`IDatabase`].
|
||||||
|
pub fn new(library: LIB, database: DB) -> Self {
|
||||||
|
MusicHoard {
|
||||||
|
library,
|
||||||
|
database,
|
||||||
|
collection: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rescan_library(&mut self) -> Result<(), Error> {
|
||||||
|
self.collection = self.library.list(&Query::new())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_to_database(&mut self) -> Result<(), Error> {
|
||||||
|
self.database.write(&self.collection)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_collection(&self) -> &Collection {
|
||||||
|
&self.collection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod testlib;
|
mod testlib;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use mockall::predicate;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
use crate::{database::MockIDatabase, library::MockILibrary};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| collection!());
|
pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| collection!());
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_get_write() {
|
||||||
|
let mut library = MockILibrary::new();
|
||||||
|
let mut database = MockIDatabase::new();
|
||||||
|
|
||||||
|
let library_input = Query::new();
|
||||||
|
let library_result = Ok(COLLECTION.to_owned());
|
||||||
|
|
||||||
|
let database_input = COLLECTION.to_owned();
|
||||||
|
let database_result = Ok(());
|
||||||
|
|
||||||
|
library
|
||||||
|
.expect_list()
|
||||||
|
.with(predicate::eq(library_input))
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_| library_result);
|
||||||
|
|
||||||
|
database
|
||||||
|
.expect_write()
|
||||||
|
.with(predicate::eq(database_input))
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_: &Collection| database_result);
|
||||||
|
|
||||||
|
let mut music_hoard = MusicHoard::new(library, database);
|
||||||
|
|
||||||
|
music_hoard.rescan_library().unwrap();
|
||||||
|
assert_eq!(music_hoard.get_collection(), &*COLLECTION);
|
||||||
|
music_hoard.save_to_database().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn library_error() {
|
||||||
|
let mut library = MockILibrary::new();
|
||||||
|
let database = MockIDatabase::new();
|
||||||
|
|
||||||
|
let library_result = Err(library::Error::Invalid(String::from("invalid data")));
|
||||||
|
|
||||||
|
library
|
||||||
|
.expect_list()
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_| library_result);
|
||||||
|
|
||||||
|
let mut music_hoard = MusicHoard::new(library, database);
|
||||||
|
|
||||||
|
let actual_err = music_hoard.rescan_library().unwrap_err();
|
||||||
|
let expected_err =
|
||||||
|
Error::LibraryError(library::Error::Invalid(String::from("invalid data")).to_string());
|
||||||
|
|
||||||
|
assert_eq!(actual_err, expected_err);
|
||||||
|
assert_eq!(actual_err.to_string(), expected_err.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn database_error() {
|
||||||
|
let library = MockILibrary::new();
|
||||||
|
let mut database = MockIDatabase::new();
|
||||||
|
|
||||||
|
let database_result = Err(database::Error::IoError(String::from("I/O error")));
|
||||||
|
|
||||||
|
database
|
||||||
|
.expect_write()
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_: &Collection| database_result);
|
||||||
|
|
||||||
|
let mut music_hoard = MusicHoard::new(library, database);
|
||||||
|
|
||||||
|
let actual_err = music_hoard.save_to_database().unwrap_err();
|
||||||
|
let expected_err =
|
||||||
|
Error::DatabaseError(database::Error::IoError(String::from("I/O error")).to_string());
|
||||||
|
|
||||||
|
assert_eq!(actual_err, expected_err);
|
||||||
|
assert_eq!(actual_err.to_string(), expected_err.to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,11 +8,11 @@ use std::{
|
|||||||
str,
|
str,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{BeetsLibraryExecutor, Error};
|
use super::{IBeetsLibraryExecutor, Error};
|
||||||
|
|
||||||
const BEET_DEFAULT: &str = "beet";
|
const BEET_DEFAULT: &str = "beet";
|
||||||
|
|
||||||
trait BeetsLibraryExecutorPrivate {
|
trait IBeetsLibraryExecutorPrivate {
|
||||||
fn output(output: Output) -> Result<Vec<String>, Error> {
|
fn output(output: Output) -> Result<Vec<String>, Error> {
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(Error::Executor(
|
return Err(Error::Executor(
|
||||||
@ -59,7 +59,7 @@ impl Default for BeetsLibraryProcessExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BeetsLibraryExecutor for BeetsLibraryProcessExecutor {
|
impl IBeetsLibraryExecutor for BeetsLibraryProcessExecutor {
|
||||||
fn exec<S: AsRef<str> + 'static>(&mut self, arguments: &[S]) -> Result<Vec<String>, Error> {
|
fn exec<S: AsRef<str> + 'static>(&mut self, arguments: &[S]) -> Result<Vec<String>, Error> {
|
||||||
let mut cmd = Command::new(&self.bin);
|
let mut cmd = Command::new(&self.bin);
|
||||||
if let Some(ref path) = self.config {
|
if let Some(ref path) = self.config {
|
||||||
@ -71,7 +71,7 @@ impl BeetsLibraryExecutor for BeetsLibraryProcessExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BeetsLibraryExecutorPrivate for BeetsLibraryProcessExecutor {}
|
impl IBeetsLibraryExecutorPrivate for BeetsLibraryProcessExecutor {}
|
||||||
|
|
||||||
// GRCOV_EXCL_START
|
// GRCOV_EXCL_START
|
||||||
#[cfg(feature = "ssh-library")]
|
#[cfg(feature = "ssh-library")]
|
||||||
@ -128,7 +128,7 @@ pub mod ssh {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BeetsLibraryExecutor for BeetsLibrarySshExecutor {
|
impl IBeetsLibraryExecutor for BeetsLibrarySshExecutor {
|
||||||
fn exec<S: AsRef<str> + 'static>(&mut self, arguments: &[S]) -> Result<Vec<String>, Error> {
|
fn exec<S: AsRef<str> + 'static>(&mut self, arguments: &[S]) -> Result<Vec<String>, Error> {
|
||||||
let mut cmd = self.session.command(&self.bin);
|
let mut cmd = self.session.command(&self.bin);
|
||||||
if let Some(ref path) = self.config {
|
if let Some(ref path) = self.config {
|
||||||
@ -141,6 +141,6 @@ pub mod ssh {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BeetsLibraryExecutorPrivate for BeetsLibrarySshExecutor {}
|
impl IBeetsLibraryExecutorPrivate for BeetsLibrarySshExecutor {}
|
||||||
}
|
}
|
||||||
// GRCOV_EXCL_STOP
|
// GRCOV_EXCL_STOP
|
||||||
|
@ -11,7 +11,7 @@ use mockall::automock;
|
|||||||
|
|
||||||
use crate::{Album, AlbumId, Artist, ArtistId, Track, TrackFormat};
|
use crate::{Album, AlbumId, Artist, ArtistId, Track, TrackFormat};
|
||||||
|
|
||||||
use super::{Error, Field, Library, Query};
|
use super::{Error, Field, ILibrary, Query};
|
||||||
|
|
||||||
pub mod executor;
|
pub mod executor;
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ impl ToBeetsArgs for Query {
|
|||||||
|
|
||||||
/// Trait for invoking beets commands.
|
/// Trait for invoking beets commands.
|
||||||
#[cfg_attr(test, automock)]
|
#[cfg_attr(test, automock)]
|
||||||
pub trait BeetsLibraryExecutor {
|
pub trait IBeetsLibraryExecutor {
|
||||||
/// Invoke beets with the provided arguments.
|
/// Invoke beets with the provided arguments.
|
||||||
fn exec<S: AsRef<str> + 'static>(&mut self, arguments: &[S]) -> Result<Vec<String>, Error>;
|
fn exec<S: AsRef<str> + 'static>(&mut self, arguments: &[S]) -> Result<Vec<String>, Error>;
|
||||||
}
|
}
|
||||||
@ -88,12 +88,12 @@ pub struct BeetsLibrary<BLE> {
|
|||||||
executor: BLE,
|
executor: BLE,
|
||||||
}
|
}
|
||||||
|
|
||||||
trait LibraryPrivate {
|
trait ILibraryPrivate {
|
||||||
fn list_cmd_and_args(query: &Query) -> Vec<String>;
|
fn list_cmd_and_args(query: &Query) -> Vec<String>;
|
||||||
fn list_to_artists<S: AsRef<str>>(list_output: &[S]) -> Result<Vec<Artist>, Error>;
|
fn list_to_artists<S: AsRef<str>>(list_output: &[S]) -> Result<Vec<Artist>, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<BLE: BeetsLibraryExecutor> BeetsLibrary<BLE> {
|
impl<BLE: IBeetsLibraryExecutor> BeetsLibrary<BLE> {
|
||||||
/// Create a new beets library with the provided executor, e.g.
|
/// Create a new beets library with the provided executor, e.g.
|
||||||
/// [`executor::BeetsLibraryProcessExecutor`].
|
/// [`executor::BeetsLibraryProcessExecutor`].
|
||||||
pub fn new(executor: BLE) -> Self {
|
pub fn new(executor: BLE) -> Self {
|
||||||
@ -101,7 +101,7 @@ impl<BLE: BeetsLibraryExecutor> BeetsLibrary<BLE> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<BLE: BeetsLibraryExecutor> Library for BeetsLibrary<BLE> {
|
impl<BLE: IBeetsLibraryExecutor> ILibrary for BeetsLibrary<BLE> {
|
||||||
fn list(&mut self, query: &Query) -> Result<Vec<Artist>, Error> {
|
fn list(&mut self, query: &Query) -> Result<Vec<Artist>, Error> {
|
||||||
let cmd = Self::list_cmd_and_args(query);
|
let cmd = Self::list_cmd_and_args(query);
|
||||||
let output = self.executor.exec(&cmd)?;
|
let output = self.executor.exec(&cmd)?;
|
||||||
@ -109,7 +109,7 @@ impl<BLE: BeetsLibraryExecutor> Library for BeetsLibrary<BLE> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<BLE: BeetsLibraryExecutor> LibraryPrivate for BeetsLibrary<BLE> {
|
impl<BLE: IBeetsLibraryExecutor> ILibraryPrivate for BeetsLibrary<BLE> {
|
||||||
fn list_cmd_and_args(query: &Query) -> Vec<String> {
|
fn list_cmd_and_args(query: &Query) -> Vec<String> {
|
||||||
let mut cmd: Vec<String> = vec![String::from(CMD_LIST)];
|
let mut cmd: Vec<String> = vec![String::from(CMD_LIST)];
|
||||||
cmd.push(LIST_FORMAT_ARG.to_string());
|
cmd.push(LIST_FORMAT_ARG.to_string());
|
||||||
@ -293,7 +293,7 @@ mod tests {
|
|||||||
let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()];
|
let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()];
|
||||||
let result = Ok(vec![]);
|
let result = Ok(vec![]);
|
||||||
|
|
||||||
let mut executor = MockBeetsLibraryExecutor::new();
|
let mut executor = MockIBeetsLibraryExecutor::new();
|
||||||
executor
|
executor
|
||||||
.expect_exec()
|
.expect_exec()
|
||||||
.with(predicate::eq(arguments))
|
.with(predicate::eq(arguments))
|
||||||
@ -313,7 +313,7 @@ mod tests {
|
|||||||
let expected = COLLECTION.to_owned();
|
let expected = COLLECTION.to_owned();
|
||||||
let result = Ok(artists_to_beets_string(&expected));
|
let result = Ok(artists_to_beets_string(&expected));
|
||||||
|
|
||||||
let mut executor = MockBeetsLibraryExecutor::new();
|
let mut executor = MockIBeetsLibraryExecutor::new();
|
||||||
executor
|
executor
|
||||||
.expect_exec()
|
.expect_exec()
|
||||||
.with(predicate::eq(arguments))
|
.with(predicate::eq(arguments))
|
||||||
@ -348,7 +348,7 @@ mod tests {
|
|||||||
// track comes last.
|
// track comes last.
|
||||||
expected[1].albums[0].tracks.rotate_left(1);
|
expected[1].albums[0].tracks.rotate_left(1);
|
||||||
|
|
||||||
let mut executor = MockBeetsLibraryExecutor::new();
|
let mut executor = MockIBeetsLibraryExecutor::new();
|
||||||
executor
|
executor
|
||||||
.expect_exec()
|
.expect_exec()
|
||||||
.with(predicate::eq(arguments))
|
.with(predicate::eq(arguments))
|
||||||
@ -370,7 +370,7 @@ mod tests {
|
|||||||
let output = artists_to_beets_string(&expected);
|
let output = artists_to_beets_string(&expected);
|
||||||
let result = Ok(output);
|
let result = Ok(output);
|
||||||
|
|
||||||
let mut executor = MockBeetsLibraryExecutor::new();
|
let mut executor = MockIBeetsLibraryExecutor::new();
|
||||||
executor
|
executor
|
||||||
.expect_exec()
|
.expect_exec()
|
||||||
.with(predicate::eq(arguments))
|
.with(predicate::eq(arguments))
|
||||||
@ -400,7 +400,7 @@ mod tests {
|
|||||||
];
|
];
|
||||||
let result = Ok(vec![]);
|
let result = Ok(vec![]);
|
||||||
|
|
||||||
let mut executor = MockBeetsLibraryExecutor::new();
|
let mut executor = MockIBeetsLibraryExecutor::new();
|
||||||
executor
|
executor
|
||||||
.expect_exec()
|
.expect_exec()
|
||||||
.with(predicate::function(move |x: &[String]| {
|
.with(predicate::function(move |x: &[String]| {
|
||||||
@ -431,7 +431,7 @@ mod tests {
|
|||||||
output[2] = invalid_string.clone();
|
output[2] = invalid_string.clone();
|
||||||
let result = Ok(output);
|
let result = Ok(output);
|
||||||
|
|
||||||
let mut executor = MockBeetsLibraryExecutor::new();
|
let mut executor = MockIBeetsLibraryExecutor::new();
|
||||||
executor
|
executor
|
||||||
.expect_exec()
|
.expect_exec()
|
||||||
.with(predicate::eq(arguments))
|
.with(predicate::eq(arguments))
|
||||||
@ -462,7 +462,7 @@ mod tests {
|
|||||||
output[2] = invalid_string.clone();
|
output[2] = invalid_string.clone();
|
||||||
let result = Ok(output);
|
let result = Ok(output);
|
||||||
|
|
||||||
let mut executor = MockBeetsLibraryExecutor::new();
|
let mut executor = MockIBeetsLibraryExecutor::new();
|
||||||
executor
|
executor
|
||||||
.expect_exec()
|
.expect_exec()
|
||||||
.with(predicate::eq(arguments))
|
.with(predicate::eq(arguments))
|
||||||
|
@ -10,6 +10,13 @@ use crate::Artist;
|
|||||||
#[cfg(feature = "library-beets")]
|
#[cfg(feature = "library-beets")]
|
||||||
pub mod beets;
|
pub mod beets;
|
||||||
|
|
||||||
|
/// Trait for interacting with the music library.
|
||||||
|
#[cfg_attr(test, automock)]
|
||||||
|
pub trait ILibrary {
|
||||||
|
/// List lirbary items that match the a specific query.
|
||||||
|
fn list(&mut self, query: &Query) -> Result<Vec<Artist>, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
/// Individual fields that can be queried on.
|
/// Individual fields that can be queried on.
|
||||||
#[derive(Debug, Hash, PartialEq, Eq)]
|
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||||
pub enum Field {
|
pub enum Field {
|
||||||
@ -103,13 +110,6 @@ impl From<Utf8Error> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait for interacting with the music library.
|
|
||||||
#[cfg_attr(test, automock)]
|
|
||||||
pub trait Library {
|
|
||||||
/// List lirbary items that match the a specific query.
|
|
||||||
fn list(&mut self, query: &Query) -> Result<Vec<Artist>, Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::io;
|
use std::io;
|
||||||
|
32
src/main.rs
32
src/main.rs
@ -5,23 +5,23 @@ use ratatui::{backend::CrosstermBackend, Terminal};
|
|||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
use musichoard::{
|
use musichoard::{
|
||||||
collection::MhCollectionManager,
|
|
||||||
database::{
|
database::{
|
||||||
json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
||||||
Database,
|
IDatabase,
|
||||||
},
|
},
|
||||||
library::{
|
library::{
|
||||||
beets::{
|
beets::{
|
||||||
executor::{ssh::BeetsLibrarySshExecutor, BeetsLibraryProcessExecutor},
|
executor::{ssh::BeetsLibrarySshExecutor, BeetsLibraryProcessExecutor},
|
||||||
BeetsLibrary,
|
BeetsLibrary,
|
||||||
},
|
},
|
||||||
Library,
|
ILibrary,
|
||||||
},
|
},
|
||||||
|
MusicHoard,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod tui;
|
mod tui;
|
||||||
use tui::ui::MhUi;
|
use tui::ui::Ui;
|
||||||
use tui::{event::EventChannel, handler::TuiEventHandler, listener::TuiEventListener, Tui};
|
use tui::{event::EventChannel, handler::EventHandler, listener::EventListener, Tui};
|
||||||
|
|
||||||
#[derive(StructOpt)]
|
#[derive(StructOpt)]
|
||||||
struct Opt {
|
struct Opt {
|
||||||
@ -39,18 +39,18 @@ struct Opt {
|
|||||||
database_file_path: PathBuf,
|
database_file_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with<LIB: Library, DB: Database>(lib: LIB, db: DB) {
|
fn with<LIB: ILibrary, DB: IDatabase>(lib: LIB, db: DB) {
|
||||||
let collection_manager = MhCollectionManager::new(lib, db);
|
let music_hoard = MusicHoard::new(lib, db);
|
||||||
|
|
||||||
// Initialize the terminal user interface.
|
// Initialize the terminal user interface.
|
||||||
let backend = CrosstermBackend::new(io::stdout());
|
let backend = CrosstermBackend::new(io::stdout());
|
||||||
let terminal = Terminal::new(backend).expect("failed to initialise terminal");
|
let terminal = Terminal::new(backend).expect("failed to initialise terminal");
|
||||||
|
|
||||||
let channel = EventChannel::new();
|
let channel = EventChannel::new();
|
||||||
let listener = TuiEventListener::new(channel.sender());
|
let listener = EventListener::new(channel.sender());
|
||||||
let handler = TuiEventHandler::new(channel.receiver());
|
let handler = EventHandler::new(channel.receiver());
|
||||||
|
|
||||||
let ui = MhUi::new(collection_manager).expect("failed to initialise ui");
|
let ui = Ui::new(music_hoard).expect("failed to initialise ui");
|
||||||
|
|
||||||
// Run the TUI application.
|
// Run the TUI application.
|
||||||
Tui::run(terminal, ui, handler, listener).expect("failed to run tui");
|
Tui::run(terminal, ui, handler, listener).expect("failed to run tui");
|
||||||
@ -85,21 +85,9 @@ mod testlib;
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use mockall::mock;
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
use musichoard::collection::{self, Collection, CollectionManager};
|
|
||||||
use musichoard::*;
|
use musichoard::*;
|
||||||
|
|
||||||
pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| collection!());
|
pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| collection!());
|
||||||
|
|
||||||
mock! {
|
|
||||||
pub CollectionManager {}
|
|
||||||
|
|
||||||
impl CollectionManager for CollectionManager {
|
|
||||||
fn rescan_library(&mut self) -> Result<(), collection::Error>;
|
|
||||||
fn save_to_database(&mut self) -> Result<(), collection::Error>;
|
|
||||||
fn get_collection(&self) -> &Collection;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,30 +5,30 @@ use mockall::automock;
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
event::{Event, EventError, EventReceiver},
|
event::{Event, EventError, EventReceiver},
|
||||||
ui::Ui,
|
ui::IUi,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg_attr(test, automock)]
|
#[cfg_attr(test, automock)]
|
||||||
pub trait EventHandler<UI> {
|
pub trait IEventHandler<UI> {
|
||||||
fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError>;
|
fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
trait EventHandlerPrivate<UI> {
|
trait IEventHandlerPrivate<UI> {
|
||||||
fn handle_key_event(ui: &mut UI, key_event: KeyEvent);
|
fn handle_key_event(ui: &mut UI, key_event: KeyEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TuiEventHandler {
|
pub struct EventHandler {
|
||||||
events: EventReceiver,
|
events: EventReceiver,
|
||||||
}
|
}
|
||||||
|
|
||||||
// GRCOV_EXCL_START
|
// GRCOV_EXCL_START
|
||||||
impl TuiEventHandler {
|
impl EventHandler {
|
||||||
pub fn new(events: EventReceiver) -> Self {
|
pub fn new(events: EventReceiver) -> Self {
|
||||||
TuiEventHandler { events }
|
EventHandler { events }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<UI: Ui> EventHandler<UI> for TuiEventHandler {
|
impl<UI: IUi> IEventHandler<UI> for EventHandler {
|
||||||
fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError> {
|
fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError> {
|
||||||
match self.events.recv()? {
|
match self.events.recv()? {
|
||||||
Event::Key(key_event) => Self::handle_key_event(ui, key_event),
|
Event::Key(key_event) => Self::handle_key_event(ui, key_event),
|
||||||
@ -39,7 +39,7 @@ impl<UI: Ui> EventHandler<UI> for TuiEventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<UI: Ui> EventHandlerPrivate<UI> for TuiEventHandler {
|
impl<UI: IUi> IEventHandlerPrivate<UI> for EventHandler {
|
||||||
fn handle_key_event(ui: &mut UI, key_event: KeyEvent) {
|
fn handle_key_event(ui: &mut UI, key_event: KeyEvent) {
|
||||||
match key_event.code {
|
match key_event.code {
|
||||||
// Exit application on `ESC` or `q`.
|
// Exit application on `ESC` or `q`.
|
||||||
|
30
src/tui/lib.rs
Normal file
30
src/tui/lib.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
use musichoard::{database::IDatabase, library::ILibrary, Collection, MusicHoard};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use mockall::automock;
|
||||||
|
|
||||||
|
use super::Error;
|
||||||
|
|
||||||
|
#[cfg_attr(test, automock)]
|
||||||
|
pub trait IMusicHoard {
|
||||||
|
fn rescan_library(&mut self) -> Result<(), Error>;
|
||||||
|
fn get_collection(&self) -> &Collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<musichoard::Error> for Error {
|
||||||
|
fn from(err: musichoard::Error) -> Error {
|
||||||
|
Error::Lib(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GRCOV_EXCL_START
|
||||||
|
impl<LIB: ILibrary, DB: IDatabase> IMusicHoard for MusicHoard<LIB, DB> {
|
||||||
|
fn rescan_library(&mut self) -> Result<(), Error> {
|
||||||
|
Ok(MusicHoard::rescan_library(self)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_collection(&self) -> &Collection {
|
||||||
|
MusicHoard::get_collection(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// GRCOV_EXCL_STOP
|
@ -7,22 +7,22 @@ use mockall::automock;
|
|||||||
use super::event::{Event, EventError, EventSender};
|
use super::event::{Event, EventError, EventSender};
|
||||||
|
|
||||||
#[cfg_attr(test, automock)]
|
#[cfg_attr(test, automock)]
|
||||||
pub trait EventListener {
|
pub trait IEventListener {
|
||||||
fn spawn(self) -> thread::JoinHandle<EventError>;
|
fn spawn(self) -> thread::JoinHandle<EventError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TuiEventListener {
|
pub struct EventListener {
|
||||||
events: EventSender,
|
events: EventSender,
|
||||||
}
|
}
|
||||||
|
|
||||||
// GRCOV_EXCL_START
|
// GRCOV_EXCL_START
|
||||||
impl TuiEventListener {
|
impl EventListener {
|
||||||
pub fn new(events: EventSender) -> Self {
|
pub fn new(events: EventSender) -> Self {
|
||||||
TuiEventListener { events }
|
EventListener { events }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventListener for TuiEventListener {
|
impl IEventListener for EventListener {
|
||||||
fn spawn(self) -> thread::JoinHandle<EventError> {
|
fn spawn(self) -> thread::JoinHandle<EventError> {
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
loop {
|
loop {
|
||||||
|
@ -1,35 +1,30 @@
|
|||||||
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
|
||||||
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
|
|
||||||
use musichoard::collection;
|
|
||||||
use ratatui::backend::Backend;
|
|
||||||
use ratatui::Terminal;
|
|
||||||
use std::io;
|
|
||||||
use std::marker::PhantomData;
|
|
||||||
|
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod handler;
|
pub mod handler;
|
||||||
pub mod listener;
|
pub mod listener;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
|
mod lib;
|
||||||
|
|
||||||
|
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
||||||
|
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
|
||||||
|
use ratatui::backend::Backend;
|
||||||
|
use ratatui::Terminal;
|
||||||
|
use std::io;
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
use self::event::EventError;
|
use self::event::EventError;
|
||||||
use self::handler::EventHandler;
|
use self::handler::IEventHandler;
|
||||||
use self::listener::EventListener;
|
use self::listener::IEventListener;
|
||||||
use self::ui::Ui;
|
use self::ui::IUi;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Collection(String),
|
Lib(String),
|
||||||
Io(String),
|
Io(String),
|
||||||
Event(String),
|
Event(String),
|
||||||
ListenerPanic,
|
ListenerPanic,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<collection::Error> for Error {
|
|
||||||
fn from(err: collection::Error) -> Error {
|
|
||||||
Error::Collection(err.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<io::Error> for Error {
|
impl From<io::Error> for Error {
|
||||||
fn from(err: io::Error) -> Error {
|
fn from(err: io::Error) -> Error {
|
||||||
Error::Io(err.to_string())
|
Error::Io(err.to_string())
|
||||||
@ -47,7 +42,7 @@ pub struct Tui<B: Backend, UI> {
|
|||||||
_phantom: PhantomData<UI>,
|
_phantom: PhantomData<UI>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B: Backend, UI: Ui> Tui<B, UI> {
|
impl<B: Backend, UI: IUi> Tui<B, UI> {
|
||||||
fn init(&mut self) -> Result<(), Error> {
|
fn init(&mut self) -> Result<(), Error> {
|
||||||
self.terminal.hide_cursor()?;
|
self.terminal.hide_cursor()?;
|
||||||
self.terminal.clear()?;
|
self.terminal.clear()?;
|
||||||
@ -64,7 +59,7 @@ impl<B: Backend, UI: Ui> Tui<B, UI> {
|
|||||||
self.exit();
|
self.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main_loop(&mut self, mut ui: UI, handler: impl EventHandler<UI>) -> Result<(), Error> {
|
fn main_loop(&mut self, mut ui: UI, handler: impl IEventHandler<UI>) -> Result<(), Error> {
|
||||||
while ui.is_running() {
|
while ui.is_running() {
|
||||||
self.terminal.draw(|frame| ui.render(frame))?;
|
self.terminal.draw(|frame| ui.render(frame))?;
|
||||||
handler.handle_next_event(&mut ui)?;
|
handler.handle_next_event(&mut ui)?;
|
||||||
@ -76,8 +71,8 @@ impl<B: Backend, UI: Ui> Tui<B, UI> {
|
|||||||
fn main(
|
fn main(
|
||||||
term: Terminal<B>,
|
term: Terminal<B>,
|
||||||
ui: UI,
|
ui: UI,
|
||||||
handler: impl EventHandler<UI>,
|
handler: impl IEventHandler<UI>,
|
||||||
listener: impl EventListener,
|
listener: impl IEventListener,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut tui = Tui {
|
let mut tui = Tui {
|
||||||
terminal: term,
|
terminal: term,
|
||||||
@ -135,8 +130,8 @@ impl<B: Backend, UI: Ui> Tui<B, UI> {
|
|||||||
pub fn run(
|
pub fn run(
|
||||||
term: Terminal<B>,
|
term: Terminal<B>,
|
||||||
ui: UI,
|
ui: UI,
|
||||||
handler: impl EventHandler<UI>,
|
handler: impl IEventHandler<UI>,
|
||||||
listener: impl EventListener,
|
listener: impl IEventListener,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
Self::enable()?;
|
Self::enable()?;
|
||||||
let result = Self::main(term, ui, handler, listener);
|
let result = Self::main(term, ui, handler, listener);
|
||||||
@ -160,16 +155,17 @@ impl<B: Backend, UI: Ui> Tui<B, UI> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::{io, thread};
|
use std::{io, thread};
|
||||||
|
|
||||||
use musichoard::collection::{self, Collection};
|
use musichoard::Collection;
|
||||||
use ratatui::{backend::TestBackend, Terminal};
|
use ratatui::{backend::TestBackend, Terminal};
|
||||||
|
|
||||||
use crate::tests::{MockCollectionManager, COLLECTION};
|
use crate::tests::COLLECTION;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
event::EventError,
|
event::EventError,
|
||||||
handler::MockEventHandler,
|
handler::MockIEventHandler,
|
||||||
listener::MockEventListener,
|
lib::MockIMusicHoard,
|
||||||
ui::{MhUi, Ui},
|
listener::MockIEventListener,
|
||||||
|
ui::{IUi, Ui},
|
||||||
Error, Tui,
|
Error, Tui,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -178,21 +174,17 @@ mod tests {
|
|||||||
Terminal::new(backend).unwrap()
|
Terminal::new(backend).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(collection: Collection) -> MhUi<MockCollectionManager> {
|
pub fn ui(collection: Collection) -> Ui<MockIMusicHoard> {
|
||||||
let mut collection_manager = MockCollectionManager::new();
|
let mut music_hoard = MockIMusicHoard::new();
|
||||||
|
|
||||||
collection_manager
|
music_hoard.expect_rescan_library().returning(|| Ok(()));
|
||||||
.expect_rescan_library()
|
music_hoard.expect_get_collection().return_const(collection);
|
||||||
.returning(|| Ok(()));
|
|
||||||
collection_manager
|
|
||||||
.expect_get_collection()
|
|
||||||
.return_const(collection);
|
|
||||||
|
|
||||||
MhUi::new(collection_manager).unwrap()
|
Ui::new(music_hoard).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn listener() -> MockEventListener {
|
fn listener() -> MockIEventListener {
|
||||||
let mut listener = MockEventListener::new();
|
let mut listener = MockIEventListener::new();
|
||||||
listener.expect_spawn().return_once(|| {
|
listener.expect_spawn().return_once(|| {
|
||||||
thread::spawn(|| {
|
thread::spawn(|| {
|
||||||
thread::park();
|
thread::park();
|
||||||
@ -202,11 +194,11 @@ mod tests {
|
|||||||
listener
|
listener
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handler() -> MockEventHandler<MhUi<MockCollectionManager>> {
|
fn handler() -> MockIEventHandler<Ui<MockIMusicHoard>> {
|
||||||
let mut handler = MockEventHandler::new();
|
let mut handler = MockIEventHandler::new();
|
||||||
handler
|
handler
|
||||||
.expect_handle_next_event()
|
.expect_handle_next_event()
|
||||||
.return_once(|ui: &mut MhUi<MockCollectionManager>| {
|
.return_once(|ui: &mut Ui<MockIMusicHoard>| {
|
||||||
ui.quit();
|
ui.quit();
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
@ -232,7 +224,7 @@ mod tests {
|
|||||||
|
|
||||||
let listener = listener();
|
let listener = listener();
|
||||||
|
|
||||||
let mut handler = MockEventHandler::new();
|
let mut handler = MockIEventHandler::new();
|
||||||
handler
|
handler
|
||||||
.expect_handle_next_event()
|
.expect_handle_next_event()
|
||||||
.return_once(|_| Err(EventError::Recv));
|
.return_once(|_| Err(EventError::Recv));
|
||||||
@ -254,10 +246,10 @@ mod tests {
|
|||||||
let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| error);
|
let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| error);
|
||||||
while !listener_handle.is_finished() {}
|
while !listener_handle.is_finished() {}
|
||||||
|
|
||||||
let mut listener = MockEventListener::new();
|
let mut listener = MockIEventListener::new();
|
||||||
listener.expect_spawn().return_once(|| listener_handle);
|
listener.expect_spawn().return_once(|| listener_handle);
|
||||||
|
|
||||||
let mut handler = MockEventHandler::new();
|
let mut handler = MockIEventHandler::new();
|
||||||
handler
|
handler
|
||||||
.expect_handle_next_event()
|
.expect_handle_next_event()
|
||||||
.return_once(|_| Err(EventError::Recv));
|
.return_once(|_| Err(EventError::Recv));
|
||||||
@ -277,10 +269,10 @@ mod tests {
|
|||||||
let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| panic!());
|
let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| panic!());
|
||||||
while !listener_handle.is_finished() {}
|
while !listener_handle.is_finished() {}
|
||||||
|
|
||||||
let mut listener = MockEventListener::new();
|
let mut listener = MockIEventListener::new();
|
||||||
listener.expect_spawn().return_once(|| listener_handle);
|
listener.expect_spawn().return_once(|| listener_handle);
|
||||||
|
|
||||||
let mut handler = MockEventHandler::new();
|
let mut handler = MockIEventHandler::new();
|
||||||
handler
|
handler
|
||||||
.expect_handle_next_event()
|
.expect_handle_next_event()
|
||||||
.return_once(|_| Err(EventError::Recv));
|
.return_once(|_| Err(EventError::Recv));
|
||||||
@ -292,12 +284,12 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn errors() {
|
fn errors() {
|
||||||
let collection_err: Error = collection::Error::DatabaseError(String::from("")).into();
|
let lib_err: Error = musichoard::Error::DatabaseError(String::from("")).into();
|
||||||
let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into();
|
let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into();
|
||||||
let event_err: Error = EventError::Recv.into();
|
let event_err: Error = EventError::Recv.into();
|
||||||
let listener_err = Error::ListenerPanic;
|
let listener_err = Error::ListenerPanic;
|
||||||
|
|
||||||
assert!(!format!("{:?}", collection_err).is_empty());
|
assert!(!format!("{:?}", lib_err).is_empty());
|
||||||
assert!(!format!("{:?}", io_err).is_empty());
|
assert!(!format!("{:?}", io_err).is_empty());
|
||||||
assert!(!format!("{:?}", event_err).is_empty());
|
assert!(!format!("{:?}", event_err).is_empty());
|
||||||
assert!(!format!("{:?}", listener_err).is_empty());
|
assert!(!format!("{:?}", listener_err).is_empty());
|
||||||
|
106
src/tui/ui.rs
106
src/tui/ui.rs
@ -1,7 +1,4 @@
|
|||||||
use musichoard::{
|
use musichoard::{Album, Artist, Collection, Track, TrackFormat};
|
||||||
collection::{Collection, CollectionManager},
|
|
||||||
Album, Artist, Track, TrackFormat,
|
|
||||||
};
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
layout::{Alignment, Rect},
|
layout::{Alignment, Rect},
|
||||||
@ -10,7 +7,20 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::Error;
|
use super::{lib::IMusicHoard, Error};
|
||||||
|
|
||||||
|
pub trait IUi {
|
||||||
|
fn is_running(&self) -> bool;
|
||||||
|
fn quit(&mut self);
|
||||||
|
|
||||||
|
fn increment_category(&mut self);
|
||||||
|
fn decrement_category(&mut self);
|
||||||
|
|
||||||
|
fn increment_selection(&mut self);
|
||||||
|
fn decrement_selection(&mut self);
|
||||||
|
|
||||||
|
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>);
|
||||||
|
}
|
||||||
|
|
||||||
struct TrackSelection {
|
struct TrackSelection {
|
||||||
state: ListState,
|
state: ListState,
|
||||||
@ -239,8 +249,8 @@ impl Selection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MhUi<CM> {
|
pub struct Ui<MH> {
|
||||||
collection_manager: CM,
|
music_hoard: MH,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
running: bool,
|
running: bool,
|
||||||
}
|
}
|
||||||
@ -428,12 +438,12 @@ impl<'a, 'b> TrackState<'a, 'b> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<CM: CollectionManager> MhUi<CM> {
|
impl<MH: IMusicHoard> Ui<MH> {
|
||||||
pub fn new(mut collection_manager: CM) -> Result<Self, Error> {
|
pub fn new(mut music_hoard: MH) -> Result<Self, Error> {
|
||||||
collection_manager.rescan_library()?;
|
music_hoard.rescan_library()?;
|
||||||
let selection = Selection::new(Some(collection_manager.get_collection()));
|
let selection = Selection::new(Some(music_hoard.get_collection()));
|
||||||
Ok(MhUi {
|
Ok(Ui {
|
||||||
collection_manager,
|
music_hoard,
|
||||||
selection,
|
selection,
|
||||||
running: true,
|
running: true,
|
||||||
})
|
})
|
||||||
@ -512,20 +522,7 @@ impl<CM: CollectionManager> MhUi<CM> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Ui {
|
impl<MH: IMusicHoard> IUi for Ui<MH> {
|
||||||
fn is_running(&self) -> bool;
|
|
||||||
fn quit(&mut self);
|
|
||||||
|
|
||||||
fn increment_category(&mut self);
|
|
||||||
fn decrement_category(&mut self);
|
|
||||||
|
|
||||||
fn increment_selection(&mut self);
|
|
||||||
fn decrement_selection(&mut self);
|
|
||||||
|
|
||||||
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<CM: CollectionManager> Ui for MhUi<CM> {
|
|
||||||
fn is_running(&self) -> bool {
|
fn is_running(&self) -> bool {
|
||||||
self.running
|
self.running
|
||||||
}
|
}
|
||||||
@ -544,19 +541,19 @@ impl<CM: CollectionManager> Ui for MhUi<CM> {
|
|||||||
|
|
||||||
fn increment_selection(&mut self) {
|
fn increment_selection(&mut self) {
|
||||||
self.selection
|
self.selection
|
||||||
.increment_selection(self.collection_manager.get_collection());
|
.increment_selection(self.music_hoard.get_collection());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decrement_selection(&mut self) {
|
fn decrement_selection(&mut self) {
|
||||||
self.selection
|
self.selection
|
||||||
.decrement_selection(self.collection_manager.get_collection());
|
.decrement_selection(self.music_hoard.get_collection());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
|
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
|
||||||
let active = self.selection.active;
|
let active = self.selection.active;
|
||||||
let areas = FrameArea::new(frame.size());
|
let areas = FrameArea::new(frame.size());
|
||||||
|
|
||||||
let artists = self.collection_manager.get_collection();
|
let artists = self.music_hoard.get_collection();
|
||||||
let artist_selection = &mut self.selection.artist;
|
let artist_selection = &mut self.selection.artist;
|
||||||
let artist_state = ArtistState::new(
|
let artist_state = ArtistState::new(
|
||||||
active == Category::Artist,
|
active == Category::Artist,
|
||||||
@ -600,7 +597,8 @@ impl<CM: CollectionManager> Ui for MhUi<CM> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::tests::{MockCollectionManager, COLLECTION};
|
use crate::tests::COLLECTION;
|
||||||
|
use crate::tui::lib::MockIMusicHoard;
|
||||||
use crate::tui::tests::{terminal, ui};
|
use crate::tui::tests::{terminal, ui};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -759,17 +757,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ui_running() {
|
fn ui_running() {
|
||||||
let mut collection_manager = MockCollectionManager::new();
|
let mut music_hoard = MockIMusicHoard::new();
|
||||||
|
|
||||||
collection_manager
|
music_hoard
|
||||||
.expect_rescan_library()
|
.expect_rescan_library()
|
||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|| Ok(()));
|
.return_once(|| Ok(()));
|
||||||
collection_manager
|
music_hoard
|
||||||
.expect_get_collection()
|
.expect_get_collection()
|
||||||
.return_const(COLLECTION.to_owned());
|
.return_const(COLLECTION.to_owned());
|
||||||
|
|
||||||
let mut ui = MhUi::new(collection_manager).unwrap();
|
let mut ui = Ui::new(music_hoard).unwrap();
|
||||||
assert!(ui.is_running());
|
assert!(ui.is_running());
|
||||||
|
|
||||||
ui.quit();
|
ui.quit();
|
||||||
@ -778,17 +776,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ui_modifiers() {
|
fn ui_modifiers() {
|
||||||
let mut collection_manager = MockCollectionManager::new();
|
let mut music_hoard = MockIMusicHoard::new();
|
||||||
|
|
||||||
collection_manager
|
music_hoard
|
||||||
.expect_rescan_library()
|
.expect_rescan_library()
|
||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|| Ok(()));
|
.return_once(|| Ok(()));
|
||||||
collection_manager
|
music_hoard
|
||||||
.expect_get_collection()
|
.expect_get_collection()
|
||||||
.return_const(COLLECTION.to_owned());
|
.return_const(COLLECTION.to_owned());
|
||||||
|
|
||||||
let mut ui = MhUi::new(collection_manager).unwrap();
|
let mut ui = Ui::new(music_hoard).unwrap();
|
||||||
assert!(ui.is_running());
|
assert!(ui.is_running());
|
||||||
|
|
||||||
assert_eq!(ui.selection.active, Category::Artist);
|
assert_eq!(ui.selection.active, Category::Artist);
|
||||||
@ -877,19 +875,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn app_no_tracks() {
|
fn app_no_tracks() {
|
||||||
let mut collection_manager = MockCollectionManager::new();
|
let mut music_hoard = MockIMusicHoard::new();
|
||||||
let mut collection = COLLECTION.to_owned();
|
let mut collection = COLLECTION.to_owned();
|
||||||
collection[0].albums[0].tracks = vec![];
|
collection[0].albums[0].tracks = vec![];
|
||||||
|
|
||||||
collection_manager
|
music_hoard
|
||||||
.expect_rescan_library()
|
.expect_rescan_library()
|
||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|| Ok(()));
|
.return_once(|| Ok(()));
|
||||||
collection_manager
|
music_hoard.expect_get_collection().return_const(collection);
|
||||||
.expect_get_collection()
|
|
||||||
.return_const(collection);
|
|
||||||
|
|
||||||
let mut app = MhUi::new(collection_manager).unwrap();
|
let mut app = Ui::new(music_hoard).unwrap();
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
assert_eq!(app.selection.active, Category::Artist);
|
assert_eq!(app.selection.active, Category::Artist);
|
||||||
@ -915,19 +911,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn app_no_albums() {
|
fn app_no_albums() {
|
||||||
let mut collection_manager = MockCollectionManager::new();
|
let mut music_hoard = MockIMusicHoard::new();
|
||||||
let mut collection = COLLECTION.to_owned();
|
let mut collection = COLLECTION.to_owned();
|
||||||
collection[0].albums = vec![];
|
collection[0].albums = vec![];
|
||||||
|
|
||||||
collection_manager
|
music_hoard
|
||||||
.expect_rescan_library()
|
.expect_rescan_library()
|
||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|| Ok(()));
|
.return_once(|| Ok(()));
|
||||||
collection_manager
|
music_hoard.expect_get_collection().return_const(collection);
|
||||||
.expect_get_collection()
|
|
||||||
.return_const(collection);
|
|
||||||
|
|
||||||
let mut app = MhUi::new(collection_manager).unwrap();
|
let mut app = Ui::new(music_hoard).unwrap();
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
assert_eq!(app.selection.active, Category::Artist);
|
assert_eq!(app.selection.active, Category::Artist);
|
||||||
@ -966,18 +960,16 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn app_no_artists() {
|
fn app_no_artists() {
|
||||||
let mut collection_manager = MockCollectionManager::new();
|
let mut music_hoard = MockIMusicHoard::new();
|
||||||
let collection = vec![];
|
let collection = vec![];
|
||||||
|
|
||||||
collection_manager
|
music_hoard
|
||||||
.expect_rescan_library()
|
.expect_rescan_library()
|
||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|| Ok(()));
|
.return_once(|| Ok(()));
|
||||||
collection_manager
|
music_hoard.expect_get_collection().return_const(collection);
|
||||||
.expect_get_collection()
|
|
||||||
.return_const(collection);
|
|
||||||
|
|
||||||
let mut app = MhUi::new(collection_manager).unwrap();
|
let mut app = Ui::new(music_hoard).unwrap();
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
assert_eq!(app.selection.active, Category::Artist);
|
assert_eq!(app.selection.active, Category::Artist);
|
||||||
|
@ -3,7 +3,7 @@ use std::{fs, path::PathBuf};
|
|||||||
use musichoard::{
|
use musichoard::{
|
||||||
database::{
|
database::{
|
||||||
json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
||||||
Database,
|
IDatabase,
|
||||||
},
|
},
|
||||||
Artist,
|
Artist,
|
||||||
};
|
};
|
||||||
|
@ -9,7 +9,7 @@ use once_cell::sync::Lazy;
|
|||||||
use musichoard::{
|
use musichoard::{
|
||||||
library::{
|
library::{
|
||||||
beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary},
|
beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary},
|
||||||
Field, Library, Query,
|
Field, ILibrary, Query,
|
||||||
},
|
},
|
||||||
Artist,
|
Artist,
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user