Commit Diff


commit - b15c402ae352767c978b2637ab0b152054a6df38
commit + e75cff3b66887e210a690a05183719b9ff2ce0bc
blob - 56c4b9ae61fb38648d400808585bc78e6be03e02
blob + 4fc13f762afc500c8ce8c7391baefa908d63afbe
--- Cargo.lock
+++ Cargo.lock
@@ -24,15 +24,6 @@ dependencies = [
 ]
 
 [[package]]
-name = "android_system_properties"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
-dependencies = [
- "libc",
-]
-
-[[package]]
 name = "anes"
 version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -240,19 +231,6 @@ source = "registry+https://github.com/rust-lang/crates
 checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
 
 [[package]]
-name = "chrono"
-version = "0.4.44"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
-dependencies = [
- "iana-time-zone",
- "js-sys",
- "num-traits",
- "wasm-bindgen",
- "windows-link",
-]
-
-[[package]]
 name = "ciborium"
 version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -324,12 +302,6 @@ source = "registry+https://github.com/rust-lang/crates
 checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
 
 [[package]]
-name = "core-foundation-sys"
-version = "0.8.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
-
-[[package]]
 name = "crc32fast"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -406,27 +378,6 @@ source = "registry+https://github.com/rust-lang/crates
 checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
 
 [[package]]
-name = "csv"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"
-dependencies = [
- "csv-core",
- "itoa",
- "ryu",
- "serde_core",
-]
-
-[[package]]
-name = "csv-core"
-version = "0.1.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
-dependencies = [
- "memchr",
-]
-
-[[package]]
 name = "data-encoding"
 version = "2.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -674,30 +625,6 @@ dependencies = [
 ]
 
 [[package]]
-name = "iana-time-zone"
-version = "0.1.65"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
-dependencies = [
- "android_system_properties",
- "core-foundation-sys",
- "iana-time-zone-haiku",
- "js-sys",
- "log",
- "wasm-bindgen",
- "windows-core",
-]
-
-[[package]]
-name = "iana-time-zone-haiku"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
-dependencies = [
- "cc",
-]
-
-[[package]]
 name = "indexmap"
 version = "2.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1373,12 +1300,6 @@ source = "registry+https://github.com/rust-lang/crates
 checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
 
 [[package]]
-name = "ryu"
-version = "1.0.23"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
-
-[[package]]
 name = "same-file"
 version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1605,9 +1526,7 @@ dependencies = [
  "anyhow",
  "bliss-audio",
  "bliss-audio-aubio-rs",
- "chrono",
  "clap",
- "csv",
  "dirs",
  "lofty",
  "ndarray 0.17.2",
@@ -1744,65 +1663,12 @@ source = "registry+https://github.com/rust-lang/crates
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
 [[package]]
-name = "windows-core"
-version = "0.62.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
-dependencies = [
- "windows-implement",
- "windows-interface",
- "windows-link",
- "windows-result",
- "windows-strings",
-]
-
-[[package]]
-name = "windows-implement"
-version = "0.60.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.117",
-]
-
-[[package]]
-name = "windows-interface"
-version = "0.59.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.117",
-]
-
-[[package]]
 name = "windows-link"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
 
 [[package]]
-name = "windows-result"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
-dependencies = [
- "windows-link",
-]
-
-[[package]]
-name = "windows-strings"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
-dependencies = [
- "windows-link",
-]
-
-[[package]]
 name = "windows-sys"
 version = "0.59.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
blob - 3f9bd4b95612d866047e0744f307fbc3562fd376
blob + eb387a944d8c7b492e4bdec130626a6a5420cf94
--- Cargo.toml
+++ Cargo.toml
@@ -12,8 +12,6 @@ clap = { version = "4.5.35", features = ["cargo"] }
 walkdir = "2.5"
 lofty = "0.24.0"
 rayon = "1.10"
