mirror of
https://github.com/librespot-org/librespot.git
synced 2025-10-05 02:39:53 +02:00
Major metadata refactoring and enhancement
* Expose all fields of recent protobufs * Add support for user-scoped playlists, user root playlists and playlist annotations * Convert messages with the Rust type system * Attempt to adhere to embargos (tracks and episodes scheduled for future release) * Return `Result`s with meaningful errors instead of panicking on `unwrap`s * Add foundation for future playlist editing * Up version in connection handshake to get all version-gated features
This commit is contained in:
parent
47badd61e0
commit
0e2686863a
36 changed files with 2530 additions and 757 deletions
|
@ -1,31 +1,46 @@
|
|||
#![allow(clippy::wrong_self_convention)]
|
||||
use librespot_protocol as protocol;
|
||||
|
||||
use std::convert::TryInto;
|
||||
use thiserror::Error;
|
||||
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum SpotifyAudioType {
|
||||
pub enum SpotifyItemType {
|
||||
Album,
|
||||
Artist,
|
||||
Episode,
|
||||
Playlist,
|
||||
Show,
|
||||
Track,
|
||||
Podcast,
|
||||
NonPlayable,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<&str> for SpotifyAudioType {
|
||||
impl From<&str> for SpotifyItemType {
|
||||
fn from(v: &str) -> Self {
|
||||
match v {
|
||||
"track" => SpotifyAudioType::Track,
|
||||
"episode" => SpotifyAudioType::Podcast,
|
||||
_ => SpotifyAudioType::NonPlayable,
|
||||
"album" => Self::Album,
|
||||
"artist" => Self::Artist,
|
||||
"episode" => Self::Episode,
|
||||
"playlist" => Self::Playlist,
|
||||
"show" => Self::Show,
|
||||
"track" => Self::Track,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SpotifyAudioType> for &str {
|
||||
fn from(audio_type: SpotifyAudioType) -> &'static str {
|
||||
match audio_type {
|
||||
SpotifyAudioType::Track => "track",
|
||||
SpotifyAudioType::Podcast => "episode",
|
||||
SpotifyAudioType::NonPlayable => "unknown",
|
||||
impl From<SpotifyItemType> for &str {
|
||||
fn from(item_type: SpotifyItemType) -> &'static str {
|
||||
match item_type {
|
||||
SpotifyItemType::Album => "album",
|
||||
SpotifyItemType::Artist => "artist",
|
||||
SpotifyItemType::Episode => "episode",
|
||||
SpotifyItemType::Playlist => "playlist",
|
||||
SpotifyItemType::Show => "show",
|
||||
SpotifyItemType::Track => "track",
|
||||
_ => "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,11 +48,21 @@ impl From<SpotifyAudioType> for &str {
|
|||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct SpotifyId {
|
||||
pub id: u128,
|
||||
pub audio_type: SpotifyAudioType,
|
||||
pub item_type: SpotifyItemType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct SpotifyIdError;
|
||||
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SpotifyIdError {
|
||||
#[error("ID cannot be parsed")]
|
||||
InvalidId,
|
||||
#[error("not a valid Spotify URI")]
|
||||
InvalidFormat,
|
||||
#[error("URI does not belong to Spotify")]
|
||||
InvalidRoot,
|
||||
}
|
||||
|
||||
pub type SpotifyIdResult = Result<SpotifyId, SpotifyIdError>;
|
||||
pub type NamedSpotifyIdResult = Result<NamedSpotifyId, SpotifyIdError>;
|
||||
|
||||
const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef";
|
||||
|
@ -47,11 +72,12 @@ impl SpotifyId {
|
|||
const SIZE_BASE16: usize = 32;
|
||||
const SIZE_BASE62: usize = 22;
|
||||
|
||||
fn track(n: u128) -> SpotifyId {
|
||||
SpotifyId {
|
||||
id: n,
|
||||
audio_type: SpotifyAudioType::Track,
|
||||
}
|
||||
/// Returns whether this `SpotifyId` is for a playable audio item, if known.
|
||||
pub fn is_playable(&self) -> bool {
|
||||
return matches!(
|
||||
self.item_type,
|
||||
SpotifyItemType::Episode | SpotifyItemType::Track
|
||||
);
|
||||
}
|
||||
|
||||
/// Parses a base16 (hex) encoded [Spotify ID] into a `SpotifyId`.
|
||||
|
@ -59,29 +85,32 @@ impl SpotifyId {
|
|||
/// `src` is expected to be 32 bytes long and encoded using valid characters.
|
||||
///
|
||||
/// [Spotify ID]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
||||
pub fn from_base16(src: &str) -> Result<SpotifyId, SpotifyIdError> {
|
||||
pub fn from_base16(src: &str) -> SpotifyIdResult {
|
||||
let mut dst: u128 = 0;
|
||||
|
||||
for c in src.as_bytes() {
|
||||
let p = match c {
|
||||
b'0'..=b'9' => c - b'0',
|
||||
b'a'..=b'f' => c - b'a' + 10,
|
||||
_ => return Err(SpotifyIdError),
|
||||
_ => return Err(SpotifyIdError::InvalidId),
|
||||
} as u128;
|
||||
|
||||
dst <<= 4;
|
||||
dst += p;
|
||||
}
|
||||
|
||||
Ok(SpotifyId::track(dst))
|
||||
Ok(Self {
|
||||
id: dst,
|
||||
item_type: SpotifyItemType::Unknown,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parses a base62 encoded [Spotify ID] into a `SpotifyId`.
|
||||
/// Parses a base62 encoded [Spotify ID] into a `u128`.
|
||||
///
|
||||
/// `src` is expected to be 22 bytes long and encoded using valid characters.
|
||||
///
|
||||
/// [Spotify ID]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
||||
pub fn from_base62(src: &str) -> Result<SpotifyId, SpotifyIdError> {
|
||||
pub fn from_base62(src: &str) -> SpotifyIdResult {
|
||||
let mut dst: u128 = 0;
|
||||
|
||||
for c in src.as_bytes() {
|
||||
|
@ -89,23 +118,29 @@ impl SpotifyId {
|
|||
b'0'..=b'9' => c - b'0',
|
||||
b'a'..=b'z' => c - b'a' + 10,
|
||||
b'A'..=b'Z' => c - b'A' + 36,
|
||||
_ => return Err(SpotifyIdError),
|
||||
_ => return Err(SpotifyIdError::InvalidId),
|
||||
} as u128;
|
||||
|
||||
dst *= 62;
|
||||
dst += p;
|
||||
}
|
||||
|
||||
Ok(SpotifyId::track(dst))
|
||||
Ok(Self {
|
||||
id: dst,
|
||||
item_type: SpotifyItemType::Unknown,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a `SpotifyId` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order.
|
||||
/// Creates a `u128` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order.
|
||||
///
|
||||
/// The resulting `SpotifyId` will default to a `SpotifyAudioType::TRACK`.
|
||||
pub fn from_raw(src: &[u8]) -> Result<SpotifyId, SpotifyIdError> {
|
||||
/// The resulting `SpotifyId` will default to a `SpotifyItemType::Unknown`.
|
||||
pub fn from_raw(src: &[u8]) -> SpotifyIdResult {
|
||||
match src.try_into() {
|
||||
Ok(dst) => Ok(SpotifyId::track(u128::from_be_bytes(dst))),
|
||||
Err(_) => Err(SpotifyIdError),
|
||||
Ok(dst) => Ok(Self {
|
||||
id: u128::from_be_bytes(dst),
|
||||
item_type: SpotifyItemType::Unknown,
|
||||
}),
|
||||
Err(_) => Err(SpotifyIdError::InvalidId),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,30 +149,37 @@ impl SpotifyId {
|
|||
/// `uri` is expected to be in the canonical form `spotify:{type}:{id}`, where `{type}`
|
||||
/// can be arbitrary while `{id}` is a 22-character long, base62 encoded Spotify ID.
|
||||
///
|
||||
/// Note that this should not be used for playlists, which have the form of
|
||||
/// `spotify:user:{owner_username}:playlist:{id}`.
|
||||
///
|
||||
/// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
||||
pub fn from_uri(src: &str) -> Result<SpotifyId, SpotifyIdError> {
|
||||
let src = src.strip_prefix("spotify:").ok_or(SpotifyIdError)?;
|
||||
pub fn from_uri(src: &str) -> SpotifyIdResult {
|
||||
let mut uri_parts: Vec<&str> = src.split(':').collect();
|
||||
|
||||
if src.len() <= SpotifyId::SIZE_BASE62 {
|
||||
return Err(SpotifyIdError);
|
||||
// At minimum, should be `spotify:{type}:{id}`
|
||||
if uri_parts.len() < 3 {
|
||||
return Err(SpotifyIdError::InvalidFormat);
|
||||
}
|
||||
|
||||
let colon_index = src.len() - SpotifyId::SIZE_BASE62 - 1;
|
||||
|
||||
if src.as_bytes()[colon_index] != b':' {
|
||||
return Err(SpotifyIdError);
|
||||
if uri_parts[0] != "spotify" {
|
||||
return Err(SpotifyIdError::InvalidRoot);
|
||||
}
|
||||
|
||||
let mut id = SpotifyId::from_base62(&src[colon_index + 1..])?;
|
||||
id.audio_type = src[..colon_index].into();
|
||||
let id = uri_parts.pop().unwrap();
|
||||
if id.len() != Self::SIZE_BASE62 {
|
||||
return Err(SpotifyIdError::InvalidId);
|
||||
}
|
||||
|
||||
Ok(id)
|
||||
Ok(Self {
|
||||
item_type: uri_parts.pop().unwrap().into(),
|
||||
..Self::from_base62(id)?
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE16` (32)
|
||||
/// character long `String`.
|
||||
pub fn to_base16(&self) -> String {
|
||||
to_base16(&self.to_raw(), &mut [0u8; SpotifyId::SIZE_BASE16])
|
||||
to_base16(&self.to_raw(), &mut [0u8; Self::SIZE_BASE16])
|
||||
}
|
||||
|
||||
/// Returns the `SpotifyId` as a [canonically] base62 encoded, `SpotifyId::SIZE_BASE62` (22)
|
||||
|
@ -190,7 +232,7 @@ impl SpotifyId {
|
|||
|
||||
/// Returns a copy of the `SpotifyId` as an array of `SpotifyId::SIZE` (16) bytes in
|
||||
/// big-endian order.
|
||||
pub fn to_raw(&self) -> [u8; SpotifyId::SIZE] {
|
||||
pub fn to_raw(&self) -> [u8; Self::SIZE] {
|
||||
self.id.to_be_bytes()
|
||||
}
|
||||
|
||||
|
@ -204,11 +246,11 @@ impl SpotifyId {
|
|||
/// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
||||
pub fn to_uri(&self) -> String {
|
||||
// 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID = 31
|
||||
// + unknown size audio_type.
|
||||
let audio_type: &str = self.audio_type.into();
|
||||
let mut dst = String::with_capacity(31 + audio_type.len());
|
||||
// + unknown size item_type.
|
||||
let item_type: &str = self.item_type.into();
|
||||
let mut dst = String::with_capacity(31 + item_type.len());
|
||||
dst.push_str("spotify:");
|
||||
dst.push_str(audio_type);
|
||||
dst.push_str(item_type);
|
||||
dst.push(':');
|
||||
dst.push_str(&self.to_base62());
|
||||
|
||||
|
@ -216,10 +258,214 @@ impl SpotifyId {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct NamedSpotifyId {
|
||||
pub inner_id: SpotifyId,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
impl NamedSpotifyId {
|
||||
pub fn from_uri(src: &str) -> NamedSpotifyIdResult {
|
||||
let uri_parts: Vec<&str> = src.split(':').collect();
|
||||
|
||||
// At minimum, should be `spotify:user:{username}:{type}:{id}`
|
||||
if uri_parts.len() < 5 {
|
||||
return Err(SpotifyIdError::InvalidFormat);
|
||||
}
|
||||
|
||||
if uri_parts[0] != "spotify" {
|
||||
return Err(SpotifyIdError::InvalidRoot);
|
||||
}
|
||||
|
||||
if uri_parts[1] != "user" {
|
||||
return Err(SpotifyIdError::InvalidFormat);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
inner_id: SpotifyId::from_uri(src)?,
|
||||
username: uri_parts[2].to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_uri(&self) -> String {
|
||||
let item_type: &str = self.inner_id.item_type.into();
|
||||
let mut dst = String::with_capacity(37 + self.username.len() + item_type.len());
|
||||
dst.push_str("spotify:user:");
|
||||
dst.push_str(&self.username);
|
||||
dst.push_str(item_type);
|
||||
dst.push(':');
|
||||
dst.push_str(&self.to_base62());
|
||||
|
||||
dst
|
||||
}
|
||||
|
||||
pub fn from_spotify_id(id: SpotifyId, username: String) -> Self {
|
||||
Self {
|
||||
inner_id: id,
|
||||
username,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for NamedSpotifyId {
|
||||
type Target = SpotifyId;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner_id
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(src: &[u8]) -> Result<Self, Self::Error> {
|
||||
Self::from_raw(src)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(src: &str) -> Result<Self, Self::Error> {
|
||||
Self::from_base62(src)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(src: String) -> Result<Self, Self::Error> {
|
||||
Self::try_from(src.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Vec<u8>> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(src: &Vec<u8>) -> Result<Self, Self::Error> {
|
||||
Self::try_from(src.as_slice())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(track: &protocol::spirc::TrackRef) -> Result<Self, Self::Error> {
|
||||
match SpotifyId::from_raw(track.get_gid()) {
|
||||
Ok(mut id) => {
|
||||
id.item_type = SpotifyItemType::Track;
|
||||
Ok(id)
|
||||
}
|
||||
Err(_) => SpotifyId::from_uri(track.get_uri()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&protocol::metadata::Album> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(album: &protocol::metadata::Album) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Album,
|
||||
..Self::from_raw(album.get_gid())?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&protocol::metadata::Artist> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(artist: &protocol::metadata::Artist) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Artist,
|
||||
..Self::from_raw(artist.get_gid())?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&protocol::metadata::Episode> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(episode: &protocol::metadata::Episode) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Episode,
|
||||
..Self::from_raw(episode.get_gid())?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&protocol::metadata::Track> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(track: &protocol::metadata::Track) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Track,
|
||||
..Self::from_raw(track.get_gid())?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&protocol::metadata::Show> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(show: &protocol::metadata::Show) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Show,
|
||||
..Self::from_raw(show.get_gid())?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Artist,
|
||||
..Self::from_raw(artist.get_artist_gid())?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(item: &protocol::playlist4_external::Item) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Track,
|
||||
..Self::from_uri(item.get_uri())?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Note that this is the unique revision of an item's metadata on a playlist,
|
||||
// not the ID of that item or playlist.
|
||||
impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result<Self, Self::Error> {
|
||||
Self::try_from(item.get_revision())
|
||||
}
|
||||
}
|
||||
|
||||
// Note that this is the unique revision of a playlist, not the ID of that playlist.
|
||||
impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(
|
||||
playlist: &protocol::playlist4_external::SelectedListContent,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Self::try_from(playlist.get_revision())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check meaning and format of this field in the wild. This might be a FileId,
|
||||
// which is why we now don't create a separate `Playlist` enum value yet and choose
|
||||
// to discard any item type.
|
||||
impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(
|
||||
picture: &protocol::playlist_annotate3::TranscodedPicture,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Self::from_base62(picture.get_uri())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct FileId(pub [u8; 20]);
|
||||
|
||||
impl FileId {
|
||||
pub fn from_raw(src: &[u8]) -> FileId {
|
||||
let mut dst = [0u8; 20];
|
||||
dst.clone_from_slice(src);
|
||||
FileId(dst)
|
||||
}
|
||||
|
||||
pub fn to_base16(&self) -> String {
|
||||
to_base16(&self.0, &mut [0u8; 40])
|
||||
}
|
||||
|
@ -237,6 +483,29 @@ impl fmt::Display for FileId {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for FileId {
|
||||
fn from(src: &[u8]) -> Self {
|
||||
Self::from_raw(src)
|
||||
}
|
||||
}
|
||||
impl From<&protocol::metadata::Image> for FileId {
|
||||
fn from(image: &protocol::metadata::Image) -> Self {
|
||||
Self::from(image.get_file_id())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&protocol::metadata::AudioFile> for FileId {
|
||||
fn from(file: &protocol::metadata::AudioFile) -> Self {
|
||||
Self::from(file.get_file_id())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&protocol::metadata::VideoFile> for FileId {
|
||||
fn from(video: &protocol::metadata::VideoFile) -> Self {
|
||||
Self::from(video.get_file_id())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn to_base16(src: &[u8], buf: &mut [u8]) -> String {
|
||||
let mut i = 0;
|
||||
|
@ -258,7 +527,8 @@ mod tests {
|
|||
|
||||
struct ConversionCase {
|
||||
id: u128,
|
||||
kind: SpotifyAudioType,
|
||||
kind: SpotifyItemType,
|
||||
uri_error: Option<SpotifyIdError>,
|
||||
uri: &'static str,
|
||||
base16: &'static str,
|
||||
base62: &'static str,
|
||||
|
@ -268,7 +538,8 @@ mod tests {
|
|||
static CONV_VALID: [ConversionCase; 4] = [
|
||||
ConversionCase {
|
||||
id: 238762092608182713602505436543891614649,
|
||||
kind: SpotifyAudioType::Track,
|
||||
kind: SpotifyItemType::Track,
|
||||
uri_error: None,
|
||||
uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH",
|
||||
base16: "b39fe8081e1f4c54be38e8d6f9f12bb9",
|
||||
base62: "5sWHDYs0csV6RS48xBl0tH",
|
||||
|
@ -278,7 +549,8 @@ mod tests {
|
|||
},
|
||||
ConversionCase {
|
||||
id: 204841891221366092811751085145916697048,
|
||||
kind: SpotifyAudioType::Track,
|
||||
kind: SpotifyItemType::Track,
|
||||
uri_error: None,
|
||||
uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4",
|
||||
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
|
||||
base62: "4GNcXTGWmnZ3ySrqvol3o4",
|
||||
|
@ -288,7 +560,8 @@ mod tests {
|
|||
},
|
||||
ConversionCase {
|
||||
id: 204841891221366092811751085145916697048,
|
||||
kind: SpotifyAudioType::Podcast,
|
||||
kind: SpotifyItemType::Episode,
|
||||
uri_error: None,
|
||||
uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4",
|
||||
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
|
||||
base62: "4GNcXTGWmnZ3ySrqvol3o4",
|
||||
|
@ -298,8 +571,9 @@ mod tests {
|
|||
},
|
||||
ConversionCase {
|
||||
id: 204841891221366092811751085145916697048,
|
||||
kind: SpotifyAudioType::NonPlayable,
|
||||
uri: "spotify:unknown:4GNcXTGWmnZ3ySrqvol3o4",
|
||||
kind: SpotifyItemType::Show,
|
||||
uri_error: None,
|
||||
uri: "spotify:show:4GNcXTGWmnZ3ySrqvol3o4",
|
||||
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
|
||||
base62: "4GNcXTGWmnZ3ySrqvol3o4",
|
||||
raw: &[
|
||||
|
@ -311,8 +585,9 @@ mod tests {
|
|||
static CONV_INVALID: [ConversionCase; 3] = [
|
||||
ConversionCase {
|
||||
id: 0,
|
||||
kind: SpotifyAudioType::NonPlayable,
|
||||
kind: SpotifyItemType::Unknown,
|
||||
// Invalid ID in the URI.
|
||||
uri_error: Some(SpotifyIdError::InvalidId),
|
||||
uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH",
|
||||
base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9",
|
||||
base62: "!!!!!Ys0csV6RS48xBl0tH",
|
||||
|
@ -323,8 +598,9 @@ mod tests {
|
|||
},
|
||||
ConversionCase {
|
||||
id: 0,
|
||||
kind: SpotifyAudioType::NonPlayable,
|
||||
kind: SpotifyItemType::Unknown,
|
||||
// Missing colon between ID and type.
|
||||
uri_error: Some(SpotifyIdError::InvalidFormat),
|
||||
uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH",
|
||||
base16: "--------------------",
|
||||
base62: "....................",
|
||||
|
@ -335,8 +611,9 @@ mod tests {
|
|||
},
|
||||
ConversionCase {
|
||||
id: 0,
|
||||
kind: SpotifyAudioType::NonPlayable,
|
||||
kind: SpotifyItemType::Unknown,
|
||||
// Uri too short
|
||||
uri_error: Some(SpotifyIdError::InvalidId),
|
||||
uri: "spotify:azb:aRS48xBl0tH",
|
||||
base16: "--------------------",
|
||||
base62: "....................",
|
||||
|
@ -354,7 +631,10 @@ mod tests {
|
|||
}
|
||||
|
||||
for c in &CONV_INVALID {
|
||||
assert_eq!(SpotifyId::from_base62(c.base62), Err(SpotifyIdError));
|
||||
assert_eq!(
|
||||
SpotifyId::from_base62(c.base62),
|
||||
Err(SpotifyIdError::InvalidId)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -363,7 +643,7 @@ mod tests {
|
|||
for c in &CONV_VALID {
|
||||
let id = SpotifyId {
|
||||
id: c.id,
|
||||
audio_type: c.kind,
|
||||
item_type: c.kind,
|
||||
};
|
||||
|
||||
assert_eq!(id.to_base62(), c.base62);
|
||||
|
@ -377,7 +657,10 @@ mod tests {
|
|||
}
|
||||
|
||||
for c in &CONV_INVALID {
|
||||
assert_eq!(SpotifyId::from_base16(c.base16), Err(SpotifyIdError));
|
||||
assert_eq!(
|
||||
SpotifyId::from_base16(c.base16),
|
||||
Err(SpotifyIdError::InvalidId)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -386,7 +669,7 @@ mod tests {
|
|||
for c in &CONV_VALID {
|
||||
let id = SpotifyId {
|
||||
id: c.id,
|
||||
audio_type: c.kind,
|
||||
item_type: c.kind,
|
||||
};
|
||||
|
||||
assert_eq!(id.to_base16(), c.base16);
|
||||
|
@ -399,11 +682,11 @@ mod tests {
|
|||
let actual = SpotifyId::from_uri(c.uri).unwrap();
|
||||
|
||||
assert_eq!(actual.id, c.id);
|
||||
assert_eq!(actual.audio_type, c.kind);
|
||||
assert_eq!(actual.item_type, c.kind);
|
||||
}
|
||||
|
||||
for c in &CONV_INVALID {
|
||||
assert_eq!(SpotifyId::from_uri(c.uri), Err(SpotifyIdError));
|
||||
assert_eq!(SpotifyId::from_uri(c.uri), Err(c.uri_error.unwrap()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -412,7 +695,7 @@ mod tests {
|
|||
for c in &CONV_VALID {
|
||||
let id = SpotifyId {
|
||||
id: c.id,
|
||||
audio_type: c.kind,
|
||||
item_type: c.kind,
|
||||
};
|
||||
|
||||
assert_eq!(id.to_uri(), c.uri);
|
||||
|
@ -426,7 +709,7 @@ mod tests {
|
|||
}
|
||||
|
||||
for c in &CONV_INVALID {
|
||||
assert_eq!(SpotifyId::from_raw(c.raw), Err(SpotifyIdError));
|
||||
assert_eq!(SpotifyId::from_raw(c.raw), Err(SpotifyIdError::InvalidId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue