Clippy fixes
This commit is contained in:
203
src/main.rs
203
src/main.rs
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user