about summary refs log tree commit diff stats
path: root/pkgs/by-name
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm.rs106
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/filters.lalrpop51
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/filters_ast.rs47
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/lib.rs46
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/messages.rs409
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/storage/mod.rs4
-rwxr-xr-xpkgs/by-name/mp/mpp-lyrics/mpp-lyrics.sh2
7 files changed, 111 insertions, 554 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..82272aeb 100644
--- a/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm.rs
+++ b/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm.rs
@@ -27,8 +27,10 @@
 //! for `mppopmd`. Run `mppopm --help` for detailed usage.
 
 use mpdpopm::{
-    clients::{Client, PlayerStatus, quote},
+    clients::{Client, PlayerStatus},
     config::{self, Config},
+    filters::ExpressionParser,
+    filters_ast::{FilterStickerNames, evaluate},
     storage::{last_played, play_count, rating_count},
 };
 
@@ -263,28 +265,34 @@ 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<()> {
-    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(())
-}
+async fn searchadd(client: &mut Client, filter: &str, case_sensitive: bool) -> Result<()> {
+    let ast = match ExpressionParser::new().parse(filter) {
+        Ok(ast) => ast,
+        Err(err) => {
+            bail!("Failed to parse filter: `{}`", err)
+        }
+    };
 
-/// Send an arbitrary command
-async fn send_command(client: &mut Client, chan: &str, args: Vec<String>) -> Result<()> {
-    client
-        .send_message(
-            chan,
-            args.iter()
-                .map(String::as_str)
-                .map(quote)
-                .collect::<Vec<String>>()
-                .join(" ")
-                .as_str(),
-        )
-        .await?;
-    Ok(())
+    debug!("ast: {:#?}", ast);
+
+    let mut results = Vec::new();
+    for song in evaluate(&ast, case_sensitive, client, &FilterStickerNames::default())
+        .await
+        .context("Failed to evaluate filter")?
+    {
+        let out = client.add(&song).await;
+
+        if out.is_ok() {
+            eprintln!("Added: `{}`", song)
+        }
+
+        results.push(out);
+    }
+
+    match results.into_iter().collect::<Result<Vec<()>>>() {
+        Ok(_) => Ok(()),
+        Err(err) => Err(err),
+    }
 }
 
 /// `mppopmd' client
@@ -442,34 +450,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,14 +472,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,
 
-    /// Send a command to mpd.
-    #[clap(verbatim_doc_comment)]
-    SendCommand { args: Vec<String> },
+        /// Respect the casing, when performing the filter evaluation.
+        #[arg(short, long, default_value_t = true)]
+        case_sensitive: bool,
+    },
 }
 
 #[tokio::main]
@@ -584,14 +565,9 @@ 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::SendCommand { args } => {
-            send_command(&mut client, &config.commands_chan, args).await
-        }
+        SubCommand::Searchadd {
+            filter,
+            case_sensitive,
+        } => searchadd(&mut client, &filter, case_sensitive).await,
     }
 }
diff --git a/pkgs/by-name/mp/mpdpopm/src/filters.lalrpop b/pkgs/by-name/mp/mpdpopm/src/filters.lalrpop
index a591a3ba..0c12f59b 100644
--- a/pkgs/by-name/mp/mpdpopm/src/filters.lalrpop
+++ b/pkgs/by-name/mp/mpdpopm/src/filters.lalrpop
@@ -15,8 +15,18 @@
 
 use lalrpop_util::ParseError;
 
-use crate::filters_ast::{Conjunction, Disjunction, Expression, OpCode, Selector, Term, Value,
-                         expect_quoted, parse_iso_8601};
+use crate::filters_ast::{
+    Conjunction,
+    Disjunction,
+    Expression,
+    OpCode,
+    Selector,
+    Term,
+    Value,
+    expect_quoted,
+    parse_iso_8601
+};
+use tracing::debug;
 
 grammar;
 
