virittaa

Log | Files | Refs | README | LICENSE

commit 1805b3d5f73e1c839b2de8b9855c0a1c2ebafb4b
parent 1a2fc99e702712236695b83983e00b20337a5ecb
Author: mtmn <miro@haravara.org>
Date:   Thu, 14 May 2026 12:32:32 +0200

sled: add mixxx import; limit default to 1000

Diffstat:
M.gitignore | 1+
MCargo.lock | 86++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
MCargo.toml | 1+
MREADME.md | 39++++++++++++++++++++++++---------------
Msrc/db.rs | 54++++++++++++++++++++++++++++++++----------------------
Msrc/file_io.rs | 11+++++------
Asrc/import.rs | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main.rs | 63++++++++++++++++++++++++++++++++++++++++++++++-----------------
8 files changed, 267 insertions(+), 61 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1 +1,2 @@ target +result diff --git a/Cargo.lock b/Cargo.lock @@ -487,6 +487,18 @@ dependencies = [ ] [[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] name = "ffmpeg-next" version = "8.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -528,6 +540,12 @@ dependencies = [ ] [[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] name = "fs2" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -612,11 +630,29 @@ dependencies = [ [[package]] name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] +name = "hashlink" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -668,7 +704,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", ] [[package]] @@ -787,6 +823,16 @@ dependencies = [ ] [[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1257,6 +1303,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] +name = "rsqlite-vfs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +dependencies = [ + "hashbrown 0.16.1", + "thiserror", +] + +[[package]] +name = "rusqlite" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" +dependencies = [ + "bitflags 2.11.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + +[[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1406,6 +1477,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] +name = "sqlite-wasm-rs" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b2c760607300407ddeaee518acf28c795661b7108c75421303dbefb237d3a36" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] name = "strength_reduce" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1530,6 +1613,7 @@ dependencies = [ "ndarray 0.17.2", "rayon", "regex", + "rusqlite", "serde", "serde_json", "sled", diff --git a/Cargo.toml b/Cargo.toml @@ -19,3 +19,4 @@ serde_json = "1.0" sled = "0.34" dirs = "6" regex = "1" +rusqlite = "0.39" diff --git a/README.md b/README.md @@ -8,6 +8,7 @@ A command-line tool that analyzes audio files to detect `BPM` and `Key` tags. * Write detected BPM and Key to metadata tags * Save track metadata to a database * Query the database by artist, key, BPM range, or regex +* Import tracks from a Mixxx SQLite database ## Building @@ -25,21 +26,23 @@ cargo build --release ### Arguments -* `[PATH]...`: One or more files or directories to analyze. Not required when using database query flags. +* `[PATH]...`: Files or directories to analyze. Not required when using database query flags. ### Options * `-w`, `--write`: Write detected BPM and Key to audio file metadata. -* `-f`, `--force`: Re-analyze files even if they already have BPM/Key tags. -* `-j`, `--jobs <JOBS>`: Number of threads to use (0 = all cores). -* `-S`, `--save`: Save track reports (artist, track, BPM, key) to the database. -* `-l`, `--list`: List all track reports stored in the database. -* `--query <KEY>`: Look up a track report by exact artist - track key. -* `-s`, `--search <PATTERN>`: Search track reports by regex pattern (matched against key, artist, track, BPM, key). -* `-a`, `--artist <PREFIX>`: Find tracks by artist name prefix (uses database index). -* `--key <PREFIX>`: Find tracks by key prefix (uses database index). -* `--bpm <MIN> <MAX>`: Find tracks with BPM in the given range (uses database index). -* `--db-path <PATH>`: Path to the database directory (default: `~/.local/share/virittaa`). +* `-f`, `--force`: Re-analyze files that already have BPM/Key tags. +* `-j`, `--jobs <JOBS>`: Number of threads (0 = all cores). +* `-S`, `--save`: Save track reports to the database. +* `-l`, `--list`: List track reports in the database. +* `--limit <N>`: Max entries to list (default: 1000, 0 = all). +* `--query <KEY>`: Look up a track by exact key. +* `-s`, `--search <PATTERN>`: Search track reports by regex. +* `-a`, `--artist <PREFIX>`: Find tracks by artist name prefix. +* `--key <PREFIX>`: Find tracks by key prefix (e.g. 'C Major', 'Am'). +* `--bpm <MIN> <MAX>`: Find tracks with BPM in range MIN MAX. +* `--import-mixxx <PATH>`: Import tracks from a Mixxx SQLite database. +* `--db-path <PATH>`: Database directory (default: `~/.local/share/virittaa`). * `-h`, `--help`: Print help information. * `-V`, `--version`: Print version information. @@ -64,9 +67,12 @@ cargo build --release # Write tags and save to database with custom path ./target/release/virittaa -w --save --db-path ~/my_library /path/to/music/ -# List all tracks in the database +# List tracks in the database (default limit: 1000) ./target/release/virittaa --list +# List all tracks (no limit) +./target/release/virittaa --list --limit 0 + # Look up a specific track ./target/release/virittaa --query "Aphex Twin - Xtal" @@ -74,12 +80,15 @@ cargo build --release ./target/release/virittaa --search 'weed' ./target/release/virittaa --search '^Aphex' -# Find all tracks by an artist (indexed, fast prefix scan) +# Find all tracks by an artist ./target/release/virittaa --artist 'Aphex Twin' -# Find all tracks in a key (indexed) +# Find all tracks in a key ./target/release/virittaa --key 'C Major' -# Find tracks with BPM between 120 and 140 (indexed range query) +# Find tracks with BPM between 120 and 140 ./target/release/virittaa --bpm 120 140 + +# Import from a Mixxx database +./target/release/virittaa --import-mixxx ~/.mixxx/mixxxdb.sqlite ``` diff --git a/src/db.rs b/src/db.rs @@ -25,16 +25,13 @@ impl fmt::Display for TrackReport { } } -/// Sled-backed library database using multiple trees as indexes. +/// Sled-backed library database with secondary indexes for artist, key, and BPM. /// /// Schema: /// - default tree: `"Artist - Track"` → JSON `TrackReport` -/// - `"artist"` tree: `"artist\x00primary_key"` → empty -/// - `"key"` tree: `"key\x00primary_key"` → empty -/// - `"bpm"` tree: `f64_be_bytes + "\x00" + primary_key` → empty -/// -/// This enables fast prefix scans by artist, key, and BPM range queries -/// using sled's native `scan_prefix` and `range` operations. +/// - `"artist"` tree: `"artist\0primary_key"` → empty +/// - `"key"` tree: `"key\0primary_key"` → empty +/// - `"bpm"` tree: `f64_be_bytes + "\0" + primary_key` → empty pub struct LibraryDb { db: sled::Db, artist_tree: Tree, @@ -44,7 +41,6 @@ pub struct LibraryDb { const SEP: u8 = 0x00; -/// Build a prefix-style index key: `value\0primary_key`. fn prefix_index_key(value: &[u8], primary_key: &[u8]) -> Vec<u8> { let mut buf = Vec::with_capacity(value.len() + 1 + primary_key.len()); buf.extend_from_slice(value); @@ -53,7 +49,6 @@ fn prefix_index_key(value: &[u8], primary_key: &[u8]) -> Vec<u8> { buf } -/// Build a BPM range index key: `f64_be_bytes\0primary_key`. fn bpm_index_key(bpm: f64, primary_key: &[u8]) -> Vec<u8> { let mut buf = Vec::with_capacity(8 + 1 + primary_key.len()); buf.extend_from_slice(&bpm.to_be_bytes()); @@ -62,7 +57,6 @@ fn bpm_index_key(bpm: f64, primary_key: &[u8]) -> Vec<u8> { buf } -/// Extract the primary key portion after the first separator from an index key. fn extract_primary_key(idx_key: &[u8]) -> Result<&[u8]> { let pos = idx_key .iter() @@ -72,6 +66,8 @@ fn extract_primary_key(idx_key: &[u8]) -> Result<&[u8]> { } impl LibraryDb { + /// Open or create the database at the given path. + /// /// # Errors /// /// Returns an error if the database cannot be opened or created. @@ -91,13 +87,14 @@ impl LibraryDb { }) } + /// Save a track report and update all secondary indexes. + /// /// # Errors /// - /// Returns an error if serialization or the database write fails. + /// Returns an error if serialization or any database write fails. pub fn save(&self, key: &str, report: &TrackReport) -> Result<()> { let value = serde_json::to_vec(report).context("Failed to serialize track report")?; - // Primary store self.db .insert(key.as_bytes(), value) .context("Failed to write to database")?; @@ -109,7 +106,7 @@ impl LibraryDb { .insert(&idx_key, &[][..]) .context("Failed to write artist index")?; - // Key signature index + // Key index if let Some(key_val) = &report.key { let key_lower = key_val.to_lowercase(); let idx_key = prefix_index_key(key_lower.as_bytes(), key.as_bytes()); @@ -139,6 +136,8 @@ impl LibraryDb { Ok(()) } + /// Look up a track report by exact key. + /// /// # Errors /// /// Returns an error if the database read or deserialization fails. @@ -154,9 +153,11 @@ impl LibraryDb { } } + /// Delete a track report and remove its secondary index entries. + /// /// # Errors /// - /// Returns an error if the database delete fails. + /// Returns an error if the database delete or deserialization fails. #[allow(dead_code)] pub fn delete(&self, key: &str) -> Result<()> { let old = self @@ -194,14 +195,23 @@ impl LibraryDb { Ok(()) } - /// List all entries in the primary tree. + /// Count all entries in the primary tree. + /// + /// # Errors + /// + /// Returns an error if the database iteration fails. + pub fn count(&self) -> Result<usize> { + Ok(self.db.len()) + } + + /// List entries in the primary tree, up to `limit` items. /// /// # Errors /// /// Returns an error if the database iteration or deserialization fails. - pub fn list(&self) -> Result<Vec<(String, TrackReport)>> { - let mut results = Vec::new(); - for item in self.db.iter() { + pub fn list(&self, limit: usize) -> Result<Vec<(String, TrackReport)>> { + let mut results = Vec::with_capacity(limit.min(1024)); + for item in self.db.iter().take(limit) { let (key, value) = item.context("Failed to read iterator entry")?; let key_str = String::from_utf8(key.to_vec()).context("Invalid UTF-8 key in database")?; @@ -212,7 +222,7 @@ impl LibraryDb { Ok(results) } - /// Find all tracks by a given artist prefix (case-insensitive). + /// Find tracks by artist name prefix (case-insensitive). /// /// # Errors /// @@ -235,7 +245,7 @@ impl LibraryDb { Ok(results) } - /// Find all tracks in a given key signature (case-insensitive prefix match). + /// Find tracks by key signature prefix (case-insensitive). /// /// # Errors /// @@ -245,7 +255,7 @@ impl LibraryDb { let mut results = Vec::new(); for item in self.key_tree.scan_prefix(prefix_lower.as_bytes()) { - let (idx_key, _) = item.context("Failed to read Key index")?; + let (idx_key, _) = item.context("Failed to read key index")?; let primary_key_bytes = extract_primary_key(&idx_key)?; let primary_key = String::from_utf8(primary_key_bytes.to_vec()) .context("Invalid UTF-8 in primary key")?; @@ -258,7 +268,7 @@ impl LibraryDb { Ok(results) } - /// Find all tracks with BPM in the given range (inclusive). + /// Find tracks with BPM in the given range (inclusive). /// /// # Errors /// diff --git a/src/file_io.rs b/src/file_io.rs @@ -32,11 +32,11 @@ fn build_track_info(path: &Path, metadata: FileMetadata, bpm: f32, key: String) } } -/// Process a single file. +/// Process a single file, detecting BPM and key. /// /// # Errors /// -/// Returns an error if the file cannot be processed. +/// Returns an error string if the file cannot be decoded or key detection fails. pub fn process_file(path: &Path, write_tags: bool, force: bool) -> Result<TrackInfo, String> { let (metadata, existing_tags) = read_tags(path); @@ -116,11 +116,11 @@ struct ReportRecord<'a> { error: Option<&'a str>, } -/// Write a CSV report of the processed tracks. +/// Write a CSV report of processed tracks. /// /// # Errors /// -/// Returns an error if the report cannot be written. +/// Returns an error if the CSV file cannot be created or written. pub fn write_report(results: &[TrackInfo], errors: &[TrackError]) -> Result<()> { let timestamp = Local::now().format("%Y-%m-%dT%H-%M-%S").to_string(); let filename = format!("virittaa-report-{timestamp}.csv"); @@ -164,7 +164,7 @@ pub fn write_report(results: &[TrackInfo], errors: &[TrackError]) -> Result<()> /// /// # Errors /// -/// Returns an error if the metadata cannot be written. +/// Returns an error if the file cannot be opened, read, or written. /// /// # Panics /// @@ -177,7 +177,6 @@ pub fn write_metadata(path: &Path, bpm: f32, key: &str) -> Result<()> { if let Some(first_tag) = tagged_file.first_tag_mut() { first_tag } else { - // If there are no tags, create a new one based on the file type let tag_type = tagged_file.primary_tag_type(); tagged_file.insert_tag(Tag::new(tag_type)); tagged_file diff --git a/src/import.rs b/src/import.rs @@ -0,0 +1,73 @@ +use anyhow::{Context, Result}; +use rusqlite::Connection; + +use crate::db::{LibraryDb, TrackReport}; + +/// Convert a Mixxx key string (e.g. "Cm", "D", "Bbm") to virittaa format +/// (e.g. "C Minor", "D Major", "Bb Minor"). Returns `None` for empty or +/// unknown keys. +fn convert_mixxx_key(raw: &str) -> Option<String> { + let raw = raw.trim(); + if raw.is_empty() { + return None; + } + // Mixxx minor keys end with 'm': "Cm", "C#m", "Bbm", etc. + if raw.ends_with('m') && raw.len() > 1 { + let root = &raw[..raw.len() - 1]; + Some(format!("{root} Minor")) + } else { + Some(format!("{raw} Major")) + } +} + +/// Import tracks from a Mixxx `SQLite` database into the sled library database. +/// +/// Reads the `library` and `track_locations` tables, skipping entries marked as +/// deleted, and saves each track with its BPM and key. +/// +/// # Errors +/// +/// Returns an error if the Mixxx database cannot be opened or queried, or if +/// sled writes fail. +pub fn import_mixxx(db: &LibraryDb, mixxx_path: &str) -> Result<usize> { + let conn = Connection::open(mixxx_path).with_context(|| "Failed to open Mixxx database")?; + + let mut stmt = conn.prepare( + "SELECT l.artist, l.title, l.bpm, l.key \ + FROM library l \ + WHERE l.mixxx_deleted = 0 AND l.bpm IS NOT NULL AND l.bpm > 0", + )?; + + let rows = stmt.query_map([], |row| { + let artist: Option<String> = row.get(0)?; + let title: Option<String> = row.get(1)?; + let bpm: Option<f64> = row.get(2)?; + let key_raw: Option<String> = row.get(3)?; + Ok((artist, title, bpm, key_raw)) + })?; + + let mut count = 0usize; + for row in rows { + let (artist, title, bpm, key_raw) = row?; + let key = key_raw.as_deref().and_then(convert_mixxx_key); + + let primary_key = format!( + "{} - {}", + artist.as_deref().unwrap_or("Unknown"), + title.as_deref().unwrap_or("Unknown") + ); + + let report = TrackReport { + artist, + track: title, + bpm, + key, + }; + + db.save(&primary_key, &report)?; + count += 1; + } + + db.flush()?; + Ok(count) +} diff --git a/src/main.rs b/src/main.rs @@ -2,6 +2,7 @@ pub mod audio_processing; pub mod constants; pub mod db; pub mod file_io; +pub mod import; pub mod types; pub mod utils; @@ -15,6 +16,7 @@ use walkdir::WalkDir; use crate::db::LibraryDb; use crate::db::TrackReport; use crate::file_io::process_file; +use crate::import::import_mixxx; use crate::types::TrackError; use crate::utils::{is_supported_audio_file, print_entries}; @@ -34,7 +36,7 @@ fn main() -> Result<()> { .arg( Arg::new("path") .num_args(1..) - .help("One or more files or directories to analyze (not required with --list, --query, or --search)"), + .help("Files or directories to analyze"), ) .arg( Arg::new("write") @@ -48,13 +50,13 @@ fn main() -> Result<()> { .short('f') .long("force") .action(clap::ArgAction::SetTrue) - .help("Re-analyze files even if they already have BPM/Key tags"), + .help("Re-analyze files that already have BPM/Key tags"), ) .arg( Arg::new("jobs") .short('j') .long("jobs") - .help("Number of threads to use (0 = all cores)") + .help("Number of threads (0 = all cores)") .default_value("0") .value_parser(clap::value_parser!(usize)), ) @@ -63,47 +65,59 @@ fn main() -> Result<()> { .short('S') .long("save") .action(clap::ArgAction::SetTrue) - .help("Save track reports (artist, track, BPM, key) to a database"), + .help("Save track reports to the database"), ) .arg( Arg::new("list") .short('l') .long("list") .action(clap::ArgAction::SetTrue) - .help("List all track reports stored in the database"), + .help("List track reports in the database"), + ) + .arg( + Arg::new("limit") + .long("limit") + .help("Max entries to list (default: 1000, 0 = all)") + .default_value("1000") + .value_parser(clap::value_parser!(usize)), ) .arg( Arg::new("query") .long("query") - .help("Look up a track report by exact artist - track key"), + .help("Look up a track by exact key"), ) .arg( Arg::new("search") .short('s') .long("search") - .help("Search track reports by regex pattern (matched against key, artist, track, BPM, key)"), + .help("Search track reports by regex"), ) .arg( Arg::new("artist") .short('a') .long("artist") - .help("Find tracks by artist name prefix (uses database index)"), + .help("Find tracks by artist name prefix"), ) .arg( Arg::new("key") .long("key") - .help("Find tracks by key prefix, e.g. 'C Major', 'Am' (uses database index)"), + .help("Find tracks by key prefix (e.g. 'C Major', 'Am')"), ) .arg( Arg::new("bpm") .long("bpm") .num_args(2) - .help("Find tracks with BPM in range MIN MAX (uses database index)"), + .help("Find tracks with BPM in range MIN MAX"), + ) + .arg( + Arg::new("import-mixxx") + .long("import-mixxx") + .help("Import tracks from a Mixxx SQLite database"), ) .arg( Arg::new("db-path") .long("db-path") - .help("Path to the database directory for saved reports (default: ~/.local/share/virittaa)"), + .help("Database directory (default: ~/.local/share/virittaa)"), ) .get_matches(); @@ -116,6 +130,8 @@ fn main() -> Result<()> { let search_pattern = matches.get_one::<String>("search"); let artist_prefix = matches.get_one::<String>("artist").map(String::as_str); let key_prefix = matches.get_one::<String>("key").map(String::as_str); + let import_mixxx_path = matches.get_one::<String>("import-mixxx"); + let limit = *matches.get_one::<usize>("limit").unwrap(); let bpm_range: Option<(f64, f64)> = matches.get_many::<String>("bpm").map(|vals| { let v: Vec<&String> = vals.collect(); let min = v[0].parse::<f64>().expect("Invalid BPM min value"); @@ -130,17 +146,23 @@ fn main() -> Result<()> { dir.to_string_lossy().to_string() }; - // Read-only DB operations that don't need file paths let is_db_query = list_db || query_key.is_some() || search_pattern.is_some() || artist_prefix.is_some() || key_prefix.is_some() - || bpm_range.is_some(); + || bpm_range.is_some() + || import_mixxx_path.is_some(); if is_db_query { let db = LibraryDb::open(std::path::Path::new(&db_path))?; + if let Some(mixxx_path) = import_mixxx_path { + let count = import_mixxx(&db, mixxx_path)?; + println!("Imported {count} tracks from Mixxx database."); + return Ok(()); + } + if let Some(query_key) = query_key { match db.get(query_key)? { Some(report) => println!("{query_key}: {report}"), @@ -169,7 +191,8 @@ fn main() -> Result<()> { ); } else if let Some(pattern) = search_pattern { let re = Regex::new(pattern)?; - let entries = db.list()?; + let list_limit = if limit == 0 { usize::MAX } else { limit }; + let entries = db.list(list_limit)?; let matches_found: Vec<_> = entries .into_iter() .filter(|(key, report)| re.is_match(key) || re.is_match(&report.to_string())) @@ -180,14 +203,20 @@ fn main() -> Result<()> { "match(es) found.", ); } else { - let entries = db.list()?; + let effective_limit = if limit == 0 { usize::MAX } else { limit }; + let entries = db.list(effective_limit)?; + let total = db.count()?; if entries.is_empty() { println!("No track reports found in database."); } else { for (key, report) in &entries { println!("{key}: {report}"); } - println!("\n{} track(s) in database.", entries.len()); + if total > entries.len() { + println!("\n{} of {} track(s) shown.", entries.len(), total); + } else { + println!("\n{} track(s) in database.", entries.len()); + } } } return Ok(()); @@ -197,7 +226,7 @@ fn main() -> Result<()> { .get_many::<String>("path") .ok_or_else(|| { anyhow::anyhow!( - "Provide file paths, or use --list / --query / --search / --artist / --key / --bpm to read the database" + "Provide file paths, or use --list / --query / --search / --artist / --key / --bpm / --import-mixxx to read the database" ) })? .cloned()