Support remote libraries #36
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::{Error, BeetsLibraryExecutor};
|
||||||
|
|
||||||
|
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,15 +3,9 @@
|
|||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
ffi::OsString,
|
|
||||||
path::PathBuf,
|
|
||||||
process::{Command, Output},
|
|
||||||
str,
|
str,
|
||||||
};
|
};
|
||||||
|
|
||||||
use openssh::{KnownHosts, Session};
|
|
||||||
use tokio::runtime::{self, Runtime};
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
|
|
||||||
@ -19,13 +13,14 @@ 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 {
|
||||||
() => {
|
() => {
|
||||||
" -*^- "
|
" -*^- "
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const BEET_DEFAULT: &str = "beet";
|
|
||||||
const CMD_LIST: &str = "ls";
|
const CMD_LIST: &str = "ls";
|
||||||
const LIST_FORMAT_SEPARATOR: &str = list_format_separator!();
|
const LIST_FORMAT_SEPARATOR: &str = list_format_separator!();
|
||||||
const LIST_FORMAT_ARG: &str = concat!(
|
const LIST_FORMAT_ARG: &str = concat!(
|
||||||
@ -202,130 +197,6 @@ impl<BLE: BeetsLibraryExecutor> LibraryPrivate for BeetsLibrary<BLE> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use mockall::predicate;
|
use mockall::predicate;
|
19
src/main.rs
19
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::path::PathBuf;
|
||||||
use std::{ffi::OsString, io};
|
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, BeetsLibraryProcessExecutor},
|
json::{JsonDatabase, JsonDatabaseFileBackend},
|
||||||
|
Database,
|
||||||
|
},
|
||||||
|
library::{
|
||||||
|
beets::{
|
||||||
|
executor::{BeetsLibraryProcessExecutor, BeetsLibrarySshExecutor},
|
||||||
|
BeetsLibrary,
|
||||||
|
},
|
||||||
|
Library,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
mod tui;
|
mod tui;
|
||||||
|
@ -8,7 +8,7 @@ use once_cell::sync::Lazy;
|
|||||||
|
|
||||||
use musichoard::{
|
use musichoard::{
|
||||||
library::{
|
library::{
|
||||||
beets::{BeetsLibrary, BeetsLibraryProcessExecutor},
|
beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary},
|
||||||
Field, Library, Query,
|
Field, Library, Query,
|
||||||
},
|
},
|
||||||
Artist,
|
Artist,
|
||||||
|
Loading…
Reference in New Issue
Block a user