about summary refs log tree commit diff stats
path: root/pkgs/by-name/mp/mpdpopm/src
diff options
context:
space:
mode:
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
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/config.rs127
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/dj/algorithms.rs206
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/dj/mod.rs2
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/lib.rs12
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/messanges/mod.rs40
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/playcounts.rs53
8 files changed, 677 insertions, 417 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(())
+        }
     }
 }
diff --git a/pkgs/by-name/mp/mpdpopm/src/config.rs b/pkgs/by-name/mp/mpdpopm/src/config.rs
index b4fe3c53..8bb5abfb 100644
--- a/pkgs/by-name/mp/mpdpopm/src/config.rs
+++ b/pkgs/by-name/mp/mpdpopm/src/config.rs
@@ -116,6 +116,17 @@ mod test_connection {
     }
 }
 
+/// THe possible start-up mode.
+#[derive(Default, Deserialize, Debug, Serialize)]
+pub enum Mode {
+    #[default]
+    /// Don't do anything special
+    Normal,
+
+    /// Already start the DJ mode on start-up
+    Dj,
+}
+
 /// This is the most recent `mppopmd` configuration struct.
 #[derive(Deserialize, Debug, Serialize)]
 #[serde(default)]
@@ -133,6 +144,9 @@ pub struct Config {
     /// How to connect to mpd
     pub conn: Connection,
 
+    /// The mode to start in
+    pub mode: Mode,
+
     /// The `mpd' root music directory, relative to the host on which *this* daemon is running
     pub local_music_dir: PathBuf,
 
@@ -142,9 +156,6 @@ pub struct Config {
 
     /// The interval, in milliseconds, at which to poll `mpd' for the current state
     pub poll_interval_ms: u64,
-
-    /// Channel to setup for assorted commands-- channel names must satisfy "[-a-zA-Z-9_.:]+"
-    pub commands_chan: String,
 }
 
 impl Default for Config {
@@ -162,7 +173,7 @@ impl Config {
             local_music_dir: [PREFIX, "Music"].iter().collect(),
             played_thresh: 0.6,
             poll_interval_ms: 5000,
-            commands_chan: String::from("unwoundstack.com:commands"),
+            mode: Mode::default(),
         })
     }
 }
@@ -176,111 +187,3 @@ pub fn from_str(text: &str) -> Result<Config> {
     };
     Ok(cfg)
 }
