diff --git a/src/local.rs b/src/local.rs index bd9588a..975f507 100644 --- a/src/local.rs +++ b/src/local.rs @@ -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 = 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 = + 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> = + 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> = + 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> = + 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, + _uid: Option, + _gid: Option, + _size: Option, + _atime: Option, + _mtime: Option, + _ctime: Option, + _fh: Option, + _crtime: Option, + _chgtime: Option, + _bkuptime: Option, + _flags: Option, + 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, + 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 { diff --git a/src/music_metadata.rs b/src/music_metadata.rs index f8b1d89..e82aab1 100644 --- a/src/music_metadata.rs +++ b/src/music_metadata.rs @@ -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 { }) } -fn build_flac_header(blocks: Vec<(u8, Vec)>, metadata: &MusicMetadata) -> Vec { +const PADDING_SIZE: usize = 8192; + +fn build_flac_header(blocks: Vec<(u8, Vec)>, metadata: &MusicMetadata) -> (Vec, u64, u64) { let vorbis = build_vorbis_comment(metadata); + let vorbis_len = vorbis.len() as u64; let mut patched: Vec<(u8, Vec)> = 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)>, 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)> { + 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 {