use std::{ fs, io::{Cursor, Read, Seek, SeekFrom}, path::Path, }; use symphonia::core::{ formats::FormatOptions, io::MediaSourceStream, meta::{Metadata, MetadataOptions, MetadataRevision, StandardTagKey}, probe::Hint, }; 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; /// 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) } #[derive(Debug, Default, Clone)] pub struct MusicMetadata { pub artist: Vec, pub album: String, pub track_number: i32, pub track_title: String, pub other_tags: Vec, pub header: Vec, 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 { pub fn virtual_size(&self, real_file_size: u64) -> u64 { let pictures_size: u64 = self .picture_data_ranges .iter() .map(|(_, len)| 4 + len) .sum(); self.header.len() as u64 + pictures_size + (real_file_size - self.real_audio_start) } pub fn parse_music_metadata(path: &Path) -> Option { let src = fs::File::open(path).expect("failed to open media"); 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) .expect("unsupported format"); let metadata: Metadata = format.format.metadata(); let revision: &MetadataRevision = metadata.current().unwrap(); let mut music_metadata = MusicMetadata::default(); for tag in revision.tags() { let value = tag.value.to_string(); match tag.std_key { Some(StandardTagKey::Artist) => music_metadata.artist.push(value), Some(StandardTagKey::Album) => music_metadata.album = value, Some(StandardTagKey::TrackNumber) => { music_metadata.track_number = value.parse::().unwrap_or(0) } Some(StandardTagKey::TrackTitle) => music_metadata.track_title = value, _ => music_metadata .other_tags .push(format!("{}={}", tag.key, value)), } } 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) } 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, 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; } } } 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 { other_blocks: Vec<(u8, Vec)>, picture_block_headers: Vec<[u8; 4]>, 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<[u8; 4]> = 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); 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, }) } 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((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 } pub mod db { pub mod music_metadata { use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "music_metadata")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub inode: i64, pub track_title: String, pub album: String, pub track_number: i32, #[sea_orm(column_type = "Blob")] pub header: Vec, pub real_audio_start: i64, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation {} impl ActiveModelBehavior for ActiveModel {} } pub mod artists { use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "music_metadata_artists")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub inode: i64, #[sea_orm(primary_key, auto_increment = false)] pub artist: String, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation {} impl ActiveModelBehavior for ActiveModel {} } pub mod other_tags { use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "music_metadata_other_tags")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub inode: i64, #[sea_orm(primary_key, auto_increment = false)] pub position: i32, pub tag: String, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation {} impl ActiveModelBehavior for ActiveModel {} } pub mod pictures { use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "music_metadata_pictures")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub inode: i64, #[sea_orm(primary_key, auto_increment = false)] pub position: i32, #[sea_orm(column_type = "Blob")] pub block_header: Vec, pub data_offset: i64, pub data_length: i64, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation {} impl ActiveModelBehavior for ActiveModel {} } } 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 virtual_size_calculation() { let mut metadata = MusicMetadata::default(); metadata.header = vec![0u8; 100]; metadata.picture_data_ranges = vec![(0, 50)]; metadata.real_audio_start = 200; let file_size = 1000u64; let virtual_size = metadata.virtual_size(file_size); assert_eq!(virtual_size, 954); } #[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); } }