aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs')
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm.rs78
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/lib.rs43
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/messages.rs409
3 files changed, 38 insertions, 492 deletions
diff --git a/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm.rs b/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm.rs
index d9d607d5..746088ca 100644
--- a/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm.rs
+++ b/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm.rs
@@ -29,6 +29,8 @@
use mpdpopm::{
clients::{Client, PlayerStatus, quote},
config::{self, Config},
+ filters::ExpressionParser,
+ filters_ast::{FilterStickerNames, evaluate},
storage::{last_played, play_count, rating_count},
};
@@ -263,12 +265,31 @@ async fn get_playlists(client: &mut Client) -> Result<()> {
}
/// Add songs selected by filter to the queue
-async fn findadd(client: &mut Client, chan: &str, filter: &str, case: bool) -> Result<()> {
+async fn searchadd(client: &mut Client, filter: &str, case_sensitive: bool) -> Result<()> {
let qfilter = quote(filter);
debug!("findadd: got ``{}'', quoted to ``{}''.", filter, qfilter);
- let cmd = format!("{} {}", if case { "findadd" } else { "searchadd" }, qfilter);
- client.send_message(chan, &cmd).await?;
- Ok(())
+
+ let ast = match ExpressionParser::new().parse(&qfilter) {
+ Ok(ast) => ast,
+ Err(err) => {
+ bail!("Failed to parse filter: `{}`", err)
+ }
+ };
+
+ debug!("ast: {:#?}", ast);
+
+ let mut results = Vec::new();
+ for song in evaluate(&ast, case_sensitive, client, &FilterStickerNames::default())
+ .await
+ .context("Failed to evaluate filter")?
+ {
+ results.push(client.add(&song).await);
+ }
+
+ match results.into_iter().collect::<Result<Vec<()>>>() {
+ Ok(_) => Ok(()),
+ Err(err) => Err(err),
+ }
}
/// Send an arbitrary command
@@ -442,34 +463,7 @@ enum SubCommand {
command: PlaylistsCommand,
},
- /// search case-sensitively for songs matching matching a filter and add them to the queue
- ///
- /// This command extends the MPD command `findadd' (which will search the MPD database) to allow
- /// searches on attributes managed by mpdpopm: rating, playcount & last played time.
- ///
- /// The MPD `findadd' <https://www.musicpd.org/doc/html/protocol.html#command-findadd> will search the
- /// MPD database for songs that match a given filter & add them to the play queue. The filter syntax is
- /// documented here <https://www.musicpd.org/doc/html/protocol.html#filter-syntax>.
- ///
- /// This command adds three new terms on which you can filter: rating, playcount & lastplayed. Each is
- /// expressed as an unsigned integer, with zero interpreted as "not set". For instance:
- ///
- /// mppopm findadd "(rating > 128)"
- ///
- /// Will add all songs in the library with a rating sticker > 128 to the play queue.
- ///
- /// mppopm also introduces OR clauses (MPD only supports AND), so that:
- ///
- /// mppopm findadd "((rating > 128) AND (artist =~ \"pogues\"))"
- ///
- /// will add all songs whose artist tag matches the regexp "pogues" with a rating greater than
- /// 128.
- ///
- /// `findadd' is case-sensitive; for case-insensitive searching see the `searchadd' command.
- #[clap(verbatim_doc_comment)]
- Findadd { filter: String },
-
- /// search case-insensitively for songs matching matching a filter and add them to the queue
+ /// search for songs matching matching a filter and add them to the queue
///
/// This command extends the MPD command `searchadd' (which will search the MPD database) to allow
/// searches on attributes managed by mpdpopm: rating, playcount & last played time.
@@ -491,10 +485,14 @@ enum SubCommand {
///
/// will add all songs whose artist tag matches the regexp "pogues" with a rating greater than
/// 128.
- ///
- /// `searchadd' is case-insensitive; for case-sensitive searching see the `findadd' command.
#[clap(verbatim_doc_comment)]
- Searchadd { filter: String },
+ Searchadd {
+ filter: String,
+
+ /// Respect the casing, when performing the filter evaluation.
+ #[arg(short, long, default_value_t = true)]
+ case_sensitive: bool,
+ },
/// Send a command to mpd.
#[clap(verbatim_doc_comment)]
@@ -584,12 +582,10 @@ async fn main() -> Result<()> {
SubCommand::Playlists { command } => match command {
PlaylistsCommand::Get {} => get_playlists(&mut client).await,
},
- SubCommand::Findadd { filter } => {
- findadd(&mut client, &config.commands_chan, &filter, true).await
- }
- SubCommand::Searchadd { filter } => {
- findadd(&mut client, &config.commands_chan, &filter, false).await
- }
+ SubCommand::Searchadd {
+ filter,
+ case_sensitive,
+ } => searchadd(&mut client, &filter, case_sensitive).await,
SubCommand::SendCommand { args } => {
send_command(&mut client, &config.commands_chan, args).await
}
diff --git a/pkgs/by-name/mp/mpdpopm/src/lib.rs b/pkgs/by-name/mp/mpdpopm/src/lib.rs
index 4fe523ea..7e1d3357 100644
--- a/pkgs/by-name/mp/mpdpopm/src/lib.rs
+++ b/pkgs/by-name/mp/mpdpopm/src/lib.rs
@@ -35,7 +35,6 @@
pub mod clients;
pub mod config;
pub mod filters_ast;
-pub mod messages;
pub mod playcounts;
pub mod storage;
pub mod vars;
@@ -53,7 +52,6 @@ use crate::{
clients::{Client, IdleClient, IdleSubSystem},
config::{Config, Connection},
filters_ast::FilterStickerNames,
- messages::MessageProcessor,
playcounts::PlayState,
};
@@ -110,12 +108,9 @@ pub async fn mpdpopm(cfg: Config) -> std::result::Result<(), Error> {
let tick = sleep(Duration::from_millis(cfg.poll_interval_ms)).fuse();
pin_mut!(ctrl_c, sighup, sigkill, tick);
- let mproc = MessageProcessor::new();
-
let mut done = false;
while !done {
debug!("selecting...");
- let mut msg_check_needed = false;
{
// `idle_client' mutably borrowed here
let mut idle = Box::pin(idle_client.idle().fuse());
@@ -142,24 +137,6 @@ pub async fn mpdpopm(cfg: Config) -> std::result::Result<(), Error> {
.await
.context("PlayState update failed")?
},
- // next = cmds.next() => match next {
- // Some(out) => {
- // debug!("output status is {:#?}", out.out);
- // match out.upd {
- // Some(uri) => {
- // debug!("{} needs to be updated", uri);
- // client.update(&uri).await.map_err(|err| Error::Client {
- // source: err,
- // back: Backtrace::new(),
- // })?;
- // },
- // None => debug!("No database update needed"),
- // }
- // },
- // None => {
- // debug!("No more commands to process.");
- // }
- // },
res = idle => match res {
Ok(subsys) => {
debug!("subsystem {} changed", subsys);
@@ -168,7 +145,7 @@ pub async fn mpdpopm(cfg: Config) -> std::result::Result<(), Error> {
.await
.context("PlayState update failed")?
} else if subsys == IdleSubSystem::Message {
- msg_check_needed = true;
+ error!("Message handling is not supported!");
}
break;
},
@@ -181,24 +158,6 @@ pub async fn mpdpopm(cfg: Config) -> std::result::Result<(), Error> {
}
}
}
-
- if msg_check_needed {
- // Check for any messages that have come in; if there's an error there's not a lot we
- // can do about it (suppose some client fat-fingers a command name, e.g.)-- just log it
- // & move on.
- if let Err(err) = mproc
- .check_messages(
- &mut client,
- &mut idle_client,
- state.last_status(),
- &cfg.commands_chan,
- &filter_stickers,
- )
- .await
- {
- error!("Error while processing messages: {err:#?}");
- }
- }
}
info!("mpdpopm exiting.");
diff --git a/pkgs/by-name/mp/mpdpopm/src/messages.rs b/pkgs/by-name/mp/mpdpopm/src/messages.rs
deleted file mode 100644
index 171a246a..00000000
--- a/pkgs/by-name/mp/mpdpopm/src/messages.rs
+++ /dev/null
@@ -1,409 +0,0 @@
-// Copyright (C) 2020-2025 Michael herstine <sp1ff@pobox.com>
-//
-// This file is part of mpdpopm.
-//
-// mpdpopm is free software: you can redistribute it and/or modify it under the terms of the GNU
-// General Public License as published by the Free Software Foundation, either version 3 of the
-// License, or (at your option) any later version.
-//
-// mpdpopm is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
-// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
-// Public License for more details.
-//
-// You should have received a copy of the GNU General Public License along with mpdpopm. If not,
-// see <http://www.gnu.org/licenses/>.
-
-//! # messages
-//!
-//! Process incoming messages to the [mpdpopm](https://github.com/sp1ff/mpdpopm) daemon.
-//!
-//! # Introduction
-//!
-//! The [mpdpopm](https://github.com/sp1ff/mpdpopm) daemon accepts commands over a dedicated
-//! [channel](https://www.musicpd.org/doc/html/protocol.html#client-to-client). It also provides for
-//! a generalized framework in which the [mpdpopm](https://github.com/sp1ff/mpdpopm) administrator
-//! can define new commands backed by arbitrary command execution server-side.
-//!
-//! # Commands
-//!
-//! The following commands are built-in:
-//!
-//! - set rating: `rate RATING( TRACK)?`
-//! - set playcount: `setpc PC( TRACK)?`
-//! - set lastplayed: `setlp TIMESTAMP( TRACK)?`
-//!
-//! There is no need to provide corresponding accessors since this functionality is already provided
-//! via "sticker get". Dedicated accessors could provide the same functionality with slightly more
-//! convenience since the sticker name would not have to be specified (as with "sticker get") & may
-//! be added at a later date.
-//!
-//! I'm expanding the MPD filter functionality to include attributes tracked by mpdpopm:
-//!
-//! - findadd replacement: `findadd FILTER [sort TYPE] [window START:END]`
-//! (cf. [here](https://www.musicpd.org/doc/html/protocol.html#the-music-database))
-//!
-//! - searchadd replacement: `searchadd FILTER [sort TYPE] [window START:END]`
-//! (cf. [here](https://www.musicpd.org/doc/html/protocol.html#the-music-database))
-//!
-//! Additional commands may be added through the
-//! [generalized commands](crate::commands#the-generalized-command-framework) feature.
-
-use crate::{
- clients::{Client, IdleClient, PlayerStatus},
- filters::ExpressionParser,
- filters_ast::{FilterStickerNames, evaluate},
-};
-
-use anyhow::{Context, Error, Result, anyhow, bail};
-use boolinator::Boolinator;
-use tracing::debug;
-
-use std::collections::VecDeque;
-
-/// Break `buf` up into individual tokens while removing MPD-style quoting.
-///
-/// When a client sends a command to [mpdpopm](crate), it will look like this on the wire:
-///
-/// ```text
-/// sendmessage ${CHANNEL} "some-command \"with space\" simple \"'with single' and \\\\\""
-/// ```
-///
-/// In other words, the MPD "sendmessage" command takes two parameters: the channel and the
-/// message. The recipient (i.e. us) is responsible for breaking up the message into its constituent
-/// parts (a command name & its arguments in our case).
-///
-/// The message will perforce be quoted according ot the MPD rules:
-///
-/// 1. an un-quoted token may contain any printable ASCII character except space, tab, ' & "
-///
-/// 2. to include spaces, tabs, '-s or "-s, the token must be enclosed in "-s, and any "-s or \\-s
-/// therein must be backslash escaped
-///
-/// When the messages is delivered to us, it has already been un-escaped; i.e. we will see the
-/// string:
-///
-/// ```text
-/// some-command "with space" simple "'with single' and \\"
-/// ```
-///
-/// This function will break that string up into individual tokens with one more level
-/// of escaping removed; i.e. it will return an iterator that will yield the four tokens:
-///
-/// 1. some-command
-/// 2. with space
-/// 3. simple
-/// 4. 'with single' and \\
-///
-/// [MPD](https://github.com/MusicPlayerDaemon/MPD) has a nice
-/// [implementation](https://github.com/MusicPlayerDaemon/MPD/blob/master/src/util/Tokenizer.cxx#L170)
-/// that modifies the string in place by copying subsequent characters on top of escape characters
-/// in the same buffer, inserting nulls in between the resulting tokens,and then working in terms of
-/// pointers to the resulting null-terminated strings.
-///
-/// Once I realized that I could split slices I saw how to implement an Iterator that do the same
-/// thing (an idiomatic interface to the tokenization backed by a zero-copy implementation). I was
-/// inspired by [My Favorite Rust Function
-/// Signature](<https://www.brandonsmith.ninja/blog/favorite-rust-function>).
-///
-/// NB. This method works in terms of a slice of [`u8`] because we can't index into Strings in
-/// Rust, and MPD deals only in terms of ASCII at any rate.
-pub fn tokenize(buf: &mut [u8]) -> impl Iterator<Item = Result<&[u8]>> {
- TokenIterator::new(buf)
-}
-
-struct TokenIterator<'a> {
- /// The slice on which we operate; modified in-place as we yield tokens
- slice: &'a mut [u8],
- /// Index into [`slice`] of the first non-whitespace character
- input: usize,
-}
-
-impl<'a> TokenIterator<'a> {
- pub fn new(slice: &'a mut [u8]) -> TokenIterator<'a> {
- let input = match slice.iter().position(|&x| x > 0x20) {
- Some(n) => n,
- None => slice.len(),
- };
- TokenIterator { slice, input }
- }
-}
-
-impl<'a> Iterator for TokenIterator<'a> {
- type Item = Result<&'a [u8]>;
-
- fn next(&mut self) -> Option<Self::Item> {
- let nslice = self.slice.len();
- if self.slice.is_empty() || self.input == nslice {
- None
- } else if '"' == self.slice[self.input] as char {
- // This is NextString in MPD: walk self.slice, un-escaping characters, until we find
- // a closing ". Note that we un-escape by moving characters forward in the slice.
- let mut inp = self.input + 1;
- let mut out = self.input;
- while self.slice[inp] as char != '"' {
- if '\\' == self.slice[inp] as char {
- inp += 1;
- if inp == nslice {
- return Some(Err(anyhow!("Trailing backslash")));
- }
- }
- self.slice[out] = self.slice[inp];
- out += 1;
- inp += 1;
- if inp == nslice {
- return Some(Err(anyhow!("No closing quote")));
- }
- }
- // The next token is in self.slice[self.input..out] and self.slice[inp] is "
- let tmp = std::mem::take(&mut self.slice);
- let (_, tmp) = tmp.split_at_mut(self.input);
- let (result, new_slice) = tmp.split_at_mut(out - self.input);
- self.slice = new_slice;
- // strip any leading whitespace
- self.input = inp - out + 1; // +1 to skip the closing "
- while self.input < self.slice.len() && self.slice[self.input] as char == ' ' {
- self.input += 1;
- }
- Some(Ok(result))
- } else {
- // This is NextUnquoted in MPD; walk self.slice, validating characters until the end
- // or the next whitespace
- let mut i = self.input;
- while i < nslice {
- if 0x20 >= self.slice[i] {
- break;
- }
- if self.slice[i] as char == '"' || self.slice[i] as char == '\'' {
- return Some(Err(anyhow!("Invalid char: `{}`", self.slice[i])));
- }
- i += 1;
- }
- // The next token is in self.slice[self.input..i] & self.slice[i] is either one-
- // past-the end or whitespace.
- let tmp = std::mem::take(&mut self.slice);
- let (_, tmp) = tmp.split_at_mut(self.input);
- let (result, new_slice) = tmp.split_at_mut(i - self.input);
- self.slice = new_slice;
- // strip any leading whitespace
- self.input = match self.slice.iter().position(|&x| x > 0x20) {
- Some(n) => n,
- None => self.slice.len(),
- };
- Some(Ok(result))
- }
- }
-}
-
-/// Collective state needed for processing messages, both built-in & generalized
-#[derive(Default)]
-pub struct MessageProcessor {}
-
-impl MessageProcessor {
- /// Whip up a new instance; other than cloning the iterators, should just hold references in the
- /// enclosing scope
- pub fn new() -> MessageProcessor {
- Self::default()
- }
-
- /// Read messages off the commands channel & dispatch 'em
- pub async fn check_messages<'a>(
- &self,
- client: &mut Client,
- idle_client: &mut IdleClient,
- state: PlayerStatus,
- command_chan: &str,
- stickers: &FilterStickerNames<'a>,
- ) -> Result<()> {
- let m = idle_client
- .get_messages()
- .await
- .context("Failed to `get_messages` from client")?;
-
- for (chan, msgs) in m {
- // Only supporting a single channel, ATM
- (chan == command_chan).ok_or_else(|| anyhow!("Unknown chanell: `{}`", chan))?;
- for msg in msgs {
- self.process(msg, client, &state, stickers).await?;
- }
- }
-
- Ok(())
- }
-
- /// Process a single command
- pub async fn process<'a>(
- &self,
- msg: String,
- client: &mut Client,
- state: &PlayerStatus,
- stickers: &FilterStickerNames<'a>,
- ) -> Result<()> {
- if let Some(stripped) = msg.strip_prefix("findadd ") {
- self.findadd(stripped.to_string(), client, stickers, state)
- .await
- } else if let Some(stripped) = msg.strip_prefix("searchadd ") {
- self.searchadd(stripped.to_string(), client, stickers, state)
- .await
- } else {
- unreachable!("Unkonwn command")
- }
- }
-
- /// Handle `findadd': "FILTER [sort TYPE] [window START:END]"
- async fn findadd<'a>(
- &self,
- msg: String,
- client: &mut Client,
- stickers: &FilterStickerNames<'a>,
- _state: &PlayerStatus,
- ) -> Result<()> {
- let mut buf = msg.into_bytes();
- let args: VecDeque<&str> = tokenize(&mut buf)
- .map(|r| match r {
- Ok(buf) => Ok(std::str::from_utf8(buf)
- .context("Failed to interpete `findadd` string as utf8")?),
- Err(err) => Err(err),
- })
- .collect::<Result<VecDeque<&str>>>()?;
-
- debug!("findadd arguments: {:#?}", args);
-
- // there should be 1, 3 or 5 arguments. `sort' & `window' are not supported, yet.
-
- // ExpressionParser's not terribly ergonomic: it returns a ParesError<L, T, E>; T is the
- // offending token, which has the same lifetime as our input, which makes it tough to
- // capture. Nor is there a convenient way in which to treat all variants other than the
- // Error Trait.
- let ast = match ExpressionParser::new().parse(args[0]) {
- Ok(ast) => ast,
- Err(err) => {
- bail!("Failed to parse filter: `{}`", err)
- }
- };
-
- debug!("ast: {:#?}", ast);
-
- let mut results = Vec::new();
- for song in evaluate(&ast, true, client, stickers)
- .await
- .context("Failed to evaluate filter")?
- {
- results.push(client.add(&song).await);
- }
- match results
- .into_iter()
- .collect::<std::result::Result<Vec<()>, Error>>()
- {
- Ok(_) => Ok(()),
- Err(err) => Err(err),
- }
- }
-
- /// Handle `searchadd': "FILTER [sort TYPE] [window START:END]"
- async fn searchadd<'a>(
- &self,
- msg: String,
- client: &mut Client,
- stickers: &FilterStickerNames<'a>,
- _state: &PlayerStatus,
- ) -> Result<()> {
- // Tokenize the message
- let mut buf = msg.into_bytes();
- let args: VecDeque<&str> = tokenize(&mut buf)
- .map(|r| match r {
- Ok(buf) => Ok(std::str::from_utf8(buf)
- .context("Failed to interpete `searchadd` string as utf8")?),
- Err(err) => Err(err),
- })
- .collect::<Result<VecDeque<_>>>()?;
-
- debug!("searchadd arguments: {:#?}", args);
-
- // there should be 1, 3 or 5 arguments. `sort' & `window' are not supported, yet.
-
- // ExpressionParser's not terribly ergonomic: it returns a ParesError<L, T, E>; T is the
- // offending token, which has the same lifetime as our input, which makes it tough to
- // capture. Nor is there a convenient way in which to treat all variants other than the
- // Error Trait.
- let ast = match ExpressionParser::new().parse(args[0]) {
- Ok(ast) => ast,
- Err(err) => {
- bail!("Failed to parse filter: `{err}`")
- }
- };
-
- debug!("ast: {:#?}", ast);
-
- let mut results = Vec::new();
- for song in evaluate(&ast, false, client, stickers)
- .await
- .context("Failed to evaluate ast")?
- {
- results.push(client.add(&song).await);
- }
- match results
- .into_iter()
- .collect::<std::result::Result<Vec<()>, Error>>()
- {
- Ok(_) => Ok(()),
- Err(err) => Err(err),
- }
- }
-}
-
-#[cfg(test)]
-mod tokenize_tests {
- use super::Result;
- use super::tokenize;
-
- #[test]
- fn tokenize_smoke() {
- let mut buf1 = String::from("some-command").into_bytes();
- let x1: Vec<&[u8]> = tokenize(&mut buf1).collect::<Result<Vec<&[u8]>>>().unwrap();
- assert_eq!(x1[0], b"some-command");
-
- let mut buf2 = String::from("a b").into_bytes();
- let x2: Vec<&[u8]> = tokenize(&mut buf2).collect::<Result<Vec<&[u8]>>>().unwrap();
- assert_eq!(x2[0], b"a");
- assert_eq!(x2[1], b"b");
-
- let mut buf3 = String::from("a \"b c\"").into_bytes();
- let x3: Vec<&[u8]> = tokenize(&mut buf3).collect::<Result<Vec<&[u8]>>>().unwrap();
- assert_eq!(x3[0], b"a");
- assert_eq!(x3[1], b"b c");
-
- let mut buf4 = String::from("a \"b c\" d").into_bytes();
- let x4: Vec<&[u8]> = tokenize(&mut buf4).collect::<Result<Vec<&[u8]>>>().unwrap();
- assert_eq!(x4[0], b"a");
- assert_eq!(x4[1], b"b c");
- assert_eq!(x4[2], b"d");
-
- let mut buf5 = String::from("simple-command \"with space\" \"with '\"").into_bytes();
- let x5: Vec<&[u8]> = tokenize(&mut buf5).collect::<Result<Vec<&[u8]>>>().unwrap();
- assert_eq!(x5[0], b"simple-command");
- assert_eq!(x5[1], b"with space");
- assert_eq!(x5[2], b"with '");
-
- let mut buf6 = String::from("cmd \"with\\\\slash and space\"").into_bytes();
- let x6: Vec<&[u8]> = tokenize(&mut buf6).collect::<Result<Vec<&[u8]>>>().unwrap();
- assert_eq!(x6[0], b"cmd");
- assert_eq!(x6[1], b"with\\slash and space");
-
- let mut buf7 = String::from(" cmd \"with\\\\slash and space\" ").into_bytes();
- let x7: Vec<&[u8]> = tokenize(&mut buf7).collect::<Result<Vec<&[u8]>>>().unwrap();
- assert_eq!(x7[0], b"cmd");
- assert_eq!(x7[1], b"with\\slash and space");
- }
-
- #[test]
- fn tokenize_filter() {
- let mut buf1 = String::from(r#""(artist =~ \"foo\\\\bar\\\"\")""#).into_bytes();
- let x1: Vec<&[u8]> = tokenize(&mut buf1).collect::<Result<Vec<&[u8]>>>().unwrap();
- assert_eq!(1, x1.len());
- eprintln!("x1[0] is ``{}''", std::str::from_utf8(x1[0]).unwrap());
- assert_eq!(
- std::str::from_utf8(x1[0]).unwrap(),
- r#"(artist =~ "foo\\bar\"")"#
- );
- }
-}