From 96b432aa4c0993eb79dfde3364b322f0cda1e10b Mon Sep 17 00:00:00 2001 From: ashthespy Date: Fri, 12 Oct 2018 19:15:26 +0200 Subject: [PATCH 1/4] Implement support for dynamic playlists (Radio) --- connect/src/lib.rs | 3 + connect/src/spirc.rs | 133 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 133 insertions(+), 3 deletions(-) diff --git a/connect/src/lib.rs b/connect/src/lib.rs index 27ec2b03..f1eb9972 100644 --- a/connect/src/lib.rs +++ b/connect/src/lib.rs @@ -2,6 +2,9 @@ extern crate log; #[macro_use] extern crate serde_json; +#[macro_use] +extern crate serde_derive; +extern crate serde; extern crate base64; extern crate crypto; diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 987b9daa..54be3534 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -12,16 +12,60 @@ use core::version; use core::volume::Volume; use protocol; -use protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State}; +use protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State, TrackRef}; use playback::mixer::Mixer; use playback::player::Player; +use serde; +use serde_json; use rand; use rand::seq::SliceRandom; use std; use std::time::{SystemTime, UNIX_EPOCH}; +// Keep this here for now + +#[derive(Deserialize, Debug)] +struct TrackContext { + album_uri: String, + artist_uri: String, + // metadata: String, + #[serde(rename = "original_gid")] + gid: String, + uid: String, + uri: String, +} +#[derive(Deserialize, Debug)] +struct StationContext { + uri: String, + next_page_url: String, + seeds: Vec, + #[serde(deserialize_with = "deserialize_protobuf_TrackRef")] + tracks: Vec, +} + +#[allow(non_snake_case)] +fn deserialize_protobuf_TrackRef(de: D) -> Result, D::Error> +where + D: serde::Deserializer, +{ + let v: Vec = try!(serde::Deserialize::deserialize(de)); + let track_vec = v + .iter() + .map(|v| { + let mut t = TrackRef::new(); + // This has got to be the most round about way of doing this. + t.set_gid(SpotifyId::from_base62(&v.gid).unwrap().to_raw().to_vec()); + t.set_uri(v.uri.to_owned()); + + t + }) + .collect::>(); + + Ok(track_vec) +} + pub struct SpircTask { player: Player, mixer: Box, @@ -40,6 +84,8 @@ pub struct SpircTask { shutdown: bool, session: Session, + context_fut: Box>, + context: Option, } pub enum SpircCommand { @@ -139,6 +185,15 @@ fn initial_device_state(config: ConnectConfig) -> DeviceState { }; msg }; + { + let msg = repeated.push_default(); + msg.set_typ(protocol::spirc::CapabilityType::kSupportsPlaylistV2); + { + let repeated = msg.mut_intValue(); + repeated.push(64) + }; + msg + }; { let msg = repeated.push_default(); msg.set_typ(protocol::spirc::CapabilityType::kSupportedContexts); @@ -176,7 +231,7 @@ fn calc_logarithmic_volume(volume: u16) -> u16 { // Volume conversion taken from https://www.dr-lex.be/info-stuff/volumecontrols.html#ideal2 // Convert the given volume [0..0xffff] to a dB gain // We assume a dB range of 60dB. - // Use the equatation: a * exp(b * x) + // Use the equation: a * exp(b * x) // in which a = IDEAL_FACTOR, b = 1/1000 const IDEAL_FACTOR: f64 = 6.908; let normalized_volume = volume as f64 / std::u16::MAX as f64; // To get a value between 0 and 1 @@ -259,6 +314,9 @@ impl Spirc { shutdown: false, session: session.clone(), + + context_fut: Box::new(future::empty()), + context: None, }; task.set_volume(volume); @@ -335,6 +393,25 @@ impl Future for SpircTask { Ok(Async::NotReady) => (), Err(oneshot::Canceled) => self.end_of_track = Box::new(future::empty()), } + + match self.context_fut.poll() { + Ok(Async::Ready(value)) => { + let r_context = serde_json::from_value::(value).ok(); + debug!("Radio Context: {:#?}", r_context); + if let Some(ref context) = r_context { + warn!("Got {:?} tracks from <{}>", context.tracks.len(), context.uri); + } + self.context = r_context; + + progress = true; + self.context_fut = Box::new(future::empty()); + } + Ok(Async::NotReady) => (), + Err(err) => { + self.context_fut = Box::new(future::empty()); + error!("Error: {:?}", err) + } + } } let poll_sender = self.sender.poll_complete().unwrap(); @@ -455,6 +532,7 @@ impl SpircTask { let play = frame.get_state().get_status() == PlayStatus::kPlayStatusPlay; self.load_track(play); } else { + info!("No more tracks left in queue"); self.state.set_status(PlayStatus::kPlayStatusStop); } @@ -600,6 +678,19 @@ impl SpircTask { fn handle_next(&mut self) { let mut new_index = self.consume_queued_track() as u32; let mut continue_playing = true; + debug!( + "At track {:?} of {:?} <{:?}> update [{}]", + new_index, + self.state.get_track().len(), + self.state.get_context_uri(), + self.state.get_track().len() as u32 - new_index < 5 + ); + let context_uri = self.state.get_context_uri().to_owned(); + if context_uri.contains("station") && ((self.state.get_track().len() as u32) - new_index) < 5 { + self.context_fut = self.resolve_station(&context_uri); + self.update_tracks_from_context(); + } + if new_index >= self.state.get_track().len() as u32 { new_index = 0; // Loop around back to start continue_playing = self.state.get_repeat(); @@ -680,10 +771,46 @@ impl SpircTask { self.state.get_position_ms() + diff as u32 } + fn resolve_station(&self, uri: &str) -> Box> { + let radio_uri = format!("hm://radio-apollo/v3/stations/{}", uri); + + self.resolve_uri(&radio_uri) + } + + fn resolve_uri(&self, uri: &str) -> Box> { + let request = self.session.mercury().get(uri); + + Box::new(request.and_then(move |response| { + let data = response.payload.first().expect("Empty payload on context uri"); + let response: serde_json::Value = serde_json::from_slice(&data).unwrap(); + + Ok(response) + })) + } + + fn update_tracks_from_context(&mut self) { + if let Some(ref context) = self.context { + self.context_fut = self.resolve_uri(&context.next_page_url); + + let new_tracks = &context.tracks; + debug!("Adding {:?} tracks from context to playlist", new_tracks.len()); + // Can we just push the new tracks and forget it? + let tracks = self.state.mut_track(); + // tracks.append(new_tracks.to_owned()); + for t in new_tracks { + tracks.push(t.to_owned()); + } + } + } + fn update_tracks(&mut self, frame: &protocol::spirc::Frame) { let index = frame.get_state().get_playing_track_index(); - let tracks = frame.get_state().get_track(); let context_uri = frame.get_state().get_context_uri().to_owned(); + let tracks = frame.get_state().get_track(); + debug!("Frame has {:?} tracks", tracks.len()); + if context_uri.contains("station") { + self.context_fut = self.resolve_station(&context_uri); + } self.state.set_playing_track_index(index); self.state.set_track(tracks.into_iter().cloned().collect()); From c0416972b6492ebc693cb924e162d8298fc35467 Mon Sep 17 00:00:00 2001 From: ashthespy Date: Wed, 13 Mar 2019 20:35:46 +0100 Subject: [PATCH 2/4] Support Dailymixes and refactor dynamic playlists --- connect/src/context.rs | 86 +++++++++++++++++++++++++++++++++++ connect/src/lib.rs | 1 + connect/src/spirc.rs | 101 +++++++++++++++++++---------------------- 3 files changed, 133 insertions(+), 55 deletions(-) create mode 100644 connect/src/context.rs diff --git a/connect/src/context.rs b/connect/src/context.rs new file mode 100644 index 00000000..36e55711 --- /dev/null +++ b/connect/src/context.rs @@ -0,0 +1,86 @@ +use core::spotify_id::SpotifyId; +use protocol::spirc::TrackRef; + +use serde; + +#[derive(Deserialize, Debug)] +pub struct StationContext { + pub uri: Option, + pub next_page_url: String, + #[serde(deserialize_with = "deserialize_protobuf_TrackRef")] + pub tracks: Vec, + // Not required for core functionality + // pub seeds: Vec, + // #[serde(rename = "imageUri")] + // pub image_uri: String, + // pub subtitle: Option, + // pub subtitles: Vec, + // #[serde(rename = "subtitleUri")] + // pub subtitle_uri: Option, + // pub title: String, + // #[serde(rename = "titleUri")] + // pub title_uri: String, + // pub related_artists: Vec, +} + +#[derive(Deserialize, Debug)] +pub struct PageContext { + pub uri: String, + pub next_page_url: String, + #[serde(deserialize_with = "deserialize_protobuf_TrackRef")] + pub tracks: Vec, + // Not required for core functionality + // pub url: String, + // // pub restrictions: +} + +#[derive(Deserialize, Debug)] +pub struct TrackContext { + #[serde(rename = "original_gid")] + pub gid: String, + pub uri: String, + pub uid: String, + // Not required for core functionality + // pub album_uri: String, + // pub artist_uri: String, + // pub metadata: MetadataContext, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ArtistContext { + artist_name: String, + artist_uri: String, + image_uri: String, +} + +#[derive(Deserialize, Debug)] +pub struct MetadataContext { + album_title: String, + artist_name: String, + artist_uri: String, + image_url: String, + title: String, + uid: String, +} + +#[allow(non_snake_case)] +fn deserialize_protobuf_TrackRef(de: D) -> Result, D::Error> +where + D: serde::Deserializer, +{ + let v: Vec = try!(serde::Deserialize::deserialize(de)); + let track_vec = v + .iter() + .map(|v| { + let mut t = TrackRef::new(); + // This has got to be the most round about way of doing this. + t.set_gid(SpotifyId::from_base62(&v.gid).unwrap().to_raw().to_vec()); + t.set_uri(v.uri.to_owned()); + + t + }) + .collect::>(); + + Ok(track_vec) +} diff --git a/connect/src/lib.rs b/connect/src/lib.rs index f1eb9972..9dad97f1 100644 --- a/connect/src/lib.rs +++ b/connect/src/lib.rs @@ -26,5 +26,6 @@ extern crate librespot_core as core; extern crate librespot_playback as playback; extern crate librespot_protocol as protocol; +pub mod context; pub mod discovery; pub mod spirc; diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 54be3534..e810ef9a 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -12,60 +12,18 @@ use core::version; use core::volume::Volume; use protocol; -use protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State, TrackRef}; +use protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State}; use playback::mixer::Mixer; use playback::player::Player; -use serde; use serde_json; +use context::StationContext; use rand; use rand::seq::SliceRandom; use std; use std::time::{SystemTime, UNIX_EPOCH}; -// Keep this here for now - -#[derive(Deserialize, Debug)] -struct TrackContext { - album_uri: String, - artist_uri: String, - // metadata: String, - #[serde(rename = "original_gid")] - gid: String, - uid: String, - uri: String, -} -#[derive(Deserialize, Debug)] -struct StationContext { - uri: String, - next_page_url: String, - seeds: Vec, - #[serde(deserialize_with = "deserialize_protobuf_TrackRef")] - tracks: Vec, -} - -#[allow(non_snake_case)] -fn deserialize_protobuf_TrackRef(de: D) -> Result, D::Error> -where - D: serde::Deserializer, -{ - let v: Vec = try!(serde::Deserialize::deserialize(de)); - let track_vec = v - .iter() - .map(|v| { - let mut t = TrackRef::new(); - // This has got to be the most round about way of doing this. - t.set_gid(SpotifyId::from_base62(&v.gid).unwrap().to_raw().to_vec()); - t.set_uri(v.uri.to_owned()); - - t - }) - .collect::>(); - - Ok(track_vec) -} - pub struct SpircTask { player: Player, mixer: Box, @@ -396,12 +354,26 @@ impl Future for SpircTask { match self.context_fut.poll() { Ok(Async::Ready(value)) => { - let r_context = serde_json::from_value::(value).ok(); - debug!("Radio Context: {:#?}", r_context); - if let Some(ref context) = r_context { - warn!("Got {:?} tracks from <{}>", context.tracks.len(), context.uri); - } - self.context = r_context; + let r_context = serde_json::from_value::(value.clone()); + self.context = match r_context { + Ok(context) => { + info!( + "Resolved {:?} tracks from <{:?}>", + context.tracks.len(), + self.state.get_context_uri(), + ); + Some(context) + } + Err(e) => { + error!("Unable to parse JSONContext {:?}\n{:?}", e, value); + None + } + }; + // It needn't be so verbose - can be as simple as + // if let Some(ref context) = r_context { + // info!("Got {:?} tracks from <{}>", context.tracks.len(), context.uri); + // } + // self.context = r_context; progress = true; self.context_fut = Box::new(future::empty()); @@ -409,7 +381,7 @@ impl Future for SpircTask { Ok(Async::NotReady) => (), Err(err) => { self.context_fut = Box::new(future::empty()); - error!("Error: {:?}", err) + error!("ContextError: {:?}", err) } } } @@ -686,7 +658,9 @@ impl SpircTask { self.state.get_track().len() as u32 - new_index < 5 ); let context_uri = self.state.get_context_uri().to_owned(); - if context_uri.contains("station") && ((self.state.get_track().len() as u32) - new_index) < 5 { + if (context_uri.starts_with("spotify:station:") || context_uri.starts_with("spotify:dailymix:")) + && ((self.state.get_track().len() as u32) - new_index) < 5 + { self.context_fut = self.resolve_station(&context_uri); self.update_tracks_from_context(); } @@ -808,7 +782,7 @@ impl SpircTask { let context_uri = frame.get_state().get_context_uri().to_owned(); let tracks = frame.get_state().get_track(); debug!("Frame has {:?} tracks", tracks.len()); - if context_uri.contains("station") { + if context_uri.starts_with("spotify:station:") || context_uri.starts_with("spotify:dailymix:") { self.context_fut = self.resolve_station(&context_uri); } @@ -820,9 +794,26 @@ impl SpircTask { } fn load_track(&mut self, play: bool) { - let index = self.state.get_playing_track_index(); + let mut index = self.state.get_playing_track_index(); let track = { - let gid = self.state.get_track()[index as usize].get_gid(); + // let gid = self.state.get_track()[index as usize].get_gid(); + let track_ref = self.state.get_track()[index as usize].to_owned(); + let mut gid = track_ref.get_gid(); + if gid.len() != 16 { + let track_len = self.state.get_track().len() as u32; + if index < track_len - 1 { + index = 0; + } else { + index = index + 1; + } + warn!( + "Skipping track {:?} at position [{}] of {}", + track_ref.get_uri(), + index, + track_len + ); + gid = self.state.get_track()[index as usize].get_gid(); + } SpotifyId::from_raw(gid).unwrap() }; let position = self.state.get_position_ms(); From b0ee8ec74d40379147b8e1d404199f5b0df1ceaa Mon Sep 17 00:00:00 2001 From: ashthespy Date: Fri, 15 Mar 2019 08:26:58 +0100 Subject: [PATCH 3/4] Tweak malformed gid handling --- connect/src/spirc.rs | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index e810ef9a..db4c3add 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -794,30 +794,25 @@ impl SpircTask { } fn load_track(&mut self, play: bool) { - let mut index = self.state.get_playing_track_index(); let track = { - // let gid = self.state.get_track()[index as usize].get_gid(); - let track_ref = self.state.get_track()[index as usize].to_owned(); - let mut gid = track_ref.get_gid(); - if gid.len() != 16 { - let track_len = self.state.get_track().len() as u32; - if index < track_len - 1 { - index = 0; - } else { - index = index + 1; - } + let mut index = self.state.get_playing_track_index(); + // Check for malformed gid + let tracks_len = self.state.get_track().len() as u32; + let mut track_ref = &self.state.get_track()[index as usize]; + while track_ref.get_gid().len() != 16 { warn!( "Skipping track {:?} at position [{}] of {}", track_ref.get_uri(), index, - track_len + tracks_len ); - gid = self.state.get_track()[index as usize].get_gid(); + index = if index + 1 < tracks_len { index + 1 } else { 0 }; + track_ref = &self.state.get_track()[index as usize]; } - SpotifyId::from_raw(gid).unwrap() + SpotifyId::from_raw(track_ref.get_gid()).unwrap() }; - let position = self.state.get_position_ms(); + let position = self.state.get_position_ms(); let end_of_track = self.player.load(track, play, position); if play { From 6870c76a439451cf3aa03a8e08e247650fa17b3c Mon Sep 17 00:00:00 2001 From: ashthespy Date: Sat, 16 Mar 2019 16:18:38 +0100 Subject: [PATCH 4/4] Limit new context tracks added to frame Keep only a fixed history of previous tracks to prior pushing new tracks --- connect/src/spirc.rs | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index db4c3add..4bb53e77 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -57,6 +57,9 @@ pub enum SpircCommand { Shutdown, } +const CONTEXT_TRACKS_HISTORY: usize = 10; +const CONTEXT_FETCH_THRESHOLD: u32 = 5; + pub struct Spirc { commands: mpsc::UnboundedSender, } @@ -655,11 +658,11 @@ impl SpircTask { new_index, self.state.get_track().len(), self.state.get_context_uri(), - self.state.get_track().len() as u32 - new_index < 5 + self.state.get_track().len() as u32 - new_index < CONTEXT_FETCH_THRESHOLD ); let context_uri = self.state.get_context_uri().to_owned(); if (context_uri.starts_with("spotify:station:") || context_uri.starts_with("spotify:dailymix:")) - && ((self.state.get_track().len() as u32) - new_index) < 5 + && ((self.state.get_track().len() as u32) - new_index) < CONTEXT_FETCH_THRESHOLD { self.context_fut = self.resolve_station(&context_uri); self.update_tracks_from_context(); @@ -768,12 +771,25 @@ impl SpircTask { let new_tracks = &context.tracks; debug!("Adding {:?} tracks from context to playlist", new_tracks.len()); - // Can we just push the new tracks and forget it? - let tracks = self.state.mut_track(); - // tracks.append(new_tracks.to_owned()); - for t in new_tracks { - tracks.push(t.to_owned()); + let current_index = self.state.get_playing_track_index(); + let mut new_index = 0; + { + let mut tracks = self.state.mut_track(); + // Does this need to be optimised - we don't need to actually traverse the len of tracks + let tracks_len = tracks.len(); + if tracks_len > CONTEXT_TRACKS_HISTORY { + tracks.rotate_right(tracks_len - CONTEXT_TRACKS_HISTORY); + tracks.truncate(CONTEXT_TRACKS_HISTORY); + } + // tracks.extend_from_slice(&mut new_tracks); // method doesn't exist for protobuf::RepeatedField + for t in new_tracks { + tracks.push(t.to_owned()); + } + if current_index > CONTEXT_TRACKS_HISTORY as u32 { + new_index = current_index - CONTEXT_TRACKS_HISTORY as u32; + } } + self.state.set_playing_track_index(new_index); } }