Support remote libraries (#36)

Closes #5

Reviewed-on: https://git.wojciechkozlowski.eu/wojtek/musichoard/pulls/36
This commit is contained in:
Wojciech Kozlowski 2023-04-14 16:21:25 +02:00
parent a5c1bb8558
commit 0545e5324e
10 changed files with 440 additions and 128 deletions

202
Cargo.lock generated
View File

@ -43,6 +43,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bytes"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
[[package]] [[package]]
name = "cassowary" name = "cassowary"
version = "0.3.0" version = "0.3.0"
@ -107,6 +113,26 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "dirs"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]] [[package]]
name = "downcast" name = "downcast"
version = "0.11.0" version = "0.11.0"
@ -164,6 +190,17 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa"
[[package]]
name = "getrandom"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.3.3" version = "0.3.3"
@ -312,11 +349,13 @@ dependencies = [
"crossterm", "crossterm",
"mockall", "mockall",
"once_cell", "once_cell",
"openssh",
"ratatui", "ratatui",
"serde", "serde",
"serde_json", "serde_json",
"structopt", "structopt",
"tempfile", "tempfile",
"tokio",
"uuid", "uuid",
] ]
@ -341,6 +380,39 @@ version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "openssh"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ca6c277973fb549b36dd8980941b5ea3ecebea026f5b1f0060acde74d893c22"
dependencies = [
"dirs",
"libc",
"once_cell",
"openssh-mux-client",
"shell-escape",
"tempfile",
"thiserror",
"tokio",
"tokio-pipe",
]
[[package]]
name = "openssh-mux-client"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88eac793af6170bcd6d4f39c3b7ba3f4227cab5680d7189ba30f9d174600b75f"
dependencies = [
"once_cell",
"sendfd",
"serde",
"ssh_format",
"thiserror",
"tokio",
"tokio-io-utility",
"typed-builder",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.1" version = "0.12.1"
@ -364,6 +436,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "pin-project-lite"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
[[package]] [[package]]
name = "predicates" name = "predicates"
version = "2.1.5" version = "2.1.5"
@ -467,6 +545,17 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "redox_users"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
"getrandom",
"redox_syscall 0.2.16",
"thiserror",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.7.3" version = "1.7.3"
@ -510,6 +599,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sendfd"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "604b71b8fc267e13bb3023a2c901126c8f349393666a6d98ac1ae5729b701798"
dependencies = [
"libc",
"tokio",
]
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.159" version = "1.0.159"
@ -541,6 +640,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "shell-escape"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f"
[[package]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.3.15" version = "0.3.15"
@ -577,6 +682,25 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "socket2"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "ssh_format"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8701239872766d43b8a5f9a560ff7f002b48064fadea87f44a70507069fb482"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.8.0" version = "0.8.0"
@ -657,6 +781,84 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "thiserror"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.11",
]
[[package]]
name = "tokio"
version = "1.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001"
dependencies = [
"autocfg",
"bytes",
"libc",
"mio",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-io-utility"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d672654d175710e52c7c41f6aec77c62b3c0954e2a7ebce9049d1e94ed7c263"
dependencies = [
"tokio",
]
[[package]]
name = "tokio-macros"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.11",
]
[[package]]
name = "tokio-pipe"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f213a84bffbd61b8fa0ba8a044b4bbe35d471d0b518867181e82bd5c15542784"
dependencies = [
"libc",
"tokio",
]
[[package]]
name = "typed-builder"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89851716b67b937e393b3daa8423e67ddfc4bbbf1654bcf05488e95e0828db0c"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.8" version = "1.0.8"

View File

@ -7,10 +7,12 @@ edition = "2021"
[dependencies] [dependencies]
crossterm = "0.26.1" crossterm = "0.26.1"
openssh = { version = "0.9.9", features = ["native-mux"], default-features = false }
serde = { version = "1.0.159", features = ["derive"] } serde = { version = "1.0.159", features = ["derive"] }
serde_json = "1.0.95" serde_json = "1.0.95"
structopt = "0.3.26" structopt = "0.3.26"
ratatui = "0.20.1" ratatui = "0.20.1"
tokio = { version = "1.27.0", features = ["rt"] }
uuid = { version = "1.3.0", features = ["serde"] } uuid = { version = "1.3.0", features = ["serde"] }
[dev-dependencies] [dev-dependencies]

View File

@ -0,0 +1,30 @@
//! Module for storing MusicHoard data in a JSON file database.
use std::fs;
use std::path::PathBuf;
use super::JsonDatabaseBackend;
/// JSON database backend that uses a local file for persistent storage.
pub struct JsonDatabaseFileBackend {
path: PathBuf,
}
impl JsonDatabaseFileBackend {
/// Create a [`JsonDatabaseFileBackend`] that will read/write to the provided path.
pub fn new<P: Into<PathBuf>>(path: P) -> Self {
JsonDatabaseFileBackend { path: path.into() }
}
}
impl JsonDatabaseBackend for JsonDatabaseFileBackend {
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:
// https://github.com/serde-rs/json/issues/160
fs::read_to_string(&self.path)
}
fn write(&mut self, json: &str) -> Result<(), std::io::Error> {
fs::write(&self.path, json)
}
}

