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:
Roderick van Domburg 2021-12-07 23:22:24 +01:00
parent 47badd61e0
commit 0e2686863a
No known key found for this signature in database
GPG key ID: A9EF5222A26F0451
36 changed files with 2530 additions and 757 deletions

2
Cargo.lock generated
View file

@ -1350,11 +1350,13 @@ dependencies = [
"async-trait", "async-trait",
"byteorder", "byteorder",
"bytes", "bytes",
"chrono",
"librespot-core", "librespot-core",
"librespot-protocol", "librespot-protocol",
"log", "log",
"protobuf", "protobuf",
"thiserror", "thiserror",
"uuid",
] ]
[[package]] [[package]]

View file

@ -1,3 +1,4 @@
use std::convert::TryFrom;
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
@ -6,7 +7,7 @@ use crate::context::StationContext;
use crate::core::config::ConnectConfig; use crate::core::config::ConnectConfig;
use crate::core::mercury::{MercuryError, MercurySender}; use crate::core::mercury::{MercuryError, MercurySender};
use crate::core::session::Session; use crate::core::session::Session;
use crate::core::spotify_id::{SpotifyAudioType, SpotifyId, SpotifyIdError}; use crate::core::spotify_id::SpotifyId;
use crate::core::util::SeqGenerator; use crate::core::util::SeqGenerator;
use crate::core::version; use crate::core::version;
use crate::playback::mixer::Mixer; use crate::playback::mixer::Mixer;
@ -1099,15 +1100,6 @@ impl SpircTask {
} }
} }
// should this be a method of SpotifyId directly?
fn get_spotify_id_for_track(&self, track_ref: &TrackRef) -> Result<SpotifyId, SpotifyIdError> {
SpotifyId::from_raw(track_ref.get_gid()).or_else(|_| {
let uri = track_ref.get_uri();
debug!("Malformed or no gid, attempting to parse URI <{}>", uri);
SpotifyId::from_uri(uri)
})
}
// Helper to find corresponding index(s) for track_id // Helper to find corresponding index(s) for track_id
fn get_track_index_for_spotify_id( fn get_track_index_for_spotify_id(
&self, &self,
@ -1146,11 +1138,8 @@ impl SpircTask {
// E.g - context based frames sometimes contain tracks with <spotify:meta:page:> // E.g - context based frames sometimes contain tracks with <spotify:meta:page:>
let mut track_ref = self.state.get_track()[new_playlist_index].clone(); let mut track_ref = self.state.get_track()[new_playlist_index].clone();
let mut track_id = self.get_spotify_id_for_track(&track_ref); let mut track_id = SpotifyId::try_from(&track_ref);
while self.track_ref_is_unavailable(&track_ref) while self.track_ref_is_unavailable(&track_ref) || track_id.is_err() {
|| track_id.is_err()
|| track_id.unwrap().audio_type == SpotifyAudioType::NonPlayable
{
warn!( warn!(
"Skipping track <{:?}> at position [{}] of {}", "Skipping track <{:?}> at position [{}] of {}",
track_ref, new_playlist_index, tracks_len track_ref, new_playlist_index, tracks_len
@ -1166,7 +1155,7 @@ impl SpircTask {
return None; return None;
} }
track_ref = self.state.get_track()[new_playlist_index].clone(); track_ref = self.state.get_track()[new_playlist_index].clone();
track_id = self.get_spotify_id_for_track(&track_ref); track_id = SpotifyId::try_from(&track_ref);
} }
match track_id { match track_id {

View file

@ -49,7 +49,7 @@ where
packet packet
.mut_build_info() .mut_build_info()
.set_platform(protocol::keyexchange::Platform::PLATFORM_LINUX_X86); .set_platform(protocol::keyexchange::Platform::PLATFORM_LINUX_X86);
packet.mut_build_info().set_version(109800078); packet.mut_build_info().set_version(999999999);
packet packet
.mut_cryptosuites_supported() .mut_cryptosuites_supported()
.push(protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON); .push(protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON);

View file

@ -227,7 +227,6 @@ impl SpClient {
self.get_metadata("show", show_id).await self.get_metadata("show", show_id).await
} }
// TODO: Not working at the moment, always returns 400.
pub async fn get_lyrics(&self, track_id: SpotifyId, image_id: FileId) -> SpClientResult { pub async fn get_lyrics(&self, track_id: SpotifyId, image_id: FileId) -> SpClientResult {
let endpoint = format!( let endpoint = format!(
"/color-lyrics/v2/track/{}/image/spotify:image:{}", "/color-lyrics/v2/track/{}/image/spotify:image:{}",

View file

@ -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::fmt;
use std::ops::Deref;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SpotifyAudioType { pub enum SpotifyItemType {
Album,
Artist,
Episode,
Playlist,
Show,
Track, Track,
Podcast, Unknown,
NonPlayable,
} }
impl From<&str> for SpotifyAudioType { impl From<&str> for SpotifyItemType {
fn from(v: &str) -> Self { fn from(v: &str) -> Self {
match v { match v {
"track" => SpotifyAudioType::Track, "album" => Self::Album,
"episode" => SpotifyAudioType::Podcast, "artist" => Self::Artist,
_ => SpotifyAudioType::NonPlayable, "episode" => Self::Episode,
"playlist" => Self::Playlist,
"show" => Self::Show,
"track" => Self::Track,
_ => Self::Unknown,
} }
} }
} }
impl From<SpotifyAudioType> for &str { impl From<SpotifyItemType> for &str {
fn from(audio_type: SpotifyAudioType) -> &'static str { fn from(item_type: SpotifyItemType) -> &'static str {
match audio_type { match item_type {
SpotifyAudioType::Track => "track", SpotifyItemType::Album => "album",
SpotifyAudioType::Podcast => "episode", SpotifyItemType::Artist => "artist",
SpotifyAudioType::NonPlayable => "unknown", 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SpotifyId { pub struct SpotifyId {
pub id: u128, pub id: u128,
pub audio_type: SpotifyAudioType, pub item_type: SpotifyItemType,
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
pub struct SpotifyIdError; 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 BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef"; const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef";
@ -47,11 +72,12 @@ impl SpotifyId {
const SIZE_BASE16: usize = 32; const SIZE_BASE16: usize = 32;
const SIZE_BASE62: usize = 22; const SIZE_BASE62: usize = 22;
fn track(n: u128) -> SpotifyId { /// Returns whether this `SpotifyId` is for a playable audio item, if known.
SpotifyId { pub fn is_playable(&self) -> bool {
id: n, return matches!(
audio_type: SpotifyAudioType::Track, self.item_type,
} SpotifyItemType::Episode | SpotifyItemType::Track
);
} }
/// Parses a base16 (hex) encoded [Spotify ID] into a `SpotifyId`. /// 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. /// `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 /// [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; let mut dst: u128 = 0;
for c in src.as_bytes() { for c in src.as_bytes() {
let p = match c { let p = match c {
b'0'..=b'9' => c - b'0', b'0'..=b'9' => c - b'0',
b'a'..=b'f' => c - b'a' + 10, b'a'..=b'f' => c - b'a' + 10,
_ => return Err(SpotifyIdError), _ => return Err(SpotifyIdError::InvalidId),
} as u128; } as u128;
dst <<= 4; dst <<= 4;
dst += p; 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. /// `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 /// [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; let mut dst: u128 = 0;
for c in src.as_bytes() { for c in src.as_bytes() {
@ -89,23 +118,29 @@ impl SpotifyId {
b'0'..=b'9' => c - b'0', b'0'..=b'9' => c - b'0',
b'a'..=b'z' => c - b'a' + 10, b'a'..=b'z' => c - b'a' + 10,
b'A'..=b'Z' => c - b'A' + 36, b'A'..=b'Z' => c - b'A' + 36,
_ => return Err(SpotifyIdError), _ => return Err(SpotifyIdError::InvalidId),
} as u128; } as u128;
dst *= 62; dst *= 62;
dst += p; 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`. /// The resulting `SpotifyId` will default to a `SpotifyItemType::Unknown`.
pub fn from_raw(src: &[u8]) -> Result<SpotifyId, SpotifyIdError> { pub fn from_raw(src: &[u8]) -> SpotifyIdResult {
match src.try_into() { match src.try_into() {
Ok(dst) => Ok(SpotifyId::track(u128::from_be_bytes(dst))), Ok(dst) => Ok(Self {
Err(_) => Err(SpotifyIdError), 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}` /// `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. /// 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 /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
pub fn from_uri(src: &str) -> Result<SpotifyId, SpotifyIdError> { pub fn from_uri(src: &str) -> SpotifyIdResult {
let src = src.strip_prefix("spotify:").ok_or(SpotifyIdError)?; let mut uri_parts: Vec<&str> = src.split(':').collect();
if src.len() <= SpotifyId::SIZE_BASE62 { // At minimum, should be `spotify:{type}:{id}`
return Err(SpotifyIdError); if uri_parts.len() < 3 {
return Err(SpotifyIdError::InvalidFormat);
} }
let colon_index = src.len() - SpotifyId::SIZE_BASE62 - 1; if uri_parts[0] != "spotify" {
return Err(SpotifyIdError::InvalidRoot);
if src.as_bytes()[colon_index] != b':' {
return Err(SpotifyIdError);
} }
let mut id = SpotifyId::from_base62(&src[colon_index + 1..])?; let id = uri_parts.pop().unwrap();
id.audio_type = src[..colon_index].into(); 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) /// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE16` (32)
/// character long `String`. /// character long `String`.
pub fn to_base16(&self) -> 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) /// 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 /// Returns a copy of the `SpotifyId` as an array of `SpotifyId::SIZE` (16) bytes in
/// big-endian order. /// big-endian order.
pub fn to_raw(&self) -> [u8; SpotifyId::SIZE] { pub fn to_raw(&self) -> [u8; Self::SIZE] {
self.id.to_be_bytes() self.id.to_be_bytes()
} }
@ -204,11 +246,11 @@ impl SpotifyId {
/// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
pub fn to_uri(&self) -> String { pub fn to_uri(&self) -> String {
// 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID = 31 // 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID = 31
// + unknown size audio_type. // + unknown size item_type.
let audio_type: &str = self.audio_type.into(); let item_type: &str = self.item_type.into();
let mut dst = String::with_capacity(31 + audio_type.len()); let mut dst = String::with_capacity(31 + item_type.len());
dst.push_str("spotify:"); dst.push_str("spotify:");
dst.push_str(audio_type); dst.push_str(item_type);
dst.push(':'); dst.push(':');
dst.push_str(&self.to_base62()); 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)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FileId(pub [u8; 20]); pub struct FileId(pub [u8; 20]);
impl FileId { 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 { pub fn to_base16(&self) -> String {
to_base16(&self.0, &mut [0u8; 40]) 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] #[inline]
fn to_base16(src: &[u8], buf: &mut [u8]) -> String { fn to_base16(src: &[u8], buf: &mut [u8]) -> String {
let mut i = 0; let mut i = 0;
@ -258,7 +527,8 @@ mod tests {
struct ConversionCase { struct ConversionCase {
id: u128, id: u128,
kind: SpotifyAudioType, kind: SpotifyItemType,
uri_error: Option<SpotifyIdError>,
uri: &'static str, uri: &'static str,
base16: &'static str, base16: &'static str,
base62: &'static str, base62: &'static str,
@ -268,7 +538,8 @@ mod tests {
static CONV_VALID: [ConversionCase; 4] = [ static CONV_VALID: [ConversionCase; 4] = [
ConversionCase { ConversionCase {
id: 238762092608182713602505436543891614649, id: 238762092608182713602505436543891614649,
kind: SpotifyAudioType::Track, kind: SpotifyItemType::Track,
uri_error: None,
uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH", uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH",
base16: "b39fe8081e1f4c54be38e8d6f9f12bb9", base16: "b39fe8081e1f4c54be38e8d6f9f12bb9",
base62: "5sWHDYs0csV6RS48xBl0tH", base62: "5sWHDYs0csV6RS48xBl0tH",
@ -278,7 +549,8 @@ mod tests {
}, },
ConversionCase { ConversionCase {
id: 204841891221366092811751085145916697048, id: 204841891221366092811751085145916697048,
kind: SpotifyAudioType::Track, kind: SpotifyItemType::Track,
uri_error: None,
uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4", uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4",
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
base62: "4GNcXTGWmnZ3ySrqvol3o4", base62: "4GNcXTGWmnZ3ySrqvol3o4",
@ -288,7 +560,8 @@ mod tests {
}, },
ConversionCase { ConversionCase {
id: 204841891221366092811751085145916697048, id: 204841891221366092811751085145916697048,
kind: SpotifyAudioType::Podcast, kind: SpotifyItemType::Episode,
uri_error: None,
uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4", uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4",
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
base62: "4GNcXTGWmnZ3ySrqvol3o4", base62: "4GNcXTGWmnZ3ySrqvol3o4",
@ -298,8 +571,9 @@ mod tests {
}, },
ConversionCase { ConversionCase {
id: 204841891221366092811751085145916697048, id: 204841891221366092811751085145916697048,
kind: SpotifyAudioType::NonPlayable, kind: SpotifyItemType::Show,
uri: "spotify:unknown:4GNcXTGWmnZ3ySrqvol3o4", uri_error: None,
uri: "spotify:show:4GNcXTGWmnZ3ySrqvol3o4",
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
base62: "4GNcXTGWmnZ3ySrqvol3o4", base62: "4GNcXTGWmnZ3ySrqvol3o4",
raw: &[ raw: &[
@ -311,8 +585,9 @@ mod tests {
static CONV_INVALID: [ConversionCase; 3] = [ static CONV_INVALID: [ConversionCase; 3] = [
ConversionCase { ConversionCase {
id: 0, id: 0,
kind: SpotifyAudioType::NonPlayable, kind: SpotifyItemType::Unknown,
// Invalid ID in the URI. // Invalid ID in the URI.
uri_error: Some(SpotifyIdError::InvalidId),
uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH", uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH",
base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9", base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9",
base62: "!!!!!Ys0csV6RS48xBl0tH", base62: "!!!!!Ys0csV6RS48xBl0tH",
@ -323,8 +598,9 @@ mod tests {
}, },
ConversionCase { ConversionCase {
id: 0, id: 0,
kind: SpotifyAudioType::NonPlayable, kind: SpotifyItemType::Unknown,
// Missing colon between ID and type. // Missing colon between ID and type.
uri_error: Some(SpotifyIdError::InvalidFormat),
uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH", uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH",
base16: "--------------------", base16: "--------------------",
base62: "....................", base62: "....................",
@ -335,8 +611,9 @@ mod tests {
}, },
ConversionCase { ConversionCase {
id: 0, id: 0,
kind: SpotifyAudioType::NonPlayable, kind: SpotifyItemType::Unknown,
// Uri too short // Uri too short
uri_error: Some(SpotifyIdError::InvalidId),
uri: "spotify:azb:aRS48xBl0tH", uri: "spotify:azb:aRS48xBl0tH",
base16: "--------------------", base16: "--------------------",
base62: "....................", base62: "....................",
@ -354,7 +631,10 @@ mod tests {
} }
for c in &CONV_INVALID { 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 { for c in &CONV_VALID {
let id = SpotifyId { let id = SpotifyId {
id: c.id, id: c.id,
audio_type: c.kind, item_type: c.kind,
}; };
assert_eq!(id.to_base62(), c.base62); assert_eq!(id.to_base62(), c.base62);
@ -377,7 +657,10 @@ mod tests {
} }
for c in &CONV_INVALID { 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 { for c in &CONV_VALID {
let id = SpotifyId { let id = SpotifyId {
id: c.id, id: c.id,
audio_type: c.kind, item_type: c.kind,
}; };
assert_eq!(id.to_base16(), c.base16); assert_eq!(id.to_base16(), c.base16);
@ -399,11 +682,11 @@ mod tests {
let actual = SpotifyId::from_uri(c.uri).unwrap(); let actual = SpotifyId::from_uri(c.uri).unwrap();
assert_eq!(actual.id, c.id); 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 { 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 { for c in &CONV_VALID {
let id = SpotifyId { let id = SpotifyId {
id: c.id, id: c.id,
audio_type: c.kind, item_type: c.kind,
}; };
assert_eq!(id.to_uri(), c.uri); assert_eq!(id.to_uri(), c.uri);
@ -426,7 +709,7 @@ mod tests {
} }
for c in &CONV_INVALID { for c in &CONV_INVALID {
assert_eq!(SpotifyId::from_raw(c.raw), Err(SpotifyIdError)); assert_eq!(SpotifyId::from_raw(c.raw), Err(SpotifyIdError::InvalidId));
} }
} }
} }

View file

@ -11,9 +11,11 @@ edition = "2018"
async-trait = "0.1" async-trait = "0.1"
byteorder = "1.3" byteorder = "1.3"
bytes = "1.0" bytes = "1.0"
chrono = "0.4"
log = "0.4" log = "0.4"
protobuf = "2.14.0" protobuf = "2.14.0"
thiserror = "1" thiserror = "1"
uuid = { version = "0.8", default-features = false }
[dependencies.librespot-core] [dependencies.librespot-core]
path = "../core" path = "../core"

151
metadata/src/album.rs Normal file
View file

@ -0,0 +1,151 @@
use std::convert::{TryFrom, TryInto};
use std::fmt::Debug;
use std::ops::Deref;
use crate::{
artist::Artists,
availability::Availabilities,
copyright::Copyrights,
date::Date,
error::{MetadataError, RequestError},
external_id::ExternalIds,
image::Images,
request::RequestResult,
restriction::Restrictions,
sale_period::SalePeriods,
track::Tracks,
util::try_from_repeated_message,
Metadata,
};
use librespot_core::session::Session;
use librespot_core::spotify_id::SpotifyId;
use librespot_protocol as protocol;
use protocol::metadata::Disc as DiscMessage;
pub use protocol::metadata::Album_Type as AlbumType;
#[derive(Debug, Clone)]
pub struct Album {
pub id: SpotifyId,
pub name: String,
pub artists: Artists,
pub album_type: AlbumType,
pub label: String,
pub date: Date,
pub popularity: i32,
pub genres: Vec<String>,
pub covers: Images,
pub external_ids: ExternalIds,
pub discs: Discs,
pub reviews: Vec<String>,
pub copyrights: Copyrights,
pub restrictions: Restrictions,
pub related: Albums,
pub sale_periods: SalePeriods,
pub cover_group: Images,
pub original_title: String,
pub version_title: String,
pub type_str: String,
pub availability: Availabilities,
}
#[derive(Debug, Clone)]
pub struct Albums(pub Vec<SpotifyId>);
impl Deref for Albums {
type Target = Vec<SpotifyId>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct Disc {
pub number: i32,
pub name: String,
pub tracks: Tracks,
}
#[derive(Debug, Clone)]
pub struct Discs(pub Vec<Disc>);
impl Deref for Discs {
type Target = Vec<Disc>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Album {
pub fn tracks(&self) -> Tracks {
let result = self
.discs
.iter()
.flat_map(|disc| disc.tracks.deref().clone())
.collect();
Tracks(result)
}
}
#[async_trait]
impl Metadata for Album {
type Message = protocol::metadata::Album;
async fn request(session: &Session, album_id: SpotifyId) -> RequestResult {
session
.spclient()
.get_album_metadata(album_id)
.await
.map_err(RequestError::Http)
}
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, MetadataError> {
Self::try_from(msg)
}
}
impl TryFrom<&<Self as Metadata>::Message> for Album {
type Error = MetadataError;
fn try_from(album: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self {
id: album.try_into()?,
name: album.get_name().to_owned(),
artists: album.get_artist().try_into()?,
album_type: album.get_field_type(),
label: album.get_label().to_owned(),
date: album.get_date().into(),
popularity: album.get_popularity(),
genres: album.get_genre().to_vec(),
covers: album.get_cover().into(),
external_ids: album.get_external_id().into(),
discs: album.get_disc().try_into()?,
reviews: album.get_review().to_vec(),
copyrights: album.get_copyright().into(),
restrictions: album.get_restriction().into(),
related: album.get_related().try_into()?,
sale_periods: album.get_sale_period().into(),
cover_group: album.get_cover_group().get_image().into(),
original_title: album.get_original_title().to_owned(),
version_title: album.get_version_title().to_owned(),
type_str: album.get_type_str().to_owned(),
availability: album.get_availability().into(),
})
}
}
try_from_repeated_message!(<Album as Metadata>::Message, Albums);
impl TryFrom<&DiscMessage> for Disc {
type Error = MetadataError;
fn try_from(disc: &DiscMessage) -> Result<Self, Self::Error> {
Ok(Self {
number: disc.get_number(),
name: disc.get_name().to_owned(),
tracks: disc.get_track().try_into()?,
})
}
}
try_from_repeated_message!(DiscMessage, Discs);

139
metadata/src/artist.rs Normal file
View file

@ -0,0 +1,139 @@
use std::convert::{TryFrom, TryInto};
use std::fmt::Debug;
use std::ops::Deref;
use crate::{
error::{MetadataError, RequestError},
request::RequestResult,
track::Tracks,
util::try_from_repeated_message,
Metadata,
};
use librespot_core::session::Session;
use librespot_core::spotify_id::SpotifyId;
use librespot_protocol as protocol;
use protocol::metadata::ArtistWithRole as ArtistWithRoleMessage;
use protocol::metadata::TopTracks as TopTracksMessage;
pub use protocol::metadata::ArtistWithRole_ArtistRole as ArtistRole;
#[derive(Debug, Clone)]
pub struct Artist {
pub id: SpotifyId,
pub name: String,
pub top_tracks: CountryTopTracks,
}
#[derive(Debug, Clone)]
pub struct Artists(pub Vec<SpotifyId>);
impl Deref for Artists {
type Target = Vec<SpotifyId>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct ArtistWithRole {
pub id: SpotifyId,
pub name: String,
pub role: ArtistRole,
}
#[derive(Debug, Clone)]
pub struct ArtistsWithRole(pub Vec<ArtistWithRole>);
impl Deref for ArtistsWithRole {
type Target = Vec<ArtistWithRole>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct TopTracks {
pub country: String,
pub tracks: Tracks,
}
#[derive(Debug, Clone)]
pub struct CountryTopTracks(pub Vec<TopTracks>);
impl Deref for CountryTopTracks {
type Target = Vec<TopTracks>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl CountryTopTracks {
pub fn for_country(&self, country: &str) -> Tracks {
if let Some(country) = self.0.iter().find(|top_track| top_track.country == country) {
return country.tracks.clone();
}
if let Some(global) = self.0.iter().find(|top_track| top_track.country.is_empty()) {
return global.tracks.clone();
}
Tracks(vec![]) // none found
}
}
#[async_trait]
impl Metadata for Artist {
type Message = protocol::metadata::Artist;
async fn request(session: &Session, artist_id: SpotifyId) -> RequestResult {
session
.spclient()
.get_artist_metadata(artist_id)
.await
.map_err(RequestError::Http)
}
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, MetadataError> {
Self::try_from(msg)
}
}
impl TryFrom<&<Self as Metadata>::Message> for Artist {
type Error = MetadataError;
fn try_from(artist: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self {
id: artist.try_into()?,
name: artist.get_name().to_owned(),
top_tracks: artist.get_top_track().try_into()?,
})
}
}
try_from_repeated_message!(<Artist as Metadata>::Message, Artists);
impl TryFrom<&ArtistWithRoleMessage> for ArtistWithRole {
type Error = MetadataError;
fn try_from(artist_with_role: &ArtistWithRoleMessage) -> Result<Self, Self::Error> {
Ok(Self {
id: artist_with_role.try_into()?,
name: artist_with_role.get_artist_name().to_owned(),
role: artist_with_role.get_role(),
})
}
}
try_from_repeated_message!(ArtistWithRoleMessage, ArtistsWithRole);
impl TryFrom<&TopTracksMessage> for TopTracks {
type Error = MetadataError;
fn try_from(top_tracks: &TopTracksMessage) -> Result<Self, Self::Error> {
Ok(Self {
country: top_tracks.get_country().to_owned(),
tracks: top_tracks.get_track().try_into()?,
})
}
}
try_from_repeated_message!(TopTracksMessage, CountryTopTracks);

View file

@ -0,0 +1,31 @@
use std::collections::HashMap;
use std::fmt::Debug;
use std::ops::Deref;
use librespot_core::spotify_id::FileId;
use librespot_protocol as protocol;
use protocol::metadata::AudioFile as AudioFileMessage;
pub use protocol::metadata::AudioFile_Format as AudioFileFormat;
#[derive(Debug, Clone)]
pub struct AudioFiles(pub HashMap<AudioFileFormat, FileId>);
impl Deref for AudioFiles {
type Target = HashMap<AudioFileFormat, FileId>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<&[AudioFileMessage]> for AudioFiles {
fn from(files: &[AudioFileMessage]) -> Self {
let audio_files = files
.iter()
.map(|file| (file.get_format(), FileId::from(file.get_file_id())))
.collect();
AudioFiles(audio_files)
}
}

104
metadata/src/audio/item.rs Normal file
View file

@ -0,0 +1,104 @@
use std::fmt::Debug;
use chrono::Local;
use crate::{
availability::{AudioItemAvailability, Availabilities, UnavailabilityReason},
episode::Episode,
error::MetadataError,
restriction::Restrictions,
track::{Track, Tracks},
};
use super::file::AudioFiles;
use librespot_core::session::Session;
use librespot_core::spotify_id::{SpotifyId, SpotifyItemType};
pub type AudioItemResult = Result<AudioItem, MetadataError>;
// A wrapper with fields the player needs
#[derive(Debug, Clone)]
pub struct AudioItem {
pub id: SpotifyId,
pub spotify_uri: String,
pub files: AudioFiles,
pub name: String,
pub duration: i32,
pub availability: AudioItemAvailability,
pub alternatives: Option<Tracks>,
}
impl AudioItem {
pub async fn get_file(session: &Session, id: SpotifyId) -> AudioItemResult {
match id.item_type {
SpotifyItemType::Track => Track::get_audio_item(session, id).await,
SpotifyItemType::Episode => Episode::get_audio_item(session, id).await,
_ => Err(MetadataError::NonPlayable),
}
}
}
#[async_trait]
pub trait InnerAudioItem {
async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult;
fn allowed_in_country(restrictions: &Restrictions, country: &str) -> AudioItemAvailability {
for premium_restriction in restrictions.iter().filter(|restriction| {
restriction
.catalogue_strs
.iter()
.any(|catalogue| *catalogue == "premium")
}) {
if let Some(allowed_countries) = &premium_restriction.countries_allowed {
// A restriction will specify either a whitelast *or* a blacklist,
// but not both. So restrict availability if there is a whitelist
// and the country isn't on it.
if allowed_countries.iter().any(|allowed| country == *allowed) {
return Ok(());
} else {
return Err(UnavailabilityReason::NotWhitelisted);
}
}
if let Some(forbidden_countries) = &premium_restriction.countries_forbidden {
if forbidden_countries
.iter()
.any(|forbidden| country == *forbidden)
{
return Err(UnavailabilityReason::Blacklisted);
} else {
return Ok(());
}
}
}
Ok(()) // no restrictions in place
}
fn available(availability: &Availabilities) -> AudioItemAvailability {
if availability.is_empty() {
// not all items have availability specified
return Ok(());
}
if !(availability
.iter()
.any(|availability| Local::now() >= availability.start.as_utc()))
{
return Err(UnavailabilityReason::Embargo);
}
Ok(())
}
fn available_in_country(
availability: &Availabilities,
restrictions: &Restrictions,
country: &str,
) -> AudioItemAvailability {
Self::available(availability)?;
Self::allowed_in_country(restrictions, country)?;
Ok(())
}
}

View file

@ -0,0 +1,5 @@
pub mod file;
pub mod item;
pub use file::AudioFileFormat;
pub use item::AudioItem;

View file

@ -0,0 +1,49 @@
use std::fmt::Debug;
use std::ops::Deref;
use thiserror::Error;
use crate::{date::Date, util::from_repeated_message};
use librespot_protocol as protocol;
use protocol::metadata::Availability as AvailabilityMessage;
pub type AudioItemAvailability = Result<(), UnavailabilityReason>;
#[derive(Debug, Clone)]
pub struct Availability {
pub catalogue_strs: Vec<String>,
pub start: Date,
}
#[derive(Debug, Clone)]
pub struct Availabilities(pub Vec<Availability>);
impl Deref for Availabilities {
type Target = Vec<Availability>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Copy, Clone, Error)]
pub enum UnavailabilityReason {
#[error("blacklist present and country on it")]
Blacklisted,
#[error("available date is in the future")]
Embargo,
#[error("whitelist present and country not on it")]
NotWhitelisted,
}
impl From<&AvailabilityMessage> for Availability {
fn from(availability: &AvailabilityMessage) -> Self {
Self {
catalogue_strs: availability.get_catalogue_str().to_vec(),
start: availability.get_start().into(),
}
}
}
from_repeated_message!(AvailabilityMessage, Availabilities);

View file

@ -0,0 +1,35 @@
use std::fmt::Debug;
use std::ops::Deref;
use crate::util::from_repeated_message;
use librespot_protocol as protocol;
use protocol::metadata::ContentRating as ContentRatingMessage;
#[derive(Debug, Clone)]
pub struct ContentRating {
pub country: String,
pub tags: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ContentRatings(pub Vec<ContentRating>);
impl Deref for ContentRatings {
type Target = Vec<ContentRating>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<&ContentRatingMessage> for ContentRating {
fn from(content_rating: &ContentRatingMessage) -> Self {
Self {
country: content_rating.get_country().to_owned(),
tags: content_rating.get_tag().to_vec(),
}
}
}
from_repeated_message!(ContentRatingMessage, ContentRatings);

37
metadata/src/copyright.rs Normal file
View file

@ -0,0 +1,37 @@
use std::fmt::Debug;
use std::ops::Deref;
use librespot_protocol as protocol;
use crate::util::from_repeated_message;
use protocol::metadata::Copyright as CopyrightMessage;
pub use protocol::metadata::Copyright_Type as CopyrightType;
#[derive(Debug, Clone)]
pub struct Copyright {
pub copyright_type: CopyrightType,
pub text: String,
}
#[derive(Debug, Clone)]
pub struct Copyrights(pub Vec<Copyright>);
impl Deref for Copyrights {
type Target = Vec<Copyright>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<&CopyrightMessage> for Copyright {
fn from(copyright: &CopyrightMessage) -> Self {
Self {
copyright_type: copyright.get_field_type(),
text: copyright.get_text().to_owned(),
}
}
}
from_repeated_message!(CopyrightMessage, Copyrights);

View file

@ -1,20 +0,0 @@
use byteorder::{BigEndian, WriteBytesExt};
use std::io::Write;
use librespot_core::channel::ChannelData;
use librespot_core::packet::PacketType;
use librespot_core::session::Session;
use librespot_core::spotify_id::FileId;
pub fn get(session: &Session, file: FileId) -> ChannelData {
let (channel_id, channel) = session.channel().allocate();
let (_headers, data) = channel.split();
let mut packet: Vec<u8> = Vec::new();
packet.write_u16::<BigEndian>(channel_id).unwrap();
packet.write_u16::<BigEndian>(0).unwrap();
packet.write(&file.0).unwrap();
session.send_packet(PacketType::Image, packet);
data
}

70
metadata/src/date.rs Normal file
View file

@ -0,0 +1,70 @@
use std::convert::TryFrom;
use std::fmt::Debug;
use std::ops::Deref;
use chrono::{DateTime, Utc};
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use crate::error::MetadataError;
use librespot_protocol as protocol;
use protocol::metadata::Date as DateMessage;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Date(pub DateTime<Utc>);
impl Deref for Date {
type Target = DateTime<Utc>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Date {
pub fn as_timestamp(&self) -> i64 {
self.0.timestamp()
}
pub fn from_timestamp(timestamp: i64) -> Result<Self, MetadataError> {
if let Some(date_time) = NaiveDateTime::from_timestamp_opt(timestamp, 0) {
Ok(Self::from_utc(date_time))
} else {
Err(MetadataError::InvalidTimestamp)
}
}
pub fn as_utc(&self) -> DateTime<Utc> {
self.0
}
pub fn from_utc(date_time: NaiveDateTime) -> Self {
Self(DateTime::<Utc>::from_utc(date_time, Utc))
}
}
impl From<&DateMessage> for Date {
fn from(date: &DateMessage) -> Self {
let naive_date = NaiveDate::from_ymd(
date.get_year() as i32,
date.get_month() as u32,
date.get_day() as u32,
);
let naive_time = NaiveTime::from_hms(date.get_hour() as u32, date.get_minute() as u32, 0);
let naive_datetime = NaiveDateTime::new(naive_date, naive_time);
Self(DateTime::<Utc>::from_utc(naive_datetime, Utc))
}
}
impl From<DateTime<Utc>> for Date {
fn from(date: DateTime<Utc>) -> Self {
Self(date)
}
}
impl TryFrom<i64> for Date {
type Error = MetadataError;
fn try_from(timestamp: i64) -> Result<Self, Self::Error> {
Self::from_timestamp(timestamp)
}
}

132
metadata/src/episode.rs Normal file
View file

@ -0,0 +1,132 @@
use std::convert::{TryFrom, TryInto};
use std::fmt::Debug;
use std::ops::Deref;
use crate::{
audio::{
file::AudioFiles,
item::{AudioItem, AudioItemResult, InnerAudioItem},
},
availability::Availabilities,
date::Date,
error::{MetadataError, RequestError},
image::Images,
request::RequestResult,
restriction::Restrictions,
util::try_from_repeated_message,
video::VideoFiles,
Metadata,
};
use librespot_core::session::Session;
use librespot_core::spotify_id::SpotifyId;
use librespot_protocol as protocol;
pub use protocol::metadata::Episode_EpisodeType as EpisodeType;
#[derive(Debug, Clone)]
pub struct Episode {
pub id: SpotifyId,
pub name: String,
pub duration: i32,
pub audio: AudioFiles,
pub description: String,
pub number: i32,
pub publish_time: Date,
pub covers: Images,
pub language: String,
pub is_explicit: bool,
pub show: SpotifyId,
pub videos: VideoFiles,
pub video_previews: VideoFiles,
pub audio_previews: AudioFiles,
pub restrictions: Restrictions,
pub freeze_frames: Images,
pub keywords: Vec<String>,
pub allow_background_playback: bool,
pub availability: Availabilities,
pub external_url: String,
pub episode_type: EpisodeType,
pub has_music_and_talk: bool,
}
#[derive(Debug, Clone)]
pub struct Episodes(pub Vec<SpotifyId>);
impl Deref for Episodes {
type Target = Vec<SpotifyId>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait]
impl InnerAudioItem for Episode {
async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult {
let episode = Self::get(session, id).await?;
let availability = Self::available_in_country(
&episode.availability,
&episode.restrictions,
&session.country(),
);
Ok(AudioItem {
id,
spotify_uri: id.to_uri(),
files: episode.audio,
name: episode.name,
duration: episode.duration,
availability,
alternatives: None,
})
}
}
#[async_trait]
impl Metadata for Episode {
type Message = protocol::metadata::Episode;
async fn request(session: &Session, episode_id: SpotifyId) -> RequestResult {
session
.spclient()
.get_episode_metadata(episode_id)
.await
.map_err(RequestError::Http)
}
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, MetadataError> {
Self::try_from(msg)
}
}
impl TryFrom<&<Self as Metadata>::Message> for Episode {
type Error = MetadataError;
fn try_from(episode: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self {
id: episode.try_into()?,
name: episode.get_name().to_owned(),
duration: episode.get_duration().to_owned(),
audio: episode.get_audio().into(),
description: episode.get_description().to_owned(),
number: episode.get_number(),
publish_time: episode.get_publish_time().into(),
covers: episode.get_cover_image().get_image().into(),
language: episode.get_language().to_owned(),
is_explicit: episode.get_explicit().to_owned(),
show: episode.get_show().try_into()?,
videos: episode.get_video().into(),
video_previews: episode.get_video_preview().into(),
audio_previews: episode.get_audio_preview().into(),
restrictions: episode.get_restriction().into(),
freeze_frames: episode.get_freeze_frame().get_image().into(),
keywords: episode.get_keyword().to_vec(),
allow_background_playback: episode.get_allow_background_playback(),
availability: episode.get_availability().into(),
external_url: episode.get_external_url().to_owned(),
episode_type: episode.get_field_type(),
has_music_and_talk: episode.get_music_and_talk(),
})
}
}
try_from_repeated_message!(<Episode as Metadata>::Message, Episodes);

34
metadata/src/error.rs Normal file
View file

@ -0,0 +1,34 @@
use std::fmt::Debug;
use thiserror::Error;
use protobuf::ProtobufError;
use librespot_core::mercury::MercuryError;
use librespot_core::spclient::SpClientError;
use librespot_core::spotify_id::SpotifyIdError;
#[derive(Debug, Error)]
pub enum RequestError {
#[error("could not get metadata over HTTP: {0}")]
Http(#[from] SpClientError),
#[error("could not get metadata over Mercury: {0}")]
Mercury(#[from] MercuryError),
#[error("response was empty")]
Empty,
}
#[derive(Debug, Error)]
pub enum MetadataError {
#[error("{0}")]
InvalidSpotifyId(#[from] SpotifyIdError),
#[error("item has invalid date")]
InvalidTimestamp,
#[error("audio item is non-playable")]
NonPlayable,
#[error("could not parse protobuf: {0}")]
Protobuf(#[from] ProtobufError),
#[error("error executing request: {0}")]
Request(#[from] RequestError),
#[error("could not parse repeated fields")]
InvalidRepeated,
}

View file

@ -0,0 +1,35 @@
use std::fmt::Debug;
use std::ops::Deref;
use crate::util::from_repeated_message;
use librespot_protocol as protocol;
use protocol::metadata::ExternalId as ExternalIdMessage;
#[derive(Debug, Clone)]
pub struct ExternalId {
pub external_type: String,
pub id: String,
}
#[derive(Debug, Clone)]
pub struct ExternalIds(pub Vec<ExternalId>);
impl Deref for ExternalIds {
type Target = Vec<ExternalId>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<&ExternalIdMessage> for ExternalId {
fn from(external_id: &ExternalIdMessage) -> Self {
Self {
external_type: external_id.get_field_type().to_owned(),
id: external_id.get_id().to_owned(),
}
}
}
from_repeated_message!(ExternalIdMessage, ExternalIds);

103
metadata/src/image.rs Normal file
View file

@ -0,0 +1,103 @@
use std::convert::{TryFrom, TryInto};
use std::fmt::Debug;
use std::ops::Deref;
use crate::{
error::MetadataError,
util::{from_repeated_message, try_from_repeated_message},
};
use librespot_core::spotify_id::{FileId, SpotifyId};
use librespot_protocol as protocol;
use protocol::metadata::Image as ImageMessage;
use protocol::playlist4_external::PictureSize as PictureSizeMessage;
use protocol::playlist_annotate3::TranscodedPicture as TranscodedPictureMessage;
pub use protocol::metadata::Image_Size as ImageSize;
#[derive(Debug, Clone)]
pub struct Image {
pub id: FileId,
pub size: ImageSize,
pub width: i32,
pub height: i32,
}
#[derive(Debug, Clone)]
pub struct Images(pub Vec<Image>);
impl Deref for Images {
type Target = Vec<Image>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct PictureSize {
pub target_name: String,
pub url: String,
}
#[derive(Debug, Clone)]
pub struct PictureSizes(pub Vec<PictureSize>);
impl Deref for PictureSizes {
type Target = Vec<PictureSize>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct TranscodedPicture {
pub target_name: String,
pub uri: SpotifyId,
}
#[derive(Debug, Clone)]
pub struct TranscodedPictures(pub Vec<TranscodedPicture>);
impl Deref for TranscodedPictures {
type Target = Vec<TranscodedPicture>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<&ImageMessage> for Image {
fn from(image: &ImageMessage) -> Self {
Self {
id: image.into(),
size: image.get_size(),
width: image.get_width(),
height: image.get_height(),
}
}
}
from_repeated_message!(ImageMessage, Images);
impl From<&PictureSizeMessage> for PictureSize {
fn from(size: &PictureSizeMessage) -> Self {
Self {
target_name: size.get_target_name().to_owned(),
url: size.get_url().to_owned(),
}
}
}
from_repeated_message!(PictureSizeMessage, PictureSizes);
impl TryFrom<&TranscodedPictureMessage> for TranscodedPicture {
type Error = MetadataError;
fn try_from(picture: &TranscodedPictureMessage) -> Result<Self, Self::Error> {
Ok(Self {
target_name: picture.get_target_name().to_owned(),
uri: picture.try_into()?,
})
}
}
try_from_repeated_message!(TranscodedPictureMessage, TranscodedPictures);

View file

@ -1,643 +1,51 @@
#![allow(clippy::unused_io_amount)]
#[macro_use] #[macro_use]
extern crate log; extern crate log;
#[macro_use] #[macro_use]
extern crate async_trait; extern crate async_trait;
pub mod cover; use protobuf::Message;
use std::collections::HashMap;
use librespot_core::mercury::MercuryError;
use librespot_core::session::Session; use librespot_core::session::Session;
use librespot_core::spclient::SpClientError; use librespot_core::spotify_id::SpotifyId;
use librespot_core::spotify_id::{FileId, SpotifyAudioType, SpotifyId};
use librespot_protocol as protocol;
use protobuf::{Message, ProtobufError};
use thiserror::Error; pub mod album;
pub mod artist;
pub mod audio;
pub mod availability;
pub mod content_rating;
pub mod copyright;
pub mod date;
pub mod episode;
pub mod error;
pub mod external_id;
pub mod image;
pub mod playlist;
mod request;
pub mod restriction;
pub mod sale_period;
pub mod show;
pub mod track;
mod util;
pub mod video;
pub use crate::protocol::metadata::AudioFile_Format as FileFormat; use error::MetadataError;
use request::RequestResult;
fn countrylist_contains(list: &str, country: &str) -> bool {
list.chunks(2).any(|cc| cc == country)
}
fn parse_restrictions<'s, I>(restrictions: I, country: &str, catalogue: &str) -> bool
where
I: IntoIterator<Item = &'s protocol::metadata::Restriction>,
{
let mut forbidden = "".to_string();
let mut has_forbidden = false;
let mut allowed = "".to_string();
let mut has_allowed = false;
let rs = restrictions
.into_iter()
.filter(|r| r.get_catalogue_str().contains(&catalogue.to_owned()));
for r in rs {
if r.has_countries_forbidden() {
forbidden.push_str(r.get_countries_forbidden());
has_forbidden = true;
}
if r.has_countries_allowed() {
allowed.push_str(r.get_countries_allowed());
has_allowed = true;
}
}
!(has_forbidden && countrylist_contains(forbidden.as_str(), country)
|| has_allowed && !countrylist_contains(allowed.as_str(), country))
}
// A wrapper with fields the player needs
#[derive(Debug, Clone)]
pub struct AudioItem {
pub id: SpotifyId,
pub uri: String,
pub files: HashMap<FileFormat, FileId>,
pub name: String,
pub duration: i32,
pub available: bool,
pub alternatives: Option<Vec<SpotifyId>>,
}
impl AudioItem {
pub async fn get_audio_item(session: &Session, id: SpotifyId) -> Result<Self, MetadataError> {
match id.audio_type {
SpotifyAudioType::Track => Track::get_audio_item(session, id).await,
SpotifyAudioType::Podcast => Episode::get_audio_item(session, id).await,
SpotifyAudioType::NonPlayable => Err(MetadataError::NonPlayable),
}
}
}
pub type AudioItemResult = Result<AudioItem, MetadataError>;
#[async_trait]
trait AudioFiles {
async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult;
}
#[async_trait]
impl AudioFiles for Track {
async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult {
let item = Self::get(session, id).await?;
let alternatives = {
if item.alternatives.is_empty() {
None
} else {
Some(item.alternatives)
}
};
Ok(AudioItem {
id,
uri: format!("spotify:track:{}", id.to_base62()),
files: item.files,
name: item.name,
duration: item.duration,
available: item.available,
alternatives,
})
}
}
#[async_trait]
impl AudioFiles for Episode {
async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult {
let item = Self::get(session, id).await?;
Ok(AudioItem {
id,
uri: format!("spotify:episode:{}", id.to_base62()),
files: item.files,
name: item.name,
duration: item.duration,
available: item.available,
alternatives: None,
})
}
}
#[derive(Debug, Error)]
pub enum MetadataError {
#[error("could not get metadata over HTTP: {0}")]
Http(#[from] SpClientError),
#[error("could not get metadata over Mercury: {0}")]
Mercury(#[from] MercuryError),
#[error("could not parse metadata: {0}")]
Parsing(#[from] ProtobufError),
#[error("response was empty")]
Empty,
#[error("audio item is non-playable")]
NonPlayable,
}
pub type MetadataResult = Result<bytes::Bytes, MetadataError>;
#[async_trait] #[async_trait]
pub trait Metadata: Send + Sized + 'static { pub trait Metadata: Send + Sized + 'static {
type Message: protobuf::Message; type Message: protobuf::Message;
async fn request(session: &Session, id: SpotifyId) -> MetadataResult; // Request a protobuf
fn parse(msg: &Self::Message, session: &Session) -> Self; async fn request(session: &Session, id: SpotifyId) -> RequestResult;
// Request a metadata struct
async fn get(session: &Session, id: SpotifyId) -> Result<Self, MetadataError> { async fn get(session: &Session, id: SpotifyId) -> Result<Self, MetadataError> {
let response = Self::request(session, id).await?; let response = Self::request(session, id).await?;
let msg = Self::Message::parse_from_bytes(&response)?; let msg = Self::Message::parse_from_bytes(&response)?;
Ok(Self::parse(&msg, session)) trace!("Received metadata: {:?}", msg);
} Self::parse(&msg, id)
}
// TODO: expose more fields available in the protobufs
#[derive(Debug, Clone)]
pub struct Track {
pub id: SpotifyId,
pub name: String,
pub duration: i32,
pub album: SpotifyId,
pub artists: Vec<SpotifyId>,
pub files: HashMap<FileFormat, FileId>,
pub alternatives: Vec<SpotifyId>,
pub available: bool,
}
#[derive(Debug, Clone)]
pub struct Album {
pub id: SpotifyId,
pub name: String,
pub artists: Vec<SpotifyId>,
pub tracks: Vec<SpotifyId>,
pub covers: Vec<FileId>,
}
#[derive(Debug, Clone)]
pub struct Episode {
pub id: SpotifyId,
pub name: String,
pub external_url: String,
pub duration: i32,
pub language: String,
pub show: SpotifyId,
pub files: HashMap<FileFormat, FileId>,
pub covers: Vec<FileId>,
pub available: bool,
pub explicit: bool,
}
#[derive(Debug, Clone)]
pub struct Show {
pub id: SpotifyId,
pub name: String,
pub publisher: String,
pub episodes: Vec<SpotifyId>,
pub covers: Vec<FileId>,
}
#[derive(Debug, Clone)]
pub struct TranscodedPicture {
pub target_name: String,
pub uri: String,
}
#[derive(Debug, Clone)]
pub struct PlaylistAnnotation {
pub description: String,
pub picture: String,
pub transcoded_pictures: Vec<TranscodedPicture>,
pub abuse_reporting: bool,
pub taken_down: bool,
}
#[derive(Debug, Clone)]
pub struct Playlist {
pub revision: Vec<u8>,
pub user: String,
pub name: String,
pub tracks: Vec<SpotifyId>,
}
#[derive(Debug, Clone)]
pub struct Artist {
pub id: SpotifyId,
pub name: String,
pub top_tracks: Vec<SpotifyId>,
}
#[async_trait]
impl Metadata for Track {
type Message = protocol::metadata::Track;
async fn request(session: &Session, track_id: SpotifyId) -> MetadataResult {
session
.spclient()
.get_track_metadata(track_id)
.await
.map_err(MetadataError::Http)
}
fn parse(msg: &Self::Message, session: &Session) -> Self {
debug!("MESSAGE: {:?}", msg);
let country = session.country();
let artists = msg
.get_artist()
.iter()
.filter(|artist| artist.has_gid())
.map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap())
.collect::<Vec<_>>();
let files = msg
.get_file()
.iter()
.filter(|file| file.has_file_id())
.map(|file| {
let mut dst = [0u8; 20];
dst.clone_from_slice(file.get_file_id());
(file.get_format(), FileId(dst))
})
.collect();
Self {
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
name: msg.get_name().to_owned(),
duration: msg.get_duration(),
album: SpotifyId::from_raw(msg.get_album().get_gid()).unwrap(),
artists,
files,
alternatives: msg
.get_alternative()
.iter()
.map(|alt| SpotifyId::from_raw(alt.get_gid()).unwrap())
.collect(),
available: parse_restrictions(msg.get_restriction(), &country, "premium"),
}
}
}
#[async_trait]
impl Metadata for Album {
type Message = protocol::metadata::Album;
async fn request(session: &Session, album_id: SpotifyId) -> MetadataResult {
session
.spclient()
.get_album_metadata(album_id)
.await
.map_err(MetadataError::Http)
}
fn parse(msg: &Self::Message, _: &Session) -> Self {
let artists = msg
.get_artist()
.iter()
.filter(|artist| artist.has_gid())
.map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap())
.collect::<Vec<_>>();
let tracks = msg
.get_disc()
.iter()
.flat_map(|disc| disc.get_track())
.filter(|track| track.has_gid())
.map(|track| SpotifyId::from_raw(track.get_gid()).unwrap())
.collect::<Vec<_>>();
let covers = msg
.get_cover_group()
.get_image()
.iter()
.filter(|image| image.has_file_id())
.map(|image| {
let mut dst = [0u8; 20];
dst.clone_from_slice(image.get_file_id());
FileId(dst)
})
.collect::<Vec<_>>();
Self {
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
name: msg.get_name().to_owned(),
artists,
tracks,
covers,
}
}
}
#[async_trait]
impl Metadata for PlaylistAnnotation {
type Message = protocol::playlist_annotate3::PlaylistAnnotation;
async fn request(session: &Session, playlist_id: SpotifyId) -> MetadataResult {
let current_user = session.username();
Self::request_for_user(session, current_user, playlist_id).await
}
fn parse(msg: &Self::Message, _: &Session) -> Self {
let transcoded_pictures = msg
.get_transcoded_picture()
.iter()
.map(|picture| TranscodedPicture {
target_name: picture.get_target_name().to_string(),
uri: picture.get_uri().to_string(),
})
.collect::<Vec<_>>();
let taken_down = !matches!(
msg.get_abuse_report_state(),
protocol::playlist_annotate3::AbuseReportState::OK
);
Self {
description: msg.get_description().to_string(),
picture: msg.get_picture().to_string(),
transcoded_pictures,
abuse_reporting: msg.get_is_abuse_reporting_enabled(),
taken_down,
}
}
}
impl PlaylistAnnotation {
async fn request_for_user(
session: &Session,
username: String,
playlist_id: SpotifyId,
) -> MetadataResult {
let uri = format!(
"hm://playlist-annotate/v1/annotation/user/{}/playlist/{}",
username,
playlist_id.to_base62()
);
let response = session.mercury().get(uri).await?;
match response.payload.first() {
Some(data) => Ok(data.to_vec().into()),
None => Err(MetadataError::Empty),
}
}
#[allow(dead_code)]
async fn get_for_user(
session: &Session,
username: String,
playlist_id: SpotifyId,
) -> Result<Self, MetadataError> {
let response = Self::request_for_user(session, username, playlist_id).await?;
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
Ok(Self::parse(&msg, session))
}
}
#[async_trait]
impl Metadata for Playlist {
type Message = protocol::playlist4_external::SelectedListContent;
async fn request(session: &Session, playlist_id: SpotifyId) -> MetadataResult {
let uri = format!("hm://playlist/v2/playlist/{}", playlist_id.to_base62());
let response = session.mercury().get(uri).await?;
match response.payload.first() {
Some(data) => Ok(data.to_vec().into()),
None => Err(MetadataError::Empty),
}
}
fn parse(msg: &Self::Message, _: &Session) -> Self {
let tracks = msg
.get_contents()
.get_items()
.iter()
.map(|item| {
let uri_split = item.get_uri().split(':');
let uri_parts: Vec<&str> = uri_split.collect();
SpotifyId::from_base62(uri_parts[2]).unwrap()
})
.collect::<Vec<_>>();
if tracks.len() != msg.get_length() as usize {
warn!(
"Got {} tracks, but the playlist should contain {} tracks.",
tracks.len(),
msg.get_length()
);
}
Self {
revision: msg.get_revision().to_vec(),
name: msg.get_attributes().get_name().to_owned(),
tracks,
user: msg.get_owner_username().to_string(),
}
}
}
impl Playlist {
async fn request_for_user(
session: &Session,
username: String,
playlist_id: SpotifyId,
) -> MetadataResult {
let uri = format!(
"hm://playlist/user/{}/playlist/{}",
username,
playlist_id.to_base62()
);
let response = session.mercury().get(uri).await?;
match response.payload.first() {
Some(data) => Ok(data.to_vec().into()),
None => Err(MetadataError::Empty),
}
}
async fn request_root_for_user(session: &Session, username: String) -> MetadataResult {
let uri = format!("hm://playlist/user/{}/rootlist", username);
let response = session.mercury().get(uri).await?;
match response.payload.first() {
Some(data) => Ok(data.to_vec().into()),
None => Err(MetadataError::Empty),
}
}
#[allow(dead_code)]
async fn get_for_user(
session: &Session,
username: String,
playlist_id: SpotifyId,
) -> Result<Self, MetadataError> {
let response = Self::request_for_user(session, username, playlist_id).await?;
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
Ok(Self::parse(&msg, session))
}
#[allow(dead_code)]
async fn get_root_for_user(session: &Session, username: String) -> Result<Self, MetadataError> {
let response = Self::request_root_for_user(session, username).await?;
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
Ok(Self::parse(&msg, session))
}
}
#[async_trait]
impl Metadata for Artist {
type Message = protocol::metadata::Artist;
async fn request(session: &Session, artist_id: SpotifyId) -> MetadataResult {
session
.spclient()
.get_artist_metadata(artist_id)
.await
.map_err(MetadataError::Http)
}
fn parse(msg: &Self::Message, session: &Session) -> Self {
let country = session.country();
let top_tracks: Vec<SpotifyId> = match msg
.get_top_track()
.iter()
.find(|tt| !tt.has_country() || countrylist_contains(tt.get_country(), &country))
{
Some(tracks) => tracks
.get_track()
.iter()
.filter(|track| track.has_gid())
.map(|track| SpotifyId::from_raw(track.get_gid()).unwrap())
.collect::<Vec<_>>(),
None => Vec::new(),
};
Self {
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
name: msg.get_name().to_owned(),
top_tracks,
}
}
}
// Podcast
#[async_trait]
impl Metadata for Episode {
type Message = protocol::metadata::Episode;
async fn request(session: &Session, episode_id: SpotifyId) -> MetadataResult {
session
.spclient()
.get_album_metadata(episode_id)
.await
.map_err(MetadataError::Http)
}
fn parse(msg: &Self::Message, session: &Session) -> Self {
let country = session.country();
let files = msg
.get_audio()
.iter()
.filter(|file| file.has_file_id())
.map(|file| {
let mut dst = [0u8; 20];
dst.clone_from_slice(file.get_file_id());
(file.get_format(), FileId(dst))
})
.collect();
let covers = msg
.get_cover_image()
.get_image()
.iter()
.filter(|image| image.has_file_id())
.map(|image| {
let mut dst = [0u8; 20];
dst.clone_from_slice(image.get_file_id());
FileId(dst)
})
.collect::<Vec<_>>();
Self {
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
name: msg.get_name().to_owned(),
external_url: msg.get_external_url().to_owned(),
duration: msg.get_duration().to_owned(),
language: msg.get_language().to_owned(),
show: SpotifyId::from_raw(msg.get_show().get_gid()).unwrap(),
covers,
files,
available: parse_restrictions(msg.get_restriction(), &country, "premium"),
explicit: msg.get_explicit().to_owned(),
}
}
}
#[async_trait]
impl Metadata for Show {
type Message = protocol::metadata::Show;
async fn request(session: &Session, show_id: SpotifyId) -> MetadataResult {
session
.spclient()
.get_show_metadata(show_id)
.await
.map_err(MetadataError::Http)
}
fn parse(msg: &Self::Message, _: &Session) -> Self {
let episodes = msg
.get_episode()
.iter()
.filter(|episode| episode.has_gid())
.map(|episode| SpotifyId::from_raw(episode.get_gid()).unwrap())
.collect::<Vec<_>>();
let covers = msg
.get_cover_image()
.get_image()
.iter()
.filter(|image| image.has_file_id())
.map(|image| {
let mut dst = [0u8; 20];
dst.clone_from_slice(image.get_file_id());
FileId(dst)
})
.collect::<Vec<_>>();
Self {
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
name: msg.get_name().to_owned(),
publisher: msg.get_publisher().to_owned(),
episodes,
covers,
}
}
}
struct StrChunks<'s>(&'s str, usize);
trait StrChunksExt {
fn chunks(&self, size: usize) -> StrChunks;
}
impl StrChunksExt for str {
fn chunks(&self, size: usize) -> StrChunks {
StrChunks(self, size)
}
}
impl<'s> Iterator for StrChunks<'s> {
type Item = &'s str;
fn next(&mut self) -> Option<&'s str> {
let &mut StrChunks(data, size) = self;
if data.is_empty() {
None
} else {
let ret = Some(&data[..size]);
self.0 = &data[size..];
ret
}
} }
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, MetadataError>;
} }

View file

@ -0,0 +1,89 @@
use std::convert::{TryFrom, TryInto};
use std::fmt::Debug;
use protobuf::Message;
use crate::{
error::MetadataError,
image::TranscodedPictures,
request::{MercuryRequest, RequestResult},
Metadata,
};
use librespot_core::session::Session;
use librespot_core::spotify_id::SpotifyId;
use librespot_protocol as protocol;
pub use protocol::playlist_annotate3::AbuseReportState;
#[derive(Debug, Clone)]
pub struct PlaylistAnnotation {
pub description: String,
pub picture: String,
pub transcoded_pictures: TranscodedPictures,
pub has_abuse_reporting: bool,
pub abuse_report_state: AbuseReportState,
}
#[async_trait]
impl Metadata for PlaylistAnnotation {
type Message = protocol::playlist_annotate3::PlaylistAnnotation;
async fn request(session: &Session, playlist_id: SpotifyId) -> RequestResult {
let current_user = session.username();
Self::request_for_user(session, &current_user, playlist_id).await
}
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, MetadataError> {
Ok(Self {
description: msg.get_description().to_owned(),
picture: msg.get_picture().to_owned(), // TODO: is this a URL or Spotify URI?
transcoded_pictures: msg.get_transcoded_picture().try_into()?,
has_abuse_reporting: msg.get_is_abuse_reporting_enabled(),
abuse_report_state: msg.get_abuse_report_state(),
})
}
}
impl PlaylistAnnotation {
async fn request_for_user(
session: &Session,
username: &str,
playlist_id: SpotifyId,
) -> RequestResult {
let uri = format!(
"hm://playlist-annotate/v1/annotation/user/{}/playlist/{}",
username,
playlist_id.to_base62()
);
<Self as MercuryRequest>::request(session, &uri).await
}
#[allow(dead_code)]
async fn get_for_user(
session: &Session,
username: &str,
playlist_id: SpotifyId,
) -> Result<Self, MetadataError> {
let response = Self::request_for_user(session, username, playlist_id).await?;
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
Self::parse(&msg, playlist_id)
}
}
impl MercuryRequest for PlaylistAnnotation {}
impl TryFrom<&<PlaylistAnnotation as Metadata>::Message> for PlaylistAnnotation {
type Error = MetadataError;
fn try_from(
annotation: &<PlaylistAnnotation as Metadata>::Message,
) -> Result<Self, Self::Error> {
Ok(Self {
description: annotation.get_description().to_owned(),
picture: annotation.get_picture().to_owned(),
transcoded_pictures: annotation.get_transcoded_picture().try_into()?,
has_abuse_reporting: annotation.get_is_abuse_reporting_enabled(),
abuse_report_state: annotation.get_abuse_report_state(),
})
}
}

View file

@ -0,0 +1,195 @@
use std::collections::HashMap;
use std::convert::{TryFrom, TryInto};
use std::fmt::Debug;
use std::ops::Deref;
use crate::{date::Date, error::MetadataError, image::PictureSizes, util::from_repeated_enum};
use librespot_core::spotify_id::SpotifyId;
use librespot_protocol as protocol;
use protocol::playlist4_external::FormatListAttribute as PlaylistFormatAttributeMessage;
use protocol::playlist4_external::ItemAttributes as PlaylistItemAttributesMessage;
use protocol::playlist4_external::ItemAttributesPartialState as PlaylistPartialItemAttributesMessage;
use protocol::playlist4_external::ListAttributes as PlaylistAttributesMessage;
use protocol::playlist4_external::ListAttributesPartialState as PlaylistPartialAttributesMessage;
use protocol::playlist4_external::UpdateItemAttributes as PlaylistUpdateItemAttributesMessage;
use protocol::playlist4_external::UpdateListAttributes as PlaylistUpdateAttributesMessage;
pub use protocol::playlist4_external::ItemAttributeKind as PlaylistItemAttributeKind;
pub use protocol::playlist4_external::ListAttributeKind as PlaylistAttributeKind;
#[derive(Debug, Clone)]
pub struct PlaylistAttributes {
pub name: String,
pub description: String,
pub picture: SpotifyId,
pub is_collaborative: bool,
pub pl3_version: String,
pub is_deleted_by_owner: bool,
pub client_id: String,
pub format: String,
pub format_attributes: PlaylistFormatAttribute,
pub picture_sizes: PictureSizes,
}
#[derive(Debug, Clone)]
pub struct PlaylistAttributeKinds(pub Vec<PlaylistAttributeKind>);
impl Deref for PlaylistAttributeKinds {
type Target = Vec<PlaylistAttributeKind>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
from_repeated_enum!(PlaylistAttributeKind, PlaylistAttributeKinds);
#[derive(Debug, Clone)]
pub struct PlaylistFormatAttribute(pub HashMap<String, String>);
impl Deref for PlaylistFormatAttribute {
type Target = HashMap<String, String>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct PlaylistItemAttributes {
pub added_by: String,
pub timestamp: Date,
pub seen_at: Date,
pub is_public: bool,
pub format_attributes: PlaylistFormatAttribute,
pub item_id: SpotifyId,
}
#[derive(Debug, Clone)]
pub struct PlaylistItemAttributeKinds(pub Vec<PlaylistItemAttributeKind>);
impl Deref for PlaylistItemAttributeKinds {
type Target = Vec<PlaylistItemAttributeKind>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
from_repeated_enum!(PlaylistItemAttributeKind, PlaylistItemAttributeKinds);
#[derive(Debug, Clone)]
pub struct PlaylistPartialAttributes {
#[allow(dead_code)]
values: PlaylistAttributes,
#[allow(dead_code)]
no_value: PlaylistAttributeKinds,
}
#[derive(Debug, Clone)]
pub struct PlaylistPartialItemAttributes {
#[allow(dead_code)]
values: PlaylistItemAttributes,
#[allow(dead_code)]
no_value: PlaylistItemAttributeKinds,
}
#[derive(Debug, Clone)]
pub struct PlaylistUpdateAttributes {
pub new_attributes: PlaylistPartialAttributes,
pub old_attributes: PlaylistPartialAttributes,
}
#[derive(Debug, Clone)]
pub struct PlaylistUpdateItemAttributes {
pub index: i32,
pub new_attributes: PlaylistPartialItemAttributes,
pub old_attributes: PlaylistPartialItemAttributes,
}
impl TryFrom<&PlaylistAttributesMessage> for PlaylistAttributes {
type Error = MetadataError;
fn try_from(attributes: &PlaylistAttributesMessage) -> Result<Self, Self::Error> {
Ok(Self {
name: attributes.get_name().to_owned(),
description: attributes.get_description().to_owned(),
picture: attributes.get_picture().try_into()?,
is_collaborative: attributes.get_collaborative(),
pl3_version: attributes.get_pl3_version().to_owned(),
is_deleted_by_owner: attributes.get_deleted_by_owner(),
client_id: attributes.get_client_id().to_owned(),
format: attributes.get_format().to_owned(),
format_attributes: attributes.get_format_attributes().into(),
picture_sizes: attributes.get_picture_size().into(),
})
}
}
impl From<&[PlaylistFormatAttributeMessage]> for PlaylistFormatAttribute {
fn from(attributes: &[PlaylistFormatAttributeMessage]) -> Self {
let format_attributes = attributes
.iter()
.map(|attribute| {
(
attribute.get_key().to_owned(),
attribute.get_value().to_owned(),
)
})
.collect();
PlaylistFormatAttribute(format_attributes)
}
}
impl TryFrom<&PlaylistItemAttributesMessage> for PlaylistItemAttributes {
type Error = MetadataError;
fn try_from(attributes: &PlaylistItemAttributesMessage) -> Result<Self, Self::Error> {
Ok(Self {
added_by: attributes.get_added_by().to_owned(),
timestamp: attributes.get_timestamp().try_into()?,
seen_at: attributes.get_seen_at().try_into()?,
is_public: attributes.get_public(),
format_attributes: attributes.get_format_attributes().into(),
item_id: attributes.get_item_id().try_into()?,
})
}
}
impl TryFrom<&PlaylistPartialAttributesMessage> for PlaylistPartialAttributes {
type Error = MetadataError;
fn try_from(attributes: &PlaylistPartialAttributesMessage) -> Result<Self, Self::Error> {
Ok(Self {
values: attributes.get_values().try_into()?,
no_value: attributes.get_no_value().into(),
})
}
}
impl TryFrom<&PlaylistPartialItemAttributesMessage> for PlaylistPartialItemAttributes {
type Error = MetadataError;
fn try_from(attributes: &PlaylistPartialItemAttributesMessage) -> Result<Self, Self::Error> {
Ok(Self {
values: attributes.get_values().try_into()?,
no_value: attributes.get_no_value().into(),
})
}
}
impl TryFrom<&PlaylistUpdateAttributesMessage> for PlaylistUpdateAttributes {
type Error = MetadataError;
fn try_from(update: &PlaylistUpdateAttributesMessage) -> Result<Self, Self::Error> {
Ok(Self {
new_attributes: update.get_new_attributes().try_into()?,
old_attributes: update.get_old_attributes().try_into()?,
})
}
}
impl TryFrom<&PlaylistUpdateItemAttributesMessage> for PlaylistUpdateItemAttributes {
type Error = MetadataError;
fn try_from(update: &PlaylistUpdateItemAttributesMessage) -> Result<Self, Self::Error> {
Ok(Self {
index: update.get_index(),
new_attributes: update.get_new_attributes().try_into()?,
old_attributes: update.get_old_attributes().try_into()?,
})
}
}

View file

@ -0,0 +1,29 @@
use std::convert::{TryFrom, TryInto};
use std::fmt::Debug;
use crate::error::MetadataError;
use super::operation::PlaylistOperations;
use librespot_core::spotify_id::SpotifyId;
use librespot_protocol as protocol;
use protocol::playlist4_external::Diff as DiffMessage;
#[derive(Debug, Clone)]
pub struct PlaylistDiff {
pub from_revision: SpotifyId,
pub operations: PlaylistOperations,
pub to_revision: SpotifyId,
}
impl TryFrom<&DiffMessage> for PlaylistDiff {
type Error = MetadataError;
fn try_from(diff: &DiffMessage) -> Result<Self, Self::Error> {
Ok(Self {
from_revision: diff.get_from_revision().try_into()?,
operations: diff.get_ops().try_into()?,
to_revision: diff.get_to_revision().try_into()?,
})
}
}

View file

@ -0,0 +1,96 @@
use std::convert::{TryFrom, TryInto};
use std::fmt::Debug;
use std::ops::Deref;
use crate::{date::Date, error::MetadataError, util::try_from_repeated_message};
use super::attribute::{PlaylistAttributes, PlaylistItemAttributes};
use librespot_core::spotify_id::SpotifyId;
use librespot_protocol as protocol;
use protocol::playlist4_external::Item as PlaylistItemMessage;
use protocol::playlist4_external::ListItems as PlaylistItemsMessage;
use protocol::playlist4_external::MetaItem as PlaylistMetaItemMessage;
#[derive(Debug, Clone)]
pub struct PlaylistItem {
pub id: SpotifyId,
pub attributes: PlaylistItemAttributes,
}
#[derive(Debug, Clone)]
pub struct PlaylistItems(pub Vec<PlaylistItem>);
impl Deref for PlaylistItems {
type Target = Vec<PlaylistItem>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct PlaylistItemList {
pub position: i32,
pub is_truncated: bool,
pub items: PlaylistItems,
pub meta_items: PlaylistMetaItems,
}
#[derive(Debug, Clone)]
pub struct PlaylistMetaItem {
pub revision: SpotifyId,
pub attributes: PlaylistAttributes,
pub length: i32,
pub timestamp: Date,
pub owner_username: String,
}
#[derive(Debug, Clone)]
pub struct PlaylistMetaItems(pub Vec<PlaylistMetaItem>);
impl Deref for PlaylistMetaItems {
type Target = Vec<PlaylistMetaItem>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl TryFrom<&PlaylistItemMessage> for PlaylistItem {
type Error = MetadataError;
fn try_from(item: &PlaylistItemMessage) -> Result<Self, Self::Error> {
Ok(Self {
id: item.try_into()?,
attributes: item.get_attributes().try_into()?,
})
}
}
try_from_repeated_message!(PlaylistItemMessage, PlaylistItems);
impl TryFrom<&PlaylistItemsMessage> for PlaylistItemList {
type Error = MetadataError;
fn try_from(list_items: &PlaylistItemsMessage) -> Result<Self, Self::Error> {
Ok(Self {
position: list_items.get_pos(),
is_truncated: list_items.get_truncated(),
items: list_items.get_items().try_into()?,
meta_items: list_items.get_meta_items().try_into()?,
})
}
}
impl TryFrom<&PlaylistMetaItemMessage> for PlaylistMetaItem {
type Error = MetadataError;
fn try_from(item: &PlaylistMetaItemMessage) -> Result<Self, Self::Error> {
Ok(Self {
revision: item.try_into()?,
attributes: item.get_attributes().try_into()?,
length: item.get_length(),
timestamp: item.get_timestamp().try_into()?,
owner_username: item.get_owner_username().to_owned(),
})
}
}
try_from_repeated_message!(PlaylistMetaItemMessage, PlaylistMetaItems);

View file

@ -0,0 +1,201 @@
use std::convert::{TryFrom, TryInto};
use std::fmt::Debug;
use std::ops::Deref;
use protobuf::Message;
use crate::{
date::Date,
error::MetadataError,
request::{MercuryRequest, RequestResult},
util::try_from_repeated_message,
Metadata,
};
use super::{attribute::PlaylistAttributes, diff::PlaylistDiff, item::PlaylistItemList};
use librespot_core::session::Session;
use librespot_core::spotify_id::{NamedSpotifyId, SpotifyId};
use librespot_protocol as protocol;
#[derive(Debug, Clone)]
pub struct Playlist {
pub id: NamedSpotifyId,
pub revision: SpotifyId,
pub length: i32,
pub attributes: PlaylistAttributes,
pub contents: PlaylistItemList,
pub diff: PlaylistDiff,
pub sync_result: PlaylistDiff,
pub resulting_revisions: Playlists,
pub has_multiple_heads: bool,
pub is_up_to_date: bool,
pub nonces: Vec<i64>,
pub timestamp: Date,
pub has_abuse_reporting: bool,
}
#[derive(Debug, Clone)]
pub struct Playlists(pub Vec<SpotifyId>);
impl Deref for Playlists {
type Target = Vec<SpotifyId>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct RootPlaylist(pub SelectedListContent);
impl Deref for RootPlaylist {
type Target = SelectedListContent;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct SelectedListContent {
pub revision: SpotifyId,
pub length: i32,
pub attributes: PlaylistAttributes,
pub contents: PlaylistItemList,
pub diff: PlaylistDiff,
pub sync_result: PlaylistDiff,
pub resulting_revisions: Playlists,
pub has_multiple_heads: bool,
pub is_up_to_date: bool,
pub nonces: Vec<i64>,
pub timestamp: Date,
pub owner_username: String,
pub has_abuse_reporting: bool,
}
impl Playlist {
#[allow(dead_code)]
async fn request_for_user(
session: &Session,
username: &str,
playlist_id: SpotifyId,
) -> RequestResult {
let uri = format!(
"hm://playlist/user/{}/playlist/{}",
username,
playlist_id.to_base62()
);
<Self as MercuryRequest>::request(session, &uri).await
}
#[allow(dead_code)]
pub async fn get_for_user(
session: &Session,
username: &str,
playlist_id: SpotifyId,
) -> Result<Self, MetadataError> {
let response = Self::request_for_user(session, username, playlist_id).await?;
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
Self::parse(&msg, playlist_id)
}
pub fn tracks(&self) -> Vec<SpotifyId> {
let tracks = self
.contents
.items
.iter()
.map(|item| item.id)
.collect::<Vec<_>>();
let length = tracks.len();
let expected_length = self.length as usize;
if length != expected_length {
warn!(
"Got {} tracks, but the list should contain {} tracks.",
length, expected_length,
);
}
tracks
}
pub fn name(&self) -> &str {
&self.attributes.name
}
}
impl MercuryRequest for Playlist {}
#[async_trait]
impl Metadata for Playlist {
type Message = protocol::playlist4_external::SelectedListContent;
async fn request(session: &Session, playlist_id: SpotifyId) -> RequestResult {
let uri = format!("hm://playlist/v2/playlist/{}", playlist_id.to_base62());
<Self as MercuryRequest>::request(session, &uri).await
}
fn parse(msg: &Self::Message, id: SpotifyId) -> Result<Self, MetadataError> {
// the playlist proto doesn't contain the id so we decorate it
let playlist = SelectedListContent::try_from(msg)?;
let id = NamedSpotifyId::from_spotify_id(id, playlist.owner_username);
Ok(Self {
id,
revision: playlist.revision,
length: playlist.length,
attributes: playlist.attributes,
contents: playlist.contents,
diff: playlist.diff,
sync_result: playlist.sync_result,
resulting_revisions: playlist.resulting_revisions,
has_multiple_heads: playlist.has_multiple_heads,
is_up_to_date: playlist.is_up_to_date,
nonces: playlist.nonces,
timestamp: playlist.timestamp,
has_abuse_reporting: playlist.has_abuse_reporting,
})
}
}
impl MercuryRequest for RootPlaylist {}
impl RootPlaylist {
#[allow(dead_code)]
async fn request_for_user(session: &Session, username: &str) -> RequestResult {
let uri = format!("hm://playlist/user/{}/rootlist", username,);
<Self as MercuryRequest>::request(session, &uri).await
}
#[allow(dead_code)]
pub async fn get_root_for_user(
session: &Session,
username: &str,
) -> Result<Self, MetadataError> {
let response = Self::request_for_user(session, username).await?;
let msg = protocol::playlist4_external::SelectedListContent::parse_from_bytes(&response)?;
Ok(Self(SelectedListContent::try_from(&msg)?))
}
}
impl TryFrom<&<Playlist as Metadata>::Message> for SelectedListContent {
type Error = MetadataError;
fn try_from(playlist: &<Playlist as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self {
revision: playlist.get_revision().try_into()?,
length: playlist.get_length(),
attributes: playlist.get_attributes().try_into()?,
contents: playlist.get_contents().try_into()?,
diff: playlist.get_diff().try_into()?,
sync_result: playlist.get_sync_result().try_into()?,
resulting_revisions: playlist.get_resulting_revisions().try_into()?,
has_multiple_heads: playlist.get_multiple_heads(),
is_up_to_date: playlist.get_up_to_date(),
nonces: playlist.get_nonces().into(),
timestamp: playlist.get_timestamp().try_into()?,
owner_username: playlist.get_owner_username().to_owned(),
has_abuse_reporting: playlist.get_abuse_reporting_enabled(),
})
}
}
try_from_repeated_message!(Vec<u8>, Playlists);

View file

@ -0,0 +1,9 @@
pub mod annotation;
pub mod attribute;
pub mod diff;
pub mod item;
pub mod list;
pub mod operation;
pub use annotation::PlaylistAnnotation;
pub use list::Playlist;

View file

@ -0,0 +1,114 @@
use std::convert::{TryFrom, TryInto};
use std::fmt::Debug;
use std::ops::Deref;
use crate::{
error::MetadataError,
playlist::{
attribute::{PlaylistUpdateAttributes, PlaylistUpdateItemAttributes},
item::PlaylistItems,
},
util::try_from_repeated_message,
};
use librespot_protocol as protocol;
use protocol::playlist4_external::Add as PlaylistAddMessage;
use protocol::playlist4_external::Mov as PlaylistMoveMessage;
use protocol::playlist4_external::Op as PlaylistOperationMessage;
use protocol::playlist4_external::Rem as PlaylistRemoveMessage;
pub use protocol::playlist4_external::Op_Kind as PlaylistOperationKind;
#[derive(Debug, Clone)]
pub struct PlaylistOperation {
pub kind: PlaylistOperationKind,
pub add: PlaylistOperationAdd,
pub rem: PlaylistOperationRemove,
pub mov: PlaylistOperationMove,
pub update_item_attributes: PlaylistUpdateItemAttributes,
pub update_list_attributes: PlaylistUpdateAttributes,
}
#[derive(Debug, Clone)]
pub struct PlaylistOperations(pub Vec<PlaylistOperation>);
impl Deref for PlaylistOperations {
type Target = Vec<PlaylistOperation>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct PlaylistOperationAdd {
pub from_index: i32,
pub items: PlaylistItems,
pub add_last: bool,
pub add_first: bool,
}
#[derive(Debug, Clone)]
pub struct PlaylistOperationMove {
pub from_index: i32,
pub length: i32,
pub to_index: i32,
}
#[derive(Debug, Clone)]
pub struct PlaylistOperationRemove {
pub from_index: i32,
pub length: i32,
pub items: PlaylistItems,
pub has_items_as_key: bool,
}
impl TryFrom<&PlaylistOperationMessage> for PlaylistOperation {
type Error = MetadataError;
fn try_from(operation: &PlaylistOperationMessage) -> Result<Self, Self::Error> {
Ok(Self {
kind: operation.get_kind(),
add: operation.get_add().try_into()?,
rem: operation.get_rem().try_into()?,
mov: operation.get_mov().into(),
update_item_attributes: operation.get_update_item_attributes().try_into()?,
update_list_attributes: operation.get_update_list_attributes().try_into()?,
})
}
}
try_from_repeated_message!(PlaylistOperationMessage, PlaylistOperations);
impl TryFrom<&PlaylistAddMessage> for PlaylistOperationAdd {
type Error = MetadataError;
fn try_from(add: &PlaylistAddMessage) -> Result<Self, Self::Error> {
Ok(Self {
from_index: add.get_from_index(),
items: add.get_items().try_into()?,
add_last: add.get_add_last(),
add_first: add.get_add_first(),
})
}
}
impl From<&PlaylistMoveMessage> for PlaylistOperationMove {
fn from(mov: &PlaylistMoveMessage) -> Self {
Self {
from_index: mov.get_from_index(),
length: mov.get_length(),
to_index: mov.get_to_index(),
}
}
}
impl TryFrom<&PlaylistRemoveMessage> for PlaylistOperationRemove {
type Error = MetadataError;
fn try_from(remove: &PlaylistRemoveMessage) -> Result<Self, Self::Error> {
Ok(Self {
from_index: remove.get_from_index(),
length: remove.get_length(),
items: remove.get_items().try_into()?,
has_items_as_key: remove.get_items_as_key(),
})
}
}

20
metadata/src/request.rs Normal file
View file

@ -0,0 +1,20 @@
use crate::error::RequestError;
use librespot_core::session::Session;
pub type RequestResult = Result<bytes::Bytes, RequestError>;
#[async_trait]
pub trait MercuryRequest {
async fn request(session: &Session, uri: &str) -> RequestResult {
let response = session.mercury().get(uri).await?;
match response.payload.first() {
Some(data) => {
let data = data.to_vec().into();
trace!("Received metadata: {:?}", data);
Ok(data)
}
None => Err(RequestError::Empty),
}
}
}

106
metadata/src/restriction.rs Normal file
View file

@ -0,0 +1,106 @@
use std::fmt::Debug;
use std::ops::Deref;
use crate::util::{from_repeated_enum, from_repeated_message};
use librespot_protocol as protocol;
use protocol::metadata::Restriction as RestrictionMessage;
pub use protocol::metadata::Restriction_Catalogue as RestrictionCatalogue;
pub use protocol::metadata::Restriction_Type as RestrictionType;
#[derive(Debug, Clone)]
pub struct Restriction {
pub catalogues: RestrictionCatalogues,
pub restriction_type: RestrictionType,
pub catalogue_strs: Vec<String>,
pub countries_allowed: Option<Vec<String>>,
pub countries_forbidden: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct Restrictions(pub Vec<Restriction>);
impl Deref for Restrictions {
type Target = Vec<Restriction>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct RestrictionCatalogues(pub Vec<RestrictionCatalogue>);
impl Deref for RestrictionCatalogues {
type Target = Vec<RestrictionCatalogue>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Restriction {
fn parse_country_codes(country_codes: &str) -> Vec<String> {
country_codes
.chunks(2)
.map(|country_code| country_code.to_owned())
.collect()
}
}
impl From<&RestrictionMessage> for Restriction {
fn from(restriction: &RestrictionMessage) -> Self {
let countries_allowed = if restriction.has_countries_allowed() {
Some(Self::parse_country_codes(
restriction.get_countries_allowed(),
))
} else {
None
};
let countries_forbidden = if restriction.has_countries_forbidden() {
Some(Self::parse_country_codes(
restriction.get_countries_forbidden(),
))
} else {
None
};
Self {
catalogues: restriction.get_catalogue().into(),
restriction_type: restriction.get_field_type(),
catalogue_strs: restriction.get_catalogue_str().to_vec(),
countries_allowed,
countries_forbidden,
}
}
}
from_repeated_message!(RestrictionMessage, Restrictions);
from_repeated_enum!(RestrictionCatalogue, RestrictionCatalogues);
struct StrChunks<'s>(&'s str, usize);
trait StrChunksExt {
fn chunks(&self, size: usize) -> StrChunks;
}
impl StrChunksExt for str {
fn chunks(&self, size: usize) -> StrChunks {
StrChunks(self, size)
}
}
impl<'s> Iterator for StrChunks<'s> {
type Item = &'s str;
fn next(&mut self) -> Option<&'s str> {
let &mut StrChunks(data, size) = self;
if data.is_empty() {
None
} else {
let ret = Some(&data[..size]);
self.0 = &data[size..];
ret
}
}
}

View file

@ -0,0 +1,37 @@
use std::fmt::Debug;
use std::ops::Deref;
use crate::{date::Date, restriction::Restrictions, util::from_repeated_message};
use librespot_protocol as protocol;
use protocol::metadata::SalePeriod as SalePeriodMessage;
#[derive(Debug, Clone)]
pub struct SalePeriod {
pub restrictions: Restrictions,
pub start: Date,
pub end: Date,
}
#[derive(Debug, Clone)]
pub struct SalePeriods(pub Vec<SalePeriod>);
impl Deref for SalePeriods {
type Target = Vec<SalePeriod>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<&SalePeriodMessage> for SalePeriod {
fn from(sale_period: &SalePeriodMessage) -> Self {
Self {
restrictions: sale_period.get_restriction().into(),
start: sale_period.get_start().into(),
end: sale_period.get_end().into(),
}
}
}
from_repeated_message!(SalePeriodMessage, SalePeriods);

75
metadata/src/show.rs Normal file
View file

@ -0,0 +1,75 @@
use std::convert::{TryFrom, TryInto};
use std::fmt::Debug;
use crate::{
availability::Availabilities, copyright::Copyrights, episode::Episodes, error::RequestError,
image::Images, restriction::Restrictions, Metadata, MetadataError, RequestResult,
};
use librespot_core::session::Session;
use librespot_core::spotify_id::SpotifyId;
use librespot_protocol as protocol;
pub use protocol::metadata::Show_ConsumptionOrder as ShowConsumptionOrder;
pub use protocol::metadata::Show_MediaType as ShowMediaType;
#[derive(Debug, Clone)]
pub struct Show {
pub id: SpotifyId,
pub name: String,
pub description: String,
pub publisher: String,
pub language: String,
pub is_explicit: bool,
pub covers: Images,
pub episodes: Episodes,
pub copyrights: Copyrights,
pub restrictions: Restrictions,
pub keywords: Vec<String>,
pub media_type: ShowMediaType,
pub consumption_order: ShowConsumptionOrder,
pub availability: Availabilities,
pub trailer_uri: SpotifyId,
pub has_music_and_talk: bool,
}
#[async_trait]
impl Metadata for Show {
type Message = protocol::metadata::Show;
async fn request(session: &Session, show_id: SpotifyId) -> RequestResult {
session
.spclient()
.get_show_metadata(show_id)
.await
.map_err(RequestError::Http)
}
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, MetadataError> {
Self::try_from(msg)
}
}
impl TryFrom<&<Self as Metadata>::Message> for Show {
type Error = MetadataError;
fn try_from(show: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self {
id: show.try_into()?,
name: show.get_name().to_owned(),
description: show.get_description().to_owned(),
publisher: show.get_publisher().to_owned(),
language: show.get_language().to_owned(),
is_explicit: show.get_explicit(),
covers: show.get_cover_image().get_image().into(),
episodes: show.get_episode().try_into()?,
copyrights: show.get_copyright().into(),
restrictions: show.get_restriction().into(),
keywords: show.get_keyword().to_vec(),
media_type: show.get_media_type(),
consumption_order: show.get_consumption_order(),
availability: show.get_availability().into(),
trailer_uri: SpotifyId::from_uri(show.get_trailer_uri())?,
has_music_and_talk: show.get_music_and_talk(),
})
}
}

150
metadata/src/track.rs Normal file
View file

@ -0,0 +1,150 @@
use std::convert::{TryFrom, TryInto};
use std::fmt::Debug;
use std::ops::Deref;
use chrono::Local;
use uuid::Uuid;
use crate::{
artist::{Artists, ArtistsWithRole},
audio::{
file::AudioFiles,
item::{AudioItem, AudioItemResult, InnerAudioItem},
},
availability::{Availabilities, UnavailabilityReason},
content_rating::ContentRatings,
date::Date,
error::RequestError,
external_id::ExternalIds,
restriction::Restrictions,
sale_period::SalePeriods,
util::try_from_repeated_message,
Metadata, MetadataError, RequestResult,
};
use librespot_core::session::Session;
use librespot_core::spotify_id::SpotifyId;
use librespot_protocol as protocol;
#[derive(Debug, Clone)]
pub struct Track {
pub id: SpotifyId,
pub name: String,
pub album: SpotifyId,
pub artists: Artists,
pub number: i32,
pub disc_number: i32,
pub duration: i32,
pub popularity: i32,
pub is_explicit: bool,
pub external_ids: ExternalIds,
pub restrictions: Restrictions,
pub files: AudioFiles,
pub alternatives: Tracks,
pub sale_periods: SalePeriods,
pub previews: AudioFiles,
pub tags: Vec<String>,
pub earliest_live_timestamp: Date,
pub has_lyrics: bool,
pub availability: Availabilities,
pub licensor: Uuid,
pub language_of_performance: Vec<String>,
pub content_ratings: ContentRatings,
pub original_title: String,
pub version_title: String,
pub artists_with_role: ArtistsWithRole,
}
#[derive(Debug, Clone)]
pub struct Tracks(pub Vec<SpotifyId>);
impl Deref for Tracks {
type Target = Vec<SpotifyId>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait]
impl InnerAudioItem for Track {
async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult {
let track = Self::get(session, id).await?;
let alternatives = {
if track.alternatives.is_empty() {
None
} else {
Some(track.alternatives.clone())
}
};
// TODO: check meaning of earliest_live_timestamp in
let availability = if Local::now() < track.earliest_live_timestamp.as_utc() {
Err(UnavailabilityReason::Embargo)
} else {
Self::available_in_country(&track.availability, &track.restrictions, &session.country())
};
Ok(AudioItem {
id,
spotify_uri: id.to_uri(),
files: track.files,
name: track.name,
duration: track.duration,
availability,
alternatives,
})
}
}
#[async_trait]
impl Metadata for Track {
type Message = protocol::metadata::Track;
async fn request(session: &Session, track_id: SpotifyId) -> RequestResult {
session
.spclient()
.get_track_metadata(track_id)
.await
.map_err(RequestError::Http)
}
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, MetadataError> {
Self::try_from(msg)
}
}
impl TryFrom<&<Self as Metadata>::Message> for Track {
type Error = MetadataError;
fn try_from(track: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
Ok(Self {
id: track.try_into()?,
name: track.get_name().to_owned(),
album: track.get_album().try_into()?,
artists: track.get_artist().try_into()?,
number: track.get_number(),
disc_number: track.get_disc_number(),
duration: track.get_duration(),
popularity: track.get_popularity(),
is_explicit: track.get_explicit(),
external_ids: track.get_external_id().into(),
restrictions: track.get_restriction().into(),
files: track.get_file().into(),
alternatives: track.get_alternative().try_into()?,
sale_periods: track.get_sale_period().into(),
previews: track.get_preview().into(),
tags: track.get_tags().to_vec(),
earliest_live_timestamp: track.get_earliest_live_timestamp().try_into()?,
has_lyrics: track.get_has_lyrics(),
availability: track.get_availability().into(),
licensor: Uuid::from_slice(track.get_licensor().get_uuid())
.unwrap_or_else(|_| Uuid::nil()),
language_of_performance: track.get_language_of_performance().to_vec(),
content_ratings: track.get_content_rating().into(),
original_title: track.get_original_title().to_owned(),
version_title: track.get_version_title().to_owned(),
artists_with_role: track.get_artist_with_role().try_into()?,
})
}
}
try_from_repeated_message!(<Track as Metadata>::Message, Tracks);

39
metadata/src/util.rs Normal file
View file

@ -0,0 +1,39 @@
macro_rules! from_repeated_message {
($src:ty, $dst:ty) => {
impl From<&[$src]> for $dst {
fn from(src: &[$src]) -> Self {
let result = src.iter().map(From::from).collect();
Self(result)
}
}
};
}
pub(crate) use from_repeated_message;
macro_rules! from_repeated_enum {
($src:ty, $dst:ty) => {
impl From<&[$src]> for $dst {
fn from(src: &[$src]) -> Self {
let result = src.iter().map(|x| <$src>::from(*x)).collect();
Self(result)
}
}
};
}
pub(crate) use from_repeated_enum;
macro_rules! try_from_repeated_message {
($src:ty, $dst:ty) => {
impl TryFrom<&[$src]> for $dst {
type Error = MetadataError;
fn try_from(src: &[$src]) -> Result<Self, Self::Error> {
let result: Result<Vec<_>, _> = src.iter().map(TryFrom::try_from).collect();
Ok(Self(result?))
}
}
};
}
pub(crate) use try_from_repeated_message;

21
metadata/src/video.rs Normal file
View file

@ -0,0 +1,21 @@
use std::fmt::Debug;
use std::ops::Deref;
use crate::util::from_repeated_message;
use librespot_core::spotify_id::FileId;
use librespot_protocol as protocol;
use protocol::metadata::VideoFile as VideoFileMessage;
#[derive(Debug, Clone)]
pub struct VideoFiles(pub Vec<FileId>);
impl Deref for VideoFiles {
type Target = Vec<FileId>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
from_repeated_message!(VideoFileMessage, VideoFiles);

View file

@ -24,7 +24,7 @@ use crate::core::session::Session;
use crate::core::spotify_id::SpotifyId; use crate::core::spotify_id::SpotifyId;
use crate::core::util::SeqGenerator; use crate::core::util::SeqGenerator;
use crate::decoder::{AudioDecoder, AudioError, AudioPacket, PassthroughDecoder, VorbisDecoder}; use crate::decoder::{AudioDecoder, AudioError, AudioPacket, PassthroughDecoder, VorbisDecoder};
use crate::metadata::{AudioItem, FileFormat}; use crate::metadata::audio::{AudioFileFormat, AudioItem};
use crate::mixer::AudioFilter; use crate::mixer::AudioFilter;
use crate::{NUM_CHANNELS, SAMPLES_PER_SECOND}; use crate::{NUM_CHANNELS, SAMPLES_PER_SECOND};
@ -639,17 +639,17 @@ struct PlayerTrackLoader {
impl PlayerTrackLoader { impl PlayerTrackLoader {
async fn find_available_alternative(&self, audio: AudioItem) -> Option<AudioItem> { async fn find_available_alternative(&self, audio: AudioItem) -> Option<AudioItem> {
if audio.available { if audio.availability.is_ok() {
Some(audio) Some(audio)
} else if let Some(alternatives) = &audio.alternatives { } else if let Some(alternatives) = &audio.alternatives {
let alternatives: FuturesUnordered<_> = alternatives let alternatives: FuturesUnordered<_> = alternatives
.iter() .iter()
.map(|alt_id| AudioItem::get_audio_item(&self.session, *alt_id)) .map(|alt_id| AudioItem::get_file(&self.session, *alt_id))
.collect(); .collect();
alternatives alternatives
.filter_map(|x| future::ready(x.ok())) .filter_map(|x| future::ready(x.ok()))
.filter(|x| future::ready(x.available)) .filter(|x| future::ready(x.availability.is_ok()))
.next() .next()
.await .await
} else { } else {
@ -657,19 +657,19 @@ impl PlayerTrackLoader {
} }
} }
fn stream_data_rate(&self, format: FileFormat) -> usize { fn stream_data_rate(&self, format: AudioFileFormat) -> usize {
match format { match format {
FileFormat::OGG_VORBIS_96 => 12 * 1024, AudioFileFormat::OGG_VORBIS_96 => 12 * 1024,
FileFormat::OGG_VORBIS_160 => 20 * 1024, AudioFileFormat::OGG_VORBIS_160 => 20 * 1024,
FileFormat::OGG_VORBIS_320 => 40 * 1024, AudioFileFormat::OGG_VORBIS_320 => 40 * 1024,
FileFormat::MP3_256 => 32 * 1024, AudioFileFormat::MP3_256 => 32 * 1024,
FileFormat::MP3_320 => 40 * 1024, AudioFileFormat::MP3_320 => 40 * 1024,
FileFormat::MP3_160 => 20 * 1024, AudioFileFormat::MP3_160 => 20 * 1024,
FileFormat::MP3_96 => 12 * 1024, AudioFileFormat::MP3_96 => 12 * 1024,
FileFormat::MP3_160_ENC => 20 * 1024, AudioFileFormat::MP3_160_ENC => 20 * 1024,
FileFormat::AAC_24 => 3 * 1024, AudioFileFormat::AAC_24 => 3 * 1024,
FileFormat::AAC_48 => 6 * 1024, AudioFileFormat::AAC_48 => 6 * 1024,
FileFormat::FLAC_FLAC => 112 * 1024, // assume 900 kbps on average AudioFileFormat::FLAC_FLAC => 112 * 1024, // assume 900 kbps on average
} }
} }
@ -678,7 +678,7 @@ impl PlayerTrackLoader {
spotify_id: SpotifyId, spotify_id: SpotifyId,
position_ms: u32, position_ms: u32,
) -> Option<PlayerLoadedTrackData> { ) -> Option<PlayerLoadedTrackData> {
let audio = match AudioItem::get_audio_item(&self.session, spotify_id).await { let audio = match AudioItem::get_file(&self.session, spotify_id).await {
Ok(audio) => audio, Ok(audio) => audio,
Err(_) => { Err(_) => {
error!("Unable to load audio item."); error!("Unable to load audio item.");
@ -686,7 +686,10 @@ impl PlayerTrackLoader {
} }
}; };
info!("Loading <{}> with Spotify URI <{}>", audio.name, audio.uri); info!(
"Loading <{}> with Spotify URI <{}>",
audio.name, audio.spotify_uri
);
let audio = match self.find_available_alternative(audio).await { let audio = match self.find_available_alternative(audio).await {
Some(audio) => audio, Some(audio) => audio,
@ -699,22 +702,23 @@ impl PlayerTrackLoader {
assert!(audio.duration >= 0); assert!(audio.duration >= 0);
let duration_ms = audio.duration as u32; let duration_ms = audio.duration as u32;
// (Most) podcasts seem to support only 96 bit Vorbis, so fall back to it // (Most) podcasts seem to support only 96 kbps Vorbis, so fall back to it
// TODO: update this logic once we also support MP3 and/or FLAC
let formats = match self.config.bitrate { let formats = match self.config.bitrate {
Bitrate::Bitrate96 => [ Bitrate::Bitrate96 => [
FileFormat::OGG_VORBIS_96, AudioFileFormat::OGG_VORBIS_96,
FileFormat::OGG_VORBIS_160, AudioFileFormat::OGG_VORBIS_160,
FileFormat::OGG_VORBIS_320, AudioFileFormat::OGG_VORBIS_320,
], ],
Bitrate::Bitrate160 => [ Bitrate::Bitrate160 => [
FileFormat::OGG_VORBIS_160, AudioFileFormat::OGG_VORBIS_160,
FileFormat::OGG_VORBIS_96, AudioFileFormat::OGG_VORBIS_96,
FileFormat::OGG_VORBIS_320, AudioFileFormat::OGG_VORBIS_320,
], ],
Bitrate::Bitrate320 => [ Bitrate::Bitrate320 => [
FileFormat::OGG_VORBIS_320, AudioFileFormat::OGG_VORBIS_320,
FileFormat::OGG_VORBIS_160, AudioFileFormat::OGG_VORBIS_160,
FileFormat::OGG_VORBIS_96, AudioFileFormat::OGG_VORBIS_96,
], ],
}; };