librespot_core/
spotify_id.rs

1use std::{fmt, ops::Deref};
2
3use thiserror::Error;
4
5use crate::Error;
6
7use librespot_protocol as protocol;
8
9// re-export FileId for historic reasons, when it was part of this mod
10pub use crate::FileId;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum SpotifyItemType {
14    Album,
15    Artist,
16    Episode,
17    Playlist,
18    Show,
19    Track,
20    Local,
21    Unknown,
22}
23
24impl From<&str> for SpotifyItemType {
25    fn from(v: &str) -> Self {
26        match v {
27            "album" => Self::Album,
28            "artist" => Self::Artist,
29            "episode" => Self::Episode,
30            "playlist" => Self::Playlist,
31            "show" => Self::Show,
32            "track" => Self::Track,
33            "local" => Self::Local,
34            _ => Self::Unknown,
35        }
36    }
37}
38
39impl From<SpotifyItemType> for &str {
40    fn from(item_type: SpotifyItemType) -> &'static str {
41        match item_type {
42            SpotifyItemType::Album => "album",
43            SpotifyItemType::Artist => "artist",
44            SpotifyItemType::Episode => "episode",
45            SpotifyItemType::Playlist => "playlist",
46            SpotifyItemType::Show => "show",
47            SpotifyItemType::Track => "track",
48            SpotifyItemType::Local => "local",
49            _ => "unknown",
50        }
51    }
52}
53
54#[derive(Clone, Copy, PartialEq, Eq, Hash)]
55pub struct SpotifyId {
56    pub id: u128,
57    pub item_type: SpotifyItemType,
58}
59
60#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
61pub enum SpotifyIdError {
62    #[error("ID cannot be parsed")]
63    InvalidId,
64    #[error("not a valid Spotify URI")]
65    InvalidFormat,
66    #[error("URI does not belong to Spotify")]
67    InvalidRoot,
68}
69
70impl From<SpotifyIdError> for Error {
71    fn from(err: SpotifyIdError) -> Self {
72        Error::invalid_argument(err)
73    }
74}
75
76pub type SpotifyIdResult = Result<SpotifyId, Error>;
77pub type NamedSpotifyIdResult = Result<NamedSpotifyId, Error>;
78
79const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
80const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef";
81
82impl SpotifyId {
83    const SIZE: usize = 16;
84    const SIZE_BASE16: usize = 32;
85    const SIZE_BASE62: usize = 22;
86
87    /// Returns whether this `SpotifyId` is for a playable audio item, if known.
88    pub fn is_playable(&self) -> bool {
89        matches!(
90            self.item_type,
91            SpotifyItemType::Episode | SpotifyItemType::Track
92        )
93    }
94
95    /// Parses a base16 (hex) encoded [Spotify ID] into a `SpotifyId`.
96    ///
97    /// `src` is expected to be 32 bytes long and encoded using valid characters.
98    ///
99    /// [Spotify ID]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids
100    pub fn from_base16(src: &str) -> SpotifyIdResult {
101        if src.len() != 32 {
102            return Err(SpotifyIdError::InvalidId.into());
103        }
104        let mut dst: u128 = 0;
105
106        for c in src.as_bytes() {
107            let p = match c {
108                b'0'..=b'9' => c - b'0',
109                b'a'..=b'f' => c - b'a' + 10,
110                _ => return Err(SpotifyIdError::InvalidId.into()),
111            } as u128;
112
113            dst <<= 4;
114            dst += p;
115        }
116
117        Ok(Self {
118            id: dst,
119            item_type: SpotifyItemType::Unknown,
120        })
121    }
122
123    /// Parses a base62 encoded [Spotify ID] into a `u128`.
124    ///
125    /// `src` is expected to be 22 bytes long and encoded using valid characters.
126    ///
127    /// [Spotify ID]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids
128    pub fn from_base62(src: &str) -> SpotifyIdResult {
129        if src.len() != 22 {
130            return Err(SpotifyIdError::InvalidId.into());
131        }
132        let mut dst: u128 = 0;
133
134        for c in src.as_bytes() {
135            let p = match c {
136                b'0'..=b'9' => c - b'0',
137                b'a'..=b'z' => c - b'a' + 10,
138                b'A'..=b'Z' => c - b'A' + 36,
139                _ => return Err(SpotifyIdError::InvalidId.into()),
140            } as u128;
141
142            dst = dst.checked_mul(62).ok_or(SpotifyIdError::InvalidId)?;
143            dst = dst.checked_add(p).ok_or(SpotifyIdError::InvalidId)?;
144        }
145
146        Ok(Self {
147            id: dst,
148            item_type: SpotifyItemType::Unknown,
149        })
150    }
151
152    /// Creates a `u128` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order.
153    ///
154    /// The resulting `SpotifyId` will default to a `SpotifyItemType::Unknown`.
155    pub fn from_raw(src: &[u8]) -> SpotifyIdResult {
156        match src.try_into() {
157            Ok(dst) => Ok(Self {
158                id: u128::from_be_bytes(dst),
159                item_type: SpotifyItemType::Unknown,
160            }),
161            Err(_) => Err(SpotifyIdError::InvalidId.into()),
162        }
163    }
164
165    /// Parses a [Spotify URI] into a `SpotifyId`.
166    ///
167    /// `uri` is expected to be in the canonical form `spotify:{type}:{id}`, where `{type}`
168    /// can be arbitrary while `{id}` is a 22-character long, base62 encoded Spotify ID.
169    ///
170    /// Note that this should not be used for playlists, which have the form of
171    /// `spotify:playlist:{id}`.
172    ///
173    /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids
174    pub fn from_uri(src: &str) -> SpotifyIdResult {
175        // Basic: `spotify:{type}:{id}`
176        // Named: `spotify:user:{user}:{type}:{id}`
177        // Local: `spotify:local:{artist}:{album_title}:{track_title}:{duration_in_seconds}`
178        let mut parts = src.split(':');
179
180        let scheme = parts.next().ok_or(SpotifyIdError::InvalidFormat)?;
181
182        let item_type = {
183            let next = parts.next().ok_or(SpotifyIdError::InvalidFormat)?;
184            if next == "user" {
185                let _username = parts.next().ok_or(SpotifyIdError::InvalidFormat)?;
186                parts.next().ok_or(SpotifyIdError::InvalidFormat)?
187            } else {
188                next
189            }
190        };
191
192        let id = parts.next().ok_or(SpotifyIdError::InvalidFormat)?;
193
194        if scheme != "spotify" {
195            return Err(SpotifyIdError::InvalidRoot.into());
196        }
197
198        let item_type = item_type.into();
199
200        // Local files have a variable-length ID: https://developer.spotify.com/documentation/general/guides/local-files-spotify-playlists/
201        // TODO: find a way to add this local file ID to SpotifyId.
202        // One possible solution would be to copy the contents of `id` to a new String field in SpotifyId,
203        // but then we would need to remove the derived Copy trait, which would be a breaking change.
204        if item_type == SpotifyItemType::Local {
205            return Ok(Self { item_type, id: 0 });
206        }
207
208        if id.len() != Self::SIZE_BASE62 {
209            return Err(SpotifyIdError::InvalidId.into());
210        }
211
212        Ok(Self {
213            item_type,
214            ..Self::from_base62(id)?
215        })
216    }
217
218    /// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE16` (32)
219    /// character long `String`.
220    #[allow(clippy::wrong_self_convention)]
221    pub fn to_base16(&self) -> Result<String, Error> {
222        to_base16(&self.to_raw(), &mut [0u8; Self::SIZE_BASE16])
223    }
224
225    /// Returns the `SpotifyId` as a [canonically] base62 encoded, `SpotifyId::SIZE_BASE62` (22)
226    /// character long `String`.
227    ///
228    /// [canonically]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids
229    #[allow(clippy::wrong_self_convention)]
230    pub fn to_base62(&self) -> Result<String, Error> {
231        let mut dst = [0u8; 22];
232        let mut i = 0;
233        let n = self.id;
234
235        // The algorithm is based on:
236        // https://github.com/trezor/trezor-crypto/blob/c316e775a2152db255ace96b6b65ac0f20525ec0/base58.c
237        //
238        // We are not using naive division of self.id as it is an u128 and div + mod are software
239        // emulated at runtime (and unoptimized into mul + shift) on non-128bit platforms,
240        // making them very expensive.
241        //
242        // Trezor's algorithm allows us to stick to arithmetic on native registers making this
243        // an order of magnitude faster. Additionally, as our sizes are known, instead of
244        // dealing with the ID on a byte by byte basis, we decompose it into four u32s and
245        // use 64-bit arithmetic on them for an additional speedup.
246        for shift in &[96, 64, 32, 0] {
247            let mut carry = (n >> shift) as u32 as u64;
248
249            for b in &mut dst[..i] {
250                carry += (*b as u64) << 32;
251                *b = (carry % 62) as u8;
252                carry /= 62;
253            }
254
255            while carry > 0 {
256                dst[i] = (carry % 62) as u8;
257                carry /= 62;
258                i += 1;
259            }
260        }
261
262        for b in &mut dst {
263            *b = BASE62_DIGITS[*b as usize];
264        }
265
266        dst.reverse();
267
268        String::from_utf8(dst.to_vec()).map_err(|_| SpotifyIdError::InvalidId.into())
269    }
270
271    /// Returns a copy of the `SpotifyId` as an array of `SpotifyId::SIZE` (16) bytes in
272    /// big-endian order.
273    #[allow(clippy::wrong_self_convention)]
274    pub fn to_raw(&self) -> [u8; Self::SIZE] {
275        self.id.to_be_bytes()
276    }
277
278    /// Returns the `SpotifyId` as a [Spotify URI] in the canonical form `spotify:{type}:{id}`,
279    /// where `{type}` is an arbitrary string and `{id}` is a 22-character long, base62 encoded
280    /// Spotify ID.
281    ///
282    /// If the `SpotifyId` has an associated type unrecognized by the library, `{type}` will
283    /// be encoded as `unknown`.
284    ///
285    /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids
286    #[allow(clippy::wrong_self_convention)]
287    pub fn to_uri(&self) -> Result<String, Error> {
288        // 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID  = 31
289        // + unknown size item_type.
290        let item_type: &str = self.item_type.into();
291        let mut dst = String::with_capacity(31 + item_type.len());
292        dst.push_str("spotify:");
293        dst.push_str(item_type);
294        dst.push(':');
295        let base_62 = self.to_base62()?;
296        dst.push_str(&base_62);
297
298        Ok(dst)
299    }
300}
301
302impl fmt::Debug for SpotifyId {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        f.debug_tuple("SpotifyId")
305            .field(&self.to_uri().unwrap_or_else(|_| "invalid uri".into()))
306            .finish()
307    }
308}
309
310impl fmt::Display for SpotifyId {
311    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312        f.write_str(&self.to_uri().unwrap_or_else(|_| "invalid uri".into()))
313    }
314}
315
316#[derive(Clone, PartialEq, Eq, Hash)]
317pub struct NamedSpotifyId {
318    pub inner_id: SpotifyId,
319    pub username: String,
320}
321
322impl NamedSpotifyId {
323    pub fn from_uri(src: &str) -> NamedSpotifyIdResult {
324        let uri_parts: Vec<&str> = src.split(':').collect();
325
326        // At minimum, should be `spotify:user:{username}:{type}:{id}`
327        if uri_parts.len() < 5 {
328            return Err(SpotifyIdError::InvalidFormat.into());
329        }
330
331        if uri_parts[0] != "spotify" {
332            return Err(SpotifyIdError::InvalidRoot.into());
333        }
334
335        if uri_parts[1] != "user" {
336            return Err(SpotifyIdError::InvalidFormat.into());
337        }
338
339        Ok(Self {
340            inner_id: SpotifyId::from_uri(src)?,
341            username: uri_parts[2].to_owned(),
342        })
343    }
344
345    pub fn to_uri(&self) -> Result<String, Error> {
346        let item_type: &str = self.inner_id.item_type.into();
347        let mut dst = String::with_capacity(37 + self.username.len() + item_type.len());
348        dst.push_str("spotify:user:");
349        dst.push_str(&self.username);
350        dst.push(':');
351        dst.push_str(item_type);
352        dst.push(':');
353        let base_62 = self.to_base62()?;
354        dst.push_str(&base_62);
355
356        Ok(dst)
357    }
358
359    pub fn from_spotify_id(id: SpotifyId, username: &str) -> Self {
360        Self {
361            inner_id: id,
362            username: username.to_owned(),
363        }
364    }
365}
366
367impl Deref for NamedSpotifyId {
368    type Target = SpotifyId;
369    fn deref(&self) -> &Self::Target {
370        &self.inner_id
371    }
372}
373
374impl fmt::Debug for NamedSpotifyId {
375    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
376        f.debug_tuple("NamedSpotifyId")
377            .field(
378                &self
379                    .inner_id
380                    .to_uri()
381                    .unwrap_or_else(|_| "invalid id".into()),
382            )
383            .finish()
384    }
385}
386
387impl fmt::Display for NamedSpotifyId {
388    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
389        f.write_str(
390            &self
391                .inner_id
392                .to_uri()
393                .unwrap_or_else(|_| "invalid id".into()),
394        )
395    }
396}
397
398impl TryFrom<&[u8]> for SpotifyId {
399    type Error = crate::Error;
400    fn try_from(src: &[u8]) -> Result<Self, Self::Error> {
401        Self::from_raw(src)
402    }
403}
404
405impl TryFrom<&str> for SpotifyId {
406    type Error = crate::Error;
407    fn try_from(src: &str) -> Result<Self, Self::Error> {
408        Self::from_base62(src)
409    }
410}
411
412impl TryFrom<String> for SpotifyId {
413    type Error = crate::Error;
414    fn try_from(src: String) -> Result<Self, Self::Error> {
415        Self::try_from(src.as_str())
416    }
417}
418
419impl TryFrom<&Vec<u8>> for SpotifyId {
420    type Error = crate::Error;
421    fn try_from(src: &Vec<u8>) -> Result<Self, Self::Error> {
422        Self::try_from(src.as_slice())
423    }
424}
425
426impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId {
427    type Error = crate::Error;
428    fn try_from(track: &protocol::spirc::TrackRef) -> Result<Self, Self::Error> {
429        match SpotifyId::from_raw(track.gid()) {
430            Ok(mut id) => {
431                id.item_type = SpotifyItemType::Track;
432                Ok(id)
433            }
434            Err(_) => SpotifyId::from_uri(track.uri()),
435        }
436    }
437}
438
439impl TryFrom<&protocol::metadata::Album> for SpotifyId {
440    type Error = crate::Error;
441    fn try_from(album: &protocol::metadata::Album) -> Result<Self, Self::Error> {
442        Ok(Self {
443            item_type: SpotifyItemType::Album,
444            ..Self::from_raw(album.gid())?
445        })
446    }
447}
448
449impl TryFrom<&protocol::metadata::Artist> for SpotifyId {
450    type Error = crate::Error;
451    fn try_from(artist: &protocol::metadata::Artist) -> Result<Self, Self::Error> {
452        Ok(Self {
453            item_type: SpotifyItemType::Artist,
454            ..Self::from_raw(artist.gid())?
455        })
456    }
457}
458
459impl TryFrom<&protocol::metadata::Episode> for SpotifyId {
460    type Error = crate::Error;
461    fn try_from(episode: &protocol::metadata::Episode) -> Result<Self, Self::Error> {
462        Ok(Self {
463            item_type: SpotifyItemType::Episode,
464            ..Self::from_raw(episode.gid())?
465        })
466    }
467}
468
469impl TryFrom<&protocol::metadata::Track> for SpotifyId {
470    type Error = crate::Error;
471    fn try_from(track: &protocol::metadata::Track) -> Result<Self, Self::Error> {
472        Ok(Self {
473            item_type: SpotifyItemType::Track,
474            ..Self::from_raw(track.gid())?
475        })
476    }
477}
478
479impl TryFrom<&protocol::metadata::Show> for SpotifyId {
480    type Error = crate::Error;
481    fn try_from(show: &protocol::metadata::Show) -> Result<Self, Self::Error> {
482        Ok(Self {
483            item_type: SpotifyItemType::Show,
484            ..Self::from_raw(show.gid())?
485        })
486    }
487}
488
489impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId {
490    type Error = crate::Error;
491    fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result<Self, Self::Error> {
492        Ok(Self {
493            item_type: SpotifyItemType::Artist,
494            ..Self::from_raw(artist.artist_gid())?
495        })
496    }
497}
498
499impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId {
500    type Error = crate::Error;
501    fn try_from(item: &protocol::playlist4_external::Item) -> Result<Self, Self::Error> {
502        Ok(Self {
503            item_type: SpotifyItemType::Track,
504            ..Self::from_uri(item.uri())?
505        })
506    }
507}
508
509// Note that this is the unique revision of an item's metadata on a playlist,
510// not the ID of that item or playlist.
511impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId {
512    type Error = crate::Error;
513    fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result<Self, Self::Error> {
514        Self::try_from(item.revision())
515    }
516}
517
518// Note that this is the unique revision of a playlist, not the ID of that playlist.
519impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId {
520    type Error = crate::Error;
521    fn try_from(
522        playlist: &protocol::playlist4_external::SelectedListContent,
523    ) -> Result<Self, Self::Error> {
524        Self::try_from(playlist.revision())
525    }
526}
527
528// TODO: check meaning and format of this field in the wild. This might be a FileId,
529// which is why we now don't create a separate `Playlist` enum value yet and choose
530// to discard any item type.
531impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId {
532    type Error = crate::Error;
533    fn try_from(
534        picture: &protocol::playlist_annotate3::TranscodedPicture,
535    ) -> Result<Self, Self::Error> {
536        Self::from_base62(picture.uri())
537    }
538}
539
540pub fn to_base16(src: &[u8], buf: &mut [u8]) -> Result<String, Error> {
541    let mut i = 0;
542    for v in src {
543        buf[i] = BASE16_DIGITS[(v >> 4) as usize];
544        buf[i + 1] = BASE16_DIGITS[(v & 0x0f) as usize];
545        i += 2;
546    }
547
548    String::from_utf8(buf.to_vec()).map_err(|_| SpotifyIdError::InvalidId.into())
549}
550
551#[cfg(test)]
552mod tests {
553    use super::*;
554
555    struct ConversionCase {
556        id: u128,
557        kind: SpotifyItemType,
558        uri: &'static str,
559        base16: &'static str,
560        base62: &'static str,
561        raw: &'static [u8],
562    }
563
564    static CONV_VALID: [ConversionCase; 5] = [
565        ConversionCase {
566            id: 238762092608182713602505436543891614649,
567            kind: SpotifyItemType::Track,
568            uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH",
569            base16: "b39fe8081e1f4c54be38e8d6f9f12bb9",
570            base62: "5sWHDYs0csV6RS48xBl0tH",
571            raw: &[
572                179, 159, 232, 8, 30, 31, 76, 84, 190, 56, 232, 214, 249, 241, 43, 185,
573            ],
574        },
575        ConversionCase {
576            id: 204841891221366092811751085145916697048,
577            kind: SpotifyItemType::Track,
578            uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4",
579            base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
580            base62: "4GNcXTGWmnZ3ySrqvol3o4",
581            raw: &[
582                154, 27, 28, 251, 198, 242, 68, 86, 154, 224, 53, 108, 119, 187, 233, 216,
583            ],
584        },
585        ConversionCase {
586            id: 204841891221366092811751085145916697048,
587            kind: SpotifyItemType::Episode,
588            uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4",
589            base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
590            base62: "4GNcXTGWmnZ3ySrqvol3o4",
591            raw: &[
592                154, 27, 28, 251, 198, 242, 68, 86, 154, 224, 53, 108, 119, 187, 233, 216,
593            ],
594        },
595        ConversionCase {
596            id: 204841891221366092811751085145916697048,
597            kind: SpotifyItemType::Show,
598            uri: "spotify:show:4GNcXTGWmnZ3ySrqvol3o4",
599            base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
600            base62: "4GNcXTGWmnZ3ySrqvol3o4",
601            raw: &[
602                154, 27, 28, 251, 198, 242, 68, 86, 154, 224, 53, 108, 119, 187, 233, 216,
603            ],
604        },
605        ConversionCase {
606            id: 0,
607            kind: SpotifyItemType::Local,
608            uri: "spotify:local:0000000000000000000000",
609            base16: "00000000000000000000000000000000",
610            base62: "0000000000000000000000",
611            raw: &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
612        },
613    ];
614
615    static CONV_INVALID: [ConversionCase; 5] = [
616        ConversionCase {
617            id: 0,
618            kind: SpotifyItemType::Unknown,
619            // Invalid ID in the URI.
620            uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH",
621            base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9",
622            base62: "!!!!!Ys0csV6RS48xBl0tH",
623            raw: &[
624                // Invalid length.
625                154, 27, 28, 251, 198, 242, 68, 86, 154, 224, 5, 3, 108, 119, 187, 233, 216, 255,
626            ],
627        },
628        ConversionCase {
629            id: 0,
630            kind: SpotifyItemType::Unknown,
631            // Missing colon between ID and type.
632            uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH",
633            base16: "--------------------",
634            base62: "....................",
635            raw: &[
636                // Invalid length.
637                154, 27, 28, 251,
638            ],
639        },
640        ConversionCase {
641            id: 0,
642            kind: SpotifyItemType::Unknown,
643            // Uri too short
644            uri: "spotify:azb:aRS48xBl0tH",
645            // too long, should return error but not panic overflow
646            base16: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
647            // too long, should return error but not panic overflow
648            base62: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
649            raw: &[
650                // Invalid length.
651                154, 27, 28, 251,
652            ],
653        },
654        ConversionCase {
655            id: 0,
656            kind: SpotifyItemType::Unknown,
657            // Uri too short
658            uri: "spotify:azb:aRS48xBl0tH",
659            base16: "--------------------",
660            // too short to encode a 128 bits int
661            base62: "aa",
662            raw: &[
663                // Invalid length.
664                154, 27, 28, 251,
665            ],
666        },
667        ConversionCase {
668            id: 0,
669            kind: SpotifyItemType::Unknown,
670            uri: "cleary invalid uri",
671            base16: "--------------------",
672            // too high of a value, this would need a 132 bits int
673            base62: "ZZZZZZZZZZZZZZZZZZZZZZ",
674            raw: &[
675                // Invalid length.
676                154, 27, 28, 251,
677            ],
678        },
679    ];
680
681    #[test]
682    fn from_base62() {
683        for c in &CONV_VALID {
684            assert_eq!(SpotifyId::from_base62(c.base62).unwrap().id, c.id);
685        }
686
687        for c in &CONV_INVALID {
688            assert!(SpotifyId::from_base62(c.base62).is_err(),);
689        }
690    }
691
692    #[test]
693    fn to_base62() {
694        for c in &CONV_VALID {
695            let id = SpotifyId {
696                id: c.id,
697                item_type: c.kind,
698            };
699
700            assert_eq!(id.to_base62().unwrap(), c.base62);
701        }
702    }
703
704    #[test]
705    fn from_base16() {
706        for c in &CONV_VALID {
707            assert_eq!(SpotifyId::from_base16(c.base16).unwrap().id, c.id);
708        }
709
710        for c in &CONV_INVALID {
711            assert!(SpotifyId::from_base16(c.base16).is_err(),);
712        }
713    }
714
715    #[test]
716    fn to_base16() {
717        for c in &CONV_VALID {
718            let id = SpotifyId {
719                id: c.id,
720                item_type: c.kind,
721            };
722
723            assert_eq!(id.to_base16().unwrap(), c.base16);
724        }
725    }
726
727    #[test]
728    fn from_uri() {
729        for c in &CONV_VALID {
730            let actual = SpotifyId::from_uri(c.uri).unwrap();
731
732            assert_eq!(actual.id, c.id);
733            assert_eq!(actual.item_type, c.kind);
734        }
735
736        for c in &CONV_INVALID {
737            assert!(SpotifyId::from_uri(c.uri).is_err());
738        }
739    }
740
741    #[test]
742    fn from_local_uri() {
743        let actual = SpotifyId::from_uri("spotify:local:xyz:123").unwrap();
744
745        assert_eq!(actual.id, 0);
746        assert_eq!(actual.item_type, SpotifyItemType::Local);
747    }
748
749    #[test]
750    fn from_named_uri() {
751        let actual =
752            NamedSpotifyId::from_uri("spotify:user:spotify:playlist:37i9dQZF1DWSw8liJZcPOI")
753                .unwrap();
754
755        assert_eq!(actual.id, 136159921382084734723401526672209703396);
756        assert_eq!(actual.item_type, SpotifyItemType::Playlist);
757        assert_eq!(actual.username, "spotify");
758    }
759
760    #[test]
761    fn to_uri() {
762        for c in &CONV_VALID {
763            let id = SpotifyId {
764                id: c.id,
765                item_type: c.kind,
766            };
767
768            assert_eq!(id.to_uri().unwrap(), c.uri);
769        }
770    }
771
772    #[test]
773    fn from_raw() {
774        for c in &CONV_VALID {
775            assert_eq!(SpotifyId::from_raw(c.raw).unwrap().id, c.id);
776        }
777
778        for c in &CONV_INVALID {
779            assert!(SpotifyId::from_raw(c.raw).is_err());
780        }
781    }
782}