aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/mp/mpdpopm/src
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-02-19 22:31:49 +0100
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-02-19 22:31:49 +0100
commit0f08d6ae51a14144ff791b4a051751a5156707e8 (patch)
tree2b10e911fd4baf59e92c5a8d938019c0265b1bf1 /pkgs/by-name/mp/mpdpopm/src
parentmodules/legacy/beets/plugins/inline: Don't fail on empty `albumartists` (diff)
downloadnixos-config-0f08d6ae51a14144ff791b4a051751a5156707e8.zip
pkgs/mpdpopmd: Support a stats show and setting selection priority for dj
Diffstat (limited to '')
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm/cli.rs233
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm/main.rs (renamed from pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm.rs)421
2 files changed, 437 insertions, 217 deletions
diff --git a/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm/cli.rs b/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm/cli.rs
new file mode 100644
index 00000000..c20bf3fa
--- /dev/null
+++ b/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm/cli.rs
@@ -0,0 +1,233 @@
+use clap::{Parser, Subcommand};
+use std::path::PathBuf;
+
+/// `mppopmd' client
+#[derive(Parser)]
+pub(crate) struct Args {
+ /// path to configuration file
+ #[arg(short, long)]
+ pub(crate) config: Option<PathBuf>,
+
+ /// enable verbose logging
+ #[arg(short, long)]
+ pub(crate) verbose: bool,
+
+ /// enable debug loggin (implies --verbose)
+ #[arg(short, long)]
+ pub(crate) debug: bool,
+
+ #[command(subcommand)]
+ pub(crate) command: SubCommand,
+}
+
+#[derive(Subcommand)]
+pub(crate) enum RatingCommand {
+ /// retrieve the rating for one or more tracks
+ ///
+ /// With no arguments, retrieve the rating of the current song & print it
+ /// on stdout. With one argument, retrieve that track's rating & print it
+ /// on stdout. With multiple arguments, print their ratings on stdout, one
+ /// per line, prefixed by the track name.
+ ///
+ /// Ratings are expressed as an integer between -128 & 128, exclusive, with
+ /// the convention that 0 denotes "un-rated".
+ #[clap(verbatim_doc_comment)]
+ Get {
+ /// Always show the song URI, even when there is only one track
+ #[arg(short, long)]
+ with_uri: bool,
+
+ tracks: Option<Vec<String>>,
+ },
+
+ /// set the rating for one track
+ ///
+ /// With one argument, set the rating of the current song to that argument.
+ /// With a second argument, rate that song at the first argument. Ratings
+ /// may be expressed a an integer between 0 & 255, inclusive.
+ #[clap(verbatim_doc_comment)]
+ Set { rating: i8, track: Option<String> },
+
+ /// increment the rating for one track
+ ///
+ /// With one argument, increment the rating of the current song.
+ /// With a second argument, rate that song at the first argument.
+ #[clap(verbatim_doc_comment)]
+ Inc { track: Option<String> },
+
+ /// decrement the rating for one track
+ ///
+ /// With one argument, decrement the rating of the current song.
+ /// With a second argument, rate that song at the first argument.
+ #[clap(verbatim_doc_comment)]
+ Decr { track: Option<String> },
+}
+
+#[derive(Subcommand)]
+pub(crate) enum PlayCountCommand {
+ /// retrieve the play count for one or more tracks
+ ///
+ /// With no arguments, retrieve the play count of the current song & print it
+ /// on stdout. With one argument, retrieve that track's play count & print it
+ /// on stdout. With multiple arguments, print their play counts on stdout, one
+ /// per line, prefixed by the track name.
+ #[clap(verbatim_doc_comment)]
+ Get {
+ /// Always show the song URI, even when there is only one track
+ #[arg(short, long)]
+ with_uri: bool,
+
+ tracks: Option<Vec<String>>,
+ },
+
+ /// set the play count for one track
+ ///
+ /// With one argument, set the play count of the current song to that argument. With a
+ /// second argument, set the play count for that song to the first.
+ #[clap(verbatim_doc_comment)]
+ Set {
+ play_count: usize,
+ track: Option<String>,
+ },
+}
+
+#[derive(Subcommand)]
+pub(crate) enum LastPlayedCommand {
+ /// retrieve the last played timestamp for one or more tracks
+ ///
+ /// With no arguments, retrieve the last played timestamp of the current
+ /// song & print it on stdout. With one argument, retrieve that track's
+ /// last played time & print it on stdout. With multiple arguments, print
+ /// their last played times on stdout, one per line, prefixed by the track
+ /// name.
+ ///
+ /// The last played timestamp is expressed in seconds since Unix epoch.
+ #[clap(verbatim_doc_comment)]
+ Get {
+ /// Always show the song URI, even when there is only one track
+ #[arg(short, long)]
+ with_uri: bool,
+
+ tracks: Option<Vec<String>>,
+ },
+
+ /// set the last played timestamp for one track
+ ///
+ /// With one argument, set the last played time of the current song. With two
+ /// arguments, set the last played time for the second argument to the first.
+ /// The last played timestamp is expressed in seconds since Unix epoch.
+ #[clap(verbatim_doc_comment)]
+ Set {
+ last_played: u64,
+ track: Option<String>,
+ },
+}
+
+#[derive(Subcommand)]
+pub(crate) enum PlaylistsCommand {
+ /// retrieve the list of stored playlists
+ #[clap(verbatim_doc_comment)]
+ Get {},
+}
+
+#[derive(Subcommand)]
+pub(crate) enum DjCommand {
+ /// Activate the automatic DJ mode on the mpdpopmd daemon.
+ ///
+ /// In this mode, the daemon will automatically add new tracks to the playlist based on a
+ /// recommendation algorithm.
+ #[clap(verbatim_doc_comment)]
+ Start {
+ /// The chance to select a "positive" track
+ #[arg(long, default_value_t = 0.65)]
+ positive_chance: f64,
+
+ /// The chance to select a "neutral" track
+ #[arg(long, default_value_t = 0.5)]
+ neutral_chance: f64,
+
+ /// The chance to select a "negative" track
+ #[arg(long, default_value_t = 0.2)]
+ negative_chance: f64,
+ },
+
+ /// Deactivate the automatic DJ mode on the mpdpopmd daemon.
+ ///
+ /// In this mode, the daemon will automatically add new tracks to the playlist based on a
+ /// recommendation algorithm.
+ #[clap(verbatim_doc_comment)]
+ Stop {},
+}
+
+#[derive(Subcommand)]
+pub(crate) enum SubCommand {
+ /// Change details about rating.
+ Rating {
+ #[command(subcommand)]
+ command: RatingCommand,
+ },
+
+ /// Change details about play count.
+ PlayCount {
+ #[command(subcommand)]
+ command: PlayCountCommand,
+ },
+
+ /// Change details about last played date.
+ LastPlayed {
+ #[command(subcommand)]
+ command: LastPlayedCommand,
+ },
+
+ /// Change details about generated playlists.
+ Playlists {
+ #[command(subcommand)]
+ command: PlaylistsCommand,
+ },
+
+ /// 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.
+ ///
+ /// The MPD `searchadd' <https://www.musicpd.org/doc/html/protocol.html#command-searchadd> 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 searchadd "(rating > 2)"
+ ///
+ /// Will add all songs in the library with a rating sticker > 2 to the play queue.
+ ///
+ /// mppopm also introduces OR clauses (MPD only supports AND), so that:
+ ///
+ /// mppopm searchadd "((rating > 2) AND (artist =~ \"pogues\"))"
+ ///
+ /// will add all songs whose artist tag matches the regexp "pogues" with a rating greater than
+ /// 2.
+ #[clap(verbatim_doc_comment)]
+ Searchadd {
+ filter: String,
+
+ /// Respect the casing, when performing the filter evaluation.
+ #[arg(short, long, default_value_t = false)]
+ case_sensitive: bool,
+ },
+
+ /// Modify the automatic DJ mode on the mpdpopmd daemon.
+ ///
+ /// In this mode, the daemon will automatically add new tracks to the playlist based on a
+ /// recommendation algorithm.
+ Dj {
+ #[command(subcommand)]
+ command: DjCommand,
+ },
+
+ /// Show general stats about your music collection.
+ ///
+ /// This includes favorite artist, songs and also the negative ones.
+ Stats {
+ }
+}
diff --git a/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm.rs b/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm/main.rs
index faa651bf..42f01873 100644
--- a/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm.rs
+++ b/pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm/main.rs
@@ -26,21 +26,38 @@
//! along the lines of [mpdcron](https://alip.github.io/mpdcron)). `mppopm` is a command-line client
//! for `mppopmd`. Run `mppopm --help` for detailed usage.
+use std::{collections::HashMap, io::stdout};
+
+use clap::Parser;
use mpdpopm::{
clients::{Client, PlayerStatus},
config::{self, Config},
+ dj::algorithms::Discovery,
filters::ExpressionParser,
filters_ast::{FilterStickerNames, evaluate},
messanges::COMMAND_CHANNEL,
- storage::{last_played, play_count, rating},
+ storage::{last_played, play_count, rating, skip_count},
};
use anyhow::{Context, Result, anyhow, bail};
-use clap::{Parser, Subcommand};
+use ratatui::{
+ Terminal, TerminalOptions, Viewport,
+ crossterm::style::Stylize,
+ layout::HorizontalAlignment,
+ prelude::CrosstermBackend,
+ style::{Color, Style},
+ text::Line,
+ widgets::{Bar, BarChart, BarGroup, Block, Borders},
+};
use tracing::{debug, info, level_filters::LevelFilter, trace};
use tracing_subscriber::{EnvFilter, Registry, layer::SubscriberExt};
-use std::path::PathBuf;
+use crate::cli::{
+ Args, DjCommand, LastPlayedCommand, PlayCountCommand, PlaylistsCommand, RatingCommand,
+ SubCommand,
+};
+
+mod cli;
/// Map `tracks' argument(s) to a Vec of String containing one or more mpd URIs
///
@@ -296,219 +313,6 @@ async fn searchadd(client: &mut Client, filter: &str, case_sensitive: bool) -> R
}
}
-/// `mppopmd' client
-#[derive(Parser)]
-struct Args {
- /// path to configuration file
- #[arg(short, long)]
- config: Option<PathBuf>,
-
- /// enable verbose logging
- #[arg(short, long)]
- verbose: bool,
-
- /// enable debug loggin (implies --verbose)
- #[arg(short, long)]
- debug: bool,
-
- #[command(subcommand)]
- command: SubCommand,
-}
-
-#[derive(Subcommand)]
-enum RatingCommand {
- /// retrieve the rating for one or more tracks
- ///
- /// With no arguments, retrieve the rating of the current song & print it
- /// on stdout. With one argument, retrieve that track's rating & print it
- /// on stdout. With multiple arguments, print their ratings on stdout, one
- /// per line, prefixed by the track name.
- ///
- /// Ratings are expressed as an integer between -128 & 128, exclusive, with
- /// the convention that 0 denotes "un-rated".
- #[clap(verbatim_doc_comment)]
- Get {
- /// Always show the song URI, even when there is only one track
- #[arg(short, long)]
- with_uri: bool,
-
- tracks: Option<Vec<String>>,
- },
-
- /// set the rating for one track
- ///
- /// With one argument, set the rating of the current song to that argument.
- /// With a second argument, rate that song at the first argument. Ratings
- /// may be expressed a an integer between 0 & 255, inclusive.
- #[clap(verbatim_doc_comment)]
- Set { rating: i8, track: Option<String> },
-
- /// increment the rating for one track
- ///
- /// With one argument, increment the rating of the current song.
- /// With a second argument, rate that song at the first argument.
- #[clap(verbatim_doc_comment)]
- Inc { track: Option<String> },
-
- /// decrement the rating for one track
- ///
- /// With one argument, decrement the rating of the current song.
- /// With a second argument, rate that song at the first argument.
- #[clap(verbatim_doc_comment)]
- Decr { track: Option<String> },
-}
-
-#[derive(Subcommand)]
-enum PlayCountCommand {
- /// retrieve the play count for one or more tracks
- ///
- /// With no arguments, retrieve the play count of the current song & print it
- /// on stdout. With one argument, retrieve that track's play count & print it
- /// on stdout. With multiple arguments, print their play counts on stdout, one
- /// per line, prefixed by the track name.
- #[clap(verbatim_doc_comment)]
- Get {
- /// Always show the song URI, even when there is only one track
- #[arg(short, long)]
- with_uri: bool,
-
- tracks: Option<Vec<String>>,
- },
-
- /// set the play count for one track
- ///
- /// With one argument, set the play count of the current song to that argument. With a
- /// second argument, set the play count for that song to the first.
- #[clap(verbatim_doc_comment)]
- Set {
- play_count: usize,
- track: Option<String>,
- },
-}
-
-#[derive(Subcommand)]
-enum LastPlayedCommand {
- /// retrieve the last played timestamp for one or more tracks
- ///
- /// With no arguments, retrieve the last played timestamp of the current
- /// song & print it on stdout. With one argument, retrieve that track's
- /// last played time & print it on stdout. With multiple arguments, print
- /// their last played times on stdout, one per line, prefixed by the track
- /// name.
- ///
- /// The last played timestamp is expressed in seconds since Unix epoch.
- #[clap(verbatim_doc_comment)]
- Get {
- /// Always show the song URI, even when there is only one track
- #[arg(short, long)]
- with_uri: bool,
-
- tracks: Option<Vec<String>>,
- },
-
- /// set the last played timestamp for one track
- ///
- /// With one argument, set the last played time of the current song. With two
- /// arguments, set the last played time for the second argument to the first.
- /// The last played timestamp is expressed in seconds since Unix epoch.
- #[clap(verbatim_doc_comment)]
- Set {
- last_played: u64,
- track: Option<String>,
- },
-}
-
-#[derive(Subcommand)]
-enum PlaylistsCommand {
- /// retrieve the list of stored playlists
- #[clap(verbatim_doc_comment)]
- Get {},
-}
-
-#[derive(Subcommand)]
-enum DjCommand {
- /// Activate the automatic DJ mode on the mpdpopmd daemon.
- ///
- /// In this mode, the daemon will automatically add new tracks to the playlist based on a
- /// recommendation algorithm.
- #[clap(verbatim_doc_comment)]
- Start {},
-
- /// Deactivate the automatic DJ mode on the mpdpopmd daemon.
- ///
- /// In this mode, the daemon will automatically add new tracks to the playlist based on a
- /// recommendation algorithm.
- #[clap(verbatim_doc_comment)]
- Stop {},
-}
-
-#[derive(Subcommand)]
-enum SubCommand {
- /// Change details about rating.
- Rating {
- #[command(subcommand)]
- command: RatingCommand,
- },
-
- /// Change details about play count.
- PlayCount {
- #[command(subcommand)]
- command: PlayCountCommand,
- },
-
- /// Change details about last played date.
- LastPlayed {
- #[command(subcommand)]
- command: LastPlayedCommand,
- },
-
- /// Change details about generated playlists.
- Playlists {
- #[command(subcommand)]
- command: PlaylistsCommand,
- },
-
- /// 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.
- ///
- /// The MPD `searchadd' <https://www.musicpd.org/doc/html/protocol.html#command-searchadd> 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 searchadd "(rating > 2)"
- ///
- /// Will add all songs in the library with a rating sticker > 2 to the play queue.
- ///
- /// mppopm also introduces OR clauses (MPD only supports AND), so that:
- ///
- /// mppopm searchadd "((rating > 2) AND (artist =~ \"pogues\"))"
- ///
- /// will add all songs whose artist tag matches the regexp "pogues" with a rating greater than
- /// 2.
- #[clap(verbatim_doc_comment)]
- Searchadd {
- filter: String,
-
- /// Respect the casing, when performing the filter evaluation.
- #[arg(short, long, default_value_t = false)]
- case_sensitive: bool,
- },
-
- /// Modify the automatic DJ mode on the mpdpopmd daemon.
- ///
- /// In this mode, the daemon will automatically add new tracks to the playlist based on a
- /// recommendation algorithm.
- Dj {
- #[command(subcommand)]
- command: DjCommand,
- },
-}
-
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
@@ -597,8 +401,191 @@ async fn main() -> Result<()> {
case_sensitive,
} => searchadd(&mut client, &filter, case_sensitive).await,
SubCommand::Dj { command } => match command {
- DjCommand::Start {} => client.send_message(COMMAND_CHANNEL, "dj start").await,
+ DjCommand::Start {
+ positive_chance,
+ neutral_chance,
+ negative_chance,
+ } => {
+ client
+ .send_message(
+ COMMAND_CHANNEL,
+ format!(
+ "dj start \
+ --positive-chance {positive_chance} \
+ --neutral-chance {neutral_chance} \
+ --negative-chance {negative_chance}"
+ )
+ .as_str(),
+ )
+ .await
+ }
DjCommand::Stop {} => client.send_message(COMMAND_CHANNEL, "dj stop").await,
},
+ SubCommand::Stats {} => {
+ struct Rating {
+ play_count: Option<usize>,
+ skip_count: Option<usize>,
+ last_played: Option<u64>,
+ rating: Option<i8>,
+ dj_weight: i64,
+ }
+ fn vertical_bar<'a>(count: i64, amount: usize) -> Bar<'a> {
+ fn amount_style(amount: usize) -> Style {
+ let green = (255.0 * (1.0 - ((amount as f64) - 50.0) / 40.0)) as u8;
+ let color = Color::Rgb(255, green, 0);
+
+ Style::new().fg(color)
+ }
+
+ Bar::default()
+ .value(amount as u64)
+ .label(Line::from(count.to_string()))
+ .style(amount_style(amount))
+ .value_style(amount_style(amount).reversed())
+ }
+ macro_rules! top_five {
+ ($(@$convert:tt)? mode = $mode:tt, $rating_map:expr, $key:ident, $($other:ident),* $(,)?) => {
+ let mut vec = $rating_map
+ .iter()
+ .filter_map(|(track, rating)| top_five!(@convert $($convert)? rating.$key).map(|v| (track, v, rating)))
+ .collect::<Vec<_>>();
+ vec.sort_by_key(|(_, pc, _)| *pc);
+
+ top_five!(@gen_mode $mode, vec.iter())
+ .take(5)
+ .for_each(|(song, play_count, rating)| {
+ println!(
+ concat!(" - {}: {}", $(top_five!(@gen_empty $other)),*),
+ <String as Clone>::clone(&song).bold().blue(),
+ play_count.to_string().bold().white(),
+ $(
+ rating
+ .$other
+ .map(|r| format!(" ({}: {r})", stringify!($other)))
+ .unwrap_or(String::new()),
+ )*
+ )
+ });
+ };
+ (@gen_mode top, $expr:expr) => {
+ $expr.rev()
+ };
+ (@gen_mode bottom, $expr:expr) => {
+ $expr
+ };
+ (@gen_empty $tt:tt) => {
+ "{}"
+ };
+ (@convert convert_to_option $tt:expr) => {
+ Some($tt)
+ };
+ (@convert $tt:expr) => {
+ $tt
+ }
+ }
+ macro_rules! histogram {
+ ($(@$convert:tt)? $rating_map:expr, $key:ident, $title:literal) => {
+ let backend = CrosstermBackend::new(stdout());
+ let viewport = Viewport::Inline(20);
+ let mut terminal =
+ Terminal::with_options(backend, TerminalOptions { viewport })?;
+
+ let result = (|| {
+ terminal.draw(|frame| {
+ let line_chart = frame.area();
+
+ let bars: Vec<Bar> = {
+ let mut map = HashMap::new();
+ $rating_map
+ .values()
+ .filter_map(|rating| histogram!(@convert $($convert)? rating.$key))
+ .for_each(|dj_weight| {
+ map.entry(dj_weight)
+ .and_modify(|e| {
+ *e += 1;
+ })
+ .or_insert(1);
+ });
+
+ let mut vec = map.into_iter().collect::<Vec<(_, _)>>();
+ vec.sort_by_key(|(pc, _)| *pc);
+
+ vec.into_iter()
+ }
+ .map(|(dj_weight, amount)| vertical_bar(dj_weight.try_into().expect("Should be convertible"), amount))
+ .collect();
+
+ let title = Line::from($title).centered();
+ let chart = BarChart::default()
+ .data(BarGroup::default().bars(&bars))
+ .block(
+ Block::new()
+ .title(title)
+ .title_alignment(HorizontalAlignment::Left)
+ .borders(Borders::all()),
+ )
+ .bar_width(5);
+
+ frame.render_widget(chart, line_chart);
+ })?;
+
+ Ok::<_, anyhow::Error>(())
+ })();
+
+ ratatui::restore();
+ println!();
+ result?;
+ };
+ (@convert convert_to_option $val:expr) => {
+ Some($val)
+ };
+ (@convert $val:expr) => {
+ $val
+ };
+ }
+
+ let all = client.get_all_songs().await?;
+
+ let mut rating_map = HashMap::new();
+ for song in &all {
+ let rating = Rating {
+ play_count: play_count::get(&mut client, song).await?,
+ skip_count: skip_count::get(&mut client, song).await?,
+ last_played: last_played::get(&mut client, song).await?,
+ rating: rating::get(&mut client, song).await?,
+ dj_weight: Discovery::weight_track(&mut client, song).await?,
+ };
+ rating_map.insert(song, rating);
+ }
+
+ let played_songs = rating_map
+ .values()
+ .filter(|s| s.last_played.is_some())
+ .count();
+
+ println!(
+ "Songs played: {:.2}%",
+ (played_songs as f64 / all.len() as f64) * 100.0
+ );
+
+ histogram!(rating_map, play_count, "Play counts");
+
+ println!("\nMost played songs:");
+ top_five!(mode = top, rating_map, play_count, skip_count, rating);
+
+ println!("\nMost skipped songs:");
+ top_five!(mode = top, rating_map, skip_count, play_count, rating);
+
+ println!("\nTop songs based on dj weight:");
+ top_five!(@convert_to_option mode = top, rating_map, dj_weight, rating, play_count, skip_count);
+
+ println!("\nBottom 5 songs based on dj weight:");
+ top_five!(@convert_to_option mode = bottom, rating_map, dj_weight, rating, play_count, skip_count);
+
+ println!();
+ histogram!(@convert_to_option rating_map, dj_weight, "Dj weights");
+
+ Ok(())
+ }
}
}