1
0
Fork 0
mirror of https://github.com/librespot-org/librespot.git synced 2025-10-03 09:49:31 +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

@ -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)
}
}