Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [playback] `load` function changed from accepting a `SpotifyId` to accepting a `SpotifyUri` (breaking)
- [playback] `preload` function changed from accepting a `SpotifyId` to accepting a `SpotifyUri` (breaking)
- [core] `get_radio_for_track` function changed from accepting a `SpotifyId` to accepting a `SpotifyUri` (breaking)
- [core] Changed return type of `get_extended_metadata` to return `BatchedExtensionResponse` (breaking)
- [core] Changed parameter of `get_<item>_metadata` from `SpotifyId` to `SpotifyUri` (breaking)

### Fixed

- [connect] Fixed failed transferring with transfer data that had an empty context uri and no tracks
- [connect] Use the provided index or the first as fallback value to always play a track on loading
- [core] Fixed a problem where the metadata didn't include the audio file by switching to `get_extended_metadata`

### Removed

Expand Down
114 changes: 69 additions & 45 deletions core/src/spclient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ use crate::{
connect::PutStateRequest,
context::Context,
extended_metadata::BatchedEntityRequest,
extended_metadata::{BatchedExtensionResponse, EntityRequest, ExtensionQuery},
extension_kind::ExtensionKind,
},
token::Token,
util,
Expand All @@ -33,7 +35,7 @@ use hyper::{
header::{ACCEPT, AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE, HeaderName, RANGE},
};
use hyper_util::client::legacy::ResponseFuture;
use protobuf::{Enum, Message, MessageFull};
use protobuf::{Enum, EnumOrUnknown, Message, MessageFull};
use rand::RngCore;
use serde::Serialize;
use sysinfo::System;
Expand All @@ -60,18 +62,14 @@ const NO_METRICS_AND_SALT: RequestOptions = RequestOptions {
base_url: None,
};

const SPCLIENT_FALLBACK_ENDPOINT: RequestOptions = RequestOptions {
metrics: true,
salt: true,
base_url: Some("https://spclient.wg.spotify.com"),
};

