mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
commit
4a04e48f8a
6 changed files with 3993 additions and 107 deletions
|
@ -13,14 +13,14 @@ use context::StationContext;
|
||||||
use librespot_core::config::ConnectConfig;
|
use librespot_core::config::ConnectConfig;
|
||||||
use librespot_core::mercury::MercuryError;
|
use librespot_core::mercury::MercuryError;
|
||||||
use librespot_core::session::Session;
|
use librespot_core::session::Session;
|
||||||
use librespot_core::spotify_id::SpotifyId;
|
use librespot_core::spotify_id::{SpotifyAudioType, SpotifyId, SpotifyIdError};
|
||||||
use librespot_core::util::SeqGenerator;
|
use librespot_core::util::SeqGenerator;
|
||||||
use librespot_core::version;
|
use librespot_core::version;
|
||||||
use librespot_core::volume::Volume;
|
use librespot_core::volume::Volume;
|
||||||
use playback::mixer::Mixer;
|
use playback::mixer::Mixer;
|
||||||
use playback::player::Player;
|
use playback::player::Player;
|
||||||
use protocol;
|
use protocol;
|
||||||
use protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State};
|
use protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State, TrackRef};
|
||||||
|
|
||||||
pub struct SpircTask {
|
pub struct SpircTask {
|
||||||
player: Player,
|
player: Player,
|
||||||
|
@ -168,6 +168,7 @@ fn initial_device_state(config: ConnectConfig) -> DeviceState {
|
||||||
let repeated = msg.mut_stringValue();
|
let repeated = msg.mut_stringValue();
|
||||||
repeated.push(::std::convert::Into::into("audio/local"));
|
repeated.push(::std::convert::Into::into("audio/local"));
|
||||||
repeated.push(::std::convert::Into::into("audio/track"));
|
repeated.push(::std::convert::Into::into("audio/track"));
|
||||||
|
repeated.push(::std::convert::Into::into("audio/episode"));
|
||||||
repeated.push(::std::convert::Into::into("local"));
|
repeated.push(::std::convert::Into::into("local"));
|
||||||
repeated.push(::std::convert::Into::into("track"))
|
repeated.push(::std::convert::Into::into("track"))
|
||||||
};
|
};
|
||||||
|
@ -796,6 +797,7 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_tracks(&mut self, frame: &protocol::spirc::Frame) {
|
fn update_tracks(&mut self, frame: &protocol::spirc::Frame) {
|
||||||
|
debug!("State: {:?}", frame.get_state());
|
||||||
let index = frame.get_state().get_playing_track_index();
|
let index = frame.get_state().get_playing_track_index();
|
||||||
let context_uri = frame.get_state().get_context_uri().to_owned();
|
let context_uri = frame.get_state().get_context_uri().to_owned();
|
||||||
let tracks = frame.get_state().get_track();
|
let tracks = frame.get_state().get_track();
|
||||||
|
@ -811,24 +813,50 @@ impl SpircTask {
|
||||||
self.state.set_shuffle(frame.get_state().get_shuffle());
|
self.state.set_shuffle(frame.get_state().get_shuffle());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn load_track(&mut self, play: bool) {
|
fn load_track(&mut self, play: bool) {
|
||||||
|
let context_uri = self.state.get_context_uri().to_owned();
|
||||||
|
let mut index = self.state.get_playing_track_index();
|
||||||
|
let start_index = index;
|
||||||
|
let tracks_len = self.state.get_track().len() as u32;
|
||||||
|
debug!(
|
||||||
|
"Loading context: <{}> index: [{}] of {}",
|
||||||
|
context_uri, index, tracks_len
|
||||||
|
);
|
||||||
|
// Cycle through all tracks, break if we don't find any playable tracks
|
||||||
|
// TODO: This will panic if no playable tracks are found!
|
||||||
|
// tracks in each frame either have a gid or uri (that may or may not be a valid track)
|
||||||
|
// E.g - context based frames sometimes contain tracks with <spotify:meta:page:>
|
||||||
let track = {
|
let track = {
|
||||||
let mut index = self.state.get_playing_track_index();
|
let mut track_ref = self.state.get_track()[index as usize].clone();
|
||||||
// Check for malformed gid
|
let mut track_id = self.get_spotify_id_for_track(&track_ref);
|
||||||
let tracks_len = self.state.get_track().len() as u32;
|
while track_id.is_err() || track_id.unwrap().audio_type == SpotifyAudioType::NonPlayable {
|
||||||
let mut track_ref = &self.state.get_track()[index as usize];
|
|
||||||
while track_ref.get_gid().len() != 16 {
|
|
||||||
warn!(
|
warn!(
|
||||||
"Skipping track {:?} at position [{}] of {}",
|
"Skipping track <{:?}> at position [{}] of {}",
|
||||||
track_ref.get_uri(),
|
track_ref.get_uri(),
|
||||||
index,
|
index,
|
||||||
tracks_len
|
tracks_len
|
||||||
);
|
);
|
||||||
index = if index + 1 < tracks_len { index + 1 } else { 0 };
|
index = if index + 1 < tracks_len { index + 1 } else { 0 };
|
||||||
track_ref = &self.state.get_track()[index as usize];
|
self.state.set_playing_track_index(index);
|
||||||
|
if index == start_index {
|
||||||
|
warn!("No playable track found in state: {:?}", self.state);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
track_ref = self.state.get_track()[index as usize].clone();
|
||||||
|
track_id = self.get_spotify_id_for_track(&track_ref);
|
||||||
}
|
}
|
||||||
SpotifyId::from_raw(track_ref.get_gid()).unwrap()
|
track_id
|
||||||
};
|
}
|
||||||
|
.expect("Invalid SpotifyId");
|
||||||
|
|
||||||
let position = self.state.get_position_ms();
|
let position = self.state.get_position_ms();
|
||||||
let end_of_track = self.player.load(track, play, position);
|
let end_of_track = self.player.load(track, play, position);
|
||||||
|
|
|
@ -1,8 +1,18 @@
|
||||||
use std;
|
use std;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct SpotifyId(u128);
|
pub enum SpotifyAudioType {
|
||||||
|
Track,
|
||||||
|
Podcast,
|
||||||
|
NonPlayable,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct SpotifyId {
|
||||||
|
pub id: u128,
|
||||||
|
pub audio_type: SpotifyAudioType,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct SpotifyIdError;
|
pub struct SpotifyIdError;
|
||||||
|
@ -11,6 +21,13 @@ const BASE62_DIGITS: &'static [u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDE
|
||||||
const BASE16_DIGITS: &'static [u8] = b"0123456789abcdef";
|
const BASE16_DIGITS: &'static [u8] = b"0123456789abcdef";
|
||||||
|
|
||||||
impl SpotifyId {
|
impl SpotifyId {
|
||||||
|
fn as_track(n: u128) -> SpotifyId {
|
||||||
|
SpotifyId {
|
||||||
|
id: n.to_owned(),
|
||||||
|
audio_type: SpotifyAudioType::Track,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn from_base16(id: &str) -> Result<SpotifyId, SpotifyIdError> {
|
pub fn from_base16(id: &str) -> Result<SpotifyId, SpotifyIdError> {
|
||||||
let data = id.as_bytes();
|
let data = id.as_bytes();
|
||||||
|
|
||||||
|
@ -24,7 +41,7 @@ impl SpotifyId {
|
||||||
n = n + d;
|
n = n + d;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SpotifyId(n))
|
Ok(SpotifyId::as_track(n))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_base62(id: &str) -> Result<SpotifyId, SpotifyIdError> {
|
pub fn from_base62(id: &str) -> Result<SpotifyId, SpotifyIdError> {
|
||||||
|
@ -39,8 +56,7 @@ impl SpotifyId {
|
||||||
n = n * 62;
|
n = n * 62;
|
||||||
n = n + d;
|
n = n + d;
|
||||||
}
|
}
|
||||||
|
Ok(SpotifyId::as_track(n))
|
||||||
Ok(SpotifyId(n))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_raw(data: &[u8]) -> Result<SpotifyId, SpotifyIdError> {
|
pub fn from_raw(data: &[u8]) -> Result<SpotifyId, SpotifyIdError> {
|
||||||
|
@ -51,15 +67,32 @@ impl SpotifyId {
|
||||||
let mut arr: [u8; 16] = Default::default();
|
let mut arr: [u8; 16] = Default::default();
|
||||||
arr.copy_from_slice(&data[0..16]);
|
arr.copy_from_slice(&data[0..16]);
|
||||||
|
|
||||||
Ok(SpotifyId(u128::from_be_bytes(arr)))
|
Ok(SpotifyId::as_track(u128::from_be_bytes(arr)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_uri(uri: &str) -> Result<SpotifyId, SpotifyIdError> {
|
||||||
|
let parts = uri.split(":").collect::<Vec<&str>>();
|
||||||
|
let gid = parts.last().unwrap();
|
||||||
|
if uri.contains(":episode:") {
|
||||||
|
let mut spotify_id = SpotifyId::from_base62(gid).unwrap();
|
||||||
|
let _ = std::mem::replace(&mut spotify_id.audio_type, SpotifyAudioType::Podcast);
|
||||||
|
Ok(spotify_id)
|
||||||
|
} else if uri.contains(":track:") {
|
||||||
|
SpotifyId::from_base62(gid)
|
||||||
|
} else {
|
||||||
|
// show/playlist/artist/album/??
|
||||||
|
let mut spotify_id = SpotifyId::from_base62(gid).unwrap();
|
||||||
|
let _ = std::mem::replace(&mut spotify_id.audio_type, SpotifyAudioType::NonPlayable);
|
||||||
|
Ok(spotify_id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_base16(&self) -> String {
|
pub fn to_base16(&self) -> String {
|
||||||
format!("{:032x}", self.0)
|
format!("{:032x}", self.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_base62(&self) -> String {
|
pub fn to_base62(&self) -> String {
|
||||||
let &SpotifyId(mut n) = self;
|
let &SpotifyId { id: mut n, .. } = self;
|
||||||
|
|
||||||
let mut data = [0u8; 22];
|
let mut data = [0u8; 22];
|
||||||
for i in 0..22 {
|
for i in 0..22 {
|
||||||
|
@ -71,7 +104,7 @@ impl SpotifyId {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_raw(&self) -> [u8; 16] {
|
pub fn to_raw(&self) -> [u8; 16] {
|
||||||
self.0.to_be_bytes()
|
self.id.to_be_bytes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,13 @@ extern crate librespot_protocol as protocol;
|
||||||
|
|
||||||
pub mod cover;
|
pub mod cover;
|
||||||
|
|
||||||
|
use futures::future;
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
use linear_map::LinearMap;
|
use linear_map::LinearMap;
|
||||||
|
|
||||||
use librespot_core::mercury::MercuryError;
|
use librespot_core::mercury::MercuryError;
|
||||||
use librespot_core::session::Session;
|
use librespot_core::session::Session;
|
||||||
use librespot_core::spotify_id::{FileId, SpotifyId};
|
use librespot_core::spotify_id::{FileId, SpotifyAudioType, SpotifyId};
|
||||||
|
|
||||||
pub use protocol::metadata::AudioFile_Format as FileFormat;
|
pub use protocol::metadata::AudioFile_Format as FileFormat;
|
||||||
|
|
||||||
|
@ -52,13 +53,81 @@ where
|
||||||
&& (!has_allowed || countrylist_contains(allowed.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: 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),
|
||||||
|
SpotifyAudioType::NonPlayable => {
|
||||||
|
Box::new(future::err::<AudioItem, MercuryError>(MercuryError))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
pub trait Metadata: Send + Sized + 'static {
|
pub trait Metadata: Send + Sized + 'static {
|
||||||
type Message: protobuf::Message;
|
type Message: protobuf::Message;
|
||||||
|
|
||||||
fn base_url() -> &'static str;
|
fn base_url() -> &'static str;
|
||||||
fn parse(msg: &Self::Message, session: &Session) -> Self;
|
fn parse(msg: &Self::Message, session: &Session) -> Self;
|
||||||
|
|
||||||
fn get(session: &Session, id: SpotifyId) -> Box<Future<Item = Self, Error = MercuryError>> {
|
fn get(session: &Session, id: SpotifyId) -> Box<dyn Future<Item = Self, Error = MercuryError>> {
|
||||||
let uri = format!("{}/{}", Self::base_url(), id.to_base16());
|
let uri = format!("{}/{}", Self::base_url(), id.to_base16());
|
||||||
let request = session.mercury().get(uri);
|
let request = session.mercury().get(uri);
|
||||||
|
|
||||||
|
@ -93,6 +162,29 @@ pub struct Album {
|
||||||
pub covers: Vec<FileId>,
|
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: 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,
|
||||||
|
pub publisher: String,
|
||||||
|
pub episodes: Vec<SpotifyId>,
|
||||||
|
pub covers: Vec<FileId>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Artist {
|
pub struct Artist {
|
||||||
pub id: SpotifyId,
|
pub id: SpotifyId,
|
||||||
|
@ -222,6 +314,92 @@ impl Metadata for Artist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Podcast
|
||||||
|
impl Metadata for Episode {
|
||||||
|
type Message = protocol::metadata::Episode;
|
||||||
|
|
||||||
|
fn base_url() -> &'static str {
|
||||||
|
"hm://metadata/3/episode"
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
fn base_url() -> &'static str {
|
||||||
|
"hm://metadata/3/show"
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
publisher: msg.get_publisher().to_owned(),
|
||||||
|
episodes: episodes,
|
||||||
|
covers: covers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct StrChunks<'s>(&'s str, usize);
|
struct StrChunks<'s>(&'s str, usize);
|
||||||
|
|
||||||
trait StrChunksExt {
|
trait StrChunksExt {
|
||||||
|
|
|
@ -17,7 +17,7 @@ use librespot_core::spotify_id::SpotifyId;
|
||||||
use audio::{AudioDecrypt, AudioFile};
|
use audio::{AudioDecrypt, AudioFile};
|
||||||
use audio::{VorbisDecoder, VorbisPacket};
|
use audio::{VorbisDecoder, VorbisPacket};
|
||||||
use audio_backend::Sink;
|
use audio_backend::Sink;
|
||||||
use metadata::{FileFormat, Metadata, Track};
|
use metadata::{AudioItem, FileFormat};
|
||||||
use mixer::AudioFilter;
|
use mixer::AudioFilter;
|
||||||
|
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
|
@ -512,58 +512,69 @@ impl PlayerInternal {
|
||||||
let _ = self.event_sender.unbounded_send(event.clone());
|
let _ = self.event_sender.unbounded_send(event.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_available_alternative<'a>(&self, track: &'a Track) -> Option<Cow<'a, Track>> {
|
fn find_available_alternative<'a>(&self, audio: &'a AudioItem) -> Option<Cow<'a, AudioItem>> {
|
||||||
if track.available {
|
if audio.available {
|
||||||
Some(Cow::Borrowed(track))
|
Some(Cow::Borrowed(audio))
|
||||||
} else {
|
} else {
|
||||||
let alternatives = track
|
if let Some(alternatives) = &audio.alternatives {
|
||||||
.alternatives
|
let alternatives = alternatives
|
||||||
.iter()
|
.iter()
|
||||||
.map(|alt_id| Track::get(&self.session, *alt_id));
|
.map(|alt_id| AudioItem::get_audio_item(&self.session, *alt_id));
|
||||||
let alternatives = future::join_all(alternatives).wait().unwrap();
|
let alternatives = future::join_all(alternatives).wait().unwrap();
|
||||||
|
alternatives.into_iter().find(|alt| alt.available).map(Cow::Owned)
|
||||||
alternatives.into_iter().find(|alt| alt.available).map(Cow::Owned)
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_track(&self, track_id: SpotifyId, position: i64) -> Option<(Decoder, f32)> {
|
fn load_track(&self, spotify_id: SpotifyId, position: i64) -> Option<(Decoder, f32)> {
|
||||||
let track = Track::get(&self.session, track_id).wait().unwrap();
|
let audio = AudioItem::get_audio_item(&self.session, spotify_id)
|
||||||
|
.wait()
|
||||||
|
.unwrap();
|
||||||
|
info!("Loading <{}> with Spotify URI <{}>", audio.name, audio.uri);
|
||||||
|
|
||||||
info!(
|
let audio = match self.find_available_alternative(&audio) {
|
||||||
"Loading track \"{}\" with Spotify URI \"spotify:track:{}\"",
|
Some(audio) => audio,
|
||||||
track.name,
|
|
||||||
track_id.to_base62()
|
|
||||||
);
|
|
||||||
|
|
||||||
let track = match self.find_available_alternative(&track) {
|
|
||||||
Some(track) => track,
|
|
||||||
None => {
|
None => {
|
||||||
warn!("Track \"{}\" is not available", track.name);
|
warn!("<{}> is not available", audio.uri);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// (Most) podcasts seem to support only 96 bit Vorbis, so fall back to it
|
||||||
let format = match self.config.bitrate {
|
let formats = match self.config.bitrate {
|
||||||
Bitrate::Bitrate96 => FileFormat::OGG_VORBIS_96,
|
Bitrate::Bitrate96 => [
|
||||||
Bitrate::Bitrate160 => FileFormat::OGG_VORBIS_160,
|
FileFormat::OGG_VORBIS_96,
|
||||||
Bitrate::Bitrate320 => FileFormat::OGG_VORBIS_320,
|
FileFormat::OGG_VORBIS_160,
|
||||||
|
FileFormat::OGG_VORBIS_320,
|
||||||
|
],
|
||||||
|
Bitrate::Bitrate160 => [
|
||||||
|
FileFormat::OGG_VORBIS_160,
|
||||||
|
FileFormat::OGG_VORBIS_96,
|
||||||
|
FileFormat::OGG_VORBIS_320,
|
||||||
|
],
|
||||||
|
Bitrate::Bitrate320 => [
|
||||||
|
FileFormat::OGG_VORBIS_320,
|
||||||
|
FileFormat::OGG_VORBIS_160,
|
||||||
|
FileFormat::OGG_VORBIS_96,
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
let format = formats
|
||||||
|
.iter()
|
||||||
|
.find(|format| audio.files.contains_key(format))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let file_id = match track.files.get(&format) {
|
let file_id = match audio.files.get(&format) {
|
||||||
Some(&file_id) => file_id,
|
Some(&file_id) => file_id,
|
||||||
None => {
|
None => {
|
||||||
warn!("Track \"{}\" is not available in format {:?}", track.name, format);
|
warn!("<{}> in not available in format {:?}", audio.name, format);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let key = self
|
let key = self.session.audio_key().request(spotify_id, file_id);
|
||||||
.session
|
|
||||||
.audio_key()
|
|
||||||
.request(track.id, file_id);
|
|
||||||
let encrypted_file = AudioFile::open(&self.session, file_id);
|
let encrypted_file = AudioFile::open(&self.session, file_id);
|
||||||
|
|
||||||
|
|
||||||
let encrypted_file = encrypted_file.wait().unwrap();
|
let encrypted_file = encrypted_file.wait().unwrap();
|
||||||
let key = key.wait().unwrap();
|
let key = key.wait().unwrap();
|
||||||
let mut decrypted_file = AudioDecrypt::new(key, encrypted_file);
|
let mut decrypted_file = AudioDecrypt::new(key, encrypted_file);
|
||||||
|
@ -586,9 +597,7 @@ impl PlayerInternal {
|
||||||
Err(err) => error!("Vorbis error: {:?}", err),
|
Err(err) => error!("Vorbis error: {:?}", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
info!("<{}> loaded", audio.name);
|
||||||
info!("Track \"{}\" loaded", track.name);
|
|
||||||
|
|
||||||
Some((decoder, normalisation_factor))
|
Some((decoder, normalisation_factor))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,8 @@ message Date {
|
||||||
optional sint32 year = 0x1;
|
optional sint32 year = 0x1;
|
||||||
optional sint32 month = 0x2;
|
optional sint32 month = 0x2;
|
||||||
optional sint32 day = 0x3;
|
optional sint32 day = 0x3;
|
||||||
|
optional sint32 hour = 0x4;
|
||||||
|
optional sint32 minute = 0x5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Album {
|
message Album {
|
||||||
|
@ -124,15 +126,29 @@ message Copyright {
|
||||||
}
|
}
|
||||||
|
|
||||||
message Restriction {
|
message Restriction {
|
||||||
optional string countries_allowed = 0x2;
|
enum Catalogue {
|
||||||
optional string countries_forbidden = 0x3;
|
AD = 0;
|
||||||
optional Type typ = 0x4;
|
SUBSCRIPTION = 1;
|
||||||
|
CATALOGUE_ALL = 2;
|
||||||
|
SHUFFLE = 3;
|
||||||
|
COMMERCIAL = 4;
|
||||||
|
}
|
||||||
enum Type {
|
enum Type {
|
||||||
STREAMING = 0x0;
|
STREAMING = 0x0;
|
||||||
}
|
}
|
||||||
|
repeated Catalogue catalogue = 0x1;
|
||||||
|
optional string countries_allowed = 0x2;
|
||||||
|
optional string countries_forbidden = 0x3;
|
||||||
|
optional Type typ = 0x4;
|
||||||
|
|
||||||
repeated string catalogue_str = 0x5;
|
repeated string catalogue_str = 0x5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message Availability {
|
||||||
|
repeated string catalogue_str = 0x1;
|
||||||
|
optional Date start = 0x2;
|
||||||
|
}
|
||||||
|
|
||||||
message SalePeriod {
|
message SalePeriod {
|
||||||
repeated Restriction restriction = 0x1;
|
repeated Restriction restriction = 0x1;
|
||||||
optional Date start = 0x2;
|
optional Date start = 0x2;
|
||||||
|
@ -156,12 +172,95 @@ message AudioFile {
|
||||||
MP3_160 = 0x5;
|
MP3_160 = 0x5;
|
||||||
MP3_96 = 0x6;
|
MP3_96 = 0x6;
|
||||||
MP3_160_ENC = 0x7;
|
MP3_160_ENC = 0x7;
|
||||||
OTHER2 = 0x8;
|
// v4
|
||||||
|
// AAC_24 = 0x8;
|
||||||
|
// AAC_48 = 0x9;
|
||||||
|
MP4_128_DUAL = 0x8;
|
||||||
OTHER3 = 0x9;
|
OTHER3 = 0x9;
|
||||||
AAC_160 = 0xa;
|
AAC_160 = 0xa;
|
||||||
AAC_320 = 0xb;
|
AAC_320 = 0xb;
|
||||||
OTHER4 = 0xc;
|
MP4_128 = 0xc;
|
||||||
OTHER5 = 0xd;
|
OTHER5 = 0xd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message VideoFile {
|
||||||
|
optional bytes file_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Podcast Protos
|
||||||
|
message Show {
|
||||||
|
enum MediaType {
|
||||||
|
MIXED = 0;
|
||||||
|
AUDIO = 1;
|
||||||
|
VIDEO = 2;
|
||||||
|
}
|
||||||
|
enum ConsumptionOrder {
|
||||||
|
SEQUENTIAL = 1;
|
||||||
|
EPISODIC = 2;
|
||||||
|
RECENT = 3;
|
||||||
|
}
|
||||||
|
enum PassthroughEnum {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
NONE = 1;
|
||||||
|
ALLOWED = 2;
|
||||||
|
}
|
||||||
|
optional bytes gid = 0x1;
|
||||||
|
optional string name = 0x2;
|
||||||
|
optional string description = 0x40;
|
||||||
|
optional sint32 deprecated_popularity = 0x41;
|
||||||
|
optional string publisher = 0x42;
|
||||||
|
optional string language = 0x43;
|
||||||
|
optional bool explicit = 0x44;
|
||||||
|
optional ImageGroup covers = 0x45;
|
||||||
|
repeated Episode episode = 0x46;
|
||||||
|
repeated Copyright copyright = 0x47;
|
||||||
|
repeated Restriction restriction = 0x48;
|
||||||
|
repeated string keyword = 0x49;
|
||||||
|
optional MediaType media_type = 0x4A;
|
||||||
|
optional ConsumptionOrder consumption_order = 0x4B;
|
||||||
|
optional bool interpret_restriction_using_geoip = 0x4C;
|
||||||
|
repeated Availability availability = 0x4E;
|
||||||
|
optional string country_of_origin = 0x4F;
|
||||||
|
repeated Category categories = 0x50;
|
||||||
|
optional PassthroughEnum passthrough = 0x51;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Episode {
|
||||||
|
optional bytes gid = 0x1;
|
||||||
|
optional string name = 0x2;
|
||||||
|
optional sint32 duration = 0x7;
|
||||||
|
optional sint32 popularity = 0x8;
|
||||||
|
repeated AudioFile file = 0xc;
|
||||||
|
optional string description = 0x40;
|
||||||
|
optional sint32 number = 0x41;
|
||||||
|
optional Date publish_time = 0x42;
|
||||||
|
optional sint32 deprecated_popularity = 0x43;
|
||||||
|
optional ImageGroup covers = 0x44;
|
||||||
|
optional string language = 0x45;
|
||||||
|
optional bool explicit = 0x46;
|
||||||
|
optional Show show = 0x47;
|
||||||
|
repeated VideoFile video = 0x48;
|
||||||
|
repeated VideoFile video_preview = 0x49;
|
||||||
|
repeated AudioFile audio_preview = 0x4A;
|
||||||
|
repeated Restriction restriction = 0x4B;
|
||||||
|
optional ImageGroup freeze_frame = 0x4C;
|
||||||
|
repeated string keyword = 0x4D;
|
||||||
|
// Order of these two flags might be wrong!
|
||||||
|
optional bool suppress_monetization = 0x4E;
|
||||||
|
optional bool interpret_restriction_using_geoip = 0x4F;
|
||||||
|
|
||||||
|
optional bool allow_background_playback = 0x51;
|
||||||
|
repeated Availability availability = 0x52;
|
||||||
|
optional string external_url = 0x53;
|
||||||
|
optional OriginalAudio original_audio = 0x54;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Category {
|
||||||
|
optional string name = 0x1;
|
||||||
|
repeated Category subcategories = 0x2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OriginalAudio {
|
||||||
|
optional bytes uuid = 0x1;
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue