Fix update of metadata for mp3
This commit is contained in:
@@ -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
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user