Read and store flac metadata
This commit is contained in:
+226
-6
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user