Commit Diff


commit - 04889ba77382af8455a3b56e1a249bb8125920c7
commit + b4f514be3b06a3897179331687cac0844089fa07
blob - 2ded768f0ad04518390183d630756d11d6dd3d87
blob + 4f4f43dbcf8275c3d5109c08729f115c3140213e
--- Cargo.lock
+++ Cargo.lock
@@ -86,12 +86,30 @@ source = "registry+https://github.com/rust-lang/crates
 checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
 
 [[package]]
+name = "atomic-polyfill"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
+dependencies = [
+ "critical-section",
+]
+
+[[package]]
 name = "autocfg"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
 
 [[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
 name = "bindgen"
 version = "0.64.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -198,6 +216,15 @@ source = "registry+https://github.com/rust-lang/crates
 checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
 
 [[package]]
+name = "cache-advisor"
+version = "1.0.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f89ab55ca4e6a46a0740a1c5346db1ad66e4a76598bbfa060dc3259935a7450"
+dependencies = [
+ "crossbeam-queue",
+]
+
+[[package]]
 name = "cast"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -296,12 +323,32 @@ source = "registry+https://github.com/rust-lang/crates
 checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
 
 [[package]]
+name = "cobs"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
 name = "colorchoice"
 version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
 
 [[package]]
+name = "concurrent-map"
+version = "5.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6542c565fbcba786db59307d7840f0bf5cd9e0aba6502755337e15f0e06fd65"
+dependencies = [
+ "ebr",
+ "serde",
+ "stack-map",
+]
+
+[[package]]
 name = "crc32fast"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -347,6 +394,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "critical-section"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
+
+[[package]]
 name = "crossbeam-deque"
 version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -366,6 +419,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "crossbeam-queue"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
 name = "crossbeam-utils"
 version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -405,12 +467,33 @@ dependencies = [
 ]
 
 [[package]]
+name = "ebr"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b1ea3b18359d566f360eaf811a2d69bc6c8eb6faaeecc8839975633860a076e"
+dependencies = [
+ "shared-local-state",
+]
+
+[[package]]
 name = "either"
 version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
 
 [[package]]
+name = "embedded-io"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
+
+[[package]]
+name = "embedded-io"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+
+[[package]]
 name = "equivalent"
 version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -450,6 +533,12 @@ source = "registry+https://github.com/rust-lang/crates
 checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
 
 [[package]]
+name = "fault-injection"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e3d175246dec3fddef3b1fcd57acdb023e4c562d032e9eccc5f246da3d7fed3"
+
+[[package]]
 name = "ffmpeg-next"
 version = "8.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -491,6 +580,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
 name = "foldhash"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -531,15 +626,6 @@ dependencies = [
 ]
 
 [[package]]
-name = "fxhash"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
-dependencies = [
- "byteorder",
-]
-
-[[package]]
 name = "getrandom"
 version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -580,6 +666,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "hash32"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
 name = "hashbrown"
 version = "0.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -604,6 +699,20 @@ dependencies = [
 ]
 
 [[package]]
+name = "heapless"
+version = "0.7.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
+dependencies = [
+ "atomic-polyfill",
+ "hash32",
+ "rustc_version",
+ "serde",
+ "spin",
+ "stable_deref_trait",
+]
+
+[[package]]
 name = "heck"
 version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -635,12 +744,13 @@ dependencies = [
 ]
 
 [[package]]
-name = "instant"
-version = "0.1.13"
+name = "inline-array"
+version = "0.1.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
+checksum = "45e8b42f7d66073247744b2971fcc4df24afe3e686616c20a98439ec4f156d43"
 dependencies = [
- "cfg-if",
+ "concurrent-map",
+ "serde",
 ]
 
 [[package]]
@@ -975,28 +1085,32 @@ source = "registry+https://github.com/rust-lang/crates
 checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
 
 [[package]]
+name = "pagetable"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69b57830c885fc057ecbf2f1f99f0427c3d102cf2ee5e80a52c09948d45a460e"
+
+[[package]]
 name = "parking_lot"
-version = "0.11.2"
+version = "0.12.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
 dependencies = [
- "instant",
  "lock_api",
  "parking_lot_core",
 ]
 
 [[package]]
 name = "parking_lot_core"
-version = "0.8.6"
+version = "0.9.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
 dependencies = [
  "cfg-if",
- "instant",
  "libc",
  "redox_syscall",
  "smallvec",
- "winapi",
+ "windows-link",
 ]
 
 [[package]]
@@ -1067,6 +1181,19 @@ dependencies = [
 ]
 
 [[package]]
+name = "postcard"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
+dependencies = [
+ "cobs",
+ "embedded-io 0.4.0",
+ "embedded-io 0.6.1",
+ "heapless",
+ "serde",
+]
+
+[[package]]
 name = "ppv-lite86"
 version = "0.2.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1182,11 +1309,11 @@ checksum = "fca1481d62f18158646de2ec552dd63f8bdc5be644
 
 [[package]]
 name = "redox_syscall"
-version = "0.2.16"
+version = "0.5.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
 dependencies = [
- "bitflags 1.3.2",
+ "bitflags 2.11.1",
 ]
 
 [[package]]
@@ -1267,6 +1394,15 @@ source = "registry+https://github.com/rust-lang/crates
 checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
 
 [[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
 name = "rustfft"
 version = "6.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1315,6 +1451,12 @@ source = "registry+https://github.com/rust-lang/crates
 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
 [[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
 name = "serde"
 version = "1.0.228"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1358,6 +1500,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "shared-local-state"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a50ccb2f45251772ed15abfd1e5f10a305288187b1582ab2e4295b29bbb4929"
+dependencies = [
+ "parking_lot",
+]
+
+[[package]]
 name = "shlex"
 version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1377,18 +1528,26 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479
 
 [[package]]
 name = "sled"
-version = "0.34.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935"
+version = "1.0.0-alpha.124"
+source = "git+https://github.com/spacejam/sled?rev=e449d171#e449d17111f4a097e1c66b6db241962ccb6a4136"
 dependencies = [
+ "bincode",
+ "cache-advisor",
+ "concurrent-map",
  "crc32fast",
- "crossbeam-epoch",
- "crossbeam-utils",
+ "crossbeam-queue",
+ "ebr",
+ "fault-injection",
+ "fnv",
  "fs2",
- "fxhash",
- "libc",
+ "inline-array",
  "log",
+ "pagetable",
  "parking_lot",
+ "rayon",
+ "serde",
+ "stack-map",
+ "zstd",
 ]
 
 [[package]]
@@ -1398,6 +1557,15 @@ source = "registry+https://github.com/rust-lang/crates
 checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
 
 [[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
 name = "sqlite-wasm-rs"
 version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1410,6 +1578,21 @@ dependencies = [
 ]
 
 [[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "stack-map"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b49d6d36fee60faad91e23603db2356677b58ec2429237b39d5c60c26868f37c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
 name = "strength_reduce"
 version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1530,11 +1713,11 @@ dependencies = [
  "dirs",
  "lofty",
  "ndarray 0.17.2",
+ "postcard",
  "rayon",
  "regex",
  "rusqlite",
  "serde",
- "serde_json",
  "sled",
  "walkdir",
 ]
@@ -1781,3 +1964,32 @@ name = "zmij"
 version = "1.0.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[package]]
+name = "zstd"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "6.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581"
+dependencies = [
+ "libc",
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.16+zstd.1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
blob - 86ebc37c2fd10aafd9c8875005e8b72c080abd18
blob + 7900cbb2f42bd14dd5890a8a416284b6f17ddaa4
--- Cargo.toml
+++ Cargo.toml
@@ -13,8 +13,8 @@ walkdir = "2.5"
 lofty = "0.24.0"
 rayon = "1.10"
 serde = { version = "1.0", features = ["derive"] }
-serde_json = "1.0"
-sled = "0.34"
+postcard = { version = "1", features = ["alloc"] }
+sled = { git = "https://github.com/spacejam/sled", rev = "e449d171" }
 dirs = "6"
 regex = "1"
 rusqlite = "0.39"
blob - ee4d962199856141e6dc8e5b2ee23e1b1295f8f2
blob + a069d119d5bc7a93aa1cf1fe72b28dcb3d1d3163
--- README.md
+++ README.md
@@ -6,7 +6,7 @@ A command-line tool that analyzes audio files to detec
 
 * Detect BPM and Key of audio files
 * Write detected BPM and Key to metadata tags
-* Save track metadata to a database
+* Save track metadata to a database (on by default)
 * Query the database by artist, key, BPM range, or regex
 * Import tracks from a Mixxx SQLite database
 
@@ -30,14 +30,14 @@ cargo build --release
 
 ### Options
 
-*   `-w`, `--write`: Write detected BPM and Key to audio file metadata.
+*   `-t`, `--write-tags`: Write detected BPM and Key to audio file metadata.
+*   `--no-store`: Skip saving track reports to the database.
 *   `-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.
+*   `-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.
@@ -49,24 +49,24 @@ cargo build --release
 ### Examples
 
 ```sh
-# Analyze a single file
+# Analyze a single file (saves to DB by default)
 ./target/release/virittaa /path/to/track.mp3
 
 # Analyze a directory and write metadata tags
-./target/release/virittaa --write /path/to/music/
+./target/release/virittaa --write-tags /path/to/music/
 
 # Re-analyze files that already have tags
-./target/release/virittaa --force --write /path/to/music/
+./target/release/virittaa --force --write-tags /path/to/music/
 
+# Analyze without saving to the database
+./target/release/virittaa --no-store /path/to/music/
+
 # Limit to 4 threads
 ./target/release/virittaa -j 4 /path/to/music/
 
-# Analyze and save reports to database
-./target/release/virittaa --save /path/to/music/
+# Write tags and use a custom database path
+./target/release/virittaa -t --db-path ~/my_library /path/to/music/
 
-# Write tags and save to database with custom path
-./target/release/virittaa -w --save --db-path ~/my_library /path/to/music/
-
 # List tracks in the database (default limit: 1000)
 ./target/release/virittaa --list
 
blob - 4bd824afb7a33902e078d4841402f100152751a2
blob + e41dde064bf1fc728c64bc3b3990d533c89da583
--- src/db.rs
+++ src/db.rs
@@ -1,6 +1,8 @@
 use anyhow::{Context, Result};
+use regex::Regex;
 use serde::{Deserialize, Serialize};
 use sled::Tree;
+use std::cmp::Reverse;
 use std::fmt;
 use std::path::Path;
 
@@ -30,10 +32,10 @@ impl fmt::Display for TrackReport {
 /// Sled-backed library database with secondary indexes for artist, key, and BPM.
 ///
 /// Schema:
-/// - default tree: `"Artist - Track"` → JSON `TrackReport`
-/// - `"artist"` tree: `"artist\0primary_key"` → empty
-/// - `"key"` tree: `"key\0primary_key"` → empty
-/// - `"bpm"` tree: `f64_be_bytes + "\0" + primary_key` → empty
+/// - default tree: `"Artist - Track"` → postcard-encoded `TrackReport`
+/// - `"artist"` tree: `"artist\0primary_key"` → 8-byte BE timestamp
+/// - `"key"` tree: `"key\0primary_key"` → 8-byte BE timestamp
+/// - `"bpm"` tree: `f64_be_bytes + "\0" + primary_key` → 8-byte BE timestamp
 /// - `"time"` tree: `i64_be_bytes(timestamp) + "\0" + primary_key` → empty
 pub struct LibraryDb {
     db: sled::Db,
@@ -79,6 +81,31 @@ fn extract_primary_key(idx_key: &[u8]) -> Result<&[u8]
     Ok(&idx_key[pos + 1..])
 }
 
+fn extract_primary_key_string(idx_key: &[u8]) -> Result<String> {
+    let bytes = extract_primary_key(idx_key)?;
+    String::from_utf8(bytes.to_vec()).context("Invalid UTF-8 in primary key")
+}
+
+fn parse_timestamp(value: &[u8]) -> i64 {
+    if value.len() == 8 {
+        i64::from_be_bytes(value[..8].try_into().unwrap_or([0; 8]))
+    } else {
+        0
+    }
+}
+
+fn serialize_report(report: &TrackReport) -> Result<Vec<u8>> {
+    postcard::to_allocvec(report).context("Failed to serialize track report")
+}
+
+fn deserialize_report(bytes: &[u8]) -> Result<TrackReport> {
+    postcard::from_bytes(bytes).context("Failed to deserialize track report")
+}
+
+fn effective_limit(limit: usize) -> usize {
+    if limit == 0 { usize::MAX } else { limit }
+}
+
 impl LibraryDb {
     /// Open or create the database at the given path.
     ///
@@ -103,206 +130,176 @@ impl LibraryDb {
         })
     }
 
-    /// Save a track report and update all secondary indexes.
-    ///
-    /// # Errors
-    ///
-    /// Returns an error if serialization or any database write fails.
-    pub fn save(&self, key: &str, report: &TrackReport) -> Result<()> {
-        #[allow(clippy::cast_possible_wrap)]
-        let now = std::time::SystemTime::now()
-            .duration_since(std::time::UNIX_EPOCH)
-            .unwrap_or_default()
-            .as_secs() as i64;
+    /// Write secondary index entries for a report.
+    fn update_indexes(&self, key: &str, report: &TrackReport) -> Result<()> {
+        let ts = report.timestamp.unwrap_or(0);
+        let ts_bytes = ts.to_be_bytes();
 
-        let report = TrackReport {
-            artist: report.artist.clone(),
-            track: report.track.clone(),
-            bpm: report.bpm,
-            key: report.key.clone(),
-            timestamp: Some(now),
-        };
-
-        let value = serde_json::to_vec(&report).context("Failed to serialize track report")?;
-
-        let old = self
-            .db
-            .insert(key.as_bytes(), value)
-            .context("Failed to write to database")?;
-
-        // Remove old time index entry if overwriting an existing record
-        if let Some(old_bytes) = old
-            && let Ok(old_report) = serde_json::from_slice::<TrackReport>(&old_bytes)
-            && let Some(old_ts) = old_report.timestamp
-        {
-            let old_time_key = time_index_key(old_ts, key.as_bytes());
-            self.time_tree
-                .remove(&old_time_key)
-                .context("Failed to remove old time index")?;
-        }
-
         // Artist index
         let artist_lower = report.artist.as_deref().unwrap_or("unknown").to_lowercase();
         let idx_key = prefix_index_key(artist_lower.as_bytes(), key.as_bytes());
-        self.artist_tree
-            .insert(&idx_key, &[][..])
-            .context("Failed to write artist index")?;
+        self.artist_tree.insert(&idx_key, &ts_bytes[..])?;
 
-        // 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());
-            self.key_tree
-                .insert(&idx_key, &[][..])
-                .context("Failed to write key index")?;
-        }
+        // Key-signature index
+        let key_lower = report.key.as_deref().unwrap_or("unknown").to_lowercase();
+        let idx_key = prefix_index_key(key_lower.as_bytes(), key.as_bytes());
+        self.key_tree.insert(&idx_key, &ts_bytes[..])?;
 
         // BPM index
         if let Some(bpm) = report.bpm {
             let idx_key = bpm_index_key(bpm, key.as_bytes());
-            self.bpm_tree
-                .insert(&idx_key, &[][..])
-                .context("Failed to write bpm index")?;
+            self.bpm_tree.insert(&idx_key, &ts_bytes[..])?;
         }
 
-        // Time index
-        let time_key = time_index_key(now, key.as_bytes());
-        self.time_tree
-            .insert(&time_key, &[][..])
-            .context("Failed to write time index")?;
+        // Time index (always written, timestamp=0 for entries without one)
+        let idx_key = time_index_key(ts, key.as_bytes());
+        self.time_tree.insert(&idx_key, &[][..])?;
 
         Ok(())
     }
 
-    /// Flush pending writes to disk.
-    ///
-    /// # Errors
-    ///
-    /// Returns an error if the flush fails.
-    pub fn flush(&self) -> Result<()> {
-        self.db.flush().context("Failed to flush database")?;
+    /// Remove all secondary index entries for a report.
+    fn remove_indexes(&self, key: &str, report: &TrackReport) -> Result<()> {
+        let artist_lower = report.artist.as_deref().unwrap_or("unknown").to_lowercase();
+        let idx_key = prefix_index_key(artist_lower.as_bytes(), key.as_bytes());
+        self.artist_tree.remove(&idx_key)?;
+
+        let key_lower = report.key.as_deref().unwrap_or("unknown").to_lowercase();
+        let idx_key = prefix_index_key(key_lower.as_bytes(), key.as_bytes());
+        self.key_tree.remove(&idx_key)?;
+
+        if let Some(bpm) = report.bpm {
+            let idx_key = bpm_index_key(bpm, key.as_bytes());
+            self.bpm_tree.remove(&idx_key)?;
+        }
+
+        let ts = report.timestamp.unwrap_or(0);
+        let idx_key = time_index_key(ts, key.as_bytes());
+        self.time_tree.remove(&idx_key)?;
+
         Ok(())
     }
 
-    /// Look up a track report by exact key.
+    /// Get a track report by exact primary key.
     ///
     /// # Errors
     ///
-    /// Returns an error if the database read or deserialization fails.
+    /// Returns an error if the database operation fails.
     pub fn get(&self, key: &str) -> Result<Option<TrackReport>> {
-        let ivec = self.db.get(key).context("Failed to read from database")?;
-        match ivec {
-            Some(bytes) => {
-                let report: TrackReport =
-                    serde_json::from_slice(&bytes).context("Failed to deserialize track report")?;
-                Ok(Some(report))
+        self.db
+            .get(key.as_bytes())?
+            .map(|bytes| deserialize_report(&bytes))
+            .transpose()
+    }
+
+    /// Save a single track report, inserting or updating as needed.
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if the database operation fails.
+    pub fn save(&self, key: &str, report: &TrackReport) -> Result<()> {
+        // Remove stale indexes if overwriting an existing entry.
+        if let Some(old_bytes) = self
+            .db
+            .insert(key.as_bytes(), serialize_report(report)?.as_slice())?
+        {
+            let old_report = deserialize_report(&old_bytes)?;
+            self.remove_indexes(key, &old_report)?;
+        }
+
+        self.update_indexes(key, report)?;
+        Ok(())
+    }
+
+    /// Save a batch of track reports, inserting or updating as needed.
+    ///
+    /// Uses sled's batch API for the default tree to reduce write amplification.
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if the database operation fails.
+    pub fn save_batch(&self, entries: &[(String, TrackReport)]) -> Result<()> {
+        for (key, report) in entries {
+            // Remove stale indexes for any existing entries.
+            if let Some(old_bytes) = self.db.get(key.as_bytes())? {
+                let old_report = deserialize_report(&old_bytes)?;
+                self.remove_indexes(key, &old_report)?;
             }
-            None => Ok(None),
+            self.update_indexes(key, report)?;
         }
+
+        let mut batch = sled::Batch::default();
+        for (key, report) in entries {
+            let serialized = serialize_report(report)?;
+            batch.insert(key.as_bytes(), serialized.as_slice());
+        }
+        self.db.apply_batch(batch)?;
+        Ok(())
     }
 
-    /// Delete a track report and remove its secondary index entries.
+    /// Delete a track report and its secondary index entries.
     ///
     /// # Errors
     ///
-    /// Returns an error if the database delete or deserialization fails.
-    #[allow(dead_code)]
+    /// Returns an error if the database operation fails.
     pub fn delete(&self, key: &str) -> Result<()> {
-        let old = self
-            .db
-            .remove(key)
-            .context("Failed to delete from database")?;
-
-        if let Some(bytes) = old {
-            let report: TrackReport =
-                serde_json::from_slice(&bytes).context("Failed to deserialize old record")?;
-
-            let artist_lower = report.artist.as_deref().unwrap_or("unknown").to_lowercase();
-            let idx_key = prefix_index_key(artist_lower.as_bytes(), key.as_bytes());
-            self.artist_tree
-                .remove(&idx_key)
-                .context("Failed to delete from artist 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());
-                self.key_tree
-                    .remove(&idx_key)
-                    .context("Failed to delete from key index")?;
-            }
-
-            if let Some(bpm) = report.bpm {
-                let idx_key = bpm_index_key(bpm, key.as_bytes());
-                self.bpm_tree
-                    .remove(&idx_key)
-                    .context("Failed to delete from bpm index")?;
-            }
-
-            if let Some(ts) = report.timestamp {
-                let time_key = time_index_key(ts, key.as_bytes());
-                self.time_tree
-                    .remove(&time_key)
-                    .context("Failed to delete from time index")?;
-            }
+        if let Some(old_bytes) = self.db.remove(key.as_bytes())? {
+            let old_report = deserialize_report(&old_bytes)?;
+            self.remove_indexes(key, &old_report)?;
         }
-
-        self.db.flush().context("Failed to flush database")?;
         Ok(())
     }
 
-    /// Count all entries in the primary tree.
+    /// List up to `limit` track reports (pass `0` for no limit),
+    /// ordered chronologically (oldest first, most recent at the bottom).
     ///
     /// # 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.
+    /// Returns an error if the database operation fails.
     pub fn list(&self, limit: usize) -> Result<Vec<(String, TrackReport)>> {
-        let effective_limit = if limit == 0 { usize::MAX } else { limit };
-        let mut results = Vec::new();
+        let mut candidates: Vec<(String, i64)> = Vec::new();
 
-        // Iterate time index in reverse to collect the most recent entries first,
-        // then reverse so newest prints at the bottom.
-        for item in self.time_tree.iter().rev() {
-            if results.len() >= effective_limit {
-                break;
-            }
+        for item in &self.time_tree {
             let (idx_key, _) = item.context("Failed to read time index")?;
-            // Primary key starts after 8-byte timestamp + 1-byte separator.
             let primary_key_bytes = &idx_key[TIME_KEY_LEN + 1..];
             let primary_key = String::from_utf8(primary_key_bytes.to_vec())
                 .context("Invalid UTF-8 in primary key")?;
+            let ts = i64::from_be_bytes(idx_key[..TIME_KEY_LEN].try_into().unwrap_or([0; 8]));
+            candidates.push((primary_key, ts));
+        }
 
+        self.resolve_candidates(candidates, limit)
+    }
+
+    /// Count the total number of entries in the database.
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if the database operation fails.
+    pub fn count(&self) -> Result<usize> {
+        Ok(self.db.len()?)
+    }
+
+    /// Resolve a list of (`primary_key`, timestamp) candidates into report entries.
+    ///
+    /// Sorts by timestamp descending, truncates to `limit`, and looks up each
+    /// primary key in the default tree. Returns results in reverse (oldest-first)
+    /// order so that most recently added entries appear at the bottom.
+    fn resolve_candidates(
+        &self,
+        mut candidates: Vec<(String, i64)>,
+        limit: usize,
+    ) -> Result<Vec<(String, TrackReport)>> {
+        let lim = effective_limit(limit);
+        candidates.sort_by_key(|b| Reverse(b.1));
+        candidates.truncate(lim);
+
+        let mut results = Vec::with_capacity(candidates.len());
+        for (primary_key, _) in candidates {
             if let Some(report) = self.get(&primary_key)? {
                 results.push((primary_key, report));
             }
         }
-
-        // Fallback: entries without a time index (migrated from old format)
-        if results.len() < effective_limit {
-            for item in self.db.iter() {
-                if results.len() >= effective_limit {
-                    break;
-                }
-                let (key, value) = item.context("Failed to read iterator entry")?;
-                let report: TrackReport =
-                    serde_json::from_slice(&value).context("Failed to deserialize track report")?;
-                if report.timestamp.is_some() {
-                    continue; // already included via time index
-                }
-                let key_str =
-                    String::from_utf8(key.to_vec()).context("Invalid UTF-8 key in database")?;
-                results.push((key_str, report));
-            }
-        }
-
         results.reverse();
         Ok(results)
     }
@@ -311,64 +308,52 @@ impl LibraryDb {
     ///
     /// # Errors
     ///
-    /// Returns an error if the index scan or deserialization fails.
+    /// Returns an error if the database operation fails.
     pub fn find_by_artist(&self, prefix: &str, limit: usize) -> Result<Vec<(String, TrackReport)>> {
         let prefix_lower = prefix.to_lowercase();
-        let mut results = Vec::new();
+        let mut candidates: Vec<(String, i64)> = Vec::new();
 
         for item in self.artist_tree.scan_prefix(prefix_lower.as_bytes()) {
-            let (idx_key, _) = item.context("Failed to read artist 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")?;
-
-            if let Some(report) = self.get(&primary_key)? {
-                results.push((primary_key, report));
-            }
+            let (idx_key, value) = item.context("Failed to read artist index")?;
+            let primary_key = extract_primary_key_string(&idx_key)?;
+            let ts = parse_timestamp(&value);
+            candidates.push((primary_key, ts));
         }
 
-        results.sort_by_key(|b| b.1.timestamp);
-        results.truncate(limit);
-        Ok(results)
+        self.resolve_candidates(candidates, limit)
     }
 
     /// Find tracks by key signature prefix (case-insensitive), up to `limit` items.
     ///
     /// # Errors
     ///
-    /// Returns an error if the index scan or deserialization fails.
+    /// Returns an error if the database operation fails.
     pub fn find_by_key(&self, prefix: &str, limit: usize) -> Result<Vec<(String, TrackReport)>> {
         let prefix_lower = prefix.to_lowercase();
-        let mut results = Vec::new();
+        let mut candidates: Vec<(String, i64)> = 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 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")?;
-
-            if let Some(report) = self.get(&primary_key)? {
-                results.push((primary_key, report));
-            }
+            let (idx_key, value) = item.context("Failed to read key index")?;
+            let primary_key = extract_primary_key_string(&idx_key)?;
+            let ts = parse_timestamp(&value);
+            candidates.push((primary_key, ts));
         }
 
-        results.sort_by_key(|b| b.1.timestamp);
-        results.truncate(limit);
-        Ok(results)
+        self.resolve_candidates(candidates, limit)
     }
 
     /// Find tracks with BPM in the given range (inclusive), up to `limit` items.
     ///
     /// # Errors
     ///
-    /// Returns an error if the index scan or deserialization fails.
+    /// Returns an error if the database operation fails.
     pub fn find_by_bpm_range(
         &self,
         min: f64,
         max: f64,
         limit: usize,
     ) -> Result<Vec<(String, TrackReport)>> {
-        let mut results = Vec::new();
+        let mut candidates: Vec<(String, i64)> = Vec::new();
 
         // Sled sorts longer keys after their prefix, so a bare 8-byte bound would
         // exclude keys like [max_be, 0x00, primary_key].
@@ -380,19 +365,67 @@ impl LibraryDb {
             .bpm_tree
             .range(min.to_be_bytes().as_slice()..upper.as_slice())
         {
-            let (idx_key, _) = item.context("Failed to read bpm index")?;
-            // Primary key starts after 8-byte BPM + 1-byte separator.
+            let (idx_key, value) = item.context("Failed to read bpm index")?;
             let primary_key_bytes = &idx_key[BPM_KEY_LEN + 1..];
             let primary_key = String::from_utf8(primary_key_bytes.to_vec())
                 .context("Invalid UTF-8 in primary key")?;
+            let ts = parse_timestamp(&value);
+            candidates.push((primary_key, ts));
+        }
 
-            if let Some(report) = self.get(&primary_key)? {
-                results.push((primary_key, report));
+        self.resolve_candidates(candidates, limit)
+    }
+
+    /// Search tracks by regex pattern.
+    ///
+    /// Checks the primary key first (cheap, no deserialization), then
+    /// falls back to checking individual report fields. Returns up to
+    /// `limit` matches (pass `0` for no limit).
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if the database operation fails.
+    pub fn search(&self, pattern: &Regex, limit: usize) -> Result<Vec<(String, TrackReport)>> {
+        let lim = effective_limit(limit);
+        let mut results = Vec::new();
+
+        for item in &self.db {
+            let (key, value) = item.context("Failed to read entry")?;
+            let key_str = String::from_utf8(key.to_vec()).context("Invalid UTF-8 key")?;
+
+            let report = deserialize_report(&value)?;
+
+            // Check the primary key and individual fields — avoids
+            // allocating a display String for every non-matching entry.
+            let matches = pattern.is_match(&key_str)
+                || report
+                    .artist
+                    .as_deref()
+                    .is_some_and(|a| pattern.is_match(a))
+                || report.track.as_deref().is_some_and(|t| pattern.is_match(t))
+                || report.key.as_deref().is_some_and(|k| pattern.is_match(k))
+                || report
+                    .bpm
+                    .is_some_and(|b| pattern.is_match(&format!("{b:.1}")));
+
+            if matches {
+                results.push((key_str, report));
+                if results.len() >= lim {
+                    break;
+                }
             }
         }
 
-        results.sort_by_key(|b| b.1.timestamp);
-        results.truncate(limit);
         Ok(results)
     }
+
+    /// Flush pending writes to disk.
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if the database operation fails.
+    pub fn flush(&self) -> Result<()> {
+        self.db.flush()?;
+        Ok(())
+    }
 }
blob - f4d3fc2882466c2d6f097ff5c04968059eeced02
blob + ab6cacd972f1d754a4db29e559f1ed3eac4f23c2
--- src/file_io.rs
+++ src/file_io.rs
@@ -13,18 +13,14 @@ use bliss_audio::decoder::ffmpeg::FFmpegDecoder as Dec
 #[derive(Debug, Default)]
 pub struct FileMetadata {
     pub artist: Option<String>,
-    pub album: Option<String>,
     pub track: Option<String>,
-    pub label: Option<String>,
 }
 
 fn build_track_info(path: &Path, metadata: FileMetadata, bpm: f32, key: String) -> TrackInfo {
     TrackInfo {
         path: path.to_path_buf(),
         artist: metadata.artist,
-        album: metadata.album,
         track: metadata.track,
-        label: metadata.label,
         bpm,
         key,
     }
@@ -78,9 +74,7 @@ fn read_tags(path: &Path) -> (FileMetadata, Option<(f3
                     .get_string(ItemKey::TrackArtist)
                     .or_else(|| t.get_string(ItemKey::AlbumArtist))
                     .map(String::from),
-                album: t.get_string(ItemKey::AlbumTitle).map(String::from),
                 track: t.get_string(ItemKey::TrackTitle).map(String::from),
-                label: t.get_string(ItemKey::Label).map(String::from),
             };
 
             let existing_tags = t
blob - 3ca728eb5d5ef55a709dd874305dc6af3ffaaa97
blob + b0de3cc147ce4b0855d5d2304e4f8bb98845f853
--- src/import.rs
+++ src/import.rs
@@ -2,6 +2,7 @@ use anyhow::{Context, Result};
 use rusqlite::Connection;
 
 use crate::db::{LibraryDb, TrackReport};
+use crate::utils::format_primary_key;
 
 /// 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
@@ -46,29 +47,31 @@ pub fn import_mixxx(db: &LibraryDb, mixxx_path: &str) 
         Ok((artist, title, bpm, key_raw))
     })?;
 
-    let mut count = 0usize;
+    let mut entries = Vec::new();
     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 primary_key = format_primary_key(artist.as_deref(), title.as_deref());
 
         let report = TrackReport {
             artist,
             track: title,
             bpm,
             key,
-            timestamp: None,
+            timestamp: Some(
+                std::time::SystemTime::now()
+                    .duration_since(std::time::UNIX_EPOCH)
+                    .unwrap_or_default()
+                    .as_secs()
+                    .cast_signed(),
+            ),
         };
 
-        db.save(&primary_key, &report)?;
-        count += 1;
+        entries.push((primary_key, report));
     }
 
-    db.flush()?;
+    let count = entries.len();
+    db.save_batch(&entries)?;
     Ok(count)
 }
blob - bc9f2d62243103459fde2f9773c1e53cb168a380
blob + fec00169505d4587a5086a3162857ce9835469ae
--- src/main.rs
+++ src/main.rs
@@ -18,19 +18,10 @@ 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};
+use crate::utils::{format_primary_key, is_supported_audio_file, print_entries};
 
-#[allow(clippy::too_many_lines)]
-fn main() -> Result<()> {
-    if std::env::args().len() == 1 {
-        let _ = Command::new("virittaa")
-            .version(clap::crate_version!())
-            .about("Analyze audio files to detect BPM and Key")
-            .print_help();
-        return Ok(());
-    }
-
-    let matches = Command::new("virittaa")
+fn build_cli() -> Command {
+    Command::new("virittaa")
         .version(clap::crate_version!())
         .about("Analyze audio files to detect BPM and Key")
         .arg(
@@ -39,13 +30,19 @@ fn main() -> Result<()> {
                 .help("Files or directories to analyze"),
         )
         .arg(
-            Arg::new("write")
-                .short('w')
-                .long("write")
+            Arg::new("write-tags")
+                .short('t')
+                .long("write-tags")
                 .action(clap::ArgAction::SetTrue)
                 .help("Write detected BPM and Key to file metadata"),
         )
         .arg(
+            Arg::new("no-store")
+                .long("no-store")
+                .action(clap::ArgAction::SetTrue)
+                .help("Skip saving track reports to the database"),
+        )
+        .arg(
             Arg::new("force")
                 .short('f')
                 .long("force")
@@ -61,13 +58,6 @@ fn main() -> Result<()> {
                 .value_parser(clap::value_parser!(usize)),
         )
         .arg(
-            Arg::new("save")
-                .short('S')
-                .long("save")
-                .action(clap::ArgAction::SetTrue)
-                .help("Save track reports to the database"),
-        )
-        .arg(
             Arg::new("list")
                 .short('l')
                 .long("list")
@@ -88,7 +78,7 @@ fn main() -> Result<()> {
         )
         .arg(
             Arg::new("search")
-                .short('s')
+                .short('S')
                 .long("search")
                 .help("Search track reports by regex"),
         )
@@ -119,12 +109,34 @@ fn main() -> Result<()> {
                 .long("db-path")
                 .help("Database directory (default: ~/.local/share/virittaa)"),
         )
-        .get_matches();
+}
 
-    let write_tags = matches.get_flag("write");
+#[allow(clippy::too_many_lines)]
+fn main() -> Result<()> {
+    // Sled's B-tree operations can overflow the default stack on large databases.
+    std::thread::Builder::new()
+        .stack_size(32 * 1024 * 1024)
+        .spawn(real_main)
+        .expect("Failed to spawn main thread")
+        .join()
+        .expect("Main thread panicked")
+}
+
+#[allow(clippy::too_many_lines)]
+fn real_main() -> Result<()> {
+    let mut cmd = build_cli();
+
+    if std::env::args().len() == 1 {
+        let _ = cmd.print_help();
+        return Ok(());
+    }
+
+    let matches = cmd.get_matches();
+
+    let write_tags = matches.get_flag("write-tags");
+    let no_store = matches.get_flag("no-store");
     let force = matches.get_flag("force");
     let jobs = *matches.get_one::<usize>("jobs").unwrap();
-    let save_to_db = matches.get_flag("save");
     let list_db = matches.get_flag("list");
     let query_key = matches.get_one::<String>("query").map(String::as_str);
     let search_pattern = matches.get_one::<String>("search");
@@ -145,14 +157,17 @@ fn main() -> Result<()> {
         }
         None => None,
     };
-    let db_path = if let Some(p) = matches.get_one::<String>("db-path") {
-        p.clone()
-    } else {
-        let mut dir = dirs::data_local_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
-        dir.push("virittaa");
-        dir.to_string_lossy().to_string()
-    };
 
+    let db_path = matches.get_one::<String>("db-path").map_or_else(
+        || {
+            dirs::data_dir().map_or_else(
+                || "~/.local/share/virittaa".to_string(),
+                |p| p.join("virittaa").to_string_lossy().to_string(),
+            )
+        },
+        String::from,
+    );
+
     let is_db_query = list_db
         || query_key.is_some()
         || search_pattern.is_some()
@@ -163,7 +178,6 @@ fn main() -> Result<()> {
 
     if is_db_query {
         let db = LibraryDb::open(std::path::Path::new(&db_path))?;
-        let effective_limit = if limit == 0 { usize::MAX } else { limit };
 
         if let Some(mixxx_path) = import_mixxx_path {
             let count = import_mixxx(&db, mixxx_path)?;
@@ -177,21 +191,21 @@ fn main() -> Result<()> {
                 None => eprintln!("No track found for key: {query_key}"),
             }
         } else if let Some(prefix) = artist_prefix {
-            let entries = db.find_by_artist(prefix, effective_limit)?;
+            let entries = db.find_by_artist(prefix, limit)?;
             print_entries(
                 &entries,
                 &format!("No tracks found for artist prefix: {prefix}"),
                 "track(s) found.",
             );
         } else if let Some(prefix) = key_prefix {
-            let entries = db.find_by_key(prefix, effective_limit)?;
+            let entries = db.find_by_key(prefix, limit)?;
             print_entries(
                 &entries,
                 &format!("No tracks found for key prefix: {prefix}"),
                 "track(s) found.",
             );
         } else if let Some((min, max)) = bpm_range {
-            let entries = db.find_by_bpm_range(min, max, effective_limit)?;
+            let entries = db.find_by_bpm_range(min, max, limit)?;
             print_entries(
                 &entries,
                 &format!("No tracks found with BPM between {min} and {max}"),
@@ -199,19 +213,14 @@ fn main() -> Result<()> {
             );
         } else if let Some(pattern) = search_pattern {
             let re = Regex::new(pattern)?;
-            let entries = db.list(0)?; // search all entries regardless of limit
-            let matches_found: Vec<_> = entries
-                .into_iter()
-                .filter(|(key, report)| re.is_match(key) || re.is_match(&report.to_string()))
-                .take(effective_limit)
-                .collect();
+            let entries = db.search(&re, limit)?;
             print_entries(
-                &matches_found,
+                &entries,
                 &format!("No matches for: {pattern}"),
                 "match(es) found.",
             );
         } else {
-            let entries = db.list(effective_limit)?;
+            let entries = db.list(limit)?;
             let total = db.count()?;
             if entries.is_empty() {
                 println!("No track reports found in database.");
@@ -246,10 +255,10 @@ fn main() -> Result<()> {
             .expect("Failed to build thread pool");
     }
 
-    let report_db = if save_to_db {
-        Some(LibraryDb::open(std::path::Path::new(&db_path))?)
-    } else {
+    let report_db = if no_store {
         None
+    } else {
+        Some(LibraryDb::open(std::path::Path::new(&db_path))?)
     };
 
     let files_to_process: Vec<PathBuf> = paths
@@ -291,26 +300,33 @@ fn main() -> Result<()> {
     }
 
     if let Some(db) = &report_db {
-        for track in &results {
-            let key = format!(
-                "{} - {}",
-                track.artist.as_deref().unwrap_or("Unknown"),
-                track.track.as_deref().unwrap_or("Unknown")
-            );
-            let report = TrackReport {
-                artist: track.artist.clone(),
-                track: track.track.clone(),
-                bpm: Some(f64::from(track.bpm)),
-                key: Some(track.key.clone()),
-                timestamp: None,
-            };
-            if let Err(e) = db.save(&key, &report) {
-                eprintln!("Failed to save report for {}: {e:?}", track.path.display());
-            } else {
-                println!("  Saved: {report}");
-            }
-        }
+        let entries: Vec<(String, TrackReport)> = results
+            .iter()
+            .map(|track| {
+                let key = format_primary_key(track.artist.as_deref(), track.track.as_deref());
+                let report = TrackReport {
+                    artist: track.artist.clone(),
+                    track: track.track.clone(),
+                    bpm: Some(f64::from(track.bpm)),
+                    key: Some(track.key.clone()),
+                    timestamp: Some(
+                        std::time::SystemTime::now()
+                            .duration_since(std::time::UNIX_EPOCH)
+                            .unwrap_or_default()
+                            .as_secs()
+                            .cast_signed(),
+                    ),
+                };
+                (key, report)
+            })
+            .collect();
+
+        db.save_batch(&entries)?;
         db.flush()?;
+
+        for report in entries.iter().map(|(_, r)| r) {
+            println!("  Saved: {report}");
+        }
     }
 
     Ok(())
blob - d7ed62b41db7a397763a2eaa45b883040fdd72aa
blob + 559df748edf86a1561aa2df0a2f074f3f3e45cee
--- src/types.rs
+++ src/types.rs
@@ -4,9 +4,7 @@ use std::path::PathBuf;
 pub struct TrackInfo {
     pub path: PathBuf,
     pub artist: Option<String>,
-    pub album: Option<String>,
     pub track: Option<String>,
-    pub label: Option<String>,
     pub bpm: f32,
     pub key: String,
 }
blob - c7099a21de605756414c62a40ac81b2560955821
blob + d542730063178e9b07b4ef555354f4ad40939588
--- src/utils.rs
+++ src/utils.rs
@@ -22,3 +22,13 @@ pub fn print_entries(entries: &[(String, TrackReport)]
         println!("\n{} {count_suffix}", entries.len());
     }
 }
+
+/// Format a primary database key from artist and track name.
+#[must_use]
+pub fn format_primary_key(artist: Option<&str>, track: Option<&str>) -> String {
+    format!(
+        "{} - {}",
+        artist.unwrap_or("Unknown"),
+        track.unwrap_or("Unknown")
+    )
+}