Added exact artist matching

This commit is contained in:
Connor Johnstone
2026-03-05 11:31:53 -05:00
parent 70aedb49f2
commit 2ffdce4fbc
8 changed files with 152 additions and 74 deletions

View File

@@ -16,7 +16,7 @@ use rand::prelude::*;
use playlist::Candidate;
#[derive(Parser)]
#[command(name = "playlists")]
#[command(name = "drift")]
struct Cli {
#[command(subcommand)]
command: Command,
@@ -58,8 +58,8 @@ enum Command {
/// 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>,
/// Artist name to seed (or pick interactively)
artist: Option<String>,
},
}
@@ -88,9 +88,9 @@ fn db_path() -> PathBuf {
let home = env::var("HOME").expect("HOME not set");
PathBuf::from(home).join(".local/share")
});
let dir = data_dir.join("playlists");
let dir = data_dir.join("drift");
std::fs::create_dir_all(&dir).expect("failed to create data directory");
dir.join("playlists.db")
dir.join("drift.db")
}
fn main() {
@@ -100,11 +100,11 @@ fn main() {
Command::Index { verbose, force, directory } => {
cmd_index(verbose, force, &directory);
}
Command::Build { verbose, mpd, airsonic, shuffle, random, count, popularity, file } => {
Command::Build { verbose, mpd, airsonic, shuffle, random, count, popularity, artist } => {
let opts = BuildOptions {
verbose, mpd, airsonic, shuffle, random, count, popularity_bias: popularity,
};
cmd_build(opts, file.as_deref());
cmd_build(opts, artist.as_deref());
}
}
}
@@ -195,40 +195,61 @@ fn cmd_index(verbose: bool, force: bool, directory: &str) {
}
}
fn cmd_build(opts: BuildOptions, file: Option<&str>) {
fn resolve_artist(artists: &[(String, String)], query: &str) -> Option<(String, String)> {
let q = query.to_lowercase();
// Tier 1: exact case-insensitive match
for (mbid, name) in artists {
if name.to_lowercase() == q {
return Some((mbid.clone(), name.clone()));
}
}
// Tier 2: contains case-insensitive
for (mbid, name) in artists {
if name.to_lowercase().contains(&q) {
return Some((mbid.clone(), name.clone()));
}
}
// Tier 3: subsequence fuzzy match
for (mbid, name) in artists {
if tui::fuzzy_match(&q, name) {
return Some((mbid.clone(), name.clone()));
}
}
None
}
fn cmd_build(opts: BuildOptions, artist: 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) = file {
let path = Path::new(file_arg);
let mbid = match metadata::read_artist_mbid(path) {
Ok(Some(mbid)) => mbid,
Ok(None) => {
eprintln!("{}: no artist MBID found", path.display());
std::process::exit(1);
}
Err(e) => {
eprintln!("{}: could not read artist MBID: {e}", path.display());
std::process::exit(1);
}
};
let name = metadata::read_artist_name(path)
.ok()
.flatten()
.unwrap_or_else(|| mbid.clone());
(mbid, name)
} else {
let artists = match db::get_all_artists(&conn) {
Ok(a) => a,
Err(e) => {
eprintln!("DB error: {e}");
std::process::exit(1);
}
};
if artists.is_empty() {
eprintln!("No artists in database. Run 'index' first.");
let artists = match db::get_all_artists(&conn) {
Ok(a) => a,
Err(e) => {
eprintln!("DB error: {e}");
std::process::exit(1);
}
};
if artists.is_empty() {
eprintln!("No artists in database. Run 'index' first.");
std::process::exit(1);
}
let (artist_mbid, seed_name) = if let Some(query) = artist {
match resolve_artist(&artists, query) {
Some((mbid, name)) => {
eprintln!("Matched: {name}");
(mbid, name)
}
None => {
eprintln!("No artist matching \"{query}\"");
std::process::exit(1);
}
}
} else {
match tui::run_artist_picker(&artists) {
Some(selection) => selection,
None => std::process::exit(0),