Commit Diff


commit - ff20cd2d83e62c6e2a0bd5dae0b8c77d3b6128e6
commit + b15c402ae352767c978b2637ab0b152054a6df38
blob - 0e03dee51a4d785712069a253a754c01ba65ed4a
blob + dd46e4d8bd9d8792474ed4f761f41d069c9a09cf
--- src/db.rs
+++ src/db.rs
@@ -10,6 +10,8 @@ pub struct TrackReport {
     pub track: Option<String>,
     pub bpm: Option<f64>,
     pub key: Option<String>,
+    #[serde(default)]
+    pub timestamp: Option<i64>,
 }
 
 impl fmt::Display for TrackReport {
@@ -32,15 +34,18 @@ impl fmt::Display for TrackReport {
 /// - `"artist"` tree: `"artist\0primary_key"` → empty
 /// - `"key"` tree: `"key\0primary_key"` → empty
 /// - `"bpm"` tree: `f64_be_bytes + "\0" + primary_key` → empty
+/// - `"time"` tree: `i64_be_bytes(timestamp) + "\0" + primary_key` → empty
 pub struct LibraryDb {
     db: sled::Db,
     artist_tree: Tree,
     key_tree: Tree,
     bpm_tree: Tree,
+    time_tree: Tree,
 }
 
 const SEP: u8 = 0x00;
 const BPM_KEY_LEN: usize = 8; // f64 big-endian
+const TIME_KEY_LEN: usize = 8; // i64 big-endian
 
 fn prefix_index_key(value: &[u8], primary_key: &[u8]) -> Vec<u8> {
     let mut buf = Vec::with_capacity(value.len() + 1 + primary_key.len());
@@ -58,6 +63,14 @@ fn bpm_index_key(bpm: f64, primary_key: &[u8]) -> Vec<
     buf
 }
 
+fn time_index_key(timestamp: i64, primary_key: &[u8]) -> Vec<u8> {
+    let mut buf = Vec::with_capacity(TIME_KEY_LEN + 1 + primary_key.len());
+    buf.extend_from_slice(&timestamp.to_be_bytes());
+    buf.push(SEP);
+    buf.extend_from_slice(primary_key);
+    buf
+}
+
 fn extract_primary_key(idx_key: &[u8]) -> Result<&[u8]> {
     let pos = idx_key
         .iter()
@@ -80,11 +93,15 @@ 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")?;
         Ok(Self {
             db,
             artist_tree,
             key_tree,
             bpm_tree,
+            time_tree,
         })
     }
 
@@ -94,8 +111,34 @@ impl LibraryDb {
     ///
     /// 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")?;
+        // 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)
+            .unwrap_or_default()
+            .as_secs() as i64;
+
+        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")?;
+
         self.db
             .insert(key.as_bytes(), value)
             .context("Failed to write to database")?;
@@ -124,6 +167,12 @@ impl LibraryDb {
                 .context("Failed to write bpm index")?;
         }
 
+        // Time index
+        let time_key = time_index_key(now, key.as_bytes());
+        self.time_tree
+            .insert(&time_key, &[][..])
+            .context("Failed to write time index")?;
+
         Ok(())
     }
 
@@ -190,6 +239,13 @@ impl LibraryDb {
                     .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")?;
+            }
         }
 
         self.db.flush().context("Failed to flush database")?;
@@ -211,15 +267,52 @@ impl LibraryDb {
     ///
     /// Returns an error if the database iteration or deserialization fails.
     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")?;
-            let report: TrackReport =
-                serde_json::from_slice(&value).context("Failed to deserialize track report")?;
-            results.push((key_str, report));
+        let effective_limit = if limit == 0 { usize::MAX } else { limit };
+        let mut results = Vec::new();
+
+        // Iterate time index in reverse (most recent first)
+        for item in self.time_tree.iter().rev() {
+            if results.len() >= effective_limit {
+                break;
+            }
+            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")?;
+
+            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 {
+            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 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));
+            }
+        }
+
         Ok(results)
     }
 
@@ -233,9 +326,6 @@ impl LibraryDb {
         let mut results = Vec::new();
 
         for item in self.artist_tree.scan_prefix(prefix_lower.as_bytes()) {
-            if results.len() >= limit {
-                break;
-            }
             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())
@@ -246,6 +336,8 @@ impl LibraryDb {
             }
         }
 
+        results.sort_by_key(|b| std::cmp::Reverse(b.1.timestamp));
+        results.truncate(limit);
         Ok(results)
     }
 
@@ -259,9 +351,6 @@ impl LibraryDb {
         let mut results = Vec::new();
 
         for item in self.key_tree.scan_prefix(prefix_lower.as_bytes()) {
-            if results.len() >= limit {
-                break;
-            }
             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())
@@ -272,6 +361,8 @@ impl LibraryDb {
             }
         }
 
+        results.sort_by_key(|b| std::cmp::Reverse(b.1.timestamp));
+        results.truncate(limit);
         Ok(results)
     }
 
@@ -298,9 +389,6 @@ impl LibraryDb {
             .bpm_tree
             .range(min.to_be_bytes().as_slice()..upper.as_slice())
         {
-            if results.len() >= limit {
-                break;
-            }
             let (idx_key, _) = item.context("Failed to read bpm index")?;
             // Primary key starts after 8-byte BPM + 1-byte separator.
             let primary_key_bytes = &idx_key[BPM_KEY_LEN + 1..];
@@ -312,6 +400,8 @@ impl LibraryDb {
             }
         }
 
+        results.sort_by_key(|b| std::cmp::Reverse(b.1.timestamp));
+        results.truncate(limit);
         Ok(results)
     }
 }
blob - 12d9f8b707ccb8ef10506661f2e9e4d48c50d60c
blob + 3ca728eb5d5ef55a709dd874305dc6af3ffaaa97
--- src/import.rs
+++ src/import.rs
@@ -62,6 +62,7 @@ pub fn import_mixxx(db: &LibraryDb, mixxx_path: &str) 
             track: title,
             bpm,
             key,
+            timestamp: None,
         };
 
         db.save(&primary_key, &report)?;
blob - 31c72cec257e8f0347a67a35a95a4b52f6ed786d
blob + c57de6b84c10115bd1f73464b15b58df52a13195
--- src/main.rs
+++ src/main.rs
@@ -294,6 +294,7 @@ fn main() -> Result<()> {
                 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());