1use anyhow::anyhow;
2use id3::TagLike;
3use id3::frame::{Frame, Content, Picture, PictureType};
4use base64::prelude::*;
5
6use crate::input::Args;
7
8pub fn read_from_tag(tag: &id3::Tag, args: &Args) -> serde_json::Value {
9 let comment = tag.comments().
13 find(|c| c.description.is_empty()).
14 map(|c| remove_nul_byte(&c.text).to_string());
15
16 let covers = tag.pictures().
17 filter(|p| is_cover(p)).
18 map(|p| if args.with_covers {
19 serde_json::json!({
20 "mime_type": p.mime_type,
21 "type": cover_type(p),
22 "description": p.description,
23 "size": p.data.len(),
24 "data": BASE64_STANDARD.encode(&p.data),
25 })
26 } else {
27 serde_json::json!({
28 "mime_type": p.mime_type,
29 "type": cover_type(p),
30 "description": p.description,
31 "size": p.data.len(),
32 })
33 }).
34 collect::<Vec<_>>();
35
36 if tag.version() == id3::Version::Id3v24 {
37 serde_json::json!({
38 "version": format!("{}", tag.version()),
39 "data": {
40 "title": tag.title().map(remove_nul_byte),
41 "artist": tag.artist().map(remove_nul_byte),
42 "album": tag.album().map(remove_nul_byte),
43 "track": tag.track(),
44 "date": tag.date_recorded().map(|ts| format!("{}", ts)),
45 "genre": tag.genre().map(remove_nul_byte),
46 "comment": comment,
47 "covers": covers,
48 },
49 })
50 } else {
51 serde_json::json!({
52 "version": format!("{}", tag.version()),
53 "data": {
54 "title": tag.title().map(remove_nul_byte),
55 "artist": tag.artist().map(remove_nul_byte),
56 "album": tag.album().map(remove_nul_byte),
57 "track": tag.track(),
58 "year": tag.year(),
59 "genre": tag.genre().map(remove_nul_byte),
60 "comment": comment,
61 "covers": covers,
62 },
63 })
64 }
65}
66
67pub fn write_to_tag(
68 json_map: &serde_json::Map<String, serde_json::Value>,
69 tag: &mut id3::Tag,
70 version: Option<id3::Version>,
71) -> anyhow::Result<()> {
72 if let Some(serde_json::Value::Object(fields_map)) = json_map.get("data") {
74 return write_to_tag(fields_map, tag, version);
75 };
76
77 let version = version.unwrap_or_else(|| tag.version());
78
79 for (key, value) in json_map {
80 match key.as_str() {
81 "title" => {
82 if let Some(title) = extract_string("title", value)? {
83 tag.set_title(title);
84 } else {
85 tag.remove_title();
86 }
87 },
88 "artist" => {
89 if let Some(artist) = extract_string("artist", value)? {
90 tag.set_artist(artist);
91 } else {
92 tag.remove_artist();
93 }
94 },
95 "album" => {
96 if let Some(album) = extract_string("album", value)? {
97 tag.set_album(album);
98 } else {
99 tag.remove_album();
100 }
101 },
102 "track" => {
103 if let Some(track) = extract_u32("track", value)? {
104 tag.set_track(track);
105 } else {
106 tag.remove_track();
107 }
108 },
109 "year" if version < id3::Version::Id3v24 => {
110 if let Some(year) = extract_u32("year", value)? {
111 tag.set_year(year.try_into()?);
112 } else {
113 tag.remove_year();
114 }
115 },
116 "date" if version >= id3::Version::Id3v24 => {
117 if let Some(date) = extract_string("date", value)? {
118 tag.set_date_recorded(date.parse()?);
119 } else {
120 tag.remove_date_recorded();
121 }
122 },
123 "genre" => {
124 if let Some(genre) = extract_string("genre", value)? {
125 tag.set_genre(genre);
126 } else {
127 tag.remove_genre();
128 }
129 },
130 "comment" => {
131 let mut comment_frames = tag.remove("COMM");
132 let existing_index = comment_frames.iter().
133 position(|c| c.content().comment().unwrap().description.is_empty());
134 let new_comment_body = extract_string("comment", value)?;
135
136 match (existing_index, new_comment_body) {
137 (Some(index), None) => {
138 comment_frames.remove(index);
139 },
140 (Some(index), Some(text)) => {
141 let existing_comment = comment_frames[index].content().comment().unwrap();
142 let mut new_comment = existing_comment.clone();
143 new_comment.text = text;
144
145 let new_frame = id3::Frame::with_content("COMM", id3::Content::Comment(new_comment));
146 comment_frames[index] = new_frame;
147 },
148 (None, Some(text)) => {
149 let new_comment = id3::frame::Comment {
150 lang: String::new(),
151 description: String::new(),
152 text,
153 };
154 let new_frame = id3::Frame::with_content("COMM", id3::Content::Comment(new_comment));
155
156 comment_frames.push(new_frame);
157 }
158 (None, None) => continue,
159 }
160
161 for frame in comment_frames {
162 tag.add_frame(frame);
163 }
164 },
165 "covers" => {
166 let covers = value.as_array().
167 ok_or_else(|| anyhow!("The `covers` key needs to be an array of entries"))?;
168
169 tag.remove("APIC");
170
171 for cover_data in covers {
172 let cover_data = cover_data.as_object().
173 ok_or_else(|| anyhow!("Entries in the `covers` array need to be objects"))?;
174
175 let mime_type = cover_data.get("mime_type").
176 and_then(serde_json::Value::as_str).
177 map(String::from).
178 unwrap_or_else(|| String::from("image/jpeg"));
179
180 let picture_type = match cover_data.get("type").and_then(serde_json::Value::as_str) {
181 Some("front") => PictureType::CoverFront,
182 Some("back") => PictureType::CoverBack,
183 None => PictureType::CoverFront,
184 _ => PictureType::Other,
185 };
186
187 let data_base64 = cover_data.get("data").
188 and_then(serde_json::Value::as_str).
189 map(String::from).
190 ok_or_else(|| anyhow!("Entries in the `covers` array need to have a base64-encoded `data` field"))?;
191 let data = BASE64_STANDARD.decode(&data_base64)?;
192
193 let description = cover_data.get("description").
194 and_then(serde_json::Value::as_str).
195 map(String::from).
196 unwrap_or_else(String::new);
197
198 let picture = Picture { mime_type, picture_type, data, description };
199
200 tag.add_frame(Frame::with_content("APIC", Content::Picture(picture)));
201 }
202 },
203 _ => (),
204 }
205 }
206
207 Ok(())
208}
209
210
211fn extract_string(label: &str, json_value: &serde_json::Value) -> anyhow::Result<Option<String>> {
212 match json_value {
213 serde_json::Value::Null => Ok(None),
214 serde_json::Value::String(value) => Ok(Some(value.clone())),
215 _ => Err(anyhow!("Invalid string value for \"{}\": {:?}", label, json_value)),
216 }
217}
218
219fn extract_u32(label: &str, json_value: &serde_json::Value) -> anyhow::Result<Option<u32>> {
220 let invalid_number = || anyhow!("Invalid numeric value for \"{}\": {:?}", label, json_value);
221
222 match json_value {
223 serde_json::Value::Null => Ok(None),
224 serde_json::Value::String(value) => Ok(Some(value.parse()?)),
225 serde_json::Value::Number(number) => {
226 let value = number.as_u64().ok_or_else(invalid_number)?.try_into()?;
227 Ok(Some(value))
228 },
229 _ => Err(invalid_number()),
230 }
231}
232
233fn remove_nul_byte(input: &str) -> &str {
234 input.trim_end_matches('\u{0000}')
235}
236
237fn is_cover(picture: &Picture) -> bool {
238 matches!(
239 picture.picture_type,
240 PictureType::CoverFront | PictureType::CoverBack | PictureType::Other
241 )
242}
243
244fn cover_type(picture: &Picture) -> &'static str {
245 match picture.picture_type {
246 PictureType::CoverFront => "front",
247 PictureType::CoverBack => "back",
248 _ => "other",
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_extract_string() {
258 let json = serde_json::json!("String!");
259 let value = extract_string("_", &json).unwrap();
260 assert_eq!(value, Some(String::from("String!")));
261
262 let json = serde_json::json!({ "key": "String!" });
263 let value = extract_string("key", &json.get("key").unwrap()).unwrap();
264 assert_eq!(value, Some(String::from("String!")));
265
266 let json = serde_json::json!({ "key": None::<String> });
267 let value = extract_string("key", &json.get("key").unwrap()).unwrap();
268 assert_eq!(value, None);
269
270 let json = serde_json::json!({ "key": 13 });
271 assert!(extract_string("key", &json.get("key").unwrap()).is_err());
272
273 let json = serde_json::json!({ "key": ["String!"] });
274 assert!(extract_string("key", &json.get("key").unwrap()).is_err());
275 }
276
277 #[test]
278 fn test_extract_u32() {
279 let json = serde_json::json!(42);
280 let value = extract_u32("_", &json).unwrap();
281 assert_eq!(value, Some(42));
282
283 let json = serde_json::json!(None::<u64>);
284 let value = extract_u32("_", &json).unwrap();
285 assert_eq!(value, None);
286
287 let json = serde_json::json!({ "key": "13" });
288 let value = extract_u32("key", &json.get("key").unwrap()).unwrap();
289 assert_eq!(value, Some(13));
290
291 let json = serde_json::json!({ "key": "String!" });
292 assert!(extract_u32("key", &json.get("key").unwrap()).is_err());
293
294 let json = serde_json::json!({ "key": ["String!"] });
295 assert!(extract_u32("key", &json.get("key").unwrap()).is_err());
296
297 let json = serde_json::json!({ "key": u64::MAX });
298 assert!(extract_u32("key", &json.get("key").unwrap()).is_err());
299 }
300}