From b97b650f645c41c4d6f42bbe99f7c8a72b803f5a Mon Sep 17 00:00:00 2001 From: h7x4 Date: Tue, 25 Feb 2025 12:12:43 +0100 Subject: [PATCH] common/types: add db selection print types --- src/common/types.rs | 8 ++ src/common/types/db_directory_info.rs | 44 ++++++++++ src/common/types/db_playlist_info.rs | 44 ++++++++++ src/common/types/db_selection_print.rs | 93 ++++++++++++++++++++++ src/common/types/db_song_info.rs | 106 +++++++++++++++++++++++++ 5 files changed, 295 insertions(+) create mode 100644 src/common/types/db_directory_info.rs create mode 100644 src/common/types/db_playlist_info.rs create mode 100644 src/common/types/db_selection_print.rs create mode 100644 src/common/types/db_song_info.rs diff --git a/src/common/types.rs b/src/common/types.rs index cf5e1757..4ff3c831 100644 --- a/src/common/types.rs +++ b/src/common/types.rs @@ -2,6 +2,10 @@ mod absolute_relative_song_position; mod audio; mod bool_or_oneshot; mod channel_name; +mod db_directory_info; +mod db_playlist_info; +mod db_selection_print; +mod db_song_info; mod group_type; mod one_or_range; mod replay_gain_mode_mode; @@ -20,6 +24,10 @@ pub use absolute_relative_song_position::AbsouluteRelativeSongPosition; pub use audio::Audio; pub use bool_or_oneshot::BoolOrOneshot; pub use channel_name::ChannelName; +pub use db_directory_info::DbDirectoryInfo; +pub use db_playlist_info::DbPlaylistInfo; +pub use db_selection_print::DbSelectionPrintResponse; +pub use db_song_info::DbSongInfo; pub use group_type::GroupType; pub use one_or_range::OneOrRange; pub use replay_gain_mode_mode::ReplayGainModeMode; diff --git a/src/common/types/db_directory_info.rs b/src/common/types/db_directory_info.rs new file mode 100644 index 00000000..aefcbb89 --- /dev/null +++ b/src/common/types/db_directory_info.rs @@ -0,0 +1,44 @@ +use std::{collections::HashMap, path::PathBuf}; + +use serde::{Deserialize, Serialize}; + +use crate::{ + commands::ResponseParserError, + response_tokenizer::{ + GenericResponseValue, ResponseAttributes, get_optional_property, get_property, + }, +}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DbDirectoryInfo { + pub directory: PathBuf, + // TODO: parse this + pub last_modified: Option, +} + +impl DbDirectoryInfo { + pub fn is_full(&self) -> bool { + self.last_modified.is_some() + } + + pub fn parse(parts: ResponseAttributes<'_>) -> Result> { + let parts: HashMap<_, _> = parts.into_map()?; + Self::parse_map(parts) + } + + pub(crate) fn parse_map<'a>( + parts: HashMap<&str, GenericResponseValue<'a>>, + ) -> Result> { + let directory = get_property!(parts, "directory", Text); + let directory = directory + .parse() + .map_err(|_| ResponseParserError::InvalidProperty("directory", directory))?; + + let last_modified = + get_optional_property!(parts, "Last-Modified", Text).map(|s| s.to_owned()); + Ok(DbDirectoryInfo { + directory, + last_modified, + }) + } +} diff --git a/src/common/types/db_playlist_info.rs b/src/common/types/db_playlist_info.rs new file mode 100644 index 00000000..17a21e91 --- /dev/null +++ b/src/common/types/db_playlist_info.rs @@ -0,0 +1,44 @@ +use std::{collections::HashMap, path::PathBuf}; + +use serde::{Deserialize, Serialize}; + +use crate::{ + commands::ResponseParserError, + response_tokenizer::{ + GenericResponseValue, ResponseAttributes, get_optional_property, get_property, + }, +}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DbPlaylistInfo { + pub playlist: PathBuf, + // TODO: parse this + pub last_modified: Option, +} + +impl DbPlaylistInfo { + pub fn is_full(&self) -> bool { + self.last_modified.is_some() + } + + pub fn parse(parts: ResponseAttributes<'_>) -> Result> { + let parts: HashMap<_, _> = parts.into_map()?; + Self::parse_map(parts) + } + + pub(crate) fn parse_map<'a>( + parts: HashMap<&str, GenericResponseValue<'a>>, + ) -> Result> { + let playlist = get_property!(parts, "playlist", Text); + let playlist = playlist + .parse() + .map_err(|_| ResponseParserError::InvalidProperty("playlist", playlist))?; + let last_modified = + get_optional_property!(parts, "Last-Modified", Text).map(|s| s.to_owned()); + + Ok(DbPlaylistInfo { + playlist, + last_modified, + }) + } +} diff --git a/src/common/types/db_selection_print.rs b/src/common/types/db_selection_print.rs new file mode 100644 index 00000000..7a7ce9dc --- /dev/null +++ b/src/common/types/db_selection_print.rs @@ -0,0 +1,93 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::{commands::ResponseParserError, response_tokenizer::ResponseAttributes}; + +use super::{DbDirectoryInfo, DbPlaylistInfo, DbSongInfo}; + +// TODO: transform to a tree structure? + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum DbSelectionPrintResponse { + Directory(DbDirectoryInfo), + Song(DbSongInfo), + Playlist(DbPlaylistInfo), +} + +impl DbSelectionPrintResponse { + pub fn is_full(&self) -> bool { + match self { + DbSelectionPrintResponse::Directory(info) => info.is_full(), + DbSelectionPrintResponse::Song(info) => info.is_full(), + DbSelectionPrintResponse::Playlist(info) => info.is_full(), + } + } + + pub fn parse( + parts: ResponseAttributes<'_>, + ) -> Result, ResponseParserError<'_>> { + debug_assert!(!parts.is_empty()); + let vec: Vec<_> = parts.into_vec()?; + + vec.into_iter() + .fold(Vec::new(), |mut acc, (key, value)| { + if ["directory", "file", "playlist"].contains(&key) { + acc.push((key, HashMap::new())); + } + let result = acc.last_mut().unwrap().1.insert(key, value); + debug_assert_eq!(result, None); + acc + }) + .into_iter() + .map(|(key, attrs)| match key { + "directory" => { + DbDirectoryInfo::parse_map(attrs).map(DbSelectionPrintResponse::Directory) + } + "file" => DbSongInfo::parse_map(attrs).map(DbSelectionPrintResponse::Song), + "playlist" => { + DbPlaylistInfo::parse_map(attrs).map(DbSelectionPrintResponse::Playlist) + } + p => Err(ResponseParserError::UnexpectedProperty(p)), + }) + .collect::, ResponseParserError<'_>>>() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const INPUT: &str = indoc::indoc! {r#" + directory: Albums + Last-Modified: 2023-05-15T17:25:12Z + directory: Albums/Name - Album + Last-Modified: 2022-12-31T17:40:04Z + file: Albums/Artist - Title.mp3 + Last-Modified: 2022-12-31T02:43:04Z + Added: 2025-01-18T18:44:26Z + Format: 44100:16:2 + Title: Title + Artist: Artist + Album: Album + AlbumArtist: Artist + Composer: Composer + Genre: Pop, Rock + Track: 1 + Disc: 1 + Date: 2022-04-27 + Time: 341 + duration: 341.463 + playlist: Playlists/Playlist + Last-Modified: 2021-10-31T13:57:44Z + playlist: Playlists/Playlist 2 + OK + "#}; + + #[test] + fn test_parse_db_selection_print_response() { + let parts = ResponseAttributes::new(INPUT.trim()); + let result = DbSelectionPrintResponse::parse(parts); + assert!(result.is_ok()); + } +} diff --git a/src/common/types/db_song_info.rs b/src/common/types/db_song_info.rs new file mode 100644 index 00000000..8821f444 --- /dev/null +++ b/src/common/types/db_song_info.rs @@ -0,0 +1,106 @@ +use std::{collections::HashMap, path::PathBuf}; + +use serde::{Deserialize, Serialize}; + +use crate::{ + commands::ResponseParserError, + response_tokenizer::{GenericResponseValue, ResponseAttributes, expect_property_type}, +}; + +use super::{PlaylistName, Seconds, Tag, TimeInterval}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DbSongInfo { + pub file: PathBuf, + pub range: Option, + // TODO: parse this + pub last_modified: Option, + // TODO: parse this + pub added: Option, + pub format: Option, + pub tags: Vec, + pub time: Option, + pub duration: Option, + pub playlist: Option, +} + +macro_rules! remove_and_parse_next_optional_property { + ($parts:ident, $name:literal, $variant:ident) => {{ + let prop = crate::response_tokenizer::_expect_property_type!( + { $parts.remove($name) }, + $name, + $variant + ); + crate::response_tokenizer::_parse_optional_property_type!($name, prop) + }}; +} + +impl DbSongInfo { + pub fn is_full(&self) -> bool { + self.range.is_some() + || self.last_modified.is_some() + || self.added.is_some() + || self.format.is_some() + || !self.tags.is_empty() + || self.time.is_some() + || self.duration.is_some() + || self.playlist.is_some() + } + + pub fn parse(parts: ResponseAttributes<'_>) -> Result> { + let parts: HashMap<_, _> = parts.into_map()?; + Self::parse_map(parts) + } + + pub(crate) fn parse_map<'a>( + mut parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result> { + let file: PathBuf = match remove_and_parse_next_optional_property!(parts, "file", Text) { + Some(f) => f, + None => return Err(ResponseParserError::MissingProperty("file")), + }; + + if parts.is_empty() { + return Ok(DbSongInfo { + file, + range: None, + last_modified: None, + added: None, + format: None, + tags: Vec::new(), + time: None, + duration: None, + playlist: None, + }); + } + + let range = remove_and_parse_next_optional_property!(parts, "Range", Text); + let last_modified = remove_and_parse_next_optional_property!(parts, "Last-Modified", Text); + let added = remove_and_parse_next_optional_property!(parts, "Added", Text); + let format = remove_and_parse_next_optional_property!(parts, "Format", Text); + let time = remove_and_parse_next_optional_property!(parts, "Time", Text); + let duration = remove_and_parse_next_optional_property!(parts, "duration", Text); + let playlist = remove_and_parse_next_optional_property!(parts, "Playlist", Text); + + let mut tags = parts + .into_iter() + .map(|(key, value)| { + let value = expect_property_type!(Some(value), key, Text).to_string(); + Ok(Tag::new(key.to_string(), value)) + }) + .collect::, ResponseParserError<'_>>>()?; + tags.sort_unstable(); + + Ok(DbSongInfo { + file, + range, + last_modified, + added, + format, + tags, + time, + duration, + playlist, + }) + } +}