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 | + |
| M | Cargo.lock | | | 86 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- |
| M | Cargo.toml | | | 1 | + |
| M | README.md | | | 39 | ++++++++++++++++++++++++--------------- |
| M | src/db.rs | | | 54 | ++++++++++++++++++++++++++++++++---------------------- |
| M | src/file_io.rs | | | 11 | +++++------ |
| A | src/import.rs | | | 73 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | src/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()