mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
WIP: Podcast support
This commit is contained in:
parent
cbba63f60b
commit
0cb7a3f7c8
7 changed files with 3073 additions and 56 deletions
|
@ -168,6 +168,7 @@ fn initial_device_state(config: ConnectConfig) -> DeviceState {
|
|||
let repeated = msg.mut_stringValue();
|
||||
repeated.push(::std::convert::Into::into("audio/local"));
|
||||
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("track"))
|
||||
};
|
||||
|
@ -796,6 +797,7 @@ impl SpircTask {
|
|||
}
|
||||
|
||||
fn update_tracks(&mut self, frame: &protocol::spirc::Frame) {
|
||||
// debug!("State: {:?}", frame.get_state());
|
||||
let index = frame.get_state().get_playing_track_index();
|
||||
let context_uri = frame.get_state().get_context_uri().to_owned();
|
||||
let tracks = frame.get_state().get_track();
|
||||
|
@ -812,7 +814,14 @@ impl SpircTask {
|
|||
}
|
||||
|
||||
fn load_track(&mut self, play: bool) {
|
||||
let track = {
|
||||
let context_uri = self.state.get_context_uri().to_owned();
|
||||
let index = self.state.get_playing_track_index();
|
||||
info!("context: {}", context_uri);
|
||||
// Redundant check here
|
||||
let track = if context_uri.contains(":show:") || context_uri.contains(":episode:") {
|
||||
let uri = self.state.get_track()[index as usize].get_uri();
|
||||
SpotifyId::from_uri(uri).expect("Unable to parse uri")
|
||||
} else {
|
||||
let mut index = self.state.get_playing_track_index();
|
||||
// Check for malformed gid
|
||||
let tracks_len = self.state.get_track().len() as u32;
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
use std;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct SpotifyId(u128);
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum SpotifyTrackType {
|
||||
Track,
|
||||
Podcast,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct SpotifyId {
|
||||
pub id: u128,
|
||||
pub track_type: SpotifyTrackType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct SpotifyIdError;
|
||||
|
@ -11,6 +20,13 @@ const BASE62_DIGITS: &'static [u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDE
|
|||
const BASE16_DIGITS: &'static [u8] = b"0123456789abcdef";
|
||||
|
||||
impl SpotifyId {
|
||||
fn as_track(n: u128) -> SpotifyId {
|
||||
SpotifyId {
|
||||
id: n.to_owned(),
|
||||
track_type: SpotifyTrackType::Track,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_base16(id: &str) -> Result<SpotifyId, SpotifyIdError> {
|
||||
let data = id.as_bytes();
|
||||
|
||||
|
@ -24,7 +40,7 @@ impl SpotifyId {
|
|||
n = n + d;
|
||||
}
|
||||
|
||||
Ok(SpotifyId(n))
|
||||
Ok(SpotifyId::as_track(n))
|
||||
}
|
||||
|
||||
pub fn from_base62(id: &str) -> Result<SpotifyId, SpotifyIdError> {
|
||||
|
@ -39,8 +55,7 @@ impl SpotifyId {
|
|||
n = n * 62;
|
||||
n = n + d;
|
||||
}
|
||||
|
||||
Ok(SpotifyId(n))
|
||||
Ok(SpotifyId::as_track(n))
|
||||
}
|
||||
|
||||
pub fn from_raw(data: &[u8]) -> Result<SpotifyId, SpotifyIdError> {
|
||||
|
@ -51,15 +66,26 @@ impl SpotifyId {
|
|||
let mut arr: [u8; 16] = Default::default();
|
||||
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>>();
|
||||
if uri.contains(":show:") || uri.contains(":episode:") {
|
||||
let mut spotify_id = SpotifyId::from_base62(parts[2]).unwrap();
|
||||
spotify_id.track_type = SpotifyTrackType::Podcast;
|
||||
Ok(spotify_id)
|
||||
} else {
|
||||
SpotifyId::from_base62(parts[2])
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_base16(&self) -> String {
|
||||
format!("{:032x}", self.0)
|
||||
format!("{:032x}", self.id)
|
||||
}
|
||||
|
||||
pub fn to_base62(&self) -> String {
|
||||
let &SpotifyId(mut n) = self;
|
||||
let &SpotifyId { id: mut n, .. } = self;
|
||||
|
||||
let mut data = [0u8; 22];
|
||||
for i in 0..22 {
|
||||
|
@ -71,7 +97,7 @@ impl SpotifyId {
|
|||
}
|
||||
|
||||
pub fn to_raw(&self) -> [u8; 16] {
|
||||
self.0.to_be_bytes()
|
||||
self.id.to_be_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -93,6 +93,28 @@ pub struct Album {
|
|||
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 episodes: Vec<SpotifyId>,
|
||||
pub covers: Vec<FileId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Artist {
|
||||
pub id: SpotifyId,
|
||||
|
@ -222,6 +244,91 @@ 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(),
|
||||
episodes: episodes,
|
||||
covers: covers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StrChunks<'s>(&'s str, usize);
|
||||
|
||||
trait StrChunksExt {
|
||||
|
|
|
@ -12,12 +12,12 @@ use std::time::Duration;
|
|||
|
||||
use config::{Bitrate, PlayerConfig};
|
||||
use librespot_core::session::Session;
|
||||
use librespot_core::spotify_id::SpotifyId;
|
||||
use librespot_core::spotify_id::{FileId, SpotifyId, SpotifyTrackType};
|
||||
|
||||
use audio::{AudioDecrypt, AudioFile};
|
||||
use audio::{VorbisDecoder, VorbisPacket};
|
||||
use audio_backend::Sink;
|
||||
use metadata::{FileFormat, Metadata, Track};
|
||||
use metadata::{Episode, FileFormat, Metadata, Track};
|
||||
use mixer::AudioFilter;
|
||||
|
||||
pub struct Player {
|
||||
|
@ -526,44 +526,77 @@ impl PlayerInternal {
|
|||
}
|
||||
}
|
||||
|
||||
fn load_track(&self, track_id: SpotifyId, position: i64) -> Option<(Decoder, f32)> {
|
||||
let track = Track::get(&self.session, track_id).wait().unwrap();
|
||||
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,
|
||||
track_id.to_base62()
|
||||
);
|
||||
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 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);
|
||||
}
|
||||
|
||||
let format = match self.config.bitrate {
|
||||
Bitrate::Bitrate96 => FileFormat::OGG_VORBIS_96,
|
||||
Bitrate::Bitrate160 => FileFormat::OGG_VORBIS_160,
|
||||
Bitrate::Bitrate320 => FileFormat::OGG_VORBIS_320,
|
||||
};
|
||||
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 track.files.get(&format) {
|
||||
Some(&file_id) => file_id,
|
||||
None => {
|
||||
warn!("Track \"{}\" is not available in format {:?}", track.name, format);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let key = self
|
||||
.session
|
||||
.audio_key()
|
||||
.request(track.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 = encrypted_file.wait().unwrap();
|
||||
let key = key.wait().unwrap();
|
||||
let mut decrypted_file = AudioDecrypt::new(key, encrypted_file);
|
||||
|
@ -587,7 +620,7 @@ impl PlayerInternal {
|
|||
}
|
||||
}
|
||||
|
||||
info!("Track \"{}\" loaded", track.name);
|
||||
// info!("Track \"{}\" loaded", track.name);
|
||||
|
||||
Some((decoder, normalisation_factor))
|
||||
}
|
||||
|
|
9
protocol/files.rs
Normal file
9
protocol/files.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
// Autogenerated by build.rs
|
||||
pub const FILES: &'static [(&'static str, u32)] = &[
|
||||
("proto/authentication.proto", 2098196376),
|
||||
("proto/keyexchange.proto", 451735664),
|
||||
("proto/mercury.proto", 709993906),
|
||||
("proto/metadata.proto", 1409162985),
|
||||
("proto/pubsub.proto", 2686584829),
|
||||
("proto/spirc.proto", 1587493382),
|
||||
];
|
|
@ -156,12 +156,87 @@ message AudioFile {
|
|||
MP3_160 = 0x5;
|
||||
MP3_96 = 0x6;
|
||||
MP3_160_ENC = 0x7;
|
||||
OTHER2 = 0x8;
|
||||
MP4_128_DUAL = 0x8;
|
||||
OTHER3 = 0x9;
|
||||
AAC_160 = 0xa;
|
||||
AAC_320 = 0xb;
|
||||
OTHER4 = 0xc;
|
||||
MP4_128 = 0xc;
|
||||
OTHER5 = 0xd;
|
||||
}
|
||||
}
|
||||
|
||||
// Podcast Protos
|
||||
message PublishTime {
|
||||
optional sint32 year = 0x1;
|
||||
optional sint32 month = 0x2;
|
||||
optional sint32 day = 0x3;
|
||||
// These seem to be differently encoded
|
||||
optional sint32 minute = 0x5;
|
||||
optional sint32 hour = 0x4;
|
||||
}
|
||||
|
||||
message Show {
|
||||
optional bytes gid = 0x1;
|
||||
optional string name = 0x2;
|
||||
repeated Episode episode = 0x46;
|
||||
// Educated guesses
|
||||
optional string description = 0x40;
|
||||
optional string publisher = 0x42;
|
||||
optional string language = 0x43;
|
||||
optional bool explicit = 0x44;
|
||||
optional ImageGroup covers = 0x45;
|
||||
repeated Restriction restriction = 0x48;
|
||||
optional MediaType media_type = 0x4A;
|
||||
optional ConsumptionOrder consumption_order = 0x4B;
|
||||
optional bool interpret_restriction_using_geoip = 0x4C;
|
||||
optional string country_of_origin = 0x4F;
|
||||
repeated Category categories = 0x50;
|
||||
optional PassthroughEnum passthrough = 0x51;
|
||||
}
|
||||
|
||||
enum ConsumptionOrder {
|
||||
SEQUENTIAL = 1;
|
||||
EPISODIC = 2;
|
||||
RECENT = 3;
|
||||
}
|
||||
enum MediaType {
|
||||
MIXED = 0;
|
||||
AUDIO = 1;
|
||||
VIDEO = 2;
|
||||
}
|
||||
enum PassthroughEnum {
|
||||
UNKNOWN = 0;
|
||||
NONE = 1;
|
||||
}
|
||||
|
||||
message Episode {
|
||||
optional bytes gid = 0x1;
|
||||
optional string name = 0x2;
|
||||
optional sint32 duration = 0x7;
|
||||
optional sint32 popularity = 0x8;
|
||||
repeated AudioFile file = 0xc;
|
||||
// Educated guesses
|
||||
optional string description = 0x40;
|
||||
optional Date publish_time = 0x42;
|
||||
optional ImageGroup covers = 0x44;
|
||||
optional string language = 0x45;
|
||||
optional bool explicit = 0x46;
|
||||
optional Show show = 0x47;
|
||||
repeated AudioFile preview = 0x4A;
|
||||
repeated Restriction restriction = 0x4B;
|
||||
// Order of these flags might be wrong!
|
||||
optional bool suppress_monetization = 0x4E;
|
||||
optional bool allow_background_playback = 0x4F;
|
||||
optional bool interpret_restriction_using_geoip = 0x51;
|
||||
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