From 4f4a4169f8beefdcaa75c2c1248273ed06170ff1 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sun, 17 May 2026 17:27:24 +0200 Subject: [PATCH] feat(cache): update database layer for expanded metadata - Update upsert_file() to include all 26 new AudioMeta fields - Update get_file_by_virtual_path() to read all new columns - Add get_file_metadata_row() for overlay synthesis - Add update_metadata() for partial metadata updates - Add clear_overlay() to reset metadata to NULL - Handle format_layout BLOB with msgpack serialization - Handle custom_tags JSON with serde_json - Add 8 comprehensive unit tests - All 92 tests pass, LSP diagnostics clean --- Cargo.lock | 1 + crates/musicfs-cache/Cargo.toml | 1 + crates/musicfs-cache/src/db.rs | 679 +++++++++++++++++++++++++++++++- 3 files changed, 676 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cba7e9d..10ad624 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1925,6 +1925,7 @@ dependencies = [ "rmp-serde", "rusqlite", "serde", + "serde_json", "sled", "tempfile", "thiserror 1.0.69", diff --git a/crates/musicfs-cache/Cargo.toml b/crates/musicfs-cache/Cargo.toml index d6311c8..5f84330 100644 --- a/crates/musicfs-cache/Cargo.toml +++ b/crates/musicfs-cache/Cargo.toml @@ -14,6 +14,7 @@ tracing.workspace = true thiserror.workspace = true serde.workspace = true rmp-serde.workspace = true +serde_json.workspace = true image.workspace = true lofty = "0.24" parking_lot.workspace = true diff --git a/crates/musicfs-cache/src/db.rs b/crates/musicfs-cache/src/db.rs index e745ff1..cf160b6 100644 --- a/crates/musicfs-cache/src/db.rs +++ b/crates/musicfs-cache/src/db.rs @@ -1,8 +1,10 @@ +use crate::FormatLayout; use musicfs_core::{ AudioFormat, AudioMeta, ContentHash, Error, FileId, FileMeta, OriginId, RealPath, Result, VirtualPath, }; use rusqlite::{params, Connection, OptionalExtension}; +use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -81,6 +83,29 @@ impl Database { audio_meta: &AudioMeta, origin_mtime: SystemTime, origin_size: u64, + ) -> Result { + self.upsert_file_with_layout( + origin_id, + real_path, + virtual_path, + audio_meta, + origin_mtime, + origin_size, + None, + None, + ) + } + + pub fn upsert_file_with_layout( + &self, + origin_id: &OriginId, + real_path: &Path, + virtual_path: &VirtualPath, + audio_meta: &AudioMeta, + origin_mtime: SystemTime, + origin_size: u64, + format_layout: Option<&FormatLayout>, + custom_tags: Option<&HashMap>, ) -> Result { let conn = self.conn.lock().unwrap(); @@ -89,6 +114,18 @@ impl Database { .unwrap_or_default() .as_secs() as i64; + // Serialize format_layout as msgpack BLOB + let format_layout_blob: Option> = format_layout + .map(|fl| rmp_serde::to_vec(fl)) + .transpose() + .map_err(|e| Error::Database(format!("format_layout serialization failed: {}", e)))?; + + // Serialize custom_tags as JSON TEXT + let custom_tags_json: Option = custom_tags + .map(|ct| serde_json::to_string(ct)) + .transpose() + .map_err(|e| Error::Database(format!("custom_tags serialization failed: {}", e)))?; + conn.execute( r#" INSERT INTO files ( @@ -96,13 +133,27 @@ impl Database { title, artist, album, album_artist, genre, year, track, disc, duration_ms, bitrate, sample_rate, format, + track_total, disc_total, date, composer, comment, + lyrics, copyright, compilation, + artist_sort, album_artist_sort, album_sort, title_sort, + mb_recording_id, mb_album_id, mb_artist_id, mb_album_artist_id, mb_release_group_id, + replaygain_track_gain, replaygain_track_peak, replaygain_album_gain, replaygain_album_peak, + channels, bits_per_sample, encoder, + custom_tags, format_layout, origin_mtime, origin_size ) VALUES ( ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, - ?16, ?17 + ?16, ?17, ?18, ?19, ?20, + ?21, ?22, ?23, + ?24, ?25, ?26, ?27, + ?28, ?29, ?30, ?31, ?32, + ?33, ?34, ?35, ?36, + ?37, ?38, ?39, + ?40, ?41, + ?42, ?43 ) ON CONFLICT(origin_id, real_path) DO UPDATE SET virtual_path = excluded.virtual_path, @@ -118,6 +169,32 @@ impl Database { bitrate = excluded.bitrate, sample_rate = excluded.sample_rate, format = excluded.format, + track_total = excluded.track_total, + disc_total = excluded.disc_total, + date = excluded.date, + composer = excluded.composer, + comment = excluded.comment, + lyrics = excluded.lyrics, + copyright = excluded.copyright, + compilation = excluded.compilation, + artist_sort = excluded.artist_sort, + album_artist_sort = excluded.album_artist_sort, + album_sort = excluded.album_sort, + title_sort = excluded.title_sort, + mb_recording_id = excluded.mb_recording_id, + mb_album_id = excluded.mb_album_id, + mb_artist_id = excluded.mb_artist_id, + mb_album_artist_id = excluded.mb_album_artist_id, + mb_release_group_id = excluded.mb_release_group_id, + replaygain_track_gain = excluded.replaygain_track_gain, + replaygain_track_peak = excluded.replaygain_track_peak, + replaygain_album_gain = excluded.replaygain_album_gain, + replaygain_album_peak = excluded.replaygain_album_peak, + channels = excluded.channels, + bits_per_sample = excluded.bits_per_sample, + encoder = excluded.encoder, + custom_tags = excluded.custom_tags, + format_layout = excluded.format_layout, origin_mtime = excluded.origin_mtime, origin_size = excluded.origin_size, last_sync = strftime('%s', 'now') @@ -138,6 +215,32 @@ impl Database { &audio_meta.bitrate, &audio_meta.sample_rate, format!("{:?}", audio_meta.format), + &audio_meta.track_total, + &audio_meta.disc_total, + &audio_meta.date, + &audio_meta.composer, + &audio_meta.comment, + &audio_meta.lyrics, + &audio_meta.copyright, + &audio_meta.compilation.map(|b| if b { 1i32 } else { 0i32 }), + &audio_meta.artist_sort, + &audio_meta.album_artist_sort, + &audio_meta.album_sort, + &audio_meta.title_sort, + &audio_meta.mb_recording_id, + &audio_meta.mb_album_id, + &audio_meta.mb_artist_id, + &audio_meta.mb_album_artist_id, + &audio_meta.mb_release_group_id, + &audio_meta.replaygain_track_gain, + &audio_meta.replaygain_track_peak, + &audio_meta.replaygain_album_gain, + &audio_meta.replaygain_album_peak, + &audio_meta.channels, + &audio_meta.bits_per_sample, + &audio_meta.encoder, + &custom_tags_json, + &format_layout_blob, mtime_secs, origin_size as i64, ], @@ -169,6 +272,12 @@ impl Database { title, artist, album, album_artist, genre, year, track, disc, duration_ms, bitrate, sample_rate, format, + track_total, disc_total, date, composer, comment, + lyrics, copyright, compilation, + artist_sort, album_artist_sort, album_sort, title_sort, + mb_recording_id, mb_album_id, mb_artist_id, mb_album_artist_id, mb_release_group_id, + replaygain_track_gain, replaygain_track_peak, replaygain_album_gain, replaygain_album_peak, + channels, bits_per_sample, encoder, origin_mtime, origin_size, content_hash FROM files WHERE virtual_path = ?1 @@ -181,7 +290,8 @@ impl Database { .map(parse_audio_format) .unwrap_or(AudioFormat::Unknown); - let content_hash: Option = row.get(18)?; + let compilation_int: Option = row.get(23)?; + let content_hash: Option = row.get(42)?; Ok(FileMeta { id: FileId(row.get(0)?), @@ -203,10 +313,33 @@ impl Database { bitrate: row.get(13)?, sample_rate: row.get(14)?, format, - ..Default::default() + track_total: row.get(16)?, + disc_total: row.get(17)?, + date: row.get(18)?, + composer: row.get(19)?, + comment: row.get(20)?, + lyrics: row.get(21)?, + copyright: row.get(22)?, + compilation: compilation_int.map(|i| i != 0), + artist_sort: row.get(24)?, + album_artist_sort: row.get(25)?, + album_sort: row.get(26)?, + title_sort: row.get(27)?, + mb_recording_id: row.get(28)?, + mb_album_id: row.get(29)?, + mb_artist_id: row.get(30)?, + mb_album_artist_id: row.get(31)?, + mb_release_group_id: row.get(32)?, + replaygain_track_gain: row.get(33)?, + replaygain_track_peak: row.get(34)?, + replaygain_album_gain: row.get(35)?, + replaygain_album_peak: row.get(36)?, + channels: row.get(37)?, + bits_per_sample: row.get(38)?, + encoder: row.get(39)?, }), - size: row.get::<_, i64>(17)? as u64, - mtime: UNIX_EPOCH + Duration::from_secs(row.get::<_, i64>(16)? as u64), + size: row.get::<_, i64>(41)? as u64, + mtime: UNIX_EPOCH + Duration::from_secs(row.get::<_, i64>(40)? as u64), content_hash: content_hash.and_then(|s| parse_content_hash(&s)), }) }, @@ -436,6 +569,257 @@ impl Database { .map_err(|e| Error::Database(format!("query failed: {}", e))) } + pub fn get_file_metadata_row(&self, file_id: FileId) -> Result { + let conn = self.conn.lock().unwrap(); + + conn.query_row( + r#" + SELECT title, artist, album, album_artist, genre, + year, track, disc, + duration_ms, bitrate, sample_rate, format, + track_total, disc_total, date, composer, comment, + lyrics, copyright, compilation, + artist_sort, album_artist_sort, album_sort, title_sort, + mb_recording_id, mb_album_id, mb_artist_id, mb_album_artist_id, mb_release_group_id, + replaygain_track_gain, replaygain_track_peak, replaygain_album_gain, replaygain_album_peak, + channels, bits_per_sample, encoder + FROM files + WHERE id = ?1 + "#, + params![file_id.0], + |row| { + let format_str: Option = row.get(11)?; + let format = format_str + .as_deref() + .map(parse_audio_format) + .unwrap_or(AudioFormat::Unknown); + + let compilation_int: Option = row.get(19)?; + + Ok(AudioMeta { + title: row.get(0)?, + artist: row.get(1)?, + album: row.get(2)?, + album_artist: row.get(3)?, + genre: row.get(4)?, + year: row.get(5)?, + track: row.get(6)?, + disc: row.get(7)?, + duration_ms: row.get::<_, Option>(8)?.map(|d| d as u64), + bitrate: row.get(9)?, + sample_rate: row.get(10)?, + format, + track_total: row.get(12)?, + disc_total: row.get(13)?, + date: row.get(14)?, + composer: row.get(15)?, + comment: row.get(16)?, + lyrics: row.get(17)?, + copyright: row.get(18)?, + compilation: compilation_int.map(|i| i != 0), + artist_sort: row.get(20)?, + album_artist_sort: row.get(21)?, + album_sort: row.get(22)?, + title_sort: row.get(23)?, + mb_recording_id: row.get(24)?, + mb_album_id: row.get(25)?, + mb_artist_id: row.get(26)?, + mb_album_artist_id: row.get(27)?, + mb_release_group_id: row.get(28)?, + replaygain_track_gain: row.get(29)?, + replaygain_track_peak: row.get(30)?, + replaygain_album_gain: row.get(31)?, + replaygain_album_peak: row.get(32)?, + channels: row.get(33)?, + bits_per_sample: row.get(34)?, + encoder: row.get(35)?, + }) + }, + ) + .map_err(|e| Error::Database(format!("get_file_metadata_row failed: {}", e))) + } + + pub fn get_format_layout(&self, file_id: FileId) -> Result> { + let conn = self.conn.lock().unwrap(); + + let blob: Option> = conn + .query_row( + "SELECT format_layout FROM files WHERE id = ?1", + params![file_id.0], + |row| row.get(0), + ) + .optional() + .map_err(|e| Error::Database(format!("get_format_layout query failed: {}", e)))? + .flatten(); + + match blob { + Some(data) => { + let layout: FormatLayout = rmp_serde::from_slice(&data).map_err(|e| { + Error::Database(format!("format_layout deserialization failed: {}", e)) + })?; + Ok(Some(layout)) + } + None => Ok(None), + } + } + + pub fn get_custom_tags(&self, file_id: FileId) -> Result>> { + let conn = self.conn.lock().unwrap(); + + let json: Option = conn + .query_row( + "SELECT custom_tags FROM files WHERE id = ?1", + params![file_id.0], + |row| row.get(0), + ) + .optional() + .map_err(|e| Error::Database(format!("get_custom_tags query failed: {}", e)))? + .flatten(); + + match json { + Some(data) => { + let tags: HashMap = serde_json::from_str(&data).map_err(|e| { + Error::Database(format!("custom_tags deserialization failed: {}", e)) + })?; + Ok(Some(tags)) + } + None => Ok(None), + } + } + + pub fn update_metadata(&self, file_id: FileId, metadata: &AudioMeta) -> Result<()> { + let mut updates = Vec::new(); + let mut params_vec: Vec> = Vec::new(); + + macro_rules! add_field { + ($field:ident, $col:literal) => { + if let Some(ref val) = metadata.$field { + updates.push(concat!($col, " = ?")); + params_vec.push(Box::new(val.clone())); + } + }; + ($field:ident, $col:literal, u32) => { + if let Some(val) = metadata.$field { + updates.push(concat!($col, " = ?")); + params_vec.push(Box::new(val as i64)); + } + }; + ($field:ident, $col:literal, u64) => { + if let Some(val) = metadata.$field { + updates.push(concat!($col, " = ?")); + params_vec.push(Box::new(val as i64)); + } + }; + ($field:ident, $col:literal, f32) => { + if let Some(val) = metadata.$field { + updates.push(concat!($col, " = ?")); + params_vec.push(Box::new(val as f64)); + } + }; + ($field:ident, $col:literal, bool) => { + if let Some(val) = metadata.$field { + updates.push(concat!($col, " = ?")); + params_vec.push(Box::new(if val { 1i32 } else { 0i32 })); + } + }; + } + + add_field!(title, "title"); + add_field!(artist, "artist"); + add_field!(album, "album"); + add_field!(album_artist, "album_artist"); + add_field!(genre, "genre"); + add_field!(year, "year", u32); + add_field!(track, "track", u32); + add_field!(disc, "disc", u32); + add_field!(duration_ms, "duration_ms", u64); + add_field!(bitrate, "bitrate", u32); + add_field!(sample_rate, "sample_rate", u32); + add_field!(track_total, "track_total", u32); + add_field!(disc_total, "disc_total", u32); + add_field!(date, "date"); + add_field!(composer, "composer"); + add_field!(comment, "comment"); + add_field!(lyrics, "lyrics"); + add_field!(copyright, "copyright"); + add_field!(compilation, "compilation", bool); + add_field!(artist_sort, "artist_sort"); + add_field!(album_artist_sort, "album_artist_sort"); + add_field!(album_sort, "album_sort"); + add_field!(title_sort, "title_sort"); + add_field!(mb_recording_id, "mb_recording_id"); + add_field!(mb_album_id, "mb_album_id"); + add_field!(mb_artist_id, "mb_artist_id"); + add_field!(mb_album_artist_id, "mb_album_artist_id"); + add_field!(mb_release_group_id, "mb_release_group_id"); + add_field!(replaygain_track_gain, "replaygain_track_gain", f32); + add_field!(replaygain_track_peak, "replaygain_track_peak", f32); + add_field!(replaygain_album_gain, "replaygain_album_gain", f32); + add_field!(replaygain_album_peak, "replaygain_album_peak", f32); + add_field!(channels, "channels", u32); + add_field!(bits_per_sample, "bits_per_sample", u32); + add_field!(encoder, "encoder"); + + if updates.is_empty() { + return Ok(()); + } + + let sql = format!("UPDATE files SET {} WHERE id = ?", updates.join(", ")); + params_vec.push(Box::new(file_id.0)); + + let conn = self.conn.lock().unwrap(); + let params_refs: Vec<&dyn rusqlite::ToSql> = + params_vec.iter().map(|p| p.as_ref()).collect(); + + let rows = conn + .execute(&sql, params_refs.as_slice()) + .map_err(|e| Error::Database(format!("update_metadata failed: {}", e)))?; + + if rows == 0 { + return Err(Error::FileNotFound(format!( + "file id {} not found", + file_id.0 + ))); + } + + debug!(id = file_id.0, fields = updates.len(), "updated metadata"); + Ok(()) + } + + pub fn clear_overlay(&self, file_id: FileId) -> Result<()> { + let conn = self.conn.lock().unwrap(); + + let rows = conn + .execute( + r#" + UPDATE files SET + title = NULL, artist = NULL, album = NULL, album_artist = NULL, genre = NULL, + year = NULL, track = NULL, disc = NULL, + duration_ms = NULL, bitrate = NULL, sample_rate = NULL, format = NULL, + track_total = NULL, disc_total = NULL, date = NULL, composer = NULL, comment = NULL, + lyrics = NULL, copyright = NULL, compilation = NULL, + artist_sort = NULL, album_artist_sort = NULL, album_sort = NULL, title_sort = NULL, + mb_recording_id = NULL, mb_album_id = NULL, mb_artist_id = NULL, mb_album_artist_id = NULL, mb_release_group_id = NULL, + replaygain_track_gain = NULL, replaygain_track_peak = NULL, replaygain_album_gain = NULL, replaygain_album_peak = NULL, + channels = NULL, bits_per_sample = NULL, encoder = NULL, + custom_tags = NULL, format_layout = NULL + WHERE id = ?1 + "#, + params![file_id.0], + ) + .map_err(|e| Error::Database(format!("clear_overlay failed: {}", e)))?; + + if rows == 0 { + return Err(Error::FileNotFound(format!( + "file id {} not found", + file_id.0 + ))); + } + + debug!(id = file_id.0, "cleared overlay metadata"); + Ok(()) + } + pub fn mark_trashed(&self, id: FileId, original_path: &VirtualPath) -> Result<()> { let conn = self.conn.lock().unwrap(); let rows = conn @@ -1047,4 +1431,289 @@ mod tests { assert_eq!(db.list_trashed(&TrashedFilter::default()).unwrap().len(), 0); assert_eq!(db.file_count().unwrap(), 0); } + + #[test] + fn test_new_metadata_fields_roundtrip() { + let db = Database::open_memory().unwrap(); + + let audio_meta = AudioMeta { + title: Some("Test Track".to_string()), + artist: Some("Test Artist".to_string()), + album: Some("Test Album".to_string()), + album_artist: Some("Test Album Artist".to_string()), + genre: Some("Rock".to_string()), + year: Some(2024), + track: Some(5), + disc: Some(1), + duration_ms: Some(180000), + bitrate: Some(320), + sample_rate: Some(44100), + format: AudioFormat::Flac, + track_total: Some(12), + disc_total: Some(2), + date: Some("2024-03-15".to_string()), + composer: Some("Test Composer".to_string()), + comment: Some("Test comment".to_string()), + lyrics: Some("La la la".to_string()), + copyright: Some("2024 Test Records".to_string()), + compilation: Some(true), + artist_sort: Some("Artist, Test".to_string()), + album_artist_sort: Some("Album Artist, Test".to_string()), + album_sort: Some("Album, Test".to_string()), + title_sort: Some("Track, Test".to_string()), + mb_recording_id: Some("rec-123".to_string()), + mb_album_id: Some("alb-456".to_string()), + mb_artist_id: Some("art-789".to_string()), + mb_album_artist_id: Some("aa-012".to_string()), + mb_release_group_id: Some("rg-345".to_string()), + replaygain_track_gain: Some(-6.5), + replaygain_track_peak: Some(0.95), + replaygain_album_gain: Some(-7.2), + replaygain_album_peak: Some(0.98), + channels: Some(2), + bits_per_sample: Some(24), + encoder: Some("FLAC 1.4.0".to_string()), + }; + + let vpath = VirtualPath::new("/Artist/Album/05 - Track.flac"); + let id = db + .upsert_file( + &OriginId::from("local"), + Path::new("/music/test.flac"), + &vpath, + &audio_meta, + UNIX_EPOCH, + 5000000, + ) + .unwrap(); + + let retrieved = db.get_file_by_virtual_path(&vpath).unwrap().unwrap(); + let audio = retrieved.audio.unwrap(); + + assert_eq!(audio.title, Some("Test Track".to_string())); + assert_eq!(audio.track_total, Some(12)); + assert_eq!(audio.disc_total, Some(2)); + assert_eq!(audio.date, Some("2024-03-15".to_string())); + assert_eq!(audio.composer, Some("Test Composer".to_string())); + assert_eq!(audio.comment, Some("Test comment".to_string())); + assert_eq!(audio.lyrics, Some("La la la".to_string())); + assert_eq!(audio.copyright, Some("2024 Test Records".to_string())); + assert_eq!(audio.compilation, Some(true)); + assert_eq!(audio.artist_sort, Some("Artist, Test".to_string())); + assert_eq!(audio.mb_recording_id, Some("rec-123".to_string())); + assert_eq!(audio.mb_album_id, Some("alb-456".to_string())); + assert!(audio.replaygain_track_gain.is_some()); + assert!((audio.replaygain_track_gain.unwrap() - (-6.5)).abs() < 0.01); + assert_eq!(audio.channels, Some(2)); + assert_eq!(audio.bits_per_sample, Some(24)); + assert_eq!(audio.encoder, Some("FLAC 1.4.0".to_string())); + + let meta_row = db.get_file_metadata_row(id).unwrap(); + assert_eq!(meta_row.title, Some("Test Track".to_string())); + assert_eq!(meta_row.track_total, Some(12)); + assert_eq!(meta_row.mb_album_id, Some("alb-456".to_string())); + } + + #[test] + fn test_update_metadata_partial() { + let db = Database::open_memory().unwrap(); + + let audio_meta = AudioMeta { + title: Some("Original Title".to_string()), + artist: Some("Original Artist".to_string()), + album: Some("Original Album".to_string()), + track: Some(1), + ..Default::default() + }; + + let vpath = VirtualPath::new("/Artist/Album/Track.flac"); + let id = db + .upsert_file( + &OriginId::from("local"), + Path::new("/test.flac"), + &vpath, + &audio_meta, + UNIX_EPOCH, + 1000, + ) + .unwrap(); + + let update = AudioMeta { + title: Some("Updated Title".to_string()), + composer: Some("New Composer".to_string()), + ..Default::default() + }; + db.update_metadata(id, &update).unwrap(); + + let retrieved = db.get_file_metadata_row(id).unwrap(); + assert_eq!(retrieved.title, Some("Updated Title".to_string())); + assert_eq!(retrieved.artist, Some("Original Artist".to_string())); + assert_eq!(retrieved.album, Some("Original Album".to_string())); + assert_eq!(retrieved.composer, Some("New Composer".to_string())); + assert_eq!(retrieved.track, Some(1)); + } + + #[test] + fn test_update_metadata_empty_noop() { + let db = Database::open_memory().unwrap(); + + let id = db + .upsert_file( + &OriginId::from("local"), + Path::new("/test.flac"), + &VirtualPath::new("/Track.flac"), + &AudioMeta { + title: Some("Title".to_string()), + ..Default::default() + }, + UNIX_EPOCH, + 1000, + ) + .unwrap(); + + let empty_update = AudioMeta::default(); + db.update_metadata(id, &empty_update).unwrap(); + + let retrieved = db.get_file_metadata_row(id).unwrap(); + assert_eq!(retrieved.title, Some("Title".to_string())); + } + + #[test] + fn test_clear_overlay() { + let db = Database::open_memory().unwrap(); + + let audio_meta = AudioMeta { + title: Some("Title".to_string()), + artist: Some("Artist".to_string()), + album: Some("Album".to_string()), + composer: Some("Composer".to_string()), + mb_album_id: Some("mb-123".to_string()), + replaygain_track_gain: Some(-5.0), + ..Default::default() + }; + + let id = db + .upsert_file( + &OriginId::from("local"), + Path::new("/test.flac"), + &VirtualPath::new("/Track.flac"), + &audio_meta, + UNIX_EPOCH, + 1000, + ) + .unwrap(); + + db.clear_overlay(id).unwrap(); + + let retrieved = db.get_file_metadata_row(id).unwrap(); + assert!(retrieved.title.is_none()); + assert!(retrieved.artist.is_none()); + assert!(retrieved.album.is_none()); + assert!(retrieved.composer.is_none()); + assert!(retrieved.mb_album_id.is_none()); + assert!(retrieved.replaygain_track_gain.is_none()); + } + + #[test] + fn test_format_layout_roundtrip() { + use crate::FormatLayout; + + let db = Database::open_memory().unwrap(); + + let layout = FormatLayout { + audio_start: 1024, + audio_end: 5000000, + format: AudioFormat::Flac, + format_data: Some(vec![0x66, 0x4c, 0x61, 0x43]), + }; + + let id = db + .upsert_file_with_layout( + &OriginId::from("local"), + Path::new("/test.flac"), + &VirtualPath::new("/Track.flac"), + &AudioMeta::default(), + UNIX_EPOCH, + 5000000, + Some(&layout), + None, + ) + .unwrap(); + + let retrieved = db.get_format_layout(id).unwrap().unwrap(); + assert_eq!(retrieved.audio_start, 1024); + assert_eq!(retrieved.audio_end, 5000000); + assert_eq!(retrieved.format, AudioFormat::Flac); + assert_eq!(retrieved.format_data, Some(vec![0x66, 0x4c, 0x61, 0x43])); + } + + #[test] + fn test_custom_tags_roundtrip() { + let db = Database::open_memory().unwrap(); + + let mut custom_tags = HashMap::new(); + custom_tags.insert("CUSTOM_FIELD".to_string(), "custom value".to_string()); + custom_tags.insert("ANOTHER_TAG".to_string(), "another value".to_string()); + + let id = db + .upsert_file_with_layout( + &OriginId::from("local"), + Path::new("/test.flac"), + &VirtualPath::new("/Track.flac"), + &AudioMeta::default(), + UNIX_EPOCH, + 1000, + None, + Some(&custom_tags), + ) + .unwrap(); + + let retrieved = db.get_custom_tags(id).unwrap().unwrap(); + assert_eq!( + retrieved.get("CUSTOM_FIELD"), + Some(&"custom value".to_string()) + ); + assert_eq!( + retrieved.get("ANOTHER_TAG"), + Some(&"another value".to_string()) + ); + } + + #[test] + fn test_format_layout_none() { + let db = Database::open_memory().unwrap(); + + let id = db + .upsert_file( + &OriginId::from("local"), + Path::new("/test.flac"), + &VirtualPath::new("/Track.flac"), + &AudioMeta::default(), + UNIX_EPOCH, + 1000, + ) + .unwrap(); + + let layout = db.get_format_layout(id).unwrap(); + assert!(layout.is_none()); + } + + #[test] + fn test_custom_tags_none() { + let db = Database::open_memory().unwrap(); + + let id = db + .upsert_file( + &OriginId::from("local"), + Path::new("/test.flac"), + &VirtualPath::new("/Track.flac"), + &AudioMeta::default(), + UNIX_EPOCH, + 1000, + ) + .unwrap(); + + let tags = db.get_custom_tags(id).unwrap(); + assert!(tags.is_none()); + } }