diff options
| author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-02-19 22:31:49 +0100 |
|---|---|---|
| committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-02-19 22:31:49 +0100 |
| commit | 0f08d6ae51a14144ff791b4a051751a5156707e8 (patch) | |
| tree | 2b10e911fd4baf59e92c5a8d938019c0265b1bf1 /pkgs/by-name/mp/mpdpopm/src | |
| parent | modules/legacy/beets/plugins/inline: Don't fail on empty `albumartists` (diff) | |
| download | nixos-config-0f08d6ae51a14144ff791b4a051751a5156707e8.zip | |
pkgs/mpdpopmd: Support a stats show and setting selection priority for dj
Diffstat (limited to 'pkgs/by-name/mp/mpdpopm/src')
| -rw-r--r-- | pkgs/by-name/mp/mpdpopm/src/bin/mpdpopm/cli.rs | 233 | ||||
| -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(()) + } } } |
