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",
|
"async-trait",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"chrono",
|
||||||
"librespot-core",
|
"librespot-core",
|
||||||
"librespot-protocol",
|
"librespot-protocol",
|
||||||
"log",
|
"log",
|
||||||
"protobuf",
|
"protobuf",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
@ -6,7 +7,7 @@ use crate::context::StationContext;
|
||||||
use crate::core::config::ConnectConfig;
|
use crate::core::config::ConnectConfig;
|
||||||
use crate::core::mercury::{MercuryError, MercurySender};
|
use crate::core::mercury::{MercuryError, MercurySender};
|
||||||
use crate::core::session::Session;
|
use crate::core::session::Session;
|
||||||
use crate::core::spotify_id::{SpotifyAudioType, SpotifyId, SpotifyIdError};
|
use crate::core::spotify_id::SpotifyId;
|
||||||
use crate::core::util::SeqGenerator;
|
use crate::core::util::SeqGenerator;
|
||||||
use crate::core::version;
|
use crate::core::version;
|
||||||
use crate::playback::mixer::Mixer;
|
use crate::playback::mixer::Mixer;
|
||||||
|
@ -1099,15 +1100,6 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// should this be a method of SpotifyId directly?
|
|
||||||
fn get_spotify_id_for_track(&self, track_ref: &TrackRef) -> Result<SpotifyId, SpotifyIdError> {
|
|
||||||
SpotifyId::from_raw(track_ref.get_gid()).or_else(|_| {
|
|
||||||
let uri = track_ref.get_uri();
|
|
||||||
debug!("Malformed or no gid, attempting to parse URI <{}>", uri);
|
|
||||||
SpotifyId::from_uri(uri)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to find corresponding index(s) for track_id
|
// Helper to find corresponding index(s) for track_id
|
||||||
fn get_track_index_for_spotify_id(
|
fn get_track_index_for_spotify_id(
|
||||||
&self,
|
&self,
|
||||||
|
@ -1146,11 +1138,8 @@ impl SpircTask {
|
||||||
// E.g - context based frames sometimes contain tracks with <spotify:meta:page:>
|
// E.g - context based frames sometimes contain tracks with <spotify:meta:page:>
|
||||||
|
|
||||||
let mut track_ref = self.state.get_track()[new_playlist_index].clone();
|
let mut track_ref = self.state.get_track()[new_playlist_index].clone();
|
||||||
let mut track_id = self.get_spotify_id_for_track(&track_ref);
|
let mut track_id = SpotifyId::try_from(&track_ref);
|
||||||
while self.track_ref_is_unavailable(&track_ref)
|
while self.track_ref_is_unavailable(&track_ref) || track_id.is_err() {
|
||||||
|| track_id.is_err()
|
|
||||||
|| track_id.unwrap().audio_type == SpotifyAudioType::NonPlayable
|
|
||||||
{
|
|
||||||
warn!(
|
warn!(
|
||||||
"Skipping track <{:?}> at position [{}] of {}",
|
"Skipping track <{:?}> at position [{}] of {}",
|
||||||
track_ref, new_playlist_index, tracks_len
|
track_ref, new_playlist_index, tracks_len
|
||||||
|
@ -1166,7 +1155,7 @@ impl SpircTask {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
track_ref = self.state.get_track()[new_playlist_index].clone();
|
track_ref = self.state.get_track()[new_playlist_index].clone();
|
||||||
track_id = self.get_spotify_id_for_track(&track_ref);
|
track_id = SpotifyId::try_from(&track_ref);
|
||||||
}
|
}
|
||||||
|
|
||||||
match track_id {
|
match track_id {
|
||||||
|
|
|
@ -49,7 +49,7 @@ where
|
||||||
packet
|
packet
|
||||||
.mut_build_info()
|
.mut_build_info()
|
||||||
.set_platform(protocol::keyexchange::Platform::PLATFORM_LINUX_X86);
|
.set_platform(protocol::keyexchange::Platform::PLATFORM_LINUX_X86);
|
||||||
packet.mut_build_info().set_version(109800078);
|
packet.mut_build_info().set_version(999999999);
|
||||||
packet
|
packet
|
||||||
.mut_cryptosuites_supported()
|
.mut_cryptosuites_supported()
|
||||||
.push(protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON);
|
.push(protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON);
|
||||||
|
|
|
@ -227,7 +227,6 @@ impl SpClient {
|
||||||
self.get_metadata("show", show_id).await
|
self.get_metadata("show", show_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Not working at the moment, always returns 400.
|
|
||||||
pub async fn get_lyrics(&self, track_id: SpotifyId, image_id: FileId) -> SpClientResult {
|
pub async fn get_lyrics(&self, track_id: SpotifyId, image_id: FileId) -> SpClientResult {
|
||||||
let endpoint = format!(
|
let endpoint = format!(
|
||||||
"/color-lyrics/v2/track/{}/image/spotify:image:{}",
|
"/color-lyrics/v2/track/{}/image/spotify:image:{}",
|
||||||
|
|
|
@ -1,31 +1,46 @@
|
||||||
#![allow(clippy::wrong_self_convention)]
|
use librespot_protocol as protocol;
|
||||||
|
|
||||||
use std::convert::TryInto;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use std::convert::{TryFrom, TryInto};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum SpotifyAudioType {
|
pub enum SpotifyItemType {
|
||||||
|
Album,
|
||||||
|
Artist,
|
||||||
|
Episode,
|
||||||
|
Playlist,
|
||||||
|
Show,
|
||||||
Track,
|
Track,
|
||||||
Podcast,
|
Unknown,
|
||||||
NonPlayable,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&str> for SpotifyAudioType {
|
impl From<&str> for SpotifyItemType {
|
||||||
fn from(v: &str) -> Self {
|
fn from(v: &str) -> Self {
|
||||||
match v {
|
match v {
|
||||||
"track" => SpotifyAudioType::Track,
|
"album" => Self::Album,
|
||||||
"episode" => SpotifyAudioType::Podcast,
|
"artist" => Self::Artist,
|
||||||
_ => SpotifyAudioType::NonPlayable,
|
"episode" => Self::Episode,
|
||||||
|
"playlist" => Self::Playlist,
|
||||||
|
"show" => Self::Show,
|
||||||
|
"track" => Self::Track,
|
||||||
|
_ => Self::Unknown,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SpotifyAudioType> for &str {
|
impl From<SpotifyItemType> for &str {
|
||||||
fn from(audio_type: SpotifyAudioType) -> &'static str {
|
fn from(item_type: SpotifyItemType) -> &'static str {
|
||||||
match audio_type {
|
match item_type {
|
||||||
SpotifyAudioType::Track => "track",
|
SpotifyItemType::Album => "album",
|
||||||
SpotifyAudioType::Podcast => "episode",
|
SpotifyItemType::Artist => "artist",
|
||||||
SpotifyAudioType::NonPlayable => "unknown",
|
SpotifyItemType::Episode => "episode",
|
||||||
|
SpotifyItemType::Playlist => "playlist",
|
||||||
|
SpotifyItemType::Show => "show",
|
||||||
|
SpotifyItemType::Track => "track",
|
||||||
|
_ => "unknown",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,11 +48,21 @@ impl From<SpotifyAudioType> for &str {
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct SpotifyId {
|
pub struct SpotifyId {
|
||||||
pub id: u128,
|
pub id: u128,
|
||||||
pub audio_type: SpotifyAudioType,
|
pub item_type: SpotifyItemType,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct SpotifyIdError;
|
pub enum SpotifyIdError {
|
||||||
|
#[error("ID cannot be parsed")]
|
||||||
|
InvalidId,
|
||||||
|
#[error("not a valid Spotify URI")]
|
||||||
|
InvalidFormat,
|
||||||
|
#[error("URI does not belong to Spotify")]
|
||||||
|
InvalidRoot,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SpotifyIdResult = Result<SpotifyId, SpotifyIdError>;
|
||||||
|
pub type NamedSpotifyIdResult = Result<NamedSpotifyId, SpotifyIdError>;
|
||||||
|
|
||||||
const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef";
|
const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef";
|
||||||
|
@ -47,11 +72,12 @@ impl SpotifyId {
|
||||||
const SIZE_BASE16: usize = 32;
|
const SIZE_BASE16: usize = 32;
|
||||||
const SIZE_BASE62: usize = 22;
|
const SIZE_BASE62: usize = 22;
|
||||||
|
|
||||||
fn track(n: u128) -> SpotifyId {
|
/// Returns whether this `SpotifyId` is for a playable audio item, if known.
|
||||||
SpotifyId {
|
pub fn is_playable(&self) -> bool {
|
||||||
id: n,
|
return matches!(
|
||||||
audio_type: SpotifyAudioType::Track,
|
self.item_type,
|
||||||
}
|
SpotifyItemType::Episode | SpotifyItemType::Track
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a base16 (hex) encoded [Spotify ID] into a `SpotifyId`.
|
/// Parses a base16 (hex) encoded [Spotify ID] into a `SpotifyId`.
|
||||||
|
@ -59,29 +85,32 @@ impl SpotifyId {
|
||||||
/// `src` is expected to be 32 bytes long and encoded using valid characters.
|
/// `src` is expected to be 32 bytes long and encoded using valid characters.
|
||||||
///
|
///
|
||||||
/// [Spotify ID]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
/// [Spotify ID]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
||||||
pub fn from_base16(src: &str) -> Result<SpotifyId, SpotifyIdError> {
|
pub fn from_base16(src: &str) -> SpotifyIdResult {
|
||||||
let mut dst: u128 = 0;
|
let mut dst: u128 = 0;
|
||||||
|
|
||||||
for c in src.as_bytes() {
|
for c in src.as_bytes() {
|
||||||
let p = match c {
|
let p = match c {
|
||||||
b'0'..=b'9' => c - b'0',
|
b'0'..=b'9' => c - b'0',
|
||||||
b'a'..=b'f' => c - b'a' + 10,
|
b'a'..=b'f' => c - b'a' + 10,
|
||||||
_ => return Err(SpotifyIdError),
|
_ => return Err(SpotifyIdError::InvalidId),
|
||||||
} as u128;
|
} as u128;
|
||||||
|
|
||||||
dst <<= 4;
|
dst <<= 4;
|
||||||
dst += p;
|
dst += p;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SpotifyId::track(dst))
|
Ok(Self {
|
||||||
|
id: dst,
|
||||||
|
item_type: SpotifyItemType::Unknown,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a base62 encoded [Spotify ID] into a `SpotifyId`.
|
/// Parses a base62 encoded [Spotify ID] into a `u128`.
|
||||||
///
|
///
|
||||||
/// `src` is expected to be 22 bytes long and encoded using valid characters.
|
/// `src` is expected to be 22 bytes long and encoded using valid characters.
|
||||||
///
|
///
|
||||||
/// [Spotify ID]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
/// [Spotify ID]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
||||||
pub fn from_base62(src: &str) -> Result<SpotifyId, SpotifyIdError> {
|
pub fn from_base62(src: &str) -> SpotifyIdResult {
|
||||||
let mut dst: u128 = 0;
|
let mut dst: u128 = 0;
|
||||||
|
|
||||||
for c in src.as_bytes() {
|
for c in src.as_bytes() {
|
||||||
|
@ -89,23 +118,29 @@ impl SpotifyId {
|
||||||
b'0'..=b'9' => c - b'0',
|
b'0'..=b'9' => c - b'0',
|
||||||
b'a'..=b'z' => c - b'a' + 10,
|
b'a'..=b'z' => c - b'a' + 10,
|
||||||
b'A'..=b'Z' => c - b'A' + 36,
|
b'A'..=b'Z' => c - b'A' + 36,
|
||||||
_ => return Err(SpotifyIdError),
|
_ => return Err(SpotifyIdError::InvalidId),
|
||||||
} as u128;
|
} as u128;
|
||||||
|
|
||||||
dst *= 62;
|
dst *= 62;
|
||||||
dst += p;
|
dst += p;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SpotifyId::track(dst))
|
Ok(Self {
|
||||||
|
id: dst,
|
||||||
|
item_type: SpotifyItemType::Unknown,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a `SpotifyId` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order.
|
/// Creates a `u128` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order.
|
||||||
///
|
///
|
||||||
/// The resulting `SpotifyId` will default to a `SpotifyAudioType::TRACK`.
|
/// The resulting `SpotifyId` will default to a `SpotifyItemType::Unknown`.
|
||||||
pub fn from_raw(src: &[u8]) -> Result<SpotifyId, SpotifyIdError> {
|
pub fn from_raw(src: &[u8]) -> SpotifyIdResult {
|
||||||
match src.try_into() {
|
match src.try_into() {
|
||||||
Ok(dst) => Ok(SpotifyId::track(u128::from_be_bytes(dst))),
|
Ok(dst) => Ok(Self {
|
||||||
Err(_) => Err(SpotifyIdError),
|
id: u128::from_be_bytes(dst),
|
||||||
|
item_type: SpotifyItemType::Unknown,
|
||||||
|
}),
|
||||||
|
Err(_) => Err(SpotifyIdError::InvalidId),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,30 +149,37 @@ impl SpotifyId {
|
||||||
/// `uri` is expected to be in the canonical form `spotify:{type}:{id}`, where `{type}`
|
/// `uri` is expected to be in the canonical form `spotify:{type}:{id}`, where `{type}`
|
||||||
/// can be arbitrary while `{id}` is a 22-character long, base62 encoded Spotify ID.
|
/// can be arbitrary while `{id}` is a 22-character long, base62 encoded Spotify ID.
|
||||||
///
|
///
|
||||||
|
/// Note that this should not be used for playlists, which have the form of
|
||||||
|
/// `spotify:user:{owner_username}:playlist:{id}`.
|
||||||
|
///
|
||||||
/// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
/// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
||||||
pub fn from_uri(src: &str) -> Result<SpotifyId, SpotifyIdError> {
|
pub fn from_uri(src: &str) -> SpotifyIdResult {
|
||||||
let src = src.strip_prefix("spotify:").ok_or(SpotifyIdError)?;
|
let mut uri_parts: Vec<&str> = src.split(':').collect();
|
||||||
|
|
||||||
if src.len() <= SpotifyId::SIZE_BASE62 {
|
// At minimum, should be `spotify:{type}:{id}`
|
||||||
return Err(SpotifyIdError);
|
if uri_parts.len() < 3 {
|
||||||
|
return Err(SpotifyIdError::InvalidFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
let colon_index = src.len() - SpotifyId::SIZE_BASE62 - 1;
|
if uri_parts[0] != "spotify" {
|
||||||
|
return Err(SpotifyIdError::InvalidRoot);
|
||||||
if src.as_bytes()[colon_index] != b':' {
|
|
||||||
return Err(SpotifyIdError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut id = SpotifyId::from_base62(&src[colon_index + 1..])?;
|
let id = uri_parts.pop().unwrap();
|
||||||
id.audio_type = src[..colon_index].into();
|
if id.len() != Self::SIZE_BASE62 {
|
||||||
|
return Err(SpotifyIdError::InvalidId);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(id)
|
Ok(Self {
|
||||||
|
item_type: uri_parts.pop().unwrap().into(),
|
||||||
|
..Self::from_base62(id)?
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE16` (32)
|
/// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE16` (32)
|
||||||
/// character long `String`.
|
/// character long `String`.
|
||||||
pub fn to_base16(&self) -> String {
|
pub fn to_base16(&self) -> String {
|
||||||
to_base16(&self.to_raw(), &mut [0u8; SpotifyId::SIZE_BASE16])
|
to_base16(&self.to_raw(), &mut [0u8; Self::SIZE_BASE16])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `SpotifyId` as a [canonically] base62 encoded, `SpotifyId::SIZE_BASE62` (22)
|
/// Returns the `SpotifyId` as a [canonically] base62 encoded, `SpotifyId::SIZE_BASE62` (22)
|
||||||
|
@ -190,7 +232,7 @@ impl SpotifyId {
|
||||||
|
|
||||||
/// Returns a copy of the `SpotifyId` as an array of `SpotifyId::SIZE` (16) bytes in
|
/// Returns a copy of the `SpotifyId` as an array of `SpotifyId::SIZE` (16) bytes in
|
||||||
/// big-endian order.
|
/// big-endian order.
|
||||||
pub fn to_raw(&self) -> [u8; SpotifyId::SIZE] {
|
pub fn to_raw(&self) -> [u8; Self::SIZE] {
|
||||||
self.id.to_be_bytes()
|
self.id.to_be_bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,11 +246,11 @@ impl SpotifyId {
|
||||||
/// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
/// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
||||||
pub fn to_uri(&self) -> String {
|
pub fn to_uri(&self) -> String {
|
||||||
// 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID = 31
|
// 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID = 31
|
||||||
// + unknown size audio_type.
|
// + unknown size item_type.
|
||||||
let audio_type: &str = self.audio_type.into();
|
let item_type: &str = self.item_type.into();
|
||||||
let mut dst = String::with_capacity(31 + audio_type.len());
|
let mut dst = String::with_capacity(31 + item_type.len());
|
||||||
dst.push_str("spotify:");
|
dst.push_str("spotify:");
|
||||||
dst.push_str(audio_type);
|
dst.push_str(item_type);
|
||||||
dst.push(':');
|
dst.push(':');
|
||||||
dst.push_str(&self.to_base62());
|
dst.push_str(&self.to_base62());
|
||||||
|
|
||||||
|
@ -216,10 +258,214 @@ impl SpotifyId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct NamedSpotifyId {
|
||||||
|
pub inner_id: SpotifyId,
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NamedSpotifyId {
|
||||||
|
pub fn from_uri(src: &str) -> NamedSpotifyIdResult {
|
||||||
|
let uri_parts: Vec<&str> = src.split(':').collect();
|
||||||
|
|
||||||
|
// At minimum, should be `spotify:user:{username}:{type}:{id}`
|
||||||
|
if uri_parts.len() < 5 {
|
||||||
|
return Err(SpotifyIdError::InvalidFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
if uri_parts[0] != "spotify" {
|
||||||
|
return Err(SpotifyIdError::InvalidRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
if uri_parts[1] != "user" {
|
||||||
|
return Err(SpotifyIdError::InvalidFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
inner_id: SpotifyId::from_uri(src)?,
|
||||||
|
username: uri_parts[2].to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_uri(&self) -> String {
|
||||||
|
let item_type: &str = self.inner_id.item_type.into();
|
||||||
|
let mut dst = String::with_capacity(37 + self.username.len() + item_type.len());
|
||||||
|
dst.push_str("spotify:user:");
|
||||||
|
dst.push_str(&self.username);
|
||||||
|
dst.push_str(item_type);
|
||||||
|
dst.push(':');
|
||||||
|
dst.push_str(&self.to_base62());
|
||||||
|
|
||||||
|
dst
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_spotify_id(id: SpotifyId, username: String) -> Self {
|
||||||
|
Self {
|
||||||
|
inner_id: id,
|
||||||
|
username,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for NamedSpotifyId {
|
||||||
|
type Target = SpotifyId;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.inner_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&[u8]> for SpotifyId {
|
||||||
|
type Error = SpotifyIdError;
|
||||||
|
fn try_from(src: &[u8]) -> Result<Self, Self::Error> {
|
||||||
|
Self::from_raw(src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for SpotifyId {
|
||||||
|
type Error = SpotifyIdError;
|
||||||
|
fn try_from(src: &str) -> Result<Self, Self::Error> {
|
||||||
|
Self::from_base62(src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for SpotifyId {
|
||||||
|
type Error = SpotifyIdError;
|
||||||
|
fn try_from(src: String) -> Result<Self, Self::Error> {
|
||||||
|
Self::try_from(src.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&Vec<u8>> for SpotifyId {
|
||||||
|
type Error = SpotifyIdError;
|
||||||
|
fn try_from(src: &Vec<u8>) -> Result<Self, Self::Error> {
|
||||||
|
Self::try_from(src.as_slice())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId {
|
||||||
|
type Error = SpotifyIdError;
|
||||||
|
fn try_from(track: &protocol::spirc::TrackRef) -> Result<Self, Self::Error> {
|
||||||
|
match SpotifyId::from_raw(track.get_gid()) {
|
||||||
|
Ok(mut id) => {
|
||||||
|
id.item_type = SpotifyItemType::Track;
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
Err(_) => SpotifyId::from_uri(track.get_uri()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&protocol::metadata::Album> for SpotifyId {
|
||||||
|
type Error = SpotifyIdError;
|
||||||
|
fn try_from(album: &protocol::metadata::Album) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
item_type: SpotifyItemType::Album,
|
||||||
|
..Self::from_raw(album.get_gid())?
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&protocol::metadata::Artist> for SpotifyId {
|
||||||
|
type Error = SpotifyIdError;
|
||||||
|
fn try_from(artist: &protocol::metadata::Artist) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
item_type: SpotifyItemType::Artist,
|
||||||
|
..Self::from_raw(artist.get_gid())?
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&protocol::metadata::Episode> for SpotifyId {
|
||||||
|
type Error = SpotifyIdError;
|
||||||
|
fn try_from(episode: &protocol::metadata::Episode) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
item_type: SpotifyItemType::Episode,
|
||||||
|
..Self::from_raw(episode.get_gid())?
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&protocol::metadata::Track> for SpotifyId {
|
||||||
|
type Error = SpotifyIdError;
|
||||||
|
fn try_from(track: &protocol::metadata::Track) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
item_type: SpotifyItemType::Track,
|
||||||
|
..Self::from_raw(track.get_gid())?
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&protocol::metadata::Show> for SpotifyId {
|
||||||
|
type Error = SpotifyIdError;
|
||||||
|
fn try_from(show: &protocol::metadata::Show) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
item_type: SpotifyItemType::Show,
|
||||||
|
..Self::from_raw(show.get_gid())?
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId {
|
||||||
|
type Error = SpotifyIdError;
|
||||||
|
fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
item_type: SpotifyItemType::Artist,
|
||||||
|
..Self::from_raw(artist.get_artist_gid())?
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId {
|
||||||
|
type Error = SpotifyIdError;
|
||||||
|
fn try_from(item: &protocol::playlist4_external::Item) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
item_type: SpotifyItemType::Track,
|
||||||
|
..Self::from_uri(item.get_uri())?
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that this is the unique revision of an item's metadata on a playlist,
|
||||||
|
// not the ID of that item or playlist.
|
||||||
|
impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId {
|
||||||
|
type Error = SpotifyIdError;
|
||||||
|
fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result<Self, Self::Error> {
|
||||||
|
Self::try_from(item.get_revision())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that this is the unique revision of a playlist, not the ID of that playlist.
|
||||||
|
impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId {
|
||||||
|
type Error = SpotifyIdError;
|
||||||
|
fn try_from(
|
||||||
|
playlist: &protocol::playlist4_external::SelectedListContent,
|
||||||
|
) -> Result<Self, Self::Error> {
|
||||||
|
Self::try_from(playlist.get_revision())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: check meaning and format of this field in the wild. This might be a FileId,
|
||||||
|
// which is why we now don't create a separate `Playlist` enum value yet and choose
|
||||||
|
// to discard any item type.
|
||||||
|
impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId {
|
||||||
|
type Error = SpotifyIdError;
|
||||||
|
fn try_from(
|
||||||
|
picture: &protocol::playlist_annotate3::TranscodedPicture,
|
||||||
|
) -> Result<Self, Self::Error> {
|
||||||
|
Self::from_base62(picture.get_uri())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct FileId(pub [u8; 20]);
|
pub struct FileId(pub [u8; 20]);
|
||||||
|
|
||||||
impl FileId {
|
impl FileId {
|
||||||
|
pub fn from_raw(src: &[u8]) -> FileId {
|
||||||
|
let mut dst = [0u8; 20];
|
||||||
|
dst.clone_from_slice(src);
|
||||||
|
FileId(dst)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_base16(&self) -> String {
|
pub fn to_base16(&self) -> String {
|
||||||
to_base16(&self.0, &mut [0u8; 40])
|
to_base16(&self.0, &mut [0u8; 40])
|
||||||
}
|
}
|
||||||
|
@ -237,6 +483,29 @@ impl fmt::Display for FileId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&[u8]> for FileId {
|
||||||
|
fn from(src: &[u8]) -> Self {
|
||||||
|
Self::from_raw(src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<&protocol::metadata::Image> for FileId {
|
||||||
|
fn from(image: &protocol::metadata::Image) -> Self {
|
||||||
|
Self::from(image.get_file_id())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&protocol::metadata::AudioFile> for FileId {
|
||||||
|
fn from(file: &protocol::metadata::AudioFile) -> Self {
|
||||||
|
Self::from(file.get_file_id())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&protocol::metadata::VideoFile> for FileId {
|
||||||
|
fn from(video: &protocol::metadata::VideoFile) -> Self {
|
||||||
|
Self::from(video.get_file_id())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn to_base16(src: &[u8], buf: &mut [u8]) -> String {
|
fn to_base16(src: &[u8], buf: &mut [u8]) -> String {
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
|
@ -258,7 +527,8 @@ mod tests {
|
||||||
|
|
||||||
struct ConversionCase {
|
struct ConversionCase {
|
||||||
id: u128,
|
id: u128,
|
||||||
kind: SpotifyAudioType,
|
kind: SpotifyItemType,
|
||||||
|
uri_error: Option<SpotifyIdError>,
|
||||||
uri: &'static str,
|
uri: &'static str,
|
||||||
base16: &'static str,
|
base16: &'static str,
|
||||||
base62: &'static str,
|
base62: &'static str,
|
||||||
|
@ -268,7 +538,8 @@ mod tests {
|
||||||
static CONV_VALID: [ConversionCase; 4] = [
|
static CONV_VALID: [ConversionCase; 4] = [
|
||||||
ConversionCase {
|
ConversionCase {
|
||||||
id: 238762092608182713602505436543891614649,
|
id: 238762092608182713602505436543891614649,
|
||||||
kind: SpotifyAudioType::Track,
|
kind: SpotifyItemType::Track,
|
||||||
|
uri_error: None,
|
||||||
uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH",
|
uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH",
|
||||||
base16: "b39fe8081e1f4c54be38e8d6f9f12bb9",
|
base16: "b39fe8081e1f4c54be38e8d6f9f12bb9",
|
||||||
base62: "5sWHDYs0csV6RS48xBl0tH",
|
base62: "5sWHDYs0csV6RS48xBl0tH",
|
||||||
|
@ -278,7 +549,8 @@ mod tests {
|
||||||
},
|
},
|
||||||
ConversionCase {
|
ConversionCase {
|
||||||
id: 204841891221366092811751085145916697048,
|
id: 204841891221366092811751085145916697048,
|
||||||
kind: SpotifyAudioType::Track,
|
kind: SpotifyItemType::Track,
|
||||||
|
uri_error: None,
|
||||||
uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4",
|
uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4",
|
||||||
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
|
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
|
||||||
base62: "4GNcXTGWmnZ3ySrqvol3o4",
|
base62: "4GNcXTGWmnZ3ySrqvol3o4",
|
||||||
|
@ -288,7 +560,8 @@ mod tests {
|
||||||
},
|
},
|
||||||
ConversionCase {
|
ConversionCase {
|
||||||
id: 204841891221366092811751085145916697048,
|
id: 204841891221366092811751085145916697048,
|
||||||
kind: SpotifyAudioType::Podcast,
|
kind: SpotifyItemType::Episode,
|
||||||
|
uri_error: None,
|
||||||
uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4",
|
uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4",
|
||||||
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
|
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
|
||||||
base62: "4GNcXTGWmnZ3ySrqvol3o4",
|
base62: "4GNcXTGWmnZ3ySrqvol3o4",
|
||||||
|
@ -298,8 +571,9 @@ mod tests {
|
||||||
},
|
},
|
||||||
ConversionCase {
|
ConversionCase {
|
||||||
id: 204841891221366092811751085145916697048,
|
id: 204841891221366092811751085145916697048,
|
||||||
kind: SpotifyAudioType::NonPlayable,
|
kind: SpotifyItemType::Show,
|
||||||
uri: "spotify:unknown:4GNcXTGWmnZ3ySrqvol3o4",
|
uri_error: None,
|
||||||
|
uri: "spotify:show:4GNcXTGWmnZ3ySrqvol3o4",
|
||||||
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
|
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
|
||||||
base62: "4GNcXTGWmnZ3ySrqvol3o4",
|
base62: "4GNcXTGWmnZ3ySrqvol3o4",
|
||||||
raw: &[
|
raw: &[
|
||||||
|
@ -311,8 +585,9 @@ mod tests {
|
||||||
static CONV_INVALID: [ConversionCase; 3] = [
|
static CONV_INVALID: [ConversionCase; 3] = [
|
||||||
ConversionCase {
|
ConversionCase {
|
||||||
id: 0,
|
id: 0,
|
||||||
kind: SpotifyAudioType::NonPlayable,
|
kind: SpotifyItemType::Unknown,
|
||||||
// Invalid ID in the URI.
|
// Invalid ID in the URI.
|
||||||
|
uri_error: Some(SpotifyIdError::InvalidId),
|
||||||
uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH",
|
uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH",
|
||||||
base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9",
|
base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9",
|
||||||
base62: "!!!!!Ys0csV6RS48xBl0tH",
|
base62: "!!!!!Ys0csV6RS48xBl0tH",
|
||||||
|
@ -323,8 +598,9 @@ mod tests {
|
||||||
},
|
},
|
||||||
ConversionCase {
|
ConversionCase {
|
||||||
id: 0,
|
id: 0,
|
||||||
kind: SpotifyAudioType::NonPlayable,
|
kind: SpotifyItemType::Unknown,
|
||||||
// Missing colon between ID and type.
|
// Missing colon between ID and type.
|
||||||
|
uri_error: Some(SpotifyIdError::InvalidFormat),
|
||||||
uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH",
|
uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH",
|
||||||
base16: "--------------------",
|
base16: "--------------------",
|
||||||
base62: "....................",
|
base62: "....................",
|
||||||
|
@ -335,8 +611,9 @@ mod tests {
|
||||||
},
|
},
|
||||||
ConversionCase {
|
ConversionCase {
|
||||||
id: 0,
|
id: 0,
|
||||||
kind: SpotifyAudioType::NonPlayable,
|
kind: SpotifyItemType::Unknown,
|
||||||
// Uri too short
|
// Uri too short
|
||||||
|
uri_error: Some(SpotifyIdError::InvalidId),
|
||||||
uri: "spotify:azb:aRS48xBl0tH",
|
uri: "spotify:azb:aRS48xBl0tH",
|
||||||
base16: "--------------------",
|
base16: "--------------------",
|
||||||
base62: "....................",
|
base62: "....................",
|
||||||
|
@ -354,7 +631,10 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
for c in &CONV_INVALID {
|
for c in &CONV_INVALID {
|
||||||
assert_eq!(SpotifyId::from_base62(c.base62), Err(SpotifyIdError));
|
assert_eq!(
|
||||||
|
SpotifyId::from_base62(c.base62),
|
||||||
|
Err(SpotifyIdError::InvalidId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,7 +643,7 @@ mod tests {
|
||||||
for c in &CONV_VALID {
|
for c in &CONV_VALID {
|
||||||
let id = SpotifyId {
|
let id = SpotifyId {
|
||||||
id: c.id,
|
id: c.id,
|
||||||
audio_type: c.kind,
|
item_type: c.kind,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(id.to_base62(), c.base62);
|
assert_eq!(id.to_base62(), c.base62);
|
||||||
|
@ -377,7 +657,10 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
for c in &CONV_INVALID {
|
for c in &CONV_INVALID {
|
||||||
assert_eq!(SpotifyId::from_base16(c.base16), Err(SpotifyIdError));
|
assert_eq!(
|
||||||
|
SpotifyId::from_base16(c.base16),
|
||||||
|
Err(SpotifyIdError::InvalidId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -386,7 +669,7 @@ mod tests {
|
||||||
for c in &CONV_VALID {
|
for c in &CONV_VALID {
|
||||||
let id = SpotifyId {
|
let id = SpotifyId {
|
||||||
id: c.id,
|
id: c.id,
|
||||||
audio_type: c.kind,
|
item_type: c.kind,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(id.to_base16(), c.base16);
|
assert_eq!(id.to_base16(), c.base16);
|
||||||
|
@ -399,11 +682,11 @@ mod tests {
|
||||||
let actual = SpotifyId::from_uri(c.uri).unwrap();
|
let actual = SpotifyId::from_uri(c.uri).unwrap();
|
||||||
|
|
||||||
assert_eq!(actual.id, c.id);
|
assert_eq!(actual.id, c.id);
|
||||||
assert_eq!(actual.audio_type, c.kind);
|
assert_eq!(actual.item_type, c.kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
for c in &CONV_INVALID {
|
for c in &CONV_INVALID {
|
||||||
assert_eq!(SpotifyId::from_uri(c.uri), Err(SpotifyIdError));
|
assert_eq!(SpotifyId::from_uri(c.uri), Err(c.uri_error.unwrap()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -412,7 +695,7 @@ mod tests {
|
||||||
for c in &CONV_VALID {
|
for c in &CONV_VALID {
|
||||||
let id = SpotifyId {
|
let id = SpotifyId {
|
||||||
id: c.id,
|
id: c.id,
|
||||||
audio_type: c.kind,
|
item_type: c.kind,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(id.to_uri(), c.uri);
|
assert_eq!(id.to_uri(), c.uri);
|
||||||
|
@ -426,7 +709,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
for c in &CONV_INVALID {
|
for c in &CONV_INVALID {
|
||||||
assert_eq!(SpotifyId::from_raw(c.raw), Err(SpotifyIdError));
|
assert_eq!(SpotifyId::from_raw(c.raw), Err(SpotifyIdError::InvalidId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,11 @@ edition = "2018"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
byteorder = "1.3"
|
byteorder = "1.3"
|
||||||
bytes = "1.0"
|
bytes = "1.0"
|
||||||
|
chrono = "0.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
protobuf = "2.14.0"
|
protobuf = "2.14.0"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
uuid = { version = "0.8", default-features = false }
|
||||||
|
|
||||||
[dependencies.librespot-core]
|
[dependencies.librespot-core]
|
||||||
path = "../core"
|
path = "../core"
|
||||||
|
|
151
metadata/src/album.rs
Normal file
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]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate async_trait;
|
extern crate async_trait;
|
||||||
|
|
||||||
pub mod cover;
|
use protobuf::Message;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use librespot_core::mercury::MercuryError;
|
|
||||||
use librespot_core::session::Session;
|
use librespot_core::session::Session;
|
||||||
use librespot_core::spclient::SpClientError;
|
use librespot_core::spotify_id::SpotifyId;
|
||||||
use librespot_core::spotify_id::{FileId, SpotifyAudioType, SpotifyId};
|
|
||||||
use librespot_protocol as protocol;
|
|
||||||
use protobuf::{Message, ProtobufError};
|
|
||||||
|
|
||||||
use thiserror::Error;
|
pub mod album;
|
||||||
|
pub mod artist;
|
||||||
|
pub mod audio;
|
||||||
|
pub mod availability;
|
||||||
|
pub mod content_rating;
|
||||||
|
pub mod copyright;
|
||||||
|
pub mod date;
|
||||||
|
pub mod episode;
|
||||||
|
pub mod error;
|
||||||
|
pub mod external_id;
|
||||||
|
pub mod image;
|
||||||
|
pub mod playlist;
|
||||||
|
mod request;
|
||||||
|
pub mod restriction;
|
||||||
|
pub mod sale_period;
|
||||||
|
pub mod show;
|
||||||
|
pub mod track;
|
||||||
|
mod util;
|
||||||
|
pub mod video;
|
||||||
|
|
||||||
pub use crate::protocol::metadata::AudioFile_Format as FileFormat;
|
use error::MetadataError;
|
||||||
|
use request::RequestResult;
|
||||||
fn countrylist_contains(list: &str, country: &str) -> bool {
|
|
||||||
list.chunks(2).any(|cc| cc == country)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_restrictions<'s, I>(restrictions: I, country: &str, catalogue: &str) -> bool
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = &'s protocol::metadata::Restriction>,
|
|
||||||
{
|
|
||||||
let mut forbidden = "".to_string();
|
|
||||||
let mut has_forbidden = false;
|
|
||||||
|
|
||||||
let mut allowed = "".to_string();
|
|
||||||
let mut has_allowed = false;
|
|
||||||
|
|
||||||
let rs = restrictions
|
|
||||||
.into_iter()
|
|
||||||
.filter(|r| r.get_catalogue_str().contains(&catalogue.to_owned()));
|
|
||||||
|
|
||||||
for r in rs {
|
|
||||||
if r.has_countries_forbidden() {
|
|
||||||
forbidden.push_str(r.get_countries_forbidden());
|
|
||||||
has_forbidden = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.has_countries_allowed() {
|
|
||||||
allowed.push_str(r.get_countries_allowed());
|
|
||||||
has_allowed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
!(has_forbidden && countrylist_contains(forbidden.as_str(), country)
|
|
||||||
|| has_allowed && !countrylist_contains(allowed.as_str(), country))
|
|
||||||
}
|
|
||||||
|
|
||||||
// A wrapper with fields the player needs
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct AudioItem {
|
|
||||||
pub id: SpotifyId,
|
|
||||||
pub uri: String,
|
|
||||||
pub files: HashMap<FileFormat, FileId>,
|
|
||||||
pub name: String,
|
|
||||||
pub duration: i32,
|
|
||||||
pub available: bool,
|
|
||||||
pub alternatives: Option<Vec<SpotifyId>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AudioItem {
|
|
||||||
pub async fn get_audio_item(session: &Session, id: SpotifyId) -> Result<Self, MetadataError> {
|
|
||||||
match id.audio_type {
|
|
||||||
SpotifyAudioType::Track => Track::get_audio_item(session, id).await,
|
|
||||||
SpotifyAudioType::Podcast => Episode::get_audio_item(session, id).await,
|
|
||||||
SpotifyAudioType::NonPlayable => Err(MetadataError::NonPlayable),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type AudioItemResult = Result<AudioItem, MetadataError>;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
trait AudioFiles {
|
|
||||||
async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AudioFiles for Track {
|
|
||||||
async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult {
|
|
||||||
let item = Self::get(session, id).await?;
|
|
||||||
let alternatives = {
|
|
||||||
if item.alternatives.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(item.alternatives)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(AudioItem {
|
|
||||||
id,
|
|
||||||
uri: format!("spotify:track:{}", id.to_base62()),
|
|
||||||
files: item.files,
|
|
||||||
name: item.name,
|
|
||||||
duration: item.duration,
|
|
||||||
available: item.available,
|
|
||||||
alternatives,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AudioFiles for Episode {
|
|
||||||
async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult {
|
|
||||||
let item = Self::get(session, id).await?;
|
|
||||||
|
|
||||||
Ok(AudioItem {
|
|
||||||
id,
|
|
||||||
uri: format!("spotify:episode:{}", id.to_base62()),
|
|
||||||
files: item.files,
|
|
||||||
name: item.name,
|
|
||||||
duration: item.duration,
|
|
||||||
available: item.available,
|
|
||||||
alternatives: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum MetadataError {
|
|
||||||
#[error("could not get metadata over HTTP: {0}")]
|
|
||||||
Http(#[from] SpClientError),
|
|
||||||
#[error("could not get metadata over Mercury: {0}")]
|
|
||||||
Mercury(#[from] MercuryError),
|
|
||||||
#[error("could not parse metadata: {0}")]
|
|
||||||
Parsing(#[from] ProtobufError),
|
|
||||||
#[error("response was empty")]
|
|
||||||
Empty,
|
|
||||||
#[error("audio item is non-playable")]
|
|
||||||
NonPlayable,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type MetadataResult = Result<bytes::Bytes, MetadataError>;
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Metadata: Send + Sized + 'static {
|
pub trait Metadata: Send + Sized + 'static {
|
||||||
type Message: protobuf::Message;
|
type Message: protobuf::Message;
|
||||||
|
|
||||||
async fn request(session: &Session, id: SpotifyId) -> MetadataResult;
|
// Request a protobuf
|
||||||
fn parse(msg: &Self::Message, session: &Session) -> Self;
|
async fn request(session: &Session, id: SpotifyId) -> RequestResult;
|
||||||
|
|
||||||
|
// Request a metadata struct
|
||||||
async fn get(session: &Session, id: SpotifyId) -> Result<Self, MetadataError> {
|
async fn get(session: &Session, id: SpotifyId) -> Result<Self, MetadataError> {
|
||||||
let response = Self::request(session, id).await?;
|
let response = Self::request(session, id).await?;
|
||||||
let msg = Self::Message::parse_from_bytes(&response)?;
|
let msg = Self::Message::parse_from_bytes(&response)?;
|
||||||
Ok(Self::parse(&msg, session))
|
trace!("Received metadata: {:?}", msg);
|
||||||
}
|
Self::parse(&msg, id)
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: expose more fields available in the protobufs
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Track {
|
|
||||||
pub id: SpotifyId,
|
|
||||||
pub name: String,
|
|
||||||
pub duration: i32,
|
|
||||||
pub album: SpotifyId,
|
|
||||||
pub artists: Vec<SpotifyId>,
|
|
||||||
pub files: HashMap<FileFormat, FileId>,
|
|
||||||
pub alternatives: Vec<SpotifyId>,
|
|
||||||
pub available: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Album {
|
|
||||||
pub id: SpotifyId,
|
|
||||||
pub name: String,
|
|
||||||
pub artists: Vec<SpotifyId>,
|
|
||||||
pub tracks: Vec<SpotifyId>,
|
|
||||||
pub covers: Vec<FileId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Episode {
|
|
||||||
pub id: SpotifyId,
|
|
||||||
pub name: String,
|
|
||||||
pub external_url: String,
|
|
||||||
pub duration: i32,
|
|
||||||
pub language: String,
|
|
||||||
pub show: SpotifyId,
|
|
||||||
pub files: HashMap<FileFormat, FileId>,
|
|
||||||
pub covers: Vec<FileId>,
|
|
||||||
pub available: bool,
|
|
||||||
pub explicit: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Show {
|
|
||||||
pub id: SpotifyId,
|
|
||||||
pub name: String,
|
|
||||||
pub publisher: String,
|
|
||||||
pub episodes: Vec<SpotifyId>,
|
|
||||||
pub covers: Vec<FileId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct TranscodedPicture {
|
|
||||||
pub target_name: String,
|
|
||||||
pub uri: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct PlaylistAnnotation {
|
|
||||||
pub description: String,
|
|
||||||
pub picture: String,
|
|
||||||
pub transcoded_pictures: Vec<TranscodedPicture>,
|
|
||||||
pub abuse_reporting: bool,
|
|
||||||
pub taken_down: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Playlist {
|
|
||||||
pub revision: Vec<u8>,
|
|
||||||
pub user: String,
|
|
||||||
pub name: String,
|
|
||||||
pub tracks: Vec<SpotifyId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Artist {
|
|
||||||
pub id: SpotifyId,
|
|
||||||
pub name: String,
|
|
||||||
pub top_tracks: Vec<SpotifyId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Metadata for Track {
|
|
||||||
type Message = protocol::metadata::Track;
|
|
||||||
|
|
||||||
async fn request(session: &Session, track_id: SpotifyId) -> MetadataResult {
|
|
||||||
session
|
|
||||||
.spclient()
|
|
||||||
.get_track_metadata(track_id)
|
|
||||||
.await
|
|
||||||
.map_err(MetadataError::Http)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse(msg: &Self::Message, session: &Session) -> Self {
|
|
||||||
debug!("MESSAGE: {:?}", msg);
|
|
||||||
let country = session.country();
|
|
||||||
|
|
||||||
let artists = msg
|
|
||||||
.get_artist()
|
|
||||||
.iter()
|
|
||||||
.filter(|artist| artist.has_gid())
|
|
||||||
.map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let files = msg
|
|
||||||
.get_file()
|
|
||||||
.iter()
|
|
||||||
.filter(|file| file.has_file_id())
|
|
||||||
.map(|file| {
|
|
||||||
let mut dst = [0u8; 20];
|
|
||||||
dst.clone_from_slice(file.get_file_id());
|
|
||||||
(file.get_format(), FileId(dst))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
|
|
||||||
name: msg.get_name().to_owned(),
|
|
||||||
duration: msg.get_duration(),
|
|
||||||
album: SpotifyId::from_raw(msg.get_album().get_gid()).unwrap(),
|
|
||||||
artists,
|
|
||||||
files,
|
|
||||||
alternatives: msg
|
|
||||||
.get_alternative()
|
|
||||||
.iter()
|
|
||||||
.map(|alt| SpotifyId::from_raw(alt.get_gid()).unwrap())
|
|
||||||
.collect(),
|
|
||||||
available: parse_restrictions(msg.get_restriction(), &country, "premium"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Metadata for Album {
|
|
||||||
type Message = protocol::metadata::Album;
|
|
||||||
|
|
||||||
async fn request(session: &Session, album_id: SpotifyId) -> MetadataResult {
|
|
||||||
session
|
|
||||||
.spclient()
|
|
||||||
.get_album_metadata(album_id)
|
|
||||||
.await
|
|
||||||
.map_err(MetadataError::Http)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse(msg: &Self::Message, _: &Session) -> Self {
|
|
||||||
let artists = msg
|
|
||||||
.get_artist()
|
|
||||||
.iter()
|
|
||||||
.filter(|artist| artist.has_gid())
|
|
||||||
.map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let tracks = msg
|
|
||||||
.get_disc()
|
|
||||||
.iter()
|
|
||||||
.flat_map(|disc| disc.get_track())
|
|
||||||
.filter(|track| track.has_gid())
|
|
||||||
.map(|track| SpotifyId::from_raw(track.get_gid()).unwrap())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let covers = msg
|
|
||||||
.get_cover_group()
|
|
||||||
.get_image()
|
|
||||||
.iter()
|
|
||||||
.filter(|image| image.has_file_id())
|
|
||||||
.map(|image| {
|
|
||||||
let mut dst = [0u8; 20];
|
|
||||||
dst.clone_from_slice(image.get_file_id());
|
|
||||||
FileId(dst)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
|
|
||||||
name: msg.get_name().to_owned(),
|
|
||||||
artists,
|
|
||||||
tracks,
|
|
||||||
covers,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Metadata for PlaylistAnnotation {
|
|
||||||
type Message = protocol::playlist_annotate3::PlaylistAnnotation;
|
|
||||||
|
|
||||||
async fn request(session: &Session, playlist_id: SpotifyId) -> MetadataResult {
|
|
||||||
let current_user = session.username();
|
|
||||||
Self::request_for_user(session, current_user, playlist_id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse(msg: &Self::Message, _: &Session) -> Self {
|
|
||||||
let transcoded_pictures = msg
|
|
||||||
.get_transcoded_picture()
|
|
||||||
.iter()
|
|
||||||
.map(|picture| TranscodedPicture {
|
|
||||||
target_name: picture.get_target_name().to_string(),
|
|
||||||
uri: picture.get_uri().to_string(),
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let taken_down = !matches!(
|
|
||||||
msg.get_abuse_report_state(),
|
|
||||||
protocol::playlist_annotate3::AbuseReportState::OK
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
description: msg.get_description().to_string(),
|
|
||||||
picture: msg.get_picture().to_string(),
|
|
||||||
transcoded_pictures,
|
|
||||||
abuse_reporting: msg.get_is_abuse_reporting_enabled(),
|
|
||||||
taken_down,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlaylistAnnotation {
|
|
||||||
async fn request_for_user(
|
|
||||||
session: &Session,
|
|
||||||
username: String,
|
|
||||||
playlist_id: SpotifyId,
|
|
||||||
) -> MetadataResult {
|
|
||||||
let uri = format!(
|
|
||||||
"hm://playlist-annotate/v1/annotation/user/{}/playlist/{}",
|
|
||||||
username,
|
|
||||||
playlist_id.to_base62()
|
|
||||||
);
|
|
||||||
let response = session.mercury().get(uri).await?;
|
|
||||||
match response.payload.first() {
|
|
||||||
Some(data) => Ok(data.to_vec().into()),
|
|
||||||
None => Err(MetadataError::Empty),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
async fn get_for_user(
|
|
||||||
session: &Session,
|
|
||||||
username: String,
|
|
||||||
playlist_id: SpotifyId,
|
|
||||||
) -> Result<Self, MetadataError> {
|
|
||||||
let response = Self::request_for_user(session, username, playlist_id).await?;
|
|
||||||
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
|
|
||||||
Ok(Self::parse(&msg, session))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Metadata for Playlist {
|
|
||||||
type Message = protocol::playlist4_external::SelectedListContent;
|
|
||||||
|
|
||||||
async fn request(session: &Session, playlist_id: SpotifyId) -> MetadataResult {
|
|
||||||
let uri = format!("hm://playlist/v2/playlist/{}", playlist_id.to_base62());
|
|
||||||
let response = session.mercury().get(uri).await?;
|
|
||||||
match response.payload.first() {
|
|
||||||
Some(data) => Ok(data.to_vec().into()),
|
|
||||||
None => Err(MetadataError::Empty),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse(msg: &Self::Message, _: &Session) -> Self {
|
|
||||||
let tracks = msg
|
|
||||||
.get_contents()
|
|
||||||
.get_items()
|
|
||||||
.iter()
|
|
||||||
.map(|item| {
|
|
||||||
let uri_split = item.get_uri().split(':');
|
|
||||||
let uri_parts: Vec<&str> = uri_split.collect();
|
|
||||||
SpotifyId::from_base62(uri_parts[2]).unwrap()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if tracks.len() != msg.get_length() as usize {
|
|
||||||
warn!(
|
|
||||||
"Got {} tracks, but the playlist should contain {} tracks.",
|
|
||||||
tracks.len(),
|
|
||||||
msg.get_length()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
revision: msg.get_revision().to_vec(),
|
|
||||||
name: msg.get_attributes().get_name().to_owned(),
|
|
||||||
tracks,
|
|
||||||
user: msg.get_owner_username().to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Playlist {
|
|
||||||
async fn request_for_user(
|
|
||||||
session: &Session,
|
|
||||||
username: String,
|
|
||||||
playlist_id: SpotifyId,
|
|
||||||
) -> MetadataResult {
|
|
||||||
let uri = format!(
|
|
||||||
"hm://playlist/user/{}/playlist/{}",
|
|
||||||
username,
|
|
||||||
playlist_id.to_base62()
|
|
||||||
);
|
|
||||||
let response = session.mercury().get(uri).await?;
|
|
||||||
match response.payload.first() {
|
|
||||||
Some(data) => Ok(data.to_vec().into()),
|
|
||||||
None => Err(MetadataError::Empty),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn request_root_for_user(session: &Session, username: String) -> MetadataResult {
|
|
||||||
let uri = format!("hm://playlist/user/{}/rootlist", username);
|
|
||||||
let response = session.mercury().get(uri).await?;
|
|
||||||
match response.payload.first() {
|
|
||||||
Some(data) => Ok(data.to_vec().into()),
|
|
||||||
None => Err(MetadataError::Empty),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[allow(dead_code)]
|
|
||||||
async fn get_for_user(
|
|
||||||
session: &Session,
|
|
||||||
username: String,
|
|
||||||
playlist_id: SpotifyId,
|
|
||||||
) -> Result<Self, MetadataError> {
|
|
||||||
let response = Self::request_for_user(session, username, playlist_id).await?;
|
|
||||||
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
|
|
||||||
Ok(Self::parse(&msg, session))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
async fn get_root_for_user(session: &Session, username: String) -> Result<Self, MetadataError> {
|
|
||||||
let response = Self::request_root_for_user(session, username).await?;
|
|
||||||
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
|
|
||||||
Ok(Self::parse(&msg, session))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Metadata for Artist {
|
|
||||||
type Message = protocol::metadata::Artist;
|
|
||||||
|
|
||||||
async fn request(session: &Session, artist_id: SpotifyId) -> MetadataResult {
|
|
||||||
session
|
|
||||||
.spclient()
|
|
||||||
.get_artist_metadata(artist_id)
|
|
||||||
.await
|
|
||||||
.map_err(MetadataError::Http)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse(msg: &Self::Message, session: &Session) -> Self {
|
|
||||||
let country = session.country();
|
|
||||||
|
|
||||||
let top_tracks: Vec<SpotifyId> = match msg
|
|
||||||
.get_top_track()
|
|
||||||
.iter()
|
|
||||||
.find(|tt| !tt.has_country() || countrylist_contains(tt.get_country(), &country))
|
|
||||||
{
|
|
||||||
Some(tracks) => tracks
|
|
||||||
.get_track()
|
|
||||||
.iter()
|
|
||||||
.filter(|track| track.has_gid())
|
|
||||||
.map(|track| SpotifyId::from_raw(track.get_gid()).unwrap())
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
None => Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
|
|
||||||
name: msg.get_name().to_owned(),
|
|
||||||
top_tracks,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Podcast
|
|
||||||
#[async_trait]
|
|
||||||
impl Metadata for Episode {
|
|
||||||
type Message = protocol::metadata::Episode;
|
|
||||||
|
|
||||||
async fn request(session: &Session, episode_id: SpotifyId) -> MetadataResult {
|
|
||||||
session
|
|
||||||
.spclient()
|
|
||||||
.get_album_metadata(episode_id)
|
|
||||||
.await
|
|
||||||
.map_err(MetadataError::Http)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse(msg: &Self::Message, session: &Session) -> Self {
|
|
||||||
let country = session.country();
|
|
||||||
|
|
||||||
let files = msg
|
|
||||||
.get_audio()
|
|
||||||
.iter()
|
|
||||||
.filter(|file| file.has_file_id())
|
|
||||||
.map(|file| {
|
|
||||||
let mut dst = [0u8; 20];
|
|
||||||
dst.clone_from_slice(file.get_file_id());
|
|
||||||
(file.get_format(), FileId(dst))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let covers = msg
|
|
||||||
.get_cover_image()
|
|
||||||
.get_image()
|
|
||||||
.iter()
|
|
||||||
.filter(|image| image.has_file_id())
|
|
||||||
.map(|image| {
|
|
||||||
let mut dst = [0u8; 20];
|
|
||||||
dst.clone_from_slice(image.get_file_id());
|
|
||||||
FileId(dst)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
|
|
||||||
name: msg.get_name().to_owned(),
|
|
||||||
external_url: msg.get_external_url().to_owned(),
|
|
||||||
duration: msg.get_duration().to_owned(),
|
|
||||||
language: msg.get_language().to_owned(),
|
|
||||||
show: SpotifyId::from_raw(msg.get_show().get_gid()).unwrap(),
|
|
||||||
covers,
|
|
||||||
files,
|
|
||||||
available: parse_restrictions(msg.get_restriction(), &country, "premium"),
|
|
||||||
explicit: msg.get_explicit().to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Metadata for Show {
|
|
||||||
type Message = protocol::metadata::Show;
|
|
||||||
|
|
||||||
async fn request(session: &Session, show_id: SpotifyId) -> MetadataResult {
|
|
||||||
session
|
|
||||||
.spclient()
|
|
||||||
.get_show_metadata(show_id)
|
|
||||||
.await
|
|
||||||
.map_err(MetadataError::Http)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse(msg: &Self::Message, _: &Session) -> Self {
|
|
||||||
let episodes = msg
|
|
||||||
.get_episode()
|
|
||||||
.iter()
|
|
||||||
.filter(|episode| episode.has_gid())
|
|
||||||
.map(|episode| SpotifyId::from_raw(episode.get_gid()).unwrap())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let covers = msg
|
|
||||||
.get_cover_image()
|
|
||||||
.get_image()
|
|
||||||
.iter()
|
|
||||||
.filter(|image| image.has_file_id())
|
|
||||||
.map(|image| {
|
|
||||||
let mut dst = [0u8; 20];
|
|
||||||
dst.clone_from_slice(image.get_file_id());
|
|
||||||
FileId(dst)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
|
|
||||||
name: msg.get_name().to_owned(),
|
|
||||||
publisher: msg.get_publisher().to_owned(),
|
|
||||||
episodes,
|
|
||||||
covers,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct StrChunks<'s>(&'s str, usize);
|
|
||||||
|
|
||||||
trait StrChunksExt {
|
|
||||||
fn chunks(&self, size: usize) -> StrChunks;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StrChunksExt for str {
|
|
||||||
fn chunks(&self, size: usize) -> StrChunks {
|
|
||||||
StrChunks(self, size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'s> Iterator for StrChunks<'s> {
|
|
||||||
type Item = &'s str;
|
|
||||||
fn next(&mut self) -> Option<&'s str> {
|
|
||||||
let &mut StrChunks(data, size) = self;
|
|
||||||
if data.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let ret = Some(&data[..size]);
|
|
||||||
self.0 = &data[size..];
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse(msg: &Self::Message, _: SpotifyId) -> Result<Self, MetadataError>;
|
||||||
}
|
}
|
||||||
|
|
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::spotify_id::SpotifyId;
|
||||||
use crate::core::util::SeqGenerator;
|
use crate::core::util::SeqGenerator;
|
||||||
use crate::decoder::{AudioDecoder, AudioError, AudioPacket, PassthroughDecoder, VorbisDecoder};
|
use crate::decoder::{AudioDecoder, AudioError, AudioPacket, PassthroughDecoder, VorbisDecoder};
|
||||||
use crate::metadata::{AudioItem, FileFormat};
|
use crate::metadata::audio::{AudioFileFormat, AudioItem};
|
||||||
use crate::mixer::AudioFilter;
|
use crate::mixer::AudioFilter;
|
||||||
|
|
||||||
use crate::{NUM_CHANNELS, SAMPLES_PER_SECOND};
|
use crate::{NUM_CHANNELS, SAMPLES_PER_SECOND};
|
||||||
|
@ -639,17 +639,17 @@ struct PlayerTrackLoader {
|
||||||
|
|
||||||
impl PlayerTrackLoader {
|
impl PlayerTrackLoader {
|
||||||
async fn find_available_alternative(&self, audio: AudioItem) -> Option<AudioItem> {
|
async fn find_available_alternative(&self, audio: AudioItem) -> Option<AudioItem> {
|
||||||
if audio.available {
|
if audio.availability.is_ok() {
|
||||||
Some(audio)
|
Some(audio)
|
||||||
} else if let Some(alternatives) = &audio.alternatives {
|
} else if let Some(alternatives) = &audio.alternatives {
|
||||||
let alternatives: FuturesUnordered<_> = alternatives
|
let alternatives: FuturesUnordered<_> = alternatives
|
||||||
.iter()
|
.iter()
|
||||||
.map(|alt_id| AudioItem::get_audio_item(&self.session, *alt_id))
|
.map(|alt_id| AudioItem::get_file(&self.session, *alt_id))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
alternatives
|
alternatives
|
||||||
.filter_map(|x| future::ready(x.ok()))
|
.filter_map(|x| future::ready(x.ok()))
|
||||||
.filter(|x| future::ready(x.available))
|
.filter(|x| future::ready(x.availability.is_ok()))
|
||||||
.next()
|
.next()
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
|
@ -657,19 +657,19 @@ impl PlayerTrackLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stream_data_rate(&self, format: FileFormat) -> usize {
|
fn stream_data_rate(&self, format: AudioFileFormat) -> usize {
|
||||||
match format {
|
match format {
|
||||||
FileFormat::OGG_VORBIS_96 => 12 * 1024,
|
AudioFileFormat::OGG_VORBIS_96 => 12 * 1024,
|
||||||
FileFormat::OGG_VORBIS_160 => 20 * 1024,
|
AudioFileFormat::OGG_VORBIS_160 => 20 * 1024,
|
||||||
FileFormat::OGG_VORBIS_320 => 40 * 1024,
|
AudioFileFormat::OGG_VORBIS_320 => 40 * 1024,
|
||||||
FileFormat::MP3_256 => 32 * 1024,
|
AudioFileFormat::MP3_256 => 32 * 1024,
|
||||||
FileFormat::MP3_320 => 40 * 1024,
|
AudioFileFormat::MP3_320 => 40 * 1024,
|
||||||
FileFormat::MP3_160 => 20 * 1024,
|
AudioFileFormat::MP3_160 => 20 * 1024,
|
||||||
FileFormat::MP3_96 => 12 * 1024,
|
AudioFileFormat::MP3_96 => 12 * 1024,
|
||||||
FileFormat::MP3_160_ENC => 20 * 1024,
|
AudioFileFormat::MP3_160_ENC => 20 * 1024,
|
||||||
FileFormat::AAC_24 => 3 * 1024,
|
AudioFileFormat::AAC_24 => 3 * 1024,
|
||||||
FileFormat::AAC_48 => 6 * 1024,
|
AudioFileFormat::AAC_48 => 6 * 1024,
|
||||||
FileFormat::FLAC_FLAC => 112 * 1024, // assume 900 kbps on average
|
AudioFileFormat::FLAC_FLAC => 112 * 1024, // assume 900 kbps on average
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -678,7 +678,7 @@ impl PlayerTrackLoader {
|
||||||
spotify_id: SpotifyId,
|
spotify_id: SpotifyId,
|
||||||
position_ms: u32,
|
position_ms: u32,
|
||||||
) -> Option<PlayerLoadedTrackData> {
|
) -> Option<PlayerLoadedTrackData> {
|
||||||
let audio = match AudioItem::get_audio_item(&self.session, spotify_id).await {
|
let audio = match AudioItem::get_file(&self.session, spotify_id).await {
|
||||||
Ok(audio) => audio,
|
Ok(audio) => audio,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
error!("Unable to load audio item.");
|
error!("Unable to load audio item.");
|
||||||
|
@ -686,7 +686,10 @@ impl PlayerTrackLoader {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("Loading <{}> with Spotify URI <{}>", audio.name, audio.uri);
|
info!(
|
||||||
|
"Loading <{}> with Spotify URI <{}>",
|
||||||
|
audio.name, audio.spotify_uri
|
||||||
|
);
|
||||||
|
|
||||||
let audio = match self.find_available_alternative(audio).await {
|
let audio = match self.find_available_alternative(audio).await {
|
||||||
Some(audio) => audio,
|
Some(audio) => audio,
|
||||||
|
@ -699,22 +702,23 @@ impl PlayerTrackLoader {
|
||||||
assert!(audio.duration >= 0);
|
assert!(audio.duration >= 0);
|
||||||
let duration_ms = audio.duration as u32;
|
let duration_ms = audio.duration as u32;
|
||||||
|
|
||||||
// (Most) podcasts seem to support only 96 bit Vorbis, so fall back to it
|
// (Most) podcasts seem to support only 96 kbps Vorbis, so fall back to it
|
||||||
|
// TODO: update this logic once we also support MP3 and/or FLAC
|
||||||
let formats = match self.config.bitrate {
|
let formats = match self.config.bitrate {
|
||||||
Bitrate::Bitrate96 => [
|
Bitrate::Bitrate96 => [
|
||||||
FileFormat::OGG_VORBIS_96,
|
AudioFileFormat::OGG_VORBIS_96,
|
||||||
FileFormat::OGG_VORBIS_160,
|
AudioFileFormat::OGG_VORBIS_160,
|
||||||
FileFormat::OGG_VORBIS_320,
|
AudioFileFormat::OGG_VORBIS_320,
|
||||||
],
|
],
|
||||||
Bitrate::Bitrate160 => [
|
Bitrate::Bitrate160 => [
|
||||||
FileFormat::OGG_VORBIS_160,
|
AudioFileFormat::OGG_VORBIS_160,
|
||||||
FileFormat::OGG_VORBIS_96,
|
AudioFileFormat::OGG_VORBIS_96,
|
||||||
FileFormat::OGG_VORBIS_320,
|
AudioFileFormat::OGG_VORBIS_320,
|
||||||
],
|
],
|
||||||
Bitrate::Bitrate320 => [
|
Bitrate::Bitrate320 => [
|
||||||
FileFormat::OGG_VORBIS_320,
|
AudioFileFormat::OGG_VORBIS_320,
|
||||||
FileFormat::OGG_VORBIS_160,
|
AudioFileFormat::OGG_VORBIS_160,
|
||||||
FileFormat::OGG_VORBIS_96,
|
AudioFileFormat::OGG_VORBIS_96,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue