1
0
Fork 0
mirror of https://github.com/librespot-org/librespot.git synced 2025-10-02 17:29:22 +02:00

refactor: Introduce SpotifyUri struct (#1538)

* refactor: Introduce SpotifyUri struct

Contributes to #1266

Introduces a new `SpotifyUri` struct which is layered on top of the
existing `SpotifyId`, but has the capability to support URIs that do
not confirm to the canonical base62 encoded format. This allows it to
describe URIs like `spotify:local`, `spotify:genre` and others that
`SpotifyId` cannot represent.

Changed the internal player state to use these URIs as much as possible,
such that the player could in the future accept a URI of the type
`spotify:local`, as a means of laying the groundwork for local file
support.

* fix: Don't pass unknown URIs from deprecated player methods

* refactor: remove SpotifyUri::to_base16

This should be deprecated for the same reason to_base62 is, and could unpredictably throw errors -- consumers should match on the inner ID if they need a base62 representation and handle failure appropriately

* refactor: Store original data in SpotifyUri::Unknown

Instead of assuming Unknown has a u128 SpotifyId, store the original data and type that we failed to parse.

* refactor: Remove SpotifyItemType

* refactor: Address review feedback

* test: Add more SpotifyUri tests

* chore: Correctly mark changes as breaking in CHANGELOG.md

* refactor: Respond to review feedback

* chore: Changelog updates
This commit is contained in:
Jay Malhotra 2025-09-11 20:59:53 +01:00 committed by GitHub
parent 0e5531ff54
commit df5f957bdd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 937 additions and 625 deletions

View file

@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since v0.2.0.
## [Unreleased]
### Added
- [core] Add `SpotifyUri` type to represent more types of URI than `SpotifyId` can
### Changed
- [playback] Changed type of `SpotifyId` fields in `PlayerEvent` members to `SpotifyUri` (breaking)
- [metadata] Changed arguments for `Metadata` trait from `&SpotifyId` to `&SpotifyUri` (breaking)
- [player] `load` function changed from accepting a `SpotifyId` to accepting a `SpotifyUri` (breaking)
- [player] `preload` function changed from accepting a `SpotifyId` to accepting a `SpotifyUri` (breaking)
- [spclient] `get_radio_for_track` function changed from accepting a `SpotifyId` to accepting a `SpotifyUri` (breaking)
### Removed
- [core] Removed `SpotifyItemType` enum; the new `SpotifyUri` is an enum over all item types and so which variant it is
describes its item type (breaking)
- [core] Removed `NamedSpotifyId` struct; it was made obsolete by `SpotifyUri` (breaking)
- [core] The following methods have been removed from `SpotifyId` and moved to `SpotifyUri` (breaking):
- `is_playable`
- `from_uri`
- `to_uri`
## [v0.7.1] - 2025-08-31
### Changed

View file

@ -2,7 +2,7 @@ use crate::{
LoadContextOptions, LoadRequestOptions, PlayContext,
context_resolver::{ContextAction, ContextResolver, ResolveContext},
core::{
Error, Session, SpotifyId,
Error, Session, SpotifyUri,
authentication::Credentials,
dealer::{
manager::{BoxedStream, BoxedStreamResult, Reply, RequestReply},
@ -778,7 +778,7 @@ impl SpircTask {
return Ok(());
}
PlayerEvent::Unavailable { track_id, .. } => {
self.handle_unavailable(track_id)?;
self.handle_unavailable(&track_id)?;
if self.connect_state.current_track(|t| &t.uri) == &track_id.to_uri()? {
self.handle_next(None)?
}
@ -1499,7 +1499,7 @@ impl SpircTask {
}
// Mark unavailable tracks so we can skip them later
fn handle_unavailable(&mut self, track_id: SpotifyId) -> Result<(), Error> {
fn handle_unavailable(&mut self, track_id: &SpotifyUri) -> Result<(), Error> {
self.connect_state.mark_unavailable(track_id)?;
self.handle_preload_next_track();
@ -1704,7 +1704,7 @@ impl SpircTask {
}
let current_uri = self.connect_state.current_track(|t| &t.uri);
let id = SpotifyId::from_uri(current_uri)?;
let id = SpotifyUri::from_uri(current_uri)?;
self.player.load(id, start_playing, position_ms);
self.connect_state

View file

@ -1,5 +1,5 @@
use crate::{
core::{Error, SpotifyId},
core::{Error, SpotifyId, SpotifyUri},
protocol::{
context::Context,
context_page::ContextPage,
@ -449,8 +449,10 @@ impl ConnectState {
(Some(uri), _) if uri.contains(['?', '%']) => {
Err(StateError::InvalidTrackUri(Some(uri.clone())))?
}
(Some(uri), _) if !uri.is_empty() => SpotifyId::from_uri(uri)?,
(_, Some(gid)) if !gid.is_empty() => SpotifyId::from_raw(gid)?,
(Some(uri), _) if !uri.is_empty() => SpotifyUri::from_uri(uri)?,
(_, Some(gid)) if !gid.is_empty() => SpotifyUri::Track {
id: SpotifyId::from_raw(gid)?,
},
_ => Err(StateError::InvalidTrackUri(None))?,
};

View file

@ -1,5 +1,5 @@
use crate::{
core::{Error, SpotifyId},
core::{Error, SpotifyUri},
protocol::player::ProvidedTrack,
state::{
ConnectState, SPOTIFY_MAX_NEXT_TRACKS_SIZE, SPOTIFY_MAX_PREV_TRACKS_SIZE, StateError,
@ -352,14 +352,14 @@ impl<'ct> ConnectState {
Ok(())
}
pub fn preview_next_track(&mut self) -> Option<SpotifyId> {
pub fn preview_next_track(&mut self) -> Option<SpotifyUri> {
let next = if self.repeat_track() {
self.current_track(|t| &t.uri)
} else {
&self.next_tracks().first()?.uri
};
SpotifyId::from_uri(next).ok()
SpotifyUri::from_uri(next).ok()
}
pub fn has_next_tracks(&self, min: Option<usize>) -> bool {
@ -381,7 +381,7 @@ impl<'ct> ConnectState {
prev
}
pub fn mark_unavailable(&mut self, id: SpotifyId) -> Result<(), Error> {
pub fn mark_unavailable(&mut self, id: &SpotifyUri) -> Result<(), Error> {
let uri = id.to_uri()?;
debug!("marking {uri} as unavailable");

View file

@ -32,6 +32,7 @@ mod socket;
#[allow(dead_code)]
pub mod spclient;
pub mod spotify_id;
pub mod spotify_uri;
pub mod token;
#[doc(hidden)]
pub mod util;
@ -42,3 +43,4 @@ pub use error::Error;
pub use file_id::FileId;
pub use session::Session;
pub use spotify_id::SpotifyId;
pub use spotify_uri::SpotifyUri;

View file

@ -5,7 +5,7 @@ use std::{
use crate::config::{OS, os_version};
use crate::{
Error, FileId, SpotifyId,
Error, FileId, SpotifyId, SpotifyUri,
apresolve::SocketAddress,
config::SessionConfig,
error::ErrorKind,
@ -676,10 +676,10 @@ impl SpClient {
.await
}
pub async fn get_radio_for_track(&self, track_id: &SpotifyId) -> SpClientResult {
pub async fn get_radio_for_track(&self, track_uri: &SpotifyUri) -> SpClientResult {
let endpoint = format!(
"/inspiredby-mix/v2/seed_to_playlist/{}?response-format=json",
track_id.to_uri()?
track_uri.to_uri()?
);
self.request_as_json(&Method::GET, &endpoint, None, None)

View file

@ -1,70 +1,23 @@
use std::{fmt, ops::Deref};
use std::fmt;
use thiserror::Error;
use crate::Error;
use librespot_protocol as protocol;
use crate::{Error, SpotifyUri};
// re-export FileId for historic reasons, when it was part of this mod
pub use crate::FileId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SpotifyItemType {
Album,
Artist,
Episode,
Playlist,
Show,
Track,
Local,
Unknown,
}
impl From<&str> for SpotifyItemType {
fn from(v: &str) -> Self {
match v {
"album" => Self::Album,
"artist" => Self::Artist,
"episode" => Self::Episode,
"playlist" => Self::Playlist,
"show" => Self::Show,
"track" => Self::Track,
"local" => Self::Local,
_ => Self::Unknown,
}
}
}
impl From<SpotifyItemType> for &str {
fn from(item_type: SpotifyItemType) -> &'static str {
match item_type {
SpotifyItemType::Album => "album",
SpotifyItemType::Artist => "artist",
SpotifyItemType::Episode => "episode",
SpotifyItemType::Playlist => "playlist",
SpotifyItemType::Show => "show",
SpotifyItemType::Track => "track",
SpotifyItemType::Local => "local",
_ => "unknown",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct SpotifyId {
pub id: u128,
pub item_type: SpotifyItemType,
}
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
pub enum SpotifyIdError {
#[error("ID cannot be parsed")]
InvalidId,
#[error("not a valid Spotify URI")]
#[error("not a valid Spotify ID")]
InvalidFormat,
#[error("URI does not belong to Spotify")]
InvalidRoot,
}
impl From<SpotifyIdError> for Error {
@ -74,7 +27,6 @@ impl From<SpotifyIdError> for Error {
}
pub type SpotifyIdResult = Result<SpotifyId, Error>;
pub type NamedSpotifyIdResult = Result<NamedSpotifyId, Error>;
const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef";
@ -84,14 +36,6 @@ impl SpotifyId {
const SIZE_BASE16: usize = 32;
const SIZE_BASE62: usize = 22;
/// Returns whether this `SpotifyId` is for a playable audio item, if known.
pub fn is_playable(&self) -> bool {
matches!(
self.item_type,
SpotifyItemType::Episode | SpotifyItemType::Track
)
}
/// Parses a base16 (hex) encoded [Spotify ID] into a `SpotifyId`.
///
/// `src` is expected to be 32 bytes long and encoded using valid characters.
@ -114,10 +58,7 @@ impl SpotifyId {
dst += p;
}
Ok(Self {
id: dst,
item_type: SpotifyItemType::Unknown,
})
Ok(Self { id: dst })
}
/// Parses a base62 encoded [Spotify ID] into a `u128`.
@ -126,7 +67,7 @@ impl SpotifyId {
///
/// [Spotify ID]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids
pub fn from_base62(src: &str) -> SpotifyIdResult {
if src.len() != 22 {
if src.len() != Self::SIZE_BASE62 {
return Err(SpotifyIdError::InvalidId.into());
}
let mut dst: u128 = 0;
@ -143,10 +84,7 @@ impl SpotifyId {
dst = dst.checked_add(p).ok_or(SpotifyIdError::InvalidId)?;
}
Ok(Self {
id: dst,
item_type: SpotifyItemType::Unknown,
})
Ok(Self { id: dst })
}
/// Creates a `u128` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order.
@ -156,65 +94,11 @@ impl SpotifyId {
match src.try_into() {
Ok(dst) => Ok(Self {
id: u128::from_be_bytes(dst),
item_type: SpotifyItemType::Unknown,
}),
Err(_) => Err(SpotifyIdError::InvalidId.into()),
}
}
/// Parses a [Spotify URI] into a `SpotifyId`.
///
/// `uri` is expected to be in the canonical form `spotify:{type}:{id}`, where `{type}`
/// can be arbitrary while `{id}` is a 22-character long, base62 encoded Spotify ID.
///
/// Note that this should not be used for playlists, which have the form of
/// `spotify:playlist:{id}`.
///
/// [Spotify URI]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids
pub fn from_uri(src: &str) -> SpotifyIdResult {
// Basic: `spotify:{type}:{id}`
// Named: `spotify:user:{user}:{type}:{id}`
// Local: `spotify:local:{artist}:{album_title}:{track_title}:{duration_in_seconds}`
let mut parts = src.split(':');
let scheme = parts.next().ok_or(SpotifyIdError::InvalidFormat)?;
let item_type = {
let next = parts.next().ok_or(SpotifyIdError::InvalidFormat)?;
if next == "user" {
let _username = parts.next().ok_or(SpotifyIdError::InvalidFormat)?;
parts.next().ok_or(SpotifyIdError::InvalidFormat)?
} else {
next
}
};
let id = parts.next().ok_or(SpotifyIdError::InvalidFormat)?;
if scheme != "spotify" {
return Err(SpotifyIdError::InvalidRoot.into());
}
let item_type = item_type.into();
// Local files have a variable-length ID: https://developer.spotify.com/documentation/general/guides/local-files-spotify-playlists/
// TODO: find a way to add this local file ID to SpotifyId.
// One possible solution would be to copy the contents of `id` to a new String field in SpotifyId,
// but then we would need to remove the derived Copy trait, which would be a breaking change.
if item_type == SpotifyItemType::Local {
return Ok(Self { item_type, id: 0 });
}
if id.len() != Self::SIZE_BASE62 {
return Err(SpotifyIdError::InvalidId.into());
}
Ok(Self {
item_type,
..Self::from_base62(id)?
})
}
/// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE16` (32)
/// character long `String`.
#[allow(clippy::wrong_self_convention)]
@ -274,124 +158,19 @@ impl SpotifyId {
pub fn to_raw(&self) -> [u8; Self::SIZE] {
self.id.to_be_bytes()
}
/// Returns the `SpotifyId` as a [Spotify URI] in the canonical form `spotify:{type}:{id}`,
/// where `{type}` is an arbitrary string and `{id}` is a 22-character long, base62 encoded
/// Spotify ID.
///
/// If the `SpotifyId` has an associated type unrecognized by the library, `{type}` will
/// be encoded as `unknown`.
///
/// [Spotify URI]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids
#[allow(clippy::wrong_self_convention)]
pub fn to_uri(&self) -> Result<String, Error> {
// 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID = 31
// + unknown size item_type.
let item_type: &str = self.item_type.into();
let mut dst = String::with_capacity(31 + item_type.len());
dst.push_str("spotify:");
dst.push_str(item_type);
dst.push(':');
let base_62 = self.to_base62()?;
dst.push_str(&base_62);
Ok(dst)
}
}
impl fmt::Debug for SpotifyId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("SpotifyId")
.field(&self.to_uri().unwrap_or_else(|_| "invalid uri".into()))
.field(&self.to_base62().unwrap_or_else(|_| "invalid uri".into()))
.finish()
}
}
impl fmt::Display for SpotifyId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_uri().unwrap_or_else(|_| "invalid uri".into()))
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct NamedSpotifyId {
pub inner_id: SpotifyId,
pub username: String,
}
impl NamedSpotifyId {
pub fn from_uri(src: &str) -> NamedSpotifyIdResult {
let uri_parts: Vec<&str> = src.split(':').collect();
// At minimum, should be `spotify:user:{username}:{type}:{id}`
if uri_parts.len() < 5 {
return Err(SpotifyIdError::InvalidFormat.into());
}
if uri_parts[0] != "spotify" {
return Err(SpotifyIdError::InvalidRoot.into());
}
if uri_parts[1] != "user" {
return Err(SpotifyIdError::InvalidFormat.into());
}
Ok(Self {
inner_id: SpotifyId::from_uri(src)?,
username: uri_parts[2].to_owned(),
})
}
pub fn to_uri(&self) -> Result<String, Error> {
let item_type: &str = self.inner_id.item_type.into();
let mut dst = String::with_capacity(37 + self.username.len() + item_type.len());
dst.push_str("spotify:user:");
dst.push_str(&self.username);
dst.push(':');
dst.push_str(item_type);
dst.push(':');
let base_62 = self.to_base62()?;
dst.push_str(&base_62);
Ok(dst)
}
pub fn from_spotify_id(id: SpotifyId, username: &str) -> Self {
Self {
inner_id: id,
username: username.to_owned(),
}
}
}
impl Deref for NamedSpotifyId {
type Target = SpotifyId;
fn deref(&self) -> &Self::Target {
&self.inner_id
}
}
impl fmt::Debug for NamedSpotifyId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("NamedSpotifyId")
.field(
&self
.inner_id
.to_uri()
.unwrap_or_else(|_| "invalid id".into()),
)
.finish()
}
}
impl fmt::Display for NamedSpotifyId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(
&self
.inner_id
.to_uri()
.unwrap_or_else(|_| "invalid id".into()),
)
f.write_str(&self.to_base62().unwrap_or_else(|_| "invalid uri".into()))
}
}
@ -423,104 +202,20 @@ impl TryFrom<&Vec<u8>> for SpotifyId {
}
}
impl TryFrom<&protocol::metadata::Album> for SpotifyId {
impl TryFrom<&SpotifyUri> for SpotifyId {
type Error = crate::Error;
fn try_from(album: &protocol::metadata::Album) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Album,
..Self::from_raw(album.gid())?
})
}
}
impl TryFrom<&protocol::metadata::Artist> for SpotifyId {
type Error = crate::Error;
fn try_from(artist: &protocol::metadata::Artist) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Artist,
..Self::from_raw(artist.gid())?
})
}
}
impl TryFrom<&protocol::metadata::Episode> for SpotifyId {
type Error = crate::Error;
fn try_from(episode: &protocol::metadata::Episode) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Episode,
..Self::from_raw(episode.gid())?
})
}
}
impl TryFrom<&protocol::metadata::Track> for SpotifyId {
type Error = crate::Error;
fn try_from(track: &protocol::metadata::Track) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Track,
..Self::from_raw(track.gid())?
})
}
}
impl TryFrom<&protocol::metadata::Show> for SpotifyId {
type Error = crate::Error;
fn try_from(show: &protocol::metadata::Show) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Show,
..Self::from_raw(show.gid())?
})
}
}
impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId {
type Error = crate::Error;
fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Artist,
..Self::from_raw(artist.artist_gid())?
})
}
}
impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId {
type Error = crate::Error;
fn try_from(item: &protocol::playlist4_external::Item) -> Result<Self, Self::Error> {
Ok(Self {
item_type: SpotifyItemType::Track,
..Self::from_uri(item.uri())?
})
}
}
// Note that this is the unique revision of an item's metadata on a playlist,
// not the ID of that item or playlist.
impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId {
type Error = crate::Error;
fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result<Self, Self::Error> {
Self::try_from(item.revision())
}
}
// Note that this is the unique revision of a playlist, not the ID of that playlist.
impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId {
type Error = crate::Error;
fn try_from(
playlist: &protocol::playlist4_external::SelectedListContent,
) -> Result<Self, Self::Error> {
Self::try_from(playlist.revision())
}
}
// TODO: check meaning and format of this field in the wild. This might be a FileId,
// which is why we now don't create a separate `Playlist` enum value yet and choose
// to discard any item type.
impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId {
type Error = crate::Error;
fn try_from(
picture: &protocol::playlist_annotate3::TranscodedPicture,
) -> Result<Self, Self::Error> {
Self::from_base62(picture.uri())
fn try_from(value: &SpotifyUri) -> Result<Self, Self::Error> {
match value {
SpotifyUri::Album { id }
| SpotifyUri::Artist { id }
| SpotifyUri::Episode { id }
| SpotifyUri::Playlist { id, .. }
| SpotifyUri::Show { id }
| SpotifyUri::Track { id } => Ok(*id),
SpotifyUri::Local { .. } | SpotifyUri::Unknown { .. } => {
Err(SpotifyIdError::InvalidFormat.into())
}
}
}
}
@ -541,8 +236,6 @@ mod tests {
struct ConversionCase {
id: u128,
kind: SpotifyItemType,
uri: &'static str,
base16: &'static str,
base62: &'static str,
raw: &'static [u8],
@ -551,8 +244,6 @@ mod tests {
static CONV_VALID: [ConversionCase; 5] = [
ConversionCase {
id: 238762092608182713602505436543891614649,
kind: SpotifyItemType::Track,
uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH",
base16: "b39fe8081e1f4c54be38e8d6f9f12bb9",
base62: "5sWHDYs0csV6RS48xBl0tH",
raw: &[
@ -561,8 +252,6 @@ mod tests {
},
ConversionCase {
id: 204841891221366092811751085145916697048,
kind: SpotifyItemType::Track,
uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4",
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
base62: "4GNcXTGWmnZ3ySrqvol3o4",
raw: &[
@ -571,8 +260,6 @@ mod tests {
},
ConversionCase {
id: 204841891221366092811751085145916697048,
kind: SpotifyItemType::Episode,
uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4",
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
base62: "4GNcXTGWmnZ3ySrqvol3o4",
raw: &[
@ -581,8 +268,6 @@ mod tests {
},
ConversionCase {
id: 204841891221366092811751085145916697048,
kind: SpotifyItemType::Show,
uri: "spotify:show:4GNcXTGWmnZ3ySrqvol3o4",
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
base62: "4GNcXTGWmnZ3ySrqvol3o4",
raw: &[
@ -591,8 +276,6 @@ mod tests {
},
ConversionCase {
id: 0,
kind: SpotifyItemType::Local,
uri: "spotify:local:0000000000000000000000",
base16: "00000000000000000000000000000000",
base62: "0000000000000000000000",
raw: &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
@ -602,9 +285,6 @@ mod tests {
static CONV_INVALID: [ConversionCase; 5] = [
ConversionCase {
id: 0,
kind: SpotifyItemType::Unknown,
// Invalid ID in the URI.
uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH",
base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9",
base62: "!!!!!Ys0csV6RS48xBl0tH",
raw: &[
@ -614,9 +294,6 @@ mod tests {
},
ConversionCase {
id: 0,
kind: SpotifyItemType::Unknown,
// Missing colon between ID and type.
uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH",
base16: "--------------------",
base62: "....................",
raw: &[
@ -626,9 +303,6 @@ mod tests {
},
ConversionCase {
id: 0,
kind: SpotifyItemType::Unknown,
// Uri too short
uri: "spotify:azb:aRS48xBl0tH",
// too long, should return error but not panic overflow
base16: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
// too long, should return error but not panic overflow
@ -640,9 +314,6 @@ mod tests {
},
ConversionCase {
id: 0,
kind: SpotifyItemType::Unknown,
// Uri too short
uri: "spotify:azb:aRS48xBl0tH",
base16: "--------------------",
// too short to encode a 128 bits int
base62: "aa",
@ -653,8 +324,6 @@ mod tests {
},
ConversionCase {
id: 0,
kind: SpotifyItemType::Unknown,
uri: "cleary invalid uri",
base16: "--------------------",
// too high of a value, this would need a 132 bits int
base62: "ZZZZZZZZZZZZZZZZZZZZZZ",
@ -679,10 +348,7 @@ mod tests {
#[test]
fn to_base62() {
for c in &CONV_VALID {
let id = SpotifyId {
id: c.id,
item_type: c.kind,
};
let id = SpotifyId { id: c.id };
assert_eq!(id.to_base62().unwrap(), c.base62);
}
@ -702,60 +368,12 @@ mod tests {
#[test]
fn to_base16() {
for c in &CONV_VALID {
let id = SpotifyId {
id: c.id,
item_type: c.kind,
};
let id = SpotifyId { id: c.id };
assert_eq!(id.to_base16().unwrap(), c.base16);
}
}
#[test]
fn from_uri() {
for c in &CONV_VALID {
let actual = SpotifyId::from_uri(c.uri).unwrap();
assert_eq!(actual.id, c.id);
assert_eq!(actual.item_type, c.kind);
}
for c in &CONV_INVALID {
assert!(SpotifyId::from_uri(c.uri).is_err());
}
}
#[test]
fn from_local_uri() {
let actual = SpotifyId::from_uri("spotify:local:xyz:123").unwrap();
assert_eq!(actual.id, 0);
assert_eq!(actual.item_type, SpotifyItemType::Local);
}
#[test]
fn from_named_uri() {
let actual =
NamedSpotifyId::from_uri("spotify:user:spotify:playlist:37i9dQZF1DWSw8liJZcPOI")
.unwrap();
assert_eq!(actual.id, 136159921382084734723401526672209703396);
assert_eq!(actual.item_type, SpotifyItemType::Playlist);
assert_eq!(actual.username, "spotify");
}
#[test]
fn to_uri() {
for c in &CONV_VALID {
let id = SpotifyId {
id: c.id,
item_type: c.kind,
};
assert_eq!(id.to_uri().unwrap(), c.uri);
}
}
#[test]
fn from_raw() {
for c in &CONV_VALID {

583
core/src/spotify_uri.rs Normal file
View file

@ -0,0 +1,583 @@
use crate::{Error, SpotifyId};
use std::{borrow::Cow, fmt};
use thiserror::Error;
use librespot_protocol as protocol;
const SPOTIFY_ITEM_TYPE_ALBUM: &str = "album";
const SPOTIFY_ITEM_TYPE_ARTIST: &str = "artist";
const SPOTIFY_ITEM_TYPE_EPISODE: &str = "episode";
const SPOTIFY_ITEM_TYPE_PLAYLIST: &str = "playlist";
const SPOTIFY_ITEM_TYPE_SHOW: &str = "show";
const SPOTIFY_ITEM_TYPE_TRACK: &str = "track";
const SPOTIFY_ITEM_TYPE_LOCAL: &str = "local";
const SPOTIFY_ITEM_TYPE_UNKNOWN: &str = "unknown";
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
pub enum SpotifyUriError {
#[error("not a valid Spotify URI")]
InvalidFormat,
#[error("URI does not belong to Spotify")]
InvalidRoot,
}
impl From<SpotifyUriError> for Error {
fn from(err: SpotifyUriError) -> Self {
Error::invalid_argument(err)
}
}
pub type SpotifyUriResult = Result<SpotifyUri, Error>;
#[derive(Clone, PartialEq, Eq, Hash)]
pub enum SpotifyUri {
Album {
id: SpotifyId,
},
Artist {
id: SpotifyId,
},
Episode {
id: SpotifyId,
},
Playlist {
user: Option<String>,
id: SpotifyId,
},
Show {
id: SpotifyId,
},
Track {
id: SpotifyId,
},
Local {
artist: String,
album_title: String,
track_title: String,
duration: std::time::Duration,
},
Unknown {
kind: Cow<'static, str>,
id: String,
},
}
impl SpotifyUri {
/// Returns whether this `SpotifyUri` is for a playable audio item, if known.
pub fn is_playable(&self) -> bool {
matches!(self, SpotifyUri::Episode { .. } | SpotifyUri::Track { .. })
}
/// Gets the item type of this URI as a static string
pub fn item_type(&self) -> &'static str {
match &self {
SpotifyUri::Album { .. } => SPOTIFY_ITEM_TYPE_ALBUM,
SpotifyUri::Artist { .. } => SPOTIFY_ITEM_TYPE_ARTIST,
SpotifyUri::Episode { .. } => SPOTIFY_ITEM_TYPE_EPISODE,
SpotifyUri::Playlist { .. } => SPOTIFY_ITEM_TYPE_PLAYLIST,
SpotifyUri::Show { .. } => SPOTIFY_ITEM_TYPE_SHOW,
SpotifyUri::Track { .. } => SPOTIFY_ITEM_TYPE_TRACK,
SpotifyUri::Local { .. } => SPOTIFY_ITEM_TYPE_LOCAL,
SpotifyUri::Unknown { .. } => SPOTIFY_ITEM_TYPE_UNKNOWN,
}
}
/// Gets the ID of this URI. The resource ID is the component of the URI that identifies
/// the resource after its type label. If `self` is a named ID, the user will be omitted.
pub fn to_id(&self) -> Result<String, Error> {
match &self {
SpotifyUri::Album { id }
| SpotifyUri::Artist { id }
| SpotifyUri::Episode { id }
| SpotifyUri::Playlist { id, .. }
| SpotifyUri::Show { id }
| SpotifyUri::Track { id } => id.to_base62(),
SpotifyUri::Local {
artist,
album_title,
track_title,
duration,
} => {
let duration_secs = duration.as_secs();
Ok(format!(
"{artist}:{album_title}:{track_title}:{duration_secs}"
))
}
SpotifyUri::Unknown { id, .. } => Ok(id.clone()),
}
}
/// Parses a [Spotify URI] into a `SpotifyUri`.
///
/// `uri` is expected to be in the canonical form `spotify:{type}:{id}`, where `{type}`
/// can be arbitrary while `{id}` is in a format that varies based on the `{type}`:
///
/// - For most item types, a 22-character long, base62 encoded Spotify ID is expected.
/// - For local files, an arbitrary length string with the fields
/// `{artist}:{album_title}:{track_title}:{duration_in_seconds}` is expected.
///
/// Spotify URI: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids
pub fn from_uri(src: &str) -> SpotifyUriResult {
// Basic: `spotify:{type}:{id}`
// Named: `spotify:user:{user}:{type}:{id}`
// Local: `spotify:local:{artist}:{album_title}:{track_title}:{duration_in_seconds}`
let mut parts = src.split(':');
let scheme = parts.next().ok_or(SpotifyUriError::InvalidFormat)?;
if scheme != "spotify" {
return Err(SpotifyUriError::InvalidRoot.into());
}
let mut username: Option<String> = None;
let item_type = {
let next = parts.next().ok_or(SpotifyUriError::InvalidFormat)?;
if next == "user" {
username.replace(
parts
.next()
.ok_or(SpotifyUriError::InvalidFormat)?
.to_owned(),
);
parts.next().ok_or(SpotifyUriError::InvalidFormat)?
} else {
next
}
};
let name = parts.next().ok_or(SpotifyUriError::InvalidFormat)?;
match item_type {
SPOTIFY_ITEM_TYPE_ALBUM => Ok(Self::Album {
id: SpotifyId::from_base62(name)?,
}),
SPOTIFY_ITEM_TYPE_ARTIST => Ok(Self::Artist {
id: SpotifyId::from_base62(name)?,
}),
SPOTIFY_ITEM_TYPE_EPISODE => Ok(Self::Episode {
id: SpotifyId::from_base62(name)?,
}),
SPOTIFY_ITEM_TYPE_PLAYLIST => Ok(Self::Playlist {
id: SpotifyId::from_base62(name)?,
user: username,
}),
SPOTIFY_ITEM_TYPE_SHOW => Ok(Self::Show {
id: SpotifyId::from_base62(name)?,
}),
SPOTIFY_ITEM_TYPE_TRACK => Ok(Self::Track {
id: SpotifyId::from_base62(name)?,
}),
SPOTIFY_ITEM_TYPE_LOCAL => Ok(Self::Local {
artist: "unimplemented".to_owned(),
album_title: "unimplemented".to_owned(),
track_title: "unimplemented".to_owned(),
duration: Default::default(),
}),
_ => Ok(Self::Unknown {
kind: item_type.to_owned().into(),
id: name.to_owned(),
}),
}
}
/// Returns the `SpotifyUri` as a [Spotify URI] in the canonical form `spotify:{type}:{id}`,
/// where `{type}` is an arbitrary string and `{id}` is a 22-character long, base62 encoded
/// Spotify ID.
///
/// If the `SpotifyUri` has an associated type unrecognized by the library, `{type}` will
/// be encoded as `unknown`.
///
/// If the `SpotifyUri` is named, it will be returned in the form
/// `spotify:user:{user}:{type}:{id}`.
///
/// [Spotify URI]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids
pub fn to_uri(&self) -> Result<String, Error> {
let item_type = self.item_type();
let name = self.to_id()?;
if let SpotifyUri::Playlist {
id,
user: Some(user),
} = self
{
Ok(format!("spotify:user:{user}:{item_type}:{id}"))
} else {
Ok(format!("spotify:{item_type}:{name}"))
}
}
/// Gets the name of this URI. The resource name is the component of the URI that identifies
/// the resource after its type label. If `self` is a named ID, the user will be omitted.
///
/// Deprecated: not all IDs can be represented in Base62, so this function has been renamed to
/// [SpotifyUri::to_id], which this implementation forwards to.
#[deprecated(since = "0.8.0", note = "use to_name instead")]
pub fn to_base62(&self) -> Result<String, Error> {
self.to_id()
}
}
impl fmt::Debug for SpotifyUri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("SpotifyUri")
.field(&self.to_uri().unwrap_or_else(|_| "invalid uri".into()))
.finish()
}
}
impl fmt::Display for SpotifyUri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_uri().unwrap_or_else(|_| "invalid uri".into()))
}
}
impl TryFrom<&protocol::metadata::Album> for SpotifyUri {
type Error = crate::Error;
fn try_from(album: &protocol::metadata::Album) -> Result<Self, Self::Error> {
Ok(Self::Album {
id: SpotifyId::from_raw(album.gid())?,
})
}
}
impl TryFrom<&protocol::metadata::Artist> for SpotifyUri {
type Error = crate::Error;
fn try_from(artist: &protocol::metadata::Artist) -> Result<Self, Self::Error> {
Ok(Self::Artist {
id: SpotifyId::from_raw(artist.gid())?,
})
}
}
impl TryFrom<&protocol::metadata::Episode> for SpotifyUri {
type Error = crate::Error;
fn try_from(episode: &protocol::metadata::Episode) -> Result<Self, Self::Error> {
Ok(Self::Episode {
id: SpotifyId::from_raw(episode.gid())?,
})
}
}
impl TryFrom<&protocol::metadata::Track> for SpotifyUri {
type Error = crate::Error;
fn try_from(track: &protocol::metadata::Track) -> Result<Self, Self::Error> {
Ok(Self::Track {
id: SpotifyId::from_raw(track.gid())?,
})
}
}
impl TryFrom<&protocol::metadata::Show> for SpotifyUri {
type Error = crate::Error;
fn try_from(show: &protocol::metadata::Show) -> Result<Self, Self::Error> {
Ok(Self::Show {
id: SpotifyId::from_raw(show.gid())?,
})
}
}
impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyUri {
type Error = crate::Error;
fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result<Self, Self::Error> {
Ok(Self::Artist {
id: SpotifyId::from_raw(artist.artist_gid())?,
})
}
}
impl TryFrom<&protocol::playlist4_external::Item> for SpotifyUri {
type Error = crate::Error;
fn try_from(item: &protocol::playlist4_external::Item) -> Result<Self, Self::Error> {
Self::from_uri(item.uri())
}
}
// Note that this is the unique revision of an item's metadata on a playlist,
// not the ID of that item or playlist.
impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyUri {
type Error = crate::Error;
fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result<Self, Self::Error> {
Ok(Self::Unknown {
kind: "MetaItem".into(),
id: SpotifyId::try_from(item.revision())?.to_base62()?,
})
}
}
// Note that this is the unique revision of a playlist, not the ID of that playlist.
impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyUri {
type Error = crate::Error;
fn try_from(
playlist: &protocol::playlist4_external::SelectedListContent,
) -> Result<Self, Self::Error> {
Ok(Self::Unknown {
kind: "SelectedListContent".into(),
id: SpotifyId::try_from(playlist.revision())?.to_base62()?,
})
}
}
// TODO: check meaning and format of this field in the wild. This might be a FileId,
// which is why we now don't create a separate `Playlist` enum value yet and choose
// to discard any item type.
impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyUri {
type Error = crate::Error;
fn try_from(
picture: &protocol::playlist_annotate3::TranscodedPicture,
) -> Result<Self, Self::Error> {
Ok(Self::Unknown {
kind: "TranscodedPicture".into(),
id: picture.uri().to_owned(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
struct ConversionCase {
parsed: SpotifyUri,
uri: &'static str,
base62: &'static str,
}
static CONV_VALID: [ConversionCase; 4] = [
ConversionCase {
parsed: SpotifyUri::Track {
id: SpotifyId {
id: 238762092608182713602505436543891614649,
},
},
uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH",
base62: "5sWHDYs0csV6RS48xBl0tH",
},
ConversionCase {
parsed: SpotifyUri::Track {
id: SpotifyId {
id: 204841891221366092811751085145916697048,
},
},
uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4",
base62: "4GNcXTGWmnZ3ySrqvol3o4",
},
ConversionCase {
parsed: SpotifyUri::Episode {
id: SpotifyId {
id: 204841891221366092811751085145916697048,
},
},
uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4",
base62: "4GNcXTGWmnZ3ySrqvol3o4",
},
ConversionCase {
parsed: SpotifyUri::Show {
id: SpotifyId {
id: 204841891221366092811751085145916697048,
},
},
uri: "spotify:show:4GNcXTGWmnZ3ySrqvol3o4",
base62: "4GNcXTGWmnZ3ySrqvol3o4",
},
];
static CONV_INVALID: [ConversionCase; 5] = [
ConversionCase {
parsed: SpotifyUri::Track {
id: SpotifyId { id: 0 },
},
// Invalid ID in the URI.
uri: "spotify:track:5sWHDYs0Bl0tH",
base62: "!!!!!Ys0csV6RS48xBl0tH",
},
ConversionCase {
parsed: SpotifyUri::Track {
id: SpotifyId { id: 0 },
},
// Missing colon between ID and type.
uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH",
base62: "....................",
},
ConversionCase {
parsed: SpotifyUri::Track {
id: SpotifyId { id: 0 },
},
// Uri too short
uri: "spotify:track:aRS48xBl0tH",
// too long, should return error but not panic overflow
base62: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
ConversionCase {
parsed: SpotifyUri::Track {
id: SpotifyId { id: 0 },
},
// Uri too short
uri: "spotify:track:aRS48xBl0tH",
// too short to encode a 128 bits int
base62: "aa",
},
ConversionCase {
parsed: SpotifyUri::Track {
id: SpotifyId { id: 0 },
},
uri: "cleary invalid uri",
// too high of a value, this would need a 132 bits int
base62: "ZZZZZZZZZZZZZZZZZZZZZZ",
},
];
struct ItemTypeCase {
uri: SpotifyUri,
expected_type: &'static str,
}
static ITEM_TYPES: [ItemTypeCase; 6] = [
ItemTypeCase {
uri: SpotifyUri::Album {
id: SpotifyId { id: 0 },
},
expected_type: "album",
},
ItemTypeCase {
uri: SpotifyUri::Artist {
id: SpotifyId { id: 0 },
},
expected_type: "artist",
},
ItemTypeCase {
uri: SpotifyUri::Episode {
id: SpotifyId { id: 0 },
},
expected_type: "episode",
},
ItemTypeCase {
uri: SpotifyUri::Playlist {
user: None,
id: SpotifyId { id: 0 },
},
expected_type: "playlist",
},
ItemTypeCase {
uri: SpotifyUri::Show {
id: SpotifyId { id: 0 },
},
expected_type: "show",
},
ItemTypeCase {
uri: SpotifyUri::Track {
id: SpotifyId { id: 0 },
},
expected_type: "track",
},
];
#[test]
fn to_id() {
for c in &CONV_VALID {
assert_eq!(c.parsed.to_id().unwrap(), c.base62);
}
}
#[test]
fn item_type() {
for i in &ITEM_TYPES {
assert_eq!(i.uri.item_type(), i.expected_type);
}
// These need to use methods that can't be used in the static context like to_owned() and
// into().
let local_file = SpotifyUri::Local {
artist: "".to_owned(),
album_title: "".to_owned(),
track_title: "".to_owned(),
duration: Default::default(),
};
assert_eq!(local_file.item_type(), "local");
let unknown = SpotifyUri::Unknown {
kind: "not used".into(),
id: "".to_owned(),
};
assert_eq!(unknown.item_type(), "unknown");
}
#[test]
fn from_uri() {
for c in &CONV_VALID {
let actual = SpotifyUri::from_uri(c.uri).unwrap();
assert_eq!(actual, c.parsed);
}
for c in &CONV_INVALID {
assert!(SpotifyUri::from_uri(c.uri).is_err());
}
}
#[test]
fn from_invalid_type_uri() {
let actual =
SpotifyUri::from_uri("spotify:arbitrarywhatever:5sWHDYs0csV6RS48xBl0tH").unwrap();
assert_eq!(
actual,
SpotifyUri::Unknown {
kind: "arbitrarywhatever".into(),
id: "5sWHDYs0csV6RS48xBl0tH".to_owned()
}
)
}
#[test]
fn from_local_uri() {
let actual = SpotifyUri::from_uri("spotify:local:xyz:123").unwrap();
assert_eq!(
actual,
SpotifyUri::Local {
artist: "unimplemented".to_owned(),
album_title: "unimplemented".to_owned(),
track_title: "unimplemented".to_owned(),
duration: Default::default(),
}
);
}
#[test]
fn from_named_uri() {
let actual =
SpotifyUri::from_uri("spotify:user:spotify:playlist:37i9dQZF1DWSw8liJZcPOI").unwrap();
let SpotifyUri::Playlist { ref user, id } = actual else {
panic!("wrong id type");
};
assert_eq!(*user, Some("spotify".to_owned()));
assert_eq!(
id,
SpotifyId {
id: 136159921382084734723401526672209703396
},
);
}
#[test]
fn to_uri() {
for c in &CONV_VALID {
assert_eq!(c.parsed.to_uri().unwrap(), c.uri);
}
}
#[test]
fn to_named_uri() {
let string = "spotify:user:spotify:playlist:37i9dQZF1DWSw8liJZcPOI";
let actual =
SpotifyUri::from_uri("spotify:user:spotify:playlist:37i9dQZF1DWSw8liJZcPOI").unwrap();
assert_eq!(actual.to_uri().unwrap(), string);
}
}

View file

@ -2,10 +2,8 @@ use std::{env, process::exit};
use librespot::{
core::{
authentication::Credentials,
config::SessionConfig,
session::Session,
spotify_id::{SpotifyId, SpotifyItemType},
SpotifyUri, authentication::Credentials, config::SessionConfig, session::Session,
spotify_id::SpotifyId,
},
playback::{
audio_backend,
@ -28,8 +26,9 @@ async fn main() {
}
let credentials = Credentials::with_access_token(&args[1]);
let mut track = SpotifyId::from_base62(&args[2]).unwrap();
track.item_type = SpotifyItemType::Track;
let track = SpotifyUri::Track {
id: SpotifyId::from_base62(&args[2]).unwrap(),
};
let backend = audio_backend::find(None).unwrap();

View file

@ -2,7 +2,8 @@ use std::{env, process::exit};
use librespot::{
core::{
authentication::Credentials, config::SessionConfig, session::Session, spotify_id::SpotifyId,
authentication::Credentials, config::SessionConfig, session::Session,
spotify_uri::SpotifyUri,
},
metadata::{Metadata, Playlist, Track},
};
@ -19,7 +20,7 @@ async fn main() {
}
let credentials = Credentials::with_access_token(&args[1]);
let plist_uri = SpotifyId::from_uri(&args[2]).unwrap_or_else(|_| {
let plist_uri = SpotifyUri::from_uri(&args[2]).unwrap_or_else(|_| {
eprintln!(
"PLAYLIST should be a playlist URI such as: \
\"spotify:playlist:37i9dQZF1DXec50AjHrNTq\""

View file

@ -17,7 +17,7 @@ use crate::{
util::{impl_deref_wrapped, impl_try_from_repeated},
};
use librespot_core::{Error, Session, SpotifyId, date::Date};
use librespot_core::{Error, Session, SpotifyUri, date::Date};
use librespot_protocol as protocol;
use protocol::metadata::Disc as DiscMessage;
@ -25,7 +25,7 @@ pub use protocol::metadata::album::Type as AlbumType;
#[derive(Debug, Clone)]
pub struct Album {
pub id: SpotifyId,
pub id: SpotifyUri,
pub name: String,
pub artists: Artists,
pub album_type: AlbumType,
@ -48,9 +48,9 @@ pub struct Album {
}
#[derive(Debug, Clone, Default)]
pub struct Albums(pub Vec<SpotifyId>);
pub struct Albums(pub Vec<SpotifyUri>);
impl_deref_wrapped!(Albums, Vec<SpotifyId>);
impl_deref_wrapped!(Albums, Vec<SpotifyUri>);
#[derive(Debug, Clone)]
pub struct Disc {
@ -65,7 +65,7 @@ pub struct Discs(pub Vec<Disc>);
impl_deref_wrapped!(Discs, Vec<Disc>);
impl Album {
pub fn tracks(&self) -> impl Iterator<Item = &SpotifyId> {
pub fn tracks(&self) -> impl Iterator<Item = &SpotifyUri> {
self.discs.iter().flat_map(|disc| disc.tracks.iter())
}
}
@ -74,11 +74,15 @@ impl Album {
impl Metadata for Album {
type Message = protocol::metadata::Album;
async fn request(session: &Session, album_id: &SpotifyId) -> RequestResult {
async fn request(session: &Session, album_uri: &SpotifyUri) -> RequestResult {
let SpotifyUri::Album { id: album_id } = album_uri else {
return Err(Error::invalid_argument("album_uri"));
};
session.spclient().get_album_metadata(album_id).await
}
fn parse(msg: &Self::Message, _: &SpotifyId) -> Result<Self, Error> {
fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
Self::try_from(msg)
}
}

View file

@ -16,7 +16,7 @@ use crate::{
util::{impl_deref_wrapped, impl_from_repeated, impl_try_from_repeated},
};
use librespot_core::{Error, Session, SpotifyId};
use librespot_core::{Error, Session, SpotifyUri};
use librespot_protocol as protocol;
pub use protocol::metadata::artist_with_role::ArtistRole;
@ -29,7 +29,7 @@ use protocol::metadata::TopTracks as TopTracksMessage;
#[derive(Debug, Clone)]
pub struct Artist {
pub id: SpotifyId,
pub id: SpotifyUri,
pub name: String,
pub popularity: i32,
pub top_tracks: CountryTopTracks,
@ -56,7 +56,7 @@ impl_deref_wrapped!(Artists, Vec<Artist>);
#[derive(Debug, Clone)]
pub struct ArtistWithRole {
pub id: SpotifyId,
pub id: SpotifyUri,
pub name: String,
pub role: ArtistRole,
}
@ -140,14 +140,14 @@ impl Artist {
/// Get the full list of albums, not containing duplicate variants of the same albums.
///
/// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`]
pub fn albums_current(&self) -> impl Iterator<Item = &SpotifyId> {
pub fn albums_current(&self) -> impl Iterator<Item = &SpotifyUri> {
self.albums.current_releases()
}
/// Get the full list of singles, not containing duplicate variants of the same singles.
///
/// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`]
pub fn singles_current(&self) -> impl Iterator<Item = &SpotifyId> {
pub fn singles_current(&self) -> impl Iterator<Item = &SpotifyUri> {
self.singles.current_releases()
}
@ -155,14 +155,14 @@ impl Artist {
/// compilations.
///
/// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`]
pub fn compilations_current(&self) -> impl Iterator<Item = &SpotifyId> {
pub fn compilations_current(&self) -> impl Iterator<Item = &SpotifyUri> {
self.compilations.current_releases()
}
/// Get the full list of albums, not containing duplicate variants of the same albums.
///
/// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`]
pub fn appears_on_albums_current(&self) -> impl Iterator<Item = &SpotifyId> {
pub fn appears_on_albums_current(&self) -> impl Iterator<Item = &SpotifyUri> {
self.appears_on_albums.current_releases()
}
}
@ -171,11 +171,15 @@ impl Artist {
impl Metadata for Artist {
type Message = protocol::metadata::Artist;
async fn request(session: &Session, artist_id: &SpotifyId) -> RequestResult {
async fn request(session: &Session, artist_uri: &SpotifyUri) -> RequestResult {
let SpotifyUri::Artist { id: artist_id } = artist_uri else {
return Err(Error::invalid_argument("artist_uri"));
};
session.spclient().get_artist_metadata(artist_id).await
}
fn parse(msg: &Self::Message, _: &SpotifyId) -> Result<Self, Error> {
fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
Self::try_from(msg)
}
}
@ -249,7 +253,7 @@ impl AlbumGroups {
/// Get the contained albums. This will only use the latest release / variant of an album if
/// multiple variants are available. This should be used if multiple variants of the same album
/// are not explicitely desired.
pub fn current_releases(&self) -> impl Iterator<Item = &SpotifyId> {
pub fn current_releases(&self) -> impl Iterator<Item = &SpotifyUri> {
self.iter().filter_map(|agrp| agrp.first())
}
}

View file

@ -13,9 +13,7 @@ use crate::{
use super::file::AudioFiles;
use librespot_core::{
Error, Session, SpotifyId, date::Date, session::UserData, spotify_id::SpotifyItemType,
};
use librespot_core::{Error, Session, SpotifyUri, date::Date, session::UserData};
pub type AudioItemResult = Result<AudioItem, Error>;
@ -29,7 +27,7 @@ pub struct CoverImage {
#[derive(Debug, Clone)]
pub struct AudioItem {
pub track_id: SpotifyId,
pub track_id: SpotifyUri,
pub uri: String,
pub files: AudioFiles,
pub name: String,
@ -60,14 +58,14 @@ pub enum UniqueFields {
}
impl AudioItem {
pub async fn get_file(session: &Session, id: SpotifyId) -> AudioItemResult {
pub async fn get_file(session: &Session, uri: SpotifyUri) -> AudioItemResult {
let image_url = session
.get_user_attribute("image-url")
.unwrap_or_else(|| String::from("https://i.scdn.co/image/{file_id}"));
match id.item_type {
SpotifyItemType::Track => {
let track = Track::get(session, &id).await?;
match uri {
SpotifyUri::Track { .. } => {
let track = Track::get(session, &uri).await?;
if track.duration <= 0 {
return Err(Error::unavailable(MetadataError::InvalidDuration(
@ -79,8 +77,7 @@ impl AudioItem {
return Err(Error::unavailable(MetadataError::ExplicitContentFiltered));
}
let track_id = track.id;
let uri = track_id.to_uri()?;
let uri_string = uri.to_uri()?;
let album = track.album.name;
let album_artists = track
@ -123,8 +120,8 @@ impl AudioItem {
};
Ok(Self {
track_id,
uri,
track_id: uri,
uri: uri_string,
files: track.files,
name: track.name,
covers,
@ -136,8 +133,8 @@ impl AudioItem {
unique_fields,
})
}
SpotifyItemType::Episode => {
let episode = Episode::get(session, &id).await?;
SpotifyUri::Episode { .. } => {
let episode = Episode::get(session, &uri).await?;
if episode.duration <= 0 {
return Err(Error::unavailable(MetadataError::InvalidDuration(
@ -149,8 +146,7 @@ impl AudioItem {
return Err(Error::unavailable(MetadataError::ExplicitContentFiltered));
}
let track_id = episode.id;
let uri = track_id.to_uri()?;
let uri_string = uri.to_uri()?;
let covers = get_covers(episode.covers, image_url);
@ -167,8 +163,8 @@ impl AudioItem {
};
Ok(Self {
track_id,
uri,
track_id: uri,
uri: uri_string,
files: episode.audio,
name: episode.name,
covers,

View file

@ -15,14 +15,14 @@ use crate::{
video::VideoFiles,
};
use librespot_core::{Error, Session, SpotifyId, date::Date};
use librespot_core::{Error, Session, SpotifyUri, date::Date};
use librespot_protocol as protocol;
pub use protocol::metadata::episode::EpisodeType;
#[derive(Debug, Clone)]
pub struct Episode {
pub id: SpotifyId,
pub id: SpotifyUri,
pub name: String,
pub duration: i32,
pub audio: AudioFiles,
@ -49,19 +49,23 @@ pub struct Episode {
}
#[derive(Debug, Clone, Default)]
pub struct Episodes(pub Vec<SpotifyId>);
pub struct Episodes(pub Vec<SpotifyUri>);
impl_deref_wrapped!(Episodes, Vec<SpotifyId>);
impl_deref_wrapped!(Episodes, Vec<SpotifyUri>);
#[async_trait]
impl Metadata for Episode {
type Message = protocol::metadata::Episode;
async fn request(session: &Session, episode_id: &SpotifyId) -> RequestResult {
async fn request(session: &Session, episode_uri: &SpotifyUri) -> RequestResult {
let SpotifyUri::Episode { id: episode_id } = episode_uri else {
return Err(Error::invalid_argument("episode_uri"));
};
session.spclient().get_episode_metadata(episode_id).await
}
fn parse(msg: &Self::Message, _: &SpotifyId) -> Result<Self, Error> {
fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
Self::try_from(msg)
}
}

View file

@ -5,7 +5,7 @@ use std::{
use crate::util::{impl_deref_wrapped, impl_from_repeated, impl_try_from_repeated};
use librespot_core::{FileId, SpotifyId};
use librespot_core::{FileId, SpotifyUri};
use librespot_protocol as protocol;
use protocol::metadata::Image as ImageMessage;
@ -47,7 +47,7 @@ impl_deref_wrapped!(PictureSizes, Vec<PictureSize>);
#[derive(Debug, Clone)]
pub struct TranscodedPicture {
pub target_name: String,
pub uri: SpotifyId,
pub uri: SpotifyUri,
}
#[derive(Debug, Clone)]

View file

@ -6,7 +6,7 @@ extern crate async_trait;
use protobuf::Message;
use librespot_core::{Error, Session, SpotifyId};
use librespot_core::{Error, Session, SpotifyUri};
pub mod album;
pub mod artist;
@ -44,15 +44,15 @@ pub trait Metadata: Send + Sized + 'static {
type Message: protobuf::Message + std::fmt::Debug;
// Request a protobuf
async fn request(session: &Session, id: &SpotifyId) -> RequestResult;
async fn request(session: &Session, id: &SpotifyUri) -> RequestResult;
// Request a metadata struct
async fn get(session: &Session, id: &SpotifyId) -> Result<Self, Error> {
async fn get(session: &Session, id: &SpotifyUri) -> Result<Self, Error> {
let response = Self::request(session, id).await?;
let msg = Self::Message::parse_from_bytes(&response)?;
trace!("Received metadata: {msg:#?}");
Self::parse(&msg, id)
}
fn parse(msg: &Self::Message, _: &SpotifyId) -> Result<Self, Error>;
fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error>;
}

View file

@ -8,8 +8,7 @@ use crate::{
request::{MercuryRequest, RequestResult},
};
use librespot_core::{Error, Session, SpotifyId};
use librespot_core::{Error, Session, SpotifyId, SpotifyUri};
use librespot_protocol as protocol;
pub use protocol::playlist_annotate3::AbuseReportState;
@ -26,12 +25,20 @@ pub struct PlaylistAnnotation {
impl Metadata for PlaylistAnnotation {
type Message = protocol::playlist_annotate3::PlaylistAnnotation;
async fn request(session: &Session, playlist_id: &SpotifyId) -> RequestResult {
async fn request(session: &Session, playlist_uri: &SpotifyUri) -> RequestResult {
let current_user = session.username();
let SpotifyUri::Playlist {
id: playlist_id, ..
} = playlist_uri
else {
return Err(Error::invalid_argument("playlist_uri"));
};
Self::request_for_user(session, &current_user, playlist_id).await
}
fn parse(msg: &Self::Message, _: &SpotifyId) -> Result<Self, Error> {
fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
Ok(Self {
description: msg.description().to_owned(),
picture: msg.picture().to_owned(), // TODO: is this a URL or Spotify URI?
@ -60,11 +67,18 @@ impl PlaylistAnnotation {
async fn get_for_user(
session: &Session,
username: &str,
playlist_id: &SpotifyId,
playlist_uri: &SpotifyUri,
) -> Result<Self, Error> {
let SpotifyUri::Playlist {
id: playlist_id, ..
} = playlist_uri
else {
return Err(Error::invalid_argument("playlist_uri"));
};
let response = Self::request_for_user(session, username, playlist_id).await?;
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
Self::parse(&msg, playlist_id)
Self::parse(&msg, playlist_uri)
}
}

View file

@ -10,7 +10,7 @@ use super::{
permission::Capabilities,
};
use librespot_core::{SpotifyId, date::Date};
use librespot_core::{SpotifyUri, date::Date};
use librespot_protocol as protocol;
use protocol::playlist4_external::Item as PlaylistItemMessage;
@ -19,7 +19,7 @@ use protocol::playlist4_external::MetaItem as PlaylistMetaItemMessage;
#[derive(Debug, Clone)]
pub struct PlaylistItem {
pub id: SpotifyId,
pub id: SpotifyUri,
pub attributes: PlaylistItemAttributes,
}
@ -38,7 +38,7 @@ pub struct PlaylistItemList {
#[derive(Debug, Clone)]
pub struct PlaylistMetaItem {
pub revision: SpotifyId,
pub revision: SpotifyUri,
pub attributes: PlaylistAttributes,
pub length: i32,
pub timestamp: Date,

View file

@ -14,12 +14,7 @@ use super::{
permission::Capabilities,
};
use librespot_core::{
Error, Session,
date::Date,
spotify_id::{NamedSpotifyId, SpotifyId},
};
use librespot_core::{Error, Session, SpotifyUri, date::Date, spotify_id::SpotifyId};
use librespot_protocol as protocol;
use protocol::playlist4_external::GeoblockBlockingType as Geoblock;
@ -30,7 +25,7 @@ impl_deref_wrapped!(Geoblocks, Vec<Geoblock>);
#[derive(Debug, Clone)]
pub struct Playlist {
pub id: NamedSpotifyId,
pub id: SpotifyUri,
pub revision: Vec<u8>,
pub length: i32,
pub attributes: PlaylistAttributes,
@ -72,7 +67,7 @@ pub struct SelectedListContent {
}
impl Playlist {
pub fn tracks(&self) -> impl ExactSizeIterator<Item = &SpotifyId> {
pub fn tracks(&self) -> impl ExactSizeIterator<Item = &SpotifyUri> {
let tracks = self.contents.items.iter().map(|item| &item.id);
let length = tracks.len();
@ -93,17 +88,35 @@ impl Playlist {
impl Metadata for Playlist {
type Message = protocol::playlist4_external::SelectedListContent;
async fn request(session: &Session, playlist_id: &SpotifyId) -> RequestResult {
async fn request(session: &Session, playlist_uri: &SpotifyUri) -> RequestResult {
let SpotifyUri::Playlist {
id: playlist_id, ..
} = playlist_uri
else {
return Err(Error::invalid_argument("playlist_uri"));
};
session.spclient().get_playlist(playlist_id).await
}
fn parse(msg: &Self::Message, id: &SpotifyId) -> Result<Self, Error> {
fn parse(msg: &Self::Message, uri: &SpotifyUri) -> Result<Self, Error> {
let SpotifyUri::Playlist {
id: playlist_id, ..
} = uri
else {
return Err(Error::invalid_argument("playlist_uri"));
};
// the playlist proto doesn't contain the id so we decorate it
let playlist = SelectedListContent::try_from(msg)?;
let id = NamedSpotifyId::from_spotify_id(*id, &playlist.owner_username);
let new_uri = SpotifyUri::Playlist {
id: *playlist_id,
user: Some(playlist.owner_username),
};
Ok(Self {
id,
id: new_uri,
revision: playlist.revision,
length: playlist.length,
attributes: playlist.attributes,

View file

@ -5,7 +5,7 @@ use crate::{
episode::Episodes, image::Images, restriction::Restrictions,
};
use librespot_core::{Error, Session, SpotifyId};
use librespot_core::{Error, Session, SpotifyUri};
use librespot_protocol as protocol;
pub use protocol::metadata::show::ConsumptionOrder as ShowConsumptionOrder;
@ -13,7 +13,7 @@ pub use protocol::metadata::show::MediaType as ShowMediaType;
#[derive(Debug, Clone)]
pub struct Show {
pub id: SpotifyId,
pub id: SpotifyUri,
pub name: String,
pub description: String,
pub publisher: String,
@ -27,7 +27,7 @@ pub struct Show {
pub media_type: ShowMediaType,
pub consumption_order: ShowConsumptionOrder,
pub availability: Availabilities,
pub trailer_uri: Option<SpotifyId>,
pub trailer_uri: Option<SpotifyUri>,
pub has_music_and_talk: bool,
pub is_audiobook: bool,
}
@ -36,11 +36,15 @@ pub struct Show {
impl Metadata for Show {
type Message = protocol::metadata::Show;
async fn request(session: &Session, show_id: &SpotifyId) -> RequestResult {
async fn request(session: &Session, show_uri: &SpotifyUri) -> RequestResult {
let SpotifyUri::Show { id: show_id } = show_uri else {
return Err(Error::invalid_argument("show_uri"));
};
session.spclient().get_show_metadata(show_id).await
}
fn parse(msg: &Self::Message, _: &SpotifyId) -> Result<Self, Error> {
fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
Self::try_from(msg)
}
}
@ -67,7 +71,7 @@ impl TryFrom<&<Self as Metadata>::Message> for Show {
.trailer_uri
.as_deref()
.filter(|s| !s.is_empty())
.map(SpotifyId::from_uri)
.map(SpotifyUri::from_uri)
.transpose()?,
has_music_and_talk: show.music_and_talk(),
is_audiobook: show.is_audiobook(),

View file

@ -17,12 +17,12 @@ use crate::{
util::{impl_deref_wrapped, impl_try_from_repeated},
};
use librespot_core::{Error, Session, SpotifyId, date::Date};
use librespot_core::{Error, Session, SpotifyUri, date::Date};
use librespot_protocol as protocol;
#[derive(Debug, Clone)]
pub struct Track {
pub id: SpotifyId,
pub id: SpotifyUri,
pub name: String,
pub album: Album,
pub artists: Artists,
@ -50,19 +50,23 @@ pub struct Track {
}
#[derive(Debug, Clone, Default)]
pub struct Tracks(pub Vec<SpotifyId>);
pub struct Tracks(pub Vec<SpotifyUri>);
impl_deref_wrapped!(Tracks, Vec<SpotifyId>);
impl_deref_wrapped!(Tracks, Vec<SpotifyUri>);
#[async_trait]
impl Metadata for Track {
type Message = protocol::metadata::Track;
async fn request(session: &Session, track_id: &SpotifyId) -> RequestResult {
async fn request(session: &Session, track_uri: &SpotifyUri) -> RequestResult {
let SpotifyUri::Track { id: track_id } = track_uri else {
return Err(Error::invalid_argument("track_uri"));
};
session.spclient().get_track_metadata(track_id).await
}
fn parse(msg: &Self::Message, _: &SpotifyId) -> Result<Self, Error> {
fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
Self::try_from(msg)
}
}

View file

@ -15,27 +15,26 @@ use std::{
time::{Duration, Instant},
};
use futures_util::{
StreamExt, TryFutureExt, future, future::FusedFuture,
stream::futures_unordered::FuturesUnordered,
};
use parking_lot::Mutex;
use symphonia::core::io::MediaSource;
use tokio::sync::{mpsc, oneshot};
#[cfg(feature = "passthrough-decoder")]
use crate::decoder::PassthroughDecoder;
use crate::{
audio::{AudioDecrypt, AudioFetchParams, AudioFile, StreamLoaderController},
audio_backend::Sink,
config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig},
convert::Converter,
core::{Error, Session, SpotifyId, util::SeqGenerator},
core::{Error, Session, SpotifyId, SpotifyUri, util::SeqGenerator},
decoder::{AudioDecoder, AudioPacket, AudioPacketPosition, SymphoniaDecoder},
metadata::audio::{AudioFileFormat, AudioFiles, AudioItem},
mixer::VolumeGetter,
};
#[cfg(feature = "passthrough-decoder")]
use crate::decoder::PassthroughDecoder;
use futures_util::{
StreamExt, TryFutureExt, future, future::FusedFuture,
stream::futures_unordered::FuturesUnordered,
};
use librespot_metadata::track::Tracks;
use parking_lot::Mutex;
use symphonia::core::io::MediaSource;
use tokio::sync::{mpsc, oneshot};
use crate::SAMPLES_PER_SECOND;
@ -94,12 +93,12 @@ static PLAYER_COUNTER: AtomicUsize = AtomicUsize::new(0);
enum PlayerCommand {
Load {
track_id: SpotifyId,
track_id: SpotifyUri,
play: bool,
position_ms: u32,
},
Preload {
track_id: SpotifyId,
track_id: SpotifyUri,
},
Play,
Pause,
@ -142,17 +141,17 @@ pub enum PlayerEvent {
// Fired when the player is stopped (e.g. by issuing a "stop" command to the player).
Stopped {
play_request_id: u64,
track_id: SpotifyId,
track_id: SpotifyUri,
},
// The player is delayed by loading a track.
Loading {
play_request_id: u64,
track_id: SpotifyId,
track_id: SpotifyUri,
position_ms: u32,
},
// The player is preloading a track.
Preloading {
track_id: SpotifyId,
track_id: SpotifyUri,
},
// The player is playing a track.
// This event is issued at the start of playback of whenever the position must be communicated
@ -163,31 +162,31 @@ pub enum PlayerEvent {
// after a buffer-underrun
Playing {
play_request_id: u64,
track_id: SpotifyId,
track_id: SpotifyUri,
position_ms: u32,
},
// The player entered a paused state.
Paused {
play_request_id: u64,
track_id: SpotifyId,
track_id: SpotifyUri,
position_ms: u32,
},
// The player thinks it's a good idea to issue a preload command for the next track now.
// This event is intended for use within spirc.
TimeToPreloadNextTrack {
play_request_id: u64,
track_id: SpotifyId,
track_id: SpotifyUri,
},
// The player reached the end of a track.
// This event is intended for use within spirc. Spirc will respond by issuing another command.
EndOfTrack {
play_request_id: u64,
track_id: SpotifyId,
track_id: SpotifyUri,
},
// The player was unable to load the requested track.
Unavailable {
play_request_id: u64,
track_id: SpotifyId,
track_id: SpotifyUri,
},
// The mixer volume was set to a new level.
VolumeChanged {
@ -195,7 +194,7 @@ pub enum PlayerEvent {
},
PositionCorrection {
play_request_id: u64,
track_id: SpotifyId,
track_id: SpotifyUri,
position_ms: u32,
},
/// Requires `PlayerConfig::position_update_interval` to be set to Some.
@ -203,12 +202,12 @@ pub enum PlayerEvent {
/// current playback position
PositionChanged {
play_request_id: u64,
track_id: SpotifyId,
track_id: SpotifyUri,
position_ms: u32,
},
Seeked {
play_request_id: u64,
track_id: SpotifyId,
track_id: SpotifyUri,
position_ms: u32,
},
TrackChanged {
@ -526,7 +525,7 @@ impl Player {
}
}
pub fn load(&self, track_id: SpotifyId, start_playing: bool, position_ms: u32) {
pub fn load(&self, track_id: SpotifyUri, start_playing: bool, position_ms: u32) {
self.command(PlayerCommand::Load {
track_id,
play: start_playing,
@ -534,7 +533,7 @@ impl Player {
});
}
pub fn preload(&self, track_id: SpotifyId) {
pub fn preload(&self, track_id: SpotifyUri) {
self.command(PlayerCommand::Preload { track_id });
}
@ -660,11 +659,11 @@ struct PlayerLoadedTrackData {
enum PlayerPreload {
None,
Loading {
track_id: SpotifyId,
track_id: SpotifyUri,
loader: Pin<Box<dyn FusedFuture<Output = Result<PlayerLoadedTrackData, ()>> + Send>>,
},
Ready {
track_id: SpotifyId,
track_id: SpotifyUri,
loaded_track: Box<PlayerLoadedTrackData>,
},
}
@ -674,13 +673,13 @@ type Decoder = Box<dyn AudioDecoder + Send>;
enum PlayerState {
Stopped,
Loading {
track_id: SpotifyId,
track_id: SpotifyUri,
play_request_id: u64,
start_playback: bool,
loader: Pin<Box<dyn FusedFuture<Output = Result<PlayerLoadedTrackData, ()>> + Send>>,
},
Paused {
track_id: SpotifyId,
track_id: SpotifyUri,
play_request_id: u64,
decoder: Decoder,
audio_item: AudioItem,
@ -694,7 +693,7 @@ enum PlayerState {
is_explicit: bool,
},
Playing {
track_id: SpotifyId,
track_id: SpotifyUri,
play_request_id: u64,
decoder: Decoder,
normalisation_data: NormalisationData,
@ -709,7 +708,7 @@ enum PlayerState {
is_explicit: bool,
},
EndOfTrack {
track_id: SpotifyId,
track_id: SpotifyUri,
play_request_id: u64,
loaded_track: PlayerLoadedTrackData,
},
@ -893,10 +892,12 @@ impl PlayerTrackLoader {
None
} else if !audio_item.files.is_empty() {
Some(audio_item)
} else if let Some(alternatives) = &audio_item.alternatives {
let alternatives: FuturesUnordered<_> = alternatives
.iter()
.map(|alt_id| AudioItem::get_file(&self.session, *alt_id))
} else if let Some(alternatives) = audio_item.alternatives {
let Tracks(alternatives_vec) = alternatives; // required to make `into_iter` able to move
let alternatives: FuturesUnordered<_> = alternatives_vec
.into_iter()
.map(|alt_id| AudioItem::get_file(&self.session, alt_id))
.collect();
alternatives
@ -938,16 +939,40 @@ impl PlayerTrackLoader {
async fn load_track(
&self,
spotify_id: SpotifyId,
track_uri: SpotifyUri,
position_ms: u32,
) -> Option<PlayerLoadedTrackData> {
let audio_item = match AudioItem::get_file(&self.session, spotify_id).await {
match track_uri {
SpotifyUri::Track { .. } | SpotifyUri::Episode { .. } => {
self.load_remote_track(track_uri, position_ms).await
}
_ => {
error!("Cannot handle load of track with URI: <{track_uri}>",);
None
}
}
}
async fn load_remote_track(
&self,
track_uri: SpotifyUri,
position_ms: u32,
) -> Option<PlayerLoadedTrackData> {
let track_id: SpotifyId = match (&track_uri).try_into() {
Ok(id) => id,
Err(_) => {
warn!("<{track_uri}> could not be converted to a base62 ID");
return None;
}
};
let audio_item = match AudioItem::get_file(&self.session, track_uri).await {
Ok(audio) => match self.find_available_alternative(audio).await {
Some(audio) => audio,
None => {
warn!(
"<{}> is not available",
spotify_id.to_uri().unwrap_or_default()
"spotify:track:<{}> is not available",
track_id.to_base62().unwrap_or_default()
);
return None;
}
@ -1033,13 +1058,14 @@ impl PlayerTrackLoader {
// Not all audio files are encrypted. If we can't get a key, try loading the track
// without decryption. If the file was encrypted after all, the decoder will fail
// parsing and bail out, so we should be safe from outputting ear-piercing noise.
let key = match self.session.audio_key().request(spotify_id, file_id).await {
let key = match self.session.audio_key().request(track_id, file_id).await {
Ok(key) => Some(key),
Err(e) => {
warn!("Unable to load key, continuing without decryption: {e}");
None
}
};
let mut decrypted_file = AudioDecrypt::new(key, encrypted_file);
let is_ogg_vorbis = AudioFiles::is_ogg_vorbis(format);
@ -1195,13 +1221,15 @@ impl Future for PlayerInternal {
// Handle loading of a new track to play
if let PlayerState::Loading {
ref mut loader,
track_id,
ref track_id,
start_playback,
play_request_id,
} = self.state
{
// The loader may be terminated if we are trying to load the same track
// as before, and that track failed to open before.
let track_id = track_id.clone();
if !loader.as_mut().is_terminated() {
match loader.as_mut().poll(cx) {
Poll::Ready(Ok(loaded_track)) => {
@ -1233,12 +1261,15 @@ impl Future for PlayerInternal {
// handle pending preload requests.
if let PlayerPreload::Loading {
ref mut loader,
track_id,
ref track_id,
} = self.preload
{
let track_id = track_id.clone();
match loader.as_mut().poll(cx) {
Poll::Ready(Ok(loaded_track)) => {
self.send_event(PlayerEvent::Preloading { track_id });
self.send_event(PlayerEvent::Preloading {
track_id: track_id.clone(),
});
self.preload = PlayerPreload::Ready {
track_id,
loaded_track: Box::new(loaded_track),
@ -1269,7 +1300,7 @@ impl Future for PlayerInternal {
self.ensure_sink_running();
if let PlayerState::Playing {
track_id,
ref track_id,
play_request_id,
ref mut decoder,
normalisation_factor,
@ -1278,6 +1309,7 @@ impl Future for PlayerInternal {
..
} = self.state
{
let track_id = track_id.clone();
match decoder.next_packet() {
Ok(result) => {
if let Some((ref packet_position, ref packet)) = result {
@ -1338,7 +1370,7 @@ impl Future for PlayerInternal {
now.checked_sub(new_stream_position);
self.send_event(PlayerEvent::PositionCorrection {
play_request_id,
track_id,
track_id: track_id.clone(),
position_ms: new_stream_position_ms,
});
}
@ -1391,7 +1423,7 @@ impl Future for PlayerInternal {
}
if let PlayerState::Playing {
track_id,
ref track_id,
play_request_id,
duration_ms,
stream_position_ms,
@ -1400,7 +1432,7 @@ impl Future for PlayerInternal {
..
}
| PlayerState::Paused {
track_id,
ref track_id,
play_request_id,
duration_ms,
stream_position_ms,
@ -1409,6 +1441,8 @@ impl Future for PlayerInternal {
..
} = self.state
{
let track_id = track_id.clone();
if (!*suggested_to_preload_next_track)
&& ((duration_ms as i64 - stream_position_ms as i64)
< PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS as i64)
@ -1482,25 +1516,27 @@ impl PlayerInternal {
fn handle_player_stop(&mut self) {
match self.state {
PlayerState::Playing {
track_id,
ref track_id,
play_request_id,
..
}
| PlayerState::Paused {
track_id,
ref track_id,
play_request_id,
..
}
| PlayerState::EndOfTrack {
track_id,
ref track_id,
play_request_id,
..
}
| PlayerState::Loading {
track_id,
ref track_id,
play_request_id,
..
} => {
let track_id = track_id.clone();
self.ensure_sink_stopped(false);
self.send_event(PlayerEvent::Stopped {
track_id,
@ -1519,11 +1555,13 @@ impl PlayerInternal {
fn handle_play(&mut self) {
match self.state {
PlayerState::Paused {
track_id,
ref track_id,
play_request_id,
stream_position_ms,
..
} => {
let track_id = track_id.clone();
self.state.paused_to_playing();
self.send_event(PlayerEvent::Playing {
track_id,
@ -1546,11 +1584,13 @@ impl PlayerInternal {
match self.state {
PlayerState::Paused { .. } => self.ensure_sink_stopped(false),
PlayerState::Playing {
track_id,
ref track_id,
play_request_id,
stream_position_ms,
..
} => {
let track_id = track_id.clone();
self.state.playing_to_paused();
self.ensure_sink_stopped(false);
@ -1681,13 +1721,13 @@ impl PlayerInternal {
None => {
self.state.playing_to_end_of_track();
if let PlayerState::EndOfTrack {
track_id,
ref track_id,
play_request_id,
..
} = self.state
{
self.send_event(PlayerEvent::EndOfTrack {
track_id,
track_id: track_id.clone(),
play_request_id,
})
} else {
@ -1700,7 +1740,7 @@ impl PlayerInternal {
fn start_playback(
&mut self,
track_id: SpotifyId,
track_id: SpotifyUri,
play_request_id: u64,
loaded_track: PlayerLoadedTrackData,
start_playback: bool,
@ -1725,7 +1765,7 @@ impl PlayerInternal {
if start_playback {
self.ensure_sink_running();
self.send_event(PlayerEvent::Playing {
track_id,
track_id: track_id.clone(),
play_request_id,
position_ms,
});
@ -1750,7 +1790,7 @@ impl PlayerInternal {
self.ensure_sink_stopped(false);
self.state = PlayerState::Paused {
track_id,
track_id: track_id.clone(),
play_request_id,
decoder: loaded_track.decoder,
audio_item: loaded_track.audio_item,
@ -1774,7 +1814,7 @@ impl PlayerInternal {
fn handle_command_load(
&mut self,
track_id: SpotifyId,
track_id: SpotifyUri,
play_request_id_option: Option<u64>,
play: bool,
position_ms: u32,
@ -1803,9 +1843,9 @@ impl PlayerInternal {
if let PlayerState::EndOfTrack {
track_id: previous_track_id,
..
} = self.state
} = &self.state
{
if previous_track_id == track_id {
if *previous_track_id == track_id {
let mut loaded_track = match mem::replace(&mut self.state, PlayerState::Invalid) {
PlayerState::EndOfTrack { loaded_track, .. } => loaded_track,
_ => {
@ -1834,19 +1874,19 @@ impl PlayerInternal {
// Check if we are already playing the track. If so, just do a seek and update our info.
if let PlayerState::Playing {
track_id: current_track_id,
track_id: ref current_track_id,
ref mut stream_position_ms,
ref mut decoder,
..
}
| PlayerState::Paused {
track_id: current_track_id,
track_id: ref current_track_id,
ref mut stream_position_ms,
ref mut decoder,
..
} = self.state
{
if current_track_id == track_id {
if *current_track_id == track_id {
// we can use the current decoder. Ensure it's at the correct position.
if position_ms != *stream_position_ms {
// This may be blocking.
@ -1915,9 +1955,9 @@ impl PlayerInternal {
if let PlayerPreload::Ready {
track_id: loaded_track_id,
..
} = self.preload
} = &self.preload
{
if track_id == loaded_track_id {
if track_id == *loaded_track_id {
let preload = std::mem::replace(&mut self.preload, PlayerPreload::None);
if let PlayerPreload::Ready {
track_id,
@ -1940,7 +1980,7 @@ impl PlayerInternal {
}
self.send_event(PlayerEvent::Loading {
track_id,
track_id: track_id.clone(),
play_request_id,
position_ms,
});
@ -1949,9 +1989,9 @@ impl PlayerInternal {
let loader = if let PlayerPreload::Loading {
track_id: loaded_track_id,
..
} = self.preload
} = &self.preload
{
if (track_id == loaded_track_id) && (position_ms == 0) {
if (track_id == *loaded_track_id) && (position_ms == 0) {
let mut preload = PlayerPreload::None;
std::mem::swap(&mut preload, &mut self.preload);
if let PlayerPreload::Loading { loader, .. } = preload {
@ -1969,7 +2009,8 @@ impl PlayerInternal {
self.preload = PlayerPreload::None;
// If we don't have a loader yet, create one from scratch.
let loader = loader.unwrap_or_else(|| Box::pin(self.load_track(track_id, position_ms)));
let loader =
loader.unwrap_or_else(|| Box::pin(self.load_track(track_id.clone(), position_ms)));
// Set ourselves to a loading state.
self.state = PlayerState::Loading {
@ -1982,7 +2023,7 @@ impl PlayerInternal {
Ok(())
}
fn handle_command_preload(&mut self, track_id: SpotifyId) {
fn handle_command_preload(&mut self, track_id: SpotifyUri) {
debug!("Preloading track");
let mut preload_track = true;
// check whether the track is already loaded somewhere or being loaded.
@ -1993,9 +2034,9 @@ impl PlayerInternal {
| PlayerPreload::Ready {
track_id: currently_loading,
..
} = self.preload
} = &self.preload
{
if currently_loading == track_id {
if *currently_loading == track_id {
// we're already preloading the requested track.
preload_track = false;
} else {
@ -2015,9 +2056,9 @@ impl PlayerInternal {
| PlayerState::EndOfTrack {
track_id: current_track_id,
..
} = self.state
} = &self.state
{
if current_track_id == track_id {
if *current_track_id == track_id {
// we already have the requested track loaded.
preload_track = false;
}
@ -2025,7 +2066,7 @@ impl PlayerInternal {
// schedule the preload of the current track if desired.
if preload_track {
let loader = self.load_track(track_id, 0);
let loader = self.load_track(track_id.clone(), 0);
self.preload = PlayerPreload::Loading {
track_id,
loader: Box::pin(loader),
@ -2039,14 +2080,14 @@ impl PlayerInternal {
// that. In this case just restart the loading process but
// with the requested position.
if let PlayerState::Loading {
track_id,
ref track_id,
play_request_id,
start_playback,
..
} = self.state
{
return self.handle_command_load(
track_id,
track_id.clone(),
Some(play_request_id),
start_playback,
position_ms,
@ -2058,13 +2099,13 @@ impl PlayerInternal {
Ok(new_position_ms) => {
if let PlayerState::Playing {
ref mut stream_position_ms,
track_id,
ref track_id,
play_request_id,
..
}
| PlayerState::Paused {
ref mut stream_position_ms,
track_id,
ref track_id,
play_request_id,
..
} = self.state
@ -2073,7 +2114,7 @@ impl PlayerInternal {
self.send_event(PlayerEvent::Seeked {
play_request_id,
track_id,
track_id: track_id.clone(),
position_ms: new_position_ms,
});
}
@ -2177,18 +2218,20 @@ impl PlayerInternal {
if filter {
if let PlayerState::Playing {
track_id,
ref track_id,
play_request_id,
is_explicit,
..
}
| PlayerState::Paused {
track_id,
ref track_id,
play_request_id,
is_explicit,
..
} = self.state
{
let track_id = track_id.clone();
if is_explicit {
warn!(
"Currently loaded track is explicit, which client setting forbids -- skipping to next track."
@ -2213,7 +2256,7 @@ impl PlayerInternal {
fn load_track(
&mut self,
spotify_id: SpotifyId,
spotify_uri: SpotifyUri,
position_ms: u32,
) -> impl FusedFuture<Output = Result<PlayerLoadedTrackData, ()>> + Send + 'static {
// This method creates a future that returns the loaded stream and associated info.
@ -2231,8 +2274,9 @@ impl PlayerInternal {
let load_handles_clone = self.load_handles.clone();
let handle = tokio::runtime::Handle::current();
let load_handle = thread::spawn(move || {
let data = handle.block_on(loader.load_track(spotify_id, position_ms));
let data = handle.block_on(loader.load_track(spotify_uri, position_ms));
if let Some(data) = data {
let _ = result_tx.send(data);
}
@ -2376,7 +2420,7 @@ impl fmt::Debug for PlayerCommand {
impl fmt::Debug for PlayerState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use PlayerState::*;
match *self {
match self {
Stopped => f.debug_struct("Stopped").finish(),
Loading {
track_id,

View file

@ -28,7 +28,7 @@ impl EventHandler {
env_vars.insert("PLAY_REQUEST_ID", play_request_id.to_string());
}
PlayerEvent::TrackChanged { audio_item } => {
match audio_item.track_id.to_base62() {
match audio_item.track_id.to_id() {
Err(e) => {
warn!("PlayerEvent::TrackChanged: Invalid track id: {e}")
}
@ -104,7 +104,7 @@ impl EventHandler {
}
}
}
PlayerEvent::Stopped { track_id, .. } => match track_id.to_base62() {
PlayerEvent::Stopped { track_id, .. } => match track_id.to_id() {
Err(e) => warn!("PlayerEvent::Stopped: Invalid track id: {e}"),
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "stopped".to_string());
@ -115,7 +115,7 @@ impl EventHandler {
track_id,
position_ms,
..
} => match track_id.to_base62() {
} => match track_id.to_id() {
Err(e) => warn!("PlayerEvent::Playing: Invalid track id: {e}"),
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "playing".to_string());
@ -127,7 +127,7 @@ impl EventHandler {
track_id,
position_ms,
..
} => match track_id.to_base62() {
} => match track_id.to_id() {
Err(e) => warn!("PlayerEvent::Paused: Invalid track id: {e}"),
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "paused".to_string());
@ -135,26 +135,24 @@ impl EventHandler {
env_vars.insert("POSITION_MS", position_ms.to_string());
}
},
PlayerEvent::Loading { track_id, .. } => match track_id.to_base62() {
PlayerEvent::Loading { track_id, .. } => match track_id.to_id() {
Err(e) => warn!("PlayerEvent::Loading: Invalid track id: {e}"),
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "loading".to_string());
env_vars.insert("TRACK_ID", id);
}
},
PlayerEvent::Preloading { track_id, .. } => {
match track_id.to_base62() {
Err(e) => {
warn!("PlayerEvent::Preloading: Invalid track id: {e}")
}
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "preloading".to_string());
env_vars.insert("TRACK_ID", id);
}
PlayerEvent::Preloading { track_id, .. } => match track_id.to_id() {
Err(e) => {
warn!("PlayerEvent::Preloading: Invalid track id: {e}")
}
}
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "preloading".to_string());
env_vars.insert("TRACK_ID", id);
}
},
PlayerEvent::TimeToPreloadNextTrack { track_id, .. } => {
match track_id.to_base62() {
match track_id.to_id() {
Err(e) => warn!(
"PlayerEvent::TimeToPreloadNextTrack: Invalid track id: {e}"
),
@ -164,19 +162,16 @@ impl EventHandler {
}
}
}
PlayerEvent::EndOfTrack { track_id, .. } => {
match track_id.to_base62() {
Err(e) => {
warn!("PlayerEvent::EndOfTrack: Invalid track id: {e}")
}
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "end_of_track".to_string());
env_vars.insert("TRACK_ID", id);
}
PlayerEvent::EndOfTrack { track_id, .. } => match track_id.to_id() {
Err(e) => {
warn!("PlayerEvent::EndOfTrack: Invalid track id: {e}")
}
}
PlayerEvent::Unavailable { track_id, .. } => match track_id.to_base62()
{
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "end_of_track".to_string());
env_vars.insert("TRACK_ID", id);
}
},
PlayerEvent::Unavailable { track_id, .. } => match track_id.to_id() {
Err(e) => warn!("PlayerEvent::Unavailable: Invalid track id: {e}"),
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "unavailable".to_string());
@ -191,7 +186,7 @@ impl EventHandler {
track_id,
position_ms,
..
} => match track_id.to_base62() {
} => match track_id.to_id() {
Err(e) => warn!("PlayerEvent::Seeked: Invalid track id: {e}"),
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "seeked".to_string());
@ -203,7 +198,7 @@ impl EventHandler {
track_id,
position_ms,
..
} => match track_id.to_base62() {
} => match track_id.to_id() {
Err(e) => {
warn!("PlayerEvent::PositionCorrection: Invalid track id: {e}")
}