-
-#[cfg(test)]
-mod test {
-    use super::*;
-
-    #[test]
-    #[ignore = "We changed the config format to json"]
-    fn test_from_str() {
-        let cfg = Config::default();
-        assert_eq!(cfg.commands_chan, String::from("unwoundstack.com:commands"));
-
-        assert_eq!(
-            serde_json::to_string(&cfg).unwrap(),
-            format!(
-                r#"((version . "1") (log . "{}/log/mppopmd.log") (conn TCP (host . "localhost") (port . 6600)) (local_music_dir . "{}/Music") (playcount_sticker . "unwoundstack.com:playcount") (lastplayed_sticker . "unwoundstack.com:lastplayed") (played_thresh . 0.6) (poll_interval_ms . 5000) (commands_chan . "unwoundstack.com:commands") (playcount_command . "") (playcount_command_args) (rating_sticker . "unwoundstack.com:rating") (ratings_command . "") (ratings_command_args) (gen_cmds))"#,
-                LOCALSTATEDIR, PREFIX
-            )
-        );
-
-        let cfg: Config = serde_json::from_str(
-            r#"
-((version . "1")
- (log . "/usr/local/var/log/mppopmd.log")
- (conn TCP (host . "localhost") (port . 6600))
- (local_music_dir . "/usr/local/Music")
- (playcount_sticker . "unwoundstack.com:playcount")
- (lastplayed_sticker . "unwoundstack.com:lastplayed")
- (played_thresh . 0.6)
- (poll_interval_ms . 5000)
- (commands_chan . "unwoundstack.com:commands")
- (playcount_command . "")
- (playcount_command_args)
- (rating_sticker . "unwoundstack.com:rating")
- (ratings_command . "")
- (ratings_command_args)
- (gen_cmds))
-"#,
-        )
-        .unwrap();
-        assert_eq!(cfg._version, String::from("1"));
-
-        let cfg: Config = serde_json::from_str(
-            r#"
-((version . "1")
- (log . "/usr/local/var/log/mppopmd.log")
- (conn Local (path . "/home/mgh/var/run/mpd/mpd.sock"))
- (local_music_dir . "/usr/local/Music")
- (playcount_sticker . "unwoundstack.com:playcount")
- (lastplayed_sticker . "unwoundstack.com:lastplayed")
- (played_thresh . 0.6)
- (poll_interval_ms . 5000)
- (commands_chan . "unwoundstack.com:commands")
- (playcount_command . "")
- (playcount_command_args)
- (rating_sticker . "unwoundstack.com:rating")
- (ratings_command . "")
- (ratings_command_args)
- (gen_cmds))
-"#,
-        )
-        .unwrap();
-        assert_eq!(cfg._version, String::from("1"));
-        assert_eq!(
-            cfg.conn,
-            Connection::Local {
-                path: PathBuf::from("/home/mgh/var/run/mpd/mpd.sock")
-            }
-        );
-
-        // Test fallback to "v0" of the config struct
-        let cfg = from_str(r#"
-((log . "/home/mgh/var/log/mppopmd.log")
- (host . "192.168.1.14")
- (port . 6600)
- (local_music_dir . "/space/mp3")
- (playcount_sticker . "unwoundstack.com:playcount")
- (lastplayed_sticker . "unwoundstack.com:lastplayed")
- (played_thresh . 0.6)
- (poll_interval_ms . 5000)
- (playcount_command . "/usr/local/bin/scribbu")
- (playcount_command_args . ("popm" "-v" "-a" "-f" "-o" "sp1ff@pobox.com" "-C" "%playcount" "%full-file"))
- (commands_chan . "unwoundstack.com:commands")
- (rating_sticker . "unwoundstack.com:rating")
- (ratings_command . "/usr/local/bin/scribbu")
- (ratings_command_args . ("popm" "-v" "-a" "-f" "-o" "sp1ff@pobox.com" "-r" "%rating" "%full-file"))
- (gen_cmds .
-	   (((name . "set-genre")
-	     (formal_parameters . (Literal Track))
-	     (default_after . 1)
-	     (cmd . "/usr/local/bin/scribbu")
-	     (args . ("genre" "-a" "-C" "-g" "%1" "%full-file"))
-	     (update . TrackOnly))
-	    ((name . "set-xtag")
-	     (formal_parameters . (Literal Track))
-	     (default_after . 1)
-	     (cmd . "/usr/local/bin/scribbu")
-	     (args . ("xtag" "-A" "-o" "sp1ff@pobox.com" "-T" "%1" "%full-file"))
-	     (update . TrackOnly))
-	    ((name . "merge-xtag")
-	     (formal_parameters . (Literal Track))
-	     (default_after . 1)
-	     (cmd . "/usr/local/bin/scribbu")
-	     (args . ("xtag" "-m" "-o" "sp1ff@pobox.com" "-T" "%1" "%full-file"))
-	     (update . TrackOnly)))))
-"#).unwrap();
-        assert_eq!(cfg.log, PathBuf::from("/home/mgh/var/log/mppopmd.log"));
-    }
-}
diff --git a/pkgs/by-name/mp/mpdpopm/src/dj/algorithms.rs b/pkgs/by-name/mp/mpdpopm/src/dj/algorithms.rs
index 5ddfc7cb..2c3ddad6 100644
--- a/pkgs/by-name/mp/mpdpopm/src/dj/algorithms.rs
+++ b/pkgs/by-name/mp/mpdpopm/src/dj/algorithms.rs
@@ -1,7 +1,10 @@
-use std::collections::HashSet;
+use std::{
+    collections::HashSet,
+    time::{Duration, SystemTime},
+};
 
 use anyhow::{Context, Result};
-use rand::{Rng, distr, seq::SliceRandom};
+use rand::{Rng, distr};
 use tracing::info;
 
 use crate::{clients::Client, storage};
@@ -13,50 +16,66 @@ pub(crate) trait Algorithm {
 /// Generates generic discovery playlist, that fulfills following requirements:
 ///  - Will (eventually) include every not-played song. (So it can be used to rank a library)
 ///  - Returns liked songs more often then not-played or negative songs.
-pub(crate) struct Discovery {
+pub struct Discovery {
     already_done: HashSet<String>,
+    negative_chance: f64,
+    neutral_chance: f64,
+    positive_chance: f64,
 }
 
 impl Algorithm for Discovery {
     async fn next_track(&mut self, client: &mut Client) -> Result<String> {
         macro_rules! take {
-            ($first:expr, $second:expr, $third:expr) => {{
-                $first.pop().map_or_else(
-                    || {
-                        $second.pop().map_or_else(
-                            || {
-                                $third.pop().map_or_else(
-                                    || {
-                                        unreachable!(
-                                            "This means that there are no songs in the libary"
-                                        )
-                                    },
-                                    |val| {
-                                        tracing::info!(
-                                            "Selecting a `{}` track for the next entry in the queue",
-                                            stringify!($third)
-                                        );
-                                        Ok::<_, anyhow::Error>(val)
-                                    },
-                                )
-                            },
-                            |val| {
-                                tracing::info!(
-                                    "Selecting a `{}` track for the next entry in the queue",
-                                    stringify!($second)
-                                );
-                                Ok::<_, anyhow::Error>(val)
-                            },
-                        )
-                    },
-                    |val| {
-                        tracing::info!(
-                            "Selecting a `{}` track for the next entry in the queue",
-                            stringify!($first)
-                        );
-                        Ok::<_, anyhow::Error>(val)
-                    },
-                )
+            ($rng:expr, $from:expr) => {{
+                info!(concat!(
+                    "Trying to select a `",
+                    stringify!($from),
+                    "` track."
+                ));
+
+                assert!(!$from.is_empty());
+
+                let normalized_weights = {
+                    // We normalize the weights here, because negative values don't work for the
+                    // distribution function we use below.
+                    //    "-5" "-3" "1" "6" "19" | +5
+                    // ->  "0"  "2" "6" "11" "24"
+                    let mut weights = $from.iter().map(|(_, w)| *w).collect::<Vec<_>>();
+
+                    weights.sort_by_key(|w| *w);
+
+                    let first = *weights.first().expect(
+                        "the value to exist, because we never run `take!` with an empty vector",
+                    );
+
+                    if first.is_negative() {
+                        weights
+                            .into_iter()
+                            .rev()
+                            .map(|w| w + first.abs())
+                            .collect::<Vec<_>>()
+                    } else {
+                        weights
+                    }
+                };
+
+                let sample = $rng.sample(
+                    distr::weighted::WeightedIndex::new(normalized_weights.iter())
+                        .expect("to be okay, because the weights are normalized"),
+                );
+
+                let output = $from.remove(sample);
+
+                info!(
+                    concat!(
+                        "(",
+                        stringify!($from),
+                        ") Selected `{}` with weight: `{}` (normalized to `{}`)"
+                    ),
+                    output.0, output.1, normalized_weights[sample]
+                );
+
+                Ok::<_, anyhow::Error>(output)
             }};
         }
 
@@ -83,50 +102,74 @@ impl Algorithm for Discovery {
                 base
             };
 
-            let mut positive = vec![];
-            let mut neutral = vec![];
-            let mut negative = vec![];
-
+            let mut sorted_tracks = Vec::with_capacity(tracks.len());
             for track in tracks {
                 let weight = Self::weight_track(client, &track).await?;
 
-                match weight {
-                    1..=i64::MAX => positive.push(track),
-                    0 => neutral.push(track),
-                    i64::MIN..0 => negative.push(track),
-                }
+                sorted_tracks.push((track, weight));
             }
 
-            // Avoid an inherit ordering, that might be returned by the `Client::get_all_songs()` function.
-            positive.shuffle(&mut rng);
-            neutral.shuffle(&mut rng);
-            negative.shuffle(&mut rng);
+            sorted_tracks.sort_by_key(|(_, weight)| *weight);
+
+            let len = sorted_tracks.len() / 3;
+
+            // We split the tracks into three thirds, so that we can also force a pick from e.g.
+            // the lower third (the negative ones).
+            let negative = sorted_tracks.drain(..len).collect::<Vec<_>>();
+            let neutral = sorted_tracks.drain(..len).collect::<Vec<_>>();
+            let positive = sorted_tracks;
+
+            assert_eq!(negative.len(), neutral.len());
 
             (positive, neutral, negative)
         };
 
         let pick = rng.sample(
-            distr::weighted::WeightedIndex::new([0.65, 0.5, 0.2].iter())
-                .expect("to be valid, as hardcoded"),
+            distr::weighted::WeightedIndex::new(
+                [
+                    self.positive_chance,
+                    self.neutral_chance,
+                    self.negative_chance,
+                ]
+                .iter(),
+            )
+            .expect("to be valid, as hardcoded"),
         );
 
         let next = match pick {
-            0 => take!(positive, neutral, negative),
-            1 => take!(neutral, positive, negative),
-            2 => take!(negative, neutral, positive),
+            0 if !positive.is_empty() => take!(rng, positive),
+            1 if !neutral.is_empty() => take!(rng, neutral),
+            2 if !negative.is_empty() => take!(rng, negative),
+            0..=2 => {
+                // We couldn't actually satisfy the request, because we didn't have the required
+                // track. So we just use the first non-empty one.
+                if !positive.is_empty() {
+                    take!(rng, positive)
+                } else if !neutral.is_empty() {
+                    take!(rng, neutral)
+                } else if !negative.is_empty() {
+                    take!(rng, negative)
+                } else {
+                    assert!(positive.is_empty() && neutral.is_empty() && negative.is_empty());
+                    todo!("No songs available to select from, I don't know how to select one.");
+                }
+            }
             _ => unreachable!("These indexes are not possible"),
         }?;
 
-        self.already_done.insert(next.clone());
+        self.already_done.insert(next.0.to_owned());
 
-        Ok(next)
+        Ok(next.0)
     }
 }
 
 impl Discovery {
-    pub(crate) fn new() -> Self {
+    pub(crate) fn new(positive_chance: f64, neutral_chance: f64, negative_chance: f64) -> Self {
         Self {
             already_done: HashSet::new(),
+            positive_chance,
+            neutral_chance,
+            negative_chance,
         }
     }
 
@@ -136,15 +179,50 @@ impl Discovery {
     /// dislikes to a lower number.
     /// Currently, only the rating, skip count and play count are considered. Similarity scores,
     /// fetched from e.g. last.fm should be included in the future.
-    async fn weight_track(client: &mut Client, track: &str) -> Result<i64> {
+    pub async fn weight_track(client: &mut Client, track: &str) -> Result<i64> {
+        let last_played_delta = {
+            let last_played = storage::last_played::get(client, track).await?.unwrap_or(0);
+            let now = SystemTime::now()
+                .duration_since(SystemTime::UNIX_EPOCH)
+                .expect("to be before")
+                .as_secs();
+
+            let played_seconds_ago = now - last_played;
+
+            const HOUR: u64 = Duration::from_hours(1).as_secs();
+            const DAY: u64 = Duration::from_hours(24).as_secs();
+            const MONTH: u64 = Duration::from_hours(24 * 30).as_secs();
+
+            match played_seconds_ago {
+                ..HOUR => {
+                    // it was played in the last hour already
+                    -3
+                }
+                HOUR..DAY => {
+                    // it was not played in the last hour, but in the last day
+                    -2
+                }
+                DAY..MONTH => {
+                    // it was not played in the last day, but in the last month
+                    -1
+                }
+                MONTH.. => {
+                    // it was not played in a month
+                    1
+                }
+            }
+        };
+
         let rating = i32::from(storage::rating::get(client, track).await?.unwrap_or(0));
         let play_count = i32::try_from(storage::play_count::get(client, track).await?.unwrap_or(0))
             .context("`play_count` too big")?;
         let skip_count = i32::try_from(storage::skip_count::get(client, track).await?.unwrap_or(0))
             .context("`skip_count` too big")?;
 
-        let output: f64 =
-            1.0 * f64::from(rating) + 0.3 * f64::from(play_count) + -0.6 * f64::from(skip_count);
+        let output: f64 = 1.0 * f64::from(rating)
+            + 0.3 * f64::from(play_count)
+            + -0.6 * f64::from(skip_count)
+            + 0.65 * f64::from(last_played_delta);
 
         let weight = output.round() as i64;
 
diff --git a/pkgs/by-name/mp/mpdpopm/src/dj/mod.rs b/pkgs/by-name/mp/mpdpopm/src/dj/mod.rs
index a211a571..548ed4f4 100644
--- a/pkgs/by-name/mp/mpdpopm/src/dj/mod.rs
+++ b/pkgs/by-name/mp/mpdpopm/src/dj/mod.rs
@@ -3,7 +3,7 @@ use tracing::info;
 
 use crate::{clients::Client, dj::algorithms::Algorithm};
 
-pub(crate) mod algorithms;
+pub mod algorithms;
 
 pub(crate) struct Dj<A: Algorithm> {
     algo: A,
diff --git a/pkgs/by-name/mp/mpdpopm/src/lib.rs b/pkgs/by-name/mp/mpdpopm/src/lib.rs
index cc2765dc..2394b729 100644
--- a/pkgs/by-name/mp/mpdpopm/src/lib.rs
+++ b/pkgs/by-name/mp/mpdpopm/src/lib.rs
@@ -53,7 +53,7 @@ pub mod filters {
 use crate::{
     clients::{Client, IdleClient, IdleSubSystem},
     config::{Config, Connection},
-    messanges::MessageQueue,
+    messanges::{COMMAND_CHANNEL, MessageQueue},
     playcounts::PlayState,
 };
 
@@ -93,10 +93,10 @@ pub async fn mpdpopm(cfg: Config) -> std::result::Result<(), Error> {
             .context("Failed to connect to TCP idle client")?,
     };
 
-    let mut mqueue = MessageQueue::new();
+    let mut mqueue = MessageQueue::new(cfg.mode);
 
     idle_client
-        .subscribe(&cfg.commands_chan)
+        .subscribe(COMMAND_CHANNEL)
         .await
         .context("Failed to subscribe to idle_client")?;
 
@@ -144,14 +144,14 @@ pub async fn mpdpopm(cfg: Config) -> std::result::Result<(), Error> {
                         Ok(subsys) => {
                             debug!("subsystem {} changed", subsys);
                             if subsys == IdleSubSystem::Player {
-                                state.update(&mut client)
+                                if state.update(&mut client)
                                     .await
-                                    .context("PlayState update failed")?;
-
+                                    .context("PlayState update failed")? {
                                 mqueue
                                     .advance_dj(&mut client)
                                     .await
                                     .context("MessageQueue tick failed")?;
+                                }
                             } else if subsys == IdleSubSystem::Message {
                                 msg_check_needed = true;
                             }
diff --git a/pkgs/by-name/mp/mpdpopm/src/messanges/mod.rs b/pkgs/by-name/mp/mpdpopm/src/messanges/mod.rs
index c5320dd9..7db75672 100644
--- a/pkgs/by-name/mp/mpdpopm/src/messanges/mod.rs
+++ b/pkgs/by-name/mp/mpdpopm/src/messanges/mod.rs
@@ -5,6 +5,7 @@ use tracing::info;
 
 use crate::{
     clients::{Client, IdleClient},
+    config::Mode,
     dj::{Dj, algorithms::Discovery},
 };
 
@@ -26,7 +27,19 @@ enum SubCommand {
 
 #[derive(Subcommand)]
 enum DjCommand {
-    Start {},
+    Start {
+        /// The chance to select a "positive" track
+        #[arg(long)]
+        positive_chance: f64,
+
+        /// The chance to select a "neutral" track
+        #[arg(long)]
+        neutral_chance: f64,
+
+        /// The chance to select a "negative" track
+        #[arg(long)]
+        negative_chance: f64,
+    },
     Stop {},
 }
 
@@ -35,8 +48,17 @@ pub(crate) struct MessageQueue {
 }
 
 impl MessageQueue {
-    pub(crate) fn new() -> Self {
-        Self { dj: None }
+    pub(crate) fn new(mode: Mode) -> Self {
+        match mode {
+            Mode::Normal => Self { dj: None },
+            Mode::Dj => {
+                info!("Dj mode started on launch, as specified in config file");
+
+                Self {
+                    dj: Some(Dj::new(Discovery::new(0.65, 0.5, 0.2))),
+                }
+            }
+        }
     }
 
     pub(crate) async fn advance_dj(&mut self, client: &mut Client) -> Result<()> {
@@ -91,9 +113,17 @@ impl MessageQueue {
 
         match args.command {
             SubCommand::Dj { command } => match command {
-                DjCommand::Start {} => {
+                DjCommand::Start {
+                    positive_chance,
+                    neutral_chance,
+                    negative_chance,
+                } => {
                     info!("Dj started");
-                    self.dj = Some(Dj::new(Discovery::new()));
+                    self.dj = Some(Dj::new(Discovery::new(
+                        positive_chance,
+                        neutral_chance,
+                        negative_chance,
+                    )));
                     self.advance_dj(client).await?;
                 }
                 DjCommand::Stop {} => {
diff --git a/pkgs/by-name/mp/mpdpopm/src/playcounts.rs b/pkgs/by-name/mp/mpdpopm/src/playcounts.rs
index 417b3e7e..8fbee133 100644
--- a/pkgs/by-name/mp/mpdpopm/src/playcounts.rs
+++ b/pkgs/by-name/mp/mpdpopm/src/playcounts.rs
@@ -71,13 +71,16 @@ impl PlayState {
 
     /// Poll the server-- update our status; maybe increment the current track's play count; the
     /// caller must arrange to have this method invoked periodically to keep our state fresh
-    pub async fn update(&mut self, client: &mut Client) -> Result<()> {
+    ///
+    /// Returns whether a song finished between the last call and this one.
+    /// That can be used to add a new song to the queue.
+    pub async fn update(&mut self, client: &mut Client) -> Result<bool> {
         let new_stat = client
             .status()
             .await
             .context("Failed to get client status")?;
 
-        match (&self.last_server_stat, &new_stat) {
+        let previous_song_finished = match (&self.last_server_stat, &new_stat) {
             (PlayerStatus::Play(last), PlayerStatus::Play(curr))
             | (PlayerStatus::Pause(last), PlayerStatus::Play(curr))
             | (PlayerStatus::Play(last), PlayerStatus::Pause(curr))
@@ -93,33 +96,59 @@ impl PlayState {
                     }
 
                     self.have_incr_play_count = false;
+
+                    // We are now playing something else, as such the previous one must have
+                    // finished or was skipped.
+                    true
                 } else if last.elapsed > curr.elapsed
                     && self.have_incr_play_count
                     && curr.elapsed / curr.duration <= 0.1
                 {
                     debug!("Re-play-- resetting PC incremented flag.");
                     self.have_incr_play_count = false;
+
+                    // We are still playing the same song, just skipped at the start again.
+                    // This means that we don't need a new one.
+                    false
+                } else {
+                    // We are still playing the same song, so nothing changed
+                    false
                 }
             }
             (PlayerStatus::Stopped, PlayerStatus::Play(_))
-            | (PlayerStatus::Stopped, PlayerStatus::Pause(_))
-            | (PlayerStatus::Pause(_), PlayerStatus::Stopped)
+            | (PlayerStatus::Stopped, PlayerStatus::Pause(_)) => {
+                self.have_incr_play_count = false;
+
+                // We didn't play anything before and now we play something. This means that we
+                // obviously have something to play and thus don't need to add another song.
+                false
+            }
+            (PlayerStatus::Pause(_), PlayerStatus::Stopped)
             | (PlayerStatus::Play(_), PlayerStatus::Stopped) => {
                 self.have_incr_play_count = false;
+
+                // We played a song before and now we stopped, maybe because we ran out of songs to
+                // play. So we need to add another one.
+                true
+            }
+            (PlayerStatus::Stopped, PlayerStatus::Stopped) => {
+                // We did not play before and we are still not playing, as such nothing really
+                // changed.
+                false
             }
-            (PlayerStatus::Stopped, PlayerStatus::Stopped) => (),
-        }
+        };
 
         match &new_stat {
             PlayerStatus::Play(curr) => {
                 let pct = curr.played_pct();
                 debug!("Updating status: {:.3}% complete.", 100.0 * pct);
+
                 if !self.have_incr_play_count && pct >= self.played_thresh {
                     info!(
-                        "Increment play count for '{}' (songid: {}) at {} played.",
+                        "Increment play count for '{}' (songid: {}) at {:.2}% played.",
                         curr.file.display(),
                         curr.songid,
-                        curr.elapsed / curr.duration
+                        (curr.elapsed / curr.duration) * 100.0
                     );
 
                     let file = curr.file.to_str().ok_or_else(|| {
@@ -150,10 +179,10 @@ impl PlayState {
                         .expect("To exist, as it was skipped");
 
                     info!(
-                        "Marking '{}' (songid: {}) as skipped at {}.",
+                        "Marking '{}' (songid: {}) as skipped at {:.2}%.",
                         last.file.display(),
                         last.songid,
-                        last.elapsed / last.duration
+                        (last.elapsed / last.duration) * 100.0
                     );
 
                     let file = last.file.to_str().ok_or_else(|| {
@@ -168,7 +197,7 @@ impl PlayState {
         };
 
         self.last_server_stat = new_stat;
-        Ok(()) // No need to update the DB
+        Ok(previous_song_finished)
     }
 }
 
@@ -308,6 +337,6 @@ OK
         assert!(check);
 
         ps.update(&mut cli).await.unwrap();
-        ps.update(&mut cli).await.unwrap()
+        ps.update(&mut cli).await.unwrap();
     }
 }