Implement write method to support metadata update
This commit is contained in:
+235
@@ -161,6 +161,121 @@ impl LocalOrigin {
|
||||
}
|
||||
}
|
||||
|
||||
// For unchanged FLAC items, restore music_metadata from DB
|
||||
// (overrides the fresh FLAC parse so that writes made in a previous session survive restarts)
|
||||
let unchanged_music_inodes: Vec<i64> = db_items
|
||||
.values()
|
||||
.filter_map(|db_item| {
|
||||
let ino = INodeNo(db_item.inode as u64);
|
||||
snapshot
|
||||
.get(&ino)
|
||||
.filter(|item| {
|
||||
item.hash as i64 == db_item.hash && item.music_metadata.is_some()
|
||||
})
|
||||
.map(|_| db_item.inode)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !unchanged_music_inodes.is_empty() {
|
||||
use artists::Column as ArtCol;
|
||||
use music_metadata_entity::Column as MmCol;
|
||||
use other_tags::Column as OtCol;
|
||||
use pictures::Column as PicCol;
|
||||
|
||||
let mm_rows: std::collections::HashMap<i64, music_metadata_entity::Model> =
|
||||
music_metadata_entity::Entity::find()
|
||||
.filter(MmCol::Inode.is_in(unchanged_music_inodes.clone()))
|
||||
.all(&client)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|m| (m.inode, m))
|
||||
.collect();
|
||||
|
||||
let mut artists_by_inode: std::collections::HashMap<i64, Vec<String>> =
|
||||
std::collections::HashMap::new();
|
||||
for row in artists::Entity::find()
|
||||
.filter(ArtCol::Inode.is_in(unchanged_music_inodes.clone()))
|
||||
.all(&client)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
artists_by_inode
|
||||
.entry(row.inode)
|
||||
.or_default()
|
||||
.push(row.artist);
|
||||
}
|
||||
|
||||
let mut other_tags_by_inode: std::collections::HashMap<i64, Vec<(i32, String)>> =
|
||||
std::collections::HashMap::new();
|
||||
for row in other_tags::Entity::find()
|
||||
.filter(OtCol::Inode.is_in(unchanged_music_inodes.clone()))
|
||||
.all(&client)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
other_tags_by_inode
|
||||
.entry(row.inode)
|
||||
.or_default()
|
||||
.push((row.position, row.tag));
|
||||
}
|
||||
|
||||
let mut pictures_by_inode: std::collections::HashMap<i64, Vec<pictures::Model>> =
|
||||
std::collections::HashMap::new();
|
||||
for row in pictures::Entity::find()
|
||||
.filter(PicCol::Inode.is_in(unchanged_music_inodes))
|
||||
.all(&client)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
pictures_by_inode.entry(row.inode).or_default().push(row);
|
||||
}
|
||||
|
||||
for (inode, mm_row) in mm_rows {
|
||||
let ino = INodeNo(inode as u64);
|
||||
let Some(item) = snapshot.get_mut(&ino) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut sorted_tags = other_tags_by_inode.remove(&inode).unwrap_or_default();
|
||||
sorted_tags.sort_by_key(|(pos, _)| *pos);
|
||||
|
||||
let mut sorted_pics = pictures_by_inode.remove(&inode).unwrap_or_default();
|
||||
sorted_pics.sort_by_key(|p| p.position);
|
||||
|
||||
let picture_block_headers: Vec<[u8; 4]> = sorted_pics
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let mut hdr = [0u8; 4];
|
||||
let len = p.block_header.len().min(4);
|
||||
hdr[..len].copy_from_slice(&p.block_header[..len]);
|
||||
hdr
|
||||
})
|
||||
.collect();
|
||||
|
||||
let picture_data_ranges: Vec<(u64, u64)> = sorted_pics
|
||||
.iter()
|
||||
.map(|p| (p.data_offset as u64, p.data_length as u64))
|
||||
.collect();
|
||||
|
||||
let mut music_metadata = MusicMetadata {
|
||||
artist: artists_by_inode.remove(&inode).unwrap_or_default(),
|
||||
album: mm_row.album,
|
||||
track_number: mm_row.track_number,
|
||||
track_title: mm_row.track_title,
|
||||
other_tags: sorted_tags.into_iter().map(|(_, tag)| tag).collect(),
|
||||
header: mm_row.header,
|
||||
picture_block_headers,
|
||||
picture_data_ranges,
|
||||
real_audio_start: mm_row.real_audio_start as u64,
|
||||
vorbis_comment_offset: 0,
|
||||
vorbis_comment_length: 0,
|
||||
};
|
||||
music_metadata.find_vorbis_offsets();
|
||||
item.music_metadata = Some(music_metadata);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -496,6 +611,125 @@ impl LocalOrigin {
|
||||
}
|
||||
|
||||
impl Filesystem for LocalOrigin {
|
||||
fn open(
|
||||
&self,
|
||||
_req: &Request,
|
||||
ino: INodeNo,
|
||||
_flags: fuser::OpenFlags,
|
||||
reply: fuser::ReplyOpen,
|
||||
) {
|
||||
if self.files.lock().unwrap().contains_key(&ino) {
|
||||
reply.opened(fuser::FileHandle(ino.0), fuser::FopenFlags::empty());
|
||||
} else {
|
||||
reply.error(Errno::ENOENT);
|
||||
}
|
||||
}
|
||||
|
||||
fn setattr(
|
||||
&self,
|
||||
_req: &Request,
|
||||
ino: INodeNo,
|
||||
_mode: Option<u32>,
|
||||
_uid: Option<u32>,
|
||||
_gid: Option<u32>,
|
||||
_size: Option<u64>,
|
||||
_atime: Option<fuser::TimeOrNow>,
|
||||
_mtime: Option<fuser::TimeOrNow>,
|
||||
_ctime: Option<std::time::SystemTime>,
|
||||
_fh: Option<fuser::FileHandle>,
|
||||
_crtime: Option<std::time::SystemTime>,
|
||||
_chgtime: Option<std::time::SystemTime>,
|
||||
_bkuptime: Option<std::time::SystemTime>,
|
||||
_flags: Option<fuser::BsdFileFlags>,
|
||||
reply: fuser::ReplyAttr,
|
||||
) {
|
||||
match self.files.lock().unwrap().get(&ino) {
|
||||
Some(file) => reply.attr(
|
||||
&Duration::new(1, 0),
|
||||
&LocalOrigin::local_file_to_file_attr(file),
|
||||
),
|
||||
None => reply.error(Errno::ENOENT),
|
||||
}
|
||||
}
|
||||
|
||||
fn write(
|
||||
&self,
|
||||
_req: &Request,
|
||||
ino: INodeNo,
|
||||
_fh: fuser::FileHandle,
|
||||
offset: u64,
|
||||
data: &[u8],
|
||||
_write_flags: fuser::WriteFlags,
|
||||
_flags: fuser::OpenFlags,
|
||||
_lock_owner: Option<fuser::LockOwner>,
|
||||
reply: fuser::ReplyWrite,
|
||||
) {
|
||||
let written = data.len() as u32;
|
||||
let write_start = offset;
|
||||
let write_end = write_start + data.len() as u64;
|
||||
|
||||
let updated_music_metadata = {
|
||||
let mut files = self.files.lock().unwrap();
|
||||
let item = match files.get_mut(&ino) {
|
||||
Some(item) => item,
|
||||
None => {
|
||||
reply.written(written);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let music_metadata = match &mut item.music_metadata {
|
||||
Some(mm) if mm.vorbis_comment_length > 0 => mm,
|
||||
_ => {
|
||||
reply.written(written);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let vc_data_offset = music_metadata.vorbis_comment_offset;
|
||||
let vc_hdr_offset = vc_data_offset - 4;
|
||||
|
||||
// Must cover the 4-byte block header to read the (possibly new) data length
|
||||
if write_start <= vc_hdr_offset && write_end >= vc_data_offset {
|
||||
let hdr_from = (vc_hdr_offset - write_start) as usize;
|
||||
let new_length = u32::from_be_bytes([
|
||||
0,
|
||||
data[hdr_from + 1],
|
||||
data[hdr_from + 2],
|
||||
data[hdr_from + 3],
|
||||
]) as u64;
|
||||
let vc_data_end = vc_data_offset + new_length;
|
||||
|
||||
if write_end >= vc_data_end {
|
||||
let from = (vc_data_offset - write_start) as usize;
|
||||
let to = (vc_data_end - write_start) as usize;
|
||||
music_metadata.update_from_vorbis_comment_data(&data[from..to]);
|
||||
Some(music_metadata.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(music_metadata) = updated_music_metadata {
|
||||
let client = self.client.clone();
|
||||
let ino_i64 = ino.0 as i64;
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
LocalOrigin::save_music_metadata(ino_i64, &music_metadata, &client)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
reply.written(written);
|
||||
}
|
||||
|
||||
fn getattr(
|
||||
&self,
|
||||
_req: &fuser::Request,
|
||||
@@ -627,6 +861,7 @@ impl Filesystem for LocalOrigin {
|
||||
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_time()
|
||||
.enable_io()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
|
||||
+130
-5
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
fs,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
io::{Cursor, Read, Seek, SeekFrom},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
@@ -22,6 +22,8 @@ pub struct MusicMetadata {
|
||||
pub picture_block_headers: Vec<[u8; 4]>,
|
||||
pub picture_data_ranges: Vec<(u64, u64)>,
|
||||
pub real_audio_start: u64,
|
||||
pub vorbis_comment_offset: u64,
|
||||
pub vorbis_comment_length: u64,
|
||||
}
|
||||
|
||||
impl MusicMetadata {
|
||||
@@ -71,11 +73,53 @@ impl MusicMetadata {
|
||||
music_metadata.real_audio_start = parsed.audio_start;
|
||||
music_metadata.picture_block_headers = parsed.picture_block_headers;
|
||||
music_metadata.picture_data_ranges = parsed.picture_data_ranges;
|
||||
music_metadata.header = build_flac_header(parsed.other_blocks, &music_metadata);
|
||||
let (header, vorbis_comment_offset, vorbis_comment_length) =
|
||||
build_flac_header(parsed.other_blocks, &music_metadata);
|
||||
music_metadata.header = header;
|
||||
music_metadata.vorbis_comment_offset = vorbis_comment_offset;
|
||||
music_metadata.vorbis_comment_length = vorbis_comment_length;
|
||||
}
|
||||
|
||||
Some(music_metadata)
|
||||
}
|
||||
|
||||
pub fn find_vorbis_offsets(&mut self) {
|
||||
let mut cursor = Cursor::new(&self.header);
|
||||
let mut magic = [0u8; 4];
|
||||
if cursor.read_exact(&mut magic).is_err() {
|
||||
return;
|
||||
}
|
||||
loop {
|
||||
let mut hdr = [0u8; 4];
|
||||
if cursor.read_exact(&mut hdr).is_err() {
|
||||
break;
|
||||
}
|
||||
let is_last = (hdr[0] & 0x80) != 0;
|
||||
let block_type = hdr[0] & 0x7f;
|
||||
let length = u32::from_be_bytes([0, hdr[1], hdr[2], hdr[3]]) as u64;
|
||||
if block_type == 4 {
|
||||
self.vorbis_comment_offset = cursor.position();
|
||||
self.vorbis_comment_length = length;
|
||||
return;
|
||||
}
|
||||
if cursor.seek(SeekFrom::Current(length as i64)).is_err() {
|
||||
break;
|
||||
}
|
||||
if is_last {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_from_vorbis_comment_data(&mut self, data: &[u8]) {
|
||||
parse_vorbis_comment_block(data, self);
|
||||
let other_blocks = extract_non_vorbis_blocks(&self.header);
|
||||
let (header, vorbis_comment_offset, vorbis_comment_length) =
|
||||
build_flac_header(other_blocks, self);
|
||||
self.header = header;
|
||||
self.vorbis_comment_offset = vorbis_comment_offset;
|
||||
self.vorbis_comment_length = vorbis_comment_length;
|
||||
}
|
||||
}
|
||||
|
||||
struct FlacParsed {
|
||||
@@ -140,22 +184,30 @@ fn parse_flac(path: &Path) -> Option<FlacParsed> {
|
||||
})
|
||||
}
|
||||
|
||||
fn build_flac_header(blocks: Vec<(u8, Vec<u8>)>, metadata: &MusicMetadata) -> Vec<u8> {
|
||||
const PADDING_SIZE: usize = 8192;
|
||||
|
||||
fn build_flac_header(blocks: Vec<(u8, Vec<u8>)>, metadata: &MusicMetadata) -> (Vec<u8>, u64, u64) {
|
||||
let vorbis = build_vorbis_comment(metadata);
|
||||
let vorbis_len = vorbis.len() as u64;
|
||||
|
||||
let mut patched: Vec<(u8, Vec<u8>)> = blocks;
|
||||
patched.push((4, vorbis));
|
||||
patched.push((1, vec![0u8; PADDING_SIZE])); // PADDING — allows metaflac in-place writes
|
||||
|
||||
let vorbis_idx = patched.len() - 2;
|
||||
let has_pictures = !metadata.picture_data_ranges.is_empty();
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(b"fLaC");
|
||||
|
||||
let last = patched.len() - 1;
|
||||
let mut vorbis_offset = 0u64;
|
||||
for (i, (block_type, data)) in patched.iter().enumerate() {
|
||||
// is_last only if this is the final block AND no picture blocks follow
|
||||
let is_last_block = i == last && !has_pictures;
|
||||
let flag: u8 = if is_last_block { 0x80 } else { 0x00 };
|
||||
let length = data.len() as u32;
|
||||
if i == vorbis_idx {
|
||||
vorbis_offset = out.len() as u64 + 4; // data starts after 4-byte block header
|
||||
}
|
||||
out.push(flag | block_type);
|
||||
out.push((length >> 16) as u8);
|
||||
out.push((length >> 8) as u8);
|
||||
@@ -163,7 +215,80 @@ fn build_flac_header(blocks: Vec<(u8, Vec<u8>)>, metadata: &MusicMetadata) -> Ve
|
||||
out.extend_from_slice(data);
|
||||
}
|
||||
|
||||
out
|
||||
(out, vorbis_offset, vorbis_len)
|
||||
}
|
||||
|
||||
fn parse_vorbis_comment_block(data: &[u8], out: &mut MusicMetadata) {
|
||||
let mut cursor = Cursor::new(data);
|
||||
let mut len_bytes = [0u8; 4];
|
||||
|
||||
if cursor.read_exact(&mut len_bytes).is_err() {
|
||||
return;
|
||||
}
|
||||
let vendor_len = u32::from_le_bytes(len_bytes) as i64;
|
||||
if cursor.seek(SeekFrom::Current(vendor_len)).is_err() {
|
||||
return;
|
||||
}
|
||||
if cursor.read_exact(&mut len_bytes).is_err() {
|
||||
return;
|
||||
}
|
||||
let count = u32::from_le_bytes(len_bytes);
|
||||
|
||||
out.artist.clear();
|
||||
out.other_tags.clear();
|
||||
|
||||
for _ in 0..count {
|
||||
if cursor.read_exact(&mut len_bytes).is_err() {
|
||||
break;
|
||||
}
|
||||
let comment_len = u32::from_le_bytes(len_bytes) as usize;
|
||||
let mut comment_bytes = vec![0u8; comment_len];
|
||||
if cursor.read_exact(&mut comment_bytes).is_err() {
|
||||
break;
|
||||
}
|
||||
let comment = String::from_utf8_lossy(&comment_bytes).into_owned();
|
||||
if let Some((key, value)) = comment.split_once('=') {
|
||||
match key.to_ascii_uppercase().as_str() {
|
||||
"TITLE" => out.track_title = value.to_string(),
|
||||
"ALBUM" => out.album = value.to_string(),
|
||||
"TRACKNUMBER" => out.track_number = value.parse().unwrap_or(0),
|
||||
"ARTIST" => out.artist.push(value.to_string()),
|
||||
_ => out.other_tags.push(comment),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_non_vorbis_blocks(header: &[u8]) -> Vec<(u8, Vec<u8>)> {
|
||||
let mut cursor = Cursor::new(header);
|
||||
let mut blocks = vec![];
|
||||
|
||||
let mut magic = [0u8; 4];
|
||||
if cursor.read_exact(&mut magic).is_err() {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
loop {
|
||||
let mut hdr = [0u8; 4];
|
||||
if cursor.read_exact(&mut hdr).is_err() {
|
||||
break;
|
||||
}
|
||||
let is_last = (hdr[0] & 0x80) != 0;
|
||||
let block_type = hdr[0] & 0x7f;
|
||||
let length = u32::from_be_bytes([0, hdr[1], hdr[2], hdr[3]]) as usize;
|
||||
let mut data = vec![0u8; length];
|
||||
if cursor.read_exact(&mut data).is_err() {
|
||||
break;
|
||||
}
|
||||
if block_type != 4 && block_type != 1 {
|
||||
blocks.push((block_type, data));
|
||||
}
|
||||
if is_last {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
blocks
|
||||
}
|
||||
|
||||
pub mod db {
|
||||
|
||||
Reference in New Issue
Block a user