virittaa

audio metadata analyzer
Log | Files | Refs | README | LICENSE

import.rs (5077B)


      1 use anyhow::{Context, Result};
      2 use rusqlite::Connection;
      3 
      4 use crate::db::{LibraryDb, TrackReport};
      5 use crate::utils::format_primary_key;
      6 
      7 /// Convert a Mixxx key string (e.g. "Cm", "D", "Bbm") to virittaa format
      8 /// (e.g. "C Minor", "D Major", "Bb Minor"). Returns `None` for empty or
      9 /// unknown keys.
     10 fn convert_mixxx_key(raw: &str) -> Option<String> {
     11     let raw = raw.trim();
     12     if raw.is_empty() {
     13         return None;
     14     }
     15     // Mixxx minor keys end with 'm': "Cm", "C#m", "Bbm", etc.
     16     if raw.ends_with('m') && raw.len() > 1 {
     17         let root = &raw[..raw.len() - 1];
     18         Some(format!("{root} Minor"))
     19     } else {
     20         Some(format!("{raw} Major"))
     21     }
     22 }
     23 
     24 /// Import tracks from a Mixxx `SQLite` database into the sled library database.
     25 ///
     26 /// Reads the `library` and `track_locations` tables, skipping entries marked as
     27 /// deleted, and saves each track with its BPM and key.
     28 ///
     29 /// # Errors
     30 ///
     31 /// Returns an error if the Mixxx database cannot be opened or queried, or if
     32 /// sled writes fail.
     33 pub fn import_mixxx(db: &LibraryDb, mixxx_path: &str) -> Result<usize> {
     34     let conn = Connection::open(mixxx_path).with_context(|| "Failed to open Mixxx database")?;
     35 
     36     let mut stmt = conn.prepare(
     37         "SELECT l.artist, l.title, l.bpm, l.key \
     38          FROM library l \
     39          WHERE l.mixxx_deleted = 0 AND l.bpm IS NOT NULL AND l.bpm > 0",
     40     )?;
     41 
     42     let rows = stmt.query_map([], |row| {
     43         let artist: Option<String> = row.get(0)?;
     44         let title: Option<String> = row.get(1)?;
     45         let bpm: Option<f64> = row.get(2)?;
     46         let key_raw: Option<String> = row.get(3)?;
     47         Ok((artist, title, bpm, key_raw))
     48     })?;
     49 
     50     let mut entries = Vec::new();
     51     for row in rows {
     52         let (artist, title, bpm, key_raw) = row?;
     53         let key = key_raw.as_deref().and_then(convert_mixxx_key);
     54 
     55         let primary_key = format_primary_key(artist.as_deref(), title.as_deref());
     56 
     57         let report = TrackReport {
     58             artist,
     59             track: title,
     60             bpm,
     61             key,
     62             timestamp: Some(
     63                 std::time::SystemTime::now()
     64                     .duration_since(std::time::UNIX_EPOCH)
     65                     .unwrap_or_default()
     66                     .as_secs()
     67                     .cast_signed(),
     68             ),
     69         };
     70 
     71         entries.push((primary_key, report));
     72     }
     73 
     74     let count = entries.len();
     75     db.save_batch(&entries)?;
     76     Ok(count)
     77 }
     78 
     79 #[cfg(test)]
     80 mod tests {
     81     use super::*;
     82 
     83     #[test]
     84     fn convert_mixxx_major_keys() {
     85         assert_eq!(convert_mixxx_key("D"), Some("D Major".to_string()));
     86         assert_eq!(convert_mixxx_key("F#"), Some("F# Major".to_string()));
     87         assert_eq!(convert_mixxx_key("Bb"), Some("Bb Major".to_string()));
     88     }
     89 
     90     #[test]
     91     fn convert_mixxx_minor_keys() {
     92         assert_eq!(convert_mixxx_key("Cm"), Some("C Minor".to_string()));
     93         assert_eq!(convert_mixxx_key("C#m"), Some("C# Minor".to_string()));
     94         assert_eq!(convert_mixxx_key("Bbm"), Some("Bb Minor".to_string()));
     95     }
     96 
     97     #[test]
     98     fn convert_mixxx_key_trims_and_rejects_empty() {
     99         assert_eq!(convert_mixxx_key(""), None);
    100         assert_eq!(convert_mixxx_key("   "), None);
    101         assert_eq!(convert_mixxx_key("  Am  "), Some("A Minor".to_string()));
    102     }
    103 
    104     fn make_mixxx_db(path: &std::path::Path) {
    105         let conn = Connection::open(path).unwrap();
    106         conn.execute_batch(
    107             "CREATE TABLE library (
    108                  artist TEXT,
    109                  title TEXT,
    110                  bpm REAL,
    111                  key TEXT,
    112                  mixxx_deleted INTEGER
    113              );
    114              INSERT INTO library VALUES ('Aphex Twin', 'Xtal', 120.0, 'Am', 0);
    115              INSERT INTO library VALUES ('Boards of Canada', 'Roygbiv', 95.5, 'C', 0);
    116              -- Deleted rows are skipped.
    117              INSERT INTO library VALUES ('Deleted', 'Track', 100.0, 'D', 1);
    118              -- Rows without a usable BPM are skipped.
    119              INSERT INTO library VALUES ('No', 'Bpm', NULL, 'E', 0);
    120              INSERT INTO library VALUES ('Zero', 'Bpm', 0.0, 'F', 0);",
    121         )
    122         .unwrap();
    123     }
    124 
    125     #[test]
    126     fn import_mixxx_filters_and_converts() {
    127         let dir = tempfile::tempdir().unwrap();
    128         let mixxx_path = dir.path().join("mixxxdb.sqlite");
    129         make_mixxx_db(&mixxx_path);
    130 
    131         let db = LibraryDb::open(&dir.path().join("library")).unwrap();
    132         let count = import_mixxx(&db, mixxx_path.to_str().unwrap()).unwrap();
    133 
    134         // Only the two non-deleted, positive-BPM rows are imported.
    135         assert_eq!(count, 2);
    136         assert_eq!(db.count().unwrap(), 2);
    137 
    138         let xtal = db.get("Aphex Twin - Xtal").unwrap().unwrap();
    139         assert_eq!(xtal.key.as_deref(), Some("A Minor"));
    140         assert_eq!(xtal.bpm, Some(120.0));
    141 
    142         let roygbiv = db.get("Boards of Canada - Roygbiv").unwrap().unwrap();
    143         assert_eq!(roygbiv.key.as_deref(), Some("C Major"));
    144 
    145         assert!(db.get("Deleted - Track").unwrap().is_none());
    146         assert!(db.get("Zero - Bpm").unwrap().is_none());
    147     }
    148 }