Skip to main content

id3_json/
json.rs

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    // There could be many comments, but in my music library, it seems like it's common to just
10    // have one with a "description" set to an empty string. So let's have a single "comment" field
11    // that reads and writes there.
12    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    // Check for a nested "data" key to read fields from
73    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}