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
This commit is contained in:
Alexander
2026-05-17 17:27:24 +02:00
parent 84bbd8f630
commit 4f4a4169f8
3 changed files with 676 additions and 5 deletions
+1
View File
@@ -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
+674 -5
View File
@@ -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<FileId> {
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<String, String>>,
) -> Result<FileId> {
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<Vec<u8>> = 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<String> = 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<String> = row.get(18)?;
let compilation_int: Option<i32> = row.get(23)?;
let content_hash: Option<String> = 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<AudioMeta> {
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<String> = row.get(11)?;
let format = format_str
.as_deref()
.map(parse_audio_format)
.unwrap_or(AudioFormat::Unknown);
let compilation_int: Option<i32> = 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<i64>>(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<Option<FormatLayout>> {
let conn = self.conn.lock().unwrap();
let blob: Option<Vec<u8>> = 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<Option<HashMap<String, String>>> {
let conn = self.conn.lock().unwrap();
let json: Option<String> = 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<String, String> = 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<Box<dyn rusqlite::ToSql>> = 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());
}
}