Added a popularity adjuster

This commit is contained in:
Connor Johnstone
2026-03-04 23:03:26 -05:00
parent 98e3367822
commit 8eb6bb950e
2 changed files with 36 additions and 20 deletions

View File

@@ -28,7 +28,7 @@ fn db_path() -> PathBuf {
fn usage(program: &str) -> ! { fn usage(program: &str) -> ! {
eprintln!("Usage:"); eprintln!("Usage:");
eprintln!(" {program} index [-v] [-f] <directory>"); eprintln!(" {program} index [-v] [-f] <directory>");
eprintln!(" {program} build [-v] [-m|-a] [-s|-r] [-n COUNT] [file]"); eprintln!(" {program} build [-v] [-m|-a] [-s|-r] [-n COUNT] [-p LEVEL] [file]");
std::process::exit(1); std::process::exit(1);
} }
@@ -154,8 +154,9 @@ fn cmd_build(args: &[String]) {
std::process::exit(1); std::process::exit(1);
} }
// Parse -n COUNT // Parse -n COUNT and -p LEVEL
let mut count: usize = 20; let mut count: usize = 20;
let mut popularity_bias: u8 = 5;
let mut rest: Vec<&String> = Vec::new(); let mut rest: Vec<&String> = Vec::new();
let mut iter = args.iter().skip(2); let mut iter = args.iter().skip(2);
while let Some(arg) = iter.next() { while let Some(arg) = iter.next() {
@@ -175,13 +176,27 @@ fn cmd_build(args: &[String]) {
std::process::exit(1); std::process::exit(1);
} }
} }
} else if arg == "-p" {
match iter.next() {
Some(val) => match val.parse::<u8>() {
Ok(n) if n <= 10 => popularity_bias = n,
_ => {
eprintln!("Error: -p requires an integer 0-10");
std::process::exit(1);
}
},
None => {
eprintln!("Error: -p requires a value");
std::process::exit(1);
}
}
} else { } else {
rest.push(arg); rest.push(arg);
} }
} }
if rest.len() > 1 { if rest.len() > 1 {
eprintln!("Usage: {} build [-v] [-m|-a] [-s|-r] [-n COUNT] [file]", args[0]); eprintln!("Usage: {} build [-v] [-m|-a] [-s|-r] [-n COUNT] [-p LEVEL] [file]", args[0]);
std::process::exit(1); std::process::exit(1);
} }
@@ -224,9 +239,13 @@ fn cmd_build(args: &[String]) {
} }
}; };
build_playlist(&conn, &artist_mbid, &seed_name, count, verbose, mpd, airsonic, shuffle, random); build_playlist(&conn, &artist_mbid, &seed_name, count, verbose, mpd, airsonic, shuffle, random, popularity_bias);
} }
const POPULARITY_EXPONENTS: [f64; 11] = [
0.0, 0.03, 0.08, 0.15, 0.25, 0.35, 0.55, 0.85, 1.30, 1.80, 2.50,
];
fn build_playlist( fn build_playlist(
conn: &rusqlite::Connection, conn: &rusqlite::Connection,
artist_mbid: &str, artist_mbid: &str,
@@ -237,6 +256,7 @@ fn build_playlist(
airsonic: bool, airsonic: bool,
shuffle: bool, shuffle: bool,
random: bool, random: bool,
popularity_bias: u8,
) { ) {
let similar = match db::get_available_similar_artists(conn, artist_mbid) { let similar = match db::get_available_similar_artists(conn, artist_mbid) {
Ok(a) => a, Ok(a) => a,
@@ -302,13 +322,13 @@ fn build_playlist(
let Some(playcount) = playcount else { continue }; let Some(playcount) = playcount else { continue };
let popularity = if playcount > 0 { let popularity = if playcount > 0 {
(playcount as f64 / max_playcount as f64).powf(0.15) (playcount as f64 / max_playcount as f64).powf(POPULARITY_EXPONENTS[popularity_bias as usize])
} else { } else {
0.0 0.0
}; };
let similarity = (match_score.exp()) / std::f64::consts::E; let similarity = (match_score.exp()) / std::f64::consts::E;
let total = similarity * (1.0 + popularity); let total = similarity * popularity;
playlist.push((total, popularity, similarity, name.clone(), track_path.clone())); playlist.push((total, popularity, similarity, name.clone(), track_path.clone()));
} }
} }
@@ -337,12 +357,10 @@ fn build_playlist(
let tracks: Vec<String> = selected.iter().map(|(_, _, p)| p.clone()).collect(); let tracks: Vec<String> = selected.iter().map(|(_, _, p)| p.clone()).collect();
let local_dir = env::var("LOCAL_MUSIC_DIR").unwrap_or_default();
if mpd { if mpd {
let mpd_dir = env::var("MPD_MUSIC_DIR").unwrap_or_default(); let music_dir = env::var("MPD_MUSIC_DIR").unwrap_or_default();
if local_dir.is_empty() || mpd_dir.is_empty() { if music_dir.is_empty() {
eprintln!("Error: LOCAL_MUSIC_DIR and MPD_MUSIC_DIR must be set"); eprintln!("Error: MPD_MUSIC_DIR not set");
std::process::exit(1); std::process::exit(1);
} }
let mut client = match mpd::MpdClient::connect() { let mut client = match mpd::MpdClient::connect() {
@@ -352,7 +370,7 @@ fn build_playlist(
std::process::exit(1); std::process::exit(1);
} }
}; };
client.queue_playlist(&tracks, &local_dir, &mpd_dir); client.queue_playlist(&tracks, &music_dir);
} else if airsonic { } else if airsonic {
let client = match airsonic::AirsonicClient::new() { let client = match airsonic::AirsonicClient::new() {
Ok(c) => c, Ok(c) => c,

View File

@@ -83,7 +83,7 @@ impl MpdClient {
} }
} }
pub fn queue_playlist(&mut self, tracks: &[String], local_dir: &str, mpd_dir: &str) { pub fn queue_playlist(&mut self, tracks: &[String], music_dir: &str) {
if let Err(e) = self.send_command("clear") { if let Err(e) = self.send_command("clear") {
eprintln!("MPD clear: {e}"); eprintln!("MPD clear: {e}");
return; return;
@@ -92,7 +92,7 @@ impl MpdClient {
let mut failed: Vec<String> = Vec::new(); let mut failed: Vec<String> = Vec::new();
for track in tracks { for track in tracks {
let uri = Self::track_to_uri(track, local_dir, mpd_dir); let uri = Self::track_to_uri(track, music_dir);
let escaped = uri.replace('\\', "\\\\").replace('"', "\\\""); let escaped = uri.replace('\\', "\\\\").replace('"', "\\\"");
if self.send_command(&format!("add \"{escaped}\"")).is_err() { if self.send_command(&format!("add \"{escaped}\"")).is_err() {
failed.push(uri.to_string()); failed.push(uri.to_string());
@@ -117,12 +117,10 @@ impl MpdClient {
} }
} }
fn track_to_uri(track: &str, local_dir: &str, mpd_dir: &str) -> String { fn track_to_uri<'a>(track: &'a str, music_dir: &str) -> &'a str {
let relative = track track
.strip_prefix(local_dir) .strip_prefix(music_dir)
.map(|p| p.trim_start_matches('/')) .map(|p| p.trim_start_matches('/'))
.unwrap_or(track); .unwrap_or(track)
let mpd_base = mpd_dir.trim_end_matches('/');
format!("{mpd_base}/{relative}")
} }
} }