Read and store flac metadata

This commit is contained in:
Alexander
2026-06-26 23:27:33 +02:00
parent 4f52549e46
commit 9737e26bc9
5 changed files with 487 additions and 27 deletions
+226 -6
View File
@@ -1,8 +1,8 @@
use std::{
collections::BTreeMap,
fs,
hash::{Hash, Hasher},
io,
hash::Hasher,
io::{self, Read, Seek, SeekFrom},
os::unix::{
ffi::OsStrExt,
fs::{MetadataExt, PermissionsExt},
@@ -18,8 +18,11 @@ use notify::{Event, EventKind, RecursiveMode, Watcher};
use sea_orm::entity::prelude::*;
use twox_hash::XxHash64;
use crate::file_watcher::FileWatcher;
use crate::music_metadata::MusicMetadata;
use crate::{file_watcher::FileWatcher, music_metadata};
use crate::music_metadata::db::{
artists, music_metadata as music_metadata_entity, other_tags, pictures,
};
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
enum FileType {
@@ -109,13 +112,22 @@ impl LocalOrigin {
let mut to_insert: Vec<ActiveModel> = vec![];
let mut to_update: Vec<ActiveModel> = vec![];
let mut to_delete: Vec<i64> = vec![];
let mut to_save_music: Vec<i64> = vec![];
for (ino, item) in &snapshot {
let ino_i64 = ino.0 as i64;
match db_items.get(&ino_i64) {
None => to_insert.push(ActiveModel::from(item)),
None => {
to_insert.push(ActiveModel::from(item));
if item.music_metadata.is_some() {
to_save_music.push(ino_i64);
}
}
Some(db_item) if db_item.hash != item.hash as i64 => {
to_update.push(ActiveModel::from(item))
to_update.push(ActiveModel::from(item));
if item.music_metadata.is_some() {
to_save_music.push(ino_i64);
}
}
_ => {}
}
@@ -137,6 +149,18 @@ impl LocalOrigin {
Entity::delete_by_id(ino).exec(&client).await.unwrap();
}
for ino_i64 in &to_save_music {
let ino = INodeNo(*ino_i64 as u64);
if let Some(music_metadata) = snapshot
.get(&ino)
.and_then(|item| item.music_metadata.as_ref())
{
LocalOrigin::save_music_metadata(*ino_i64, music_metadata, &client)
.await
.unwrap();
}
}
// For unchanged items (hash matches), restore virtual paths from DB
// so that renames performed in a previous session are preserved.
let restorations: Vec<(INodeNo, String, PathBuf)> = db_items
@@ -177,6 +201,73 @@ impl LocalOrigin {
Ok(local_origin)
}
async fn save_music_metadata(
inode: i64,
music_metadata: &MusicMetadata,
client: &sea_orm::DatabaseConnection,
) -> Result<(), sea_orm::DbErr> {
use sea_orm::ActiveValue::Set;
music_metadata_entity::Entity::delete_by_id(inode)
.exec(client)
.await?;
music_metadata_entity::Entity::insert(music_metadata_entity::ActiveModel {
inode: Set(inode),
track_title: Set(music_metadata.track_title.clone()),
album: Set(music_metadata.album.clone()),
track_number: Set(music_metadata.track_number),
header: Set(music_metadata.header.clone()),
real_audio_start: Set(music_metadata.real_audio_start as i64),
})
.exec(client)
.await?;
if !music_metadata.artist.is_empty() {
artists::Entity::insert_many(music_metadata.artist.iter().map(|a| {
artists::ActiveModel {
inode: Set(inode),
artist: Set(a.clone()),
}
}))
.exec(client)
.await?;
}
if !music_metadata.other_tags.is_empty() {
other_tags::Entity::insert_many(music_metadata.other_tags.iter().enumerate().map(
|(pos, tag)| other_tags::ActiveModel {
inode: Set(inode),
position: Set(pos as i32),
tag: Set(tag.clone()),
},
))
.exec(client)
.await?;
}
if !music_metadata.picture_block_headers.is_empty() {
pictures::Entity::insert_many(
music_metadata
.picture_block_headers
.iter()
.zip(music_metadata.picture_data_ranges.iter())
.enumerate()
.map(|(pos, (hdr, (offset, len)))| pictures::ActiveModel {
inode: Set(inode),
position: Set(pos as i32),
block_header: Set(hdr.to_vec()),
data_offset: Set(*offset as i64),
data_length: Set(*len as i64),
}),
)
.exec(client)
.await?;
}
Ok(())
}
fn fill_fileset(
map: &Arc<Mutex<BTreeMap<INodeNo, LocalItem>>>,
source: &Path,
@@ -293,9 +384,14 @@ impl LocalOrigin {
let crtime = metadata.created().unwrap_or(SystemTime::UNIX_EPOCH);
let kind = LocalOrigin::file_type(local_file.file_type);
let size = match &local_file.music_metadata {
Some(mm) if mm.real_audio_start > 0 => mm.virtual_size(metadata.size()),
_ => metadata.size(),
};
return FileAttr {
ino: local_file.inode,
size: metadata.size(),
size,
blocks: metadata.blocks(),
atime: atime,
mtime: mtime,
@@ -319,6 +415,15 @@ impl LocalOrigin {
};
}
fn read_bytes_at(path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>> {
let mut f = fs::File::open(path)?;
f.seek(SeekFrom::Start(offset))?;
let mut buf = vec![0u8; len];
let n = f.read(&mut buf)?;
buf.truncate(n);
Ok(buf)
}
fn parent_inode_from_path(local_path: &Path) -> INodeNo {
let components: Vec<_> = local_path.components().collect();
if components.len() <= 1 {
@@ -540,6 +645,121 @@ impl Filesystem for LocalOrigin {
reply.ok();
}
fn read(
&self,
_req: &Request,
ino: INodeNo,
_fh: fuser::FileHandle,
offset: u64,
size: u32,
_flags: fuser::OpenFlags,
_lock_owner: Option<fuser::LockOwner>,
reply: fuser::ReplyData,
) {
let files = self.files.lock().unwrap();
let item = match files.get(&ino) {
Some(f) => f,
None => {
reply.error(Errno::ENOENT);
return;
}
};
let original_path = item.original_path.clone();
let mm = item
.music_metadata
.as_ref()
.filter(|mm| mm.real_audio_start > 0);
let is_flac = mm.is_some();
let header = mm.map(|mm| mm.header.clone()).unwrap_or_default();
let pic_hdrs = mm
.map(|mm| mm.picture_block_headers.clone())
.unwrap_or_default();
let pic_ranges = mm
.map(|mm| mm.picture_data_ranges.clone())
.unwrap_or_default();
let real_audio_start = mm.map(|mm| mm.real_audio_start).unwrap_or(0);
drop(files);
if !is_flac {
match LocalOrigin::read_bytes_at(&original_path, offset, size as usize) {
Ok(bytes) => reply.data(&bytes),
Err(_) => reply.error(Errno::EIO),
}
return;
}
let end = offset + size as u64;
let header_end = header.len() as u64;
// Pure header read — most common for tag readers
if end <= header_end {
reply.data(&header[offset as usize..end as usize]);
return;
}
// Multi-region read: walk [header][pic_hdr+pic_data]...[audio] and collect bytes
let mut buf = Vec::with_capacity(size as usize);
if offset < header_end {
buf.extend_from_slice(&header[offset as usize..]);
}
let mut virt_pos = header_end;
for (pic_hdr, (data_real_offset, data_len)) in pic_hdrs.iter().zip(pic_ranges.iter()) {
let pic_hdr_end = virt_pos + 4;
let pic_end = pic_hdr_end + data_len;
if end <= virt_pos {
break;
}
if offset >= pic_end {
virt_pos = pic_end;
continue;
}
let hdr_from = (offset.max(virt_pos) - virt_pos) as usize;
let hdr_to = ((end.min(pic_hdr_end)) - virt_pos) as usize;
if hdr_from < hdr_to {
buf.extend_from_slice(&pic_hdr[hdr_from..hdr_to.min(4)]);
}
let data_start = offset.max(pic_hdr_end);
let data_end = end.min(pic_end);
if data_start < data_end {
let real_off = data_real_offset + (data_start - pic_hdr_end);
match LocalOrigin::read_bytes_at(
&original_path,
real_off,
(data_end - data_start) as usize,
) {
Ok(bytes) => buf.extend_from_slice(&bytes),
Err(_) => {
reply.error(Errno::EIO);
return;
}
}
}
virt_pos = pic_end;
}
let audio_start = offset.max(virt_pos);
if audio_start < end {
let real_off = real_audio_start + (audio_start - virt_pos);
match LocalOrigin::read_bytes_at(&original_path, real_off, (end - audio_start) as usize)
{
Ok(bytes) => buf.extend_from_slice(&bytes),
Err(_) => {
reply.error(Errno::EIO);
return;
}
}
}
reply.data(&buf);
}
fn lookup(
&self,
_req: &Request,