View File

@ -1,8 +1,5 @@
//! Module for storing MusicHoard data in a JSON file database. //! Module for storing MusicHoard data in a JSON file database.
use std::fs;
use std::path::PathBuf;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
@ -11,6 +8,8 @@ use mockall::automock;
use super::{Database, Error}; use super::{Database, Error};
pub mod backend;
impl From<serde_json::Error> for Error { impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Error { fn from(err: serde_json::Error) -> Error {
Error::SerDeError(err.to_string()) Error::SerDeError(err.to_string())
@ -53,30 +52,6 @@ impl<JDB: JsonDatabaseBackend> Database for JsonDatabase<JDB> {
} }
} }
/// JSON database backend that uses a local file for persistent storage.
pub struct JsonDatabaseFileBackend {
path: PathBuf,
}
impl JsonDatabaseFileBackend {
/// Create a [`JsonDatabaseFileBackend`] that will read/write to the provided path.
pub fn new<P: Into<PathBuf>>(path: P) -> Self {
JsonDatabaseFileBackend { path: path.into() }
}
}
impl JsonDatabaseBackend for JsonDatabaseFileBackend {
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:
// https://github.com/serde-rs/json/issues/160
fs::read_to_string(&self.path)
}
fn write(&mut self, json: &str) -> Result<(), std::io::Error> {
fs::write(&self.path, json)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashMap; use std::collections::HashMap;

View File

@ -0,0 +1,140 @@
//! Module for interacting with the music library via
//! [beets](https://beets.readthedocs.io/en/stable/).
use std::{
ffi::OsString,
path::PathBuf,
process::{Command, Output},
str,
};
use openssh::{KnownHosts, Session};
use tokio::runtime::{self, Runtime};
use super::{BeetsLibraryExecutor, Error};
const BEET_DEFAULT: &str = "beet";
trait BeetsLibraryExecutorPrivate {
fn output(output: Output) -> Result<Vec<String>, Error> {
if !output.status.success() {
return Err(Error::Executor(
String::from_utf8_lossy(&output.stderr).to_string(),
));
}
let output = str::from_utf8(&output.stdout)?;
Ok(output.split('\n').map(|s| s.to_string()).collect())
}
}
/// Beets library executor that executes beets commands in their own process.
pub struct BeetsLibraryProcessExecutor {
bin: OsString,
config: Option<PathBuf>,
}
impl BeetsLibraryProcessExecutor {
/// Create a new [`BeetsLibraryProcessExecutor`] that uses the default beets executable.
pub fn new() -> Self {
Self::bin(BEET_DEFAULT)
}
/// Create a new [`BeetsLibraryProcessExecutor`] that uses the provided beets executable.
pub fn bin<S: Into<OsString>>(bin: S) -> Self {
BeetsLibraryProcessExecutor {
bin: bin.into(),
config: None,
}
}
/// Update the configuration file passed to the beets executable.
pub fn config<P: Into<PathBuf>>(mut self, path: Option<P>) -> Self {
self.config = path.map(|p| p.into());
self
}
}
impl Default for BeetsLibraryProcessExecutor {
/// Create a new [`BeetsLibraryProcessExecutor`] that uses the system's default beets
/// executable.
fn default() -> Self {
BeetsLibraryProcessExecutor::new()
}
}
impl BeetsLibraryExecutor for BeetsLibraryProcessExecutor {
fn exec<S: AsRef<str> + 'static>(&mut self, arguments: &[S]) -> Result<Vec<String>, Error> {
let mut cmd = Command::new(&self.bin);
if let Some(ref path) = self.config {
cmd.arg("--config");
cmd.arg(path);
}
let output = cmd.args(arguments.iter().map(|s| s.as_ref())).output()?;
Self::output(output)
}
}
impl BeetsLibraryExecutorPrivate for BeetsLibraryProcessExecutor {}
// GRCOV_EXCL_START
/// Beets library executor that executes beets commands over SSH.
pub struct BeetsLibrarySshExecutor {
rt: Runtime,
session: Session,
bin: String,
config: Option<String>,
}
impl From<openssh::Error> for Error {
fn from(err: openssh::Error) -> Error {
Error::Executor(err.to_string())
}
}
impl BeetsLibrarySshExecutor {
/// Create a new [`BeetsLibrarySshExecutor`] that uses the default beets executable over an SSH
/// connection. This call will attempt to establish the connection and will fail if the
/// connection fails.
pub fn new<H: AsRef<str>>(host: H) -> Result<Self, Error> {
Self::bin(host, BEET_DEFAULT)
}
/// Create a new [`BeetsLibrarySshExecutor`] that uses the provided beets executable over an SSH
/// connection. This call will attempt to establish the connection and will fail if the
/// connection fails.
pub fn bin<H: AsRef<str>, S: Into<String>>(host: H, bin: S) -> Result<Self, Error> {
let rt = runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let session = rt.block_on(Session::connect_mux(host, KnownHosts::Strict))?;
Ok(BeetsLibrarySshExecutor {
rt,
session,
bin: bin.into(),
config: None,
})
}
/// Update the configuration file passed to the beets executable.
pub fn config<P: Into<String>>(mut self, path: Option<P>) -> Self {
self.config = path.map(|p| p.into());
self
}
}
impl BeetsLibraryExecutor for BeetsLibrarySshExecutor {
fn exec<S: AsRef<str> + 'static>(&mut self, arguments: &[S]) -> Result<Vec<String>, Error> {
let mut cmd = self.session.command(&self.bin);
if let Some(ref path) = self.config {
cmd.arg("--config");
cmd.arg(path);
}
cmd.args(arguments.iter().map(|s| s.as_ref()));
let output = self.rt.block_on(cmd.output())?;
Self::output(output)
}
}
impl BeetsLibraryExecutorPrivate for BeetsLibrarySshExecutor {}
// GRCOV_EXCL_STOP

