Support remote libraries (#36)
Closes #5 Reviewed-on: https://git.wojciechkozlowski.eu/wojtek/musichoard/pulls/36
This commit is contained in:
parent
a5c1bb8558
commit
0545e5324e
202
Cargo.lock
generated
202
Cargo.lock
generated
@ -43,6 +43,12 @@ version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
@ -107,6 +113,26 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "downcast"
|
||||
version = "0.11.0"
|
||||
@ -164,6 +190,17 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "heck"
|
||||
version = "0.3.3"
|
||||
@ -312,11 +349,13 @@ dependencies = [
|
||||
"crossterm",
|
||||
"mockall",
|
||||
"once_cell",
|
||||
"openssh",
|
||||
"ratatui",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"structopt",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@ -341,6 +380,39 @@ version = "1.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.1"
|
||||
@ -364,6 +436,12 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
|
||||
|
||||
[[package]]
|
||||
name = "predicates"
|
||||
version = "2.1.5"
|
||||
@ -467,6 +545,17 @@ dependencies = [
|
||||
"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]]
|
||||
name = "regex"
|
||||
version = "1.7.3"
|
||||
@ -510,6 +599,16 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "serde"
|
||||
version = "1.0.159"
|
||||
@ -541,6 +640,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-escape"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.15"
|
||||
@ -577,6 +682,25 @@ version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "strsim"
|
||||
version = "0.8.0"
|
||||
@ -657,6 +781,84 @@ dependencies = [
|
||||
"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]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.8"
|
||||
|
@ -7,10 +7,12 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
crossterm = "0.26.1"
|
||||
openssh = { version = "0.9.9", features = ["native-mux"], default-features = false }
|
||||
serde = { version = "1.0.159", features = ["derive"] }
|
||||
serde_json = "1.0.95"
|
||||
structopt = "0.3.26"
|
||||
ratatui = "0.20.1"
|
||||
tokio = { version = "1.27.0", features = ["rt"] }
|
||||
uuid = { version = "1.3.0", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
30
src/database/json/backend.rs
Normal file
30
src/database/json/backend.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -1,8 +1,5 @@
|
||||
//! Module for storing MusicHoard data in a JSON file database.
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
@ -11,6 +8,8 @@ use mockall::automock;
|
||||
|
||||
use super::{Database, Error};
|
||||
|
||||
pub mod backend;
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(err: serde_json::Error) -> Error {
|
||||
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)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
140
src/library/beets/executor.rs
Normal file
140
src/library/beets/executor.rs
Normal 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
|
@ -3,9 +3,6 @@
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
ffi::OsString,
|
||||
path::PathBuf,
|
||||
process::Command,
|
||||
str,
|
||||
};
|
||||
|
||||
@ -16,6 +13,8 @@ use crate::{Album, AlbumId, Artist, ArtistId, Track, TrackFormat};
|
||||
|
||||
use super::{Error, Field, Library, Query};
|
||||
|
||||
pub mod executor;
|
||||
|
||||
macro_rules! list_format_separator {
|
||||
() => {
|
||||
" -*^- "
|
||||
@ -95,7 +94,7 @@ trait LibraryPrivate {
|
||||
}
|
||||
|
||||
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 {
|
||||
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)]
|
||||
mod tests {
|
||||
use mockall::predicate;
|
@ -60,26 +60,26 @@ impl Query {
|
||||
/// Error type for library calls.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
/// The underlying library failed to execute a command.
|
||||
CmdExec(String),
|
||||
/// The underlying library returned invalid data.
|
||||
Invalid(String),
|
||||
/// The underlying library experienced an I/O error.
|
||||
/// The library's executor failed.
|
||||
Executor(String),
|
||||
/// The library experienced an I/O error.
|
||||
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),
|
||||
/// The underlying library failed to parse a UTF-8 string.
|
||||
/// The library failed to parse a UTF-8 string.
|
||||
Utf8(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
Self::CmdExec(ref s) => write!(f, "the library failed to execute a command: {s}"),
|
||||
Self::Invalid(ref s) => write!(f, "the library received invalid data: {s}"),
|
||||
Self::Executor(ref s) => write!(f, "the library's executor failed: {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::Utf8(ref s) => write!(f, "the library received invalid UTF-8: {s}"),
|
||||
Self::Invalid(ref s) => write!(f, "the library received invalid data: {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]
|
||||
fn errors() {
|
||||
let cmd_err = Error::CmdExec(String::from("CmdExec"));
|
||||
let inv_err = Error::Invalid(String::from("Invalid"));
|
||||
let exe_err = Error::Executor(String::from("Executor"));
|
||||
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 utf_err: Error = std::str::from_utf8(b"\xe2\x28\xa1").unwrap_err().into();
|
||||
|
||||
assert!(!cmd_err.to_string().is_empty());
|
||||
assert!(!inv_err.to_string().is_empty());
|
||||
assert!(!exe_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!(!utf_err.to_string().is_empty());
|
||||
|
||||
assert!(!format!("{:?}", cmd_err).is_empty());
|
||||
assert!(!format!("{:?}", inv_err).is_empty());
|
||||
assert!(!format!("{:?}", exe_err).is_empty());
|
||||
assert!(!format!("{:?}", io_err).is_empty());
|
||||
assert!(!format!("{:?}", inv_err).is_empty());
|
||||
assert!(!format!("{:?}", int_err).is_empty());
|
||||
assert!(!format!("{:?}", utf_err).is_empty());
|
||||
}
|
||||
|
63
src/main.rs
63
src/main.rs
@ -1,14 +1,22 @@
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::Terminal;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::{ffi::OsString, io};
|
||||
|
||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
use structopt::StructOpt;
|
||||
|
||||
use musichoard::{
|
||||
collection::MhCollectionManager,
|
||||
database::json::{JsonDatabase, JsonDatabaseFileBackend},
|
||||
library::beets::{BeetsLibrary, BeetsLibraryCommandExecutor},
|
||||
database::{
|
||||
json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
||||
Database,
|
||||
},
|
||||
library::{
|
||||
beets::{
|
||||
executor::{BeetsLibraryProcessExecutor, BeetsLibrarySshExecutor},
|
||||
BeetsLibrary,
|
||||
},
|
||||
Library,
|
||||
},
|
||||
};
|
||||
|
||||
mod tui;
|
||||
@ -19,35 +27,22 @@ use tui::{
|
||||
|
||||
#[derive(StructOpt)]
|
||||
struct Opt {
|
||||
#[structopt(
|
||||
short = "b",
|
||||
long = "beets",
|
||||
name = "beets config file path",
|
||||
parse(from_os_str)
|
||||
)]
|
||||
beets_config_file_path: Option<PathBuf>,
|
||||
#[structopt(long = "ssh", name = "beets SSH URI")]
|
||||
beets_ssh_uri: Option<OsString>,
|
||||
|
||||
#[structopt(long = "beets", name = "beets config file path")]
|
||||
beets_config_file_path: Option<OsString>,
|
||||
|
||||
#[structopt(
|
||||
short = "d",
|
||||
long = "database",
|
||||
name = "database file path",
|
||||
default_value = "database.json",
|
||||
parse(from_os_str)
|
||||
default_value = "database.json"
|
||||
)]
|
||||
database_file_path: PathBuf,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Create the application.
|
||||
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);
|
||||
fn with<LIB: Library, DB: Database>(lib: LIB, db: DB) {
|
||||
let collection_manager = MhCollectionManager::new(lib, db);
|
||||
|
||||
// Initialize the terminal user interface.
|
||||
let backend = CrosstermBackend::new(io::stdout());
|
||||
@ -65,6 +60,22 @@ fn main() {
|
||||
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)]
|
||||
#[macro_use]
|
||||
mod testlib;
|
||||
|
@ -2,7 +2,7 @@ use std::{fs, path::PathBuf};
|
||||
|
||||
use musichoard::{
|
||||
database::{
|
||||
json::{JsonDatabase, JsonDatabaseFileBackend},
|
||||
json::{backend::JsonDatabaseFileBackend, JsonDatabase},
|
||||
Database,
|
||||
},
|
||||
Artist,
|
||||
|
@ -8,7 +8,7 @@ use once_cell::sync::Lazy;
|
||||
|
||||
use musichoard::{
|
||||
library::{
|
||||
beets::{BeetsLibrary, BeetsLibraryCommandExecutor},
|
||||
beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary},
|
||||
Field, Library, Query,
|
||||
},
|
||||
Artist,
|
||||
@ -16,17 +16,17 @@ use musichoard::{
|
||||
|
||||
use crate::COLLECTION;
|
||||
|
||||
static BEETS_EMPTY_CONFIG: Lazy<Arc<Mutex<BeetsLibrary<BeetsLibraryCommandExecutor>>>> =
|
||||
static BEETS_EMPTY_CONFIG: Lazy<Arc<Mutex<BeetsLibrary<BeetsLibraryProcessExecutor>>>> =
|
||||
Lazy::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(|| {
|
||||
Arc::new(Mutex::new(BeetsLibrary::new(
|
||||
BeetsLibraryCommandExecutor::default().config(Some(
|
||||
BeetsLibraryProcessExecutor::default().config(Some(
|
||||
&fs::canonicalize("./tests/files/library/config.yml").unwrap(),
|
||||
)),
|
||||
)))
|
||||
@ -45,7 +45,7 @@ fn test_no_config_list() {
|
||||
|
||||
#[test]
|
||||
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"),
|
||||
)));
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user