use std::{ fs, io::{Cursor, Read, Seek, SeekFrom}, path::Path, }; use symphonia::core::{ formats::FormatOptions, io::MediaSourceStream, meta::MetadataOptions, probe::Hint, }; use crate::music::encoder::MusicMetadataEncoder; use crate::music::metadata::{MusicMetadata, extract_standard_tags}; use crate::music::parser::MusicMetadataParser; use tracing::warn; const BLOCK_PADDING: u8 = 1; const BLOCK_VORBIS_COMMENT: u8 = 4; const BLOCK_PICTURE: u8 = 6; const BLOCK_LAST_FLAG: u8 = 0x80; const BLOCK_TYPE_MASK: u8 = 0x7f; const PADDING_SIZE: usize = 8192; /// FLAC parser. Owns all FLAC container parsing; returns `None` (never panics) /// on a file it can't read — a corrupt or mid-copy file is logged and served /// as plain passthrough rather than taking down the snapshot/watcher. pub struct FlacMusicMetadataParser; impl MusicMetadataParser for FlacMusicMetadataParser { fn parse(&self, path: &Path) -> Option { match parse_flac_metadata(path) { Some(mm) => Some(mm), None => { warn!( path = %path.display(), "failed to parse FLAC metadata; serving as passthrough" ); None } } } } /// FLAC encoder. Rebuilds the FLAC metadata header from the current tag fields, /// re-injecting the (possibly overridden) Vorbis comment. pub struct FlacMusicMetadataEncoder; impl MusicMetadataEncoder for FlacMusicMetadataEncoder { fn encode(&self, metadata: &mut MusicMetadata) { if metadata.header.is_empty() { return; } let blocks = extract_non_vorbis_blocks(&metadata.header); let (header, vorbis_comment_offset, vorbis_comment_length) = build_flac_header(blocks, metadata); metadata.header = header; metadata.vorbis_comment_offset = vorbis_comment_offset; metadata.vorbis_comment_length = vorbis_comment_length; } } fn parse_flac_metadata(path: &Path) -> Option { let src = fs::File::open(path).ok()?; let mss = MediaSourceStream::new(Box::new(src), Default::default()); let mut hint = Hint::new(); hint.with_extension("flac"); let meta_opts: MetadataOptions = Default::default(); let fmt_opts: FormatOptions = Default::default(); let mut format = symphonia::default::get_probe() .format(&hint, mss, &fmt_opts, &meta_opts) .ok()?; let metadata = format.format.metadata(); let mut music_metadata = MusicMetadata::default(); if let Some(revision) = metadata.current() { extract_standard_tags(revision, &mut music_metadata); } if let Some(parsed) = parse_flac(path) { 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; 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) } impl MusicMetadata { /// Locate the Vorbis comment block within the rebuilt FLAC header so writes /// to it can be intercepted. No-op for non-FLAC (e.g. ID3) headers. 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; } if &magic != b"fLaC" { return; } loop { let mut hdr = [0u8; 4]; if cursor.read_exact(&mut hdr).is_err() { break; } let (is_last, block_type, length) = read_block_header(&hdr); if block_type == BLOCK_VORBIS_COMMENT { 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; } } } /// Apply a freshly written Vorbis comment block and rebuild the header. 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; } } /// Parse a 4-byte FLAC metadata block header into (is_last, block_type, data_length). fn read_block_header(hdr: &[u8; 4]) -> (bool, u8, u64) { let is_last = (hdr[0] & BLOCK_LAST_FLAG) != 0; let block_type = hdr[0] & BLOCK_TYPE_MASK; let length = u32::from_be_bytes([0, hdr[1], hdr[2], hdr[3]]) as u64; (is_last, block_type, length) } struct FlacParsed { other_blocks: Vec<(u8, Vec)>, picture_block_headers: Vec>, picture_data_ranges: Vec<(u64, u64)>, audio_start: u64, } fn parse_flac(path: &Path) -> Option { let mut f = fs::File::open(path).ok()?; let mut magic = [0u8; 4]; f.read_exact(&mut magic).ok()?; if &magic != b"fLaC" { return None; } let mut other_blocks: Vec<(u8, Vec)> = vec![]; let mut picture_block_headers: Vec> = vec![]; let mut picture_data_ranges: Vec<(u64, u64)> = vec![]; let mut pos = 4u64; loop { let mut hdr = [0u8; 4]; f.read_exact(&mut hdr).ok()?; let (is_last, block_type, length) = read_block_header(&hdr); pos += 4; if block_type == BLOCK_PICTURE { // PICTURE: keep block header (we'll fix is_last later), record data range picture_block_headers.push(hdr.to_vec()); picture_data_ranges.push((pos, length)); f.seek(SeekFrom::Current(length as i64)).ok()?; } else { let mut data = vec![0u8; length as usize]; f.read_exact(&mut data).ok()?; // Skip VORBIS_COMMENT — we rebuild it if block_type != BLOCK_VORBIS_COMMENT { other_blocks.push((block_type, data)); } } pos += length; if is_last { break; } } // Fix is_last on the last picture block header: it must be 1 when audio follows if let Some(last_hdr) = picture_block_headers.last_mut() { last_hdr[0] = BLOCK_LAST_FLAG | (last_hdr[0] & BLOCK_TYPE_MASK); } Some(FlacParsed { other_blocks, picture_block_headers, picture_data_ranges, audio_start: pos, }) } 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((BLOCK_VORBIS_COMMENT, vorbis)); patched.push((BLOCK_PADDING, vec![0u8; PADDING_SIZE])); // 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() { let is_last_block = i == last && !has_pictures; let flag: u8 = if is_last_block { BLOCK_LAST_FLAG } 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); out.push(length as u8); out.extend_from_slice(data); } (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, block_type, length) = read_block_header(&hdr); let mut data = vec![0u8; length as usize]; if cursor.read_exact(&mut data).is_err() { break; } if block_type != BLOCK_VORBIS_COMMENT && block_type != BLOCK_PADDING { blocks.push((block_type, data)); } if is_last { break; } } blocks } fn build_vorbis_comment(metadata: &MusicMetadata) -> Vec { let vendor = b"musicfs"; let mut out = Vec::new(); out.extend_from_slice(&(vendor.len() as u32).to_le_bytes()); out.extend_from_slice(vendor); let mut comments: Vec = vec![ format!("TITLE={}", metadata.track_title), format!("ALBUM={}", metadata.album), format!("TRACKNUMBER={}", metadata.track_number), ]; for artist in &metadata.artist { comments.push(format!("ARTIST={}", artist)); } comments.extend(metadata.other_tags.iter().cloned()); out.extend_from_slice(&(comments.len() as u32).to_le_bytes()); for comment in &comments { let bytes = comment.as_bytes(); out.extend_from_slice(&(bytes.len() as u32).to_le_bytes()); out.extend_from_slice(bytes); } out } #[cfg(test)] mod tests { use super::*; #[test] fn read_block_header_normal() { let hdr = [0x04, 0x00, 0x01, 0x00]; let (is_last, block_type, length) = read_block_header(&hdr); assert!(!is_last); assert_eq!(block_type, 4); assert_eq!(length, 256); } #[test] fn read_block_header_last() { let hdr = [0x84, 0x00, 0x00, 0x10]; let (is_last, block_type, length) = read_block_header(&hdr); assert!(is_last); assert_eq!(block_type, 4); assert_eq!(length, 16); } #[test] fn build_vorbis_comment_roundtrip() { let mut metadata = MusicMetadata::default(); metadata.artist = vec!["Artist1".to_string(), "Artist2".to_string()]; metadata.album = "Album".to_string(); metadata.track_number = 3; metadata.track_title = "Title".to_string(); metadata.other_tags = vec!["GENRE=Rock".to_string()]; let vorbis_bytes = build_vorbis_comment(&metadata); let mut parsed = MusicMetadata::default(); parse_vorbis_comment_block(&vorbis_bytes, &mut parsed); assert_eq!(parsed.artist, vec!["Artist1", "Artist2"]); assert_eq!(parsed.album, "Album"); assert_eq!(parsed.track_number, 3); assert_eq!(parsed.track_title, "Title"); assert_eq!(parsed.other_tags, vec!["GENRE=Rock"]); } #[test] fn parse_vorbis_comment_multi_artist() { let mut vorbis_bytes = Vec::new(); let vendor = b"test"; vorbis_bytes.extend_from_slice(&(vendor.len() as u32).to_le_bytes()); vorbis_bytes.extend_from_slice(vendor); let comments = vec!["ARTIST=Artist1", "ARTIST=Artist2"]; vorbis_bytes.extend_from_slice(&(comments.len() as u32).to_le_bytes()); for comment in &comments { let bytes = comment.as_bytes(); vorbis_bytes.extend_from_slice(&(bytes.len() as u32).to_le_bytes()); vorbis_bytes.extend_from_slice(bytes); } let mut metadata = MusicMetadata::default(); parse_vorbis_comment_block(&vorbis_bytes, &mut metadata); assert_eq!(metadata.artist.len(), 2); assert_eq!(metadata.artist[0], "Artist1"); assert_eq!(metadata.artist[1], "Artist2"); } #[test] fn extract_non_vorbis_blocks_strips_vc_and_padding() { let mut header = Vec::new(); header.extend_from_slice(b"fLaC"); let streaminfo_data = vec![0u8; 34]; header.push(0x00); header.extend_from_slice(&[0x00, 0x00, 0x22]); header.extend_from_slice(&streaminfo_data); let vorbis_data = vec![0u8; 50]; header.push(0x04); header.extend_from_slice(&[0x00, 0x00, 0x32]); header.extend_from_slice(&vorbis_data); let padding_data = vec![0u8; 100]; header.push(0x81); header.extend_from_slice(&[0x00, 0x00, 0x64]); header.extend_from_slice(&padding_data); let blocks = extract_non_vorbis_blocks(&header); assert_eq!(blocks.len(), 1); assert_eq!(blocks[0].0, 0); assert_eq!(blocks[0].1.len(), 34); } #[test] fn find_vorbis_offsets_correct() { let mut metadata = MusicMetadata::default(); metadata.artist = vec!["TestArtist".to_string()]; metadata.album = "TestAlbum".to_string(); metadata.track_number = 1; metadata.track_title = "TestTitle".to_string(); let other_blocks = vec![(0, vec![0u8; 34])]; let (header, expected_offset, expected_length) = build_flac_header(other_blocks, &metadata); metadata.header = header; metadata.find_vorbis_offsets(); assert_eq!(metadata.vorbis_comment_offset, expected_offset); assert_eq!(metadata.vorbis_comment_length, expected_length); } }