View File

@ -3,9 +3,6 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
ffi::OsString,
path::PathBuf,
process::Command,
str, str,
}; };
@ -16,6 +13,8 @@ use crate::{Album, AlbumId, Artist, ArtistId, Track, TrackFormat};
use super::{Error, Field, Library, Query}; use super::{Error, Field, Library, Query};
pub mod executor;
macro_rules! list_format_separator { macro_rules! list_format_separator {
() => { () => {
" -*^- " " -*^- "
@ -95,7 +94,7 @@ trait LibraryPrivate {
} }
impl<BLE: BeetsLibraryExecutor> BeetsLibrary<BLE> { impl<BLE: BeetsLibraryExecutor> BeetsLibrary<BLE> {
/// Create a new beets library with the provided executor, e.g. [`BeetsLibraryCommandExecutor`]. /// Create a new beets library with the provided executor, e.g. [`BeetsLibraryProcessExecutor`].
pub fn new(executor: BLE) -> Self { pub fn new(executor: BLE) -> Self {
BeetsLibrary { executor } BeetsLibrary { executor }
} }
@ -198,53 +197,6 @@ impl<BLE: BeetsLibraryExecutor> LibraryPrivate for BeetsLibrary<BLE> {
} }
} }
/// Beets library executor that executes beets commands in their own process.
pub struct BeetsLibraryCommandExecutor {
bin: OsString,
config: Option<PathBuf>,
}
impl BeetsLibraryCommandExecutor {
/// Create a new [`BeetsLibraryCommandExecutor`] that uses the provided beets executable.
pub fn new<S: Into<OsString>>(bin: S) -> Self {
BeetsLibraryCommandExecutor {
bin: bin.into(),
config: None,
}
}
/// Update the configuration file passed to the beets executable.
pub fn config<P: Into<PathBuf>>(mut self, path: Option<P>) -> Self {
self.config = path.map(|p| p.into());
self
}
}
impl Default for BeetsLibraryCommandExecutor {
/// Create a new [`BeetsLibraryCommandExecutor`] that uses the system's default beets executable.
fn default() -> Self {
BeetsLibraryCommandExecutor::new("beet")
}
}
impl BeetsLibraryExecutor for BeetsLibraryCommandExecutor {
fn exec<S: AsRef<str> + 'static>(&mut self, arguments: &[S]) -> Result<Vec<String>, Error> {
let mut cmd = Command::new(&self.bin);
if let Some(ref path) = self.config {
cmd.arg("--config");
cmd.arg(path);
}
let output = cmd.args(arguments.iter().map(|s| s.as_ref())).output()?;
if !output.status.success() {
return Err(Error::CmdExec(
String::from_utf8_lossy(&output.stderr).to_string(),
));
}
let output = str::from_utf8(&output.stdout)?;
Ok(output.split('\n').map(|s| s.to_string()).collect())
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use mockall::predicate; use mockall::predicate;

View File

@ -60,26 +60,26 @@ impl Query {
/// Error type for library calls. /// Error type for library calls.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum Error { pub enum Error {
/// The underlying library failed to execute a command. /// The library's executor failed.
CmdExec(String), Executor(String),
/// The underlying library returned invalid data. /// The library experienced an I/O error.
Invalid(String),
/// The underlying library experienced an I/O error.
Io(String), Io(String),
/// The underlying library failed to parse an integer. /// The library received invalid data.
Invalid(String),
/// The library failed to parse an integer.
ParseInt(String), ParseInt(String),
/// The underlying library failed to parse a UTF-8 string. /// The library failed to parse a UTF-8 string.
Utf8(String), Utf8(String),
} }
impl fmt::Display for Error { impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self { match *self {
Self::CmdExec(ref s) => write!(f, "the library failed to execute a command: {s}"), Self::Executor(ref s) => write!(f, "the library's executor failed: {s}"),
Self::Invalid(ref s) => write!(f, "the library received invalid data: {s}"),
Self::Io(ref s) => write!(f, "the library experienced an I/O error: {s}"), Self::Io(ref s) => write!(f, "the library experienced an I/O error: {s}"),
Self::ParseInt(ref s) => write!(f, "the library received an invalid integer: {s}"), Self::Invalid(ref s) => write!(f, "the library received invalid data: {s}"),
Self::Utf8(ref s) => write!(f, "the library received invalid UTF-8: {s}"), Self::ParseInt(ref s) => write!(f, "the library failed to parse an integer: {s}"),
Self::Utf8(ref s) => write!(f, "the library failed to parse a UTF-8 string: {s}"),
} }
} }
} }
@ -133,21 +133,21 @@ mod tests {
#[test] #[test]
fn errors() { fn errors() {
let cmd_err = Error::CmdExec(String::from("CmdExec")); let exe_err = Error::Executor(String::from("Executor"));
let inv_err = Error::Invalid(String::from("Invalid"));
let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "Interrupted").into(); let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "Interrupted").into();
let inv_err = Error::Invalid(String::from("Invalid"));
let int_err: Error = "five".parse::<u32>().unwrap_err().into(); let int_err: Error = "five".parse::<u32>().unwrap_err().into();
let utf_err: Error = std::str::from_utf8(b"\xe2\x28\xa1").unwrap_err().into(); let utf_err: Error = std::str::from_utf8(b"\xe2\x28\xa1").unwrap_err().into();
assert!(!cmd_err.to_string().is_empty()); assert!(!exe_err.to_string().is_empty());
assert!(!inv_err.to_string().is_empty());
assert!(!io_err.to_string().is_empty()); assert!(!io_err.to_string().is_empty());
assert!(!inv_err.to_string().is_empty());
assert!(!int_err.to_string().is_empty()); assert!(!int_err.to_string().is_empty());
assert!(!utf_err.to_string().is_empty()); assert!(!utf_err.to_string().is_empty());
assert!(!format!("{:?}", cmd_err).is_empty()); assert!(!format!("{:?}", exe_err).is_empty());
assert!(!format!("{:?}", inv_err).is_empty());
assert!(!format!("{:?}", io_err).is_empty()); assert!(!format!("{:?}", io_err).is_empty());
assert!(!format!("{:?}", inv_err).is_empty());
assert!(!format!("{:?}", int_err).is_empty()); assert!(!format!("{:?}", int_err).is_empty());
assert!(!format!("{:?}", utf_err).is_empty()); assert!(!format!("{:?}", utf_err).is_empty());
} }

