Fix update of metadata for mp3

This commit is contained in:
Alexander
2026-06-29 13:25:16 +02:00
parent 4e946963e1
commit 1a98d62357
2 changed files with 183 additions and 30 deletions
+142
View File
@@ -110,6 +110,148 @@ fn parse_mp3_metadata(path: &Path) -> Option<MusicMetadata> {
Some(music_metadata)
}
impl MusicMetadata {
/// Apply a freshly written ID3v2 tag and rebuild the in-memory header.
pub fn update_from_id3_data(&mut self, data: &[u8]) {
parse_id3_tag_frames(data, self);
self.header = build_id3v2_header(self);
}
/// Apply a freshly written ID3v1 tag (128-byte "TAG" block) and rebuild
/// the in-memory ID3v2 header. ID3v1 is Latin-1, max 30 chars per field;
/// non-ASCII bytes are replaced with '?' since Latin-1 ≠ UTF-8.
pub fn update_from_id3v1_data(&mut self, data: &[u8]) {
if data.len() < 128 || &data[0..3] != b"TAG" {
return;
}
fn latin1_str(bytes: &[u8]) -> Option<String> {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
if end == 0 {
return None;
}
Some(
bytes[..end]
.iter()
.map(|&b| if b < 0x80 { char::from(b) } else { '?' })
.collect(),
)
}
if let Some(t) = latin1_str(&data[3..33]) {
self.track_title = t;
}
if let Some(a) = latin1_str(&data[33..63]) {
self.artist = vec![a];
}
if let Some(a) = latin1_str(&data[63..93]) {
self.album = a;
}
// ID3v1.1: byte 125 == 0 means byte 126 is the track number
if data[125] == 0 && data[126] != 0 {
self.track_number = data[126] as i32;
}
self.header = build_id3v2_header(self);
}
}
fn decode_id3_text(data: &[u8]) -> String {
if data.is_empty() {
return String::new();
}
match data[0] {
0x03 => String::from_utf8_lossy(&data[1..])
.trim_end_matches('\0')
.to_string(),
0x00 => data[1..]
.iter()
.take_while(|&&b| b != 0)
.map(|&b| char::from(b))
.collect(),
0x01 | 0x02 => {
let raw = &data[1..];
let (src, le) = if raw.len() >= 2 && raw[0] == 0xFF && raw[1] == 0xFE {
(&raw[2..], true)
} else if raw.len() >= 2 && raw[0] == 0xFE && raw[1] == 0xFF {
(&raw[2..], false)
} else {
(raw, true)
};
let words: Vec<u16> = src
.chunks_exact(2)
.map(|c| {
if le {
u16::from_le_bytes([c[0], c[1]])
} else {
u16::from_be_bytes([c[0], c[1]])
}
})
.take_while(|&w| w != 0)
.collect();
String::from_utf16_lossy(&words)
}
_ => String::new(),
}
}
fn parse_id3_tag_frames(data: &[u8], out: &mut MusicMetadata) {
if data.len() < 10 || &data[0..3] != b"ID3" {
return;
}
let version = data[3];
if data[5] & 0x80 != 0 {
return; // unsynchronised — can't copy frames verbatim
}
let tag_end = (10 + syncsafe([data[6], data[7], data[8], data[9]]) as usize).min(data.len());
out.artist.clear();
let mut offset = 10usize;
while offset + 10 <= tag_end {
if data[offset..offset + 4].iter().all(|&b| b == 0) {
break; // padding
}
let size = if version >= 4 {
syncsafe([
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
]) as usize
} else {
u32::from_be_bytes([
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
]) as usize
};
if size == 0 || offset + 10 + size > tag_end {
break;
}
let body = &data[offset + 10..offset + 10 + size];
match &data[offset..offset + 4] {
b"TIT2" => out.track_title = decode_id3_text(body),
b"TALB" => out.album = decode_id3_text(body),
b"TPE1" => {
let text = decode_id3_text(body);
out.artist = text
.split('\0')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
}
b"TRCK" => {
let text = decode_id3_text(body);
out.track_number = text
.split('/')
.next()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
}
_ => {}
}
offset += 10 + size;
}
}
/// 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.
+41 -30
View File
@@ -176,38 +176,49 @@ impl Filesystem for LocalOrigin {
return;
}
};
let music_metadata = match &mut item.music_metadata {
Some(mm) if mm.vorbis_comment_length > 0 => mm,
_ => {
reply.written(written);
return;
match &mut item.music_metadata {
// FLAC: intercept Vorbis comment block writes
Some(mm) if mm.vorbis_comment_length > 0 => {
let vc_data_offset = mm.vorbis_comment_offset;
let vc_hdr_offset = vc_data_offset - 4;
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;
mm.update_from_vorbis_comment_data(&data[from..to]);
Some(mm.clone())
} else {
None
}
} else {
None
}
}
};
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
// MP3: intercept ID3v2 header writes (must start at offset 0)
Some(mm)
if !mm.header.is_empty()
&& write_start == 0
&& data.len() >= 3
&& &data[0..3] == b"ID3" =>
{
mm.update_from_id3_data(data);
Some(mm.clone())
}
} else {
None
// MP3: intercept ID3v1 fallback writes (128-byte "TAG" block)
Some(mm) if !mm.header.is_empty() && data.len() == 128 && &data[0..3] == b"TAG" => {
mm.update_from_id3v1_data(data);
Some(mm.clone())
}
_ => None,
}
};