-chrono = "0.4.43"
-csv = "1.3"
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 sled = "0.34"
blob - dd46e4d8bd9d8792474ed4f761f41d069c9a09cf
blob + 959fd8f2aeed514cc84b055385ed2d1411a8b0b8
--- src/db.rs
+++ src/db.rs
@@ -93,9 +93,7 @@ impl LibraryDb {
             .context("Failed to open artist tree")?;
         let key_tree = db.open_tree("key").context("Failed to open key tree")?;
         let bpm_tree = db.open_tree("bpm").context("Failed to open bpm tree")?;
-        let time_tree = db
-            .open_tree("time")
-            .context("Failed to open time tree")?;
+        let time_tree = db.open_tree("time").context("Failed to open time tree")?;
         Ok(Self {
             db,
             artist_tree,
@@ -111,18 +109,6 @@ impl LibraryDb {
     ///
     /// Returns an error if serialization or any database write fails.
     pub fn save(&self, key: &str, report: &TrackReport) -> Result<()> {
-        // Remove old time index entry if re-saving an existing record
-        let old = self.db.get(key).context("Failed to read 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")?;
-        }
-
         #[allow(clippy::cast_possible_wrap)]
         let now = std::time::SystemTime::now()
             .duration_since(std::time::UNIX_EPOCH)
@@ -139,10 +125,22 @@ impl LibraryDb {
 
         let value = serde_json::to_vec(&report).context("Failed to serialize track report")?;
 
-        self.db
+        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());
@@ -288,27 +286,18 @@ impl LibraryDb {
 
         // Fallback: entries without a time index (migrated from old format)
         if results.len() < effective_limit {
-            let time_keys: Vec<_> = self.time_tree.iter().collect::<Result<Vec<_>, _>>().unwrap_or_default();
-            let indexed_keys: std::collections::HashSet<Vec<u8>> = time_keys
-                .iter()
-                .filter_map(|(k, _)| {
-                    let pos = k.iter().position(|&b| b == SEP)?;
-                    Some(k[pos + 1..].to_vec())
-                })
-                .collect();
-
             for item in self.db.iter() {
                 if results.len() >= effective_limit {
                     break;
                 }
                 let (key, value) = item.context("Failed to read iterator entry")?;
-                if indexed_keys.contains(&key.to_vec()) {
-                    continue;
+                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")?;
-                let report: TrackReport =
-                    serde_json::from_slice(&value).context("Failed to deserialize track report")?;
                 results.push((key_str, report));
             }
         }
blob - afe479fb9723f92bbf9dc437dddd4ae64fbf9871
blob + f4d3fc2882466c2d6f097ff5c04968059eeced02
--- src/file_io.rs
+++ src/file_io.rs
@@ -1,14 +1,12 @@
 use anyhow::Result;
-use chrono::Local;
 use lofty::config::WriteOptions;
 use lofty::prelude::*;
 use lofty::probe::Probe;
 use lofty::tag::{ItemKey, Tag};
-use std::borrow::Cow;
 use std::path::Path;
 
 use crate::audio_processing::{calculate_bpm, calculate_key};
-use crate::types::{TrackError, TrackInfo};
+use crate::types::TrackInfo;
 use bliss_audio::decoder::Decoder as DecoderTrait;
 use bliss_audio::decoder::ffmpeg::FFmpegDecoder as Decoder;
 
@@ -101,65 +99,6 @@ fn read_tags(path: &Path) -> (FileMetadata, Option<(f3
     }
 }
 
-#[derive(serde::Serialize)]
-#[serde(rename_all = "PascalCase")]
-struct ReportRecord<'a> {
-    path: Cow<'a, str>,
-    artist: Option<&'a str>,
-    album: Option<&'a str>,
-    track: Option<&'a str>,
-    label: Option<&'a str>,
-    #[serde(rename = "BPM")]
-    bpm: Option<String>,
-    key: Option<&'a str>,
-    status: &'a str,
-    error: Option<&'a str>,
-}
-
-/// Write a CSV report of processed tracks.
-///
-/// # Errors
-///
-/// 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");
-
-    let mut wtr = csv::Writer::from_path(&filename)?;
-
-    for track in results {
-        wtr.serialize(ReportRecord {
-            path: track.path.to_string_lossy(),
-            artist: track.artist.as_deref(),
-            album: track.album.as_deref(),
-            track: track.track.as_deref(),
-            label: track.label.as_deref(),
-            bpm: Some(format!("{:.1}", track.bpm)),
-            key: Some(&track.key),
-            status: "Success",
-            error: None,
-        })?;
-    }
-
-    for error in errors {
-        wtr.serialize(ReportRecord {
-            path: error.path.to_string_lossy(),
-            artist: None,
-            album: None,
-            track: None,
-            label: None,
-            bpm: None,
-            key: None,
-            status: "Error",
-            error: Some(&error.reason),
-        })?;
-    }
-
-    wtr.flush()?;
-    println!("Report written to: {filename}");
-    Ok(())
-}
-
 /// Write BPM and key metadata to a file.
 ///
 /// # Errors
blob - c57de6b84c10115bd1f73464b15b58df52a13195
blob + bc9f2d62243103459fde2f9773c1e53cb168a380
--- src/main.rs
+++ src/main.rs
@@ -6,7 +6,7 @@ pub mod import;
 pub mod types;
 pub mod utils;
 
-use anyhow::Result;
+use anyhow::{Context, Result};
 use clap::{Arg, Command};
 use rayon::prelude::*;
 use regex::Regex;
@@ -132,12 +132,19 @@ fn main() -> Result<()> {
     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");
-        let max = v[1].parse::<f64>().expect("Invalid BPM max value");
-        (min, max)
-    });
+    let bpm_range: Option<(f64, f64)> = match matches.get_many::<String>("bpm") {
+        Some(vals) => {
+            let v: Vec<&String> = vals.collect();
+            let min = v[0]
+                .parse::<f64>()
+                .with_context(|| format!("Invalid BPM min value: {}", v[0]))?;
+            let max = v[1]
+                .parse::<f64>()
+                .with_context(|| format!("Invalid BPM max value: {}", v[1]))?;
+            Some((min, max))
+        }
+        None => None,
+    };
     let db_path = if let Some(p) = matches.get_one::<String>("db-path") {
         p.clone()
     } else {
@@ -192,10 +199,11 @@ fn main() -> Result<()> {
             );
         } else if let Some(pattern) = search_pattern {
             let re = Regex::new(pattern)?;
-            let entries = db.list(effective_limit)?;
+            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();
             print_entries(
                 &matches_found,