Generic beets executor
This commit is contained in:
parent
5edcceeae5
commit
73b4cc8b28
@ -16,6 +16,33 @@ use crate::{Album, AlbumId, Artist, ArtistId, Track, TrackFormat};
|
|||||||
|
|
||||||
use super::{Error, Library, Query, QueryOption};
|
use super::{Error, Library, Query, QueryOption};
|
||||||
|
|
||||||
|
macro_rules! list_format_separator {
|
||||||
|
() => {
|
||||||
|
" -*^- "
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const CMD_LIST: &str = "ls";
|
||||||
|
const LIST_FORMAT_SEPARATOR: &str = list_format_separator!();
|
||||||
|
const LIST_FORMAT_ARG: &str = concat!(
|
||||||
|
"--format=",
|
||||||
|
"$albumartist",
|
||||||
|
list_format_separator!(),
|
||||||
|
"$year",
|
||||||
|
list_format_separator!(),
|
||||||
|
"$album",
|
||||||
|
list_format_separator!(),
|
||||||
|
"$track",
|
||||||
|
list_format_separator!(),
|
||||||
|
"$title",
|
||||||
|
list_format_separator!(),
|
||||||
|
"$artist",
|
||||||
|
list_format_separator!(),
|
||||||
|
"$format"
|
||||||
|
);
|
||||||
|
const TRACK_FORMAT_FLAC: &str = "FLAC";
|
||||||
|
const TRACK_FORMAT_MP3: &str = "MP3";
|
||||||
|
|
||||||
trait QueryOptionArgBeets {
|
trait QueryOptionArgBeets {
|
||||||
fn to_arg(&self, option_name: &str) -> Option<String>;
|
fn to_arg(&self, option_name: &str) -> Option<String>;
|
||||||
}
|
}
|
||||||
@ -94,29 +121,23 @@ pub trait BeetsLibraryExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Beets library.
|
/// Beets library.
|
||||||
pub struct BeetsLibrary {
|
pub struct BeetsLibrary<BLE> {
|
||||||
executor: Box<dyn BeetsLibraryExecutor + Send + Sync>,
|
executor: BLE,
|
||||||
}
|
}
|
||||||
|
|
||||||
trait LibraryPrivate {
|
trait LibraryPrivate {
|
||||||
const CMD_LIST: &'static str;
|
|
||||||
const LIST_FORMAT_SEPARATOR: &'static str;
|
|
||||||
const LIST_FORMAT_ARG: &'static str;
|
|
||||||
const TRACK_FORMAT_FLAC: &'static str;
|
|
||||||
const TRACK_FORMAT_MP3: &'static str;
|
|
||||||
|
|
||||||
fn list_cmd_and_args(query: &Query) -> Vec<String>;
|
fn list_cmd_and_args(query: &Query) -> Vec<String>;
|
||||||
fn list_to_artists(list_output: Vec<String>) -> Result<Vec<Artist>, Error>;
|
fn list_to_artists(list_output: Vec<String>) -> Result<Vec<Artist>, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BeetsLibrary {
|
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. [`BeetsLibraryCommandExecutor`].
|
||||||
pub fn new(executor: Box<dyn BeetsLibraryExecutor + Send + Sync>) -> BeetsLibrary {
|
pub fn new(executor: BLE) -> Self {
|
||||||
BeetsLibrary { executor }
|
BeetsLibrary { executor }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Library for BeetsLibrary {
|
impl<BLE: BeetsLibraryExecutor> Library for BeetsLibrary<BLE> {
|
||||||
fn list(&mut self, query: &Query) -> Result<Vec<Artist>, Error> {
|
fn list(&mut self, query: &Query) -> Result<Vec<Artist>, Error> {
|
||||||
let cmd = Self::list_cmd_and_args(query);
|
let cmd = Self::list_cmd_and_args(query);
|
||||||
let output = self.executor.exec(&cmd)?;
|
let output = self.executor.exec(&cmd)?;
|
||||||
@ -124,37 +145,10 @@ impl Library for BeetsLibrary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! list_format_separator {
|
impl<BLE: BeetsLibraryExecutor> LibraryPrivate for BeetsLibrary<BLE> {
|
||||||
() => {
|
|
||||||
" -*^- "
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LibraryPrivate for BeetsLibrary {
|
|
||||||
const CMD_LIST: &'static str = "ls";
|
|
||||||
const LIST_FORMAT_SEPARATOR: &'static str = list_format_separator!();
|
|
||||||
const LIST_FORMAT_ARG: &'static str = concat!(
|
|
||||||
"--format=",
|
|
||||||
"$albumartist",
|
|
||||||
list_format_separator!(),
|
|
||||||
"$year",
|
|
||||||
list_format_separator!(),
|
|
||||||
"$album",
|
|
||||||
list_format_separator!(),
|
|
||||||
"$track",
|
|
||||||
list_format_separator!(),
|
|
||||||
"$title",
|
|
||||||
list_format_separator!(),
|
|
||||||
"$artist",
|
|
||||||
list_format_separator!(),
|
|
||||||
"$format"
|
|
||||||
);
|
|
||||||
const TRACK_FORMAT_FLAC: &'static str = "FLAC";
|
|
||||||
const TRACK_FORMAT_MP3: &'static str = "MP3";
|
|
||||||
|
|
||||||
fn list_cmd_and_args(query: &Query) -> Vec<String> {
|
fn list_cmd_and_args(query: &Query) -> Vec<String> {
|
||||||
let mut cmd: Vec<String> = vec![String::from(Self::CMD_LIST)];
|
let mut cmd: Vec<String> = vec![String::from(CMD_LIST)];
|
||||||
cmd.push(Self::LIST_FORMAT_ARG.to_string());
|
cmd.push(LIST_FORMAT_ARG.to_string());
|
||||||
cmd.append(&mut query.to_args());
|
cmd.append(&mut query.to_args());
|
||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
@ -168,7 +162,7 @@ impl LibraryPrivate for BeetsLibrary {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let split: Vec<&str> = line.split(Self::LIST_FORMAT_SEPARATOR).collect();
|
let split: Vec<&str> = line.split(LIST_FORMAT_SEPARATOR).collect();
|
||||||
if split.len() != 7 {
|
if split.len() != 7 {
|
||||||
return Err(Error::InvalidData(line.to_string()));
|
return Err(Error::InvalidData(line.to_string()));
|
||||||
}
|
}
|
||||||
@ -193,8 +187,8 @@ impl LibraryPrivate for BeetsLibrary {
|
|||||||
title: track_title,
|
title: track_title,
|
||||||
artist: track_artist.split("; ").map(|s| s.to_owned()).collect(),
|
artist: track_artist.split("; ").map(|s| s.to_owned()).collect(),
|
||||||
format: match track_format.as_ref() {
|
format: match track_format.as_ref() {
|
||||||
Self::TRACK_FORMAT_FLAC => TrackFormat::Flac,
|
TRACK_FORMAT_FLAC => TrackFormat::Flac,
|
||||||
Self::TRACK_FORMAT_MP3 => TrackFormat::Mp3,
|
TRACK_FORMAT_MP3 => TrackFormat::Mp3,
|
||||||
_ => return Err(Error::InvalidData(track_format)),
|
_ => return Err(Error::InvalidData(track_format)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -309,14 +303,14 @@ mod tests {
|
|||||||
let track_title = &track.title;
|
let track_title = &track.title;
|
||||||
let track_artist = &track.artist.join("; ");
|
let track_artist = &track.artist.join("; ");
|
||||||
let track_format = match track.format {
|
let track_format = match track.format {
|
||||||
TrackFormat::Flac => BeetsLibrary::TRACK_FORMAT_FLAC,
|
TrackFormat::Flac => TRACK_FORMAT_FLAC,
|
||||||
TrackFormat::Mp3 => BeetsLibrary::TRACK_FORMAT_MP3,
|
TrackFormat::Mp3 => TRACK_FORMAT_MP3,
|
||||||
};
|
};
|
||||||
|
|
||||||
strings.push(format!(
|
strings.push(format!(
|
||||||
"{album_artist}{0}{album_year}{0}{album_title}{0}\
|
"{album_artist}{0}{album_year}{0}{album_title}{0}\
|
||||||
{track_number}{0}{track_title}{0}{track_artist}{0}{track_format}",
|
{track_number}{0}{track_title}{0}{track_artist}{0}{track_format}",
|
||||||
BeetsLibrary::LIST_FORMAT_SEPARATOR,
|
LIST_FORMAT_SEPARATOR,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -356,7 +350,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_list_empty() {
|
fn test_list_empty() {
|
||||||
let arguments = vec!["ls".to_string(), BeetsLibrary::LIST_FORMAT_ARG.to_string()];
|
let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()];
|
||||||
let result = Ok(vec![]);
|
let result = Ok(vec![]);
|
||||||
|
|
||||||
let mut executor = MockBeetsLibraryExecutor::new();
|
let mut executor = MockBeetsLibraryExecutor::new();
|
||||||
@ -366,7 +360,7 @@ mod tests {
|
|||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|_| result);
|
.return_once(|_| result);
|
||||||
|
|
||||||
let mut beets = BeetsLibrary::new(Box::new(executor));
|
let mut beets = BeetsLibrary::new(executor);
|
||||||
let output = beets.list(&Query::default()).unwrap();
|
let output = beets.list(&Query::default()).unwrap();
|
||||||
|
|
||||||
let expected: Vec<Artist> = vec![];
|
let expected: Vec<Artist> = vec![];
|
||||||
@ -375,7 +369,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_list_ordered() {
|
fn test_list_ordered() {
|
||||||
let arguments = vec!["ls".to_string(), BeetsLibrary::LIST_FORMAT_ARG.to_string()];
|
let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()];
|
||||||
let expected = COLLECTION.to_owned();
|
let expected = COLLECTION.to_owned();
|
||||||
let result = Ok(artists_to_beets_string(&expected));
|
let result = Ok(artists_to_beets_string(&expected));
|
||||||
|
|
||||||
@ -386,7 +380,7 @@ mod tests {
|
|||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|_| result);
|
.return_once(|_| result);
|
||||||
|
|
||||||
let mut beets = BeetsLibrary::new(Box::new(executor));
|
let mut beets = BeetsLibrary::new(executor);
|
||||||
let output = beets.list(&Query::default()).unwrap();
|
let output = beets.list(&Query::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(output, expected);
|
assert_eq!(output, expected);
|
||||||
@ -394,7 +388,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_list_unordered() {
|
fn test_list_unordered() {
|
||||||
let arguments = vec!["ls".to_string(), BeetsLibrary::LIST_FORMAT_ARG.to_string()];
|
let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()];
|
||||||
let mut expected = COLLECTION.to_owned();
|
let mut expected = COLLECTION.to_owned();
|
||||||
let mut output = artists_to_beets_string(&expected);
|
let mut output = artists_to_beets_string(&expected);
|
||||||
let last = output.len() - 1;
|
let last = output.len() - 1;
|
||||||
@ -421,7 +415,7 @@ mod tests {
|
|||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|_| result);
|
.return_once(|_| result);
|
||||||
|
|
||||||
let mut beets = BeetsLibrary::new(Box::new(executor));
|
let mut beets = BeetsLibrary::new(executor);
|
||||||
let output = beets.list(&Query::default()).unwrap();
|
let output = beets.list(&Query::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(output, expected);
|
assert_eq!(output, expected);
|
||||||
@ -429,7 +423,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_list_album_title_year_clash() {
|
fn test_list_album_title_year_clash() {
|
||||||
let arguments = vec!["ls".to_string(), BeetsLibrary::LIST_FORMAT_ARG.to_string()];
|
let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()];
|
||||||
let mut expected = COLLECTION.to_owned();
|
let mut expected = COLLECTION.to_owned();
|
||||||
expected[0].albums[0].id.year = expected[1].albums[0].id.year;
|
expected[0].albums[0].id.year = expected[1].albums[0].id.year;
|
||||||
expected[0].albums[0].id.title = expected[1].albums[0].id.title.clone();
|
expected[0].albums[0].id.title = expected[1].albums[0].id.title.clone();
|
||||||
@ -443,7 +437,7 @@ mod tests {
|
|||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|_| result);
|
.return_once(|_| result);
|
||||||
|
|
||||||
let mut beets = BeetsLibrary::new(Box::new(executor));
|
let mut beets = BeetsLibrary::new(executor);
|
||||||
let output = beets.list(&Query::default()).unwrap();
|
let output = beets.list(&Query::default()).unwrap();
|
||||||
|
|
||||||
assert_eq!(output, expected);
|
assert_eq!(output, expected);
|
||||||
@ -458,7 +452,7 @@ mod tests {
|
|||||||
|
|
||||||
let arguments = vec![
|
let arguments = vec![
|
||||||
"ls".to_string(),
|
"ls".to_string(),
|
||||||
BeetsLibrary::LIST_FORMAT_ARG.to_string(),
|
LIST_FORMAT_ARG.to_string(),
|
||||||
String::from("^album:some.album"),
|
String::from("^album:some.album"),
|
||||||
String::from("track:5"),
|
String::from("track:5"),
|
||||||
String::from("artist:some.artist"),
|
String::from("artist:some.artist"),
|
||||||
@ -472,7 +466,7 @@ mod tests {
|
|||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|_| result);
|
.return_once(|_| result);
|
||||||
|
|
||||||
let mut beets = BeetsLibrary::new(Box::new(executor));
|
let mut beets = BeetsLibrary::new(executor);
|
||||||
let output = beets.list(&query).unwrap();
|
let output = beets.list(&query).unwrap();
|
||||||
|
|
||||||
let expected: Vec<Artist> = vec![];
|
let expected: Vec<Artist> = vec![];
|
||||||
|
@ -41,9 +41,9 @@ fn main() {
|
|||||||
// Create the application.
|
// Create the application.
|
||||||
let opt = Opt::from_args();
|
let opt = Opt::from_args();
|
||||||
|
|
||||||
let beets = BeetsLibrary::new(Box::new(
|
let beets = BeetsLibrary::new(
|
||||||
BeetsLibraryCommandExecutor::default().config(opt.beets_config_file_path.as_deref()),
|
BeetsLibraryCommandExecutor::default().config(opt.beets_config_file_path.as_deref()),
|
||||||
));
|
);
|
||||||
|
|
||||||
let database = JsonDatabase::new(Box::new(JsonDatabaseFileBackend::new(
|
let database = JsonDatabase::new(Box::new(JsonDatabaseFileBackend::new(
|
||||||
&opt.database_file_path,
|
&opt.database_file_path,
|
||||||
|
@ -15,18 +15,20 @@ use musichoard::{
|
|||||||
|
|
||||||
use crate::COLLECTION;
|
use crate::COLLECTION;
|
||||||
|
|
||||||
static BEETS_EMPTY_CONFIG: Lazy<Arc<Mutex<BeetsLibrary>>> = Lazy::new(|| {
|
static BEETS_EMPTY_CONFIG: Lazy<Arc<Mutex<BeetsLibrary<BeetsLibraryCommandExecutor>>>> =
|
||||||
Arc::new(Mutex::new(BeetsLibrary::new(Box::new(
|
Lazy::new(|| {
|
||||||
|
Arc::new(Mutex::new(BeetsLibrary::new(
|
||||||
BeetsLibraryCommandExecutor::default(),
|
BeetsLibraryCommandExecutor::default(),
|
||||||
))))
|
)))
|
||||||
});
|
});
|
||||||
|
|
||||||
static BEETS_TEST_CONFIG: Lazy<Arc<Mutex<BeetsLibrary>>> = Lazy::new(|| {
|
static BEETS_TEST_CONFIG: Lazy<Arc<Mutex<BeetsLibrary<BeetsLibraryCommandExecutor>>>> =
|
||||||
Arc::new(Mutex::new(BeetsLibrary::new(Box::new(
|
Lazy::new(|| {
|
||||||
|
Arc::new(Mutex::new(BeetsLibrary::new(
|
||||||
BeetsLibraryCommandExecutor::default().config(Some(
|
BeetsLibraryCommandExecutor::default().config(Some(
|
||||||
&fs::canonicalize("./tests/files/library/config.yml").unwrap(),
|
&fs::canonicalize("./tests/files/library/config.yml").unwrap(),
|
||||||
)),
|
)),
|
||||||
))))
|
)))
|
||||||
});
|
});
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
Loading…
Reference in New Issue
Block a user