Handle SIGTERM, fix mp3 handler
This commit is contained in:
Generated
+11
-60
@@ -385,15 +385,6 @@ dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borsh"
|
||||
version = "1.7.0"
|
||||
@@ -669,17 +660,6 @@ dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
|
||||
dependencies = [
|
||||
"dispatch2",
|
||||
"nix 0.31.3",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctutils"
|
||||
version = "0.4.2"
|
||||
@@ -775,18 +755,6 @@ dependencies = [
|
||||
"ctutils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.6"
|
||||
@@ -1532,7 +1500,6 @@ name = "musicfs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"ctrlc",
|
||||
"fuser",
|
||||
"libc",
|
||||
"notify",
|
||||
@@ -1570,18 +1537,6 @@ dependencies = [
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "8.2.0"
|
||||
@@ -1675,21 +1630,6 @@ dependencies = [
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
|
||||
dependencies = [
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
@@ -2407,6 +2347,16 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
@@ -2946,6 +2896,7 @@ dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
|
||||
+1
-2
@@ -5,7 +5,6 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.6.1", features = ["derive"] }
|
||||
ctrlc = "3.5.2"
|
||||
fuser = "0.17.0"
|
||||
libc = "0.2.186"
|
||||
notify = "8.2.0"
|
||||
@@ -16,7 +15,7 @@ symphonia = { version = "0.5", default-features = false, features = [
|
||||
twox-hash = "2.1.2"
|
||||
|
||||
sea-orm = { version = "2.0.0-rc", features = [ "sqlx-postgres", "runtime-tokio", "macros" ] }
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
+2
-1
@@ -28,6 +28,7 @@
|
||||
git
|
||||
just
|
||||
flac
|
||||
ffmpeg
|
||||
|
||||
opencode
|
||||
];
|
||||
@@ -36,7 +37,7 @@
|
||||
after = [ "devenv:processes:postgres" ];
|
||||
exec = ''
|
||||
cargo run -- \
|
||||
--source /home/fujin/Music/DDT \
|
||||
--source /home/fujin/Music \
|
||||
--mountpoint /tmp/rust-fuse \
|
||||
--database "postgresql://fujin@localhost/musicfs?host=$PGHOST"
|
||||
'';
|
||||
|
||||
+9
-15
@@ -1,8 +1,6 @@
|
||||
use clap::Parser;
|
||||
use musicfs::origins::local::watcher::FileWatcher;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
@@ -22,15 +20,6 @@ async fn main() {
|
||||
let args = Args::parse();
|
||||
let mountpoint = args.mountpoint;
|
||||
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let r_clone = running.clone();
|
||||
|
||||
ctrlc::set_handler(move || {
|
||||
println!("Ctrl+C received, shutting down");
|
||||
r_clone.store(false, Ordering::SeqCst);
|
||||
})
|
||||
.expect("Error setting Ctrl+C handler");
|
||||
|
||||
let db = sea_orm::Database::connect(&args.database).await.unwrap();
|
||||
|
||||
let fs = musicfs::origins::local::LocalOrigin::new(args.source.clone(), mountpoint.clone(), db)
|
||||
@@ -40,10 +29,15 @@ async fn main() {
|
||||
fs.watch();
|
||||
|
||||
let cfg = fuser::Config::default();
|
||||
let session = fuser::spawn_mount2(fs, &mountpoint, &cfg);
|
||||
let session =
|
||||
fuser::spawn_mount2(fs, &mountpoint, &cfg).expect("Failed to mount FUSE filesystem");
|
||||
|
||||
while running.load(Ordering::SeqCst) {
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
let mut sigint = signal(SignalKind::interrupt()).expect("Failed to register SIGINT handler");
|
||||
let mut sigterm = signal(SignalKind::terminate()).expect("Failed to register SIGTERM handler");
|
||||
|
||||
tokio::select! {
|
||||
_ = sigint.recv() => println!("Received SIGINT, shutting down"),
|
||||
_ = sigterm.recv() => println!("Received SIGTERM, shutting down"),
|
||||
}
|
||||
|
||||
println!("Unmounting");
|
||||
|
||||
+3
-10
@@ -63,7 +63,7 @@ pub async fn save_music_metadata(
|
||||
.map(|(pos, (hdr, (offset, len)))| pictures::ActiveModel {
|
||||
inode: Set(inode),
|
||||
position: Set(pos as i32),
|
||||
block_header: Set(hdr.to_vec()),
|
||||
block_header: Set(hdr.clone()),
|
||||
data_offset: Set(*offset as i64),
|
||||
data_length: Set(*len as i64),
|
||||
}),
|
||||
@@ -157,15 +157,8 @@ pub async fn restore_music_metadata_from_db(
|
||||
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_block_headers: Vec<Vec<u8>> =
|
||||
sorted_pics.iter().map(|p| p.block_header.clone()).collect();
|
||||
|
||||
let picture_data_ranges: Vec<(u64, u64)> = sorted_pics
|
||||
.iter()
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::music::flac::FlacMusicMetadataEncoder;
|
||||
use crate::music::metadata::MusicMetadata;
|
||||
use crate::music::mp3::Mp3MusicMetadataEncoder;
|
||||
|
||||
/// Bakes the current (possibly overridden) tag fields of a [`MusicMetadata`]
|
||||
/// into its in-memory `header`. The original media file is never modified;
|
||||
/// the rebuilt header is served on the fly at read time, ahead of the
|
||||
/// externalized frames and the original audio.
|
||||
pub trait MusicMetadataEncoder {
|
||||
fn encode(&self, metadata: &mut MusicMetadata);
|
||||
}
|
||||
|
||||
/// Selects the right [`MusicMetadataEncoder`] for a given file.
|
||||
pub struct MusicMetadataEncoderFactory;
|
||||
|
||||
impl MusicMetadataEncoderFactory {
|
||||
pub fn for_path(path: &Path) -> Option<Box<dyn MusicMetadataEncoder>> {
|
||||
match path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(str::to_ascii_lowercase)
|
||||
.as_deref()
|
||||
{
|
||||
Some("flac") => Some(Box::new(FlacMusicMetadataEncoder)),
|
||||
Some("mp3") => Some(Box::new(Mp3MusicMetadataEncoder)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
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;
|
||||
|
||||
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<MusicMetadata> {
|
||||
match parse_flac_metadata(path) {
|
||||
Some(mm) => Some(mm),
|
||||
None => {
|
||||
eprintln!(
|
||||
"musicfs: failed to parse FLAC metadata from {}",
|
||||
path.display()
|
||||
);
|
||||
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<MusicMetadata> {
|
||||
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<u8>)>,
|
||||
picture_block_headers: Vec<Vec<u8>>,
|
||||
picture_data_ranges: Vec<(u64, u64)>,
|
||||
audio_start: u64,
|
||||
}
|
||||
|
||||
fn parse_flac(path: &Path) -> Option<FlacParsed> {
|
||||
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<u8>)> = vec![];
|
||||
let mut picture_block_headers: Vec<Vec<u8>> = 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<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((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<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, 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<u8> {
|
||||
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<String> = 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);
|
||||
}
|
||||
}
|
||||
+24
-404
@@ -1,29 +1,4 @@
|
||||
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)
|
||||
}
|
||||
use symphonia::core::meta::{MetadataRevision, StandardTagKey};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct MusicMetadata {
|
||||
@@ -33,7 +8,10 @@ pub struct MusicMetadata {
|
||||
pub track_title: String,
|
||||
pub other_tags: Vec<String>,
|
||||
pub header: Vec<u8>,
|
||||
pub picture_block_headers: Vec<[u8; 4]>,
|
||||
/// Per-externalized-region reinsert prefix kept in memory. FLAC stores its
|
||||
/// 4-byte PICTURE block header here; MP3 keeps whole frames in the original
|
||||
/// file and stores an empty prefix.
|
||||
pub picture_block_headers: Vec<Vec<u8>>,
|
||||
pub picture_data_ranges: Vec<(u64, u64)>,
|
||||
pub real_audio_start: u64,
|
||||
pub vorbis_comment_offset: u64,
|
||||
@@ -41,262 +19,34 @@ pub struct MusicMetadata {
|
||||
}
|
||||
|
||||
impl MusicMetadata {
|
||||
/// Size of the virtual file: rebuilt header + externalized regions + the
|
||||
/// original audio tail. Format-agnostic.
|
||||
pub fn virtual_size(&self, real_file_size: u64) -> u64 {
|
||||
let pictures_size: u64 = self
|
||||
.picture_data_ranges
|
||||
.picture_block_headers
|
||||
.iter()
|
||||
.map(|(_, len)| 4 + len)
|
||||
.zip(self.picture_data_ranges.iter())
|
||||
.map(|(prefix, (_, len))| prefix.len() as u64 + len)
|
||||
.sum();
|
||||
self.header.len() as u64 + pictures_size + (real_file_size - self.real_audio_start)
|
||||
}
|
||||
|
||||
pub fn parse_music_metadata(path: &Path) -> Option<MusicMetadata> {
|
||||
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::<i32>().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<u8>)>,
|
||||
picture_block_headers: Vec<[u8; 4]>,
|
||||
picture_data_ranges: Vec<(u64, u64)>,
|
||||
audio_start: u64,
|
||||
}
|
||||
|
||||
fn parse_flac(path: &Path) -> Option<FlacParsed> {
|
||||
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<u8>)> = 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));
|
||||
/// Map symphonia's normalized tags onto the shared `MusicMetadata` fields.
|
||||
/// Used by every format parser so the field mapping lives in one place.
|
||||
pub(crate) fn extract_standard_tags(revision: &MetadataRevision, out: &mut MusicMetadata) {
|
||||
for tag in revision.tags() {
|
||||
let value = tag.value.to_string();
|
||||
match tag.std_key {
|
||||
Some(StandardTagKey::Artist) => out.artist.push(value),
|
||||
Some(StandardTagKey::Album) => out.album = value,
|
||||
Some(StandardTagKey::TrackNumber) => {
|
||||
out.track_number = value.parse::<i32>().unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
pos += length;
|
||||
if is_last {
|
||||
break;
|
||||
Some(StandardTagKey::TrackTitle) => out.track_title = value,
|
||||
_ => out.other_tags.push(format!("{}={}", tag.key, value)),
|
||||
}
|
||||
}
|
||||
|
||||
// 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<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((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<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, 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 {
|
||||
@@ -382,150 +132,20 @@ pub mod db {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_vorbis_comment(metadata: &MusicMetadata) -> Vec<u8> {
|
||||
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<String> = 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_block_headers = vec![vec![0u8; 4]];
|
||||
metadata.picture_data_ranges = vec![(0, 50)];
|
||||
metadata.real_audio_start = 200;
|
||||
let file_size = 1000u64;
|
||||
let virtual_size = metadata.virtual_size(file_size);
|
||||
// 100 (header) + 4 (prefix) + 50 (picture data) + 800 (audio tail)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
pub mod db;
|
||||
pub mod encoder;
|
||||
pub mod flac;
|
||||
pub mod metadata;
|
||||
pub mod mp3;
|
||||
pub mod parser;
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
use std::{
|
||||
fs,
|
||||
io::{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;
|
||||
|
||||
/// Frames we replace from our own tag fields. Every other frame in the source
|
||||
/// ID3 tag is preserved verbatim via externalization.
|
||||
const OVERRIDE_FRAME_IDS: [[u8; 4]; 4] = [*b"TIT2", *b"TALB", *b"TPE1", *b"TRCK"];
|
||||
|
||||
/// MP3 parser. Reads ID3 tags (via symphonia) and locates where the audio
|
||||
/// frames begin (end of the ID3v2 tag).
|
||||
pub struct Mp3MusicMetadataParser;
|
||||
|
||||
impl MusicMetadataParser for Mp3MusicMetadataParser {
|
||||
fn parse(&self, path: &Path) -> Option<MusicMetadata> {
|
||||
match parse_mp3_metadata(path) {
|
||||
Some(mm) => Some(mm),
|
||||
None => {
|
||||
eprintln!(
|
||||
"musicfs: failed to parse MP3 metadata from {}",
|
||||
path.display()
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode a 28-bit syncsafe integer (7 bits per byte) as used by ID3v2 sizes.
|
||||
fn syncsafe(b: [u8; 4]) -> u64 {
|
||||
((b[0] as u64) << 21) | ((b[1] as u64) << 14) | ((b[2] as u64) << 7) | (b[3] as u64)
|
||||
}
|
||||
|
||||
/// Byte offset where the MP3 audio frames begin: the end of the ID3v2 tag, or
|
||||
/// 0 when there is no tag. Reads only the 10-byte ID3 header.
|
||||
fn id3_audio_start(path: &Path) -> u64 {
|
||||
let mut f = match fs::File::open(path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return 0,
|
||||
};
|
||||
let mut hdr = [0u8; 10];
|
||||
if f.read_exact(&mut hdr).is_err() {
|
||||
return 0;
|
||||
}
|
||||
if &hdr[0..3] != b"ID3" {
|
||||
return 0;
|
||||
}
|
||||
let size = syncsafe([hdr[6], hdr[7], hdr[8], hdr[9]]);
|
||||
// ID3v2.4 footer flag adds another 10 bytes after the tag body.
|
||||
let footer = if hdr[5] & 0x10 != 0 { 10 } else { 0 };
|
||||
10 + size + footer
|
||||
}
|
||||
|
||||
fn parse_mp3_metadata(path: &Path) -> Option<MusicMetadata> {
|
||||
let src = fs::File::open(path).ok()?;
|
||||
let mss = MediaSourceStream::new(Box::new(src), Default::default());
|
||||
let mut hint = Hint::new();
|
||||
hint.with_extension("mp3");
|
||||
|
||||
let meta_opts: MetadataOptions = Default::default();
|
||||
let fmt_opts: FormatOptions = Default::default();
|
||||
|
||||
let mut probed = symphonia::default::get_probe()
|
||||
.format(&hint, mss, &fmt_opts, &meta_opts)
|
||||
.ok()?;
|
||||
|
||||
let mut music_metadata = MusicMetadata::default();
|
||||
|
||||
// ID3v2 tags at the start of the file surface in the probe-level metadata;
|
||||
// fall back to the in-stream metadata otherwise.
|
||||
let mut got_tags = false;
|
||||
if let Some(metadata) = probed.metadata.get() {
|
||||
if let Some(revision) = metadata.current() {
|
||||
extract_standard_tags(revision, &mut music_metadata);
|
||||
got_tags = true;
|
||||
}
|
||||
}
|
||||
if !got_tags {
|
||||
let metadata = probed.format.metadata();
|
||||
if let Some(revision) = metadata.current() {
|
||||
extract_standard_tags(revision, &mut music_metadata);
|
||||
}
|
||||
}
|
||||
|
||||
music_metadata.real_audio_start = id3_audio_start(path);
|
||||
|
||||
// Record the original frames we will preserve (cover art, lyrics, …) so the
|
||||
// encoder can stitch them back in from the original file at read time.
|
||||
let (prefixes, ranges) = parse_id3_preserved_frames(path);
|
||||
music_metadata.picture_block_headers = prefixes;
|
||||
music_metadata.picture_data_ranges = ranges;
|
||||
|
||||
// Untagged files would otherwise build a degenerate empty artist/album path.
|
||||
if music_metadata.artist.is_empty() {
|
||||
music_metadata.artist = vec!["Unknown Artist".to_string()];
|
||||
}
|
||||
if music_metadata.album.is_empty() {
|
||||
music_metadata.album = "Unknown Album".to_string();
|
||||
}
|
||||
|
||||
Some(music_metadata)
|
||||
}
|
||||
|
||||
/// MP3 encoder. Builds a fresh ID3v2.4 tag from the current (possibly
|
||||
/// overridden) tag fields. Preserved frames are not copied into the header;
|
||||
/// their lengths are counted so the tag's size field spans them.
|
||||
pub struct Mp3MusicMetadataEncoder;
|
||||
|
||||
impl MusicMetadataEncoder for Mp3MusicMetadataEncoder {
|
||||
fn encode(&self, metadata: &mut MusicMetadata) {
|
||||
metadata.header = build_id3v2_header(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode a 28-bit syncsafe integer (7 bits per byte) for ID3v2 size fields.
|
||||
fn syncsafe_encode(n: u64) -> [u8; 4] {
|
||||
[
|
||||
((n >> 21) & 0x7f) as u8,
|
||||
((n >> 14) & 0x7f) as u8,
|
||||
((n >> 7) & 0x7f) as u8,
|
||||
(n & 0x7f) as u8,
|
||||
]
|
||||
}
|
||||
|
||||
/// Build a single UTF-8 ID3v2.4 text frame.
|
||||
fn build_id3_text_frame(id: &[u8; 4], text: &str) -> Vec<u8> {
|
||||
let mut body = Vec::with_capacity(text.len() + 1);
|
||||
body.push(0x03); // text encoding: UTF-8
|
||||
body.extend_from_slice(text.as_bytes());
|
||||
|
||||
let mut frame = Vec::with_capacity(10 + body.len());
|
||||
frame.extend_from_slice(id);
|
||||
frame.extend_from_slice(&syncsafe_encode(body.len() as u64));
|
||||
frame.extend_from_slice(&[0, 0]); // frame flags
|
||||
frame.extend_from_slice(&body);
|
||||
frame
|
||||
}
|
||||
|
||||
/// Build the in-memory portion of the virtual ID3v2.4 tag: the 10-byte tag
|
||||
/// header plus our four override frames. Preserved frames and audio are
|
||||
/// stitched in by the read path; the tag size field accounts for them.
|
||||
fn build_id3v2_header(m: &MusicMetadata) -> Vec<u8> {
|
||||
let mut frames = Vec::new();
|
||||
frames.extend(build_id3_text_frame(b"TIT2", &m.track_title));
|
||||
frames.extend(build_id3_text_frame(b"TALB", &m.album));
|
||||
frames.extend(build_id3_text_frame(b"TPE1", &m.artist.join("\0")));
|
||||
frames.extend(build_id3_text_frame(b"TRCK", &m.track_number.to_string()));
|
||||
|
||||
let preserved_len: u64 = m
|
||||
.picture_block_headers
|
||||
.iter()
|
||||
.zip(m.picture_data_ranges.iter())
|
||||
.map(|(prefix, (_, len))| prefix.len() as u64 + len)
|
||||
.sum();
|
||||
|
||||
let body_len = frames.len() as u64 + preserved_len;
|
||||
|
||||
let mut header = Vec::with_capacity(10 + frames.len());
|
||||
header.extend_from_slice(b"ID3");
|
||||
header.extend_from_slice(&[0x04, 0x00, 0x00]); // v2.4.0, no flags
|
||||
header.extend_from_slice(&syncsafe_encode(body_len));
|
||||
header.extend_from_slice(&frames);
|
||||
header
|
||||
}
|
||||
|
||||
/// Walk the source ID3v2 tag and record every frame we do NOT override
|
||||
/// (cover art, lyrics, other text frames) as a whole-frame range in the
|
||||
/// original file. Each gets an empty in-memory prefix — the frame is copied
|
||||
/// verbatim from disk at read time. Returns empty lists when there is no tag
|
||||
/// or the tag is unsynchronised (which can't be externalized verbatim).
|
||||
fn parse_id3_preserved_frames(path: &Path) -> (Vec<Vec<u8>>, Vec<(u64, u64)>) {
|
||||
let mut prefixes: Vec<Vec<u8>> = Vec::new();
|
||||
let mut ranges: Vec<(u64, u64)> = Vec::new();
|
||||
|
||||
let mut f = match fs::File::open(path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return (prefixes, ranges),
|
||||
};
|
||||
let mut hdr = [0u8; 10];
|
||||
if f.read_exact(&mut hdr).is_err() || &hdr[0..3] != b"ID3" {
|
||||
return (prefixes, ranges);
|
||||
}
|
||||
let version = hdr[3];
|
||||
// Unsynchronised tags store bytes we can't copy verbatim; fall back to
|
||||
// passthrough by preserving nothing (the whole original tag is dropped,
|
||||
// but that only loses art on a rare encoding — acceptable for v1).
|
||||
if hdr[5] & 0x80 != 0 {
|
||||
return (prefixes, ranges);
|
||||
}
|
||||
|
||||
let tag_end = id3_audio_start(path);
|
||||
let mut offset: u64 = 10;
|
||||
while offset + 10 <= tag_end {
|
||||
if f.seek(SeekFrom::Start(offset)).is_err() {
|
||||
break;
|
||||
}
|
||||
let mut fh = [0u8; 10];
|
||||
if f.read_exact(&mut fh).is_err() {
|
||||
break;
|
||||
}
|
||||
// A zero frame id marks the start of padding.
|
||||
if fh[0..4].iter().all(|&b| b == 0) {
|
||||
break;
|
||||
}
|
||||
let size = if version >= 4 {
|
||||
syncsafe([fh[4], fh[5], fh[6], fh[7]])
|
||||
} else {
|
||||
u32::from_be_bytes([fh[4], fh[5], fh[6], fh[7]]) as u64
|
||||
};
|
||||
let frame_total = 10 + size;
|
||||
if size == 0 || offset + frame_total > tag_end {
|
||||
break;
|
||||
}
|
||||
let id = &fh[0..4];
|
||||
let is_override = OVERRIDE_FRAME_IDS.iter().any(|o| &o[..] == id);
|
||||
if !is_override {
|
||||
prefixes.push(Vec::new());
|
||||
ranges.push((offset, frame_total));
|
||||
}
|
||||
offset += frame_total;
|
||||
}
|
||||
|
||||
(prefixes, ranges)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
|
||||
#[test]
|
||||
fn syncsafe_decodes_seven_bit_groups() {
|
||||
assert_eq!(syncsafe([0, 0, 0, 0x23]), 35);
|
||||
assert_eq!(syncsafe([0, 0, 1, 0]), 128);
|
||||
}
|
||||
|
||||
fn write_temp(bytes: &[u8]) -> tempfile::NamedTempFile {
|
||||
let mut f = tempfile::NamedTempFile::new().unwrap();
|
||||
f.write_all(bytes).unwrap();
|
||||
f.flush().unwrap();
|
||||
f
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id3_audio_start_after_tag() {
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(b"ID3");
|
||||
buf.extend_from_slice(&[0x04, 0x00, 0x00]); // version + flags
|
||||
buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x23]); // syncsafe size = 35
|
||||
buf.extend_from_slice(&[0u8; 35]); // tag body
|
||||
let f = write_temp(&buf);
|
||||
assert_eq!(id3_audio_start(f.path()), 45);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id3_audio_start_with_footer() {
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(b"ID3");
|
||||
buf.extend_from_slice(&[0x04, 0x00, 0x10]); // footer flag set
|
||||
buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x23]); // size = 35
|
||||
buf.extend_from_slice(&[0u8; 45]);
|
||||
let f = write_temp(&buf);
|
||||
assert_eq!(id3_audio_start(f.path()), 55);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id3_audio_start_no_tag() {
|
||||
// Raw MP3 frame sync, no ID3 tag.
|
||||
let f = write_temp(&[0xFF, 0xFB, 0x40, 0xC0, 0x00, 0x00]);
|
||||
assert_eq!(id3_audio_start(f.path()), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syncsafe_encode_inverts_syncsafe() {
|
||||
for n in [0u64, 35, 128, 30696, 0x0FFF_FFFF] {
|
||||
assert_eq!(syncsafe(syncsafe_encode(n)), n);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_id3v2_header_structure() {
|
||||
let mm = MusicMetadata {
|
||||
track_title: "Title".to_string(),
|
||||
album: "Album".to_string(),
|
||||
artist: vec!["A".to_string(), "B".to_string()],
|
||||
track_number: 7,
|
||||
// one preserved frame of total length 20
|
||||
picture_block_headers: vec![Vec::new()],
|
||||
picture_data_ranges: vec![(100, 20)],
|
||||
..MusicMetadata::default()
|
||||
};
|
||||
let header = build_id3v2_header(&mm);
|
||||
|
||||
assert_eq!(&header[0..3], b"ID3");
|
||||
assert_eq!(&header[3..6], &[0x04, 0x00, 0x00]);
|
||||
|
||||
let frames_len = header.len() as u64 - 10;
|
||||
let declared = syncsafe([header[6], header[7], header[8], header[9]]);
|
||||
// size field spans our frames + the preserved frame's 20 bytes
|
||||
assert_eq!(declared, frames_len + 20);
|
||||
|
||||
assert!(header.windows(4).any(|w| w == b"TIT2"));
|
||||
assert!(header.windows(4).any(|w| w == b"TPE1"));
|
||||
}
|
||||
|
||||
fn id3_frame(id: &[u8; 4], data: &[u8], syncsafe_size: bool) -> Vec<u8> {
|
||||
let mut v = Vec::new();
|
||||
v.extend_from_slice(id);
|
||||
if syncsafe_size {
|
||||
v.extend_from_slice(&syncsafe_encode(data.len() as u64));
|
||||
} else {
|
||||
v.extend_from_slice(&(data.len() as u32).to_be_bytes());
|
||||
}
|
||||
v.extend_from_slice(&[0, 0]); // flags
|
||||
v.extend_from_slice(data);
|
||||
v
|
||||
}
|
||||
|
||||
fn build_tag(version: u8, frames: &[u8]) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(b"ID3");
|
||||
buf.extend_from_slice(&[version, 0x00, 0x00]);
|
||||
buf.extend_from_slice(&syncsafe_encode(frames.len() as u64));
|
||||
buf.extend_from_slice(frames);
|
||||
buf
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserved_frames_v23_keeps_apic_drops_overrides() {
|
||||
let mut frames = Vec::new();
|
||||
frames.extend(id3_frame(b"APIC", &[1, 2, 3, 4, 5], false));
|
||||
frames.extend(id3_frame(b"TIT2", &[0x03, b'h', b'i'], false));
|
||||
let f = write_temp(&build_tag(0x03, &frames));
|
||||
|
||||
let (prefixes, ranges) = parse_id3_preserved_frames(f.path());
|
||||
// APIC starts right after the 10-byte tag header; total = 10 + 5
|
||||
assert_eq!(ranges, vec![(10, 15)]);
|
||||
assert_eq!(prefixes, vec![Vec::<u8>::new()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserved_frames_v24_syncsafe_sizes() {
|
||||
let mut frames = Vec::new();
|
||||
frames.extend(id3_frame(b"USLT", &[9, 9, 9], true));
|
||||
frames.extend(id3_frame(b"TALB", &[0x03, b'x'], true));
|
||||
let f = write_temp(&build_tag(0x04, &frames));
|
||||
|
||||
let (prefixes, ranges) = parse_id3_preserved_frames(f.path());
|
||||
assert_eq!(ranges, vec![(10, 13)]); // USLT: 10 + 3
|
||||
assert_eq!(prefixes, vec![Vec::<u8>::new()]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::music::flac::FlacMusicMetadataParser;
|
||||
use crate::music::metadata::MusicMetadata;
|
||||
use crate::music::mp3::Mp3MusicMetadataParser;
|
||||
|
||||
/// A parser that turns a single media file into our shared [`MusicMetadata`].
|
||||
/// Each supported container format provides one implementation.
|
||||
pub trait MusicMetadataParser {
|
||||
fn parse(&self, path: &Path) -> Option<MusicMetadata>;
|
||||
}
|
||||
|
||||
/// Selects the right [`MusicMetadataParser`] for a given file.
|
||||
pub struct MusicMetadataParserFactory;
|
||||
|
||||
impl MusicMetadataParserFactory {
|
||||
/// Returns a parser based on the file extension, or `None` for files we
|
||||
/// don't treat as music.
|
||||
pub fn for_path(path: &Path) -> Option<Box<dyn MusicMetadataParser>> {
|
||||
match path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(str::to_ascii_lowercase)
|
||||
.as_deref()
|
||||
{
|
||||
Some("flac") => Some(Box::new(FlacMusicMetadataParser)),
|
||||
Some("mp3") => Some(Box::new(Mp3MusicMetadataParser)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ pub fn read_bytes_at(path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>
|
||||
pub fn assemble_flac_read(
|
||||
original_path: &Path,
|
||||
header: &[u8],
|
||||
pic_hdrs: &[[u8; 4]],
|
||||
pic_hdrs: &[Vec<u8>],
|
||||
pic_ranges: &[(u64, u64)],
|
||||
real_audio_start: u64,
|
||||
offset: u64,
|
||||
@@ -38,7 +38,8 @@ pub fn assemble_flac_read(
|
||||
|
||||
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_hdr_len = pic_hdr.len() as u64;
|
||||
let pic_hdr_end = virt_pos + pic_hdr_len;
|
||||
let pic_end = pic_hdr_end + data_len;
|
||||
|
||||
if end <= virt_pos {
|
||||
@@ -52,7 +53,7 @@ pub fn assemble_flac_read(
|
||||
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)]);
|
||||
buf.extend_from_slice(&pic_hdr[hdr_from..hdr_to.min(pic_hdr.len())]);
|
||||
}
|
||||
|
||||
let data_start = offset.max(pic_hdr_end);
|
||||
|
||||
@@ -88,7 +88,7 @@ impl LocalOrigin {
|
||||
};
|
||||
|
||||
let size = match &item.music_metadata {
|
||||
Some(mm) if mm.real_audio_start > 0 => mm.virtual_size(metadata.size()),
|
||||
Some(mm) if !mm.header.is_empty() => mm.virtual_size(metadata.size()),
|
||||
_ => metadata.size(),
|
||||
};
|
||||
|
||||
@@ -407,7 +407,7 @@ impl Filesystem for LocalOrigin {
|
||||
let flac = item
|
||||
.music_metadata
|
||||
.as_ref()
|
||||
.filter(|mm| mm.real_audio_start > 0)
|
||||
.filter(|mm| !mm.header.is_empty())
|
||||
.map(|mm| {
|
||||
(
|
||||
mm.header.clone(),
|
||||
|
||||
@@ -9,7 +9,8 @@ use fuser::INodeNo;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
use crate::item::{FileType, Item};
|
||||
use crate::music::metadata::MusicMetadata;
|
||||
use crate::music::encoder::MusicMetadataEncoderFactory;
|
||||
use crate::music::parser::MusicMetadataParserFactory;
|
||||
use crate::virtual_dirs::ensure_virtual_dirs;
|
||||
|
||||
pub fn fill_fileset(map: &Arc<Mutex<BTreeMap<INodeNo, Item>>>, source: &Path, destination: &Path) {
|
||||
@@ -69,12 +70,14 @@ pub fn read_into_map(
|
||||
} else {
|
||||
FileType::File
|
||||
};
|
||||
let music_metadata =
|
||||
if item_path.extension().and_then(std::ffi::OsStr::to_str) == Some("flac") {
|
||||
MusicMetadata::parse_music_metadata(&item_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let music_metadata = MusicMetadataParserFactory::for_path(&item_path)
|
||||
.and_then(|parser| parser.parse(&item_path))
|
||||
.map(|mut mm| {
|
||||
if let Some(encoder) = MusicMetadataEncoderFactory::for_path(&item_path) {
|
||||
encoder.encode(&mut mm);
|
||||
}
|
||||
mm
|
||||
});
|
||||
|
||||
let mut local_path = PathBuf::new();
|
||||
if music_metadata.is_some() {
|
||||
|
||||
@@ -11,14 +11,14 @@ fn assemble_flac_read_picture_header_region() {
|
||||
let path = f.path();
|
||||
|
||||
let header = b"ABCD";
|
||||
let pic_hdr = [0x86, 0x00, 0x00, 0x05];
|
||||
let pic_hdr = vec![0x86u8, 0x00, 0x00, 0x05];
|
||||
let pic_ranges = [(10u64, 5u64)];
|
||||
let real_audio_start = 50u64;
|
||||
|
||||
let result = assemble_flac_read(
|
||||
path,
|
||||
header,
|
||||
&[pic_hdr],
|
||||
&[pic_hdr.clone()],
|
||||
&pic_ranges,
|
||||
real_audio_start,
|
||||
4,
|
||||
@@ -38,14 +38,14 @@ fn assemble_flac_read_picture_data_region() {
|
||||
let path = f.path();
|
||||
|
||||
let header = b"ABCD";
|
||||
let pic_hdr = [0x86, 0x00, 0x00, 0x05];
|
||||
let pic_hdr = vec![0x86u8, 0x00, 0x00, 0x05];
|
||||
let pic_ranges = [(10u64, 5u64)];
|
||||
let real_audio_start = 50u64;
|
||||
|
||||
let result = assemble_flac_read(
|
||||
path,
|
||||
header,
|
||||
&[pic_hdr],
|
||||
&[pic_hdr.clone()],
|
||||
&pic_ranges,
|
||||
real_audio_start,
|
||||
8,
|
||||
|
||||
Reference in New Issue
Block a user