#[derive(Debug, Error)]
pub enum SpClientError {
#[error("missing attribute {0}")]
Attribute(String),
#[error("expected data but received none")]
NoData,
#[error("expected an entry to exist in {0}")]
ExpectedEntry(&'static str),
}

impl From<SpClientError> for Error {
Expand Down Expand Up @@ -579,39 +577,71 @@ impl SpClient {
.await
}

pub async fn get_metadata(&self, scope: &str, id: &SpotifyId) -> SpClientResult {
let endpoint = format!("/metadata/4/{}/{}", scope, id.to_base16()?);
// For unknown reasons, metadata requests must now be sent through spclient.wg.spotify.com.
// Otherwise, the API will respond with 500 Internal Server Error responses.
// Context: https://github.com/librespot-org/librespot/issues/1527
self.request_with_options(
&Method::GET,
&endpoint,
None,
None,
&SPCLIENT_FALLBACK_ENDPOINT,
)
.await
pub async fn get_extended_metadata(
&self,
request: BatchedEntityRequest,
) -> Result<BatchedExtensionResponse, Error> {
let res = self
.request_with_protobuf(
&Method::POST,
"/extended-metadata/v0/extended-metadata",
None,
&request,
)
.await?;
Ok(BatchedExtensionResponse::parse_from_bytes(&res)?)
}

pub async fn get_metadata(&self, kind: ExtensionKind, id: &SpotifyUri) -> SpClientResult {
let req = BatchedEntityRequest {
entity_request: vec![EntityRequest {
entity_uri: id.to_uri()?,
query: vec![ExtensionQuery {
extension_kind: EnumOrUnknown::new(kind),
..Default::default()
}],
..Default::default()
}],
..Default::default()
};

let mut res = self.get_extended_metadata(req).await?;
let mut extended_metadata = res
.extended_metadata
.pop()
.ok_or(SpClientError::ExpectedEntry("extended_metadata"))?;

let mut data = extended_metadata
.extension_data
.pop()
.ok_or(SpClientError::ExpectedEntry("extension_data"))?;

match data.extension_data.take() {
None => Err(SpClientError::ExpectedEntry("data").into()),
Some(data) => Ok(Bytes::from(data.value)),
}
}

pub async fn get_track_metadata(&self, track_id: &SpotifyId) -> SpClientResult {
self.get_metadata("track", track_id).await
pub async fn get_track_metadata(&self, track_uri: &SpotifyUri) -> SpClientResult {
self.get_metadata(ExtensionKind::TRACK_V4, track_uri).await
}

pub async fn get_episode_metadata(&self, episode_id: &SpotifyId) -> SpClientResult {
self.get_metadata("episode", episode_id).await
pub async fn get_episode_metadata(&self, episode_uri: &SpotifyUri) -> SpClientResult {
self.get_metadata(ExtensionKind::EPISODE_V4, episode_uri)
.await
}

pub async fn get_album_metadata(&self, album_id: &SpotifyId) -> SpClientResult {
self.get_metadata("album", album_id).await
pub async fn get_album_metadata(&self, album_uri: &SpotifyUri) -> SpClientResult {
self.get_metadata(ExtensionKind::ALBUM_V4, album_uri).await
}

pub async fn get_artist_metadata(&self, artist_id: &SpotifyId) -> SpClientResult {
self.get_metadata("artist", artist_id).await
pub async fn get_artist_metadata(&self, artist_uri: &SpotifyUri) -> SpClientResult {
self.get_metadata(ExtensionKind::ARTIST_V4, artist_uri)
.await
}

pub async fn get_show_metadata(&self, show_id: &SpotifyId) -> SpClientResult {
self.get_metadata("show", show_id).await
pub async fn get_show_metadata(&self, show_uri: &SpotifyUri) -> SpClientResult {
self.get_metadata(ExtensionKind::SHOW_V4, show_uri).await
}

pub async fn get_lyrics(&self, track_id: &SpotifyId) -> SpClientResult {
Expand Down Expand Up @@ -740,12 +770,6 @@ impl SpClient {
// TODO: Seen-in-the-wild but unimplemented endpoints
// - /presence-view/v1/buddylist

pub async fn get_extended_metadata(&self, request: BatchedEntityRequest) -> SpClientResult {
let endpoint = "/extended-metadata/v0/extended-metadata";
self.request_with_protobuf(&Method::POST, endpoint, None, &request)
.await
}

pub async fn get_audio_storage(&self, file_id: &FileId) -> SpClientResult {
let endpoint = format!(
"/storage-resolve/files/audio/interactive/{}",
Expand Down Expand Up @@ -789,11 +813,11 @@ impl SpClient {

// Audio preview in 96 kbps MP3, unencrypted
pub async fn get_audio_preview(&self, preview_id: &FileId) -> SpClientResult {
let attribute = "audio-preview-url-template";
const ATTRIBUTE: &str = "audio-preview-url-template";
let template = self
.session()
.get_user_attribute(attribute)
.ok_or_else(|| SpClientError::Attribute(attribute.to_string()))?;
.get_user_attribute(ATTRIBUTE)
.ok_or_else(|| SpClientError::Attribute(ATTRIBUTE.to_string()))?;

let mut url = template.replace("{id}", &preview_id.to_base16()?);
let separator = match url.find('?') {
Expand All @@ -807,23 +831,23 @@ impl SpClient {

// The first 128 kB of a track, unencrypted
pub async fn get_head_file(&self, file_id: &FileId) -> SpClientResult {
let attribute = "head-files-url";
const ATTRIBUTE: &str = "head-files-url";
let template = self
.session()
.get_user_attribute(attribute)
.ok_or_else(|| SpClientError::Attribute(attribute.to_string()))?;
.get_user_attribute(ATTRIBUTE)
.ok_or_else(|| SpClientError::Attribute(ATTRIBUTE.to_string()))?;

let url = template.replace("{file_id}", &file_id.to_base16()?);

self.request_url(&url).await
}

pub async fn get_image(&self, image_id: &FileId) -> SpClientResult {
let attribute = "image-url";
const ATTRIBUTE: &str = "image-url";
let template = self
.session()
.get_user_attribute(attribute)
.ok_or_else(|| SpClientError::Attribute(attribute.to_string()))?;
.get_user_attribute(ATTRIBUTE)
.ok_or_else(|| SpClientError::Attribute(ATTRIBUTE.to_string()))?;
let url = template.replace("{file_id}", &image_id.to_base16()?);

self.request_url(&url).await
Expand Down
4 changes: 2 additions & 2 deletions metadata/src/album.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ impl Metadata for Album {
type Message = protocol::metadata::Album;

async fn request(session: &Session, album_uri: &SpotifyUri) -> RequestResult {
let SpotifyUri::Album { id: album_id } = album_uri else {
let SpotifyUri::Album { .. } = album_uri else {
return Err(Error::invalid_argument("album_uri"));
};

session.spclient().get_album_metadata(album_id).await
session.spclient().get_album_metadata(album_uri).await
}

fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
Expand Down
4 changes: 2 additions & 2 deletions metadata/src/artist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,11 @@ impl Metadata for Artist {
type Message = protocol::metadata::Artist;

async fn request(session: &Session, artist_uri: &SpotifyUri) -> RequestResult {
let SpotifyUri::Artist { id: artist_id } = artist_uri else {
let SpotifyUri::Artist { .. } = artist_uri else {
return Err(Error::invalid_argument("artist_uri"));
};

session.spclient().get_artist_metadata(artist_id).await
session.spclient().get_artist_metadata(artist_uri).await
}

fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
Expand Down
4 changes: 2 additions & 2 deletions metadata/src/episode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ impl Metadata for Episode {
type Message = protocol::metadata::Episode;

async fn request(session: &Session, episode_uri: &SpotifyUri) -> RequestResult {
let SpotifyUri::Episode { id: episode_id } = episode_uri else {
let SpotifyUri::Episode { .. } = episode_uri else {
return Err(Error::invalid_argument("episode_uri"));
};

session.spclient().get_episode_metadata(episode_id).await
session.spclient().get_episode_metadata(episode_uri).await
}

fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
Expand Down
4 changes: 2 additions & 2 deletions metadata/src/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ impl Metadata for Show {
type Message = protocol::metadata::Show;

async fn request(session: &Session, show_uri: &SpotifyUri) -> RequestResult {
let SpotifyUri::Show { id: show_id } = show_uri else {
let SpotifyUri::Show { .. } = show_uri else {
return Err(Error::invalid_argument("show_uri"));
};

session.spclient().get_show_metadata(show_id).await
session.spclient().get_show_metadata(show_uri).await
}

fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
Expand Down
4 changes: 2 additions & 2 deletions metadata/src/track.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ impl Metadata for Track {
type Message = protocol::metadata::Track;

async fn request(session: &Session, track_uri: &SpotifyUri) -> RequestResult {
let SpotifyUri::Track { id: track_id } = track_uri else {
let SpotifyUri::Track { .. } = track_uri else {
return Err(Error::invalid_argument("track_uri"));
};

session.spclient().get_track_metadata(track_id).await
session.spclient().get_track_metadata(track_uri).await
}

fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
Expand Down