mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Major metadata refactoring and enhancement
* Expose all fields of recent protobufs * Add support for user-scoped playlists, user root playlists and playlist annotations * Convert messages with the Rust type system * Attempt to adhere to embargos (tracks and episodes scheduled for future release) * Return `Result`s with meaningful errors instead of panicking on `unwrap`s * Add foundation for future playlist editing * Up version in connection handshake to get all version-gated features
This commit is contained in:
parent
47badd61e0
commit
0e2686863a
36 changed files with 2530 additions and 757 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1350,11 +1350,13 @@ dependencies = [
|
|||
"async-trait",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"librespot-core",
|
||||
"librespot-protocol",
|
||||
"log",
|
||||
"protobuf",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
@ -6,7 +7,7 @@ use crate::context::StationContext;
|
|||
use crate::core::config::ConnectConfig;
|
||||
use crate::core::mercury::{MercuryError, MercurySender};
|
||||
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::version;
|
||||
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
|
||||
fn get_track_index_for_spotify_id(
|
||||
&self,
|
||||
|
@ -1146,11 +1138,8 @@ impl SpircTask {
|
|||
// 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_id = self.get_spotify_id_for_track(&track_ref);
|
||||
while self.track_ref_is_unavailable(&track_ref)
|
||||
|| track_id.is_err()
|
||||
|| track_id.unwrap().audio_type == SpotifyAudioType::NonPlayable
|
||||
{
|
||||
let mut track_id = SpotifyId::try_from(&track_ref);
|
||||
while self.track_ref_is_unavailable(&track_ref) || track_id.is_err() {
|
||||
warn!(
|
||||
"Skipping track <{:?}> at position [{}] of {}",
|
||||
track_ref, new_playlist_index, tracks_len
|
||||
|
@ -1166,7 +1155,7 @@ impl SpircTask {
|
|||
return None;
|
||||
}
|
||||
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 {
|
||||
|
|
|
@ -49,7 +49,7 @@ where
|
|||
packet
|
||||
.mut_build_info()
|
||||
.set_platform(protocol::keyexchange::Platform::PLATFORM_LINUX_X86);
|
||||
packet.mut_build_info().set_version(109800078);
|
||||
packet.mut_build_info().set_version(999999999);
|
||||
packet
|
||||
.mut_cryptosuites_supported()
|
||||
.push(protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON);
|
||||
|
|
|
@ -227,7 +227,6 @@ impl SpClient {
|
|||
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 {
|
||||
let endpoint = format!(
|
||||
"/color-lyrics/v2/track/{}/image/spotify:image:{}",
|
||||
|
|
|
@ -1,31 +1,46 @@
|
|||
#![allow(clippy::wrong_self_convention)]
|
||||
use librespot_protocol as protocol;
|
||||
|
||||
use std::convert::TryInto;
|
||||
use thiserror::Error;
|
||||
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum SpotifyAudioType {
|
||||
pub enum SpotifyItemType {
|
||||
Album,
|
||||
Artist,
|
||||
Episode,
|
||||
Playlist,
|
||||
Show,
|
||||
Track,
|
||||
Podcast,
|
||||
NonPlayable,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<&str> for SpotifyAudioType {
|
||||
impl From<&str> for SpotifyItemType {
|
||||
fn from(v: &str) -> Self {
|
||||
match v {
|
||||
"track" => SpotifyAudioType::Track,
|
||||
"episode" => SpotifyAudioType::Podcast,
|
||||
_ => SpotifyAudioType::NonPlayable,
|
||||
"album" => Self::Album,
|
||||
"artist" => Self::Artist,
|
||||
"episode" => Self::Episode,
|
||||
"playlist" => Self::Playlist,
|
||||
"show" => Self::Show,
|
||||
"track" => Self::Track,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SpotifyAudioType> for &str {
|
||||
fn from(audio_type: SpotifyAudioType) -> &'static str {
|
||||
match audio_type {
|
||||
SpotifyAudioType::Track => "track",
|
||||
SpotifyAudioType::Podcast => "episode",
|
||||
SpotifyAudioType::NonPlayable => "unknown",
|
||||
impl From<SpotifyItemType> for &str {
|
||||
fn from(item_type: SpotifyItemType) -> &'static str {
|
||||
match item_type {
|
||||
SpotifyItemType::Album => "album",
|
||||
SpotifyItemType::Artist => "artist",
|
||||
SpotifyItemType::Episode => "episode",
|
||||
SpotifyItemType::Playlist => "playlist",
|
||||
SpotifyItemType::Show => "show",
|
||||
SpotifyItemType::Track => "track",
|
||||
_ => "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,11 +48,21 @@ impl From<SpotifyAudioType> for &str {
|
|||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct SpotifyId {
|
||||
pub id: u128,
|
||||
pub audio_type: SpotifyAudioType,
|
||||
pub item_type: SpotifyItemType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct SpotifyIdError;
|
||||
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SpotifyIdError {
|
||||
#[error("ID cannot be parsed")]
|
||||
InvalidId,
|
||||
#[error("not a valid Spotify URI")]
|
||||
InvalidFormat,
|
||||
#[error("URI does not belong to Spotify")]
|
||||
InvalidRoot,
|
||||
}
|
||||
|
||||
pub type SpotifyIdResult = Result<SpotifyId, SpotifyIdError>;
|
||||
pub type NamedSpotifyIdResult = Result<NamedSpotifyId, SpotifyIdError>;
|
||||
|
||||
const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef";
|
||||
|
@ -47,11 +72,12 @@ impl SpotifyId {
|
|||
const SIZE_BASE16: usize = 32;
|
||||
const SIZE_BASE62: usize = 22;
|
||||
|
||||
fn track(n: u128) -> SpotifyId {
|
||||
SpotifyId {
|
||||
id: n,
|
||||
audio_type: SpotifyAudioType::Track,
|
||||
}
|
||||
/// Returns whether this `SpotifyId` is for a playable audio item, if known.
|
||||
pub fn is_playable(&self) -> bool {
|
||||
return matches!(
|
||||
self.item_type,
|
||||
SpotifyItemType::Episode | SpotifyItemType::Track
|
||||
);
|
||||
}
|
||||
|
||||
/// Parses a base16 (hex) encoded [Spotify ID] into a `SpotifyId`.
|
||||
|
@ -59,29 +85,32 @@ impl SpotifyId {
|
|||
/// `src` is expected to be 32 bytes long and encoded using valid characters.
|
||||
///
|
||||
/// [Spotify ID]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
||||
pub fn from_base16(src: &str) -> Result<SpotifyId, SpotifyIdError> {
|
||||
pub fn from_base16(src: &str) -> SpotifyIdResult {
|
||||
let mut dst: u128 = 0;
|
||||
|
||||
for c in src.as_bytes() {
|
||||
let p = match c {
|
||||
b'0'..=b'9' => c - b'0',
|
||||
b'a'..=b'f' => c - b'a' + 10,
|
||||
_ => return Err(SpotifyIdError),
|
||||
_ => return Err(SpotifyIdError::InvalidId),
|
||||
} as u128;
|
||||
|
||||
dst <<= 4;
|
||||
dst += p;
|
||||
}
|
||||
|
||||
Ok(SpotifyId::track(dst))
|
||||
Ok(Self {
|
||||
id: dst,
|
||||
item_type: SpotifyItemType::Unknown,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parses a base62 encoded [Spotify ID] into a `SpotifyId`.
|
||||
/// Parses a base62 encoded [Spotify ID] into a `u128`.
|
||||
///
|
||||
/// `src` is expected to be 22 bytes long and encoded using valid characters.
|
||||
///
|
||||
/// [Spotify ID]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
||||
pub fn from_base62(src: &str) -> Result<SpotifyId, SpotifyIdError> {
|
||||
pub fn from_base62(src: &str) -> SpotifyIdResult {
|
||||
let mut dst: u128 = 0;
|
||||
|
||||
for c in src.as_bytes() {
|
||||
|
@ -89,23 +118,29 @@ impl SpotifyId {
|
|||
b'0'..=b'9' => c - b'0',
|
||||
b'a'..=b'z' => c - b'a' + 10,
|
||||
b'A'..=b'Z' => c - b'A' + 36,
|
||||
_ => return Err(SpotifyIdError),
|
||||
_ => return Err(SpotifyIdError::InvalidId),
|
||||
} as u128;
|
||||
|
||||
dst *= 62;
|
||||
dst += p;
|
||||
}
|
||||
|
||||
Ok(SpotifyId::track(dst))
|
||||
Ok(Self {
|
||||
id: dst,
|
||||
item_type: SpotifyItemType::Unknown,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a `SpotifyId` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order.
|
||||
/// Creates a `u128` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order.
|
||||
///
|
||||
/// The resulting `SpotifyId` will default to a `SpotifyAudioType::TRACK`.
|
||||
pub fn from_raw(src: &[u8]) -> Result<SpotifyId, SpotifyIdError> {
|
||||
/// The resulting `SpotifyId` will default to a `SpotifyItemType::Unknown`.
|
||||
pub fn from_raw(src: &[u8]) -> SpotifyIdResult {
|
||||
match src.try_into() {
|
||||
Ok(dst) => Ok(SpotifyId::track(u128::from_be_bytes(dst))),
|
||||
Err(_) => Err(SpotifyIdError),
|
||||
Ok(dst) => Ok(Self {
|
||||
id: u128::from_be_bytes(dst),
|
||||
item_type: SpotifyItemType::Unknown,
|
||||
}),
|
||||
Err(_) => Err(SpotifyIdError::InvalidId),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,30 +149,37 @@ impl SpotifyId {
|
|||
/// `uri` is expected to be in the canonical form `spotify:{type}:{id}`, where `{type}`
|
||||
/// can be arbitrary while `{id}` is a 22-character long, base62 encoded Spotify ID.
|
||||
///
|
||||
/// Note that this should not be used for playlists, which have the form of
|
||||
/// `spotify:user:{owner_username}:playlist:{id}`.
|
||||
///
|
||||
/// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
||||
pub fn from_uri(src: &str) -> Result<SpotifyId, SpotifyIdError> {
|
||||
let src = src.strip_prefix("spotify:").ok_or(SpotifyIdError)?;
|
||||
pub fn from_uri(src: &str) -> SpotifyIdResult {
|
||||
let mut uri_parts: Vec<&str> = src.split(':').collect();
|
||||
|
||||
if src.len() <= SpotifyId::SIZE_BASE62 {
|
||||
return Err(SpotifyIdError);
|
||||
// At minimum, should be `spotify:{type}:{id}`
|
||||
if uri_parts.len() < 3 {
|
||||
return Err(SpotifyIdError::InvalidFormat);
|
||||
}
|
||||
|
||||
let colon_index = src.len() - SpotifyId::SIZE_BASE62 - 1;
|
||||
|
||||
if src.as_bytes()[colon_index] != b':' {
|
||||
return Err(SpotifyIdError);
|
||||
if uri_parts[0] != "spotify" {
|
||||
return Err(SpotifyIdError::InvalidRoot);
|
||||
}
|
||||
|
||||
let mut id = SpotifyId::from_base62(&src[colon_index + 1..])?;
|
||||
id.audio_type = src[..colon_index].into();
|
||||
let id = uri_parts.pop().unwrap();
|
||||
if id.len() != Self::SIZE_BASE62 {
|
||||
return Err(SpotifyIdError::InvalidId);
|
||||
}
|
||||
|
||||
Ok(id)
|
||||
Ok(Self {
|
||||
item_type: uri_parts.pop().unwrap().into(),
|
||||
..Self::from_base62(id)?
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE16` (32)
|
||||
/// character long `String`.
|
||||
pub fn to_base16(&self) -> String {
|
||||
to_base16(&self.to_raw(), &mut [0u8; SpotifyId::SIZE_BASE16])
|
||||
to_base16(&self.to_raw(), &mut [0u8; Self::SIZE_BASE16])
|
||||
}
|
||||
|
||||
/// Returns the `SpotifyId` as a [canonically] base62 encoded, `SpotifyId::SIZE_BASE62` (22)
|
||||
|
@ -190,7 +232,7 @@ impl SpotifyId {
|
|||
|
||||
/// Returns a copy of the `SpotifyId` as an array of `SpotifyId::SIZE` (16) bytes in
|
||||
/// big-endian order.
|
||||
pub fn to_raw(&self) -> [u8; SpotifyId::SIZE] {
|
||||
pub fn to_raw(&self) -> [u8; Self::SIZE] {
|
||||
self.id.to_be_bytes()
|
||||
}
|
||||
|
||||
|
@ -204,11 +246,11 @@ impl SpotifyId {
|
|||
/// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
||||
pub fn to_uri(&self) -> String {
|
||||
// 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID = 31
|
||||
// + unknown size audio_type.
|
||||
let audio_type: &str = self.audio_type.into();
|
||||
let mut dst = String::with_capacity(31 + audio_type.len());
|
||||
// + unknown size item_type.
|
||||
let item_type: &str = self.item_type.into();
|
||||
let mut dst = String::with_capacity(31 + item_type.len());
|
||||
dst.push_str("spotify:");
|
||||
dst.push_str(audio_type);
|
||||
dst.push_str(item_type);
|
||||
dst.push(':');
|
||||
dst.push_str(&self.to_base62());
|
||||
|
||||
|
@ -216,10 +258,214 @@ impl SpotifyId {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct NamedSpotifyId {
|
||||
pub inner_id: SpotifyId,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
impl NamedSpotifyId {
|
||||
pub fn from_uri(src: &str) -> NamedSpotifyIdResult {
|
||||
let uri_parts: Vec<&str> = src.split(':').collect();
|
||||
|
||||
// At minimum, should be `spotify:user:{username}:{type}:{id}`
|
||||
if uri_parts.len() < 5 {
|
||||
return Err(SpotifyIdError::InvalidFormat);
|
||||
}
|
||||
|
||||
if uri_parts[0] != "spotify" {
|
||||
return Err(SpotifyIdError::InvalidRoot);
|
||||
}
|
||||
|
||||
if uri_parts[1] != "user" {
|
||||
return Err(SpotifyIdError::InvalidFormat);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
inner_id: SpotifyId::from_uri(src)?,
|
||||
username: uri_parts[2].to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_uri(&self) -> String {
|
||||
let item_type: &str = self.inner_id.item_type.into();
|
||||
let mut dst = String::with_capacity(37 + self.username.len() + item_type.len());
|
||||
dst.push_str("spotify:user:");
|
||||
dst.push_str(&self.username);
|
||||
dst.push_str(item_type);
|
||||
dst.push(':');
|
||||
dst.push_str(&self.to_base62());
|
||||
|
||||
dst
|
||||
}
|
||||
|
||||
pub fn from_spotify_id(id: SpotifyId, username: String) -> Self {
|
||||
Self {
|
||||
inner_id: id,
|
||||
username,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for NamedSpotifyId {
|
||||
type Target = SpotifyId;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner_id
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(src: &[u8]) -> Result<Self, Self::Error> {
|
||||
Self::from_raw(src)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(src: &str) -> Result<Self, Self::Error> {
|
||||
Self::from_base62(src)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(src: String) -> Result<Self, Self::Error> {
|
||||
Self::try_from(src.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Vec<u8>> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(src: &Vec<u8>) -> Result<Self, Self::Error> {
|
||||
Self::try_from(src.as_slice())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(track: &protocol::spirc::TrackRef) -> Result<Self, Self::Error> {
|
||||
match SpotifyId::from_raw(track.get_gid()) {
|
||||
Ok(mut id) => {
|
||||
id.item_type = SpotifyItemType::Track;
|
||||
Ok(id)
|
||||
}
|
||||
Err(_) => SpotifyId::from_uri(track.get_uri()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&protocol::metadata::Album> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(album: &protocol::metadata::Album) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Album,
|
||||
..Self::from_raw(album.get_gid())?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&protocol::metadata::Artist> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(artist: &protocol::metadata::Artist) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Artist,
|
||||
..Self::from_raw(artist.get_gid())?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&protocol::metadata::Episode> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(episode: &protocol::metadata::Episode) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Episode,
|
||||
..Self::from_raw(episode.get_gid())?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&protocol::metadata::Track> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(track: &protocol::metadata::Track) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Track,
|
||||
..Self::from_raw(track.get_gid())?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&protocol::metadata::Show> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(show: &protocol::metadata::Show) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Show,
|
||||
..Self::from_raw(show.get_gid())?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Artist,
|
||||
..Self::from_raw(artist.get_artist_gid())?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(item: &protocol::playlist4_external::Item) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
item_type: SpotifyItemType::Track,
|
||||
..Self::from_uri(item.get_uri())?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Note that this is the unique revision of an item's metadata on a playlist,
|
||||
// not the ID of that item or playlist.
|
||||
impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result<Self, Self::Error> {
|
||||
Self::try_from(item.get_revision())
|
||||
}
|
||||
}
|
||||
|
||||
// Note that this is the unique revision of a playlist, not the ID of that playlist.
|
||||
impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(
|
||||
playlist: &protocol::playlist4_external::SelectedListContent,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Self::try_from(playlist.get_revision())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check meaning and format of this field in the wild. This might be a FileId,
|
||||
// which is why we now don't create a separate `Playlist` enum value yet and choose
|
||||
// to discard any item type.
|
||||
impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId {
|
||||
type Error = SpotifyIdError;
|
||||
fn try_from(
|
||||
picture: &protocol::playlist_annotate3::TranscodedPicture,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Self::from_base62(picture.get_uri())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct FileId(pub [u8; 20]);
|
||||
|
||||
impl FileId {
|
||||
pub fn from_raw(src: &[u8]) -> FileId {
|
||||
let mut dst = [0u8; 20];
|
||||
dst.clone_from_slice(src);
|
||||
FileId(dst)
|
||||
}
|
||||
|
||||
pub fn to_base16(&self) -> String {
|
||||
to_base16(&self.0, &mut [0u8; 40])
|
||||
}
|
||||
|
@ -237,6 +483,29 @@ impl fmt::Display for FileId {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for FileId {
|
||||
fn from(src: &[u8]) -> Self {
|
||||
Self::from_raw(src)
|
||||
}
|
||||
}
|
||||
impl From<&protocol::metadata::Image> for FileId {
|
||||
fn from(image: &protocol::metadata::Image) -> Self {
|
||||
Self::from(image.get_file_id())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&protocol::metadata::AudioFile> for FileId {
|
||||
fn from(file: &protocol::metadata::AudioFile) -> Self {
|
||||
Self::from(file.get_file_id())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&protocol::metadata::VideoFile> for FileId {
|
||||
fn from(video: &protocol::metadata::VideoFile) -> Self {
|
||||
Self::from(video.get_file_id())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn to_base16(src: &[u8], buf: &mut [u8]) -> String {
|
||||
let mut i = 0;
|
||||
|
@ -258,7 +527,8 @@ mod tests {
|
|||
|
||||
struct ConversionCase {
|
||||
id: u128,
|
||||
kind: SpotifyAudioType,
|
||||
kind: SpotifyItemType,
|
||||
uri_error: Option<SpotifyIdError>,
|
||||
uri: &'static str,
|
||||
base16: &'static str,
|
||||
base62: &'static str,
|
||||
|
@ -268,7 +538,8 @@ mod tests {
|
|||
static CONV_VALID: [ConversionCase; 4] = [
|
||||
ConversionCase {
|
||||
id: 238762092608182713602505436543891614649,
|
||||
kind: SpotifyAudioType::Track,
|
||||
kind: SpotifyItemType::Track,
|
||||
uri_error: None,
|
||||
uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH",
|
||||
base16: "b39fe8081e1f4c54be38e8d6f9f12bb9",
|
||||
base62: "5sWHDYs0csV6RS48xBl0tH",
|
||||
|
@ -278,7 +549,8 @@ mod tests {
|
|||
},
|
||||
ConversionCase {
|
||||
id: 204841891221366092811751085145916697048,
|
||||
kind: SpotifyAudioType::Track,
|
||||
kind: SpotifyItemType::Track,
|
||||
uri_error: None,
|
||||
uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4",
|
||||
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
|
||||
base62: "4GNcXTGWmnZ3ySrqvol3o4",
|
||||
|
@ -288,7 +560,8 @@ mod tests {
|
|||
},
|
||||
ConversionCase {
|
||||
id: 204841891221366092811751085145916697048,
|
||||
kind: SpotifyAudioType::Podcast,
|
||||
kind: SpotifyItemType::Episode,
|
||||
uri_error: None,
|
||||
uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4",
|
||||
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
|
||||
base62: "4GNcXTGWmnZ3ySrqvol3o4",
|
||||
|
@ -298,8 +571,9 @@ mod tests {
|
|||
},
|
||||
ConversionCase {
|
||||
id: 204841891221366092811751085145916697048,
|
||||
kind: SpotifyAudioType::NonPlayable,
|
||||
uri: "spotify:unknown:4GNcXTGWmnZ3ySrqvol3o4",
|
||||
kind: SpotifyItemType::Show,
|
||||
uri_error: None,
|
||||
uri: "spotify:show:4GNcXTGWmnZ3ySrqvol3o4",
|
||||
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
|
||||
base62: "4GNcXTGWmnZ3ySrqvol3o4",
|
||||
raw: &[
|
||||
|
@ -311,8 +585,9 @@ mod tests {
|
|||
static CONV_INVALID: [ConversionCase; 3] = [
|
||||
ConversionCase {
|
||||
id: 0,
|
||||
kind: SpotifyAudioType::NonPlayable,
|
||||
kind: SpotifyItemType::Unknown,
|
||||
// Invalid ID in the URI.
|
||||
uri_error: Some(SpotifyIdError::InvalidId),
|
||||
uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH",
|
||||
base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9",
|
||||
base62: "!!!!!Ys0csV6RS48xBl0tH",
|
||||
|
@ -323,8 +598,9 @@ mod tests {
|
|||
},
|
||||
ConversionCase {
|
||||
id: 0,
|
||||
kind: SpotifyAudioType::NonPlayable,
|
||||
kind: SpotifyItemType::Unknown,
|
||||
// Missing colon between ID and type.
|
||||
uri_error: Some(SpotifyIdError::InvalidFormat),
|
||||
uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH",
|
||||
base16: "--------------------",
|
||||
base62: "....................",
|
||||
|
@ -335,8 +611,9 @@ mod tests {
|
|||
},
|
||||
ConversionCase {
|
||||
id: 0,
|
||||
kind: SpotifyAudioType::NonPlayable,
|
||||
kind: SpotifyItemType::Unknown,
|
||||
// Uri too short
|
||||
uri_error: Some(SpotifyIdError::InvalidId),
|
||||
uri: "spotify:azb:aRS48xBl0tH",
|
||||
base16: "--------------------",
|
||||
base62: "....................",
|
||||
|
@ -354,7 +631,10 @@ mod tests {
|
|||
}
|
||||
|
||||
for c in &CONV_INVALID {
|
||||
assert_eq!(SpotifyId::from_base62(c.base62), Err(SpotifyIdError));
|
||||
assert_eq!(
|
||||
SpotifyId::from_base62(c.base62),
|
||||
Err(SpotifyIdError::InvalidId)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -363,7 +643,7 @@ mod tests {
|
|||
for c in &CONV_VALID {
|
||||
let id = SpotifyId {
|
||||
id: c.id,
|
||||
audio_type: c.kind,
|
||||
item_type: c.kind,
|
||||
};
|
||||
|
||||
assert_eq!(id.to_base62(), c.base62);
|
||||
|
@ -377,7 +657,10 @@ mod tests {
|
|||
}
|
||||
|
||||
for c in &CONV_INVALID {
|
||||
assert_eq!(SpotifyId::from_base16(c.base16), Err(SpotifyIdError));
|
||||
assert_eq!(
|
||||
SpotifyId::from_base16(c.base16),
|
||||
Err(SpotifyIdError::InvalidId)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -386,7 +669,7 @@ mod tests {
|
|||
for c in &CONV_VALID {
|
||||
let id = SpotifyId {
|
||||
id: c.id,
|
||||
audio_type: c.kind,
|
||||
item_type: c.kind,
|
||||
};
|
||||
|
||||
assert_eq!(id.to_base16(), c.base16);
|
||||
|
@ -399,11 +682,11 @@ mod tests {
|
|||
let actual = SpotifyId::from_uri(c.uri).unwrap();
|
||||
|
||||
assert_eq!(actual.id, c.id);
|
||||
assert_eq!(actual.audio_type, c.kind);
|
||||
assert_eq!(actual.item_type, c.kind);
|
||||
}
|
||||
|
||||
for c in &CONV_INVALID {
|
||||
assert_eq!(SpotifyId::from_uri(c.uri), Err(SpotifyIdError));
|
||||
assert_eq!(SpotifyId::from_uri(c.uri), Err(c.uri_error.unwrap()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -412,7 +695,7 @@ mod tests {
|
|||
for c in &CONV_VALID {
|
||||
let id = SpotifyId {
|
||||
id: c.id,
|
||||
audio_type: c.kind,
|
||||
item_type: c.kind,
|
||||
};
|
||||
|
||||
assert_eq!(id.to_uri(), c.uri);
|
||||
|
@ -426,7 +709,7 @@ mod tests {
|
|||
}
|
||||
|
||||
for c in &CONV_INVALID {
|
||||
assert_eq!(SpotifyId::from_raw(c.raw), Err(SpotifyIdError));
|
||||
assert_eq!(SpotifyId::from_raw(c.raw), Err(SpotifyIdError::InvalidId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,11 @@ edition = "2018"
|
|||
async-trait = "0.1"
|
||||
byteorder = "1.3"
|
||||
bytes = "1.0"
|
||||
chrono = "0.4"
|
||||
log = "0.4"
|
||||
protobuf = "2.14.0"
|
||||
thiserror = "1"
|
||||
uuid = { version = "0.8", default-features = false }
|
||||
|
||||
[dependencies.librespot-core]
|
||||
path = "../core"
|
||||
|
|
151
metadata/src/album.rs
Normal file
151
metadata/src/album.rs
Normal 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
139
metadata/src/artist.rs
Normal 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);
|
31
metadata/src/audio/file.rs
Normal file
31
metadata/src/audio/file.rs
Normal 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
104
metadata/src/audio/item.rs
Normal 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(())
|
||||
}
|
||||
}
|
5
metadata/src/audio/mod.rs
Normal file
5
metadata/src/audio/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub mod file;
|
||||
pub mod item;
|
||||
|
||||
pub use file::AudioFileFormat;
|
||||
pub use item::AudioItem;
|
49
metadata/src/availability.rs
Normal file
49
metadata/src/availability.rs
Normal 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);
|
35
metadata/src/content_rating.rs
Normal file
35
metadata/src/content_rating.rs
Normal 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
37
metadata/src/copyright.rs
Normal 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);
|
|
@ -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
70
metadata/src/date.rs
Normal 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
132
metadata/src/episode.rs
Normal 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
34
metadata/src/error.rs
Normal 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,
|
||||
}
|
35
metadata/src/external_id.rs
Normal file
35
metadata/src/external_id.rs
Normal 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
103
metadata/src/image.rs
Normal 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);
|
|
@ -1,643 +1,51 @@
|
|||
#![allow(clippy::unused_io_amount)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
#[macro_use]
|
||||
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::spclient::SpClientError;
|
||||
use librespot_core::spotify_id::{FileId, SpotifyAudioType, SpotifyId};
|
||||
use librespot_protocol as protocol;
|
||||
use protobuf::{Message, ProtobufError};
|
||||
use librespot_core::spotify_id::SpotifyId;
|
||||
|
||||
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;
|
||||
|
||||
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>;
|
||||
use error::MetadataError;
|
||||
use request::RequestResult;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Metadata: Send + Sized + 'static {
|
||||
type Message: protobuf::Message;
|
||||
|
||||
async fn request(session: &Session, id: SpotifyId) -> MetadataResult;
|
||||
fn parse(msg: &Self::Message, session: &Session) -> Self;
|
||||
// Request a protobuf
|
||||
async fn request(session: &Session, id: SpotifyId) -> RequestResult;
|
||||
|
||||
// Request a metadata struct
|
||||
async fn get(session: &Session, id: SpotifyId) -> Result<Self, MetadataError> {
|
||||
let response = Self::request(session, id).await?;
|
||||
let msg = Self::Message::parse_from_bytes(&response)?;
|
||||
Ok(Self::parse(&msg, session))
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
trace!("Received metadata: {:?}", msg);
|
||||
Self::parse(&msg, id)
|
||||
}
|
||||
|
||||
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, MetadataError>;
|
||||
}
|
||||
|
|
89
metadata/src/playlist/annotation.rs
Normal file
89
metadata/src/playlist/annotation.rs
Normal 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, ¤t_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(),
|
||||
})
|
||||
}
|
||||
}
|
195
metadata/src/playlist/attribute.rs
Normal file
195
metadata/src/playlist/attribute.rs
Normal 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()?,
|
||||
})
|
||||
}
|
||||
}
|
29
metadata/src/playlist/diff.rs
Normal file
29
metadata/src/playlist/diff.rs
Normal 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()?,
|
||||
})
|
||||
}
|
||||
}
|
96
metadata/src/playlist/item.rs
Normal file
96
metadata/src/playlist/item.rs
Normal 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);
|
201
metadata/src/playlist/list.rs
Normal file
201
metadata/src/playlist/list.rs
Normal 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);
|
9
metadata/src/playlist/mod.rs
Normal file
9
metadata/src/playlist/mod.rs
Normal 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;
|
114
metadata/src/playlist/operation.rs
Normal file
114
metadata/src/playlist/operation.rs
Normal 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
20
metadata/src/request.rs
Normal 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
106
metadata/src/restriction.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
37
metadata/src/sale_period.rs
Normal file
37
metadata/src/sale_period.rs
Normal 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
75
metadata/src/show.rs
Normal 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
150
metadata/src/track.rs
Normal 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
39
metadata/src/util.rs
Normal 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
21
metadata/src/video.rs
Normal 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);
|
|
@ -24,7 +24,7 @@ use crate::core::session::Session;
|
|||
use crate::core::spotify_id::SpotifyId;
|
||||
use crate::core::util::SeqGenerator;
|
||||
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::{NUM_CHANNELS, SAMPLES_PER_SECOND};
|
||||
|
@ -639,17 +639,17 @@ struct PlayerTrackLoader {
|
|||
|
||||
impl PlayerTrackLoader {
|
||||
async fn find_available_alternative(&self, audio: AudioItem) -> Option<AudioItem> {
|
||||
if audio.available {
|
||||
if audio.availability.is_ok() {
|
||||
Some(audio)
|
||||
} else if let Some(alternatives) = &audio.alternatives {
|
||||
let alternatives: FuturesUnordered<_> = alternatives
|
||||
.iter()
|
||||
.map(|alt_id| AudioItem::get_audio_item(&self.session, *alt_id))
|
||||
.map(|alt_id| AudioItem::get_file(&self.session, *alt_id))
|
||||
.collect();
|
||||
|
||||
alternatives
|
||||
.filter_map(|x| future::ready(x.ok()))
|
||||
.filter(|x| future::ready(x.available))
|
||||
.filter(|x| future::ready(x.availability.is_ok()))
|
||||
.next()
|
||||
.await
|
||||
} 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 {
|
||||
FileFormat::OGG_VORBIS_96 => 12 * 1024,
|
||||
FileFormat::OGG_VORBIS_160 => 20 * 1024,
|
||||
FileFormat::OGG_VORBIS_320 => 40 * 1024,
|
||||
FileFormat::MP3_256 => 32 * 1024,
|
||||
FileFormat::MP3_320 => 40 * 1024,
|
||||
FileFormat::MP3_160 => 20 * 1024,
|
||||
FileFormat::MP3_96 => 12 * 1024,
|
||||
FileFormat::MP3_160_ENC => 20 * 1024,
|
||||
FileFormat::AAC_24 => 3 * 1024,
|
||||
FileFormat::AAC_48 => 6 * 1024,
|
||||
FileFormat::FLAC_FLAC => 112 * 1024, // assume 900 kbps on average
|
||||
AudioFileFormat::OGG_VORBIS_96 => 12 * 1024,
|
||||
AudioFileFormat::OGG_VORBIS_160 => 20 * 1024,
|
||||
AudioFileFormat::OGG_VORBIS_320 => 40 * 1024,
|
||||
AudioFileFormat::MP3_256 => 32 * 1024,
|
||||
AudioFileFormat::MP3_320 => 40 * 1024,
|
||||
AudioFileFormat::MP3_160 => 20 * 1024,
|
||||
AudioFileFormat::MP3_96 => 12 * 1024,
|
||||
AudioFileFormat::MP3_160_ENC => 20 * 1024,
|
||||
AudioFileFormat::AAC_24 => 3 * 1024,
|
||||
AudioFileFormat::AAC_48 => 6 * 1024,
|
||||
AudioFileFormat::FLAC_FLAC => 112 * 1024, // assume 900 kbps on average
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -678,7 +678,7 @@ impl PlayerTrackLoader {
|
|||
spotify_id: SpotifyId,
|
||||
position_ms: u32,
|
||||
) -> 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,
|
||||
Err(_) => {
|
||||
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 {
|
||||
Some(audio) => audio,
|
||||
|
@ -699,22 +702,23 @@ impl PlayerTrackLoader {
|
|||
assert!(audio.duration >= 0);
|
||||
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 {
|
||||
Bitrate::Bitrate96 => [
|
||||
FileFormat::OGG_VORBIS_96,
|
||||
FileFormat::OGG_VORBIS_160,
|
||||
FileFormat::OGG_VORBIS_320,
|
||||
AudioFileFormat::OGG_VORBIS_96,
|
||||
AudioFileFormat::OGG_VORBIS_160,
|
||||
AudioFileFormat::OGG_VORBIS_320,
|
||||
],
|
||||
Bitrate::Bitrate160 => [
|
||||
FileFormat::OGG_VORBIS_160,
|
||||
FileFormat::OGG_VORBIS_96,
|
||||
FileFormat::OGG_VORBIS_320,
|
||||
AudioFileFormat::OGG_VORBIS_160,
|
||||
AudioFileFormat::OGG_VORBIS_96,
|
||||
AudioFileFormat::OGG_VORBIS_320,
|
||||
],
|
||||
Bitrate::Bitrate320 => [
|
||||
FileFormat::OGG_VORBIS_320,
|
||||
FileFormat::OGG_VORBIS_160,
|
||||
FileFormat::OGG_VORBIS_96,
|
||||
AudioFileFormat::OGG_VORBIS_320,
|
||||
AudioFileFormat::OGG_VORBIS_160,
|
||||
AudioFileFormat::OGG_VORBIS_96,
|
||||
],
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue