mirror of
https://github.com/librespot-org/librespot.git
synced 2024-11-08 16:45:43 +00:00
Retrieve autoplay contexts over HTTPS and fix repeat/prev/next
Repeat, previous and next used to start playback regardless of the actual playback state. They now start playback only if we were already playing.
This commit is contained in:
parent
6dc7a11b09
commit
bfb7d5689c
4 changed files with 143 additions and 149 deletions
|
@ -40,6 +40,9 @@ https://github.com/librespot-org/librespot
|
||||||
- [audio] Improve file opening and seeking performance (breaking)
|
- [audio] Improve file opening and seeking performance (breaking)
|
||||||
- [chore] MSRV is now 1.61 (breaking)
|
- [chore] MSRV is now 1.61 (breaking)
|
||||||
- [connect] `DeviceType` moved out of `connect` into `core` (breaking)
|
- [connect] `DeviceType` moved out of `connect` into `core` (breaking)
|
||||||
|
- [connect] Update and expose all `spirc` context fields (breaking)
|
||||||
|
- [connect] Add `Clone, Defaut` traits to `spirc` contexts
|
||||||
|
- [connect] Autoplay contexts are now retrieved with the `spclient` (breaking)
|
||||||
- [core] Message listeners are registered before authenticating. As a result
|
- [core] Message listeners are registered before authenticating. As a result
|
||||||
there now is a separate `Session::new` and subsequent `session.connect`.
|
there now is a separate `Session::new` and subsequent `session.connect`.
|
||||||
(breaking)
|
(breaking)
|
||||||
|
@ -96,6 +99,8 @@ https://github.com/librespot-org/librespot
|
||||||
`LoadingPause` in `spirc.rs`
|
`LoadingPause` in `spirc.rs`
|
||||||
- [connect] Handle attempts to play local files better by basically ignoring
|
- [connect] Handle attempts to play local files better by basically ignoring
|
||||||
attempts to load them in `handle_remote_update` in `spirc.rs`
|
attempts to load them in `handle_remote_update` in `spirc.rs`
|
||||||
|
- [connect] Loading previous or next tracks, or looping back on repeat, will
|
||||||
|
only start playback when we were already playing
|
||||||
- [connect, playback] Clean up and de-noise events and event firing
|
- [connect, playback] Clean up and de-noise events and event firing
|
||||||
- [playback] Handle invalid track start positions by just starting the track
|
- [playback] Handle invalid track start positions by just starting the track
|
||||||
from the beginning
|
from the beginning
|
||||||
|
|
|
@ -8,67 +8,89 @@ use serde::{
|
||||||
Deserialize,
|
Deserialize,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug, Default, Clone)]
|
||||||
pub struct StationContext {
|
pub struct StationContext {
|
||||||
pub uri: Option<String>,
|
|
||||||
pub next_page_url: String,
|
|
||||||
#[serde(deserialize_with = "deserialize_protobuf_TrackRef")]
|
|
||||||
pub tracks: Vec<TrackRef>,
|
|
||||||
// Not required for core functionality
|
|
||||||
// pub seeds: Vec<String>,
|
|
||||||
// #[serde(rename = "imageUri")]
|
|
||||||
// pub image_uri: String,
|
|
||||||
// pub subtitle: Option<String>,
|
|
||||||
// pub subtitles: Vec<String>,
|
|
||||||
// #[serde(rename = "subtitleUri")]
|
|
||||||
// pub subtitle_uri: Option<String>,
|
|
||||||
// pub title: String,
|
|
||||||
// #[serde(rename = "titleUri")]
|
|
||||||
// pub title_uri: String,
|
|
||||||
// pub related_artists: Vec<ArtistContext>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
pub struct PageContext {
|
|
||||||
pub uri: String,
|
pub uri: String,
|
||||||
pub next_page_url: String,
|
pub title: String,
|
||||||
|
#[serde(rename = "titleUri")]
|
||||||
|
pub title_uri: String,
|
||||||
|
pub subtitles: Vec<SubtitleContext>,
|
||||||
|
#[serde(rename = "imageUri")]
|
||||||
|
pub image_uri: String,
|
||||||
|
pub seeds: Vec<String>,
|
||||||
#[serde(deserialize_with = "deserialize_protobuf_TrackRef")]
|
#[serde(deserialize_with = "deserialize_protobuf_TrackRef")]
|
||||||
pub tracks: Vec<TrackRef>,
|
pub tracks: Vec<TrackRef>,
|
||||||
// Not required for core functionality
|
pub next_page_url: String,
|
||||||
// pub url: String,
|
pub correlation_id: String,
|
||||||
// // pub restrictions:
|
pub related_artists: Vec<ArtistContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug, Default, Clone)]
|
||||||
|
pub struct PageContext {
|
||||||
|
#[serde(deserialize_with = "deserialize_protobuf_TrackRef")]
|
||||||
|
pub tracks: Vec<TrackRef>,
|
||||||
|
pub next_page_url: String,
|
||||||
|
pub correlation_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Default, Clone)]
|
||||||
pub struct TrackContext {
|
pub struct TrackContext {
|
||||||
#[serde(rename = "original_gid")]
|
|
||||||
pub gid: String,
|
|
||||||
pub uri: String,
|
pub uri: String,
|
||||||
pub uid: String,
|
pub uid: String,
|
||||||
// Not required for core functionality
|
pub artist_uri: String,
|
||||||
// pub album_uri: String,
|
pub album_uri: String,
|
||||||
// pub artist_uri: String,
|
#[serde(rename = "original_gid")]
|
||||||
// pub metadata: MetadataContext,
|
pub gid: String,
|
||||||
|
pub metadata: MetadataContext,
|
||||||
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug, Default, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ArtistContext {
|
pub struct ArtistContext {
|
||||||
|
#[serde(rename = "artistName")]
|
||||||
artist_name: String,
|
artist_name: String,
|
||||||
artist_uri: String,
|
#[serde(rename = "imageUri")]
|
||||||
image_uri: String,
|
image_uri: String,
|
||||||
|
#[serde(rename = "artistUri")]
|
||||||
|
artist_uri: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug, Default, Clone)]
|
||||||
pub struct MetadataContext {
|
pub struct MetadataContext {
|
||||||
album_title: String,
|
album_title: String,
|
||||||
artist_name: String,
|
artist_name: String,
|
||||||
artist_uri: String,
|
artist_uri: String,
|
||||||
image_url: String,
|
image_url: String,
|
||||||
title: String,
|
title: String,
|
||||||
uid: String,
|
#[serde(deserialize_with = "bool_from_string")]
|
||||||
|
is_explicit: bool,
|
||||||
|
#[serde(deserialize_with = "bool_from_string")]
|
||||||
|
is_promotional: bool,
|
||||||
|
decision_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug, Default, Clone)]
|
||||||
|
pub struct SubtitleContext {
|
||||||
|
name: String,
|
||||||
|
uri: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bool_from_string<'de, D>(de: D) -> Result<bool, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
match String::deserialize(de)?.as_ref() {
|
||||||
|
"true" => Ok(true),
|
||||||
|
"false" => Ok(false),
|
||||||
|
other => Err(D::Error::invalid_value(
|
||||||
|
Unexpected::Str(other),
|
||||||
|
&"true or false",
|
||||||
|
)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
|
|
@ -6,11 +6,7 @@ use std::{
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
use futures_util::{
|
use futures_util::{stream::FusedStream, FutureExt, StreamExt};
|
||||||
future::{self, FusedFuture},
|
|
||||||
stream::FusedStream,
|
|
||||||
FutureExt, StreamExt,
|
|
||||||
};
|
|
||||||
|
|
||||||
use protobuf::{self, Message};
|
use protobuf::{self, Message};
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
|
@ -20,13 +16,10 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::ConnectConfig,
|
config::ConnectConfig,
|
||||||
context::StationContext,
|
context::{PageContext, StationContext},
|
||||||
core::{
|
core::{
|
||||||
authentication::Credentials,
|
authentication::Credentials, mercury::MercurySender, session::UserAttributes,
|
||||||
mercury::{MercuryError, MercurySender},
|
util::SeqGenerator, version, Error, Session, SpotifyId,
|
||||||
session::UserAttributes,
|
|
||||||
util::SeqGenerator,
|
|
||||||
version, Error, Session, SpotifyId,
|
|
||||||
},
|
},
|
||||||
playback::{
|
playback::{
|
||||||
mixer::Mixer,
|
mixer::Mixer,
|
||||||
|
@ -81,7 +74,6 @@ enum SpircPlayStatus {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type BoxedFuture<T> = Pin<Box<dyn FusedFuture<Output = T> + Send>>;
|
|
||||||
type BoxedStream<T> = Pin<Box<dyn FusedStream<Item = T> + Send>>;
|
type BoxedStream<T> = Pin<Box<dyn FusedStream<Item = T> + Send>>;
|
||||||
|
|
||||||
struct SpircTask {
|
struct SpircTask {
|
||||||
|
@ -106,8 +98,8 @@ struct SpircTask {
|
||||||
|
|
||||||
shutdown: bool,
|
shutdown: bool,
|
||||||
session: Session,
|
session: Session,
|
||||||
context_fut: BoxedFuture<Result<serde_json::Value, Error>>,
|
resolve_context: Option<String>,
|
||||||
autoplay_fut: BoxedFuture<Result<String, Error>>,
|
autoplay_context: bool,
|
||||||
context: Option<StationContext>,
|
context: Option<StationContext>,
|
||||||
|
|
||||||
spirc_id: usize,
|
spirc_id: usize,
|
||||||
|
@ -132,6 +124,7 @@ pub enum SpircCommand {
|
||||||
SetVolume(u16),
|
SetVolume(u16),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CONTEXT_TRACKS_COUNT: usize = 50;
|
||||||
const CONTEXT_TRACKS_HISTORY: usize = 10;
|
const CONTEXT_TRACKS_HISTORY: usize = 10;
|
||||||
const CONTEXT_FETCH_THRESHOLD: u32 = 5;
|
const CONTEXT_FETCH_THRESHOLD: u32 = 5;
|
||||||
|
|
||||||
|
@ -376,8 +369,8 @@ impl Spirc {
|
||||||
shutdown: false,
|
shutdown: false,
|
||||||
session,
|
session,
|
||||||
|
|
||||||
context_fut: Box::pin(future::pending()),
|
resolve_context: None,
|
||||||
autoplay_fut: Box::pin(future::pending()),
|
autoplay_context: false,
|
||||||
context: None,
|
context: None,
|
||||||
|
|
||||||
spirc_id,
|
spirc_id,
|
||||||
|
@ -504,10 +497,35 @@ impl SpircTask {
|
||||||
error!("Cannot flush spirc event sender.");
|
error!("Cannot flush spirc event sender.");
|
||||||
break;
|
break;
|
||||||
},
|
},
|
||||||
context = &mut self.context_fut, if !self.context_fut.is_terminated() => {
|
context_uri = async { self.resolve_context.take() }, if self.resolve_context.is_some() => {
|
||||||
|
let context_uri = context_uri.unwrap();
|
||||||
|
let is_next_page = context_uri.starts_with("hm://");
|
||||||
|
|
||||||
|
let context = if is_next_page {
|
||||||
|
self.session.spclient().get_next_page(&context_uri).await
|
||||||
|
} else {
|
||||||
|
let previous_tracks = self.state.get_track().iter().filter_map(|t| SpotifyId::try_from(t).ok()).collect();
|
||||||
|
self.session.spclient().get_apollo_station(&context_uri, CONTEXT_TRACKS_COUNT, previous_tracks, self.autoplay_context).await
|
||||||
|
};
|
||||||
|
|
||||||
match context {
|
match context {
|
||||||
Ok(value) => {
|
Ok(value) => {
|
||||||
let r_context = serde_json::from_value::<StationContext>(value);
|
let r_context = if is_next_page {
|
||||||
|
match serde_json::from_slice::<PageContext>(&value) {
|
||||||
|
Ok(page_context) => {
|
||||||
|
// page contexts don't have the stations full metadata, so decorate it
|
||||||
|
let mut station_context = self.context.clone().unwrap_or_default();
|
||||||
|
station_context.tracks = page_context.tracks;
|
||||||
|
station_context.next_page_url = page_context.next_page_url;
|
||||||
|
station_context.correlation_id = page_context.correlation_id;
|
||||||
|
Ok(station_context)
|
||||||
|
},
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
serde_json::from_slice::<StationContext>(&value)
|
||||||
|
};
|
||||||
|
|
||||||
self.context = match r_context {
|
self.context = match r_context {
|
||||||
Ok(context) => {
|
Ok(context) => {
|
||||||
info!(
|
info!(
|
||||||
|
@ -522,28 +540,12 @@ impl SpircTask {
|
||||||
None
|
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;
|
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("ContextError: {:?}", err)
|
error!("ContextError: {:?}", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
autoplay = &mut self.autoplay_fut, if !self.autoplay_fut.is_terminated() => {
|
|
||||||
match autoplay {
|
|
||||||
Ok(autoplay_station_uri) => {
|
|
||||||
info!("Autoplay uri resolved to <{:?}>", autoplay_station_uri);
|
|
||||||
self.context_fut = self.resolve_station(&autoplay_station_uri);
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
error!("AutoplayError: {:?}", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => break
|
else => break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -953,7 +955,7 @@ impl SpircTask {
|
||||||
MessageType::kMessageTypeShuffle => {
|
MessageType::kMessageTypeShuffle => {
|
||||||
let shuffle = update.get_state().get_shuffle();
|
let shuffle = update.get_state().get_shuffle();
|
||||||
self.state.set_shuffle(shuffle);
|
self.state.set_shuffle(shuffle);
|
||||||
if self.state.get_shuffle() {
|
if shuffle {
|
||||||
let current_index = self.state.get_playing_track_index();
|
let current_index = self.state.get_playing_track_index();
|
||||||
let tracks = self.state.mut_track();
|
let tracks = self.state.mut_track();
|
||||||
if !tracks.is_empty() {
|
if !tracks.is_empty() {
|
||||||
|
@ -964,11 +966,7 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
self.state.set_playing_track_index(0);
|
self.state.set_playing_track_index(0);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
let context = self.state.get_context_uri();
|
|
||||||
debug!("{:?}", context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.player.emit_shuffle_changed_event(shuffle);
|
self.player.emit_shuffle_changed_event(shuffle);
|
||||||
|
|
||||||
self.notify(None)
|
self.notify(None)
|
||||||
|
@ -1191,40 +1189,41 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_next(&mut self) {
|
fn handle_next(&mut self) {
|
||||||
|
let context_uri = self.state.get_context_uri().to_owned();
|
||||||
|
let mut tracks_len = self.state.get_track().len() as u32;
|
||||||
let mut new_index = self.consume_queued_track() as u32;
|
let mut new_index = self.consume_queued_track() as u32;
|
||||||
let mut continue_playing = true;
|
let mut continue_playing = self.state.get_status() == PlayStatus::kPlayStatusPlay;
|
||||||
let tracks_len = self.state.get_track().len() as u32;
|
|
||||||
|
let update_tracks =
|
||||||
|
self.autoplay_context && tracks_len - new_index < CONTEXT_FETCH_THRESHOLD;
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"At track {:?} of {:?} <{:?}> update [{}]",
|
"At track {:?} of {:?} <{:?}> update [{}]",
|
||||||
new_index + 1,
|
new_index + 1,
|
||||||
tracks_len,
|
tracks_len,
|
||||||
self.state.get_context_uri(),
|
context_uri,
|
||||||
tracks_len - new_index < CONTEXT_FETCH_THRESHOLD
|
update_tracks,
|
||||||
);
|
);
|
||||||
let context_uri = self.state.get_context_uri().to_owned();
|
|
||||||
if (context_uri.starts_with("spotify:station:")
|
// When in autoplay, keep topping up the playlist when it nears the end
|
||||||
|| context_uri.starts_with("spotify:dailymix:")
|
if update_tracks {
|
||||||
// spotify:user:xxx:collection
|
|
||||||
|| context_uri.starts_with(&format!("spotify:user:{}:collection",url_encode(&self.session.username()))))
|
|
||||||
&& ((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();
|
self.update_tracks_from_context();
|
||||||
|
new_index = self.state.get_playing_track_index();
|
||||||
|
tracks_len = self.state.get_track().len() as u32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When not in autoplay, either start autoplay or loop back to the start
|
||||||
if new_index >= tracks_len {
|
if new_index >= tracks_len {
|
||||||
if self.session.autoplay() {
|
if self.session.autoplay() {
|
||||||
// Extend the playlist
|
// Extend the playlist
|
||||||
debug!("Extending playlist <{}>", context_uri);
|
debug!("Starting autoplay for <{}>", context_uri);
|
||||||
|
self.autoplay_context = true;
|
||||||
self.update_tracks_from_context();
|
self.update_tracks_from_context();
|
||||||
self.player.set_auto_normalise_as_album(false);
|
self.player.set_auto_normalise_as_album(false);
|
||||||
} else {
|
} else {
|
||||||
new_index = 0;
|
new_index = 0;
|
||||||
continue_playing = self.state.get_repeat();
|
continue_playing &= self.state.get_repeat();
|
||||||
debug!(
|
debug!("Looping back to start, repeat is {}", continue_playing);
|
||||||
"Looping around back to start, repeat is {}",
|
|
||||||
continue_playing
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1271,7 +1270,8 @@ impl SpircTask {
|
||||||
|
|
||||||
self.state.set_playing_track_index(new_index);
|
self.state.set_playing_track_index(new_index);
|
||||||
|
|
||||||
self.load_track(true, 0);
|
let start_playing = self.state.get_status() == PlayStatus::kPlayStatusPlay;
|
||||||
|
self.load_track(start_playing, 0);
|
||||||
} else {
|
} else {
|
||||||
self.handle_seek(0);
|
self.handle_seek(0);
|
||||||
}
|
}
|
||||||
|
@ -1304,50 +1304,16 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_station(&self, uri: &str) -> BoxedFuture<Result<serde_json::Value, Error>> {
|
|
||||||
let radio_uri = format!("hm://radio-apollo/v3/stations/{}", uri);
|
|
||||||
|
|
||||||
self.resolve_uri(&radio_uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_autoplay_uri(&self, uri: &str) -> BoxedFuture<Result<String, Error>> {
|
|
||||||
let query_uri = format!("hm://autoplay-enabled/query?uri={}", uri);
|
|
||||||
let request = self.session.mercury().get(query_uri);
|
|
||||||
Box::pin(
|
|
||||||
async {
|
|
||||||
let response = request?.await?;
|
|
||||||
|
|
||||||
if response.status_code == 200 {
|
|
||||||
let data = response.payload.first().ok_or(SpircError::NoData)?.to_vec();
|
|
||||||
Ok(String::from_utf8(data)?)
|
|
||||||
} else {
|
|
||||||
warn!("No autoplay_uri found");
|
|
||||||
Err(MercuryError::Response(response).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.fuse(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_uri(&self, uri: &str) -> BoxedFuture<Result<serde_json::Value, Error>> {
|
|
||||||
let request = self.session.mercury().get(uri);
|
|
||||||
|
|
||||||
Box::pin(
|
|
||||||
async move {
|
|
||||||
let response = request?.await?;
|
|
||||||
|
|
||||||
let data = response.payload.first().ok_or(SpircError::NoData)?;
|
|
||||||
let response: serde_json::Value = serde_json::from_slice(data)?;
|
|
||||||
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
.fuse(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_tracks_from_context(&mut self) {
|
fn update_tracks_from_context(&mut self) {
|
||||||
if let Some(ref context) = self.context {
|
if let Some(ref context) = self.context {
|
||||||
self.context_fut = self.resolve_uri(&context.next_page_url);
|
self.resolve_context =
|
||||||
|
if !self.autoplay_context || context.next_page_url.contains("autoplay=true") {
|
||||||
|
Some(context.next_page_url.to_owned())
|
||||||
|
} else {
|
||||||
|
// this arm means: we need to resolve for autoplay,
|
||||||
|
// and were previously resolving for the original context
|
||||||
|
Some(context.uri.to_owned())
|
||||||
|
};
|
||||||
|
|
||||||
let new_tracks = &context.tracks;
|
let new_tracks = &context.tracks;
|
||||||
debug!("Adding {:?} tracks from context to frame", new_tracks.len());
|
debug!("Adding {:?} tracks from context to frame", new_tracks.len());
|
||||||
|
@ -1381,15 +1347,10 @@ impl SpircTask {
|
||||||
|
|
||||||
trace!("Frame has {:?} tracks", tracks.len());
|
trace!("Frame has {:?} tracks", tracks.len());
|
||||||
|
|
||||||
if context_uri.starts_with("spotify:station:")
|
// First the tracks from the requested context, without autoplay.
|
||||||
|| context_uri.starts_with("spotify:dailymix:")
|
// We will transition into autoplay after the latest track of this context.
|
||||||
{
|
self.autoplay_context = false;
|
||||||
self.context_fut = self.resolve_station(&context_uri);
|
self.resolve_context = Some(context_uri.clone());
|
||||||
} else if self.session.autoplay() {
|
|
||||||
info!("Fetching autoplay context uri");
|
|
||||||
// Get autoplay_station_uri for regular playlists
|
|
||||||
self.autoplay_fut = self.resolve_autoplay_uri(&context_uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.player
|
self.player
|
||||||
.set_auto_normalise_as_album(context_uri.starts_with("spotify:album:"));
|
.set_auto_normalise_as_album(context_uri.starts_with("spotify:album:"));
|
||||||
|
|
|
@ -616,7 +616,7 @@ impl SpClient {
|
||||||
pub async fn get_radio_for_track(&self, track_id: &SpotifyId) -> SpClientResult {
|
pub async fn get_radio_for_track(&self, track_id: &SpotifyId) -> SpClientResult {
|
||||||
let endpoint = format!(
|
let endpoint = format!(
|
||||||
"/inspiredby-mix/v2/seed_to_playlist/{}?response-format=json",
|
"/inspiredby-mix/v2/seed_to_playlist/{}?response-format=json",
|
||||||
track_id.to_uri()?
|
track_id.to_base62()?
|
||||||
);
|
);
|
||||||
|
|
||||||
self.request_as_json(&Method::GET, &endpoint, None, None)
|
self.request_as_json(&Method::GET, &endpoint, None, None)
|
||||||
|
@ -626,13 +626,13 @@ impl SpClient {
|
||||||
pub async fn get_apollo_station(
|
pub async fn get_apollo_station(
|
||||||
&self,
|
&self,
|
||||||
context_uri: &str,
|
context_uri: &str,
|
||||||
count: u32,
|
count: usize,
|
||||||
previous_tracks: Vec<&SpotifyId>,
|
previous_tracks: Vec<SpotifyId>,
|
||||||
autoplay: bool,
|
autoplay: bool,
|
||||||
) -> SpClientResult {
|
) -> SpClientResult {
|
||||||
let previous_track_str = previous_tracks
|
let previous_track_str = previous_tracks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|track| track.to_uri())
|
.map(|track| track.to_base62())
|
||||||
.collect::<Result<Vec<_>, _>>()?
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
.join(",");
|
.join(",");
|
||||||
let endpoint = format!(
|
let endpoint = format!(
|
||||||
|
@ -644,6 +644,12 @@ impl SpClient {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_next_page(&self, next_page_uri: &str) -> SpClientResult {
|
||||||
|
let endpoint = next_page_uri.trim_start_matches("hm:/");
|
||||||
|
self.request_as_json(&Method::GET, endpoint, None, None)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Find endpoint for newer canvas.proto and upgrade to that.
|
// TODO: Find endpoint for newer canvas.proto and upgrade to that.
|
||||||
pub async fn get_canvases(&self, request: EntityCanvazRequest) -> SpClientResult {
|
pub async fn get_canvases(&self, request: EntityCanvazRequest) -> SpClientResult {
|
||||||
let endpoint = "/canvaz-cache/v0/canvases";
|
let endpoint = "/canvaz-cache/v0/canvases";
|
||||||
|
|
Loading…
Reference in a new issue