From 5ee8afc4c40a6bd415d0d7b645e20bd6bd44e5a9 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Wed, 29 Mar 2023 18:00:32 +0900 Subject: [PATCH 01/12] Add the library trait --- src/lib.rs | 1 + src/library/mod.rs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 src/library/mod.rs diff --git a/src/lib.rs b/src/lib.rs index 88c424c..78af023 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; pub mod database; +pub mod library; /// [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID). pub type Mbid = Uuid; diff --git a/src/library/mod.rs b/src/library/mod.rs new file mode 100644 index 0000000..3183450 --- /dev/null +++ b/src/library/mod.rs @@ -0,0 +1,17 @@ +use std::path::Path; + +use crate::ReleaseGroup; + +/// Options for refining library queries. +pub struct Query { + albumartist: Option, + artist: Option, + album: Option, + track: Option, + title: Option, +} + +/// Trait for interacting with the music library. +pub trait Library { + fn list(&self, album: bool, path: Path, format: &str, query: Query) -> Vec; +} -- 2.45.2 From 37f7a15dd077f9a2332b0d4eb90e4f0b843764fe Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Thu, 30 Mar 2023 21:41:25 +0900 Subject: [PATCH 02/12] Implement beets library --- src/database/json.rs | 100 +++---- src/lib.rs | 37 +-- src/library/beets.rs | 417 ++++++++++++++++++++++++++++ src/library/mod.rs | 55 +++- tests/files/database_json_test.json | 2 +- 5 files changed, 508 insertions(+), 103 deletions(-) create mode 100644 src/library/beets.rs diff --git a/src/database/json.rs b/src/database/json.rs index 7807a90..a625eef 100644 --- a/src/database/json.rs +++ b/src/database/json.rs @@ -5,7 +5,7 @@ use std::path::Path; use serde::de::DeserializeOwned; use serde::Serialize; -use crate::database::{DatabaseRead, DatabaseWrite}; +use super::{DatabaseRead, DatabaseWrite}; /// A JSON file database. pub struct DatabaseJson { @@ -63,80 +63,48 @@ mod tests { use super::*; - use crate::{Artist, Release, ReleaseGroup, ReleaseGroupType, Track}; + use crate::{Album, AlbumArtist, Track}; const TEST_FILENAME: &str = "tests/files/database_json_test.json"; - fn test_data() -> Vec { + fn test_data() -> Vec { vec![ - ReleaseGroup { - r#type: ReleaseGroupType::Album, - title: String::from("Release group A"), - artist: vec![Artist { + Album { + artist: AlbumArtist { name: String::from("Artist A"), mbid: Some(uuid!("f7769831-746b-4a12-8124-0123d7fe17c9")), - }], + }, year: 1998, - mbid: Some(uuid!("89efbf43-3395-4f6e-ac11-32c1ce514bb0")), - releases: vec![Release { - tracks: vec![ - Track { - number: 1, - title: String::from("Track A.1"), - artist: vec![Artist { - name: String::from("Artist A.A"), - mbid: Some(uuid!("b7f7163d-61d5-4c96-b305-005df54fb999")), - }], - mbid: None, - }, - Track { - number: 2, - title: String::from("Track A.2"), - artist: vec![Artist { - name: String::from("Artist A.A"), - mbid: Some(uuid!("b7f7163d-61d5-4c96-b305-005df54fb999")), - }], - mbid: None, - }, - Track { - number: 3, - title: String::from("Track A.3"), - artist: vec![ - Artist { - name: String::from("Artist A.A"), - mbid: Some(uuid!("b7f7163d-61d5-4c96-b305-005df54fb999")), - }, - Artist { - name: String::from("Artist A.B"), - mbid: Some(uuid!("6f6b46f2-4bb5-47e7-a8c8-03ebde30164f")), - }, - ], - mbid: None, - }, - ], - mbid: None, - }], + title: String::from("Release group A"), + tracks: vec![ + Track { + number: 1, + title: String::from("Track A.1"), + artist: vec![String::from("Artist A.A")], + }, + Track { + number: 2, + title: String::from("Track A.2"), + artist: vec![String::from("Artist A.A")], + }, + Track { + number: 3, + title: String::from("Track A.3"), + artist: vec![String::from("Artist A.A"), String::from("Artist A.B")], + }, + ], }, - ReleaseGroup { - r#type: ReleaseGroupType::Single, - title: String::from("Release group B"), - artist: vec![Artist { + Album { + artist: AlbumArtist { name: String::from("Artist B"), mbid: None, - }], + }, year: 2008, - mbid: None, - releases: vec![Release { - tracks: vec![Track { - number: 1, - title: String::from("Track B.1"), - artist: vec![Artist { - name: String::from("Artist B.A"), - mbid: Some(uuid!("d927e216-2e63-415c-acec-bf9f1abd3e3c")), - }], - mbid: Some(uuid!("dacc9ce4-118c-4c92-aed7-1ebe4c7543b5")), - }], - mbid: Some(uuid!("ac7b642d-8b71-4588-a694-e5ae43fac873")), + title: String::from("Release group B"), + tracks: vec![Track { + number: 1, + title: String::from("Track B.1"), + artist: vec![String::from("Artist B.A")], }], }, ] @@ -168,7 +136,7 @@ mod tests { #[test] fn read() { - let mut read_data: Vec = vec![]; + let mut read_data: Vec = vec![]; DatabaseJson::reader(Path::new(TEST_FILENAME)) .unwrap() .read(&mut read_data) @@ -179,7 +147,7 @@ mod tests { #[test] fn reverse() { let write_data = test_data(); - let mut read_data: Vec = vec![]; + let mut read_data: Vec = vec![]; let temp_file = NamedTempFile::new().unwrap(); diff --git a/src/lib.rs b/src/lib.rs index 78af023..30de129 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,45 +9,26 @@ pub mod library; /// [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID). pub type Mbid = Uuid; -/// [Artist](https://musicbrainz.org/doc/Artist). +/// An album artist. Carries a MBID to facilitate discography access. #[derive(Debug, Deserialize, Serialize, PartialEq)] -pub struct Artist { +pub struct AlbumArtist { pub name: String, pub mbid: Option, } -/// [Track](https://musicbrainz.org/doc/Track). +/// A single track on an album. #[derive(Debug, Deserialize, Serialize, PartialEq)] pub struct Track { pub number: u32, pub title: String, - pub artist: Vec, - pub mbid: Option, + pub artist: Vec, } -/// [Release](https://musicbrainz.org/doc/Release). +/// An album is a collection of tracks that were released together. #[derive(Debug, Deserialize, Serialize, PartialEq)] -pub struct Release { - pub tracks: Vec, - pub mbid: Option, -} - -/// [Release group primary type](https://musicbrainz.org/doc/Release_Group/Type). -#[derive(Debug, Deserialize, Serialize, PartialEq)] -pub enum ReleaseGroupType { - Album, - Ep, - Single, - Other, -} - -/// [Release group](https://musicbrainz.org/doc/Release_Group). -#[derive(Debug, Deserialize, Serialize, PartialEq)] -pub struct ReleaseGroup { - pub r#type: ReleaseGroupType, - pub title: String, - pub artist: Vec, +pub struct Album { + pub artist: AlbumArtist, pub year: u32, - pub mbid: Option, - pub releases: Vec, + pub title: String, + pub tracks: Vec, } diff --git a/src/library/beets.rs b/src/library/beets.rs new file mode 100644 index 0000000..bb44b60 --- /dev/null +++ b/src/library/beets.rs @@ -0,0 +1,417 @@ +use std::{collections::HashSet, fmt::Display, process::Command}; + +use crate::{Album, AlbumArtist, Track}; + +use super::{Error, Library, Query, QueryValue}; + +impl QueryValue { + fn to_string(&self, option_name: &str) -> String { + let negate = if self.negate { "^" } else { "" }; + format!("{}{}:{}", negate, option_name, self.value) + } +} + +impl Query { + fn to_args(&self) -> Vec { + let mut arguments: Vec = vec![]; + + if let Some(ref albumartist) = self.albumartist { + arguments.push(albumartist.to_string("albumartist")); + }; + + if let Some(ref album) = self.album { + arguments.push(album.to_string("album")); + }; + + if let Some(ref track) = self.track { + arguments.push(track.to_string("track")); + }; + + if let Some(ref title) = self.title { + arguments.push(title.to_string("title")); + }; + + if let Some(ref artist) = self.artist { + arguments.push(artist.to_string("artist")); + }; + + arguments + } +} + +pub trait BeetsExecutor { + fn exec(&mut self, arguments: Vec) -> Result, Error>; +} + +pub struct Beets { + executor: Box, +} + +pub struct SystemExecutor { + bin: String, +} + +impl SystemExecutor { + pub fn new(bin: &str) -> SystemExecutor { + SystemExecutor { + bin: bin.to_string(), + } + } +} + +impl Default for SystemExecutor { + fn default() -> Self { + SystemExecutor::new("beet") + } +} + +impl BeetsExecutor for SystemExecutor { + fn exec(&mut self, arguments: Vec) -> Result, Error> { + let output = Command::new(&self.bin).args(arguments).output()?; + let output = std::str::from_utf8(&output.stdout)?; + Ok(output.split('\n').map(|s| s.to_string()).collect()) + } +} + +#[derive(Debug, Hash, Eq, PartialEq)] +struct AlbumId { + artist: String, + year: u32, + title: String, +} + +impl AlbumId { + fn matches(&self, album: &Album) -> bool { + (self.artist == album.artist.name) + && (self.year == album.year) + && (self.title == album.title) + } +} + +macro_rules! separator { + () => { + "-*^-" + }; +} + +impl Beets { + const SEPARATOR: &str = separator!(); + const LIST_FORMAT: &str = concat!( + "--format=", + "$albumartist", + separator!(), + "$year", + separator!(), + "$album", + separator!(), + "$track", + separator!(), + "$title", + separator!(), + "$artist" + ); + + pub fn new(executor: Box) -> Beets { + Beets { executor } + } + + fn list_to_albums(list_output: Vec) -> Result, Error> { + let mut albums: Vec = vec![]; + let mut album_ids = HashSet::::new(); + + for line in list_output.iter() { + let split: Vec<&str> = line.split(Self::SEPARATOR).collect(); + + if split.len() != 6 { + return Err(Error::InvalidData(line.to_string())); + } + + let album_artist = split[0].to_string(); + let album_year = split[1].parse::()?; + let album_title = split[2].to_string(); + let track_number = split[3].parse::()?; + let track_title = split[4].to_string(); + let track_artist = split[5].to_string(); + + let track = Track { + number: track_number, + title: track_title, + artist: track_artist.split("; ").map(|s| s.to_owned()).collect(), + }; + + let aid = AlbumId { + artist: album_artist, + year: album_year, + title: album_title, + }; + + if album_ids.contains(&aid) { + // Beets returns results in order so we look from the back. + let album = albums.iter_mut().rev().find(|a| aid.matches(a)).unwrap(); + album.tracks.push(track); + } else { + let album_artist = AlbumArtist { + name: aid.artist.to_string(), + mbid: None, + }; + let album_title = aid.title.to_string(); + album_ids.insert(aid); + albums.push(Album { + artist: album_artist, + year: album_year, + title: album_title, + tracks: vec![track], + }); + } + } + Ok(albums) + } +} + +impl Library for Beets { + fn list(&mut self, query: &Query) -> Result, Error> { + let mut arguments: Vec = vec![String::from("ls")]; + arguments.push(Self::LIST_FORMAT.to_string()); + arguments.append(&mut query.to_args()); + + let output = self.executor.exec(arguments)?; + + Self::list_to_albums(output) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct TestExecutor { + arguments: Option>, + output: Option, Error>>, + } + + impl BeetsExecutor for TestExecutor { + fn exec(&mut self, arguments: Vec) -> Result, Error> { + if self.arguments.is_some() { + assert_eq!(self.arguments.take().unwrap(), arguments); + } + self.output.take().unwrap() + } + } + + fn test_data() -> Vec { + vec![ + Album { + artist: AlbumArtist { + name: "album_artist.a".to_string(), + mbid: None, + }, + year: 1998, + title: "album_title.a".to_string(), + tracks: vec![ + Track { + number: 1, + title: "track.a.1".to_string(), + artist: vec!["artist.a.1".to_string()], + }, + Track { + number: 2, + title: "track.a.2".to_string(), + artist: vec!["artist.a.2.1".to_string(), "artist.a.2.2".to_string()], + }, + Track { + number: 3, + title: "track.a.3".to_string(), + artist: vec!["artist.a.3".to_string()], + }, + ], + }, + Album { + artist: AlbumArtist { + name: "album_artist.b".to_string(), + mbid: None, + }, + year: 2003, + title: "album_title.b".to_string(), + tracks: vec![ + Track { + number: 1, + title: "track.b.1".to_string(), + artist: vec!["artist.b.1".to_string()], + }, + Track { + number: 2, + title: "track.b.2".to_string(), + artist: vec!["artist.b.2.1".to_string(), "artist.b.2.2".to_string()], + }, + ], + }, + ] + } + + fn album_to_beets_string(album: &Album) -> Vec { + let album_artist = &album.artist.name; + let album_year = &album.year; + let album_title = &album.title; + + let mut strings = vec![]; + for track in album.tracks.iter() { + let track_number = &track.number; + let track_title = &track.title; + let track_artist = &track.artist.join("; "); + + strings.push(format!( + "{album_artist}{0}{album_year}{0}{album_title}{0}\ + {track_number}{0}{track_title}{0}{track_artist}", + Beets::SEPARATOR, + )); + } + + strings + } + + #[test] + fn test_query() { + let query = Query { + albumartist: None, + album: Some(QueryValue { + negate: true, + value: String::from("some.album"), + }), + track: Some(QueryValue { + negate: false, + value: 5, + }), + title: None, + artist: Some(QueryValue { + negate: false, + value: String::from("some.artist"), + }), + }; + + assert_eq!( + query.to_args(), + vec![ + String::from("^album:some.album"), + String::from("track:5"), + String::from("artist:some.artist") + ] + ); + } + + #[test] + fn test_list_empty() { + let executor = TestExecutor { + arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT.to_string()]), + output: Some(Ok(vec![])), + }; + let mut beets = Beets::new(Box::new(executor)); + let output = beets.list(&Query::default()).unwrap(); + + let expected: Vec = vec![]; + assert_eq!(output, expected); + } + + #[test] + fn test_list_ordered() { + let expected = test_data(); + let mut output = vec![]; + for album in expected.iter() { + output.append(&mut album_to_beets_string(album)); + } + + let executor = TestExecutor { + arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT.to_string()]), + output: Some(Ok(output)), + }; + let mut beets = Beets::new(Box::new(executor)); + let output = beets.list(&Query::default()).unwrap(); + + assert_eq!(output, expected); + } + + #[test] + fn test_list_unordered() { + let mut expected = test_data(); + let mut output = vec![]; + for album in expected.iter() { + output.append(&mut album_to_beets_string(album)); + } + let last = output.len() - 1; + output.swap(0, last); + + // Putting the last track first will make its entire album come first in the output. + expected.rotate_right(1); + + // Same applies to that album's tracks. + expected[0].tracks.rotate_right(1); + + // And the (now) second album's tracks first track comes last. + expected[1].tracks.rotate_left(1); + + let executor = TestExecutor { + arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT.to_string()]), + output: Some(Ok(output)), + }; + let mut beets = Beets::new(Box::new(executor)); + let output = beets.list(&Query::default()).unwrap(); + + assert_eq!(output, expected); + } + + #[test] + fn test_list_album_title_year_clash() { + let mut expected = test_data(); + expected[1].year = expected[0].year; + expected[1].title = expected[0].title.clone(); + + let mut output = vec![]; + for album in expected.iter() { + output.append(&mut album_to_beets_string(album)); + } + + let executor = TestExecutor { + arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT.to_string()]), + output: Some(Ok(output)), + }; + let mut beets = Beets::new(Box::new(executor)); + let output = beets.list(&Query::default()).unwrap(); + + assert_eq!(output, expected); + } + + #[test] + fn test_list_query() { + let query = Query { + albumartist: None, + album: Some(QueryValue { + negate: true, + value: String::from("some.album"), + }), + track: Some(QueryValue { + negate: false, + value: 5, + }), + title: None, + artist: Some(QueryValue { + negate: false, + value: String::from("some.artist"), + }), + }; + + let executor = TestExecutor { + arguments: Some(vec![ + "ls".to_string(), + Beets::LIST_FORMAT.to_string(), + String::from("^album:some.album"), + String::from("track:5"), + String::from("artist:some.artist"), + ]), + output: Some(Ok(vec![])), + }; + let mut beets = Beets::new(Box::new(executor)); + let output = beets.list(&query).unwrap(); + + let expected: Vec = vec![]; + assert_eq!(output, expected); + } +} diff --git a/src/library/mod.rs b/src/library/mod.rs index 3183450..c6cfde9 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -1,17 +1,56 @@ -use std::path::Path; +use std::{num::ParseIntError, str::Utf8Error}; -use crate::ReleaseGroup; +use crate::Album; + +pub mod beets; + +pub struct QueryValue { + negate: bool, + value: T, +} /// Options for refining library queries. +#[derive(Default)] pub struct Query { - albumartist: Option, - artist: Option, - album: Option, - track: Option, - title: Option, + albumartist: Option>, + album: Option>, + track: Option>, + title: Option>, + artist: Option>, +} + +/// Error type for library calls. +#[derive(Debug)] +pub enum Error { + /// The underlying library returned invalid data. + InvalidData(String), + /// The underlying library experienced an I/O error. + IoError(String), + /// The underlying library failed to parse an integer. + ParseIntError(String), + /// The underlying library failed to parse a UTF-8 string. + Utf8Error(String), +} + +impl From for Error { + fn from(err: std::io::Error) -> Error { + Error::IoError(err.to_string()) + } +} + +impl From for Error { + fn from(err: ParseIntError) -> Error { + Error::ParseIntError(err.to_string()) + } +} + +impl From for Error { + fn from(err: Utf8Error) -> Error { + Error::Utf8Error(err.to_string()) + } } /// Trait for interacting with the music library. pub trait Library { - fn list(&self, album: bool, path: Path, format: &str, query: Query) -> Vec; + fn list(&mut self, query: &Query) -> Result, Error>; } diff --git a/tests/files/database_json_test.json b/tests/files/database_json_test.json index 4e3f7fc..5f2899a 100644 --- a/tests/files/database_json_test.json +++ b/tests/files/database_json_test.json @@ -1 +1 @@ -[{"type":"Album","title":"Release group A","artist":[{"name":"Artist A","mbid":"f7769831-746b-4a12-8124-0123d7fe17c9"}],"year":1998,"mbid":"89efbf43-3395-4f6e-ac11-32c1ce514bb0","releases":[{"tracks":[{"number":1,"title":"Track A.1","artist":[{"name":"Artist A.A","mbid":"b7f7163d-61d5-4c96-b305-005df54fb999"}],"mbid":null},{"number":2,"title":"Track A.2","artist":[{"name":"Artist A.A","mbid":"b7f7163d-61d5-4c96-b305-005df54fb999"}],"mbid":null},{"number":3,"title":"Track A.3","artist":[{"name":"Artist A.A","mbid":"b7f7163d-61d5-4c96-b305-005df54fb999"},{"name":"Artist A.B","mbid":"6f6b46f2-4bb5-47e7-a8c8-03ebde30164f"}],"mbid":null}],"mbid":null}]},{"type":"Single","title":"Release group B","artist":[{"name":"Artist B","mbid":null}],"year":2008,"mbid":null,"releases":[{"tracks":[{"number":1,"title":"Track B.1","artist":[{"name":"Artist B.A","mbid":"d927e216-2e63-415c-acec-bf9f1abd3e3c"}],"mbid":"dacc9ce4-118c-4c92-aed7-1ebe4c7543b5"}],"mbid":"ac7b642d-8b71-4588-a694-e5ae43fac873"}]}] \ No newline at end of file +[{"artist":{"name":"Artist A","mbid":"f7769831-746b-4a12-8124-0123d7fe17c9"},"year":1998,"title":"Release group A","tracks":[{"number":1,"title":"Track A.1","artist":["Artist A.A"]},{"number":2,"title":"Track A.2","artist":["Artist A.A"]},{"number":3,"title":"Track A.3","artist":["Artist A.A","Artist A.B"]}]},{"artist":{"name":"Artist B","mbid":null},"year":2008,"title":"Release group B","tracks":[{"number":1,"title":"Track B.1","artist":["Artist B.A"]}]}] \ No newline at end of file -- 2.45.2 From d5572644eeb93537024c6b7af1b006c23c2ef6fc Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Thu, 30 Mar 2023 23:34:23 +0900 Subject: [PATCH 03/12] Clean up QueryOption --- src/database/json.rs | 13 +-- src/lib.rs | 13 +-- src/library/beets.rs | 121 +++++++++++++--------------- src/library/mod.rs | 87 ++++++++++++++++++-- tests/files/database_json_test.json | 2 +- 5 files changed, 142 insertions(+), 94 deletions(-) diff --git a/src/database/json.rs b/src/database/json.rs index a625eef..9d9f939 100644 --- a/src/database/json.rs +++ b/src/database/json.rs @@ -59,21 +59,17 @@ impl DatabaseWrite for DatabaseJson { mod tests { use std::path::Path; use tempfile::NamedTempFile; - use uuid::uuid; use super::*; - use crate::{Album, AlbumArtist, Track}; + use crate::{Album, Track}; const TEST_FILENAME: &str = "tests/files/database_json_test.json"; fn test_data() -> Vec { vec![ Album { - artist: AlbumArtist { - name: String::from("Artist A"), - mbid: Some(uuid!("f7769831-746b-4a12-8124-0123d7fe17c9")), - }, + artist: String::from("Artist A"), year: 1998, title: String::from("Release group A"), tracks: vec![ @@ -95,10 +91,7 @@ mod tests { ], }, Album { - artist: AlbumArtist { - name: String::from("Artist B"), - mbid: None, - }, + artist: String::from("Artist B"), year: 2008, title: String::from("Release group B"), tracks: vec![Track { diff --git a/src/lib.rs b/src/lib.rs index 30de129..ba9f299 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,15 +9,8 @@ pub mod library; /// [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID). pub type Mbid = Uuid; -/// An album artist. Carries a MBID to facilitate discography access. -#[derive(Debug, Deserialize, Serialize, PartialEq)] -pub struct AlbumArtist { - pub name: String, - pub mbid: Option, -} - /// A single track on an album. -#[derive(Debug, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct Track { pub number: u32, pub title: String, @@ -25,9 +18,9 @@ pub struct Track { } /// An album is a collection of tracks that were released together. -#[derive(Debug, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct Album { - pub artist: AlbumArtist, + pub artist: String, pub year: u32, pub title: String, pub tracks: Vec, diff --git a/src/library/beets.rs b/src/library/beets.rs index bb44b60..13679d5 100644 --- a/src/library/beets.rs +++ b/src/library/beets.rs @@ -1,13 +1,32 @@ use std::{collections::HashSet, fmt::Display, process::Command}; -use crate::{Album, AlbumArtist, Track}; +use crate::{Album, Track}; -use super::{Error, Library, Query, QueryValue}; +use super::{Error, Library, Query, QueryOption}; -impl QueryValue { - fn to_string(&self, option_name: &str) -> String { - let negate = if self.negate { "^" } else { "" }; - format!("{}{}:{}", negate, option_name, self.value) +pub trait SimpleOption {} +impl SimpleOption for String {} +impl SimpleOption for u32 {} + +impl QueryOption { + fn to_arg(&self, option_name: &str) -> Option { + let (negate, value) = match self { + Self::Include(value) => ("", value), + Self::Exclude(value) => ("^", value), + Self::None => return None, + }; + Some(format!("{}{}:{}", negate, option_name, value)) + } +} + +impl QueryOption> { + fn to_arg(&self, option_name: &str) -> Option { + let (negate, vec) = match self { + Self::Include(value) => ("", value), + Self::Exclude(value) => ("^", value), + Self::None => return None, + }; + Some(format!("{}{}:{}", negate, option_name, vec.join("; "))) } } @@ -15,24 +34,28 @@ impl Query { fn to_args(&self) -> Vec { let mut arguments: Vec = vec![]; - if let Some(ref albumartist) = self.albumartist { - arguments.push(albumartist.to_string("albumartist")); + if let Some(album_artist) = self.album_artist.to_arg("albumartist") { + arguments.push(album_artist); }; - if let Some(ref album) = self.album { - arguments.push(album.to_string("album")); + if let Some(album_year) = self.album_year.to_arg("year") { + arguments.push(album_year); }; - if let Some(ref track) = self.track { - arguments.push(track.to_string("track")); + if let Some(album_title) = self.album_title.to_arg("album") { + arguments.push(album_title); }; - if let Some(ref title) = self.title { - arguments.push(title.to_string("title")); + if let Some(track_number) = self.track_number.to_arg("track") { + arguments.push(track_number); }; - if let Some(ref artist) = self.artist { - arguments.push(artist.to_string("artist")); + if let Some(track_title) = self.track_title.to_arg("title") { + arguments.push(track_title); + }; + + if let Some(track_artist) = self.track_artist.to_arg("artist") { + arguments.push(track_artist); }; arguments @@ -82,9 +105,7 @@ struct AlbumId { impl AlbumId { fn matches(&self, album: &Album) -> bool { - (self.artist == album.artist.name) - && (self.year == album.year) - && (self.title == album.title) + (self.artist == album.artist) && (self.year == album.year) && (self.title == album.title) } } @@ -150,10 +171,7 @@ impl Beets { let album = albums.iter_mut().rev().find(|a| aid.matches(a)).unwrap(); album.tracks.push(track); } else { - let album_artist = AlbumArtist { - name: aid.artist.to_string(), - mbid: None, - }; + let album_artist = aid.artist.to_string(); let album_title = aid.title.to_string(); album_ids.insert(aid); albums.push(Album { @@ -201,10 +219,7 @@ mod tests { fn test_data() -> Vec { vec![ Album { - artist: AlbumArtist { - name: "album_artist.a".to_string(), - mbid: None, - }, + artist: "album_artist.a".to_string(), year: 1998, title: "album_title.a".to_string(), tracks: vec![ @@ -226,10 +241,7 @@ mod tests { ], }, Album { - artist: AlbumArtist { - name: "album_artist.b".to_string(), - mbid: None, - }, + artist: "album_artist.b".to_string(), year: 2003, title: "album_title.b".to_string(), tracks: vec![ @@ -249,7 +261,7 @@ mod tests { } fn album_to_beets_string(album: &Album) -> Vec { - let album_artist = &album.artist.name; + let album_artist = &album.artist; let album_year = &album.year; let album_title = &album.title; @@ -271,29 +283,20 @@ mod tests { #[test] fn test_query() { - let query = Query { - albumartist: None, - album: Some(QueryValue { - negate: true, - value: String::from("some.album"), - }), - track: Some(QueryValue { - negate: false, - value: 5, - }), - title: None, - artist: Some(QueryValue { - negate: false, - value: String::from("some.artist"), - }), - }; + let query = Query::new() + .album_title(QueryOption::exclude(String::from("some.album"))) + .track_number(QueryOption::include(5)) + .track_artist(QueryOption::include(vec![ + String::from("some.artist.1"), + String::from("some.artist.2"), + ])); assert_eq!( query.to_args(), vec![ String::from("^album:some.album"), String::from("track:5"), - String::from("artist:some.artist") + String::from("artist:some.artist.1; some.artist.2") ] ); } @@ -381,22 +384,10 @@ mod tests { #[test] fn test_list_query() { - let query = Query { - albumartist: None, - album: Some(QueryValue { - negate: true, - value: String::from("some.album"), - }), - track: Some(QueryValue { - negate: false, - value: 5, - }), - title: None, - artist: Some(QueryValue { - negate: false, - value: String::from("some.artist"), - }), - }; + let query = Query::new() + .album_title(QueryOption::exclude(String::from("some.album"))) + .track_number(QueryOption::include(5)) + .track_artist(QueryOption::include(vec![String::from("some.artist")])); let executor = TestExecutor { arguments: Some(vec![ diff --git a/src/library/mod.rs b/src/library/mod.rs index c6cfde9..54ef0e4 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -4,19 +4,90 @@ use crate::Album; pub mod beets; -pub struct QueryValue { - negate: bool, - value: T, +/// A single query option. +pub enum QueryOption { + /// Inclusive query. + Include(T), + /// Exclusive query. + Exclude(T), + /// No query. + None, +} + +impl QueryOption { + /// Create an inclusive query option. + pub fn include(value: T) -> Self { + QueryOption::Include(value) + } + + pub fn exclude(value: T) -> Self { + QueryOption::Exclude(value) + } + + pub fn none() -> Self { + QueryOption::None + } + + pub fn is_some(&self) -> bool { + !matches!(self, QueryOption::None) + } + + pub fn is_none(&self) -> bool { + matches!(self, QueryOption::None) + } +} + +impl Default for QueryOption { + fn default() -> Self { + Self::none() + } } /// Options for refining library queries. #[derive(Default)] pub struct Query { - albumartist: Option>, - album: Option>, - track: Option>, - title: Option>, - artist: Option>, + album_artist: QueryOption, + album_year: QueryOption, + album_title: QueryOption, + track_number: QueryOption, + track_title: QueryOption, + track_artist: QueryOption>, +} + +impl Query { + pub fn new() -> Self { + Query::default() + } + + pub fn album_artist(mut self, album_artist: QueryOption) -> Self { + self.album_artist = album_artist; + self + } + + pub fn album_year(mut self, album_year: QueryOption) -> Self { + self.album_year = album_year; + self + } + + pub fn album_title(mut self, album_title: QueryOption) -> Self { + self.album_title = album_title; + self + } + + pub fn track_number(mut self, track_number: QueryOption) -> Self { + self.track_number = track_number; + self + } + + pub fn track_title(mut self, track_title: QueryOption) -> Self { + self.track_title = track_title; + self + } + + pub fn track_artist(mut self, track_artist: QueryOption>) -> Self { + self.track_artist = track_artist; + self + } } /// Error type for library calls. diff --git a/tests/files/database_json_test.json b/tests/files/database_json_test.json index 5f2899a..557e3c8 100644 --- a/tests/files/database_json_test.json +++ b/tests/files/database_json_test.json @@ -1 +1 @@ -[{"artist":{"name":"Artist A","mbid":"f7769831-746b-4a12-8124-0123d7fe17c9"},"year":1998,"title":"Release group A","tracks":[{"number":1,"title":"Track A.1","artist":["Artist A.A"]},{"number":2,"title":"Track A.2","artist":["Artist A.A"]},{"number":3,"title":"Track A.3","artist":["Artist A.A","Artist A.B"]}]},{"artist":{"name":"Artist B","mbid":null},"year":2008,"title":"Release group B","tracks":[{"number":1,"title":"Track B.1","artist":["Artist B.A"]}]}] \ No newline at end of file +[{"artist":"Artist A","year":1998,"title":"Release group A","tracks":[{"number":1,"title":"Track A.1","artist":["Artist A.A"]},{"number":2,"title":"Track A.2","artist":["Artist A.A"]},{"number":3,"title":"Track A.3","artist":["Artist A.A","Artist A.B"]}]},{"artist":"Artist B","year":2008,"title":"Release group B","tracks":[{"number":1,"title":"Track B.1","artist":["Artist B.A"]}]}] \ No newline at end of file -- 2.45.2 From dae9ed791e9ed52055314b7ac135ee60b1502dc4 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Thu, 30 Mar 2023 23:42:48 +0900 Subject: [PATCH 04/12] Use traits to keep things private --- src/library/beets.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/library/beets.rs b/src/library/beets.rs index 13679d5..26aab1c 100644 --- a/src/library/beets.rs +++ b/src/library/beets.rs @@ -4,11 +4,15 @@ use crate::{Album, Track}; use super::{Error, Library, Query, QueryOption}; -pub trait SimpleOption {} +trait QueryOptionArgBeets { + fn to_arg(&self, option_name: &str) -> Option; +} + +trait SimpleOption {} impl SimpleOption for String {} impl SimpleOption for u32 {} -impl QueryOption { +impl QueryOptionArgBeets for QueryOption { fn to_arg(&self, option_name: &str) -> Option { let (negate, value) = match self { Self::Include(value) => ("", value), @@ -19,7 +23,7 @@ impl QueryOption { } } -impl QueryOption> { +impl QueryOptionArgBeets for QueryOption> { fn to_arg(&self, option_name: &str) -> Option { let (negate, vec) = match self { Self::Include(value) => ("", value), @@ -30,7 +34,11 @@ impl QueryOption> { } } -impl Query { +trait QueryArgsBeets { + fn to_args(&self) -> Vec; +} + +impl QueryArgsBeets for Query { fn to_args(&self) -> Vec { let mut arguments: Vec = vec![]; -- 2.45.2 From 9ab00fb1de4352f9b7e77bfcdc0e1b7222545135 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Thu, 30 Mar 2023 23:45:58 +0900 Subject: [PATCH 05/12] Shifting code --- src/library/beets.rs | 52 ++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/library/beets.rs b/src/library/beets.rs index 26aab1c..d45599d 100644 --- a/src/library/beets.rs +++ b/src/library/beets.rs @@ -78,32 +78,6 @@ pub struct Beets { executor: Box, } -pub struct SystemExecutor { - bin: String, -} - -impl SystemExecutor { - pub fn new(bin: &str) -> SystemExecutor { - SystemExecutor { - bin: bin.to_string(), - } - } -} - -impl Default for SystemExecutor { - fn default() -> Self { - SystemExecutor::new("beet") - } -} - -impl BeetsExecutor for SystemExecutor { - fn exec(&mut self, arguments: Vec) -> Result, Error> { - let output = Command::new(&self.bin).args(arguments).output()?; - let output = std::str::from_utf8(&output.stdout)?; - Ok(output.split('\n').map(|s| s.to_string()).collect()) - } -} - #[derive(Debug, Hash, Eq, PartialEq)] struct AlbumId { artist: String, @@ -206,6 +180,32 @@ impl Library for Beets { } } +pub struct SystemExecutor { + bin: String, +} + +impl SystemExecutor { + pub fn new(bin: &str) -> SystemExecutor { + SystemExecutor { + bin: bin.to_string(), + } + } +} + +impl Default for SystemExecutor { + fn default() -> Self { + SystemExecutor::new("beet") + } +} + +impl BeetsExecutor for SystemExecutor { + fn exec(&mut self, arguments: Vec) -> Result, Error> { + let output = Command::new(&self.bin).args(arguments).output()?; + let output = std::str::from_utf8(&output.stdout)?; + Ok(output.split('\n').map(|s| s.to_string()).collect()) + } +} + #[cfg(test)] mod tests { use super::*; -- 2.45.2 From 6dacdd3742db5d0951c0c71593d895585d239ed9 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Thu, 30 Mar 2023 23:54:11 +0900 Subject: [PATCH 06/12] Simplify album id --- src/database/json.rs | 18 ++++++---- src/lib.rs | 11 +++++-- src/library/beets.rs | 51 +++++++++++++---------------- tests/files/database_json_test.json | 2 +- 4 files changed, 42 insertions(+), 40 deletions(-) diff --git a/src/database/json.rs b/src/database/json.rs index 9d9f939..98bdb74 100644 --- a/src/database/json.rs +++ b/src/database/json.rs @@ -62,16 +62,18 @@ mod tests { use super::*; - use crate::{Album, Track}; + use crate::{Album, AlbumId, Track}; const TEST_FILENAME: &str = "tests/files/database_json_test.json"; fn test_data() -> Vec { vec![ Album { - artist: String::from("Artist A"), - year: 1998, - title: String::from("Release group A"), + id: AlbumId { + artist: String::from("Artist A"), + year: 1998, + title: String::from("Release group A"), + }, tracks: vec![ Track { number: 1, @@ -91,9 +93,11 @@ mod tests { ], }, Album { - artist: String::from("Artist B"), - year: 2008, - title: String::from("Release group B"), + id: AlbumId { + artist: String::from("Artist B"), + year: 2008, + title: String::from("Release group B"), + }, tracks: vec![Track { number: 1, title: String::from("Track B.1"), diff --git a/src/lib.rs b/src/lib.rs index ba9f299..bbe5a8d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,11 +17,16 @@ pub struct Track { pub artist: Vec, } -/// An album is a collection of tracks that were released together. -#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] -pub struct Album { +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] +pub struct AlbumId { pub artist: String, pub year: u32, pub title: String, +} + +/// An album is a collection of tracks that were released together. +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct Album { + pub id: AlbumId, pub tracks: Vec, } diff --git a/src/library/beets.rs b/src/library/beets.rs index d45599d..6d19e8c 100644 --- a/src/library/beets.rs +++ b/src/library/beets.rs @@ -1,6 +1,6 @@ use std::{collections::HashSet, fmt::Display, process::Command}; -use crate::{Album, Track}; +use crate::{Album, AlbumId, Track}; use super::{Error, Library, Query, QueryOption}; @@ -78,19 +78,6 @@ pub struct Beets { executor: Box, } -#[derive(Debug, Hash, Eq, PartialEq)] -struct AlbumId { - artist: String, - year: u32, - title: String, -} - -impl AlbumId { - fn matches(&self, album: &Album) -> bool { - (self.artist == album.artist) && (self.year == album.year) && (self.title == album.title) - } -} - macro_rules! separator { () => { "-*^-" @@ -150,16 +137,18 @@ impl Beets { if album_ids.contains(&aid) { // Beets returns results in order so we look from the back. - let album = albums.iter_mut().rev().find(|a| aid.matches(a)).unwrap(); + let album = albums.iter_mut().rev().find(|a| a.id == aid).unwrap(); album.tracks.push(track); } else { let album_artist = aid.artist.to_string(); let album_title = aid.title.to_string(); album_ids.insert(aid); albums.push(Album { - artist: album_artist, - year: album_year, - title: album_title, + id: AlbumId { + artist: album_artist, + year: album_year, + title: album_title, + }, tracks: vec![track], }); } @@ -227,9 +216,11 @@ mod tests { fn test_data() -> Vec { vec![ Album { - artist: "album_artist.a".to_string(), - year: 1998, - title: "album_title.a".to_string(), + id: AlbumId { + artist: "album_artist.a".to_string(), + year: 1998, + title: "album_title.a".to_string(), + }, tracks: vec![ Track { number: 1, @@ -249,9 +240,11 @@ mod tests { ], }, Album { - artist: "album_artist.b".to_string(), - year: 2003, - title: "album_title.b".to_string(), + id: AlbumId { + artist: "album_artist.b".to_string(), + year: 2003, + title: "album_title.b".to_string(), + }, tracks: vec![ Track { number: 1, @@ -269,9 +262,9 @@ mod tests { } fn album_to_beets_string(album: &Album) -> Vec { - let album_artist = &album.artist; - let album_year = &album.year; - let album_title = &album.title; + let album_artist = &album.id.artist; + let album_year = &album.id.year; + let album_title = &album.id.title; let mut strings = vec![]; for track in album.tracks.iter() { @@ -372,8 +365,8 @@ mod tests { #[test] fn test_list_album_title_year_clash() { let mut expected = test_data(); - expected[1].year = expected[0].year; - expected[1].title = expected[0].title.clone(); + expected[1].id.year = expected[0].id.year; + expected[1].id.title = expected[0].id.title.clone(); let mut output = vec![]; for album in expected.iter() { diff --git a/tests/files/database_json_test.json b/tests/files/database_json_test.json index 557e3c8..afbc9fb 100644 --- a/tests/files/database_json_test.json +++ b/tests/files/database_json_test.json @@ -1 +1 @@ -[{"artist":"Artist A","year":1998,"title":"Release group A","tracks":[{"number":1,"title":"Track A.1","artist":["Artist A.A"]},{"number":2,"title":"Track A.2","artist":["Artist A.A"]},{"number":3,"title":"Track A.3","artist":["Artist A.A","Artist A.B"]}]},{"artist":"Artist B","year":2008,"title":"Release group B","tracks":[{"number":1,"title":"Track B.1","artist":["Artist B.A"]}]}] \ No newline at end of file +[{"id":{"artist":"Artist A","year":1998,"title":"Release group A"},"tracks":[{"number":1,"title":"Track A.1","artist":["Artist A.A"]},{"number":2,"title":"Track A.2","artist":["Artist A.A"]},{"number":3,"title":"Track A.3","artist":["Artist A.A","Artist A.B"]}]},{"id":{"artist":"Artist B","year":2008,"title":"Release group B"},"tracks":[{"number":1,"title":"Track B.1","artist":["Artist B.A"]}]}] \ No newline at end of file -- 2.45.2 From 84b05567ded5cbee012ad0229f6f540b51090e92 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Fri, 31 Mar 2023 00:00:32 +0900 Subject: [PATCH 07/12] Some more clean up --- src/lib.rs | 3 ++- src/library/beets.rs | 22 ++++++++-------------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bbe5a8d..1110f60 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,8 @@ pub struct Track { pub artist: Vec, } -#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] +/// The album identifier. +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone, Hash)] pub struct AlbumId { pub artist: String, pub year: u32, diff --git a/src/library/beets.rs b/src/library/beets.rs index 6d19e8c..c16b25a 100644 --- a/src/library/beets.rs +++ b/src/library/beets.rs @@ -123,32 +123,26 @@ impl Beets { let track_title = split[4].to_string(); let track_artist = split[5].to_string(); - let track = Track { - number: track_number, - title: track_title, - artist: track_artist.split("; ").map(|s| s.to_owned()).collect(), - }; - let aid = AlbumId { artist: album_artist, year: album_year, title: album_title, }; + let track = Track { + number: track_number, + title: track_title, + artist: track_artist.split("; ").map(|s| s.to_owned()).collect(), + }; + if album_ids.contains(&aid) { // Beets returns results in order so we look from the back. let album = albums.iter_mut().rev().find(|a| a.id == aid).unwrap(); album.tracks.push(track); } else { - let album_artist = aid.artist.to_string(); - let album_title = aid.title.to_string(); - album_ids.insert(aid); + album_ids.insert(aid.clone()); albums.push(Album { - id: AlbumId { - artist: album_artist, - year: album_year, - title: album_title, - }, + id: aid, tracks: vec![track], }); } -- 2.45.2 From fdf3687d54126a914f48fdbdf6713a75fd1ec918 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Fri, 31 Mar 2023 13:05:18 +0900 Subject: [PATCH 08/12] Clean up private helper methods --- src/library/beets.rs | 50 +++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/src/library/beets.rs b/src/library/beets.rs index c16b25a..72d0fcc 100644 --- a/src/library/beets.rs +++ b/src/library/beets.rs @@ -78,15 +78,39 @@ pub struct Beets { executor: Box, } +impl Beets { + pub fn new(executor: Box) -> Beets { + Beets { executor } + } +} + +trait LibraryPrivate { + const CMD_LIST: &'static str; + const SEPARATOR: &'static str; + const LIST_FORMAT: &'static str; + + fn list_cmd_and_args(query: &Query) -> Vec; + fn list_to_albums(list_output: Vec) -> Result, Error>; +} + +impl Library for Beets { + fn list(&mut self, query: &Query) -> Result, Error> { + let cmd = Self::list_cmd_and_args(query); + let output = self.executor.exec(cmd)?; + Self::list_to_albums(output) + } +} + macro_rules! separator { () => { "-*^-" }; } -impl Beets { - const SEPARATOR: &str = separator!(); - const LIST_FORMAT: &str = concat!( +impl LibraryPrivate for Beets { + const CMD_LIST: &'static str = "ls"; + const SEPARATOR: &'static str = separator!(); + const LIST_FORMAT: &'static str = concat!( "--format=", "$albumartist", separator!(), @@ -101,8 +125,11 @@ impl Beets { "$artist" ); - pub fn new(executor: Box) -> Beets { - Beets { executor } + fn list_cmd_and_args(query: &Query) -> Vec { + let mut cmd: Vec = vec![String::from(Self::CMD_LIST)]; + cmd.push(Self::LIST_FORMAT.to_string()); + cmd.append(&mut query.to_args()); + cmd } fn list_to_albums(list_output: Vec) -> Result, Error> { @@ -147,22 +174,11 @@ impl Beets { }); } } + Ok(albums) } } -impl Library for Beets { - fn list(&mut self, query: &Query) -> Result, Error> { - let mut arguments: Vec = vec![String::from("ls")]; - arguments.push(Self::LIST_FORMAT.to_string()); - arguments.append(&mut query.to_args()); - - let output = self.executor.exec(arguments)?; - - Self::list_to_albums(output) - } -} - pub struct SystemExecutor { bin: String, } -- 2.45.2 From 565d29e5483b149ecfaa747f88cc67eebe54f4de Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Fri, 31 Mar 2023 13:08:50 +0900 Subject: [PATCH 09/12] Variable name cleanup --- src/library/beets.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/library/beets.rs b/src/library/beets.rs index 72d0fcc..0556004 100644 --- a/src/library/beets.rs +++ b/src/library/beets.rs @@ -86,7 +86,7 @@ impl Beets { trait LibraryPrivate { const CMD_LIST: &'static str; - const SEPARATOR: &'static str; + const LIST_FORMAT_SEPARATOR: &'static str; const LIST_FORMAT: &'static str; fn list_cmd_and_args(query: &Query) -> Vec; @@ -101,7 +101,7 @@ impl Library for Beets { } } -macro_rules! separator { +macro_rules! list_format_separator { () => { "-*^-" }; @@ -109,19 +109,19 @@ macro_rules! separator { impl LibraryPrivate for Beets { const CMD_LIST: &'static str = "ls"; - const SEPARATOR: &'static str = separator!(); + const LIST_FORMAT_SEPARATOR: &'static str = list_format_separator!(); const LIST_FORMAT: &'static str = concat!( "--format=", "$albumartist", - separator!(), + list_format_separator!(), "$year", - separator!(), + list_format_separator!(), "$album", - separator!(), + list_format_separator!(), "$track", - separator!(), + list_format_separator!(), "$title", - separator!(), + list_format_separator!(), "$artist" ); @@ -137,7 +137,7 @@ impl LibraryPrivate for Beets { let mut album_ids = HashSet::::new(); for line in list_output.iter() { - let split: Vec<&str> = line.split(Self::SEPARATOR).collect(); + let split: Vec<&str> = line.split(Self::LIST_FORMAT_SEPARATOR).collect(); if split.len() != 6 { return Err(Error::InvalidData(line.to_string())); @@ -285,7 +285,7 @@ mod tests { strings.push(format!( "{album_artist}{0}{album_year}{0}{album_title}{0}\ {track_number}{0}{track_title}{0}{track_artist}", - Beets::SEPARATOR, + Beets::LIST_FORMAT_SEPARATOR, )); } -- 2.45.2 From ecc4b64e436da53cd9d0366d83e2b4411dc82609 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Fri, 31 Mar 2023 13:10:35 +0900 Subject: [PATCH 10/12] More variable name cleanup --- src/library/beets.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/library/beets.rs b/src/library/beets.rs index 0556004..98f5592 100644 --- a/src/library/beets.rs +++ b/src/library/beets.rs @@ -87,7 +87,7 @@ impl Beets { trait LibraryPrivate { const CMD_LIST: &'static str; const LIST_FORMAT_SEPARATOR: &'static str; - const LIST_FORMAT: &'static str; + const LIST_FORMAT_ARG: &'static str; fn list_cmd_and_args(query: &Query) -> Vec; fn list_to_albums(list_output: Vec) -> Result, Error>; @@ -110,7 +110,7 @@ macro_rules! list_format_separator { impl LibraryPrivate for Beets { const CMD_LIST: &'static str = "ls"; const LIST_FORMAT_SEPARATOR: &'static str = list_format_separator!(); - const LIST_FORMAT: &'static str = concat!( + const LIST_FORMAT_ARG: &'static str = concat!( "--format=", "$albumartist", list_format_separator!(), @@ -127,7 +127,7 @@ impl LibraryPrivate for Beets { fn list_cmd_and_args(query: &Query) -> Vec { let mut cmd: Vec = vec![String::from(Self::CMD_LIST)]; - cmd.push(Self::LIST_FORMAT.to_string()); + cmd.push(Self::LIST_FORMAT_ARG.to_string()); cmd.append(&mut query.to_args()); cmd } @@ -315,7 +315,7 @@ mod tests { #[test] fn test_list_empty() { let executor = TestExecutor { - arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT.to_string()]), + arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT_ARG.to_string()]), output: Some(Ok(vec![])), }; let mut beets = Beets::new(Box::new(executor)); @@ -334,7 +334,7 @@ mod tests { } let executor = TestExecutor { - arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT.to_string()]), + arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT_ARG.to_string()]), output: Some(Ok(output)), }; let mut beets = Beets::new(Box::new(executor)); @@ -363,7 +363,7 @@ mod tests { expected[1].tracks.rotate_left(1); let executor = TestExecutor { - arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT.to_string()]), + arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT_ARG.to_string()]), output: Some(Ok(output)), }; let mut beets = Beets::new(Box::new(executor)); @@ -384,7 +384,7 @@ mod tests { } let executor = TestExecutor { - arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT.to_string()]), + arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT_ARG.to_string()]), output: Some(Ok(output)), }; let mut beets = Beets::new(Box::new(executor)); @@ -403,7 +403,7 @@ mod tests { let executor = TestExecutor { arguments: Some(vec![ "ls".to_string(), - Beets::LIST_FORMAT.to_string(), + Beets::LIST_FORMAT_ARG.to_string(), String::from("^album:some.album"), String::from("track:5"), String::from("artist:some.artist"), -- 2.45.2 From b1058f5a0b5812bb03a0ea4746a47eee8a5034ff Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Fri, 31 Mar 2023 13:13:38 +0900 Subject: [PATCH 11/12] Change code order --- src/library/beets.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/library/beets.rs b/src/library/beets.rs index 98f5592..9831693 100644 --- a/src/library/beets.rs +++ b/src/library/beets.rs @@ -8,6 +8,10 @@ trait QueryOptionArgBeets { fn to_arg(&self, option_name: &str) -> Option; } +trait QueryArgsBeets { + fn to_args(&self) -> Vec; +} + trait SimpleOption {} impl SimpleOption for String {} impl SimpleOption for u32 {} @@ -34,10 +38,6 @@ impl QueryOptionArgBeets for QueryOption> { } } -trait QueryArgsBeets { - fn to_args(&self) -> Vec; -} - impl QueryArgsBeets for Query { fn to_args(&self) -> Vec { let mut arguments: Vec = vec![]; @@ -78,12 +78,6 @@ pub struct Beets { executor: Box, } -impl Beets { - pub fn new(executor: Box) -> Beets { - Beets { executor } - } -} - trait LibraryPrivate { const CMD_LIST: &'static str; const LIST_FORMAT_SEPARATOR: &'static str; @@ -93,6 +87,12 @@ trait LibraryPrivate { fn list_to_albums(list_output: Vec) -> Result, Error>; } +impl Beets { + pub fn new(executor: Box) -> Beets { + Beets { executor } + } +} + impl Library for Beets { fn list(&mut self, query: &Query) -> Result, Error> { let cmd = Self::list_cmd_and_args(query); -- 2.45.2 From 9f0cffdd4dd46de7de622414ff2bcb621bdac3c5 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Fri, 31 Mar 2023 21:18:03 +0900 Subject: [PATCH 12/12] Update docs --- src/database/json.rs | 2 ++ src/database/mod.rs | 4 ++++ src/library/beets.rs | 45 ++++++++++++++++++++++++++++---------------- src/library/mod.rs | 34 +++++++++++++++++++-------------- 4 files changed, 55 insertions(+), 30 deletions(-) diff --git a/src/database/json.rs b/src/database/json.rs index 98bdb74..c87d533 100644 --- a/src/database/json.rs +++ b/src/database/json.rs @@ -1,3 +1,5 @@ +//! Module for storing MusicHoard data in a JSON file database. + use std::fs::File; use std::io::{Read, Write}; use std::path::Path; diff --git a/src/database/mod.rs b/src/database/mod.rs index 5dff0b3..d5ab4c8 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,3 +1,5 @@ +//! Module for storing MusicHoard data in a database. + use serde::de::DeserializeOwned; use serde::Serialize; @@ -5,6 +7,7 @@ pub mod json; /// Trait for database reads. pub trait DatabaseRead { + /// Read collection from the database. fn read(&mut self, collection: &mut D) -> Result<(), std::io::Error> where D: DeserializeOwned; @@ -12,6 +15,7 @@ pub trait DatabaseRead { /// Trait for database writes. pub trait DatabaseWrite { + /// Write collection to the database. fn write(&mut self, collection: &S) -> Result<(), std::io::Error> where S: Serialize; diff --git a/src/library/beets.rs b/src/library/beets.rs index 9831693..853f42a 100644 --- a/src/library/beets.rs +++ b/src/library/beets.rs @@ -1,3 +1,6 @@ +//! Module for interacting with the music library via +//! [beets](https://beets.readthedocs.io/en/stable/). + use std::{collections::HashSet, fmt::Display, process::Command}; use crate::{Album, AlbumId, Track}; @@ -23,7 +26,7 @@ impl QueryOptionArgBeets for QueryOption { Self::Exclude(value) => ("^", value), Self::None => return None, }; - Some(format!("{}{}:{}", negate, option_name, value)) + Some(format!("{}{}{}", negate, option_name, value)) } } @@ -34,7 +37,7 @@ impl QueryOptionArgBeets for QueryOption> { Self::Exclude(value) => ("^", value), Self::None => return None, }; - Some(format!("{}{}:{}", negate, option_name, vec.join("; "))) + Some(format!("{}{}{}", negate, option_name, vec.join("; "))) } } @@ -42,38 +45,45 @@ impl QueryArgsBeets for Query { fn to_args(&self) -> Vec { let mut arguments: Vec = vec![]; - if let Some(album_artist) = self.album_artist.to_arg("albumartist") { + if let Some(album_artist) = self.album_artist.to_arg("albumartist:") { arguments.push(album_artist); }; - if let Some(album_year) = self.album_year.to_arg("year") { + if let Some(album_year) = self.album_year.to_arg("year:") { arguments.push(album_year); }; - if let Some(album_title) = self.album_title.to_arg("album") { + if let Some(album_title) = self.album_title.to_arg("album:") { arguments.push(album_title); }; - if let Some(track_number) = self.track_number.to_arg("track") { + if let Some(track_number) = self.track_number.to_arg("track:") { arguments.push(track_number); }; - if let Some(track_title) = self.track_title.to_arg("title") { + if let Some(track_title) = self.track_title.to_arg("title:") { arguments.push(track_title); }; - if let Some(track_artist) = self.track_artist.to_arg("artist") { + if let Some(track_artist) = self.track_artist.to_arg("artist:") { arguments.push(track_artist); }; + if let Some(all) = self.all.to_arg("") { + arguments.push(all); + } + arguments } } +/// Trait for invoking beets commands. pub trait BeetsExecutor { + /// Invoke beets with the provided arguments. fn exec(&mut self, arguments: Vec) -> Result, Error>; } +/// Struct for interacting with the music library via beets. pub struct Beets { executor: Box, } @@ -179,6 +189,7 @@ impl LibraryPrivate for Beets { } } +/// Executor for executing beets commands on the local system. pub struct SystemExecutor { bin: String, } @@ -295,19 +306,21 @@ mod tests { #[test] fn test_query() { let query = Query::new() - .album_title(QueryOption::exclude(String::from("some.album"))) - .track_number(QueryOption::include(5)) - .track_artist(QueryOption::include(vec![ + .album_title(QueryOption::Exclude(String::from("some.album"))) + .track_number(QueryOption::Include(5)) + .track_artist(QueryOption::Include(vec![ String::from("some.artist.1"), String::from("some.artist.2"), - ])); + ])) + .all(QueryOption::Exclude(String::from("some.all"))); assert_eq!( query.to_args(), vec![ String::from("^album:some.album"), String::from("track:5"), - String::from("artist:some.artist.1; some.artist.2") + String::from("artist:some.artist.1; some.artist.2"), + String::from("^some.all"), ] ); } @@ -396,9 +409,9 @@ mod tests { #[test] fn test_list_query() { let query = Query::new() - .album_title(QueryOption::exclude(String::from("some.album"))) - .track_number(QueryOption::include(5)) - .track_artist(QueryOption::include(vec![String::from("some.artist")])); + .album_title(QueryOption::Exclude(String::from("some.album"))) + .track_number(QueryOption::Include(5)) + .track_artist(QueryOption::Include(vec![String::from("some.artist")])); let executor = TestExecutor { arguments: Some(vec![ diff --git a/src/library/mod.rs b/src/library/mod.rs index 54ef0e4..4201d24 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -1,3 +1,5 @@ +//! Module for interacting with the music library. + use std::{num::ParseIntError, str::Utf8Error}; use crate::Album; @@ -15,23 +17,12 @@ pub enum QueryOption { } impl QueryOption { - /// Create an inclusive query option. - pub fn include(value: T) -> Self { - QueryOption::Include(value) - } - - pub fn exclude(value: T) -> Self { - QueryOption::Exclude(value) - } - - pub fn none() -> Self { - QueryOption::None - } - + /// Return `true` if [QueryOption] is not [QueryOption::None]. pub fn is_some(&self) -> bool { !matches!(self, QueryOption::None) } + /// Return `true` if [QueryOption] is [QueryOption::None]. pub fn is_none(&self) -> bool { matches!(self, QueryOption::None) } @@ -39,7 +30,7 @@ impl QueryOption { impl Default for QueryOption { fn default() -> Self { - Self::none() + Self::None } } @@ -52,42 +43,56 @@ pub struct Query { track_number: QueryOption, track_title: QueryOption, track_artist: QueryOption>, + all: QueryOption, } impl Query { + /// Create an empty query. pub fn new() -> Self { Query::default() } + /// Refine the query to a specific album artist. pub fn album_artist(mut self, album_artist: QueryOption) -> Self { self.album_artist = album_artist; self } + /// Refine the query to a specific album year. pub fn album_year(mut self, album_year: QueryOption) -> Self { self.album_year = album_year; self } + /// Refine the query to a specific album title. pub fn album_title(mut self, album_title: QueryOption) -> Self { self.album_title = album_title; self } + /// Refine the query to a specific track number. pub fn track_number(mut self, track_number: QueryOption) -> Self { self.track_number = track_number; self } + /// Refine the query to a specific track title. pub fn track_title(mut self, track_title: QueryOption) -> Self { self.track_title = track_title; self } + /// Refine the query to a specific set of track artists. pub fn track_artist(mut self, track_artist: QueryOption>) -> Self { self.track_artist = track_artist; self } + + /// Refine the query for all fields. + pub fn all(mut self, all: QueryOption) -> Self { + self.all = all; + self + } } /// Error type for library calls. @@ -123,5 +128,6 @@ impl From for Error { /// Trait for interacting with the music library. pub trait Library { + /// List lirbary items that match the a specific query. fn list(&mut self, query: &Query) -> Result, Error>; } -- 2.45.2