diff options
Diffstat (limited to '')
| -rw-r--r-- | pkgs/by-name/mp/mpdpopm/src/dj/algorithms.rs | 206 | ||||
| -rw-r--r-- | pkgs/by-name/mp/mpdpopm/src/dj/mod.rs | 2 |
2 files changed, 143 insertions, 65 deletions
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, |
