2019-09-22 19:21:44 +00:00
|
|
|
#[macro_use]
|
|
|
|
extern crate log;
|
|
|
|
|
2017-08-03 19:10:48 +00:00
|
|
|
extern crate byteorder;
|
|
|
|
extern crate futures;
|
|
|
|
extern crate linear_map;
|
|
|
|
extern crate protobuf;
|
|
|
|
|
2019-09-16 19:00:09 +00:00
|
|
|
extern crate librespot_core;
|
2017-08-03 19:37:04 +00:00
|
|
|
extern crate librespot_protocol as protocol;
|
|
|
|
|
2017-08-03 19:10:48 +00:00
|
|
|
pub mod cover;
|
|
|
|
|
2019-10-09 17:49:13 +00:00
|
|
|
use futures::future;
|
2018-01-21 19:29:31 +00:00
|
|
|
use futures::Future;
|
2016-05-04 09:03:46 +00:00
|
|
|
use linear_map::LinearMap;
|
2015-06-23 14:38:29 +00:00
|
|
|
|
2019-09-16 19:00:09 +00:00
|
|
|
use librespot_core::mercury::MercuryError;
|
|
|
|
use librespot_core::session::Session;
|
2019-10-07 22:27:26 +00:00
|
|
|
use librespot_core::spotify_id::{FileId, SpotifyAudioType, SpotifyId};
|
2015-06-23 14:38:29 +00:00
|
|
|
|
2019-10-08 09:31:18 +00:00
|
|
|
pub use crate::protocol::metadata::AudioFile_Format as FileFormat;
|
2016-01-02 02:30:24 +00:00
|
|
|
|
2015-12-28 15:55:01 +00:00
|
|
|
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
|
2018-02-12 20:02:27 +00:00
|
|
|
where
|
|
|
|
I: IntoIterator<Item = &'s protocol::metadata::Restriction>,
|
2016-01-02 15:19:39 +00:00
|
|
|
{
|
2017-06-01 10:04:22 +00:00
|
|
|
let mut forbidden = "".to_string();
|
2017-06-03 16:55:30 +00:00
|
|
|
let mut has_forbidden = false;
|
|
|
|
|
2017-06-01 10:04:22 +00:00
|
|
|
let mut allowed = "".to_string();
|
2017-06-03 16:55:30 +00:00
|
|
|
let mut has_allowed = false;
|
|
|
|
|
2018-02-12 20:02:27 +00:00
|
|
|
let rs = restrictions
|
|
|
|
.into_iter()
|
|
|
|
.filter(|r| r.get_catalogue_str().contains(&catalogue.to_owned()));
|
2017-06-01 10:04:22 +00:00
|
|
|
|
|
|
|
for r in rs {
|
2017-06-03 16:55:30 +00:00
|
|
|
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;
|
|
|
|
}
|
2017-06-01 10:04:22 +00:00
|
|
|
}
|
|
|
|
|
2018-02-12 20:02:27 +00:00
|
|
|
(has_forbidden || has_allowed)
|
|
|
|
&& (!has_forbidden || !countrylist_contains(forbidden.as_str(), country))
|
|
|
|
&& (!has_allowed || countrylist_contains(allowed.as_str(), country))
|
2015-12-28 15:55:01 +00:00
|
|
|
}
|
|
|
|
|
2019-10-07 22:27:26 +00:00
|
|
|
// A wrapper with fields the player needs
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
pub struct AudioItem {
|
|
|
|
pub id: SpotifyId,
|
|
|
|
pub uri: String,
|
|
|
|
pub files: LinearMap<FileFormat, FileId>,
|
|
|
|
pub name: String,
|
|
|
|
pub available: bool,
|
|
|
|
pub alternatives: Option<Vec<SpotifyId>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl AudioItem {
|
|
|
|
pub fn get_audio_item(
|
|
|
|
session: &Session,
|
|
|
|
id: SpotifyId,
|
|
|
|
) -> Box<dyn Future<Item = AudioItem, Error = MercuryError>> {
|
|
|
|
match id.audio_type {
|
|
|
|
SpotifyAudioType::Track => Track::get_audio_item(session, id),
|
|
|
|
SpotifyAudioType::Podcast => Episode::get_audio_item(session, id),
|
2019-10-09 17:49:13 +00:00
|
|
|
SpotifyAudioType::NonPlayable => {
|
|
|
|
Box::new(future::err::<AudioItem, MercuryError>(MercuryError))
|
|
|
|
}
|
2019-10-07 22:27:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
trait AudioFiles {
|
|
|
|
fn get_audio_item(
|
|
|
|
session: &Session,
|
|
|
|
id: SpotifyId,
|
|
|
|
) -> Box<dyn Future<Item = AudioItem, Error = MercuryError>>;
|
|
|
|
}
|
|
|
|
|
|
|
|
impl AudioFiles for Track {
|
|
|
|
fn get_audio_item(
|
|
|
|
session: &Session,
|
|
|
|
id: SpotifyId,
|
|
|
|
) -> Box<dyn Future<Item = AudioItem, Error = MercuryError>> {
|
|
|
|
Box::new(Self::get(session, id).and_then(move |item| {
|
|
|
|
Ok(AudioItem {
|
|
|
|
id: id,
|
|
|
|
uri: format!("spotify:track:{}", id.to_base62()),
|
|
|
|
files: item.files,
|
|
|
|
name: item.name,
|
|
|
|
available: item.available,
|
|
|
|
alternatives: Some(item.alternatives),
|
|
|
|
})
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl AudioFiles for Episode {
|
|
|
|
fn get_audio_item(
|
|
|
|
session: &Session,
|
|
|
|
id: SpotifyId,
|
|
|
|
) -> Box<dyn Future<Item = AudioItem, Error = MercuryError>> {
|
|
|
|
Box::new(Self::get(session, id).and_then(move |item| {
|
|
|
|
Ok(AudioItem {
|
|
|
|
id: id,
|
|
|
|
uri: format!("spotify:episode:{}", id.to_base62()),
|
|
|
|
files: item.files,
|
|
|
|
name: item.name,
|
|
|
|
available: item.available,
|
|
|
|
alternatives: None,
|
|
|
|
})
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
}
|
2018-02-12 20:02:27 +00:00
|
|
|
pub trait Metadata: Send + Sized + 'static {
|
2018-09-28 10:02:38 +00:00
|
|
|
type Message: protobuf::Message;
|
2015-12-28 15:53:54 +00:00
|
|
|
|
2019-08-06 21:16:30 +00:00
|
|
|
fn request_url(id: SpotifyId) -> String;
|
2015-12-29 12:13:26 +00:00
|
|
|
fn parse(msg: &Self::Message, session: &Session) -> Self;
|
2017-05-16 00:04:10 +00:00
|
|
|
|
2019-10-07 22:27:26 +00:00
|
|
|
fn get(session: &Session, id: SpotifyId) -> Box<dyn Future<Item = Self, Error = MercuryError>> {
|
2019-12-16 11:47:52 +00:00
|
|
|
let uri = Self::request_url(id);
|
2017-05-16 00:04:10 +00:00
|
|
|
let request = session.mercury().get(uri);
|
|
|
|
|
|
|
|
let session = session.clone();
|
2018-01-21 19:29:31 +00:00
|
|
|
Box::new(request.and_then(move |response| {
|
2017-05-16 00:04:10 +00:00
|
|
|
let data = response.payload.first().expect("Empty payload");
|
|
|
|
let msg: Self::Message = protobuf::parse_from_bytes(data).unwrap();
|
|
|
|
|
|
|
|
Ok(Self::parse(&msg, &session))
|
2018-01-21 19:29:31 +00:00
|
|
|
}))
|
2017-05-16 00:04:10 +00:00
|
|
|
}
|
2015-06-23 14:38:29 +00:00
|
|
|
}
|
|
|
|
|
2016-05-04 08:11:03 +00:00
|
|
|
#[derive(Debug, Clone)]
|
2015-06-23 14:38:29 +00:00
|
|
|
pub struct Track {
|
2015-12-28 15:53:54 +00:00
|
|
|
pub id: SpotifyId,
|
2015-06-23 14:38:29 +00:00
|
|
|
pub name: String,
|
2018-02-06 02:20:21 +00:00
|
|
|
pub duration: i32,
|
2015-06-23 14:38:29 +00:00
|
|
|
pub album: SpotifyId,
|
2016-01-25 09:44:48 +00:00
|
|
|
pub artists: Vec<SpotifyId>,
|
2016-05-04 09:03:46 +00:00
|
|
|
pub files: LinearMap<FileFormat, FileId>,
|
2015-12-28 15:55:01 +00:00
|
|
|
pub alternatives: Vec<SpotifyId>,
|
|
|
|
pub available: bool,
|
2015-12-28 15:53:54 +00:00
|
|
|
}
|
|
|
|
|
2016-05-04 08:11:03 +00:00
|
|
|
#[derive(Debug, Clone)]
|
2015-12-28 15:53:54 +00:00
|
|
|
pub struct Album {
|
|
|
|
pub id: SpotifyId,
|
|
|
|
pub name: String,
|
|
|
|
pub artists: Vec<SpotifyId>,
|
2016-01-25 09:44:48 +00:00
|
|
|
pub tracks: Vec<SpotifyId>,
|
2016-01-02 15:19:39 +00:00
|
|
|
pub covers: Vec<FileId>,
|
2015-06-23 14:38:29 +00:00
|
|
|
}
|
|
|
|
|
2018-09-28 18:10:22 +00:00
|
|
|
#[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: LinearMap<FileFormat, FileId>,
|
|
|
|
pub covers: Vec<FileId>,
|
|
|
|
pub available: bool,
|
|
|
|
pub explicit: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
pub struct Show {
|
|
|
|
pub id: SpotifyId,
|
|
|
|
pub name: String,
|
2019-10-07 22:27:26 +00:00
|
|
|
pub publisher: String,
|
2018-09-28 18:10:22 +00:00
|
|
|
pub episodes: Vec<SpotifyId>,
|
|
|
|
pub covers: Vec<FileId>,
|
|
|
|
}
|
|
|
|
|
2019-08-05 21:42:16 +00:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
pub struct Playlist {
|
2019-09-22 19:21:44 +00:00
|
|
|
pub revision: Vec<u8>,
|
2019-08-06 19:58:50 +00:00
|
|
|
pub user: String,
|
2019-08-05 21:42:16 +00:00
|
|
|
pub name: String,
|
|
|
|
pub tracks: Vec<SpotifyId>,
|
|
|
|
}
|
|
|
|
|
2016-05-04 08:11:03 +00:00
|
|
|
#[derive(Debug, Clone)]
|
2015-12-28 15:53:54 +00:00
|
|
|
pub struct Artist {
|
|
|
|
pub id: SpotifyId,
|
|
|
|
pub name: String,
|
2016-01-25 09:44:48 +00:00
|
|
|
pub top_tracks: Vec<SpotifyId>,
|
2015-12-28 15:53:54 +00:00
|
|
|
}
|
|
|
|
|
2017-05-16 00:04:10 +00:00
|
|
|
impl Metadata for Track {
|
2015-06-23 14:38:29 +00:00
|
|
|
type Message = protocol::metadata::Track;
|
2015-12-28 15:53:54 +00:00
|
|
|
|
2019-08-06 21:16:30 +00:00
|
|
|
fn request_url(id: SpotifyId) -> String {
|
|
|
|
format!("hm://metadata/3/track/{}", id.to_base16())
|
2015-12-28 15:53:54 +00:00
|
|
|
}
|
|
|
|
|
2015-12-29 12:13:26 +00:00
|
|
|
fn parse(msg: &Self::Message, session: &Session) -> Self {
|
2016-01-26 22:34:57 +00:00
|
|
|
let country = session.country();
|
2016-01-25 09:44:48 +00:00
|
|
|
|
2018-07-03 11:08:42 +00:00
|
|
|
let artists = msg
|
|
|
|
.get_artist()
|
2018-02-12 20:02:27 +00:00
|
|
|
.iter()
|
|
|
|
.filter(|artist| artist.has_gid())
|
2018-02-25 02:04:07 +00:00
|
|
|
.map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap())
|
2018-02-12 20:02:27 +00:00
|
|
|
.collect::<Vec<_>>();
|
2016-01-25 09:44:48 +00:00
|
|
|
|
2018-07-03 11:08:42 +00:00
|
|
|
let files = msg
|
|
|
|
.get_file()
|
2018-02-12 20:02:27 +00:00
|
|
|
.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();
|
2016-01-25 09:44:48 +00:00
|
|
|
|
2015-06-23 14:38:29 +00:00
|
|
|
Track {
|
2018-02-25 02:04:07 +00:00
|
|
|
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
|
2015-09-01 11:20:37 +00:00
|
|
|
name: msg.get_name().to_owned(),
|
2018-02-06 02:20:21 +00:00
|
|
|
duration: msg.get_duration(),
|
2018-02-25 02:04:07 +00:00
|
|
|
album: SpotifyId::from_raw(msg.get_album().get_gid()).unwrap(),
|
2016-01-25 09:44:48 +00:00
|
|
|
artists: artists,
|
|
|
|
files: files,
|
2018-07-03 11:08:42 +00:00
|
|
|
alternatives: msg
|
|
|
|
.get_alternative()
|
2018-02-12 20:02:27 +00:00
|
|
|
.iter()
|
2018-02-25 02:04:07 +00:00
|
|
|
.map(|alt| SpotifyId::from_raw(alt.get_gid()).unwrap())
|
2018-02-12 20:02:27 +00:00
|
|
|
.collect(),
|
|
|
|
available: parse_restrictions(msg.get_restriction(), &country, "premium"),
|
2015-06-23 14:38:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-16 00:04:10 +00:00
|
|
|
impl Metadata for Album {
|
2015-06-23 14:38:29 +00:00
|
|
|
type Message = protocol::metadata::Album;
|
2015-12-28 15:53:54 +00:00
|
|
|
|
2019-08-06 21:16:30 +00:00
|
|
|
fn request_url(id: SpotifyId) -> String {
|
|
|
|
format!("hm://metadata/3/album/{}", id.to_base16())
|
2015-12-28 15:53:54 +00:00
|
|
|
}
|
|
|
|
|
2015-12-29 22:12:02 +00:00
|
|
|
fn parse(msg: &Self::Message, _: &Session) -> Self {
|
2018-07-03 11:08:42 +00:00
|
|
|
let artists = msg
|
|
|
|
.get_artist()
|
2018-02-12 20:02:27 +00:00
|
|
|
.iter()
|
|
|
|
.filter(|artist| artist.has_gid())
|
2018-02-25 02:04:07 +00:00
|
|
|
.map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap())
|
2018-02-12 20:02:27 +00:00
|
|
|
.collect::<Vec<_>>();
|
2016-01-25 09:44:48 +00:00
|
|
|
|
2018-07-03 11:08:42 +00:00
|
|
|
let tracks = msg
|
|
|
|
.get_disc()
|
2018-02-12 20:02:27 +00:00
|
|
|
.iter()
|
|
|
|
.flat_map(|disc| disc.get_track())
|
|
|
|
.filter(|track| track.has_gid())
|
2018-02-25 02:04:07 +00:00
|
|
|
.map(|track| SpotifyId::from_raw(track.get_gid()).unwrap())
|
2018-02-12 20:02:27 +00:00
|
|
|
.collect::<Vec<_>>();
|
2016-01-25 09:44:48 +00:00
|
|
|
|
2018-07-03 11:08:42 +00:00
|
|
|
let covers = msg
|
|
|
|
.get_cover_group()
|
2018-02-12 20:02:27 +00:00
|
|
|
.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<_>>();
|
2016-01-25 09:44:48 +00:00
|
|
|
|
2015-06-23 14:38:29 +00:00
|
|
|
Album {
|
2018-02-25 02:04:07 +00:00
|
|
|
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
|
2015-09-01 11:20:37 +00:00
|
|
|
name: msg.get_name().to_owned(),
|
2016-01-25 09:44:48 +00:00
|
|
|
artists: artists,
|
|
|
|
tracks: tracks,
|
|
|
|
covers: covers,
|
2015-06-23 14:38:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-06 19:58:50 +00:00
|
|
|
impl Metadata for Playlist {
|
2019-08-05 21:42:16 +00:00
|
|
|
type Message = protocol::playlist4changes::SelectedListContent;
|
|
|
|
|
2019-08-06 21:16:30 +00:00
|
|
|
fn request_url(id: SpotifyId) -> String {
|
|
|
|
format!("hm://playlist/v2/playlist/{}", id.to_base62())
|
2019-08-06 19:58:50 +00:00
|
|
|
}
|
2019-08-05 21:42:16 +00:00
|
|
|
|
2019-08-05 21:42:16 +00:00
|
|
|
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<_>>();
|
2020-01-17 17:11:07 +00:00
|
|
|
|
2019-09-22 19:21:44 +00:00
|
|
|
if tracks.len() != msg.get_length() as usize {
|
2020-01-17 17:11:07 +00:00
|
|
|
warn!(
|
|
|
|
"Got {} tracks, but the playlist should contain {} tracks.",
|
|
|
|
tracks.len(),
|
|
|
|
msg.get_length()
|
|
|
|
);
|
2019-09-22 19:21:44 +00:00
|
|
|
}
|
|
|
|
|
2019-08-05 21:42:16 +00:00
|
|
|
Playlist {
|
2019-09-22 19:21:44 +00:00
|
|
|
revision: msg.get_revision().to_vec(),
|
2019-08-05 21:42:16 +00:00
|
|
|
name: msg.get_attributes().get_name().to_owned(),
|
|
|
|
tracks: tracks,
|
2019-08-06 19:58:50 +00:00
|
|
|
user: msg.get_owner_username().to_string(),
|
2019-08-05 21:42:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-16 00:04:10 +00:00
|
|
|
impl Metadata for Artist {
|
2015-06-23 14:38:29 +00:00
|
|
|
type Message = protocol::metadata::Artist;
|
2015-12-28 15:53:54 +00:00
|
|
|
|
2019-08-06 21:16:30 +00:00
|
|
|
fn request_url(id: SpotifyId) -> String {
|
|
|
|
format!("hm://metadata/3/artist/{}", id.to_base16())
|
2015-06-23 14:38:29 +00:00
|
|
|
}
|
|
|
|
|
2016-01-25 09:44:48 +00:00
|
|
|
fn parse(msg: &Self::Message, session: &Session) -> Self {
|
2016-01-26 22:34:57 +00:00
|
|
|
let country = session.country();
|
2016-01-25 09:44:48 +00:00
|
|
|
|
2018-07-03 11:08:42 +00:00
|
|
|
let top_tracks: Vec<SpotifyId> = match msg
|
|
|
|
.get_top_track()
|
2018-02-12 20:02:27 +00:00
|
|
|
.iter()
|
|
|
|
.find(|tt| !tt.has_country() || countrylist_contains(tt.get_country(), &country))
|
|
|
|
{
|
|
|
|
Some(tracks) => tracks
|
|
|
|
.get_track()
|
|
|
|
.iter()
|
|
|
|
.filter(|track| track.has_gid())
|
2018-02-25 02:04:07 +00:00
|
|
|
.map(|track| SpotifyId::from_raw(track.get_gid()).unwrap())
|
2018-02-12 20:02:27 +00:00
|
|
|
.collect::<Vec<_>>(),
|
|
|
|
None => Vec::new(),
|
|
|
|
};
|
2016-01-25 09:44:48 +00:00
|
|
|
|
2015-12-28 15:53:54 +00:00
|
|
|
Artist {
|
2018-02-25 02:04:07 +00:00
|
|
|
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
|
2015-12-28 15:53:54 +00:00
|
|
|
name: msg.get_name().to_owned(),
|
2018-02-12 20:02:27 +00:00
|
|
|
top_tracks: top_tracks,
|
2015-06-23 14:38:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-02-13 07:33:50 +00:00
|
|
|
|
2018-09-28 18:10:22 +00:00
|
|
|
// Podcast
|
|
|
|
impl Metadata for Episode {
|
|
|
|
type Message = protocol::metadata::Episode;
|
|
|
|
|
2019-12-16 11:47:52 +00:00
|
|
|
fn request_url(id: SpotifyId) -> String {
|
|
|
|
format!("hm://metadata/3/episode/{}", id.to_base16())
|
2018-09-28 18:10:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn parse(msg: &Self::Message, session: &Session) -> Self {
|
|
|
|
let country = session.country();
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
let covers = msg
|
|
|
|
.get_covers()
|
|
|
|
.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<_>>();
|
|
|
|
|
|
|
|
Episode {
|
|
|
|
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: covers,
|
|
|
|
files: files,
|
|
|
|
available: parse_restrictions(msg.get_restriction(), &country, "premium"),
|
|
|
|
explicit: msg.get_explicit().to_owned(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Metadata for Show {
|
|
|
|
type Message = protocol::metadata::Show;
|
|
|
|
|
2019-12-16 11:47:52 +00:00
|
|
|
fn request_url(id: SpotifyId) -> String {
|
|
|
|
format!("hm://metadata/3/show/{}", id.to_base16())
|
2018-09-28 18:10:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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_covers()
|
|
|
|
.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<_>>();
|
|
|
|
|
|
|
|
Show {
|
|
|
|
id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
|
|
|
|
name: msg.get_name().to_owned(),
|
2019-10-07 22:27:26 +00:00
|
|
|
publisher: msg.get_publisher().to_owned(),
|
2018-09-28 18:10:22 +00:00
|
|
|
episodes: episodes,
|
|
|
|
covers: covers,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-13 07:33:50 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|