Streamline and refactor Podcast support,

<Metadata>
  Add publisher to `Show`
  Add `ALLOWED` to `PassthroughEnum`
This commit is contained in:
ashthespy 2019-10-08 00:27:26 +02:00
parent 0cb7a3f7c8
commit 8eb51e9b55
6 changed files with 135 additions and 88 deletions

View file

@ -2,7 +2,7 @@ use std;
use std::fmt; use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SpotifyTrackType { pub enum SpotifyAudioType {
Track, Track,
Podcast, Podcast,
} }
@ -10,7 +10,7 @@ pub enum SpotifyTrackType {
#[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 track_type: SpotifyTrackType, pub audio_type: SpotifyAudioType,
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
@ -23,7 +23,7 @@ impl SpotifyId {
fn as_track(n: u128) -> SpotifyId { fn as_track(n: u128) -> SpotifyId {
SpotifyId { SpotifyId {
id: n.to_owned(), id: n.to_owned(),
track_type: SpotifyTrackType::Track, audio_type: SpotifyAudioType::Track,
} }
} }
@ -73,7 +73,7 @@ impl SpotifyId {
let parts = uri.split(":").collect::<Vec<&str>>(); let parts = uri.split(":").collect::<Vec<&str>>();
if uri.contains(":show:") || uri.contains(":episode:") { if uri.contains(":show:") || uri.contains(":episode:") {
let mut spotify_id = SpotifyId::from_base62(parts[2]).unwrap(); let mut spotify_id = SpotifyId::from_base62(parts[2]).unwrap();
spotify_id.track_type = SpotifyTrackType::Podcast; let _ = std::mem::replace(&mut spotify_id.audio_type, SpotifyAudioType::Podcast);
Ok(spotify_id) Ok(spotify_id)
} else { } else {
SpotifyId::from_base62(parts[2]) SpotifyId::from_base62(parts[2])

View file

@ -13,7 +13,7 @@ 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 +52,78 @@ 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),
}
}
}
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);
@ -111,6 +176,7 @@ pub struct Episode {
pub struct Show { pub struct Show {
pub id: SpotifyId, pub id: SpotifyId,
pub name: String, pub name: String,
pub publisher: String,
pub episodes: Vec<SpotifyId>, pub episodes: Vec<SpotifyId>,
pub covers: Vec<FileId>, pub covers: Vec<FileId>,
} }
@ -323,6 +389,7 @@ impl Metadata for Show {
Show { Show {
id: SpotifyId::from_raw(msg.get_gid()).unwrap(), id: SpotifyId::from_raw(msg.get_gid()).unwrap(),
name: msg.get_name().to_owned(), name: msg.get_name().to_owned(),
publisher: msg.get_publisher().to_owned(),
episodes: episodes, episodes: episodes,
covers: covers, covers: covers,
} }

View file

@ -12,12 +12,12 @@ use std::time::Duration;
use config::{Bitrate, PlayerConfig}; use config::{Bitrate, PlayerConfig};
use librespot_core::session::Session; use librespot_core::session::Session;
use librespot_core::spotify_id::{FileId, SpotifyId, SpotifyTrackType}; 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::{Episode, FileFormat, Metadata, Track}; use metadata::{AudioItem, FileFormat};
use mixer::AudioFilter; use mixer::AudioFilter;
pub struct Player { pub struct Player {
@ -512,87 +512,65 @@ 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 get_file_id(&self, spotify_id: SpotifyId) -> Option<FileId> {
let file_id = match spotify_id.track_type {
SpotifyTrackType::Track => {
let track = Track::get(&self.session, spotify_id).wait().unwrap();
info!(
"Loading track \"{}\" with Spotify URI \"spotify:track:{}\"",
track.name,
spotify_id.to_base62()
);
let track = match self.find_available_alternative(&track) {
Some(track) => track,
None => {
warn!("Track \"{}\" is not available", track.name);
return None;
}
};
let format = match self.config.bitrate {
Bitrate::Bitrate96 => FileFormat::OGG_VORBIS_96,
Bitrate::Bitrate160 => FileFormat::OGG_VORBIS_160,
Bitrate::Bitrate320 => FileFormat::OGG_VORBIS_320,
};
match track.files.get(&format) {
Some(&file_id) => file_id,
None => {
warn!("Track \"{}\" is not available in format {:?}", track.name, format);
return None;
}
}
}
// This should be refactored!
SpotifyTrackType::Podcast => {
let episode = Episode::get(&self.session, spotify_id).wait().unwrap();
info!("Episode {:?}", episode);
info!(
"Loading episode \"{}\" with Spotify URI \"spotify:episode:{}\"",
episode.name,
spotify_id.to_base62()
);
// Podcasts seem to have only 96 OGG_VORBIS support, other filetypes indicate
// AAC_24, MP4_128, MP4_128_DUAL, MP3_96 among others
let format = match self.config.bitrate {
_ => FileFormat::OGG_VORBIS_96,
};
match episode.files.get(&format) {
Some(&file_id) => file_id,
None => {
warn!(
"Episode \"{}\" is not available in format {:?}",
episode.name, format
);
return None;
}
}
}
};
return Some(file_id);
} }
fn load_track(&self, spotify_id: SpotifyId, position: i64) -> Option<(Decoder, f32)> { fn load_track(&self, spotify_id: SpotifyId, position: i64) -> Option<(Decoder, f32)> {
let file_id = self.get_file_id(spotify_id).unwrap(); let audio = AudioItem::get_audio_item(&self.session, spotify_id)
info!("{:?} -> {:?}", spotify_id, file_id); .wait()
.unwrap();
info!("Loading <{}> with Spotify URI <{}>", audio.name, audio.uri);
let audio = match self.find_available_alternative(&audio) {
Some(audio) => audio,
None => {
warn!("<{}> is not available", audio.uri);
return None;
}
};
// (Most) podcasts seem to support only 96 bit Vorbis, so fall back to it
let formats = match self.config.bitrate {
Bitrate::Bitrate96 => [
FileFormat::OGG_VORBIS_96,
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 audio.files.get(&format) {
Some(&file_id) => file_id,
None => {
warn!("<{}> in not available in format {:?}", audio.name, format);
return None;
}
};
let key = self.session.audio_key().request(spotify_id, file_id); let key = self.session.audio_key().request(spotify_id, file_id);
let encrypted_file = AudioFile::open(&self.session, file_id); let encrypted_file = AudioFile::open(&self.session, file_id);
@ -619,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))
} }
} }

View file

@ -3,7 +3,7 @@ pub const FILES: &'static [(&'static str, u32)] = &[
("proto/authentication.proto", 2098196376), ("proto/authentication.proto", 2098196376),
("proto/keyexchange.proto", 451735664), ("proto/keyexchange.proto", 451735664),
("proto/mercury.proto", 709993906), ("proto/mercury.proto", 709993906),
("proto/metadata.proto", 1409162985), ("proto/metadata.proto", 334477813),
("proto/pubsub.proto", 2686584829), ("proto/pubsub.proto", 2686584829),
("proto/spirc.proto", 1587493382), ("proto/spirc.proto", 1587493382),
]; ];

