Generic beets executor

This commit is contained in:
Wojciech Kozlowski 2023-04-13 14:44:30 +02:00
parent 5edcceeae5
commit 73b4cc8b28
3 changed files with 67 additions and 71 deletions

View File

@ -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![];

View File

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

View File

@ -15,19 +15,21 @@ 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]
fn test_no_config_list() { fn test_no_config_list() {