aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/mp/mpdpopm/src/dj
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/dj/algorithms.rs139
-rw-r--r--pkgs/by-name/mp/mpdpopm/src/dj/mod.rs28
2 files changed, 167 insertions, 0 deletions
diff --git a/pkgs/by-name/mp/mpdpopm/src/dj/algorithms.rs b/pkgs/by-name/mp/mpdpopm/src/dj/algorithms.rs
new file mode 100644
index 00000000..fcb05817
--- /dev/null
+++ b/pkgs/by-name/mp/mpdpopm/src/dj/algorithms.rs
@@ -0,0 +1,139 @@
+use std::collections::HashSet;
+
+use anyhow::{Context, Result};
+use rand::{Rng, distr};
+use tracing::info;
+
+use crate::{clients::Client, storage};
+
+pub(crate) trait Algorithm {
+ async fn next_track(&mut self, client: &mut Client) -> Result<String>;
+}
+
+/// 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 {
+ already_done: HashSet<String>,
+}
+
+impl Algorithm for Discovery {
+ async fn next_track(&mut self, client: &mut Client) -> Result<String> {
+ macro_rules! take {
+ ($first:expr, $second:expr, $third:expr) => {{
+ tracing::info!(
+ "Selecting a `{}` track for the next entry in the queue",
+ stringify!($first)
+ );
+
+ $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"
+ )
+ },
+ Ok::<_, anyhow::Error>,
+ )
+ },
+ Ok::<_, anyhow::Error>,
+ )
+ },
+ Ok::<_, anyhow::Error>,
+ )
+ }};
+ }
+
+ let (mut positive, mut neutral, mut negative) = {
+ let tracks = {
+ let mut base = client
+ .get_all_songs()
+ .await?
+ .into_iter()
+ .filter(|song| !self.already_done.contains(song))
+ .collect::<Vec<_>>();
+
+ if base.is_empty() {
+ // We could either have no tracks in the library,
+ // or we actually already listed to everything.
+ self.already_done = HashSet::new();
+
+ info!("Resetting already done songs, as we have no more to choose from");
+
+ base = client.get_all_songs().await?;
+ }
+
+ base
+ };
+
+ let mut positive = vec![];
+ let mut neutral = vec![];
+ let mut negative = vec![];
+
+ 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),
+ }
+ }
+
+ (positive, neutral, negative)
+ };
+
+ let pick = {
+ let mut rng = rand::rng();
+ rng.sample(
+ distr::weighted::WeightedIndex::new([0.65, 0.5, 0.2].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),
+ _ => unreachable!("These indexes are not possible"),
+ }?;
+
+ self.already_done.insert(next.clone());
+
+ Ok(next)
+ }
+}
+
+impl Discovery {
+ pub(crate) fn new() -> Self {
+ Self {
+ already_done: HashSet::new(),
+ }
+ }
+
+ /// Calculate a recommendation score for a track.
+ ///
+ /// The algorithm maps tracks, that the user likes to a high score and songs that the user
+ /// 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> {
+ 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 weight = output.round() as i64;
+
+ // info!("`{track}`: {weight}");
+
+ Ok(weight)
+ }
+}
diff --git a/pkgs/by-name/mp/mpdpopm/src/dj/mod.rs b/pkgs/by-name/mp/mpdpopm/src/dj/mod.rs
new file mode 100644
index 00000000..a211a571
--- /dev/null
+++ b/pkgs/by-name/mp/mpdpopm/src/dj/mod.rs
@@ -0,0 +1,28 @@
+use anyhow::Result;
+use tracing::info;
+
+use crate::{clients::Client, dj::algorithms::Algorithm};
+
+pub(crate) mod algorithms;
+
+pub(crate) struct Dj<A: Algorithm> {
+ algo: A,
+}
+
+impl<A: Algorithm> Dj<A> {
+ pub(crate) fn new(algo: A) -> Self {
+ Self { algo }
+ }
+
+ /// Add the next track to the playlist.
+ ///
+ /// This should be called after the previous track is finished, to avoid unbounded growth.
+ pub(crate) async fn add_track(&mut self, client: &mut Client) -> Result<()> {
+ let next = self.algo.next_track(client).await?;
+
+ info!("Adding `{next}`, due to active dj mode");
+ client.add(&next).await?;
+
+ Ok(())
+ }
+}