1use serde::{Deserialize, Deserializer, Serialize};
2use std::collections::HashMap;
3
4fn u32_from_str<'de, D>(deserializer: D) -> Result<u32, D::Error>
6where
7 D: Deserializer<'de>,
8{
9 #[derive(Deserialize)]
10 #[serde(untagged)]
11 enum StringOrNum {
12 String(String),
13 Number(u32),
14 }
15
16 match StringOrNum::deserialize(deserializer)? {
17 StringOrNum::String(s) => s
18 .replace('_', "")
19 .parse::<u32>()
20 .map_err(serde::de::Error::custom),
21 StringOrNum::Number(n) => Ok(n),
22 }
23}
24
25fn bool_from_str<'de, D>(deserializer: D) -> Result<bool, D::Error>
26where
27 D: Deserializer<'de>,
28{
29 #[derive(Deserialize)]
30 #[serde(untagged)]
31 enum StringOrBool {
32 String(String),
33 Bool(bool),
34 }
35
36 match StringOrBool::deserialize(deserializer)? {
37 StringOrBool::String(s) => match s.to_lowercase().as_str() {
38 "1" | "true" => Ok(true),
39 "0" | "false" => Ok(false),
40 _ => Err(serde::de::Error::custom("Invalid boolean value")),
41 },
42 StringOrBool::Bool(b) => Ok(b),
43 }
44}
45
46#[derive(Serialize, Deserialize, Debug, Clone)]
49pub struct BaseMbidText {
50 pub mbid: String,
51 #[serde(rename = "#text")]
52 pub text: String,
53}
54
55#[derive(Serialize, Deserialize, Debug, Clone)]
56pub struct BaseObject {
57 pub mbid: String,
58 #[serde(default)]
59 pub url: String,
60 #[serde(alias = "#text")]
61 pub name: String,
62}
63
64#[derive(Serialize, Deserialize, Debug, Clone)]
65pub struct TrackImage {
66 pub size: String,
67 #[serde(rename = "#text")]
68 pub text: String,
69}
70
71#[derive(Serialize, Deserialize, Debug, Clone)]
72pub struct Streamable {
73 pub fulltrack: String,
74 #[serde(rename = "#text")]
75 pub text: String,
76}
77
78#[derive(Serialize, Deserialize, Debug, Clone)]
79pub struct Artist {
80 pub name: String,
81 pub mbid: String,
82 #[serde(default)]
83 pub url: String,
84 pub image: Vec<TrackImage>,
85}
86
87#[derive(Serialize, Deserialize, Debug, Clone)]
91pub struct Date {
92 #[serde(deserialize_with = "u32_from_str")]
93 pub uts: u32,
94 #[serde(rename = "#text")]
95 pub text: String,
96}
97
98#[derive(Serialize, Deserialize, Debug, Clone)]
101pub struct Attributes {
102 pub nowplaying: String,
103}
104
105#[derive(Serialize, Deserialize, Debug, Clone)]
106pub struct RankAttr {
107 pub rank: String,
108}
109
110#[derive(Serialize, Deserialize, Debug, Clone)]
114pub struct RecentTrack {
115 pub artist: BaseMbidText,
116 #[serde(deserialize_with = "bool_from_str")]
117 pub streamable: bool,
118 pub image: Vec<TrackImage>,
119 pub album: BaseMbidText,
120 #[serde(rename = "@attr")]
121 pub attr: Option<Attributes>,
122 pub date: Option<Date>,
123 pub name: String,
124 pub mbid: String,
125 pub url: String,
126}
127
128#[derive(Serialize, Deserialize, Debug, Clone)]
129pub struct RecentTrackExtended {
130 pub artist: BaseObject,
131 #[serde(deserialize_with = "bool_from_str")]
132 pub streamable: bool,
133 pub image: Vec<TrackImage>,
134 pub album: BaseObject,
135 #[serde(rename = "@attr")]
136 pub attr: Option<HashMap<String, String>>,
137 pub date: Option<Date>,
138 pub name: String,
139 pub mbid: String,
140 #[serde(default)]
141 pub url: String,
142}
143
144#[derive(Serialize, Deserialize, Debug, Clone)]
147pub struct LovedTrack {
148 pub artist: BaseObject,
149 pub date: Date,
150 pub image: Vec<TrackImage>,
151 pub streamable: Streamable,
152 pub name: String,
153 pub mbid: String,
154 pub url: String,
155}
156
157#[derive(Serialize, Deserialize, Debug, Clone)]
160pub struct TopTrack {
161 pub streamable: Streamable,
162 pub mbid: String,
163 pub name: String,
164 pub image: Vec<TrackImage>,
165 pub artist: BaseObject,
166 pub url: String,
167 #[serde(deserialize_with = "u32_from_str")]
168 pub duration: u32,
169 #[serde(rename = "@attr")]
170 pub attr: RankAttr,
171 #[serde(deserialize_with = "u32_from_str")]
172 pub playcount: u32,
173}
174
175#[derive(Serialize, Deserialize, Debug, Clone)]
178pub struct BaseResponse {
179 pub user: String,
180 #[serde(deserialize_with = "u32_from_str", rename = "totalPages")]
181 pub total_pages: u32,
182 #[serde(deserialize_with = "u32_from_str")]
183 pub page: u32,
184 #[serde(deserialize_with = "u32_from_str", rename = "perPage")]
185 pub per_page: u32,
186 #[serde(deserialize_with = "u32_from_str")]
187 pub total: u32,
188}
189
190#[derive(Serialize, Deserialize, Debug)]
192pub struct RecentTracks {
193 pub track: Vec<RecentTrack>,
194 #[serde(rename = "@attr")]
195 pub attr: BaseResponse,
196}
197
198#[derive(Serialize, Deserialize, Debug)]
199pub struct UserRecentTracks {
200 pub recenttracks: RecentTracks,
201}
202
203#[derive(Serialize, Deserialize, Debug)]
205pub struct RecentTracksExtended {
206 pub track: Vec<RecentTrackExtended>,
207 #[serde(rename = "@attr")]
208 pub attr: BaseResponse,
209}
210
211#[derive(Serialize, Deserialize, Debug)]
212pub struct UserRecentTracksExtended {
213 pub recenttracks: RecentTracksExtended,
214}
215
216#[derive(Serialize, Deserialize, Debug, Clone)]
218pub struct LovedTracks {
219 pub track: Vec<LovedTrack>,
220 #[serde(rename = "@attr")]
221 pub attr: BaseResponse,
222}
223
224#[derive(Serialize, Deserialize, Debug, Clone)]
225pub struct UserLovedTracks {
226 pub lovedtracks: LovedTracks,
227}
228
229#[derive(Serialize, Deserialize, Debug, Clone)]
231pub struct TopTracks {
232 pub track: Vec<TopTrack>,
233 #[serde(rename = "@attr")]
234 pub attr: BaseResponse,
235}
236
237#[derive(Serialize, Deserialize, Debug, Clone)]
238pub struct UserTopTracks {
239 pub toptracks: TopTracks,
240}
241
242pub trait Timestamped {
245 fn get_timestamp(&self) -> Option<u32>;
246}
247
248impl Timestamped for RecentTrack {
249 fn get_timestamp(&self) -> Option<u32> {
250 self.date.as_ref().map(|d| d.uts)
251 }
252}
253
254impl Timestamped for LovedTrack {
255 fn get_timestamp(&self) -> Option<u32> {
256 Some(self.date.uts)
257 }
258}
259
260impl Timestamped for RecentTrackExtended {
261 fn get_timestamp(&self) -> Option<u32> {
262 self.date.as_ref().map(|d| d.uts)
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_date_deserialization() {
272 use serde_json::json;
273 let json_value = json!({
274 "uts": "1_234_567_890",
275 "#text": "2009-02-13 23:31:30"
276 });
277 let date: Date = serde_json::from_value(json_value).unwrap();
278 assert_eq!(date.uts, 1_234_567_890);
279 assert_eq!(date.text, "2009-02-13 23:31:30");
280 }
281
282 #[test]
283 fn test_bool_from_str() {
284 use serde_json::json;
285 let json_value = json!({
287 "artist": {"mbid": "", "#text": "Test"},
288 "streamable": "1",
289 "image": [],
290 "album": {"mbid": "", "#text": ""},
291 "name": "Test",
292 "mbid": "",
293 "url": ""
294 });
295 let track: RecentTrack = serde_json::from_value(json_value).unwrap();
296 assert!(track.streamable);
297 }
298
299 #[test]
300 fn test_timestamped_trait() {
301 let track = RecentTrack {
302 artist: BaseMbidText {
303 mbid: String::new(),
304 text: "Artist".to_string(),
305 },
306 streamable: false,
307 image: vec![],
308 album: BaseMbidText {
309 mbid: String::new(),
310 text: String::new(),
311 },
312 attr: None,
313 date: Some(Date {
314 uts: 1_234_567_890,
315 text: "test".to_string(),
316 }),
317 name: "Track".to_string(),
318 mbid: String::new(),
319 url: String::new(),
320 };
321
322 assert_eq!(track.get_timestamp(), Some(1_234_567_890));
323 }
324}