Skip to content

Commit 9b4cf83

Browse files
committed
ID3v2.3: Fix some writing issues
* TDAT is DDMM, was previously written as MMDD * TCON conversion will properly handle refinements * TIPL and TMCL are now merged into IPLS
1 parent 38002d1 commit 9b4cf83

File tree

4 files changed

+84
-22
lines changed

4 files changed

+84
-22
lines changed

lofty/src/id3/v2/read.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ fn construct_tdrc_from_v3(tag: &mut Id3v2Tag) {
8787
break 'build;
8888
}
8989

90-
let (Ok(month), Ok(day)) = (date[..2].parse::<u8>(), date[2..].parse::<u8>()) else {
90+
let (Ok(day), Ok(month)) = (date[..2].parse::<u8>(), date[2..].parse::<u8>()) else {
9191
log::warn!("Invalid TDAT frame, retaining.");
9292
break 'build;
9393
};

lofty/src/id3/v2/tag.rs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@ impl Id3v2Tag {
547547
/// ID3v2.4-style multi-value fields will be split as normal.
548548
pub fn genres(&self) -> Option<impl Iterator<Item = &str>> {
549549
if let Some(Frame::Text(TextInformationFrame { ref value, .. })) = self.get(&GENRE_ID) {
550-
return Some(GenresIter::new(value));
550+
return Some(GenresIter::new(value, false));
551551
}
552552

553553
None
@@ -570,11 +570,16 @@ impl Id3v2Tag {
570570
pub(crate) struct GenresIter<'a> {
571571
value: &'a str,
572572
pos: usize,
573+
preserve_indexes: bool,
573574
}
574575

575576
impl<'a> GenresIter<'a> {
576-
pub fn new(value: &'a str) -> GenresIter<'_> {
577-
GenresIter { value, pos: 0 }
577+
pub fn new(value: &'a str, preserve_indexes: bool) -> GenresIter<'_> {
578+
GenresIter {
579+
value,
580+
pos: 0,
581+
preserve_indexes,
582+
}
578583
}
579584
}
580585

@@ -592,7 +597,7 @@ impl<'a> Iterator for GenresIter<'a> {
592597
let start = self.pos;
593598
let end = self.pos + idx;
594599
self.pos = end + 1;
595-
return Some(parse_genre(&self.value[start..end]));
600+
return Some(parse_genre(&self.value[start..end], self.preserve_indexes));
596601
}
597602

598603
if remainder.starts_with('(') && remainder.contains(')') {
@@ -603,20 +608,20 @@ impl<'a> Iterator for GenresIter<'a> {
603608
if remainder.starts_with("((") {
604609
end += 1;
605610
}
606-
return Some(parse_genre(&self.value[start..end]));
611+
return Some(parse_genre(&self.value[start..end], self.preserve_indexes));
607612
}
608613

609614
self.pos = self.value.len();
610-
Some(parse_genre(remainder))
615+
Some(parse_genre(remainder, self.preserve_indexes))
611616
}
612617
}
613618

614-
fn parse_genre(genre: &str) -> &str {
619+
fn parse_genre(genre: &str, preserve_indexes: bool) -> &str {
615620
if genre.len() > 3 {
616621
return genre;
617622
}
618623
if let Ok(id) = genre.parse::<usize>() {
619-
if id < GENRES.len() {
624+
if id < GENRES.len() && !preserve_indexes {
620625
GENRES[id]
621626
} else {
622627
genre
@@ -1047,7 +1052,7 @@ fn handle_tag_split(tag: &mut Tag, frame: &mut Frame<'_>) -> bool {
10471052
value: content,
10481053
..
10491054
}) if id.as_str() == "TCON" => {
1050-
let genres = GenresIter::new(content);
1055+
let genres = GenresIter::new(content, false);
10511056
for genre in genres {
10521057
tag.items.push(TagItem::new(
10531058
ItemKey::Genre,

lofty/src/id3/v2/tag/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1527,7 +1527,7 @@ fn split_tdrc_on_id3v23_save() {
15271527
let date = tag_re_read
15281528
.get_text(&FrameId::Valid(Cow::Borrowed("TDAT")))
15291529
.expect("Expected TDAT frame");
1530-
assert_eq!(date, "0603");
1530+
assert_eq!(date, "0306");
15311531

15321532
let time = tag_re_read
15331533
.get_text(&FrameId::Valid(Cow::Borrowed("TIME")))

lofty/src/id3/v2/write/frame.rs

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
use crate::error::{Id3v2Error, Id3v2ErrorKind, Result};
22
use crate::id3::v2::frame::{FrameFlags, FrameRef};
3-
use crate::id3::v2::tag::GenresIter;
43
use crate::id3::v2::util::synchsafe::SynchsafeInteger;
5-
use crate::id3::v2::{Frame, FrameId, TextInformationFrame};
4+
use crate::id3::v2::{Frame, FrameId, KeyValueFrame, TextInformationFrame};
65
use crate::tag::items::Timestamp;
76

87
use std::io::Write;
98

9+
use crate::id3::v2::tag::GenresIter;
1010
use byteorder::{BigEndian, WriteBytesExt};
1111

1212
pub(in crate::id3::v2) fn create_items<W>(
@@ -43,12 +43,15 @@ where
4343
{
4444
// These are all frames from ID3v2.4
4545
const FRAMES_TO_DISCARD: &[&str] = &[
46-
"ASPI", "EQU2", "RVA2", "SEEK", "SIGN", "TDEN", "TDRL", "TDTG", "TIPL", "TMCL", "TMOO",
47-
"TPRO", "TSOA", "TSOP", "TSOT", "TSST",
46+
"ASPI", "EQU2", "RVA2", "SEEK", "SIGN", "TDEN", "TDRL", "TDTG", "TMOO", "TPRO", "TSOA",
47+
"TSOP", "TSOT", "TSST",
4848
];
4949

50+
const IPLS_ID: &str = "IPLS";
51+
5052
let is_id3v23 = true;
5153

54+
let mut ipls = None;
5255
for mut frame in frames {
5356
let id = frame.id_str();
5457

@@ -99,7 +102,7 @@ where
99102
)));
100103

101104
if let (Some(month), Some(day)) = (timestamp.month, timestamp.day) {
102-
let date = format!("{:02}{:02}", month, day);
105+
let date = format!("{:02}{:02}", day, month);
103106
new_frames.push(Frame::Text(TextInformationFrame::new(
104107
FrameId::Valid("TDAT".into()),
105108
f.encoding.to_id3v23(),
@@ -142,19 +145,63 @@ where
142145
};
143146

144147
let mut new_genre_string = String::new();
145-
for genre in GenresIter::new(&f.value) {
146-
match genre {
148+
let genres = GenresIter::new(&f.value, true).collect::<Vec<_>>();
149+
for (i, genre) in genres.iter().enumerate() {
150+
match *genre {
147151
"Remix" => new_genre_string.push_str("(RX)"),
148152
"Cover" => new_genre_string.push_str("(CR)"),
149-
_ => {
153+
_ if i == genres.len() - 1 && genre.parse::<u8>().is_err() => {
150154
new_genre_string.push_str(genre);
151155
},
156+
_ => {
157+
new_genre_string.push_str(&format!("({genre})"));
158+
},
152159
}
153160
}
154161

155162
f.value = new_genre_string;
156163
frame.0 = value;
157164
},
165+
// TIPL (Involved people list) and TMCL (Musician credits list) are
166+
// both key-value pairs. ID3v2.3 does not distinguish between the two,
167+
// so we must merge them into a single IPLS frame.
168+
"TIPL" | "TMCL" => {
169+
let mut value = frame.0.clone();
170+
let Frame::KeyValue(KeyValueFrame {
171+
ref mut key_value_pairs,
172+
encoding,
173+
..
174+
}) = value.to_mut()
175+
else {
176+
log::warn!("Discarding frame: {}, not supported in ID3v2.3", id);
177+
continue;
178+
};
179+
180+
let ipls_frame;
181+
match ipls {
182+
Some(ref mut frame) => {
183+
ipls_frame = frame;
184+
},
185+
None => {
186+
ipls = Some(TextInformationFrame::new(
187+
FrameId::Valid("IPLS".into()),
188+
encoding.to_id3v23(),
189+
String::new(),
190+
));
191+
ipls_frame = ipls.as_mut().unwrap();
192+
},
193+
}
194+
195+
for (key, value) in key_value_pairs.drain(..) {
196+
if !ipls_frame.value.is_empty() {
197+
ipls_frame.value.push('\0');
198+
}
199+
200+
ipls_frame.value.push_str(&format!("{}\0{}", key, value));
201+
}
202+
203+
continue;
204+
},
158205
_ => {},
159206
}
160207

@@ -169,6 +216,12 @@ where
169216
)?;
170217
}
171218

219+
if let Some(ipls) = ipls {
220+
let frame = Frame::Text(ipls);
221+
let value = frame.as_bytes(is_id3v23)?;
222+
write_frame(writer, IPLS_ID, frame.flags(), &value, is_id3v23)?;
223+
}
224+
172225
Ok(())
173226
}
174227

@@ -252,10 +305,14 @@ where
252305
);
253306
}
254307

255-
if let Some(len) = flags.data_length_indicator {
308+
if let Some(mut len) = flags.data_length_indicator {
256309
if len > 0 {
257310
write_frame_header(writer, name, (value.len() + 1) as u32, flags, is_id3v23)?;
258-
writer.write_u32::<BigEndian>(len.synch()?)?;
311+
if !is_id3v23 {
312+
len = len.synch()?;
313+
}
314+
315+
writer.write_u32::<BigEndian>(len)?;
259316
writer.write_u8(method_symbol)?;
260317
writer.write_all(value)?;
261318

@@ -282,11 +339,11 @@ where
282339
flags.as_id3v24_bytes()
283340
};
284341

342+
writer.write_all(name.as_bytes())?;
285343
if !is_id3v23 {
286344
len = len.synch()?;
287345
}
288346

289-
writer.write_all(name.as_bytes())?;
290347
writer.write_u32::<BigEndian>(len)?;
291348
writer.write_u16::<BigEndian>(flags)?;
292349

0 commit comments

Comments
 (0)