Added a popularity adjuster
This commit is contained in:
42
src/main.rs
42
src/main.rs
@@ -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,
|
||||||
|
|||||||
14
src/mpd.rs
14
src/mpd.rs
@@ -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}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user