diff --git a/src/library/beets/executor.rs b/src/library/beets/executor.rs new file mode 100644 index 0000000..40f5146 --- /dev/null +++ b/src/library/beets/executor.rs @@ -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::{Error, BeetsLibraryExecutor}; + +const BEET_DEFAULT: &str = "beet"; + +trait BeetsLibraryExecutorPrivate { + fn output(output: Output) -> Result, 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, +} + +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>(bin: S) -> Self { + BeetsLibraryProcessExecutor { + bin: bin.into(), + config: None, + } + } + + /// Update the configuration file passed to the beets executable. + pub fn config>(mut self, path: Option

) -> 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 + 'static>(&mut self, arguments: &[S]) -> Result, 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, +} + +impl From 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>(host: H) -> Result { + 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, S: Into>(host: H, bin: S) -> Result { + 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>(mut self, path: Option

) -> Self { + self.config = path.map(|p| p.into()); + self + } +} + +impl BeetsLibraryExecutor for BeetsLibrarySshExecutor { + fn exec + 'static>(&mut self, arguments: &[S]) -> Result, 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 diff --git a/src/library/beets.rs b/src/library/beets/mod.rs similarity index 78% rename from src/library/beets.rs rename to src/library/beets/mod.rs index 1e0e29f..7ce5214 100644 --- a/src/library/beets.rs +++ b/src/library/beets/mod.rs @@ -3,15 +3,9 @@ use std::{ collections::{HashMap, HashSet}, - ffi::OsString, - path::PathBuf, - process::{Command, Output}, str, }; -use openssh::{KnownHosts, Session}; -use tokio::runtime::{self, Runtime}; - #[cfg(test)] use mockall::automock; @@ -19,13 +13,14 @@ use crate::{Album, AlbumId, Artist, ArtistId, Track, TrackFormat}; use super::{Error, Field, Library, Query}; +pub mod executor; + macro_rules! list_format_separator { () => { " -*^- " }; } -const BEET_DEFAULT: &str = "beet"; const CMD_LIST: &str = "ls"; const LIST_FORMAT_SEPARATOR: &str = list_format_separator!(); const LIST_FORMAT_ARG: &str = concat!( @@ -202,130 +197,6 @@ impl LibraryPrivate for BeetsLibrary { } } -trait BeetsLibraryExecutorPrivate { - fn output(output: Output) -> Result, 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, -} - -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>(bin: S) -> Self { - BeetsLibraryProcessExecutor { - bin: bin.into(), - config: None, - } - } - - /// Update the configuration file passed to the beets executable. - pub fn config>(mut self, path: Option

) -> 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 + 'static>(&mut self, arguments: &[S]) -> Result, 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, -} - -impl From 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>(host: H) -> Result { - 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, S: Into>(host: H, bin: S) -> Result { - 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>(mut self, path: Option

) -> Self { - self.config = path.map(|p| p.into()); - self - } -} - -impl BeetsLibraryExecutor for BeetsLibrarySshExecutor { - fn exec + 'static>(&mut self, arguments: &[S]) -> Result, 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 - #[cfg(test)] mod tests { use mockall::predicate; diff --git a/src/main.rs b/src/main.rs index 04ad707..28c466e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,22 @@ -use musichoard::database::Database; -use musichoard::library::beets::BeetsLibrarySshExecutor; -use musichoard::library::Library; -use ratatui::backend::CrosstermBackend; -use ratatui::Terminal; 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, BeetsLibraryProcessExecutor}, + database::{ + json::{JsonDatabase, JsonDatabaseFileBackend}, + Database, + }, + library::{ + beets::{ + executor::{BeetsLibraryProcessExecutor, BeetsLibrarySshExecutor}, + BeetsLibrary, + }, + Library, + }, }; mod tui; diff --git a/tests/library/beets.rs b/tests/library/beets.rs index 8b97f45..3d9215c 100644 --- a/tests/library/beets.rs +++ b/tests/library/beets.rs @@ -8,7 +8,7 @@ use once_cell::sync::Lazy; use musichoard::{ library::{ - beets::{BeetsLibrary, BeetsLibraryProcessExecutor}, + beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary}, Field, Library, Query, }, Artist,