View File

@ -1,14 +1,22 @@
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use std::io;
use std::path::PathBuf; use std::path::PathBuf;
use std::{ffi::OsString, io};
use ratatui::{backend::CrosstermBackend, Terminal};
use structopt::StructOpt; use structopt::StructOpt;
use musichoard::{ use musichoard::{
collection::MhCollectionManager, collection::MhCollectionManager,
database::json::{JsonDatabase, JsonDatabaseFileBackend}, database::{
library::beets::{BeetsLibrary, BeetsLibraryCommandExecutor}, json::{backend::JsonDatabaseFileBackend, JsonDatabase},
Database,
},
library::{
beets::{
executor::{BeetsLibraryProcessExecutor, BeetsLibrarySshExecutor},
BeetsLibrary,
},
Library,
},
}; };
mod tui; mod tui;
@ -19,35 +27,22 @@ use tui::{
#[derive(StructOpt)] #[derive(StructOpt)]
struct Opt { struct Opt {
#[structopt( #[structopt(long = "ssh", name = "beets SSH URI")]
short = "b", beets_ssh_uri: Option<OsString>,
long = "beets",
name = "beets config file path", #[structopt(long = "beets", name = "beets config file path")]
parse(from_os_str) beets_config_file_path: Option<OsString>,
)]
beets_config_file_path: Option<PathBuf>,
#[structopt( #[structopt(
short = "d",
long = "database", long = "database",
name = "database file path", name = "database file path",
default_value = "database.json", default_value = "database.json"
parse(from_os_str)
)] )]
database_file_path: PathBuf, database_file_path: PathBuf,
} }
fn main() { fn with<LIB: Library, DB: Database>(lib: LIB, db: DB) {
// Create the application. let collection_manager = MhCollectionManager::new(lib, db);
let opt = Opt::from_args();
let beets = BeetsLibrary::new(
BeetsLibraryCommandExecutor::default().config(opt.beets_config_file_path.as_deref()),
);
let database = JsonDatabase::new(JsonDatabaseFileBackend::new(&opt.database_file_path));
let collection_manager = MhCollectionManager::new(beets, database);
// Initialize the terminal user interface. // Initialize the terminal user interface.
let backend = CrosstermBackend::new(io::stdout()); let backend = CrosstermBackend::new(io::stdout());
@ -65,6 +60,22 @@ fn main() {
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui"); Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui");
} }
fn main() {
// Create the application.
let opt = Opt::from_args();
if let Some(uri) = opt.beets_ssh_uri {
let uri = uri.into_string().expect("invalid SSH URI");
let lib_exec = BeetsLibrarySshExecutor::new(uri).expect("failed to initialise beets");
let db_exec = JsonDatabaseFileBackend::new(&opt.database_file_path);
with(BeetsLibrary::new(lib_exec), JsonDatabase::new(db_exec));
} else {
let lib_exec = BeetsLibraryProcessExecutor::default().config(opt.beets_config_file_path);
let db_exec = JsonDatabaseFileBackend::new(&opt.database_file_path);
with(BeetsLibrary::new(lib_exec), JsonDatabase::new(db_exec));
}
}
#[cfg(test)] #[cfg(test)]
#[macro_use] #[macro_use]
mod testlib; mod testlib;