@@ -63,20 +73,27 @@ pub ExprSel: Selector = {
     r"(?i)rating"                     => Selector::Rating,
     r"(?i)playcount"                  => Selector::PlayCount,
     r"(?i)lastplayed"                 => Selector::LastPlayed,
+    r"(?i)skipped"                    => Selector::Skipped,
 };
 
 pub Token: Value = {
-    <s:r"[0-9]+"> =>? {
-        eprintln!("matched token: ``{}''.", s);
+    <s:r"(-)?[0-9]+"> =>? {
+        debug!("matched token: ``{}''.", s);
         // We need to yield a Result<Value, ParseError>
         match s.parse::<usize>() {
             Ok(n) => Ok(Value::Uint(n)),
-            Err(_) => Err(ParseError::User {
-                error: "Internal parse error while parsing unsigned int" })
+            Err(_) => match s.parse::<i64>() {
+                Ok(n) => Ok(Value::Int(n)),
+                Err(_) => Err(
+                    ParseError::User {
+                        error: "Internal parse error while parsing unsigned int"
+                    }
+                )
+            }
         }
     },
     <s:r#""([ \t'a-zA-Z0-9~!@#$%^&*()-=_+\[\]{}|;:<>,./?]|\\\\|\\"|\\')+""#> => {
-        eprintln!("matched token: ``{}''.", s);
+        debug!("matched token: ``{}''.", s);
         let s = expect_quoted(s).unwrap();
         match parse_iso_8601(&mut s.as_bytes()) {
             Ok(x) => Value::UnixEpoch(x),
@@ -84,7 +101,7 @@ pub Token: Value = {
         }
     },
     <s:r#"'([ \t"a-zA-Z0-9~!@#$%^&*()-=_+\[\]{}|;:<>,./?]|\\\\|\\'|\\")+'"#> => {
-        eprintln!("matched token: ``{}''.", s);
+        debug!("matched token: ``{}''.", s);
         let s = expect_quoted(s).unwrap();
         match parse_iso_8601(&mut s.as_bytes()) {
             Ok(x) => Value::UnixEpoch(x),
@@ -95,49 +112,49 @@ pub Token: Value = {
 
 pub Term: Box<Term> = {
     <t:ExprSel> <u:Token> => {
-        eprintln!("matched unary condition: ``({}, {:#?})''", t, u);
+        debug!("matched unary condition: ``({}, {:#?})''", t, u);
         Box::new(Term::UnaryCondition(t, u))
     },
     <t:ExprSel> <o:ExprOp> <u:Token> => {
-        eprintln!("matched binary condition: ``({}, {:#?}, {:#?})''", t, o, u);
+        debug!("matched binary condition: ``({}, {:#?}, {:#?})''", t, o, u);
         Box::new(Term::BinaryCondition(t, o, u))
     },
 }
 
 pub Conjunction: Box<Conjunction> = {
     <e1:Expression> "AND" <e2:Expression> => {
-        eprintln!("matched conjunction: ``({:#?}, {:#?})''", e1, e2);
+        debug!("matched conjunction: ``({:#?}, {:#?})''", e1, e2);
         Box::new(Conjunction::Simple(e1, e2))
     },
     <c:Conjunction> "AND" <e:Expression> => {
-        eprintln!("matched conjunction: ``({:#?}, {:#?})''", c, e);
+        debug!("matched conjunction: ``({:#?}, {:#?})''", c, e);
         Box::new(Conjunction::Compound(c, e))
     },
 }
 
 pub Disjunction: Box<Disjunction> = {
     <e1:Expression> "OR" <e2:Expression> => {
-        eprintln!("matched disjunction: ``({:#?}, {:#?})''", e1, e2);
+        debug!("matched disjunction: ``({:#?}, {:#?})''", e1, e2);
         Box::new(Disjunction::Simple(e1, e2))
     },
     <c:Disjunction> "OR" <e:Expression> => {
-        eprintln!("matched disjunction: ``({:#?}, {:#?})''", c, e);
+        debug!("matched disjunction: ``({:#?}, {:#?})''", c, e);
         Box::new(Disjunction::Compound(c, e))
     },
 }
 
 pub Expression: Box<Expression> = {
     "(" <t:Term> ")" => {
-        eprintln!("matched parenthesized term: ``({:#?})''", t);
+        debug!("matched parenthesized term: ``({:#?})''", t);
         Box::new(Expression::Simple(t))
     },
     "(" "!" <e:Expression> ")" => Box::new(Expression::Negation(e)),
     "(" <c:Conjunction> ")" => {
-        eprintln!("matched parenthesized conjunction: ``({:#?})''", c);
+        debug!("matched parenthesized conjunction: ``({:#?})''", c);
         Box::new(Expression::Conjunction(c))
     },
     "(" <c:Disjunction>  ")" => {
-        eprintln!("matched parenthesized disjunction: ``({:#?})''", c);
+        debug!("matched parenthesized disjunction: ``({:#?})''", c);
         Box::new(Expression::Disjunction(c))
     },
 }
diff --git a/pkgs/by-name/mp/mpdpopm/src/filters_ast.rs b/pkgs/by-name/mp/mpdpopm/src/filters_ast.rs
index bd1a67d6..8b8c2696 100644
--- a/pkgs/by-name/mp/mpdpopm/src/filters_ast.rs
+++ b/pkgs/by-name/mp/mpdpopm/src/filters_ast.rs
@@ -18,7 +18,7 @@
 //! This module provides support for our [lalrpop](https://github.com/lalrpop/lalrpop) grammar.
 
 use crate::clients::Client;
-use crate::storage::{last_played, play_count, rating_count};
+use crate::storage::{last_played, play_count, rating_count, skipped};
 
 use anyhow::{Context, Error, Result, anyhow, bail};
 use boolinator::Boolinator;
@@ -95,6 +95,7 @@ pub enum Selector {
     Rating,
     PlayCount,
     LastPlayed,
+    Skipped,
 }
 
 impl std::fmt::Display for Selector {
@@ -133,6 +134,7 @@ impl std::fmt::Display for Selector {
                 Selector::Rating => "rating",
                 Selector::PlayCount => "playcount",
                 Selector::LastPlayed => "lastplayed",
+                Selector::Skipped => "skipped",
             }
         )
     }
@@ -143,6 +145,7 @@ pub enum Value {
     Text(String),
     UnixEpoch(i64),
     Uint(usize),
+    Int(i64),
 }
 
 fn quote_value(x: &Value) -> String {
@@ -166,6 +169,9 @@ fn quote_value(x: &Value) -> String {
         Value::Uint(n) => {
             format!("'{}'", n)
         }
+        Value::Int(n) => {
+            format!("'{}'", n)
+        }
     }
 }
 
@@ -655,6 +661,7 @@ async fn eval_numeric_sticker_term<
         .for_each(|song| {
             m.entry(song).or_insert(default_val);
         });
+
     // Now that we don't have to worry about operations that can fail, we can use
     // `filter_map'.
     Ok(m.drain()
@@ -674,6 +681,7 @@ pub struct FilterStickerNames<'a> {
     rating: &'a str,
     playcount: &'a str,
     lastplayed: &'a str,
+    skipped: &'a str,
 }
 
 impl FilterStickerNames<'static> {
@@ -688,6 +696,7 @@ impl Default for FilterStickerNames<'static> {
             rating: rating_count::STICKER,
             playcount: play_count::STICKER,
             lastplayed: last_played::STICKER,
+            skipped: skipped::STICKER,
         }
     }
 }