View file

@ -207,6 +207,7 @@ enum MediaType {
enum PassthroughEnum { enum PassthroughEnum {
UNKNOWN = 0; UNKNOWN = 0;
NONE = 1; NONE = 1;
ALLOWED = 2;
} }
message Episode { message Episode {

View file

@ -8896,6 +8896,7 @@ impl ::protobuf::reflect::ProtobufValue for MediaType {
pub enum PassthroughEnum { pub enum PassthroughEnum {
UNKNOWN = 0, UNKNOWN = 0,
NONE = 1, NONE = 1,
ALLOWED = 2,
} }
impl ::protobuf::ProtobufEnum for PassthroughEnum { impl ::protobuf::ProtobufEnum for PassthroughEnum {
@ -8907,6 +8908,7 @@ impl ::protobuf::ProtobufEnum for PassthroughEnum {
match value { match value {
0 => ::std::option::Option::Some(PassthroughEnum::UNKNOWN), 0 => ::std::option::Option::Some(PassthroughEnum::UNKNOWN),
1 => ::std::option::Option::Some(PassthroughEnum::NONE), 1 => ::std::option::Option::Some(PassthroughEnum::NONE),
2 => ::std::option::Option::Some(PassthroughEnum::ALLOWED),
_ => ::std::option::Option::None _ => ::std::option::Option::None
} }
} }
@ -8915,6 +8917,7 @@ impl ::protobuf::ProtobufEnum for PassthroughEnum {
static values: &'static [PassthroughEnum] = &[ static values: &'static [PassthroughEnum] = &[
PassthroughEnum::UNKNOWN, PassthroughEnum::UNKNOWN,
PassthroughEnum::NONE, PassthroughEnum::NONE,
PassthroughEnum::ALLOWED,
]; ];
values values
} }
@ -9057,9 +9060,9 @@ static file_descriptor_proto_data: &'static [u8] = b"\
\rOriginalAudio\x12\x0e\n\x04uuid\x18\x01\x20\x01(\x0cB\0:\0*>\n\x10Cons\ \rOriginalAudio\x12\x0e\n\x04uuid\x18\x01\x20\x01(\x0cB\0:\0*>\n\x10Cons\
umptionOrder\x12\x0e\n\nSEQUENTIAL\x10\x01\x12\x0c\n\x08EPISODIC\x10\x02\ umptionOrder\x12\x0e\n\nSEQUENTIAL\x10\x01\x12\x0c\n\x08EPISODIC\x10\x02\
\x12\n\n\x06RECENT\x10\x03\x1a\0*.\n\tMediaType\x12\t\n\x05MIXED\x10\0\ \x12\n\n\x06RECENT\x10\x03\x1a\0*.\n\tMediaType\x12\t\n\x05MIXED\x10\0\
\x12\t\n\x05AUDIO\x10\x01\x12\t\n\x05VIDEO\x10\x02\x1a\0**\n\x0fPassthro\ \x12\t\n\x05AUDIO\x10\x01\x12\t\n\x05VIDEO\x10\x02\x1a\0*7\n\x0fPassthro\
ughEnum\x12\x0b\n\x07UNKNOWN\x10\0\x12\x08\n\x04NONE\x10\x01\x1a\0B\0b\ ughEnum\x12\x0b\n\x07UNKNOWN\x10\0\x12\x08\n\x04NONE\x10\x01\x12\x0b\n\
\x06proto2\ \x07ALLOWED\x10\x02\x1a\0B\0b\x06proto2\
"; ";
static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy { static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy {