This commit is contained in:
Connor Johnstone
2026-03-02 23:30:54 -05:00
parent 59d0674d77
commit 34977ea54b
2 changed files with 200 additions and 7 deletions

View File

@@ -2,6 +2,7 @@ mod db;
mod filesystem;
mod lastfm;
mod metadata;
mod mpd;
mod tui;
use std::collections::HashMap;
@@ -14,7 +15,7 @@ use rand::prelude::*;
fn usage(program: &str) -> ! {
eprintln!("Usage:");
eprintln!(" {program} index [-v] <directory>");
eprintln!(" {program} build [-v] [-n COUNT] [file]");
eprintln!(" {program} build [-v] [-m] [-s|-r] [-n COUNT] [file]");
std::process::exit(1);
}
@@ -111,13 +112,21 @@ fn cmd_index(args: &[String]) {
fn cmd_build(args: &[String]) {
let verbose = args.iter().any(|a| a == "-v");
let mpd = args.iter().any(|a| a == "-m");
let shuffle = args.iter().any(|a| a == "-s");
let random = args.iter().any(|a| a == "-r");
if shuffle && random {
eprintln!("Error: -s and -r are mutually exclusive");
std::process::exit(1);
}
// Parse -n COUNT
let mut count: usize = 20;
let mut rest: Vec<&String> = Vec::new();
let mut iter = args.iter().skip(2);
while let Some(arg) = iter.next() {
if arg == "-v" {
if arg == "-v" || arg == "-m" || arg == "-s" || arg == "-r" {
continue;
} else if arg == "-n" {
match iter.next() {
@@ -139,7 +148,7 @@ fn cmd_build(args: &[String]) {
}
if rest.len() > 1 {
eprintln!("Usage: {} build [-v] [-n COUNT] [file]", args[0]);
eprintln!("Usage: {} build [-v] [-m] [-s|-r] [-n COUNT] [file]", args[0]);
std::process::exit(1);
}
@@ -189,7 +198,7 @@ fn cmd_build(args: &[String]) {
}
};
build_playlist(&conn, &lastfm, &artist_mbid, &seed_name, count, verbose);
build_playlist(&conn, &lastfm, &artist_mbid, &seed_name, count, verbose, mpd, shuffle, random);
}
fn build_playlist(
@@ -199,6 +208,9 @@ fn build_playlist(
seed_name: &str,
count: usize,
verbose: bool,
mpd: bool,
shuffle: bool,
random: bool,
) {
let similar = match db::get_available_similar_artists(conn, artist_mbid) {
Ok(a) => a,
@@ -290,10 +302,34 @@ fn build_playlist(
.map(|(total, _, _, artist, path)| (total, artist, path))
.collect();
let selected = generate_playlist(&candidates, count);
let mut selected = generate_playlist(&candidates, count);
for (_, _, track_path) in &selected {
println!("{track_path}");
if random {
selected.shuffle(&mut rand::rng());
} else if shuffle {
selected = interleave_artists(selected);
}
let tracks: Vec<String> = selected.iter().map(|(_, _, p)| p.clone()).collect();
if mpd {
let music_dir = env::var("MPD_MUSIC_DIR").unwrap_or_default();
if music_dir.is_empty() {
eprintln!("Error: MPD_MUSIC_DIR not set");
std::process::exit(1);
}
let mut client = match mpd::MpdClient::connect() {
Ok(c) => c,
Err(e) => {
eprintln!("MPD error: {e}");
std::process::exit(1);
}
};
client.queue_playlist(&tracks, &music_dir);
} else {
for track in &tracks {
println!("{track}");
}
}
}
@@ -360,3 +396,49 @@ fn generate_playlist(
result
}
/// Reorder tracks so that artists are evenly spread out.
/// Greedily picks from the artist with the most remaining tracks,
/// avoiding back-to-back repeats when possible.
fn interleave_artists(tracks: Vec<(f64, String, String)>) -> Vec<(f64, String, String)> {
use std::collections::BTreeMap;
let mut rng = rand::rng();
// Group by artist, shuffling within each group
let mut by_artist: BTreeMap<String, Vec<(f64, String, String)>> = BTreeMap::new();
for track in tracks {
by_artist.entry(track.1.clone()).or_default().push(track);
}
for group in by_artist.values_mut() {
group.shuffle(&mut rng);
}
let mut result = Vec::new();
let mut last_artist: Option<String> = None;
while !by_artist.is_empty() {
// Sort artists by remaining count (descending), break ties randomly
let mut artists: Vec<String> = by_artist.keys().cloned().collect();
artists.sort_by(|a, b| by_artist[b].len().cmp(&by_artist[a].len()));
// Pick the first artist that isn't the same as the last one
let pick = artists
.iter()
.find(|a| last_artist.as_ref() != Some(a))
.or(artists.first())
.cloned()
.unwrap();
let group = by_artist.get_mut(&pick).unwrap();
let track = group.pop().unwrap();
if group.is_empty() {
by_artist.remove(&pick);
}
last_artist = Some(pick);
result.push(track);
}
result
}