@@ -711,18 +720,19 @@ async fn eval_term<'a>(
             .collect()),
         Term::BinaryCondition(attr, op, val) => {
             if *attr == Selector::Rating {
-                match val {
-                    Value::Uint(n) => {
-                        if *n > 255 {
-                            bail!("Rating of `{}` is greater than allowed!", n)
-                        }
-                        Ok(
-                            eval_numeric_sticker_term(stickers.rating, client, *op, *n as u8, 0)
-                                .await?,
-                        )
-                    }
-                    _ => bail!("filter ratings expect an unsigned int; got {:#?}", val),
-                }
+                let value = match val {
+                    Value::Int(n) => *n as i128,
+                    Value::Uint(n) => *n as i128,
+                    _ => bail!("filter ratings expect an int; got {:#?}", val),
+                };
+
+                let val: i8 = value.try_into().with_context(|| {
+                    format!(
+                        "Failed to convert `{}` into a number from -128 to 128!",
+                        value
+                    )
+                })?;
+                Ok(eval_numeric_sticker_term(stickers.rating, client, *op, val, 0).await?)
             } else if *attr == Selector::PlayCount {
                 match val {
                     Value::Uint(n) => {
@@ -731,7 +741,7 @@ async fn eval_term<'a>(
                                 .await?,
                         )
                     }
-                    _ => bail!("filter ratings expect an unsigned int; got {:#?}", val),
+                    _ => bail!("filter play_count expect an unsigned int; got {:#?}", val),
                 }
             } else if *attr == Selector::LastPlayed {
                 match val {
@@ -741,7 +751,14 @@ async fn eval_term<'a>(
                                 .await?,
                         )
                     }
