1use crate::config::WriteOptions;
2use crate::error::{Id3v2Error, Id3v2ErrorKind, Result};
3use crate::id3::v2::frame::FrameFlags;
4use crate::id3::v2::tag::GenresIter;
5use crate::id3::v2::util::synchsafe::SynchsafeInteger;
6use crate::id3::v2::{Frame, FrameId, KeyValueFrame, TextInformationFrame};
7use crate::tag::items::Timestamp;
8
9use std::borrow::Cow;
10use std::io::Write;
11
12use byteorder::{BigEndian, WriteBytesExt};
13
14fn strip_outdated_frames<'a>(
16 frames: &mut dyn Iterator<Item = Frame<'a>>,
17) -> impl Iterator<Item = Frame<'a>> {
18 frames.filter_map(|f| {
19 if f.id().is_valid() {
20 Some(f)
21 } else {
22 log::warn!("Discarding outdated frame: {}", f.id_str());
23 None
24 }
25 })
26}
27
28pub(in crate::id3::v2) fn create_items<W>(
29 writer: &mut W,
30 frames: &mut dyn Iterator<Item = Frame<'_>>,
31 write_options: WriteOptions,
32) -> Result<()>
33where
34 W: Write,
35{
36 for frame in strip_outdated_frames(frames) {
37 verify_frame(&frame)?;
38 let value = frame.as_bytes(write_options)?;
39
40 write_frame(
41 writer,
42 frame.id().as_str(),
43 frame.flags(),
44 &value,
45 write_options,
46 )?;
47 }
48
49 Ok(())
50}
51
52pub(in crate::id3::v2) fn create_items_v3<W>(
53 writer: &mut W,
54 frames: &mut dyn Iterator<Item = Frame<'_>>,
55 write_options: WriteOptions,
56) -> Result<()>
57where
58 W: Write,
59{
60 const FRAMES_TO_DISCARD: &[&str] = &[
62 "ASPI", "EQU2", "RVA2", "SEEK", "SIGN", "TDEN", "TDRL", "TDTG", "TMOO", "TPRO", "TSOA",
63 "TSOP", "TSOT", "TSST",
64 ];
65
66 const IPLS_ID: &str = "IPLS";
67
68 let mut ipls = None;
69 for mut frame in strip_outdated_frames(frames) {
70 if FRAMES_TO_DISCARD.contains(&frame.id_str()) {
71 log::warn!(
72 "Discarding frame: {}, not supported in ID3v2.3",
73 frame.id_str()
74 );
75 continue;
76 }
77
78 verify_frame(&frame)?;
79
80 match frame.id_str() {
81 "TDOR" | "TDRC" => {
86 let is_tdor = frame.id_str() == "TDOR";
87
88 let Frame::Timestamp(f) = &mut frame else {
89 log::warn!(
90 "Discarding frame: {}, not supported in ID3v2.3",
91 frame.id_str()
92 );
93 continue;
94 };
95
96 if f.timestamp.verify().is_err() {
97 log::warn!("Discarding frame: {}, invalid timestamp", frame.id_str());
98 continue;
99 }
100
101 if is_tdor {
102 let year = f.timestamp.year;
103 f.timestamp = Timestamp {
104 year,
105 ..Timestamp::default()
106 };
107
108 f.header.id = FrameId::Valid("TORY".into());
109 } else {
110 let mut new_frames = Vec::with_capacity(3);
111
112 let timestamp = f.timestamp;
113
114 let year = timestamp.year;
115 new_frames.push(Frame::Text(TextInformationFrame::new(
116 FrameId::Valid("TYER".into()),
117 f.encoding.to_id3v23(),
118 year.to_string(),
119 )));
120
121 if let (Some(month), Some(day)) = (timestamp.month, timestamp.day) {
122 let date = format!("{:02}{:02}", day, month);
123 new_frames.push(Frame::Text(TextInformationFrame::new(
124 FrameId::Valid("TDAT".into()),
125 f.encoding.to_id3v23(),
126 date,
127 )));
128 }
129
130 if let (Some(hour), Some(minute)) = (timestamp.hour, timestamp.minute) {
131 let time = format!("{:02}{:02}", hour, minute);
132 new_frames.push(Frame::Text(TextInformationFrame::new(
133 FrameId::Valid("TIME".into()),
134 f.encoding.to_id3v23(),
135 time,
136 )));
137 }
138
139 for mut frame in new_frames {
140 frame.set_flags(f.header.flags);
141 let value = frame.as_bytes(write_options)?;
142
143 write_frame(
144 writer,
145 frame.id().as_str(),
146 frame.flags(),
147 &value,
148 write_options,
149 )?;
150 }
151
152 continue;
153 }
154 },
155 "TCON" => {
158 let Frame::Text(f) = &mut frame else {
159 log::warn!(
160 "Discarding frame: {}, not supported in ID3v2.3",
161 frame.id_str()
162 );
163 continue;
164 };
165
166 let mut new_genre_string = String::new();
167 let genres = GenresIter::new(&f.value, true).collect::<Vec<_>>();
168 for (i, genre) in genres.iter().enumerate() {
169 match *genre {
170 "Remix" => new_genre_string.push_str("(RX)"),
171 "Cover" => new_genre_string.push_str("(CR)"),
172 _ if i == genres.len() - 1 && genre.parse::<u8>().is_err() => {
173 new_genre_string.push_str(genre);
174 },
175 _ => {
176 new_genre_string = format!("{new_genre_string}({genre})");
177 },
178 }
179 }
180
181 f.value = Cow::Owned(new_genre_string);
182 },
183 "TIPL" | "TMCL" => {
187 let Frame::KeyValue(KeyValueFrame {
188 key_value_pairs,
189 encoding,
190 ..
191 }) = &mut frame
192 else {
193 log::warn!(
194 "Discarding frame: {}, not supported in ID3v2.3",
195 frame.id_str()
196 );
197 continue;
198 };
199
200 let ipls_frame;
201 match ipls {
202 Some(ref mut frame) => {
203 ipls_frame = frame;
204 },
205 None => {
206 ipls = Some(TextInformationFrame::new(
207 FrameId::Valid("IPLS".into()),
208 encoding.to_id3v23(),
209 String::new(),
210 ));
211 ipls_frame = ipls.as_mut().unwrap();
212 },
213 }
214
215 for (key, value) in key_value_pairs.drain(..) {
216 if !ipls_frame.value.is_empty() {
217 match &mut ipls_frame.value {
218 Cow::Owned(v) => v.push('\0'),
219 v => {
220 let mut new = String::from(&**v);
221 new.push('\0');
222
223 *v = Cow::Owned(new);
224 },
225 }
226 }
227
228 ipls_frame.value = Cow::Owned(format!("{}{key}\0{value}", ipls_frame.value));
229 }
230
231 continue;
232 },
233 _ => {},
234 }
235
236 let value = frame.as_bytes(write_options)?;
237
238 write_frame(
239 writer,
240 frame.id().as_str(),
241 frame.flags(),
242 &value,
243 write_options,
244 )?;
245 }
246
247 if let Some(ipls) = ipls {
248 let frame = Frame::Text(ipls);
249 let value = frame.as_bytes(write_options)?;
250 write_frame(writer, IPLS_ID, frame.flags(), &value, write_options)?;
251 }
252
253 Ok(())
254}
255
256fn verify_frame(frame: &Frame<'_>) -> Result<()> {
257 match (frame.id().as_str(), frame) {
258 ("APIC", Frame::Picture { .. })
259 | ("USLT", Frame::UnsynchronizedText(_))
260 | ("COMM", Frame::Comment(_))
261 | ("TXXX", Frame::UserText(_))
262 | ("WXXX", Frame::UserUrl(_))
263 | (_, Frame::Binary(_))
264 | ("UFID", Frame::UniqueFileIdentifier(_))
265 | ("POPM", Frame::Popularimeter(_))
266 | ("TIPL" | "TMCL", Frame::KeyValue { .. })
267 | ("WFED" | "GRP1" | "MVNM" | "MVIN", Frame::Text { .. })
268 | ("TDEN" | "TDOR" | "TDRC" | "TDRL" | "TDTG", Frame::Timestamp(_))
269 | ("RVA2", Frame::RelativeVolumeAdjustment(_))
270 | ("PRIV", Frame::Private(_)) => Ok(()),
271 (id, Frame::Text { .. }) if id.starts_with('T') => Ok(()),
272 (id, Frame::Url(_)) if id.starts_with('W') => Ok(()),
273 (id, frame_value) => Err(Id3v2Error::new(Id3v2ErrorKind::BadFrame(
274 id.to_string(),
275 frame_value.name(),
276 ))
277 .into()),
278 }
279}
280
281fn write_frame<W>(
282 writer: &mut W,
283 name: &str,
284 flags: FrameFlags,
285 value: &[u8],
286 write_options: WriteOptions,
287) -> Result<()>
288where
289 W: Write,
290{
291 if flags.encryption.is_some() {
292 write_encrypted(writer, name, value, flags, write_options)?;
293 return Ok(());
294 }
295
296 let len = value.len() as u32;
297 let is_grouping_identity = flags.grouping_identity.is_some();
298
299 write_frame_header(
300 writer,
301 name,
302 if is_grouping_identity { len + 1 } else { len },
303 flags,
304 write_options,
305 )?;
306
307 if is_grouping_identity {
308 writer.write_u8(flags.grouping_identity.unwrap())?;
310 }
311
312 writer.write_all(value)?;
313
314 Ok(())
315}
316
317fn write_encrypted<W>(
318 writer: &mut W,
319 name: &str,
320 value: &[u8],
321 flags: FrameFlags,
322 write_options: WriteOptions,
323) -> Result<()>
324where
325 W: Write,
326{
327 let method_symbol = flags.encryption.unwrap();
329
330 if method_symbol > 0x80 {
331 return Err(
332 Id3v2Error::new(Id3v2ErrorKind::InvalidEncryptionMethodSymbol(method_symbol)).into(),
333 );
334 }
335
336 if let Some(mut len) = flags.data_length_indicator {
337 if len > 0 {
338 write_frame_header(writer, name, (value.len() + 1) as u32, flags, write_options)?;
339 if !write_options.use_id3v23 {
340 len = len.synch()?;
341 }
342
343 writer.write_u32::<BigEndian>(len)?;
344 writer.write_u8(method_symbol)?;
345 writer.write_all(value)?;
346
347 return Ok(());
348 }
349 }
350
351 Err(Id3v2Error::new(Id3v2ErrorKind::MissingDataLengthIndicator).into())
352}
353
354fn write_frame_header<W>(
355 writer: &mut W,
356 name: &str,
357 mut len: u32,
358 flags: FrameFlags,
359 write_options: WriteOptions,
360) -> Result<()>
361where
362 W: Write,
363{
364 let flags = if write_options.use_id3v23 {
365 flags.as_id3v23_bytes()
366 } else {
367 flags.as_id3v24_bytes()
368 };
369
370 writer.write_all(name.as_bytes())?;
371 if !write_options.use_id3v23 {
372 len = len.synch()?;
373 }
374
375 writer.write_u32::<BigEndian>(len)?;
376 writer.write_u16::<BigEndian>(flags)?;
377
378 Ok(())
379}