diff --git a/src/rust/amadeus-next/amadeus/Cargo.toml b/src/rust/amadeus-next/amadeus/Cargo.toml index 804d6c43ab7ba2bbacee8369ca165ef583154f5a..0383a5d9c2ff66021ee6ed35e2511662a90de78c 100644 --- a/src/rust/amadeus-next/amadeus/Cargo.toml +++ b/src/rust/amadeus-next/amadeus/Cargo.toml @@ -6,6 +6,8 @@ authors.workspace = true license.workspace = true [dependencies] -log.workspace = true serde.workspace = true tokio.workspace = true + +commons = { path = "../commons" } +amalib = { path = "../amalib" } diff --git a/src/rust/amadeus-next/amalib/Cargo.toml b/src/rust/amadeus-next/amalib/Cargo.toml index 001f251e0515ce6e620d33e8ee8c836239ed6d2f..6def1de2e7e71aa0e950abd6a0cad47115f265f2 100644 --- a/src/rust/amadeus-next/amalib/Cargo.toml +++ b/src/rust/amadeus-next/amalib/Cargo.toml @@ -6,7 +6,6 @@ authors.workspace = true license.workspace = true [dependencies] -log.workspace = true serde.workspace = true tokio.workspace = true diff --git a/src/rust/amadeus-next/amalib/src/lib.rs b/src/rust/amadeus-next/amalib/src/lib.rs index aef81465077bb16212f1578f62a55c0bf1770516..33f96712e4480abb86ce1dd76c8371471a740197 100644 --- a/src/rust/amadeus-next/amalib/src/lib.rs +++ b/src/rust/amadeus-next/amalib/src/lib.rs @@ -13,8 +13,7 @@ pub use query::*; pub use response::*; pub use uri::*; -pub(crate) use commons::*; -pub(crate) use log::*; +pub(crate) use commons::{log::*, *}; pub(crate) use std::str::FromStr; /// The playback state of the lektord server. diff --git a/src/rust/amadeus-next/amalib/src/uri.rs b/src/rust/amadeus-next/amalib/src/uri.rs index 4d4bf040ce37ddad476d4520c227ff42c5d16859..c8fbc724145e1038e9ab6ac2135c8d478a6afdf0 100644 --- a/src/rust/amadeus-next/amalib/src/uri.rs +++ b/src/rust/amadeus-next/amalib/src/uri.rs @@ -1,6 +1,7 @@ //! Build lektord queries. use crate::*; +use std::str::FromStr; #[derive(Debug, Clone)] pub enum LektorUri { @@ -34,3 +35,26 @@ impl std::fmt::Display for LektorUri { f.write_str(&ret_str) } } + +impl TryFrom<&str> for LektorUri { + type Error = String; + + fn try_from(value: &str) -> Result<Self, Self::Error> { + match value.trim().split_once("://") { + Some(("id", id)) => { + Ok(Self::Id(i32::from_str(id).map_err(|e| { + format!("the string `{id}` is not an i32: {e}") + })?)) + } + Some(("author", author)) => Ok(Self::Author(author.to_string())), + Some(("playlist", plt)) => Ok(Self::Playlist(plt.to_string())), + Some(("query", query)) => Ok(Self::Query( + query.split(' ').map(String::from).collect::<Vec<_>>(), + )), + Some((identifier, _)) => Err(format!( + "the identifier `{identifier}` is not a valid uri identifier" + )), + None => Err(format!("the string `{value}` is not a valid uri")), + } + } +} diff --git a/src/rust/amadeus-next/commons/src/asserts.rs b/src/rust/amadeus-next/commons/src/asserts.rs new file mode 100644 index 0000000000000000000000000000000000000000..5a71a7f5ec0fae73eda8d6777c3239352d509e7c --- /dev/null +++ b/src/rust/amadeus-next/commons/src/asserts.rs @@ -0,0 +1,36 @@ +#[macro_export] +macro_rules! fatal { + (target: $target: expr, $($arg:tt)+) => {{ + $crate::log::log!(target: $target, $crate::log::Level::Error, $($arg)+); + std::process::exit(1); + }}; + + ($($arg:tt)+) => {{ + $crate::log::log!($crate::log::Level::Error, $($arg)+); + std::process::exit(1); + }}; +} + +/// Test that something was [`Err`] and return the content of the +/// [`Result::Err`] variant. Panic on [`Ok`]. +#[macro_export] +macro_rules! assert_err { + ($x: expr) => { + match $x { + Ok(e) => panic!("'{}' was successfull and got '{e:?}'", stringify!($x)), + Err(e) => e, + } + }; +} + +/// Test that something was [`Ok`] and return the content of the [`Result::Ok`] +/// variant. Panic on [`Err`]. +#[macro_export] +macro_rules! assert_ok { + ($x: expr) => { + match $x { + Ok(x) => x, + Err(e) => panic!("failed '{}' with '{e:?}'", stringify!($x)), + } + }; +} diff --git a/src/rust/amadeus-next/commons/src/lib.rs b/src/rust/amadeus-next/commons/src/lib.rs index 3c4eeed704578339984eb952b5a03b4a678359b1..5322d4f839aa6bb9eaba7a2cb3dc1a65126802e7 100644 --- a/src/rust/amadeus-next/commons/src/lib.rs +++ b/src/rust/amadeus-next/commons/src/lib.rs @@ -1,4 +1,7 @@ +mod asserts; mod error; mod macros; +pub mod log; + pub use error::*; diff --git a/src/rust/amadeus-next/commons/src/log.rs b/src/rust/amadeus-next/commons/src/log.rs new file mode 100644 index 0000000000000000000000000000000000000000..178e55f5de9c8e41fa890462a7d79891503c6c15 --- /dev/null +++ b/src/rust/amadeus-next/commons/src/log.rs @@ -0,0 +1 @@ +pub use log::{debug, error, info, log, trace, warn, Level}; diff --git a/src/rust/amadeus-next/lkt-rs/Cargo.toml b/src/rust/amadeus-next/lkt-rs/Cargo.toml index e2902802689ce6f8e27cf6a45d4291f3fe88cc9d..438b5e2958320ec1347eed8e15e14fbf2b774690 100644 --- a/src/rust/amadeus-next/lkt-rs/Cargo.toml +++ b/src/rust/amadeus-next/lkt-rs/Cargo.toml @@ -6,5 +6,16 @@ authors.workspace = true license.workspace = true [dependencies] -log.workspace = true serde.workspace = true + +commons = { path = "../commons" } +amalib = { path = "../amalib" } + +clap = { version = "^4", default-features = false, features = [ + "usage", + "help", + "std", + "suggestions", + "error-context", + "derive", +] } diff --git a/src/rust/amadeus-next/lkt-rs/src/args.rs b/src/rust/amadeus-next/lkt-rs/src/args.rs new file mode 100644 index 0000000000000000000000000000000000000000..4c8bcbcd59930fae37226c450059160abb055995 --- /dev/null +++ b/src/rust/amadeus-next/lkt-rs/src/args.rs @@ -0,0 +1,113 @@ +use crate::types::*; +use clap::{Parser, Subcommand}; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +pub struct Args { + #[command(subcommand)] + action: SubCommand, +} + +#[derive(Subcommand, Debug)] +pub enum SubCommand { + // Status commands + #[command( + about = "Prints informations about the currently playing kara. Can be used to display the current kara in a status bar like xmobar or in the i3 panel." + )] + Current, + + #[command( + about = "Prints information about the state of lektor and the currently playing kara." + )] + Status, + + // Playback commands + #[command( + about = "Toggle play/pause state. If the playback is stopped, start at a possibly specified index." + )] + Play { index: Option<usize> }, + + #[command(about = "For lektord to be paused, don't do anything is lektord is already paused.")] + Pause, + + #[command( + about = "Unpause lektord, starts at the begening of the queue if lektord was stopped." + )] + UnPause, + + #[command(about = "Stop the playback and reading the queue, the state is now stopped.")] + Stop, + + #[command(about = "Play next kara in the queue.")] + Next, + + #[command(about = "Play the previous kara in the queue.")] + Previous, + + #[command( + about = "Shuffle the queue. If lektord was paused it will unpause but if it was stopped it won't start. If it exists, the current kara will be placed in the first place in the queue. You can also pass a level to shuffle up to." + )] + Shuffle { + #[arg( + default_value_t = PriorityLevel::Add, + )] + level: PriorityLevel, + }, + + // Playlist commands + #[command(alias = "plt", short_flag = 'P')] + Playlist { + #[arg( + value_parser = clap::builder::NonEmptyStringValueParser::new(), + exclusive = true, + short = 'c', + help = "Create a new playlist with a specific name.", + )] + create: Option<String>, + + #[arg( + value_parser = clap::builder::NonEmptyStringValueParser::new(), + exclusive = true, + short = 'd', + help = "Delete a playlist with all its content, do nothing if the playlist didn't exists.", + )] + destroy: Option<String>, + + #[arg( + short = 'l', + exclusive = true, + help = "List the content of the playlist named if a name is passed. If missing returns a list of all the available playlists." + )] + list: Option<Option<String>>, + + #[arg( + action = clap::ArgAction::Append, + exclusive = true, + num_args = 2.., + short = 'r', + help = "Deletes karas from a playlist with a valid query. The first element is the name of the playlist." + )] + remove: Option<Vec<String>>, + + #[arg( + action = clap::ArgAction::Append, + exclusive = true, + num_args = 2.., + short = 'a', + help = "Add karas to a playlist with a valid query. The first element is the name of the playlist." + )] + add: Option<Vec<String>>, + }, + + // Queue commands + #[command(short_flag = 'Q')] + Queue {}, + + // Search commands + #[command(short_flag = 'S')] + Search {}, + + // Admin commands + #[command(alias = "adm", short_flag = 'A')] + Admin {}, +} diff --git a/src/rust/amadeus-next/lkt-rs/src/main.rs b/src/rust/amadeus-next/lkt-rs/src/main.rs index f328e4d9d04c31d0d70d16d21a07d1613be9d577..943affe76681d30fa437d2c90ea9f9a3706d0e86 100644 --- a/src/rust/amadeus-next/lkt-rs/src/main.rs +++ b/src/rust/amadeus-next/lkt-rs/src/main.rs @@ -1 +1,8 @@ -fn main() {} +mod args; +mod parsers; +mod types; + +fn main() { + let args = <args::Args as clap::Parser>::parse(); + println!("{args:#?}"); +} diff --git a/src/rust/amadeus-next/lkt-rs/src/parsers.rs b/src/rust/amadeus-next/lkt-rs/src/parsers.rs new file mode 100644 index 0000000000000000000000000000000000000000..2a936f2875d39a48e720e6d33cf15c24f6ae9113 --- /dev/null +++ b/src/rust/amadeus-next/lkt-rs/src/parsers.rs @@ -0,0 +1,50 @@ +use clap::{builder::TypedValueParser, error::ErrorKind, Arg, Command, Error}; + +/// A parser for the [crate::types::FilterPlaylist] structure +#[derive(Copy, Clone, Debug)] +#[non_exhaustive] +pub struct FilterPlaylistParser {} + +impl FilterPlaylistParser { + /// Parse non-empty string values + pub fn new() -> Self { + Self {} + } +} + +impl TypedValueParser for FilterPlaylistParser { + type Value = (String, String); + + fn parse_ref( + &self, + cmd: &Command, + arg: Option<&Arg>, + value: &std::ffi::OsStr, + ) -> Result<Self::Value, Error> { + let arg = arg + .map(ToString::to_string) + .unwrap_or_else(|| "...".to_owned()); + let value = value.to_str().ok_or_else(|| { + Error::raw( + ErrorKind::InvalidUtf8, + format!("not a valid utf8 string: {}", value.to_string_lossy()), + ) + })?; + match value.split_once(' ') { + Some((head, tail)) => Ok((head.to_string(), tail.to_string())), + _ => Err(Error::raw( + ErrorKind::InvalidValue, + format!( + "in `{} {arg}`, the value should be of the form: [PLAYLIST] [QUERY]", + cmd.get_name() + ), + )), + } + } +} + +impl Default for FilterPlaylistParser { + fn default() -> Self { + Self::new() + } +} diff --git a/src/rust/amadeus-next/lkt-rs/src/types.rs b/src/rust/amadeus-next/lkt-rs/src/types.rs new file mode 100644 index 0000000000000000000000000000000000000000..1b2e616e3ecc2727426f3db8e798ff5f460b9226 --- /dev/null +++ b/src/rust/amadeus-next/lkt-rs/src/types.rs @@ -0,0 +1,48 @@ +use commons::fatal; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PriorityLevel { + Add = 1, + Suggest = 2, + Insert = 3, + Pick = 4, + Enforce = 5, +} + +impl From<&str> for PriorityLevel { + fn from(value: &str) -> Self { + use PriorityLevel::*; + match value { + "add" | "1" => Add, + "suggest" | "2" => Suggest, + "insert" | "3" => Insert, + "pick" | "4" => Pick, + "enforce" | "5" => Enforce, + _ => fatal!("unknown priority level: {value}"), + } + } +} + +impl Default for PriorityLevel { + fn default() -> Self { + PriorityLevel::Add + } +} + +impl std::fmt::Display for PriorityLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + PriorityLevel::Add => "add", + PriorityLevel::Suggest => "suggest", + PriorityLevel::Insert => "insert", + PriorityLevel::Pick => "pick", + PriorityLevel::Enforce => "enforce", + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FilterPlaylist { + playlist: String, + query: String, +}