1use base64::Engine;
4use base64::engine::general_purpose::URL_SAFE_NO_PAD;
5use serde::{Deserialize, Serialize};
6
7use crate::Failure;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
14#[serde(rename_all = "camelCase")]
15pub struct Track {
16 pub source: String,
18 pub identifier: String,
20 #[serde(default)]
22 pub title: String,
23 #[serde(default)]
24 pub author: String,
25 #[serde(default, skip_serializing_if = "String::is_empty")]
26 pub album: String,
27 #[serde(default)]
28 pub duration_ms: i64,
29 #[serde(default, skip_serializing_if = "String::is_empty")]
30 pub isrc: String,
31 #[serde(default, skip_serializing_if = "String::is_empty")]
32 pub artwork_url: String,
33 #[serde(default, skip_serializing_if = "String::is_empty")]
34 pub uri: String,
35 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
36 pub is_stream: bool,
37}
38
39impl Track {
40 pub fn key(&self) -> String {
42 format!("{}:{}", self.source, self.identifier)
43 }
44
45 pub fn encode(&self) -> String {
47 let raw = serde_json::to_vec(self).expect("track serializes");
48 URL_SAFE_NO_PAD.encode(raw)
49 }
50
51 pub fn decode(encoded: &str) -> Result<Track, Failure> {
53 let raw = URL_SAFE_NO_PAD
54 .decode(encoded.trim_end_matches('='))
55 .map_err(|e| Failure::common("decode", "decode track", Some(e.to_string())))?;
56 let t: Track =
57 serde_json::from_slice(&raw).map_err(|e| Failure::common("decode", "decode track", Some(e.to_string())))?;
58 if t.source.is_empty() || t.identifier.is_empty() {
59 return Err(Failure::common(
60 "decode",
61 "decode track: missing source or identifier",
62 None,
63 ));
64 }
65 Ok(t)
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(rename_all = "lowercase")]
72pub enum LoadType {
73 Track,
74 Playlist,
75 Search,
76 Empty,
77 Error,
78}
79
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
81#[serde(rename_all = "camelCase")]
82pub struct Playlist {
83 pub name: String,
84 #[serde(default, skip_serializing_if = "String::is_empty")]
85 pub artwork_url: String,
86 #[serde(default, skip_serializing_if = "String::is_empty")]
87 pub uri: String,
88 pub total: usize,
89}
90
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct LoadResult {
94 #[serde(rename = "loadType")]
95 pub kind: LoadType,
96 #[serde(default, skip_serializing_if = "Vec::is_empty")]
97 pub tracks: Vec<Track>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub playlist: Option<Playlist>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub error: Option<Failure>,
102}
103
104impl LoadResult {
105 pub fn empty() -> Self {
106 Self {
107 kind: LoadType::Empty,
108 tracks: vec![],
109 playlist: None,
110 error: None,
111 }
112 }
113 pub fn single(t: Track) -> Self {
114 Self {
115 kind: LoadType::Track,
116 tracks: vec![t],
117 playlist: None,
118 error: None,
119 }
120 }
121 pub fn results(tracks: Vec<Track>) -> Self {
122 if tracks.is_empty() {
123 return Self::empty();
124 }
125 Self {
126 kind: LoadType::Search,
127 tracks,
128 playlist: None,
129 error: None,
130 }
131 }
132 pub fn playlist(p: Playlist, tracks: Vec<Track>) -> Self {
133 Self {
134 kind: LoadType::Playlist,
135 tracks,
136 playlist: Some(p),
137 error: None,
138 }
139 }
140 pub fn error(f: Failure) -> Self {
141 Self {
142 kind: LoadType::Error,
143 tracks: vec![],
144 playlist: None,
145 error: Some(f),
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn roundtrip() {
156 let t = Track {
157 source: "http".into(),
158 identifier: "https://example.com/a.mp3".into(),
159 title: "A".into(),
160 author: "B".into(),
161 duration_ms: 1000,
162 ..Default::default()
163 };
164 let enc = t.encode();
165 assert!(!enc.contains('='));
166 assert_eq!(Track::decode(&enc).unwrap(), t);
167 assert_eq!(t.key(), "http:https://example.com/a.mp3");
168 }
169
170 #[test]
171 fn rejects_incomplete() {
172 let enc = URL_SAFE_NO_PAD.encode(br#"{"source":"","identifier":"x","title":"","author":""}"#);
173 assert!(Track::decode(&enc).is_err());
174 assert!(Track::decode("!!!").is_err());
175 }
176
177 #[test]
178 fn title_and_author_are_optional() {
179 let enc = URL_SAFE_NO_PAD.encode(br#"{"source":"soundcloud","identifier":"123"}"#);
180 let t = Track::decode(&enc).unwrap();
181 assert_eq!((t.title.as_str(), t.author.as_str()), ("", ""));
182 }
183}