-                    _ => bail!("filter ratings expect an unsigned int; got {:#?}", val),
+                    _ => bail!("filter last_played expect an unix epoch; got {:#?}", val),
+                }
+            } else if *attr == Selector::Skipped {
+                match val {
+                    Value::Uint(t) => {
+                        Ok(eval_numeric_sticker_term(stickers.skipped, client, *op, *t, 0).await?)
+                    }
+                    _ => bail!("filter skipped expect an unsigned int; got {:#?}", val),
                 }
             } else {
                 Ok(client
diff --git a/pkgs/by-name/mp/mpdpopm/src/lib.rs b/pkgs/by-name/mp/mpdpopm/src/lib.rs
index 4fe523ea..d5db57b4 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;
@@ -52,8 +51,6 @@ pub mod filters {
 use crate::{
     clients::{Client, IdleClient, IdleSubSystem},
     config::{Config, Connection},
-    filters_ast::FilterStickerNames,
-    messages::MessageProcessor,
     playcounts::PlayState,
 };
 
@@ -70,8 +67,6 @@ use tracing::{debug, error, info};
 pub async fn mpdpopm(cfg: Config) -> std::result::Result<(), Error> {
     info!("mpdpopm {} beginning.", vars::VERSION);
 
-    let filter_stickers = FilterStickerNames::new();
-
     let mut client =
         match cfg.conn {
             Connection::Local { ref path } => Client::open(path)
@@ -110,12 +105,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 +134,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 +142,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 +155,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\"")"#
-        );
-    }
-}
diff --git a/pkgs/by-name/mp/mpdpopm/src/storage/mod.rs b/pkgs/by-name/mp/mpdpopm/src/storage/mod.rs
index 24d8dcb5..c13475ad 100644
--- a/pkgs/by-name/mp/mpdpopm/src/storage/mod.rs
+++ b/pkgs/by-name/mp/mpdpopm/src/storage/mod.rs
@@ -1,4 +1,4 @@
-use anyhow::{Error, Result};
+use anyhow::Result;
 
 pub mod play_count {
     use anyhow::Context;
@@ -67,7 +67,7 @@ pub mod skipped {
 
     use super::Result;
 
-    const STICKER: &str = "unwoundstack.com:skipped_count";
+    pub(crate) const STICKER: &str = "unwoundstack.com:skipped_count";
 
     /// Retrieve the skip count for a track
     pub async fn get(client: &mut Client, file: &str) -> Result<Option<usize>> {
diff --git a/pkgs/by-name/mp/mpp-lyrics/mpp-lyrics.sh b/pkgs/by-name/mp/mpp-lyrics/mpp-lyrics.sh
index fa1cac49..6c96a9d9 100755
--- a/pkgs/by-name/mp/mpp-lyrics/mpp-lyrics.sh
+++ b/pkgs/by-name/mp/mpp-lyrics/mpp-lyrics.sh
@@ -16,6 +16,6 @@ die() {
 }
 
 cd "$XDG_MUSIC_DIR/beets" || die "No music dir!"
-exiftool "$(mpc --format '%file%' current)" -json | jq '.[0].Lyrics' --raw-output | less
+exiftool "$(mpc --format '%file%' current)" -json | jq '.[0] | if has("Lyrics") then .Lyrics elif has("Lyrics-xxx") then ."Lyrics-xxx" else "<No lyrics key>" end' --raw-output | less
 
 # vim: ft=sh