Clippy fixes
This commit is contained in:
127
Cargo.lock
generated
127
Cargo.lock
generated
@@ -8,6 +8,56 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
@@ -48,6 +98,52 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
@@ -179,6 +275,12 @@ dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
@@ -195,6 +297,12 @@ version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
@@ -308,6 +416,12 @@ version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -353,6 +467,7 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
name = "playlists"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"crossterm",
|
||||
"dotenvy",
|
||||
"lofty",
|
||||
@@ -619,6 +734,12 @@ version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -689,6 +810,12 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
|
||||
@@ -14,3 +14,4 @@ rand = "0.9"
|
||||
walkdir = "2.5"
|
||||
crossterm = "0.28"
|
||||
urlencoding = "2.1.3"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
10
src/db.rs
10
src/db.rs
@@ -2,6 +2,12 @@ use rusqlite::Connection;
|
||||
|
||||
use crate::lastfm::{SimilarArtist, TopTrack};
|
||||
|
||||
/// A local track: (path, recording_mbid, title).
|
||||
pub type LocalTrack = (String, Option<String>, Option<String>);
|
||||
|
||||
/// Track metadata: (recording_mbid, title).
|
||||
pub type TrackMetadata = (Option<String>, Option<String>);
|
||||
|
||||
pub fn open(path: &std::path::Path) -> Result<Connection, rusqlite::Error> {
|
||||
let conn = Connection::open(path)?;
|
||||
conn.execute_batch(
|
||||
@@ -62,7 +68,7 @@ pub fn get_available_similar_artists(
|
||||
pub fn get_local_tracks_for_artist(
|
||||
conn: &Connection,
|
||||
artist_mbid: &str,
|
||||
) -> Result<Vec<(String, Option<String>, Option<String>)>, rusqlite::Error> {
|
||||
) -> Result<Vec<LocalTrack>, rusqlite::Error> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT path, recording_mbid, title FROM tracks WHERE artist_mbid = ?1",
|
||||
)?;
|
||||
@@ -114,7 +120,7 @@ pub fn get_top_tracks_by_name(
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
pub fn get_track_metadata(conn: &Connection, path: &str) -> Result<Option<(Option<String>, Option<String>)>, rusqlite::Error> {
|
||||
pub fn get_track_metadata(conn: &Connection, path: &str) -> Result<Option<TrackMetadata>, rusqlite::Error> {
|
||||
conn.query_row(
|
||||
"SELECT recording_mbid, title FROM tracks WHERE path = ?1",
|
||||
[path],
|
||||
|
||||
@@ -16,15 +16,14 @@ pub struct TopTrack {
|
||||
pub name: String,
|
||||
pub mbid: Option<String>,
|
||||
pub playcount: u64,
|
||||
pub listeners: u64,
|
||||
}
|
||||
|
||||
// Last.fm returns {"error": N, "message": "..."} on failure
|
||||
// Last.fm returns {"error": N, "message": "..."} on failure.
|
||||
// Only used to detect error responses via serde — fields aren't read directly.
|
||||
#[derive(Deserialize)]
|
||||
struct ApiError {
|
||||
#[allow(dead_code)]
|
||||
error: u32,
|
||||
message: String,
|
||||
}
|
||||
|
||||
// Deserialization structs for the Last.fm API responses
|
||||
@@ -62,7 +61,6 @@ struct TrackEntry {
|
||||
name: String,
|
||||
mbid: Option<String>,
|
||||
playcount: String,
|
||||
listeners: String,
|
||||
}
|
||||
|
||||
impl LastfmClient {
|
||||
@@ -87,14 +85,9 @@ impl LastfmClient {
|
||||
extra_params: &str,
|
||||
) -> Result<Option<String>, Box<dyn std::error::Error>> {
|
||||
if let Some(name) = artist_name {
|
||||
let name = name.replace('\u{2010}', "-")
|
||||
.replace('\u{2011}', "-")
|
||||
.replace('\u{2012}', "-")
|
||||
.replace('\u{2013}', "-")
|
||||
.replace('\u{2014}', "-")
|
||||
.replace('\u{2015}', "-")
|
||||
.replace('\u{2018}', "'")
|
||||
.replace('\u{2019}', "'");
|
||||
let name = name
|
||||
.replace(['\u{2010}', '\u{2011}', '\u{2012}', '\u{2013}', '\u{2014}', '\u{2015}'], "-")
|
||||
.replace(['\u{2018}', '\u{2019}'], "'");
|
||||
let encoded = urlencoding::encode(&name);
|
||||
let url = format!(
|
||||
"{}?method={}&artist={}&api_key={}{}&format=json",
|
||||
@@ -163,7 +156,6 @@ impl LastfmClient {
|
||||
name: t.name,
|
||||
mbid: t.mbid.filter(|s| !s.is_empty()),
|
||||
playcount: t.playcount.parse().unwrap_or(0),
|
||||
listeners: t.listeners.parse().unwrap_or(0),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -1,39 +1,7 @@
|
||||
use std::path::Path;
|
||||
|
||||
use lofty::file::TaggedFileExt;
|
||||
use lofty::tag::{ItemKey, ItemValue};
|
||||
|
||||
/// A single key-value metadata item from a tag.
|
||||
pub struct TagEntry {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// Read all metadata items from a music file.
|
||||
/// Returns `None` if no tags are present, otherwise a list of all tag entries.
|
||||
pub fn read_all_metadata(path: &Path) -> Result<Option<Vec<TagEntry>>, lofty::error::LoftyError> {
|
||||
let tagged_file = lofty::read_from_path(path)?;
|
||||
|
||||
let Some(tag) = tagged_file.primary_tag().or_else(|| tagged_file.first_tag()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let entries = tag
|
||||
.items()
|
||||
.filter_map(|item| {
|
||||
let value = match item.value() {
|
||||
ItemValue::Text(t) | ItemValue::Locator(t) => t.clone(),
|
||||
ItemValue::Binary(b) => format!("<{} bytes>", b.len()),
|
||||
};
|
||||
Some(TagEntry {
|
||||
key: format!("{:?}", item.key()),
|
||||
value,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Some(entries))
|
||||
}
|
||||
use lofty::tag::ItemKey;
|
||||
|
||||
/// Extract the artist name from a music file.
|
||||
pub fn read_artist_name(path: &Path) -> Result<Option<String>, lofty::error::LoftyError> {
|
||||
|
||||
Reference in New Issue
Block a user