1use serde::{Deserialize, Deserializer, Serialize};
2use std::collections::HashMap;
3
4fn u32_from_str<'de, D>(deserializer: D) -> Result<u32, D::Error>
11where
12 D: Deserializer<'de>,
13{
14 #[derive(Deserialize)]
15 #[serde(untagged)]
16 enum StringOrNum {
17 String(String),
18 Number(u32),
19 }
20
21 match StringOrNum::deserialize(deserializer)? {
22 StringOrNum::String(s) => s
23 .replace('_', "")
24 .parse::<u32>()
25 .map_err(serde::de::Error::custom),
26 StringOrNum::Number(n) => Ok(n),
27 }
28}
29
30fn bool_from_str<'de, D>(deserializer: D) -> Result<bool, D::Error>
35where
36 D: Deserializer<'de>,
37{
38 #[derive(Deserialize)]
39 #[serde(untagged)]
40 enum StringOrBool {
41 String(String),
42 Bool(bool),
43 }
44
45 match StringOrBool::deserialize(deserializer)? {
46 StringOrBool::String(s) => match s.to_lowercase().as_str() {
47 "1" | "true" => Ok(true),
48 "0" | "false" => Ok(false),
49 _ => Err(serde::de::Error::custom("Invalid boolean value")),
50 },
51 StringOrBool::Bool(b) => Ok(b),
52 }
53}
54
55#[derive(Serialize, Deserialize, Debug, Clone)]
61pub struct BaseMbidText {
62 pub mbid: String,
64 #[serde(rename = "#text")]
66 pub text: String,
67}
68
69#[derive(Serialize, Deserialize, Debug, Clone)]
73pub struct BaseObject {
74 pub mbid: String,
76 #[serde(default)]
78 pub url: String,
79 #[serde(alias = "#text")]
81 pub name: String,
82}
83
84#[derive(Serialize, Deserialize, Debug, Clone)]
86pub struct TrackImage {
87 pub size: String,
89 #[serde(rename = "#text")]
91 pub text: String,
92}
93
94#[derive(Serialize, Deserialize, Debug, Clone)]
96pub struct Streamable {
97 pub fulltrack: String,
99 #[serde(rename = "#text")]
101 pub text: String,
102}
103
104#[derive(Serialize, Deserialize, Debug, Clone)]
106pub struct Artist {
107 pub name: String,
109 pub mbid: String,
111 #[serde(default)]
113 pub url: String,
114 pub image: Vec<TrackImage>,
116}
117
118#[derive(Serialize, Deserialize, Debug, Clone)]
123pub struct Date {
124 #[serde(deserialize_with = "u32_from_str")]
126 pub uts: u32,
127 #[serde(rename = "#text")]
129 pub text: String,
130}
131
132#[derive(Serialize, Deserialize, Debug, Clone)]
136pub struct Attributes {
137 pub nowplaying: String,
139}
140
141#[derive(Serialize, Deserialize, Debug, Clone)]
143pub struct RankAttr {
144 pub rank: String,
146}
147
148#[derive(Serialize, Deserialize, Debug, Clone)]
155pub struct RecentTrack {
156 pub artist: BaseMbidText,
158 #[serde(deserialize_with = "bool_from_str")]
160 pub streamable: bool,
161 pub image: Vec<TrackImage>,
163 pub album: BaseMbidText,
165 #[serde(rename = "@attr")]
167 pub attr: Option<Attributes>,
168 pub date: Option<Date>,
170 pub name: String,
172 pub mbid: String,
174 pub url: String,
176}
177
178#[derive(Serialize, Deserialize, Debug, Clone)]
182pub struct RecentTrackExtended {
183 pub artist: BaseObject,
185 #[serde(deserialize_with = "bool_from_str")]
187 pub streamable: bool,
188 pub image: Vec<TrackImage>,
190 pub album: BaseObject,
192 #[serde(rename = "@attr")]
194 pub attr: Option<HashMap<String, String>>,
195 pub date: Option<Date>,
197 pub name: String,
199 pub mbid: String,
201 #[serde(default)]
203 pub url: String,
204}
205
206#[derive(Serialize, Deserialize, Debug, Clone)]
212pub struct LovedTrack {
213 pub artist: BaseObject,
215 pub date: Date,
217 pub image: Vec<TrackImage>,
219 pub streamable: Streamable,
221 pub name: String,
223 pub mbid: String,
225 pub url: String,
227}
228
229#[derive(Serialize, Deserialize, Debug, Clone)]
235pub struct TopTrack {
236 pub streamable: Streamable,
238 pub mbid: String,
240 pub name: String,
242 pub image: Vec<TrackImage>,
244 pub artist: BaseObject,
246 pub url: String,
248 #[serde(deserialize_with = "u32_from_str")]
250 pub duration: u32,
251 #[serde(rename = "@attr")]
253 pub attr: RankAttr,
254 #[serde(deserialize_with = "u32_from_str")]
256 pub playcount: u32,
257}
258
259#[derive(Serialize, Deserialize, Debug, Clone)]
263pub struct BaseResponse {
264 pub user: String,
266 #[serde(deserialize_with = "u32_from_str", rename = "totalPages")]
268 pub total_pages: u32,
269 #[serde(deserialize_with = "u32_from_str")]
271 pub page: u32,
272 #[serde(deserialize_with = "u32_from_str", rename = "perPage")]
274 pub per_page: u32,
275 #[serde(deserialize_with = "u32_from_str")]
277 pub total: u32,
278}
279
280#[derive(Serialize, Deserialize, Debug)]
282pub struct RecentTracks {
283 pub track: Vec<RecentTrack>,
284 #[serde(rename = "@attr")]
285 pub attr: BaseResponse,
286}
287
288#[derive(Serialize, Deserialize, Debug)]
289pub struct UserRecentTracks {
290 pub recenttracks: RecentTracks,
291}
292
293#[derive(Serialize, Deserialize, Debug)]
295pub struct RecentTracksExtended {
296 pub track: Vec<RecentTrackExtended>,
297 #[serde(rename = "@attr")]
298 pub attr: BaseResponse,
299}
300
301#[derive(Serialize, Deserialize, Debug)]
302pub struct UserRecentTracksExtended {
303 pub recenttracks: RecentTracksExtended,
304}
305
306#[derive(Serialize, Deserialize, Debug, Clone)]
308pub struct LovedTracks {
309 pub track: Vec<LovedTrack>,
310 #[serde(rename = "@attr")]
311 pub attr: BaseResponse,
312}
313
314#[derive(Serialize, Deserialize, Debug, Clone)]
315pub struct UserLovedTracks {
316 pub lovedtracks: LovedTracks,
317}
318
319#[derive(Serialize, Deserialize, Debug, Clone)]
321pub struct TopTracks {
322 pub track: Vec<TopTrack>,
323 #[serde(rename = "@attr")]
324 pub attr: BaseResponse,
325}
326
327#[derive(Serialize, Deserialize, Debug, Clone)]
328pub struct UserTopTracks {
329 pub toptracks: TopTracks,
330}
331
332pub trait Timestamped {
335 fn get_timestamp(&self) -> Option<u32>;
336}
337
338impl Timestamped for RecentTrack {
339 fn get_timestamp(&self) -> Option<u32> {
340 self.date.as_ref().map(|d| d.uts)
341 }
342}
343
344impl Timestamped for LovedTrack {
345 fn get_timestamp(&self) -> Option<u32> {
346 Some(self.date.uts)
347 }
348}
349
350impl Timestamped for RecentTrackExtended {
351 fn get_timestamp(&self) -> Option<u32> {
352 self.date.as_ref().map(|d| d.uts)
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn test_date_deserialization() {
362 use serde_json::json;
363 let json_value = json!({
364 "uts": "1_234_567_890",
365 "#text": "2009-02-13 23:31:30"
366 });
367 let date: Date = serde_json::from_value(json_value).unwrap();
368 assert_eq!(date.uts, 1_234_567_890);
369 assert_eq!(date.text, "2009-02-13 23:31:30");
370 }
371
372 #[test]
373 fn test_bool_from_str() {
374 use serde_json::json;
375 let json_value = json!({
377 "artist": {"mbid": "", "#text": "Test"},
378 "streamable": "1",
379 "image": [],
380 "album": {"mbid": "", "#text": ""},
381 "name": "Test",
382 "mbid": "",
383 "url": ""
384 });
385 let track: RecentTrack = serde_json::from_value(json_value).unwrap();
386 assert!(track.streamable);
387 }
388
389 #[test]
390 fn test_timestamped_trait() {
391 let track = RecentTrack {
392 artist: BaseMbidText {
393 mbid: String::new(),
394 text: "Artist".to_string(),
395 },
396 streamable: false,
397 image: vec![],
398 album: BaseMbidText {
399 mbid: String::new(),
400 text: String::new(),
401 },
402 attr: None,
403 date: Some(Date {
404 uts: 1_234_567_890,
405 text: "test".to_string(),
406 }),
407 name: "Track".to_string(),
408 mbid: String::new(),
409 url: String::new(),
410 };
411
412 assert_eq!(track.get_timestamp(), Some(1_234_567_890));
413 }
414}