id3_json/
json.rs

1use anyhow::anyhow;
2use id3::TagLike;
3
4pub fn read_from_tag(tag: &id3::Tag) -> serde_json::Value {
5    // There could be many comments, but in my music library, it seems like it's common to just
6    // have one with a "description" set to an empty string. So let's have a single "comment" field
7    // that reads and writes there.
8    let comment = tag.comments().
9        find(|c| c.description.is_empty()).
10        map(|c| remove_nul_byte(&c.text).to_string());
11
12    if tag.version() == id3::Version::Id3v24 {
13        serde_json::json!({
14            "version": format!("{}", tag.version()),
15            "data": {
16                "title": tag.title().map(remove_nul_byte),
17                "artist": tag.artist().map(remove_nul_byte),
18                "album": tag.album().map(remove_nul_byte),
19                "track": tag.track(),
20                "date": tag.date_recorded().map(|ts| format!("{}", ts)),
21                "genre": tag.genre().map(remove_nul_byte),
22                "comment": comment,
23            },
24        })
25    } else {
26        serde_json::json!({
27            "version": format!("{}", tag.version()),
28            "data": {
29                "title": tag.title().map(remove_nul_byte),
30                "artist": tag.artist().map(remove_nul_byte),
31                "album": tag.album().map(remove_nul_byte),
32                "track": tag.track(),
33                "year": tag.year(),
34                "genre": tag.genre().map(remove_nul_byte),
35                "comment": comment,
36            },
37        })
38    }
39}
40
41pub fn write_to_tag(
42    json_map: serde_json::Map<String, serde_json::Value>,
43    tag: &mut id3::Tag,
44    version: Option<id3::Version>,
45) -> anyhow::Result<()> {
46    let version = version.unwrap_or_else(|| tag.version());
47
48    for (key, value) in json_map {
49        match key.as_str() {
50            "title" => {
51                if let Some(title) = extract_string("title", &value)? {
52                    tag.set_title(title);
53                } else {
54                    tag.remove_title();
55                }
56            },
57            "artist" => {
58                if let Some(artist) = extract_string("artist", &value)? {
59                    tag.set_artist(artist);
60                } else {
61                    tag.remove_artist();
62                }
63            },
64            "album" => {
65                if let Some(album) = extract_string("album", &value)? {
66                    tag.set_album(album);
67                } else {
68                    tag.remove_album();
69                }
70            },
71            "track" => {
72                if let Some(track) = extract_u32("track", &value)? {
73                    tag.set_track(track);
74                } else {
75                    tag.remove_track();
76                }
77            },
78            "year" if version < id3::Version::Id3v24 => {
79                if let Some(year) = extract_u32("year", &value)? {
80                    tag.set_year(year.try_into()?);
81                } else {
82                    tag.remove_year();
83                }
84            },
85            "date" if version >= id3::Version::Id3v24 => {
86                if let Some(date) = extract_string("date", &value)? {
87                    tag.set_date_recorded(date.parse()?);
88                } else {
89                    tag.remove_date_recorded();
90                }
91            },
92            "genre" => {
93                if let Some(genre) = extract_string("genre", &value)? {
94                    tag.set_genre(genre);
95                } else {
96                    tag.remove_genre();
97                }
98            },
99            "comment" => {
100                let mut comment_frames = tag.remove("COMM");
101                let existing_index = comment_frames.iter().
102                    position(|c| c.content().comment().unwrap().description.is_empty());
103                let new_comment_body = extract_string("comment", &value)?;
104
105                match (existing_index, new_comment_body) {
106                    (Some(index), None) => {
107                        comment_frames.remove(index);
108                    },
109                    (Some(index), Some(text)) => {
110                        let existing_comment = comment_frames[index].content().comment().unwrap();
111                        let mut new_comment = existing_comment.clone();
112                        new_comment.text = text;
113
114                        let new_frame = id3::Frame::with_content("COMM", id3::Content::Comment(new_comment));
115                        comment_frames[index] = new_frame;
116                    },
117                    (None, Some(text)) => {
118                        let new_comment = id3::frame::Comment {
119                            lang: String::new(),
120                            description: String::new(),
121                            text,
122                        };
123                        let new_frame = id3::Frame::with_content("COMM", id3::Content::Comment(new_comment));
124
125                        comment_frames.push(new_frame);
126                    }
127                    (None, None) => continue,
128                }
129
130                for frame in comment_frames {
131                    tag.add_frame(frame);
132                }
133            },
134            _ => (),
135        }
136    }
137
138    Ok(())
139}
140
141
142fn extract_string(label: &str, json_value: &serde_json::Value) -> anyhow::Result<Option<String>> {
143    match json_value {
144        serde_json::Value::Null          => Ok(None),
145        serde_json::Value::String(value) => Ok(Some(value.clone())),
146        _ => Err(anyhow!("Invalid string value for \"{}\": {:?}", label, json_value)),
147    }
148}
149
150fn extract_u32(label: &str, json_value: &serde_json::Value) -> anyhow::Result<Option<u32>> {
151    let invalid_number = || anyhow!("Invalid numeric value for \"{}\": {:?}", label, json_value);
152
153    match json_value {
154        serde_json::Value::Null => Ok(None),
155        serde_json::Value::String(value) => Ok(Some(value.parse()?)),
156        serde_json::Value::Number(number) => {
157            let value = number.as_u64().ok_or_else(invalid_number)?.try_into()?;
158            Ok(Some(value))
159        },
160        _ => Err(invalid_number()),
161    }
162}
163
164fn remove_nul_byte(input: &str) -> &str {
165    input.trim_end_matches('\u{0000}')
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_extract_string() {
174        let json = serde_json::json!("String!");
175        let value = extract_string("_", &json).unwrap();
176        assert_eq!(value, Some(String::from("String!")));
177
178        let json = serde_json::json!({ "key": "String!" });
179        let value = extract_string("key", &json.get("key").unwrap()).unwrap();
180        assert_eq!(value, Some(String::from("String!")));
181
182        let json = serde_json::json!({ "key": None::<String> });
183        let value = extract_string("key", &json.get("key").unwrap()).unwrap();
184        assert_eq!(value, None);
185
186        let json = serde_json::json!({ "key": 13 });
187        assert!(extract_string("key", &json.get("key").unwrap()).is_err());
188
189        let json = serde_json::json!({ "key": ["String!"] });
190        assert!(extract_string("key", &json.get("key").unwrap()).is_err());
191    }
192
193    #[test]
194    fn test_extract_u32() {
195        let json = serde_json::json!(42);
196        let value = extract_u32("_", &json).unwrap();
197        assert_eq!(value, Some(42));
198
199        let json = serde_json::json!(None::<u64>);
200        let value = extract_u32("_", &json).unwrap();
201        assert_eq!(value, None);
202
203        let json = serde_json::json!({ "key": "13" });
204        let value = extract_u32("key", &json.get("key").unwrap()).unwrap();
205        assert_eq!(value, Some(13));
206
207        let json = serde_json::json!({ "key": "String!" });
208        assert!(extract_u32("key", &json.get("key").unwrap()).is_err());
209
210        let json = serde_json::json!({ "key": ["String!"] });
211        assert!(extract_u32("key", &json.get("key").unwrap()).is_err());
212
213        let json = serde_json::json!({ "key": u64::MAX });
214        assert!(extract_u32("key", &json.get("key").unwrap()).is_err());
215    }
216}