1use std::fmt;
2
3use thiserror::Error;
4
5use crate::{Error, SpotifyUri};
6
7pub use crate::FileId;
9
10#[derive(Clone, Copy, PartialEq, Eq, Hash)]
11pub struct SpotifyId {
12 pub id: u128,
13}
14
15#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
16pub enum SpotifyIdError {
17 #[error("ID cannot be parsed")]
18 InvalidId,
19 #[error("not a valid Spotify ID")]
20 InvalidFormat,
21}
22
23impl From<SpotifyIdError> for Error {
24 fn from(err: SpotifyIdError) -> Self {
25 Error::invalid_argument(err)
26 }
27}
28
29pub type SpotifyIdResult = Result<SpotifyId, Error>;
30
31const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
32const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef";
33
34impl SpotifyId {
35 const SIZE: usize = 16;
36 const SIZE_BASE16: usize = 32;
37 const SIZE_BASE62: usize = 22;
38
39 pub fn from_base16(src: &str) -> SpotifyIdResult {
45 if src.len() != 32 {
46 return Err(SpotifyIdError::InvalidId.into());
47 }
48 let mut dst: u128 = 0;
49
50 for c in src.as_bytes() {
51 let p = match c {
52 b'0'..=b'9' => c - b'0',
53 b'a'..=b'f' => c - b'a' + 10,
54 _ => return Err(SpotifyIdError::InvalidId.into()),
55 } as u128;
56
57 dst <<= 4;
58 dst += p;
59 }
60
61 Ok(Self { id: dst })
62 }
63
64 pub fn from_base62(src: &str) -> SpotifyIdResult {
70 if src.len() != Self::SIZE_BASE62 {
71 return Err(SpotifyIdError::InvalidId.into());
72 }
73 let mut dst: u128 = 0;
74
75 for c in src.as_bytes() {
76 let p = match c {
77 b'0'..=b'9' => c - b'0',
78 b'a'..=b'z' => c - b'a' + 10,
79 b'A'..=b'Z' => c - b'A' + 36,
80 _ => return Err(SpotifyIdError::InvalidId.into()),
81 } as u128;
82
83 dst = dst.checked_mul(62).ok_or(SpotifyIdError::InvalidId)?;
84 dst = dst.checked_add(p).ok_or(SpotifyIdError::InvalidId)?;
85 }
86
87 Ok(Self { id: dst })
88 }
89
90 pub fn from_raw(src: &[u8]) -> SpotifyIdResult {
94 match src.try_into() {
95 Ok(dst) => Ok(Self {
96 id: u128::from_be_bytes(dst),
97 }),
98 Err(_) => Err(SpotifyIdError::InvalidId.into()),
99 }
100 }
101
102 #[allow(clippy::wrong_self_convention)]
105 pub fn to_base16(&self) -> Result<String, Error> {
106 to_base16(&self.to_raw(), &mut [0u8; Self::SIZE_BASE16])
107 }
108
109 #[allow(clippy::wrong_self_convention)]
114 pub fn to_base62(&self) -> Result<String, Error> {
115 let mut dst = [0u8; 22];
116 let mut i = 0;
117 let n = self.id;
118
119 for shift in &[96, 64, 32, 0] {
131 let mut carry = (n >> shift) as u32 as u64;
132
133 for b in &mut dst[..i] {
134 carry += (*b as u64) << 32;
135 *b = (carry % 62) as u8;
136 carry /= 62;
137 }
138
139 while carry > 0 {
140 dst[i] = (carry % 62) as u8;
141 carry /= 62;
142 i += 1;
143 }
144 }
145
146 for b in &mut dst {
147 *b = BASE62_DIGITS[*b as usize];
148 }
149
150 dst.reverse();
151
152 String::from_utf8(dst.to_vec()).map_err(|_| SpotifyIdError::InvalidId.into())
153 }
154
155 #[allow(clippy::wrong_self_convention)]
158 pub fn to_raw(&self) -> [u8; Self::SIZE] {
159 self.id.to_be_bytes()
160 }
161}
162
163impl fmt::Debug for SpotifyId {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 f.debug_tuple("SpotifyId")
166 .field(&self.to_base62().unwrap_or_else(|_| "invalid uri".into()))
167 .finish()
168 }
169}
170
171impl fmt::Display for SpotifyId {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 f.write_str(&self.to_base62().unwrap_or_else(|_| "invalid uri".into()))
174 }
175}
176
177impl TryFrom<&[u8]> for SpotifyId {
178 type Error = crate::Error;
179 fn try_from(src: &[u8]) -> Result<Self, Self::Error> {
180 Self::from_raw(src)
181 }
182}
183
184impl TryFrom<&str> for SpotifyId {
185 type Error = crate::Error;
186 fn try_from(src: &str) -> Result<Self, Self::Error> {
187 Self::from_base62(src)
188 }
189}
190
191impl TryFrom<String> for SpotifyId {
192 type Error = crate::Error;
193 fn try_from(src: String) -> Result<Self, Self::Error> {
194 Self::try_from(src.as_str())
195 }
196}
197
198impl TryFrom<&Vec<u8>> for SpotifyId {
199 type Error = crate::Error;
200 fn try_from(src: &Vec<u8>) -> Result<Self, Self::Error> {
201 Self::try_from(src.as_slice())
202 }
203}
204
205impl TryFrom<&SpotifyUri> for SpotifyId {
206 type Error = crate::Error;
207 fn try_from(value: &SpotifyUri) -> Result<Self, Self::Error> {
208 match value {
209 SpotifyUri::Album { id }
210 | SpotifyUri::Artist { id }
211 | SpotifyUri::Episode { id }
212 | SpotifyUri::Playlist { id, .. }
213 | SpotifyUri::Show { id }
214 | SpotifyUri::Track { id } => Ok(*id),
215 SpotifyUri::Local { .. } | SpotifyUri::Unknown { .. } => {
216 Err(SpotifyIdError::InvalidFormat.into())
217 }
218 }
219 }
220}
221
222pub fn to_base16(src: &[u8], buf: &mut [u8]) -> Result<String, Error> {
223 let mut i = 0;
224 for v in src {
225 buf[i] = BASE16_DIGITS[(v >> 4) as usize];
226 buf[i + 1] = BASE16_DIGITS[(v & 0x0f) as usize];
227 i += 2;
228 }
229
230 String::from_utf8(buf.to_vec()).map_err(|_| SpotifyIdError::InvalidId.into())
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 struct ConversionCase {
238 id: u128,
239 base16: &'static str,
240 base62: &'static str,
241 raw: &'static [u8],
242 }
243
244 static CONV_VALID: [ConversionCase; 5] = [
245 ConversionCase {
246 id: 238762092608182713602505436543891614649,
247 base16: "b39fe8081e1f4c54be38e8d6f9f12bb9",
248 base62: "5sWHDYs0csV6RS48xBl0tH",
249 raw: &[
250 179, 159, 232, 8, 30, 31, 76, 84, 190, 56, 232, 214, 249, 241, 43, 185,
251 ],
252 },
253 ConversionCase {
254 id: 204841891221366092811751085145916697048,
255 base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
256 base62: "4GNcXTGWmnZ3ySrqvol3o4",
257 raw: &[
258 154, 27, 28, 251, 198, 242, 68, 86, 154, 224, 53, 108, 119, 187, 233, 216,
259 ],
260 },
261 ConversionCase {
262 id: 204841891221366092811751085145916697048,
263 base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
264 base62: "4GNcXTGWmnZ3ySrqvol3o4",
265 raw: &[
266 154, 27, 28, 251, 198, 242, 68, 86, 154, 224, 53, 108, 119, 187, 233, 216,
267 ],
268 },
269 ConversionCase {
270 id: 204841891221366092811751085145916697048,
271 base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
272 base62: "4GNcXTGWmnZ3ySrqvol3o4",
273 raw: &[
274 154, 27, 28, 251, 198, 242, 68, 86, 154, 224, 53, 108, 119, 187, 233, 216,
275 ],
276 },
277 ConversionCase {
278 id: 0,
279 base16: "00000000000000000000000000000000",
280 base62: "0000000000000000000000",
281 raw: &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
282 },
283 ];
284
285 static CONV_INVALID: [ConversionCase; 5] = [
286 ConversionCase {
287 id: 0,
288 base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9",
289 base62: "!!!!!Ys0csV6RS48xBl0tH",
290 raw: &[
291 154, 27, 28, 251, 198, 242, 68, 86, 154, 224, 5, 3, 108, 119, 187, 233, 216, 255,
293 ],
294 },
295 ConversionCase {
296 id: 0,
297 base16: "--------------------",
298 base62: "....................",
299 raw: &[
300 154, 27, 28, 251,
302 ],
303 },
304 ConversionCase {
305 id: 0,
306 base16: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
308 base62: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
310 raw: &[
311 154, 27, 28, 251,
313 ],
314 },
315 ConversionCase {
316 id: 0,
317 base16: "--------------------",
318 base62: "aa",
320 raw: &[
321 154, 27, 28, 251,
323 ],
324 },
325 ConversionCase {
326 id: 0,
327 base16: "--------------------",
328 base62: "ZZZZZZZZZZZZZZZZZZZZZZ",
330 raw: &[
331 154, 27, 28, 251,
333 ],
334 },
335 ];
336
337 #[test]
338 fn from_base62() {
339 for c in &CONV_VALID {
340 assert_eq!(SpotifyId::from_base62(c.base62).unwrap().id, c.id);
341 }
342
343 for c in &CONV_INVALID {
344 assert!(SpotifyId::from_base62(c.base62).is_err(),);
345 }
346 }
347
348 #[test]
349 fn to_base62() {
350 for c in &CONV_VALID {
351 let id = SpotifyId { id: c.id };
352
353 assert_eq!(id.to_base62().unwrap(), c.base62);
354 }
355 }
356
357 #[test]
358 fn from_base16() {
359 for c in &CONV_VALID {
360 assert_eq!(SpotifyId::from_base16(c.base16).unwrap().id, c.id);
361 }
362
363 for c in &CONV_INVALID {
364 assert!(SpotifyId::from_base16(c.base16).is_err(),);
365 }
366 }
367
368 #[test]
369 fn to_base16() {
370 for c in &CONV_VALID {
371 let id = SpotifyId { id: c.id };
372
373 assert_eq!(id.to_base16().unwrap(), c.base16);
374 }
375 }
376
377 #[test]
378 fn from_raw() {
379 for c in &CONV_VALID {
380 assert_eq!(SpotifyId::from_raw(c.raw).unwrap().id, c.id);
381 }
382
383 for c in &CONV_INVALID {
384 assert!(SpotifyId::from_raw(c.raw).is_err());
385 }
386 }
387}