1use anyhow::anyhow;
2use id3::TagLike;
3
4pub fn read_from_tag(tag: &id3::Tag) -> serde_json::Value {
5 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}