diff --git a/README.md b/README.md index f56525a..47a0fe1 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,12 @@ drift index /path/to/music Scans for tagged files, fetches similar artists and top tracks from Last.fm, and stores everything in a local SQLite database (`~/.local/share/drift/drift.db`). +Stale tracks (files previously indexed but no longer on disk) are automatically removed. + Flags: -- `-v` — print progress +- `-v` — print new artists indexed + summary +- `-vv` — also print each track added/removed +- `-vvv` — also print skipped artists - `-f` — re-index artists that were already indexed ### Build a playlist diff --git a/src/db.rs b/src/db.rs index ede036d..975a18d 100644 --- a/src/db.rs +++ b/src/db.rs @@ -145,6 +145,23 @@ pub fn insert_track( Ok(()) } +pub fn get_tracks_in_directory(conn: &Connection, dir_prefix: &str) -> Result, rusqlite::Error> { + let pattern = format!("{dir_prefix}%"); + let mut stmt = conn.prepare("SELECT path FROM tracks WHERE path LIKE ?1")?; + let rows = stmt.query_map([pattern], |row| row.get(0))?; + rows.collect() +} + +pub fn delete_tracks(conn: &Connection, paths: &[String]) -> Result { + let tx = conn.unchecked_transaction()?; + let mut deleted = 0; + for path in paths { + deleted += tx.execute("DELETE FROM tracks WHERE path = ?1", [path])?; + } + tx.commit()?; + Ok(deleted) +} + pub fn insert_artist_with_similar( conn: &Connection, mbid: &str, diff --git a/src/main.rs b/src/main.rs index 39b6f83..97c8423 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,8 +7,9 @@ mod mpd; mod playlist; mod tui; +use std::collections::HashSet; use std::env; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use clap::{Parser, Subcommand}; use rand::prelude::*; @@ -26,9 +27,9 @@ struct Cli { enum Command { /// Index a music directory Index { - /// Print progress - #[arg(short)] - verbose: bool, + /// Verbosity level (-v, -vv, -vvv) + #[arg(short, action = clap::ArgAction::Count)] + verbose: u8, /// Re-index already indexed artists #[arg(short)] force: bool, @@ -109,7 +110,7 @@ fn main() { } } -fn cmd_index(verbose: bool, force: bool, directory: &str) { +fn cmd_index(verbose: u8, force: bool, directory: &str) { dotenvy::dotenv().ok(); let api_key = env::var("LASTFM_API_KEY").unwrap_or_default(); @@ -120,9 +121,30 @@ fn cmd_index(verbose: bool, force: bool, directory: &str) { let conn = db::open(&db_path()).expect("failed to open database"); let lastfm = lastfm::LastfmClient::new(api_key); - let dir = Path::new(directory); - for path in filesystem::walk_music_files(dir) { + let dir = std::fs::canonicalize(directory).unwrap_or_else(|e| { + eprintln!("Error: cannot resolve directory {directory}: {e}"); + std::process::exit(1); + }); + + let dir_prefix = format!("{}/", dir.display()); + + let mut stale_paths: HashSet = match db::get_tracks_in_directory(&conn, &dir_prefix) { + Ok(paths) => paths.into_iter().collect(), + Err(e) => { + eprintln!("DB error loading existing tracks: {e}"); + HashSet::new() + } + }; + + let mut artists_indexed: usize = 0; + let mut artists_skipped: usize = 0; + let mut tracks_added: usize = 0; + + for path in filesystem::walk_music_files(&dir) { + let path_str = path.to_string_lossy().into_owned(); + stale_paths.remove(&path_str); + let tags = match metadata::read_tags(&path, &[ metadata::Tag::ArtistMbid, metadata::Tag::TrackMbid, @@ -152,7 +174,7 @@ fn cmd_index(verbose: bool, force: bool, directory: &str) { let display_name = artist_name.as_deref().unwrap_or(&artist_mbid); if !already_indexed || force { - if verbose { + if verbose >= 1 { println!("Indexing {display_name}..."); } @@ -184,14 +206,50 @@ fn cmd_index(verbose: bool, force: bool, directory: &str) { eprintln!("Last.fm top tracks error for {display_name}: {e}"); } } - } else if verbose { - println!("Skipping {display_name} (already indexed)"); + + artists_indexed += 1; + } else { + if verbose >= 3 { + println!("Skipping {display_name} (already indexed)"); + } + artists_skipped += 1; } - let path_str = path.to_string_lossy(); if let Err(e) = db::insert_track(&conn, &path_str, &artist_mbid, recording_mbid.as_deref(), track_title.as_deref()) { eprintln!("DB error inserting track {}: {e}", path.display()); + continue; } + tracks_added += 1; + + if verbose >= 2 { + let label = track_title.as_deref().unwrap_or(&path_str); + println!(" + {label}"); + } + } + + // Sweep stale tracks + let stale: Vec = stale_paths.into_iter().collect(); + let tracks_stale = if !stale.is_empty() { + if verbose >= 2 { + for p in &stale { + println!(" - {p}"); + } + } + match db::delete_tracks(&conn, &stale) { + Ok(n) => n, + Err(e) => { + eprintln!("DB error removing stale tracks: {e}"); + 0 + } + } + } else { + 0 + }; + + if verbose >= 1 { + println!( + "Done: {artists_indexed} artists indexed, {artists_skipped} skipped, {tracks_added} tracks added, {tracks_stale} stale tracks removed" + ); } }