Skip to main content

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    // Check for a nested "data" key to read fields from
47    if let Some(serde_json::Value::Object(fields_map)) = json_map.get("data") {
48        return write_to_tag(fields_map, tag, version);
49    };
50
51    let version = version.unwrap_or_else(|| tag.version());
52
53    for (key, value) in json_map {
54        match key.as_str() {
55            "title" => {
56                if let Some(title) = extract_string("title", value)? {
57                    tag.set_title(title);
58                } else {
59                    tag.remove_title();
60                }
61            },
62            "artist" => {
63                if let Some(artist) = extract_string("artist", value)? {
64                    tag.set_artist(artist);
65                } else {
66                    tag.remove_artist();
67                }
68            },
69            "album" => {
70                if let Some(album) = extract_string("album", value)? {
71                    tag.set_album(album);
72                } else {
73                    tag.remove_album();
74                }
75            },
76            "track" => {
77                if let Some(track) = extract_u32("track", value)? {
78                    tag.set_track(track);
79                } else {
80                    tag.remove_track();
81                }
82            },
83            "year" if version < id3::Version::Id3v24 => {
84                if let Some(year) = extract_u32("year", value)? {
85                    tag.set_year(year.try_into()?);
86                } else {
87                    tag.remove_year();
88                }
89            },
90            "date" if version >= id3::Version::Id3v24 => {
91                if let Some(date) = extract_string("date", value)? {
92                    tag.set_date_recorded(date.parse()?);
93                } else {
94                    tag.remove_date_recorded();
95                }
96            },
97            "genre" => {
98                if let Some(genre) = extract_string("genre", value)? {
99                    tag.set_genre(genre);
100                } else {
101                    tag.remove_genre();
102                }
103            },
104            "comment" => {
105                let mut comment_frames = tag.remove("COMM");
106                let existing_index = comment_frames.iter().
107                    position(|c| c.content().comment().unwrap().description.is_empty());
108                let new_comment_body = extract_string("comment", value)?;
109
110                match (existing_index, new_comment_body) {
111                    (Some(index), None) => {
112                        comment_frames.remove(index);
113                    },
114                    (Some(index), Some(text)) => {
115                        let existing_comment = comment_frames[index].content().comment().unwrap();
116                        let mut new_comment = existing_comment.clone();
117                        new_comment.text = text;
118
119                        let new_frame = id3::Frame::with_content("COMM", id3::Content::Comment(new_comment));
120                        comment_frames[index] = new_frame;
121                    },
122                    (None, Some(text)) => {
123                        let new_comment = id3::frame::Comment {
124                            lang: String::new(),
125                            description: String::new(),
126                            text,
127                        };
128                        let new_frame = id3::Frame::with_content("COMM", id3::Content::Comment(new_comment));
129
130                        comment_frames.push(new_frame);
131                    }
132                    (None, None) => continue,
133                }
134
135                for frame in comment_frames {
136                    tag.add_frame(frame);
137                }
138            },
139            _ => (),
140        }
141    }
142
143    Ok(())
144}
145
146
147fn extract_string(label: &str, json_value: &serde_json::Value) -> anyhow::Result<Option<String>> {
148    match json_value {
149        serde_json::Value::Null          => Ok(None),
150        serde_json::Value::String(value) => Ok(Some(value.clone())),
151        _ => Err(anyhow!("Invalid string value for \"{}\": {:?}", label, json_value)),
152    }
153}
154
155fn extract_u32(label: &str, json_value: &serde_json::Value) -> anyhow::Result<Option<u32>> {
156    let invalid_number = || anyhow!("Invalid numeric value for \"{}\": {:?}", label, json_value);
157
158    match json_value {
159        serde_json::Value::Null => Ok(None),
160        serde_json::Value::String(value) => Ok(Some(value.parse()?)),
161        serde_json::Value::Number(number) => {
162            let value = number.as_u64().ok_or_else(invalid_number)?.try_into()?;
163            Ok(Some(value))
164        },
165        _ => Err(invalid_number()),
166    }
167}
168
169fn remove_nul_byte(input: &str) -> &str {
170    input.trim_end_matches('\u{0000}')
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_extract_string() {
179        let json = serde_json::json!("String!");
180        let value = extract_string("_", &json).unwrap();
181        assert_eq!(value, Some(String::from("String!")));
182
183        let json = serde_json::json!({ "key": "String!" });
184        let value = extract_string("key", &json.get("key").unwrap()).unwrap();
185        assert_eq!(value, Some(String::from("String!")));
186
187        let json = serde_json::json!({ "key": None::<String> });
188        let value = extract_string("key", &json.get("key").unwrap()).unwrap();
189        assert_eq!(value, None);
190
191        let json = serde_json::json!({ "key": 13 });
192        assert!(extract_string("key", &json.get("key").unwrap()).is_err());
193
194        let json = serde_json::json!({ "key": ["String!"] });
195        assert!(extract_string("key", &json.get("key").unwrap()).is_err());
196    }
197
198    #[test]
199    fn test_extract_u32() {
200        let json = serde_json::json!(42);
201        let value = extract_u32("_", &json).unwrap();
202        assert_eq!(value, Some(42));
203
204        let json = serde_json::json!(None::<u64>);
205        let value = extract_u32("_", &json).unwrap();
206        assert_eq!(value, None);
207
208        let json = serde_json::json!({ "key": "13" });
209        let value = extract_u32("key", &json.get("key").unwrap()).unwrap();
210        assert_eq!(value, Some(13));
211
212        let json = serde_json::json!({ "key": "String!" });
213        assert!(extract_u32("key", &json.get("key").unwrap()).is_err());
214
215        let json = serde_json::json!({ "key": ["String!"] });
216        assert!(extract_u32("key", &json.get("key").unwrap()).is_err());
217
218        let json = serde_json::json!({ "key": u64::MAX });
219        assert!(extract_u32("key", &json.get("key").unwrap()).is_err());
220    }
221}