View File

@ -2,7 +2,7 @@ use std::{fs, path::PathBuf};
use musichoard::{ use musichoard::{
database::{ database::{
json::{JsonDatabase, JsonDatabaseFileBackend}, json::{backend::JsonDatabaseFileBackend, JsonDatabase},
Database, Database,
}, },
Artist, Artist,

View File

@ -8,7 +8,7 @@ use once_cell::sync::Lazy;
use musichoard::{ use musichoard::{
library::{ library::{
beets::{BeetsLibrary, BeetsLibraryCommandExecutor}, beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary},
Field, Library, Query, Field, Library, Query,
}, },
Artist, Artist,
@ -16,17 +16,17 @@ use musichoard::{
use crate::COLLECTION; use crate::COLLECTION;
static BEETS_EMPTY_CONFIG: Lazy<Arc<Mutex<BeetsLibrary<BeetsLibraryCommandExecutor>>>> = static BEETS_EMPTY_CONFIG: Lazy<Arc<Mutex<BeetsLibrary<BeetsLibraryProcessExecutor>>>> =
Lazy::new(|| { Lazy::new(|| {
Arc::new(Mutex::new(BeetsLibrary::new( Arc::new(Mutex::new(BeetsLibrary::new(
BeetsLibraryCommandExecutor::default(), BeetsLibraryProcessExecutor::default(),
))) )))
}); });
static BEETS_TEST_CONFIG: Lazy<Arc<Mutex<BeetsLibrary<BeetsLibraryCommandExecutor>>>> = static BEETS_TEST_CONFIG: Lazy<Arc<Mutex<BeetsLibrary<BeetsLibraryProcessExecutor>>>> =
Lazy::new(|| { Lazy::new(|| {
Arc::new(Mutex::new(BeetsLibrary::new( Arc::new(Mutex::new(BeetsLibrary::new(
BeetsLibraryCommandExecutor::default().config(Some( BeetsLibraryProcessExecutor::default().config(Some(
&fs::canonicalize("./tests/files/library/config.yml").unwrap(), &fs::canonicalize("./tests/files/library/config.yml").unwrap(),
)), )),
))) )))
@ -45,7 +45,7 @@ fn test_no_config_list() {
#[test] #[test]
fn test_invalid_config() { fn test_invalid_config() {
let mut beets = BeetsLibrary::new(BeetsLibraryCommandExecutor::default().config(Some( let mut beets = BeetsLibrary::new(BeetsLibraryProcessExecutor::default().config(Some(
&PathBuf::from("./tests/files/library/config-does-not-exist.yml"), &PathBuf::from("./tests/files/library/config-does-not-exist.yml"),
))); )));