diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 01c4ddd4..c8f01674 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -2,7 +2,7 @@ use std; use std::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum SpotifyTrackType { +pub enum SpotifyAudioType { Track, Podcast, } @@ -10,7 +10,7 @@ pub enum SpotifyTrackType { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct SpotifyId { pub id: u128, - pub track_type: SpotifyTrackType, + pub audio_type: SpotifyAudioType, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] @@ -23,7 +23,7 @@ impl SpotifyId { fn as_track(n: u128) -> SpotifyId { SpotifyId { id: n.to_owned(), - track_type: SpotifyTrackType::Track, + audio_type: SpotifyAudioType::Track, } } @@ -73,7 +73,7 @@ impl SpotifyId { let parts = uri.split(":").collect::>(); if uri.contains(":show:") || uri.contains(":episode:") { 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) } else { SpotifyId::from_base62(parts[2]) diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index 6aa6a5bd..75a42e2f 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -13,7 +13,7 @@ use linear_map::LinearMap; use librespot_core::mercury::MercuryError; 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; @@ -52,13 +52,78 @@ where && (!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, + pub name: String, + pub available: bool, + pub alternatives: Option>, +} + +impl AudioItem { + pub fn get_audio_item( + session: &Session, + id: SpotifyId, + ) -> Box> { + 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>; +} + +impl AudioFiles for Track { + fn get_audio_item( + session: &Session, + id: SpotifyId, + ) -> Box> { + 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> { + 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 { type Message: protobuf::Message; fn base_url() -> &'static str; fn parse(msg: &Self::Message, session: &Session) -> Self; - fn get(session: &Session, id: SpotifyId) -> Box> { + fn get(session: &Session, id: SpotifyId) -> Box> { let uri = format!("{}/{}", Self::base_url(), id.to_base16()); let request = session.mercury().get(uri); @@ -111,6 +176,7 @@ pub struct Episode { pub struct Show { pub id: SpotifyId, pub name: String, + pub publisher: String, pub episodes: Vec, pub covers: Vec, } @@ -323,6 +389,7 @@ impl Metadata for Show { 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, } diff --git a/playback/src/player.rs b/playback/src/player.rs index 3b48a594..95eb549d 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -12,12 +12,12 @@ use std::time::Duration; use config::{Bitrate, PlayerConfig}; 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::{VorbisDecoder, VorbisPacket}; use audio_backend::Sink; -use metadata::{Episode, FileFormat, Metadata, Track}; +use metadata::{AudioItem, FileFormat}; use mixer::AudioFilter; pub struct Player { @@ -512,87 +512,65 @@ impl PlayerInternal { let _ = self.event_sender.unbounded_send(event.clone()); } - fn find_available_alternative<'a>(&self, track: &'a Track) -> Option> { - if track.available { - Some(Cow::Borrowed(track)) + fn find_available_alternative<'a>(&self, audio: &'a AudioItem) -> Option> { + if audio.available { + Some(Cow::Borrowed(audio)) } else { - let alternatives = track - .alternatives - .iter() - .map(|alt_id| Track::get(&self.session, *alt_id)); - let alternatives = future::join_all(alternatives).wait().unwrap(); - - alternatives.into_iter().find(|alt| alt.available).map(Cow::Owned) + if let Some(alternatives) = &audio.alternatives { + let alternatives = alternatives + .iter() + .map(|alt_id| AudioItem::get_audio_item(&self.session, *alt_id)); + let alternatives = future::join_all(alternatives).wait().unwrap(); + alternatives.into_iter().find(|alt| alt.available).map(Cow::Owned) + } else { + None + } } } - fn get_file_id(&self, spotify_id: SpotifyId) -> Option { - let file_id = match spotify_id.track_type { - SpotifyTrackType::Track => { - let track = Track::get(&self.session, spotify_id).wait().unwrap(); + fn load_track(&self, spotify_id: SpotifyId, position: i64) -> Option<(Decoder, f32)> { + let audio = AudioItem::get_audio_item(&self.session, spotify_id) + .wait() + .unwrap(); + info!("Loading <{}> with Spotify URI <{}>", audio.name, audio.uri); - 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; - } - } + let audio = match self.find_available_alternative(&audio) { + Some(audio) => audio, + None => { + warn!("<{}> is not available", audio.uri); + return None; } }; - return Some(file_id); - } + // (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(); - fn load_track(&self, spotify_id: SpotifyId, position: i64) -> Option<(Decoder, f32)> { - let file_id = self.get_file_id(spotify_id).unwrap(); - info!("{:?} -> {:?}", spotify_id, file_id); + 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 encrypted_file = AudioFile::open(&self.session, file_id); @@ -619,9 +597,7 @@ impl PlayerInternal { Err(err) => error!("Vorbis error: {:?}", err), } } - - // info!("Track \"{}\" loaded", track.name); - + info!("<{}> loaded", audio.name); Some((decoder, normalisation_factor)) } } diff --git a/protocol/files.rs b/protocol/files.rs index f4ddd9ad..7086bd3d 100644 --- a/protocol/files.rs +++ b/protocol/files.rs @@ -3,7 +3,7 @@ pub const FILES: &'static [(&'static str, u32)] = &[ ("proto/authentication.proto", 2098196376), ("proto/keyexchange.proto", 451735664), ("proto/mercury.proto", 709993906), - ("proto/metadata.proto", 1409162985), + ("proto/metadata.proto", 334477813), ("proto/pubsub.proto", 2686584829), ("proto/spirc.proto", 1587493382), ]; diff --git a/protocol/proto/metadata.proto b/protocol/proto/metadata.proto index c3730ca7..c6c86ab6 100644 --- a/protocol/proto/metadata.proto +++ b/protocol/proto/metadata.proto @@ -207,6 +207,7 @@ enum MediaType { enum PassthroughEnum { UNKNOWN = 0; NONE = 1; + ALLOWED = 2; } message Episode { diff --git a/protocol/src/metadata.rs b/protocol/src/metadata.rs index 1a1a7f00..7d38e02d 100644 --- a/protocol/src/metadata.rs +++ b/protocol/src/metadata.rs @@ -8896,6 +8896,7 @@ impl ::protobuf::reflect::ProtobufValue for MediaType { pub enum PassthroughEnum { UNKNOWN = 0, NONE = 1, + ALLOWED = 2, } impl ::protobuf::ProtobufEnum for PassthroughEnum { @@ -8907,6 +8908,7 @@ impl ::protobuf::ProtobufEnum for PassthroughEnum { match value { 0 => ::std::option::Option::Some(PassthroughEnum::UNKNOWN), 1 => ::std::option::Option::Some(PassthroughEnum::NONE), + 2 => ::std::option::Option::Some(PassthroughEnum::ALLOWED), _ => ::std::option::Option::None } } @@ -8915,6 +8917,7 @@ impl ::protobuf::ProtobufEnum for PassthroughEnum { static values: &'static [PassthroughEnum] = &[ PassthroughEnum::UNKNOWN, PassthroughEnum::NONE, + PassthroughEnum::ALLOWED, ]; 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\ 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\t\n\x05AUDIO\x10\x01\x12\t\n\x05VIDEO\x10\x02\x1a\0**\n\x0fPassthro\ - ughEnum\x12\x0b\n\x07UNKNOWN\x10\0\x12\x08\n\x04NONE\x10\x01\x1a\0B\0b\ - \x06proto2\ + \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\x12\x0b\n\ + \x07ALLOWED\x10\x02\x1a\0B\0b\x06proto2\ "; static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy {