1use std::cmp::Ordering;
2use std::collections::HashMap;
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7use crate::types::utils::{bool_from_str, u32_from_str};
8
9#[derive(Serialize, Deserialize, Debug, Clone)]
15#[non_exhaustive]
16pub struct BaseMbidText {
17 pub mbid: String,
19 #[serde(rename = "#text")]
21 pub text: String,
22}
23
24#[derive(Serialize, Deserialize, Debug, Clone)]
28#[non_exhaustive]
29pub struct BaseObject {
30 pub mbid: String,
32 #[serde(default)]
34 pub url: String,
35 #[serde(alias = "#text")]
37 pub name: String,
38}
39
40#[derive(Serialize, Deserialize, Debug, Clone)]
42#[non_exhaustive]
43pub struct TrackImage {
44 pub size: String,
46 #[serde(rename = "#text")]
48 pub text: String,
49}
50
51#[derive(Serialize, Deserialize, Debug, Clone)]
53#[non_exhaustive]
54pub struct Streamable {
55 pub fulltrack: String,
57 #[serde(rename = "#text")]
59 pub text: String,
60}
61
62#[derive(Serialize, Deserialize, Debug, Clone)]
64#[non_exhaustive]
65pub struct Artist {
66 pub name: String,
68 pub mbid: String,
70 #[serde(default)]
72 pub url: String,
73 pub image: Vec<TrackImage>,
75}
76
77#[derive(Serialize, Deserialize, Debug, Clone)]
82#[non_exhaustive]
83pub struct Date {
84 #[serde(deserialize_with = "u32_from_str")]
86 pub uts: u32,
87 #[serde(rename = "#text")]
89 pub text: String,
90}
91
92#[derive(Serialize, Deserialize, Debug, Clone)]
96#[non_exhaustive]
97pub struct Attributes {
98 pub nowplaying: String,
100}
101
102#[derive(Serialize, Deserialize, Debug, Clone)]
104#[non_exhaustive]
105pub struct RankAttr {
106 pub rank: String,
108}
109
110#[derive(Serialize, Deserialize, Debug, Clone)]
117#[non_exhaustive]
118pub struct RecentTrack {
119 pub artist: BaseMbidText,
121 #[serde(deserialize_with = "bool_from_str")]
123 pub streamable: bool,
124 pub image: Vec<TrackImage>,
126 pub album: BaseMbidText,
128 #[serde(rename = "@attr")]
130 pub attr: Option<Attributes>,
131 pub date: Option<Date>,
133 pub name: String,
135 pub mbid: String,
137 pub url: String,
139}
140
141impl fmt::Display for RecentTrack {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 let status = if self.attr.is_some() {
144 " [NOW PLAYING]"
145 } else {
146 ""
147 };
148 let date_str = self
149 .date
150 .as_ref()
151 .map_or(String::new(), |d| format!(" ({})", d.text));
152
153 write!(
154 f,
155 "{} - {} [{}]{date_str}{status}",
156 self.name, self.artist.text, self.album.text
157 )
158 }
159}
160
161impl PartialEq for RecentTrack {
162 fn eq(&self, other: &Self) -> bool {
163 self.date.as_ref().map(|d| d.uts) == other.date.as_ref().map(|d| d.uts)
164 }
165}
166
167impl Eq for RecentTrack {}
168
169impl PartialOrd for RecentTrack {
170 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
171 Some(self.cmp(other))
172 }
173}
174
175impl Ord for RecentTrack {
176 fn cmp(&self, other: &Self) -> Ordering {
177 match (self.date.as_ref(), other.date.as_ref()) {
179 (None, None) => Ordering::Equal,
180 (None, Some(_)) => Ordering::Greater,
181 (Some(_), None) => Ordering::Less,
182 (Some(a), Some(b)) => a.uts.cmp(&b.uts),
183 }
184 }
185}
186
187#[derive(Serialize, Deserialize, Debug, Clone)]
191#[non_exhaustive]
192pub struct RecentTrackExtended {
193 pub artist: BaseObject,
195 #[serde(deserialize_with = "bool_from_str")]
197 pub streamable: bool,
198 pub image: Vec<TrackImage>,
200 pub album: BaseObject,
202 #[serde(rename = "@attr")]
204 pub attr: Option<HashMap<String, String>>,
205 pub date: Option<Date>,
207 pub name: String,
209 pub mbid: String,
211 #[serde(default)]
213 pub url: String,
214}
215
216impl fmt::Display for RecentTrackExtended {
217 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218 let is_now_playing = self
219 .attr
220 .as_ref()
221 .and_then(|a| a.get("nowplaying"))
222 .is_some_and(|v| v == "true");
223 let status = if is_now_playing { " [NOW PLAYING]" } else { "" };
224 let date_str = self
225 .date
226 .as_ref()
227 .map_or(String::new(), |d| format!(" ({})", d.text));
228
229 write!(
230 f,
231 "{} - {} [{}]{date_str}{status}",
232 self.name, self.artist.name, self.album.name
233 )
234 }
235}
236
237impl PartialEq for RecentTrackExtended {
238 fn eq(&self, other: &Self) -> bool {
239 self.date.as_ref().map(|d| d.uts) == other.date.as_ref().map(|d| d.uts)
240 }
241}
242
243impl Eq for RecentTrackExtended {}
244
245impl PartialOrd for RecentTrackExtended {
246 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
247 Some(self.cmp(other))
248 }
249}
250
251impl Ord for RecentTrackExtended {
252 fn cmp(&self, other: &Self) -> Ordering {
253 match (self.date.as_ref(), other.date.as_ref()) {
255 (None, None) => Ordering::Equal,
256 (None, Some(_)) => Ordering::Greater,
257 (Some(_), None) => Ordering::Less,
258 (Some(a), Some(b)) => a.uts.cmp(&b.uts),
259 }
260 }
261}
262
263#[derive(Serialize, Deserialize, Debug, Clone)]
269#[non_exhaustive]
270pub struct LovedTrack {
271 pub artist: BaseObject,
273 pub date: Date,
275 pub image: Vec<TrackImage>,
277 pub streamable: Streamable,
279 pub name: String,
281 pub mbid: String,
283 pub url: String,
285}
286
287impl fmt::Display for LovedTrack {
288 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289 write!(
290 f,
291 "{} - {} (loved {})",
292 self.name, self.artist.name, self.date.text
293 )
294 }
295}
296
297impl PartialEq for LovedTrack {
298 fn eq(&self, other: &Self) -> bool {
299 self.date.uts == other.date.uts
300 }
301}
302
303impl Eq for LovedTrack {}
304
305impl PartialOrd for LovedTrack {
306 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
307 Some(self.cmp(other))
308 }
309}
310
311impl Ord for LovedTrack {
312 fn cmp(&self, other: &Self) -> Ordering {
313 self.date.uts.cmp(&other.date.uts)
314 }
315}
316
317#[derive(Serialize, Deserialize, Debug, Clone)]
323#[non_exhaustive]
324pub struct TopTrack {
325 pub streamable: Streamable,
327 pub mbid: String,
329 pub name: String,
331 pub image: Vec<TrackImage>,
333 pub artist: BaseObject,
335 pub url: String,
337 #[serde(deserialize_with = "u32_from_str")]
339 pub duration: u32,
340 #[serde(rename = "@attr")]
342 pub attr: RankAttr,
343 #[serde(deserialize_with = "u32_from_str")]
345 pub playcount: u32,
346}
347
348impl fmt::Display for TopTrack {
349 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350 write!(
351 f,
352 "#{} - {} by {} ({} plays)",
353 self.attr.rank, self.name, self.artist.name, self.playcount
354 )
355 }
356}
357
358impl PartialEq for TopTrack {
359 fn eq(&self, other: &Self) -> bool {
360 self.playcount == other.playcount
361 }
362}
363
364impl Eq for TopTrack {}
365
366impl PartialOrd for TopTrack {
367 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
368 Some(self.cmp(other))
369 }
370}
371
372impl Ord for TopTrack {
373 fn cmp(&self, other: &Self) -> Ordering {
374 self.playcount.cmp(&other.playcount)
375 }
376}
377
378#[derive(Serialize, Deserialize, Debug, Clone)]
382#[non_exhaustive]
383pub struct BaseResponse {
384 pub user: String,
386 #[serde(deserialize_with = "u32_from_str", rename = "totalPages")]
388 pub total_pages: u32,
389 #[serde(deserialize_with = "u32_from_str")]
391 pub page: u32,
392 #[serde(deserialize_with = "u32_from_str", rename = "perPage")]
394 pub per_page: u32,
395 #[serde(deserialize_with = "u32_from_str")]
397 pub total: u32,
398}
399
400#[derive(Serialize, Deserialize, Debug)]
402#[non_exhaustive]
403pub struct RecentTracks {
404 pub track: Vec<RecentTrack>,
406 #[serde(rename = "@attr")]
408 pub attr: BaseResponse,
409}
410
411#[derive(Serialize, Deserialize, Debug)]
413#[non_exhaustive]
414pub struct UserRecentTracks {
415 pub recenttracks: RecentTracks,
417}
418
419#[derive(Serialize, Deserialize, Debug)]
421#[non_exhaustive]
422pub struct RecentTracksExtended {
423 pub track: Vec<RecentTrackExtended>,
425 #[serde(rename = "@attr")]
427 pub attr: BaseResponse,
428}
429
430#[derive(Serialize, Deserialize, Debug)]
432#[non_exhaustive]
433pub struct UserRecentTracksExtended {
434 pub recenttracks: RecentTracksExtended,
436}
437
438#[derive(Serialize, Deserialize, Debug, Clone)]
440#[non_exhaustive]
441pub struct LovedTracks {
442 pub track: Vec<LovedTrack>,
444 #[serde(rename = "@attr")]
446 pub attr: BaseResponse,
447}
448
449#[derive(Serialize, Deserialize, Debug, Clone)]
451#[non_exhaustive]
452pub struct UserLovedTracks {
453 pub lovedtracks: LovedTracks,
455}
456
457#[derive(Serialize, Deserialize, Debug, Clone)]
459#[non_exhaustive]
460pub struct TopTracks {
461 pub track: Vec<TopTrack>,
463 #[serde(rename = "@attr")]
465 pub attr: BaseResponse,
466}
467
468#[derive(Serialize, Deserialize, Debug, Clone)]
470#[non_exhaustive]
471pub struct UserTopTracks {
472 pub toptracks: TopTracks,
474}
475
476#[derive(Debug, Serialize)]
480#[non_exhaustive]
481pub struct TrackPlayInfo {
482 pub name: String,
484 pub play_count: u32,
486 pub artist: String,
488 pub album: Option<String>,
490 pub image_url: Option<String>,
492 pub currently_playing: bool,
494 pub date: Option<u32>,
496 pub url: String,
498}
499
500pub trait Timestamped {
504 fn get_timestamp(&self) -> Option<u32>;
506}
507
508impl Timestamped for RecentTrack {
509 fn get_timestamp(&self) -> Option<u32> {
510 self.date.as_ref().map(|d| d.uts)
511 }
512}
513
514impl Timestamped for LovedTrack {
515 fn get_timestamp(&self) -> Option<u32> {
516 Some(self.date.uts)
517 }
518}
519
520impl Timestamped for RecentTrackExtended {
521 fn get_timestamp(&self) -> Option<u32> {
522 self.date.as_ref().map(|d| d.uts)
523 }
524}
525
526#[cfg(feature = "sqlite")]
529impl crate::sqlite::SqliteExportable for RecentTrack {
530 fn table_name() -> &'static str {
531 "recent_tracks"
532 }
533
534 fn create_table_sql() -> &'static str {
535 "CREATE TABLE IF NOT EXISTS recent_tracks (
536 id INTEGER PRIMARY KEY AUTOINCREMENT,
537 name TEXT NOT NULL,
538 url TEXT NOT NULL,
539 artist TEXT NOT NULL,
540 artist_mbid TEXT NOT NULL,
541 album TEXT NOT NULL,
542 album_mbid TEXT NOT NULL,
543 date_uts INTEGER,
544 loved INTEGER NOT NULL DEFAULT 0
545 )"
546 }
547
548 fn insert_sql() -> &'static str {
549 "INSERT INTO recent_tracks (name, url, artist, artist_mbid, album, album_mbid, date_uts, loved)
550 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"
551 }
552
553 fn bind_and_execute(&self, stmt: &mut rusqlite::Statement<'_>) -> rusqlite::Result<usize> {
554 stmt.execute(rusqlite::params![
555 self.name,
556 self.url,
557 self.artist.text,
558 self.artist.mbid,
559 self.album.text,
560 self.album.mbid,
561 self.date.as_ref().map(|d| d.uts),
562 0_i32,
563 ])
564 }
565}
566
567#[cfg(feature = "sqlite")]
568impl crate::sqlite::SqliteExportable for RecentTrackExtended {
569 fn table_name() -> &'static str {
570 "recent_tracks_extended"
571 }
572
573 fn create_table_sql() -> &'static str {
574 "CREATE TABLE IF NOT EXISTS recent_tracks_extended (
575 id INTEGER PRIMARY KEY AUTOINCREMENT,
576 name TEXT NOT NULL,
577 url TEXT NOT NULL,
578 mbid TEXT NOT NULL,
579 artist TEXT NOT NULL,
580 artist_mbid TEXT NOT NULL,
581 artist_url TEXT NOT NULL,
582 album TEXT NOT NULL,
583 album_mbid TEXT NOT NULL,
584 album_url TEXT NOT NULL,
585 date_uts INTEGER,
586 loved INTEGER NOT NULL DEFAULT 0
587 )"
588 }
589
590 fn insert_sql() -> &'static str {
591 "INSERT INTO recent_tracks_extended
592 (name, url, mbid, artist, artist_mbid, artist_url, album, album_mbid, album_url, date_uts, loved)
593 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)"
594 }
595
596 fn bind_and_execute(&self, stmt: &mut rusqlite::Statement<'_>) -> rusqlite::Result<usize> {
597 stmt.execute(rusqlite::params![
598 self.name,
599 self.url,
600 self.mbid,
601 self.artist.name,
602 self.artist.mbid,
603 self.artist.url,
604 self.album.name,
605 self.album.mbid,
606 self.album.url,
607 self.date.as_ref().map(|d| d.uts),
608 0_i32,
609 ])
610 }
611}
612
613#[cfg(feature = "sqlite")]
614impl crate::sqlite::SqliteExportable for LovedTrack {
615 fn table_name() -> &'static str {
616 "loved_tracks"
617 }
618
619 fn create_table_sql() -> &'static str {
620 "CREATE TABLE IF NOT EXISTS loved_tracks (
621 id INTEGER PRIMARY KEY AUTOINCREMENT,
622 name TEXT NOT NULL,
623 url TEXT NOT NULL,
624 artist TEXT NOT NULL,
625 artist_mbid TEXT NOT NULL,
626 date_uts INTEGER NOT NULL
627 )"
628 }
629
630 fn insert_sql() -> &'static str {
631 "INSERT INTO loved_tracks (name, url, artist, artist_mbid, date_uts)
632 VALUES (?1, ?2, ?3, ?4, ?5)"
633 }
634
635 fn bind_and_execute(&self, stmt: &mut rusqlite::Statement<'_>) -> rusqlite::Result<usize> {
636 stmt.execute(rusqlite::params![
637 self.name,
638 self.url,
639 self.artist.name,
640 self.artist.mbid,
641 self.date.uts,
642 ])
643 }
644}
645
646#[cfg(feature = "sqlite")]
647impl crate::sqlite::SqliteExportable for TopTrack {
648 fn table_name() -> &'static str {
649 "top_tracks"
650 }
651
652 fn create_table_sql() -> &'static str {
653 "CREATE TABLE IF NOT EXISTS top_tracks (
654 id INTEGER PRIMARY KEY AUTOINCREMENT,
655 name TEXT NOT NULL,
656 url TEXT NOT NULL,
657 artist TEXT NOT NULL,
658 mbid TEXT NOT NULL,
659 playcount INTEGER NOT NULL,
660 rank INTEGER NOT NULL
661 )"
662 }
663
664 fn insert_sql() -> &'static str {
665 "INSERT INTO top_tracks (name, url, artist, mbid, playcount, rank)
666 VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
667 }
668
669 fn bind_and_execute(&self, stmt: &mut rusqlite::Statement<'_>) -> rusqlite::Result<usize> {
670 let rank: u32 = self.attr.rank.parse().unwrap_or_default();
671 stmt.execute(rusqlite::params![
672 self.name,
673 self.url,
674 self.artist.name,
675 self.mbid,
676 self.playcount,
677 rank,
678 ])
679 }
680}
681
682#[cfg(test)]
683#[allow(clippy::unwrap_used)]
684mod tests {
685 use super::*;
686
687 #[test]
688 fn test_date_deserialization() {
689 use serde_json::json;
690 let json_value = json!({
691 "uts": "1_234_567_890",
692 "#text": "2009-02-13 23:31:30"
693 });
694 let date: Date = serde_json::from_value(json_value).unwrap();
695 assert_eq!(date.uts, 1_234_567_890);
696 assert_eq!(date.text, "2009-02-13 23:31:30");
697 }
698
699 #[test]
700 fn test_bool_from_str() {
701 use serde_json::json;
702 let json_value = json!({
704 "artist": {"mbid": "", "#text": "Test"},
705 "streamable": "1",
706 "image": [],
707 "album": {"mbid": "", "#text": ""},
708 "name": "Test",
709 "mbid": "",
710 "url": ""
711 });
712 let track: RecentTrack = serde_json::from_value(json_value).unwrap();
713 assert!(track.streamable);
714 }
715
716 #[test]
717 fn test_timestamped_trait() {
718 let track = RecentTrack {
719 artist: BaseMbidText {
720 mbid: String::new(),
721 text: "Artist".to_string(),
722 },
723 streamable: false,
724 image: vec![],
725 album: BaseMbidText {
726 mbid: String::new(),
727 text: String::new(),
728 },
729 attr: None,
730 date: Some(Date {
731 uts: 1_234_567_890,
732 text: "test".to_string(),
733 }),
734 name: "Track".to_string(),
735 mbid: String::new(),
736 url: String::new(),
737 };
738
739 assert_eq!(track.get_timestamp(), Some(1_234_567_890));
740 }
741}