lastfm_client/types/
tracks.rs

1use serde::{Deserialize, Deserializer, Serialize};
2use std::collections::HashMap;
3
4// UTILS - Custom deserializers
5fn 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// BASE TYPES =================================================================
47
48#[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// DATE TYPE ==================================================================
88// Unified - handles both API deserialization and storage
89
90#[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// ATTRIBUTES =================================================================
99
100#[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// RECENT TRACK ===============================================================
111// Unified - no more ApiRecentTrack vs RecentTrack split!
112
113#[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// LOVED TRACK ================================================================
145
146#[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// TOP TRACK ==================================================================
158
159#[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// RESPONSE WRAPPERS ==========================================================
176
177#[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// Recent tracks response
191#[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// Recent tracks extended response
204#[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// Loved tracks response
217#[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// Top tracks response
230#[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
242// TRAITS =====================================================================
243
244pub 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        // Test that "1" deserializes to true
286        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}