lofty/id3/v2/items/
language_frame.rs

1use crate::error::{Id3v2Error, Id3v2ErrorKind, Result};
2use crate::id3::v2::frame::content::verify_encoding;
3use crate::id3::v2::header::Id3v2Version;
4use crate::id3::v2::{FrameFlags, FrameHeader, FrameId};
5use crate::tag::items::Lang;
6use crate::util::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text};
7
8use std::borrow::Cow;
9use std::hash::{Hash, Hasher};
10use std::io::Read;
11
12use byteorder::ReadBytesExt;
13
14// Generic struct for a text frame that has a language
15//
16// This exists to deduplicate some code between `CommentFrame` and `UnsynchronizedTextFrame`
17struct LanguageFrame {
18	pub encoding: TextEncoding,
19	pub language: Lang,
20	pub description: String,
21	pub content: String,
22}
23
24impl LanguageFrame {
25	fn parse<R>(reader: &mut R, version: Id3v2Version) -> Result<Option<Self>>
26	where
27		R: Read,
28	{
29		let Ok(encoding_byte) = reader.read_u8() else {
30			return Ok(None);
31		};
32
33		let encoding = verify_encoding(encoding_byte, version)?;
34
35		let mut language = [0; 3];
36		reader.read_exact(&mut language)?;
37
38		let description = decode_text(
39			reader,
40			TextDecodeOptions::new().encoding(encoding).terminated(true),
41		)?
42		.content;
43		let content = decode_text(reader, TextDecodeOptions::new().encoding(encoding))?.content;
44
45		Ok(Some(Self {
46			encoding,
47			language,
48			description,
49			content,
50		}))
51	}
52
53	fn create_bytes(
54		mut encoding: TextEncoding,
55		language: [u8; 3],
56		description: &str,
57		content: &str,
58		is_id3v23: bool,
59	) -> Result<Vec<u8>> {
60		if is_id3v23 {
61			encoding = encoding.to_id3v23();
62		}
63
64		let mut bytes = vec![encoding as u8];
65
66		if language.len() != 3 || language.iter().any(|c| !c.is_ascii_alphabetic()) {
67			return Err(Id3v2Error::new(Id3v2ErrorKind::InvalidLanguage(language)).into());
68		}
69
70		bytes.extend(language);
71		bytes.extend(encode_text(description, encoding, true).iter());
72		bytes.extend(encode_text(content, encoding, false));
73
74		Ok(bytes)
75	}
76}
77
78/// An `ID3v2` comment frame
79///
80/// Similar to `TXXX` and `WXXX` frames, comments are told apart by their descriptions.
81#[derive(Clone, Debug, Eq)]
82pub struct CommentFrame<'a> {
83	pub(crate) header: FrameHeader<'a>,
84	/// The encoding of the description and comment text
85	pub encoding: TextEncoding,
86	/// ISO-639-2 language code (3 bytes)
87	pub language: Lang,
88	/// Unique content description
89	pub description: String,
90	/// The actual frame content
91	pub content: String,
92}
93
94impl PartialEq for CommentFrame<'_> {
95	fn eq(&self, other: &Self) -> bool {
96		self.language == other.language && self.description == other.description
97	}
98}
99
100impl Hash for CommentFrame<'_> {
101	fn hash<H: Hasher>(&self, state: &mut H) {
102		self.language.hash(state);
103		self.description.hash(state);
104	}
105}
106
107impl CommentFrame<'_> {
108	const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("COMM"));
109
110	/// Create a new [`CommentFrame`]
111	pub fn new(
112		encoding: TextEncoding,
113		language: Lang,
114		description: String,
115		content: String,
116	) -> Self {
117		let header = FrameHeader::new(Self::FRAME_ID, FrameFlags::default());
118		Self {
119			header,
120			encoding,
121			language,
122			description,
123			content,
124		}
125	}
126
127	/// Get the ID for the frame
128	pub fn id(&self) -> FrameId<'_> {
129		Self::FRAME_ID
130	}
131
132	/// Get the flags for the frame
133	pub fn flags(&self) -> FrameFlags {
134		self.header.flags
135	}
136
137	/// Set the flags for the frame
138	pub fn set_flags(&mut self, flags: FrameFlags) {
139		self.header.flags = flags;
140	}
141
142	/// Read a [`CommentFrame`] from a slice
143	///
144	/// NOTE: This expects the frame header to have already been skipped
145	///
146	/// # Errors
147	///
148	/// * Unable to decode the text
149	///
150	/// ID3v2.2:
151	///
152	/// * The encoding is not [`TextEncoding::Latin1`] or [`TextEncoding::UTF16`]
153	pub fn parse<R>(
154		reader: &mut R,
155		frame_flags: FrameFlags,
156		version: Id3v2Version,
157	) -> Result<Option<Self>>
158	where
159		R: Read,
160	{
161		let Some(language_frame) = LanguageFrame::parse(reader, version)? else {
162			return Ok(None);
163		};
164
165		let header = FrameHeader::new(Self::FRAME_ID, frame_flags);
166		Ok(Some(Self {
167			header,
168			encoding: language_frame.encoding,
169			language: language_frame.language,
170			description: language_frame.description,
171			content: language_frame.content,
172		}))
173	}
174
175	/// Convert a [`CommentFrame`] to a byte vec
176	///
177	/// NOTE: This does not include a frame header
178	///
179	/// # Errors
180	///
181	/// * `language` is not exactly 3 bytes
182	/// * `language` contains invalid characters (Only `'a'..='z'` and `'A'..='Z'` allowed)
183	pub fn as_bytes(&self, is_id3v23: bool) -> Result<Vec<u8>> {
184		LanguageFrame::create_bytes(
185			self.encoding,
186			self.language,
187			&self.description,
188			&self.content,
189			is_id3v23,
190		)
191	}
192}
193
194/// An `ID3v2` unsynchronized lyrics/text frame
195///
196/// Similar to `TXXX` and `WXXX` frames, USLT frames are told apart by their descriptions.
197#[derive(Clone, Debug, Eq)]
198pub struct UnsynchronizedTextFrame<'a> {
199	pub(crate) header: FrameHeader<'a>,
200	/// The encoding of the description and content
201	pub encoding: TextEncoding,
202	/// ISO-639-2 language code (3 bytes)
203	pub language: Lang,
204	/// Unique content description
205	pub description: String,
206	/// The actual frame content
207	pub content: String,
208}
209
210impl PartialEq for UnsynchronizedTextFrame<'_> {
211	fn eq(&self, other: &Self) -> bool {
212		self.language == other.language && self.description == other.description
213	}
214}
215
216impl Hash for UnsynchronizedTextFrame<'_> {
217	fn hash<H: Hasher>(&self, state: &mut H) {
218		self.language.hash(state);
219		self.description.hash(state);
220	}
221}
222
223impl UnsynchronizedTextFrame<'_> {
224	const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("USLT"));
225
226	/// Create a new [`UnsynchronizedTextFrame`]
227	pub fn new(
228		encoding: TextEncoding,
229		language: Lang,
230		description: String,
231		content: String,
232	) -> Self {
233		let header = FrameHeader::new(Self::FRAME_ID, FrameFlags::default());
234		Self {
235			header,
236			encoding,
237			language,
238			description,
239			content,
240		}
241	}
242
243	/// Get the ID for the frame
244	pub fn id(&self) -> FrameId<'_> {
245		Self::FRAME_ID
246	}
247
248	/// Get the flags for the frame
249	pub fn flags(&self) -> FrameFlags {
250		self.header.flags
251	}
252
253	/// Set the flags for the frame
254	pub fn set_flags(&mut self, flags: FrameFlags) {
255		self.header.flags = flags;
256	}
257
258	/// Read a [`UnsynchronizedTextFrame`] from a slice
259	///
260	/// NOTE: This expects the frame header to have already been skipped
261	///
262	/// # Errors
263	///
264	/// * Unable to decode the text
265	///
266	/// ID3v2.2:
267	///
268	/// * The encoding is not [`TextEncoding::Latin1`] or [`TextEncoding::UTF16`]
269	pub fn parse<R>(
270		reader: &mut R,
271		frame_flags: FrameFlags,
272		version: Id3v2Version,
273	) -> Result<Option<Self>>
274	where
275		R: Read,
276	{
277		let Some(language_frame) = LanguageFrame::parse(reader, version)? else {
278			return Ok(None);
279		};
280
281		let header = FrameHeader::new(Self::FRAME_ID, frame_flags);
282		Ok(Some(Self {
283			header,
284			encoding: language_frame.encoding,
285			language: language_frame.language,
286			description: language_frame.description,
287			content: language_frame.content,
288		}))
289	}
290
291	/// Convert a [`UnsynchronizedTextFrame`] to a byte vec
292	///
293	/// NOTE: This does not include a frame header
294	///
295	/// # Errors
296	///
297	/// * `language` is not exactly 3 bytes
298	/// * `language` contains invalid characters (Only `'a'..='z'` and `'A'..='Z'` allowed)
299	pub fn as_bytes(&self, is_id3v23: bool) -> Result<Vec<u8>> {
300		LanguageFrame::create_bytes(
301			self.encoding,
302			self.language,
303			&self.description,
304			&self.content,
305			is_id3v23,
306		)
307	}
308}