Clippy fixes

This commit is contained in:
Connor Johnstone
2026-03-04 23:13:40 -05:00
parent 8eb6bb950e
commit d59235707d
6 changed files with 236 additions and 157 deletions

View File

@@ -10,9 +10,76 @@ use std::collections::HashMap;
use std::env;
use std::path::{Path, PathBuf};
use clap::{Parser, Subcommand};
use rand::distr::weighted::WeightedIndex;
use rand::prelude::*;
#[derive(Parser)]
#[command(name = "playlists")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Index a music directory
Index {
/// Print progress
#[arg(short)]
verbose: bool,
/// Re-index already indexed artists
#[arg(short)]
force: bool,
/// Music directory to index
directory: String,
},
/// Build a playlist from similar artists
Build {
/// Print track scores
#[arg(short)]
verbose: bool,
/// Queue in MPD
#[arg(short, conflicts_with = "airsonic")]
mpd: bool,
/// Create Airsonic playlist
#[arg(short, conflicts_with = "mpd")]
airsonic: bool,
/// Interleave artists evenly
#[arg(short, conflicts_with = "random")]
shuffle: bool,
/// Fully randomize order
#[arg(short, conflicts_with = "shuffle")]
random: bool,
/// Number of tracks
#[arg(short = 'n', default_value_t = 20, value_parser = parse_positive_usize)]
count: usize,
/// Popularity bias (0=no preference, 10=heavy popular bias)
#[arg(short, default_value_t = 5, value_parser = clap::value_parser!(u8).range(0..=10))]
popularity: u8,
/// Seed file (or pick interactively)
file: Option<String>,
},
}
struct BuildOptions {
verbose: bool,
mpd: bool,
airsonic: bool,
shuffle: bool,
random: bool,
count: usize,
popularity_bias: u8,
}
fn parse_positive_usize(s: &str) -> Result<usize, String> {
let n: usize = s.parse().map_err(|e| format!("{e}"))?;
if n == 0 {
return Err("value must be greater than 0".to_string());
}
Ok(n)
}
fn db_path() -> PathBuf {
let data_dir = env::var("XDG_DATA_HOME")
.map(PathBuf::from)
@@ -25,37 +92,23 @@ fn db_path() -> PathBuf {
dir.join("playlists.db")
}
fn usage(program: &str) -> ! {
eprintln!("Usage:");
eprintln!(" {program} index [-v] [-f] <directory>");
eprintln!(" {program} build [-v] [-m|-a] [-s|-r] [-n COUNT] [-p LEVEL] [file]");
std::process::exit(1);
}
fn main() {
let args: Vec<String> = env::args().collect();
let cli = Cli::parse();
if args.len() < 2 {
usage(&args[0]);
}
match args[1].as_str() {
"index" => cmd_index(&args),
"build" => cmd_build(&args),
_ => usage(&args[0]),
match cli.command {
Command::Index { verbose, force, directory } => {
cmd_index(verbose, force, &directory);
}
Command::Build { verbose, mpd, airsonic, shuffle, random, count, popularity, file } => {
let opts = BuildOptions {
verbose, mpd, airsonic, shuffle, random, count, popularity_bias: popularity,
};
cmd_build(opts, file.as_deref());
}
}
}
fn cmd_index(args: &[String]) {
let verbose = args.iter().any(|a| a == "-v");
let force = args.iter().any(|a| a == "-f");
let rest: Vec<&String> = args.iter().skip(2).filter(|a| *a != "-v" && *a != "-f").collect();
if rest.len() != 1 {
eprintln!("Usage: {} index [-v] [-f] <directory>", args[0]);
std::process::exit(1);
}
fn cmd_index(verbose: bool, force: bool, directory: &str) {
dotenvy::dotenv().ok();
let api_key = env::var("LASTFM_API_KEY").unwrap_or_default();
@@ -66,7 +119,7 @@ fn cmd_index(args: &[String]) {
let conn = db::open(&db_path()).expect("failed to open database");
let lastfm = lastfm::LastfmClient::new(api_key);
let dir = Path::new(rest[0].as_str());
let dir = Path::new(directory);
for path in filesystem::walk_music_files(dir) {
let artist_mbid = match metadata::read_artist_mbid(&path) {
@@ -137,74 +190,12 @@ 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 airsonic = args.iter().any(|a| a == "-a");
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);
}
if mpd && airsonic {
eprintln!("Error: -m and -a are mutually exclusive");
std::process::exit(1);
}
// Parse -n COUNT and -p LEVEL
let mut count: usize = 20;
let mut popularity_bias: u8 = 5;
let mut rest: Vec<&String> = Vec::new();
let mut iter = args.iter().skip(2);
while let Some(arg) = iter.next() {
if arg == "-v" || arg == "-m" || arg == "-a" || arg == "-s" || arg == "-r" {
continue;
} else if arg == "-n" {
match iter.next() {
Some(val) => match val.parse::<usize>() {
Ok(n) if n > 0 => count = n,
_ => {
eprintln!("Error: -n requires a positive integer");
std::process::exit(1);
}
},
None => {
eprintln!("Error: -n requires a value");
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 {
rest.push(arg);
}
}
if rest.len() > 1 {
eprintln!("Usage: {} build [-v] [-m|-a] [-s|-r] [-n COUNT] [-p LEVEL] [file]", args[0]);
std::process::exit(1);
}
fn cmd_build(opts: BuildOptions, file: Option<&str>) {
dotenvy::dotenv().ok();
let conn = db::open(&db_path()).expect("failed to open database");
let (artist_mbid, seed_name) = if let Some(file_arg) = rest.first() {
let path = Path::new(file_arg.as_str());
let (artist_mbid, seed_name) = if let Some(file_arg) = file {
let path = Path::new(file_arg);
let mbid = match metadata::read_artist_mbid(path) {
Ok(Some(mbid)) => mbid,
Ok(None) => {
@@ -239,7 +230,7 @@ fn cmd_build(args: &[String]) {
}
};
build_playlist(&conn, &artist_mbid, &seed_name, count, verbose, mpd, airsonic, shuffle, random, popularity_bias);
build_playlist(&conn, &artist_mbid, &seed_name, &opts);
}
const POPULARITY_EXPONENTS: [f64; 11] = [
@@ -250,13 +241,7 @@ fn build_playlist(
conn: &rusqlite::Connection,
artist_mbid: &str,
seed_name: &str,
count: usize,
verbose: bool,
mpd: bool,
airsonic: bool,
shuffle: bool,
random: bool,
popularity_bias: u8,
opts: &BuildOptions,
) {
let similar = match db::get_available_similar_artists(conn, artist_mbid) {
Ok(a) => a,
@@ -322,7 +307,7 @@ fn build_playlist(
let Some(playcount) = playcount else { continue };
let popularity = if playcount > 0 {
(playcount as f64 / max_playcount as f64).powf(POPULARITY_EXPONENTS[popularity_bias as usize])
(playcount as f64 / max_playcount as f64).powf(POPULARITY_EXPONENTS[opts.popularity_bias as usize])
} else {
0.0
};
@@ -333,7 +318,7 @@ fn build_playlist(
}
}
if verbose {
if opts.verbose {
let mut sorted = playlist.clone();
sorted.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
for (total, popularity, similarity, artist, track_path) in &sorted {
@@ -347,17 +332,17 @@ fn build_playlist(
.map(|(total, _, _, artist, path)| (total, artist, path))
.collect();
let mut selected = generate_playlist(&candidates, count, seed_name);
let mut selected = generate_playlist(&candidates, opts.count, seed_name);
if random {
if opts.random {
selected.shuffle(&mut rand::rng());
} else if shuffle {
} else if opts.shuffle {
selected = interleave_artists(selected);
}
let tracks: Vec<String> = selected.iter().map(|(_, _, p)| p.clone()).collect();
if mpd {
if opts.mpd {
let music_dir = env::var("MPD_MUSIC_DIR").unwrap_or_default();
if music_dir.is_empty() {
eprintln!("Error: MPD_MUSIC_DIR not set");
@@ -371,7 +356,7 @@ fn build_playlist(
}
};
client.queue_playlist(&tracks, &music_dir);
} else if airsonic {
} else if opts.airsonic {
let client = match airsonic::AirsonicClient::new() {
Ok(c) => c,
Err(e) => {
@@ -379,7 +364,7 @@ fn build_playlist(
std::process::exit(1);
}
};
if let Err(e) = client.create_playlist(seed_name, &tracks, conn, verbose) {
if let Err(e) = client.create_playlist(seed_name, &tracks, conn, opts.verbose) {
eprintln!("Airsonic error: {e}");
std::process::exit(1);
}
@@ -422,7 +407,7 @@ fn generate_playlist(
5 => 4,
_ => 5,
};
let artist_cap = ((n + divisor - 1) / divisor).max(1);
let artist_cap = n.div_ceil(divisor).max(1);
while result.len() < n && !pool.is_empty() {
let seed_count = *artist_counts.get(seed_name).unwrap_or(&0);