Support remote libraries #36

Merged
wojtek merged 5 commits from 5---support-remote-libraries into main 2023-04-14 16:21:25 +02:00
4 changed files with 155 additions and 139 deletions
Showing only changes of commit f3763f3ca1 - Show all commits

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::{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

View File

@ -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;

View File

@ -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;

View File

@ -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,