mirror of
https://github.com/librespot-org/librespot.git
synced 2025-10-03 17:59:24 +02:00
Compare commits
7 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
51a752f4d5 | ||
![]() |
a407beaa45 | ||
![]() |
eb7c65e77b | ||
![]() |
6f6cd04874 | ||
![]() |
df5f957bdd | ||
![]() |
0e5531ff54 | ||
![]() |
f16a30e86a |
45 changed files with 1636 additions and 1006 deletions
|
@ -1,5 +1,5 @@
|
||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
ARG alpine_version=alpine3.19
|
ARG alpine_version=alpine3.20
|
||||||
ARG rust_version=1.85.0
|
ARG rust_version=1.85.0
|
||||||
FROM rust:${rust_version}-${alpine_version}
|
FROM rust:${rust_version}-${alpine_version}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ RUN apk add --no-cache \
|
||||||
pkgconf \
|
pkgconf \
|
||||||
musl-dev \
|
musl-dev \
|
||||||
# developer dependencies
|
# developer dependencies
|
||||||
|
openssl-dev \
|
||||||
libunwind-dev \
|
libunwind-dev \
|
||||||
pulseaudio-dev \
|
pulseaudio-dev \
|
||||||
portaudio-dev \
|
portaudio-dev \
|
||||||
|
|
25
CHANGELOG.md
25
CHANGELOG.md
|
@ -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/),
|
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.
|
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
|
## [v0.7.1] - 2025-08-31
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
551
Cargo.lock
generated
551
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -179,7 +179,6 @@ tokio = { version = "1", features = [
|
||||||
"macros",
|
"macros",
|
||||||
"signal",
|
"signal",
|
||||||
"sync",
|
"sync",
|
||||||
"parking_lot",
|
|
||||||
"process",
|
"process",
|
||||||
] }
|
] }
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
|
|
|
@ -23,12 +23,11 @@ librespot-core = { version = "0.7.1", path = "../core", default-features = false
|
||||||
aes = "0.8"
|
aes = "0.8"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
ctr = "0.9"
|
ctr = "0.9"
|
||||||
futures-util = "0.3"
|
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
||||||
http-body-util = "0.1"
|
http-body-util = "0.1"
|
||||||
hyper = { version = "1.6", features = ["http1", "http2"] }
|
hyper = { version = "1.6", features = ["http1", "http2"] }
|
||||||
hyper-util = { version = "0.1", features = ["client", "http2"] }
|
hyper-util = { version = "0.1", features = ["client", "http2"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
|
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tokio = { version = "1", features = ["macros", "parking_lot", "sync"] }
|
tokio = { version = "1", features = ["macros", "sync"] }
|
||||||
|
|
|
@ -8,13 +8,14 @@ use std::{
|
||||||
Arc, OnceLock,
|
Arc, OnceLock,
|
||||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||||
},
|
},
|
||||||
|
sync::{Condvar, Mutex},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use futures_util::{StreamExt, TryFutureExt, future::IntoStream};
|
use futures_util::{StreamExt, TryFutureExt, future::IntoStream};
|
||||||
use hyper::{Response, StatusCode, body::Incoming, header::CONTENT_RANGE};
|
use hyper::{Response, StatusCode, body::Incoming, header::CONTENT_RANGE};
|
||||||
use hyper_util::client::legacy::ResponseFuture;
|
use hyper_util::client::legacy::ResponseFuture;
|
||||||
use parking_lot::{Condvar, Mutex};
|
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::sync::{Semaphore, mpsc, oneshot};
|
use tokio::sync::{Semaphore, mpsc, oneshot};
|
||||||
|
@ -27,6 +28,8 @@ use crate::range_set::{Range, RangeSet};
|
||||||
|
|
||||||
pub type AudioFileResult = Result<(), librespot_core::Error>;
|
pub type AudioFileResult = Result<(), librespot_core::Error>;
|
||||||
|
|
||||||
|
const DOWNLOAD_STATUS_POISON_MSG: &str = "audio download status mutex should not be poisoned";
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum AudioFileError {
|
pub enum AudioFileError {
|
||||||
#[error("other end of channel disconnected")]
|
#[error("other end of channel disconnected")]
|
||||||
|
@ -163,7 +166,10 @@ impl StreamLoaderController {
|
||||||
|
|
||||||
pub fn range_available(&self, range: Range) -> bool {
|
pub fn range_available(&self, range: Range) -> bool {
|
||||||
if let Some(ref shared) = self.stream_shared {
|
if let Some(ref shared) = self.stream_shared {
|
||||||
let download_status = shared.download_status.lock();
|
let download_status = shared
|
||||||
|
.download_status
|
||||||
|
.lock()
|
||||||
|
.expect(DOWNLOAD_STATUS_POISON_MSG);
|
||||||
|
|
||||||
range.length
|
range.length
|
||||||
<= download_status
|
<= download_status
|
||||||
|
@ -214,7 +220,10 @@ impl StreamLoaderController {
|
||||||
self.fetch(range);
|
self.fetch(range);
|
||||||
|
|
||||||
if let Some(ref shared) = self.stream_shared {
|
if let Some(ref shared) = self.stream_shared {
|
||||||
let mut download_status = shared.download_status.lock();
|
let mut download_status = shared
|
||||||
|
.download_status
|
||||||
|
.lock()
|
||||||
|
.expect(DOWNLOAD_STATUS_POISON_MSG);
|
||||||
let download_timeout = AudioFetchParams::get().download_timeout;
|
let download_timeout = AudioFetchParams::get().download_timeout;
|
||||||
|
|
||||||
while range.length
|
while range.length
|
||||||
|
@ -222,11 +231,13 @@ impl StreamLoaderController {
|
||||||
.downloaded
|
.downloaded
|
||||||
.contained_length_from_value(range.start)
|
.contained_length_from_value(range.start)
|
||||||
{
|
{
|
||||||
if shared
|
let (new_download_status, wait_result) = shared
|
||||||
.cond
|
.cond
|
||||||
.wait_for(&mut download_status, download_timeout)
|
.wait_timeout(download_status, download_timeout)
|
||||||
.timed_out()
|
.expect(DOWNLOAD_STATUS_POISON_MSG);
|
||||||
{
|
|
||||||
|
download_status = new_download_status;
|
||||||
|
if wait_result.timed_out() {
|
||||||
return Err(AudioFileError::WaitTimeout.into());
|
return Err(AudioFileError::WaitTimeout.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -558,7 +569,11 @@ impl Read for AudioFileStreaming {
|
||||||
let mut ranges_to_request = RangeSet::new();
|
let mut ranges_to_request = RangeSet::new();
|
||||||
ranges_to_request.add_range(&Range::new(offset, length_to_request));
|
ranges_to_request.add_range(&Range::new(offset, length_to_request));
|
||||||
|
|
||||||
let mut download_status = self.shared.download_status.lock();
|
let mut download_status = self
|
||||||
|
.shared
|
||||||
|
.download_status
|
||||||
|
.lock()
|
||||||
|
.expect(DOWNLOAD_STATUS_POISON_MSG);
|
||||||
|
|
||||||
ranges_to_request.subtract_range_set(&download_status.downloaded);
|
ranges_to_request.subtract_range_set(&download_status.downloaded);
|
||||||
ranges_to_request.subtract_range_set(&download_status.requested);
|
ranges_to_request.subtract_range_set(&download_status.requested);
|
||||||
|
@ -571,12 +586,14 @@ impl Read for AudioFileStreaming {
|
||||||
|
|
||||||
let download_timeout = AudioFetchParams::get().download_timeout;
|
let download_timeout = AudioFetchParams::get().download_timeout;
|
||||||
while !download_status.downloaded.contains(offset) {
|
while !download_status.downloaded.contains(offset) {
|
||||||
if self
|
let (new_download_status, wait_result) = self
|
||||||
.shared
|
.shared
|
||||||
.cond
|
.cond
|
||||||
.wait_for(&mut download_status, download_timeout)
|
.wait_timeout(download_status, download_timeout)
|
||||||
.timed_out()
|
.expect(DOWNLOAD_STATUS_POISON_MSG);
|
||||||
{
|
|
||||||
|
download_status = new_download_status;
|
||||||
|
if wait_result.timed_out() {
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::TimedOut,
|
io::ErrorKind::TimedOut,
|
||||||
Error::deadline_exceeded(AudioFileError::WaitTimeout),
|
Error::deadline_exceeded(AudioFileError::WaitTimeout),
|
||||||
|
@ -619,6 +636,7 @@ impl Seek for AudioFileStreaming {
|
||||||
.shared
|
.shared
|
||||||
.download_status
|
.download_status
|
||||||
.lock()
|
.lock()
|
||||||
|
.expect(DOWNLOAD_STATUS_POISON_MSG)
|
||||||
.downloaded
|
.downloaded
|
||||||
.contains(requested_pos as usize);
|
.contains(requested_pos as usize);
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ enum ReceivedData {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ONE_SECOND: Duration = Duration::from_secs(1);
|
const ONE_SECOND: Duration = Duration::from_secs(1);
|
||||||
|
const DOWNLOAD_STATUS_POISON_MSG: &str = "audio download status mutex should not be poisoned";
|
||||||
|
|
||||||
async fn receive_data(
|
async fn receive_data(
|
||||||
shared: Arc<AudioFileShared>,
|
shared: Arc<AudioFileShared>,
|
||||||
|
@ -124,7 +125,10 @@ async fn receive_data(
|
||||||
if bytes_remaining > 0 {
|
if bytes_remaining > 0 {
|
||||||
{
|
{
|
||||||
let missing_range = Range::new(offset, bytes_remaining);
|
let missing_range = Range::new(offset, bytes_remaining);
|
||||||
let mut download_status = shared.download_status.lock();
|
let mut download_status = shared
|
||||||
|
.download_status
|
||||||
|
.lock()
|
||||||
|
.expect(DOWNLOAD_STATUS_POISON_MSG);
|
||||||
download_status.requested.subtract_range(&missing_range);
|
download_status.requested.subtract_range(&missing_range);
|
||||||
shared.cond.notify_all();
|
shared.cond.notify_all();
|
||||||
}
|
}
|
||||||
|
@ -189,7 +193,11 @@ impl AudioFileFetch {
|
||||||
// The iteration that follows spawns streamers fast, without awaiting them,
|
// The iteration that follows spawns streamers fast, without awaiting them,
|
||||||
// so holding the lock for the entire scope of this function should be faster
|
// so holding the lock for the entire scope of this function should be faster
|
||||||
// then locking and unlocking multiple times.
|
// then locking and unlocking multiple times.
|
||||||
let mut download_status = self.shared.download_status.lock();
|
let mut download_status = self
|
||||||
|
.shared
|
||||||
|
.download_status
|
||||||
|
.lock()
|
||||||
|
.expect(DOWNLOAD_STATUS_POISON_MSG);
|
||||||
|
|
||||||
ranges_to_request.subtract_range_set(&download_status.downloaded);
|
ranges_to_request.subtract_range_set(&download_status.downloaded);
|
||||||
ranges_to_request.subtract_range_set(&download_status.requested);
|
ranges_to_request.subtract_range_set(&download_status.requested);
|
||||||
|
@ -227,7 +235,11 @@ impl AudioFileFetch {
|
||||||
let mut missing_data = RangeSet::new();
|
let mut missing_data = RangeSet::new();
|
||||||
missing_data.add_range(&Range::new(0, self.shared.file_size));
|
missing_data.add_range(&Range::new(0, self.shared.file_size));
|
||||||
{
|
{
|
||||||
let download_status = self.shared.download_status.lock();
|
let download_status = self
|
||||||
|
.shared
|
||||||
|
.download_status
|
||||||
|
.lock()
|
||||||
|
.expect(DOWNLOAD_STATUS_POISON_MSG);
|
||||||
missing_data.subtract_range_set(&download_status.downloaded);
|
missing_data.subtract_range_set(&download_status.downloaded);
|
||||||
missing_data.subtract_range_set(&download_status.requested);
|
missing_data.subtract_range_set(&download_status.requested);
|
||||||
}
|
}
|
||||||
|
@ -349,7 +361,11 @@ impl AudioFileFetch {
|
||||||
let received_range = Range::new(data.offset, data.data.len());
|
let received_range = Range::new(data.offset, data.data.len());
|
||||||
|
|
||||||
let full = {
|
let full = {
|
||||||
let mut download_status = self.shared.download_status.lock();
|
let mut download_status = self
|
||||||
|
.shared
|
||||||
|
.download_status
|
||||||
|
.lock()
|
||||||
|
.expect(DOWNLOAD_STATUS_POISON_MSG);
|
||||||
download_status.downloaded.add_range(&received_range);
|
download_status.downloaded.add_range(&received_range);
|
||||||
self.shared.cond.notify_all();
|
self.shared.cond.notify_all();
|
||||||
|
|
||||||
|
@ -415,7 +431,10 @@ pub(super) async fn audio_file_fetch(
|
||||||
initial_request.offset + initial_request.length,
|
initial_request.offset + initial_request.length,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut download_status = shared.download_status.lock();
|
let mut download_status = shared
|
||||||
|
.download_status
|
||||||
|
.lock()
|
||||||
|
.expect(DOWNLOAD_STATUS_POISON_MSG);
|
||||||
download_status.requested.add_range(&requested_range);
|
download_status.requested.add_range(&requested_range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -466,7 +485,11 @@ pub(super) async fn audio_file_fetch(
|
||||||
|
|
||||||
if fetch.shared.is_download_streaming() && fetch.has_download_slots_available() {
|
if fetch.shared.is_download_streaming() && fetch.has_download_slots_available() {
|
||||||
let bytes_pending: usize = {
|
let bytes_pending: usize = {
|
||||||
let download_status = fetch.shared.download_status.lock();
|
let download_status = fetch
|
||||||
|
.shared
|
||||||
|
.download_status
|
||||||
|
.lock()
|
||||||
|
.expect(DOWNLOAD_STATUS_POISON_MSG);
|
||||||
|
|
||||||
download_status
|
download_status
|
||||||
.requested
|
.requested
|
||||||
|
|
|
@ -22,12 +22,12 @@ librespot-core = { version = "0.7.1", path = "../core", default-features = false
|
||||||
librespot-playback = { version = "0.7.1", path = "../playback", default-features = false }
|
librespot-playback = { version = "0.7.1", path = "../playback", default-features = false }
|
||||||
librespot-protocol = { version = "0.7.1", path = "../protocol", default-features = false }
|
librespot-protocol = { version = "0.7.1", path = "../protocol", default-features = false }
|
||||||
|
|
||||||
futures-util = "0.3"
|
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
protobuf = "3.7"
|
protobuf = "3.7"
|
||||||
rand = { version = "0.9", default-features = false, features = ["small_rng"] }
|
rand = { version = "0.9", default-features = false, features = ["small_rng"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tokio = { version = "1", features = ["macros", "parking_lot", "sync"] }
|
tokio = { version = "1", features = ["macros", "sync"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = { version = "0.1", default-features = false }
|
||||||
uuid = { version = "1.18", features = ["v4"] }
|
uuid = { version = "1.18", default-features = false, features = ["v4"] }
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::{
|
||||||
LoadContextOptions, LoadRequestOptions, PlayContext,
|
LoadContextOptions, LoadRequestOptions, PlayContext,
|
||||||
context_resolver::{ContextAction, ContextResolver, ResolveContext},
|
context_resolver::{ContextAction, ContextResolver, ResolveContext},
|
||||||
core::{
|
core::{
|
||||||
Error, Session, SpotifyId,
|
Error, Session, SpotifyUri,
|
||||||
authentication::Credentials,
|
authentication::Credentials,
|
||||||
dealer::{
|
dealer::{
|
||||||
manager::{BoxedStream, BoxedStreamResult, Reply, RequestReply},
|
manager::{BoxedStream, BoxedStreamResult, Reply, RequestReply},
|
||||||
|
@ -778,7 +778,7 @@ impl SpircTask {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
PlayerEvent::Unavailable { track_id, .. } => {
|
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()? {
|
if self.connect_state.current_track(|t| &t.uri) == &track_id.to_uri()? {
|
||||||
self.handle_next(None)?
|
self.handle_next(None)?
|
||||||
}
|
}
|
||||||
|
@ -1499,7 +1499,7 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark unavailable tracks so we can skip them later
|
// 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.connect_state.mark_unavailable(track_id)?;
|
||||||
self.handle_preload_next_track();
|
self.handle_preload_next_track();
|
||||||
|
|
||||||
|
@ -1704,7 +1704,7 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_uri = self.connect_state.current_track(|t| &t.uri);
|
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.player.load(id, start_playing, position_ms);
|
||||||
|
|
||||||
self.connect_state
|
self.connect_state
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
core::{Error, SpotifyId},
|
core::{Error, SpotifyId, SpotifyUri},
|
||||||
protocol::{
|
protocol::{
|
||||||
context::Context,
|
context::Context,
|
||||||
context_page::ContextPage,
|
context_page::ContextPage,
|
||||||
|
@ -449,8 +449,10 @@ impl ConnectState {
|
||||||
(Some(uri), _) if uri.contains(['?', '%']) => {
|
(Some(uri), _) if uri.contains(['?', '%']) => {
|
||||||
Err(StateError::InvalidTrackUri(Some(uri.clone())))?
|
Err(StateError::InvalidTrackUri(Some(uri.clone())))?
|
||||||
}
|
}
|
||||||
(Some(uri), _) if !uri.is_empty() => SpotifyId::from_uri(uri)?,
|
(Some(uri), _) if !uri.is_empty() => SpotifyUri::from_uri(uri)?,
|
||||||
(_, Some(gid)) if !gid.is_empty() => SpotifyId::from_raw(gid)?,
|
(_, Some(gid)) if !gid.is_empty() => SpotifyUri::Track {
|
||||||
|
id: SpotifyId::from_raw(gid)?,
|
||||||
|
},
|
||||||
_ => Err(StateError::InvalidTrackUri(None))?,
|
_ => Err(StateError::InvalidTrackUri(None))?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
core::{Error, SpotifyId},
|
core::{Error, SpotifyUri},
|
||||||
protocol::player::ProvidedTrack,
|
protocol::player::ProvidedTrack,
|
||||||
state::{
|
state::{
|
||||||
ConnectState, SPOTIFY_MAX_NEXT_TRACKS_SIZE, SPOTIFY_MAX_PREV_TRACKS_SIZE, StateError,
|
ConnectState, SPOTIFY_MAX_NEXT_TRACKS_SIZE, SPOTIFY_MAX_PREV_TRACKS_SIZE, StateError,
|
||||||
|
@ -352,14 +352,14 @@ impl<'ct> ConnectState {
|
||||||
Ok(())
|
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() {
|
let next = if self.repeat_track() {
|
||||||
self.current_track(|t| &t.uri)
|
self.current_track(|t| &t.uri)
|
||||||
} else {
|
} else {
|
||||||
&self.next_tracks().first()?.uri
|
&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 {
|
pub fn has_next_tracks(&self, min: Option<usize>) -> bool {
|
||||||
|
@ -381,7 +381,7 @@ impl<'ct> ConnectState {
|
||||||
prev
|
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()?;
|
let uri = id.to_uri()?;
|
||||||
|
|
||||||
debug!("marking {uri} as unavailable");
|
debug!("marking {uri} as unavailable");
|
||||||
|
|
|
@ -8,10 +8,10 @@
|
||||||
# The compiled binaries will be located in /tmp/librespot-build
|
# The compiled binaries will be located in /tmp/librespot-build
|
||||||
#
|
#
|
||||||
# If only one architecture is desired, cargo can be invoked directly with the appropriate options :
|
# If only one architecture is desired, cargo can be invoked directly with the appropriate options :
|
||||||
# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "alsa-backend with-libmdns"
|
# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots"
|
||||||
# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns"
|
# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots"
|
||||||
# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend with-libmdns"
|
# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots"
|
||||||
# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features "alsa-backend with-libmdns"
|
# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots"
|
||||||
|
|
||||||
FROM debian:bookworm
|
FROM debian:bookworm
|
||||||
|
|
||||||
|
|
|
@ -14,4 +14,4 @@ PI1_LIB_DIRS=(
|
||||||
export RUSTFLAGS="-C linker=$PI1_TOOLS_DIR/bin/arm-linux-gnueabihf-gcc ${PI1_LIB_DIRS[*]/#/-L}"
|
export RUSTFLAGS="-C linker=$PI1_TOOLS_DIR/bin/arm-linux-gnueabihf-gcc ${PI1_LIB_DIRS[*]/#/-L}"
|
||||||
export BINDGEN_EXTRA_CLANG_ARGS=--sysroot=$PI1_TOOLS_SYSROOT_DIR
|
export BINDGEN_EXTRA_CLANG_ARGS=--sysroot=$PI1_TOOLS_SYSROOT_DIR
|
||||||
|
|
||||||
cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns"
|
cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -eux
|
set -eux
|
||||||
|
|
||||||
cargo build --release --no-default-features --features "alsa-backend with-libmdns"
|
cargo build --release --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots"
|
||||||
cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features "alsa-backend with-libmdns"
|
cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots"
|
||||||
cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns"
|
cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots"
|
||||||
cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend with-libmdns"
|
cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots"
|
||||||
|
|
|
@ -51,16 +51,12 @@ data-encoding = "2.9"
|
||||||
flate2 = "1.1"
|
flate2 = "1.1"
|
||||||
form_urlencoded = "1.2"
|
form_urlencoded = "1.2"
|
||||||
futures-core = "0.3"
|
futures-core = "0.3"
|
||||||
futures-util = { version = "0.3", features = [
|
futures-util = { version = "0.3", default-features = false, features = [
|
||||||
"alloc",
|
"alloc",
|
||||||
"bilock",
|
"bilock",
|
||||||
"sink",
|
|
||||||
"unstable",
|
"unstable",
|
||||||
] }
|
] }
|
||||||
governor = { version = "0.10", default-features = false, features = [
|
governor = { version = "0.10", default-features = false, features = ["std"] }
|
||||||
"std",
|
|
||||||
"jitter",
|
|
||||||
] }
|
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
httparse = "1.10"
|
httparse = "1.10"
|
||||||
http = "1.3"
|
http = "1.3"
|
||||||
|
@ -84,14 +80,13 @@ num-bigint = "0.4"
|
||||||
num-derive = "0.4"
|
num-derive = "0.4"
|
||||||
num-integer = "0.1"
|
num-integer = "0.1"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
|
|
||||||
pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] }
|
pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] }
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
priority-queue = "2.5"
|
priority-queue = "2.5"
|
||||||
protobuf = "3.7"
|
protobuf = "3.7"
|
||||||
protobuf-json-mapping = "3.7"
|
protobuf-json-mapping = "3.7"
|
||||||
quick-xml = { version = "0.38", features = ["serialize"] }
|
quick-xml = { version = "0.38", features = ["serialize"] }
|
||||||
rand = "0.9"
|
rand = { version = "0.9", default-features = false, features = ["thread_rng"] }
|
||||||
rsa = "0.9"
|
rsa = "0.9"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
@ -104,23 +99,22 @@ tokio = { version = "1", features = [
|
||||||
"io-util",
|
"io-util",
|
||||||
"macros",
|
"macros",
|
||||||
"net",
|
"net",
|
||||||
"parking_lot",
|
|
||||||
"rt",
|
"rt",
|
||||||
"sync",
|
"sync",
|
||||||
"time",
|
"time",
|
||||||
] }
|
] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = { version = "0.1", default-features = false }
|
||||||
tokio-tungstenite = { version = "0.27", default-features = false }
|
tokio-tungstenite = { version = "0.27", default-features = false }
|
||||||
tokio-util = { version = "0.7", features = ["codec"] }
|
tokio-util = { version = "0.7", default-features = false }
|
||||||
url = "2"
|
url = "2"
|
||||||
uuid = { version = "1", default-features = false, features = ["v4"] }
|
uuid = { version = "1", default-features = false, features = ["v4"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
rand = "0.9"
|
rand = { version = "0.9", default-features = false, features = ["thread_rng"] }
|
||||||
rand_distr = "0.5"
|
rand_distr = "0.5"
|
||||||
vergen-gitcl = { version = "1.0", default-features = false, features = [
|
vergen-gitcl = { version = "1.0", default-features = false, features = [
|
||||||
"build",
|
"build",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1", features = ["macros", "parking_lot"] }
|
tokio = { version = "1", features = ["macros"] }
|
||||||
|
|
|
@ -4,16 +4,17 @@ use std::{
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::{self, Read, Write},
|
io::{self, Read, Write},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::{Arc, Mutex},
|
||||||
time::SystemTime,
|
time::SystemTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use priority_queue::PriorityQueue;
|
use priority_queue::PriorityQueue;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{Error, FileId, authentication::Credentials, error::ErrorKind};
|
use crate::{Error, FileId, authentication::Credentials, error::ErrorKind};
|
||||||
|
|
||||||
|
const CACHE_LIMITER_POISON_MSG: &str = "cache limiter mutex should not be poisoned";
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum CacheError {
|
pub enum CacheError {
|
||||||
#[error("audio cache location is not configured")]
|
#[error("audio cache location is not configured")]
|
||||||
|
@ -189,15 +190,24 @@ impl FsSizeLimiter {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add(&self, file: &Path, size: u64) {
|
fn add(&self, file: &Path, size: u64) {
|
||||||
self.limiter.lock().add(file, size, SystemTime::now())
|
self.limiter
|
||||||
|
.lock()
|
||||||
|
.expect(CACHE_LIMITER_POISON_MSG)
|
||||||
|
.add(file, size, SystemTime::now())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn touch(&self, file: &Path) -> bool {
|
fn touch(&self, file: &Path) -> bool {
|
||||||
self.limiter.lock().update(file, SystemTime::now())
|
self.limiter
|
||||||
|
.lock()
|
||||||
|
.expect(CACHE_LIMITER_POISON_MSG)
|
||||||
|
.update(file, SystemTime::now())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove(&self, file: &Path) -> bool {
|
fn remove(&self, file: &Path) -> bool {
|
||||||
self.limiter.lock().remove(file)
|
self.limiter
|
||||||
|
.lock()
|
||||||
|
.expect(CACHE_LIMITER_POISON_MSG)
|
||||||
|
.remove(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prune_internal<F: FnMut() -> Option<PathBuf>>(mut pop: F) -> Result<(), Error> {
|
fn prune_internal<F: FnMut() -> Option<PathBuf>>(mut pop: F) -> Result<(), Error> {
|
||||||
|
@ -232,7 +242,7 @@ impl FsSizeLimiter {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prune(&self) -> Result<(), Error> {
|
fn prune(&self) -> Result<(), Error> {
|
||||||
Self::prune_internal(|| self.limiter.lock().pop())
|
Self::prune_internal(|| self.limiter.lock().expect(CACHE_LIMITER_POISON_MSG).pop())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(path: &Path, limit: u64) -> Result<Self, Error> {
|
fn new(path: &Path, limit: u64) -> Result<Self, Error> {
|
||||||
|
|
|
@ -1,20 +1,23 @@
|
||||||
|
pub(crate) const COMPONENT_POISON_MSG: &str = "component mutex should not be poisoned";
|
||||||
|
|
||||||
macro_rules! component {
|
macro_rules! component {
|
||||||
($name:ident : $inner:ident { $($key:ident : $ty:ty = $value:expr,)* }) => {
|
($name:ident : $inner:ident { $($key:ident : $ty:ty = $value:expr,)* }) => {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct $name(::std::sync::Arc<($crate::session::SessionWeak, ::parking_lot::Mutex<$inner>)>);
|
pub struct $name(::std::sync::Arc<($crate::session::SessionWeak, ::std::sync::Mutex<$inner>)>);
|
||||||
impl $name {
|
impl $name {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn new(session: $crate::session::SessionWeak) -> $name {
|
pub(crate) fn new(session: $crate::session::SessionWeak) -> $name {
|
||||||
debug!(target:"librespot::component", "new {}", stringify!($name));
|
debug!(target:"librespot::component", "new {}", stringify!($name));
|
||||||
|
|
||||||
$name(::std::sync::Arc::new((session, ::parking_lot::Mutex::new($inner {
|
$name(::std::sync::Arc::new((session, ::std::sync::Mutex::new($inner {
|
||||||
$($key : $value,)*
|
$($key : $value,)*
|
||||||
}))))
|
}))))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn lock<F: FnOnce(&mut $inner) -> R, R>(&self, f: F) -> R {
|
fn lock<F: FnOnce(&mut $inner) -> R, R>(&self, f: F) -> R {
|
||||||
let mut inner = (self.0).1.lock();
|
let mut inner = (self.0).1.lock()
|
||||||
|
.expect($crate::component::COMPONENT_POISON_MSG);
|
||||||
f(&mut inner)
|
f(&mut inner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ use std::{
|
||||||
iter,
|
iter,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
sync::{
|
sync::{
|
||||||
Arc,
|
Arc, Mutex,
|
||||||
atomic::{self, AtomicBool},
|
atomic::{self, AtomicBool},
|
||||||
},
|
},
|
||||||
task::Poll,
|
task::Poll,
|
||||||
|
@ -15,7 +15,6 @@ use std::{
|
||||||
|
|
||||||
use futures_core::{Future, Stream};
|
use futures_core::{Future, Stream};
|
||||||
use futures_util::{SinkExt, StreamExt, future::join_all};
|
use futures_util::{SinkExt, StreamExt, future::join_all};
|
||||||
use parking_lot::Mutex;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
select,
|
select,
|
||||||
|
@ -57,6 +56,11 @@ const PING_TIMEOUT: Duration = Duration::from_secs(3);
|
||||||
|
|
||||||
const RECONNECT_INTERVAL: Duration = Duration::from_secs(10);
|
const RECONNECT_INTERVAL: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
const DEALER_REQUEST_HANDLERS_POISON_MSG: &str =
|
||||||
|
"dealer request handlers mutex should not be poisoned";
|
||||||
|
const DEALER_MESSAGE_HANDLERS_POISON_MSG: &str =
|
||||||
|
"dealer message handlers mutex should not be poisoned";
|
||||||
|
|
||||||
struct Response {
|
struct Response {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
}
|
}
|
||||||
|
@ -350,6 +354,7 @@ impl DealerShared {
|
||||||
if self
|
if self
|
||||||
.message_handlers
|
.message_handlers
|
||||||
.lock()
|
.lock()
|
||||||
|
.expect(DEALER_MESSAGE_HANDLERS_POISON_MSG)
|
||||||
.retain(split, &mut |tx| tx.send(msg.clone()).is_ok())
|
.retain(split, &mut |tx| tx.send(msg.clone()).is_ok())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -387,7 +392,10 @@ impl DealerShared {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let handler_map = self.request_handlers.lock();
|
let handler_map = self
|
||||||
|
.request_handlers
|
||||||
|
.lock()
|
||||||
|
.expect(DEALER_REQUEST_HANDLERS_POISON_MSG);
|
||||||
|
|
||||||
if let Some(handler) = handler_map.get(split) {
|
if let Some(handler) = handler_map.get(split) {
|
||||||
handler.handle_request(payload_request, responder);
|
handler.handle_request(payload_request, responder);
|
||||||
|
@ -425,21 +433,51 @@ impl Dealer {
|
||||||
where
|
where
|
||||||
H: RequestHandler,
|
H: RequestHandler,
|
||||||
{
|
{
|
||||||
add_handler(&mut self.shared.request_handlers.lock(), uri, handler)
|
add_handler(
|
||||||
|
&mut self
|
||||||
|
.shared
|
||||||
|
.request_handlers
|
||||||
|
.lock()
|
||||||
|
.expect(DEALER_REQUEST_HANDLERS_POISON_MSG),
|
||||||
|
uri,
|
||||||
|
handler,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_handler(&self, uri: &str) -> Option<Box<dyn RequestHandler>> {
|
pub fn remove_handler(&self, uri: &str) -> Option<Box<dyn RequestHandler>> {
|
||||||
remove_handler(&mut self.shared.request_handlers.lock(), uri)
|
remove_handler(
|
||||||
|
&mut self
|
||||||
|
.shared
|
||||||
|
.request_handlers
|
||||||
|
.lock()
|
||||||
|
.expect(DEALER_REQUEST_HANDLERS_POISON_MSG),
|
||||||
|
uri,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscribe(&self, uris: &[&str]) -> Result<Subscription, Error> {
|
pub fn subscribe(&self, uris: &[&str]) -> Result<Subscription, Error> {
|
||||||
subscribe(&mut self.shared.message_handlers.lock(), uris)
|
subscribe(
|
||||||
|
&mut self
|
||||||
|
.shared
|
||||||
|
.message_handlers
|
||||||
|
.lock()
|
||||||
|
.expect(DEALER_MESSAGE_HANDLERS_POISON_MSG),
|
||||||
|
uris,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handles(&self, uri: &str) -> bool {
|
pub fn handles(&self, uri: &str) -> bool {
|
||||||
handles(
|
handles(
|
||||||
&self.shared.request_handlers.lock(),
|
&self
|
||||||
&self.shared.message_handlers.lock(),
|
.shared
|
||||||
|
.request_handlers
|
||||||
|
.lock()
|
||||||
|
.expect(DEALER_REQUEST_HANDLERS_POISON_MSG),
|
||||||
|
&self
|
||||||
|
.shared
|
||||||
|
.message_handlers
|
||||||
|
.lock()
|
||||||
|
.expect(DEALER_MESSAGE_HANDLERS_POISON_MSG),
|
||||||
uri,
|
uri,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
|
||||||
sync::OnceLock,
|
sync::OnceLock,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
@ -7,7 +6,8 @@ use std::{
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_util::{FutureExt, future::IntoStream};
|
use futures_util::{FutureExt, future::IntoStream};
|
||||||
use governor::{
|
use governor::{
|
||||||
Quota, RateLimiter, clock::MonotonicClock, middleware::NoOpMiddleware, state::InMemoryState,
|
Quota, RateLimiter, clock::MonotonicClock, middleware::NoOpMiddleware,
|
||||||
|
state::keyed::DefaultKeyedStateStore,
|
||||||
};
|
};
|
||||||
use http::{Uri, header::HeaderValue};
|
use http::{Uri, header::HeaderValue};
|
||||||
use http_body_util::{BodyExt, Full};
|
use http_body_util::{BodyExt, Full};
|
||||||
|
@ -18,7 +18,6 @@ use hyper_util::{
|
||||||
rt::TokioExecutor,
|
rt::TokioExecutor,
|
||||||
};
|
};
|
||||||
use nonzero_ext::nonzero;
|
use nonzero_ext::nonzero;
|
||||||
use parking_lot::Mutex;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
@ -100,10 +99,8 @@ pub struct HttpClient {
|
||||||
proxy_url: Option<Url>,
|
proxy_url: Option<Url>,
|
||||||
hyper_client: OnceLock<HyperClient>,
|
hyper_client: OnceLock<HyperClient>,
|
||||||
|
|
||||||
// while the DashMap variant is more performant, our level of concurrency
|
|
||||||
// is pretty low so we can save pulling in that extra dependency
|
|
||||||
rate_limiter:
|
rate_limiter:
|
||||||
RateLimiter<String, Mutex<HashMap<String, InMemoryState>>, MonotonicClock, NoOpMiddleware>,
|
RateLimiter<String, DefaultKeyedStateStore<String>, MonotonicClock, NoOpMiddleware>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HttpClient {
|
impl HttpClient {
|
||||||
|
|
|
@ -32,6 +32,7 @@ mod socket;
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub mod spclient;
|
pub mod spclient;
|
||||||
pub mod spotify_id;
|
pub mod spotify_id;
|
||||||
|
pub mod spotify_uri;
|
||||||
pub mod token;
|
pub mod token;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
@ -42,3 +43,4 @@ pub use error::Error;
|
||||||
pub use file_id::FileId;
|
pub use file_id::FileId;
|
||||||
pub use session::Session;
|
pub use session::Session;
|
||||||
pub use spotify_id::SpotifyId;
|
pub use spotify_id::SpotifyId;
|
||||||
|
pub use spotify_uri::SpotifyUri;
|
||||||
|
|
|
@ -4,8 +4,7 @@ use std::{
|
||||||
io,
|
io,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
process::exit,
|
process::exit,
|
||||||
sync::OnceLock,
|
sync::{Arc, OnceLock, RwLock, Weak},
|
||||||
sync::{Arc, Weak},
|
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
@ -34,7 +33,6 @@ use futures_core::TryStream;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use librespot_protocol::authentication::AuthenticationType;
|
use librespot_protocol::authentication::AuthenticationType;
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use parking_lot::RwLock;
|
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
use quick_xml::events::Event;
|
use quick_xml::events::Event;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
@ -45,6 +43,8 @@ use tokio::{
|
||||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
const SESSION_DATA_POISON_MSG: &str = "session data rwlock should not be poisoned";
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum SessionError {
|
pub enum SessionError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
@ -338,7 +338,11 @@ impl Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn time_delta(&self) -> i64 {
|
pub fn time_delta(&self) -> i64 {
|
||||||
self.0.data.read().time_delta
|
self.0
|
||||||
|
.data
|
||||||
|
.read()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.time_delta
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn<T>(&self, task: T)
|
pub fn spawn<T>(&self, task: T)
|
||||||
|
@ -388,15 +392,32 @@ impl Session {
|
||||||
// you need more fields at once, in which case this can spare multiple `read`
|
// you need more fields at once, in which case this can spare multiple `read`
|
||||||
// locks.
|
// locks.
|
||||||
pub fn user_data(&self) -> UserData {
|
pub fn user_data(&self) -> UserData {
|
||||||
self.0.data.read().user_data.clone()
|
self.0
|
||||||
|
.data
|
||||||
|
.read()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.user_data
|
||||||
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn session_id(&self) -> String {
|
pub fn session_id(&self) -> String {
|
||||||
self.0.data.read().session_id.clone()
|
self.0
|
||||||
|
.data
|
||||||
|
.read()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.session_id
|
||||||
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_session_id(&self, session_id: &str) {
|
pub fn set_session_id(&self, session_id: &str) {
|
||||||
session_id.clone_into(&mut self.0.data.write().session_id);
|
session_id.clone_into(
|
||||||
|
&mut self
|
||||||
|
.0
|
||||||
|
.data
|
||||||
|
.write()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.session_id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn device_id(&self) -> &str {
|
pub fn device_id(&self) -> &str {
|
||||||
|
@ -404,63 +425,155 @@ impl Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn client_id(&self) -> String {
|
pub fn client_id(&self) -> String {
|
||||||
self.0.data.read().client_id.clone()
|
self.0
|
||||||
|
.data
|
||||||
|
.read()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.client_id
|
||||||
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_client_id(&self, client_id: &str) {
|
pub fn set_client_id(&self, client_id: &str) {
|
||||||
client_id.clone_into(&mut self.0.data.write().client_id);
|
client_id.clone_into(
|
||||||
|
&mut self
|
||||||
|
.0
|
||||||
|
.data
|
||||||
|
.write()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.client_id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn client_name(&self) -> String {
|
pub fn client_name(&self) -> String {
|
||||||
self.0.data.read().client_name.clone()
|
self.0
|
||||||
|
.data
|
||||||
|
.read()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.client_name
|
||||||
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_client_name(&self, client_name: &str) {
|
pub fn set_client_name(&self, client_name: &str) {
|
||||||
client_name.clone_into(&mut self.0.data.write().client_name);
|
client_name.clone_into(
|
||||||
|
&mut self
|
||||||
|
.0
|
||||||
|
.data
|
||||||
|
.write()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.client_name,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn client_brand_name(&self) -> String {
|
pub fn client_brand_name(&self) -> String {
|
||||||
self.0.data.read().client_brand_name.clone()
|
self.0
|
||||||
|
.data
|
||||||
|
.read()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.client_brand_name
|
||||||
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_client_brand_name(&self, client_brand_name: &str) {
|
pub fn set_client_brand_name(&self, client_brand_name: &str) {
|
||||||
client_brand_name.clone_into(&mut self.0.data.write().client_brand_name);
|
client_brand_name.clone_into(
|
||||||
|
&mut self
|
||||||
|
.0
|
||||||
|
.data
|
||||||
|
.write()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.client_brand_name,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn client_model_name(&self) -> String {
|
pub fn client_model_name(&self) -> String {
|
||||||
self.0.data.read().client_model_name.clone()
|
self.0
|
||||||
|
.data
|
||||||
|
.read()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.client_model_name
|
||||||
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_client_model_name(&self, client_model_name: &str) {
|
pub fn set_client_model_name(&self, client_model_name: &str) {
|
||||||
client_model_name.clone_into(&mut self.0.data.write().client_model_name);
|
client_model_name.clone_into(
|
||||||
|
&mut self
|
||||||
|
.0
|
||||||
|
.data
|
||||||
|
.write()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.client_model_name,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn connection_id(&self) -> String {
|
pub fn connection_id(&self) -> String {
|
||||||
self.0.data.read().connection_id.clone()
|
self.0
|
||||||
|
.data
|
||||||
|
.read()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.connection_id
|
||||||
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_connection_id(&self, connection_id: &str) {
|
pub fn set_connection_id(&self, connection_id: &str) {
|
||||||
connection_id.clone_into(&mut self.0.data.write().connection_id);
|
connection_id.clone_into(
|
||||||
|
&mut self
|
||||||
|
.0
|
||||||
|
.data
|
||||||
|
.write()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.connection_id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn username(&self) -> String {
|
pub fn username(&self) -> String {
|
||||||
self.0.data.read().user_data.canonical_username.clone()
|
self.0
|
||||||
|
.data
|
||||||
|
.read()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.user_data
|
||||||
|
.canonical_username
|
||||||
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_username(&self, username: &str) {
|
pub fn set_username(&self, username: &str) {
|
||||||
username.clone_into(&mut self.0.data.write().user_data.canonical_username);
|
username.clone_into(
|
||||||
|
&mut self
|
||||||
|
.0
|
||||||
|
.data
|
||||||
|
.write()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.user_data
|
||||||
|
.canonical_username,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn auth_data(&self) -> Vec<u8> {
|
pub fn auth_data(&self) -> Vec<u8> {
|
||||||
self.0.data.read().auth_data.clone()
|
self.0
|
||||||
|
.data
|
||||||
|
.read()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.auth_data
|
||||||
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_auth_data(&self, auth_data: &[u8]) {
|
pub fn set_auth_data(&self, auth_data: &[u8]) {
|
||||||
auth_data.clone_into(&mut self.0.data.write().auth_data);
|
auth_data.clone_into(
|
||||||
|
&mut self
|
||||||
|
.0
|
||||||
|
.data
|
||||||
|
.write()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.auth_data,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn country(&self) -> String {
|
pub fn country(&self) -> String {
|
||||||
self.0.data.read().user_data.country.clone()
|
self.0
|
||||||
|
.data
|
||||||
|
.read()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.user_data
|
||||||
|
.country
|
||||||
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn filter_explicit_content(&self) -> bool {
|
pub fn filter_explicit_content(&self) -> bool {
|
||||||
|
@ -489,6 +602,7 @@ impl Session {
|
||||||
self.0
|
self.0
|
||||||
.data
|
.data
|
||||||
.write()
|
.write()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
.user_data
|
.user_data
|
||||||
.attributes
|
.attributes
|
||||||
.insert(key.to_owned(), value.to_owned())
|
.insert(key.to_owned(), value.to_owned())
|
||||||
|
@ -497,11 +611,24 @@ impl Session {
|
||||||
pub fn set_user_attributes(&self, attributes: UserAttributes) {
|
pub fn set_user_attributes(&self, attributes: UserAttributes) {
|
||||||
Self::check_catalogue(&attributes);
|
Self::check_catalogue(&attributes);
|
||||||
|
|
||||||
self.0.data.write().user_data.attributes.extend(attributes)
|
self.0
|
||||||
|
.data
|
||||||
|
.write()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.user_data
|
||||||
|
.attributes
|
||||||
|
.extend(attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_user_attribute(&self, key: &str) -> Option<String> {
|
pub fn get_user_attribute(&self, key: &str) -> Option<String> {
|
||||||
self.0.data.read().user_data.attributes.get(key).cloned()
|
self.0
|
||||||
|
.data
|
||||||
|
.read()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.user_data
|
||||||
|
.attributes
|
||||||
|
.get(key)
|
||||||
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn weak(&self) -> SessionWeak {
|
fn weak(&self) -> SessionWeak {
|
||||||
|
@ -510,13 +637,13 @@ impl Session {
|
||||||
|
|
||||||
pub fn shutdown(&self) {
|
pub fn shutdown(&self) {
|
||||||
debug!("Shutdown: Invalidating session");
|
debug!("Shutdown: Invalidating session");
|
||||||
self.0.data.write().invalid = true;
|
self.0.data.write().expect(SESSION_DATA_POISON_MSG).invalid = true;
|
||||||
self.mercury().shutdown();
|
self.mercury().shutdown();
|
||||||
self.channel().shutdown();
|
self.channel().shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_invalid(&self) -> bool {
|
pub fn is_invalid(&self) -> bool {
|
||||||
self.0.data.read().invalid
|
self.0.data.read().expect(SESSION_DATA_POISON_MSG).invalid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -643,7 +770,7 @@ where
|
||||||
.unwrap_or(Duration::ZERO)
|
.unwrap_or(Duration::ZERO)
|
||||||
.as_secs() as i64;
|
.as_secs() as i64;
|
||||||
{
|
{
|
||||||
let mut data = session.0.data.write();
|
let mut data = session.0.data.write().expect(SESSION_DATA_POISON_MSG);
|
||||||
data.time_delta = server_timestamp.saturating_sub(timestamp);
|
data.time_delta = server_timestamp.saturating_sub(timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -668,7 +795,13 @@ where
|
||||||
Some(CountryCode) => {
|
Some(CountryCode) => {
|
||||||
let country = String::from_utf8(data.as_ref().to_owned())?;
|
let country = String::from_utf8(data.as_ref().to_owned())?;
|
||||||
info!("Country: {country:?}");
|
info!("Country: {country:?}");
|
||||||
session.0.data.write().user_data.country = country;
|
session
|
||||||
|
.0
|
||||||
|
.data
|
||||||
|
.write()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.user_data
|
||||||
|
.country = country;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Some(StreamChunkRes) | Some(ChannelError) => session.channel().dispatch(cmd, data),
|
Some(StreamChunkRes) | Some(ChannelError) => session.channel().dispatch(cmd, data),
|
||||||
|
@ -713,7 +846,13 @@ where
|
||||||
trace!("Received product info: {user_attributes:#?}");
|
trace!("Received product info: {user_attributes:#?}");
|
||||||
Session::check_catalogue(&user_attributes);
|
Session::check_catalogue(&user_attributes);
|
||||||
|
|
||||||
session.0.data.write().user_data.attributes = user_attributes;
|
session
|
||||||
|
.0
|
||||||
|
.data
|
||||||
|
.write()
|
||||||
|
.expect(SESSION_DATA_POISON_MSG)
|
||||||
|
.user_data
|
||||||
|
.attributes = user_attributes;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Some(SecretBlock)
|
Some(SecretBlock)
|
||||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
||||||
|
|
||||||
use crate::config::{OS, os_version};
|
use crate::config::{OS, os_version};
|
||||||
use crate::{
|
use crate::{
|
||||||
Error, FileId, SpotifyId,
|
Error, FileId, SpotifyId, SpotifyUri,
|
||||||
apresolve::SocketAddress,
|
apresolve::SocketAddress,
|
||||||
config::SessionConfig,
|
config::SessionConfig,
|
||||||
error::ErrorKind,
|
error::ErrorKind,
|
||||||
|
@ -676,10 +676,10 @@ impl SpClient {
|
||||||
.await
|
.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!(
|
let endpoint = format!(
|
||||||
"/inspiredby-mix/v2/seed_to_playlist/{}?response-format=json",
|
"/inspiredby-mix/v2/seed_to_playlist/{}?response-format=json",
|
||||||
track_id.to_uri()?
|
track_uri.to_uri()?
|
||||||
);
|
);
|
||||||
|
|
||||||
self.request_as_json(&Method::GET, &endpoint, None, None)
|
self.request_as_json(&Method::GET, &endpoint, None, None)
|
||||||
|
|
|
@ -1,70 +1,23 @@
|
||||||
use std::{fmt, ops::Deref};
|
use std::fmt;
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::Error;
|
use crate::{Error, SpotifyUri};
|
||||||
|
|
||||||
use librespot_protocol as protocol;
|
|
||||||
|
|
||||||
// re-export FileId for historic reasons, when it was part of this mod
|
// re-export FileId for historic reasons, when it was part of this mod
|
||||||
pub use crate::FileId;
|
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)]
|
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct SpotifyId {
|
pub struct SpotifyId {
|
||||||
pub id: u128,
|
pub id: u128,
|
||||||
pub item_type: SpotifyItemType,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum SpotifyIdError {
|
pub enum SpotifyIdError {
|
||||||
#[error("ID cannot be parsed")]
|
#[error("ID cannot be parsed")]
|
||||||
InvalidId,
|
InvalidId,
|
||||||
#[error("not a valid Spotify URI")]
|
#[error("not a valid Spotify ID")]
|
||||||
InvalidFormat,
|
InvalidFormat,
|
||||||
#[error("URI does not belong to Spotify")]
|
|
||||||
InvalidRoot,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SpotifyIdError> for Error {
|
impl From<SpotifyIdError> for Error {
|
||||||
|
@ -74,7 +27,6 @@ impl From<SpotifyIdError> for Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type SpotifyIdResult = Result<SpotifyId, Error>;
|
pub type SpotifyIdResult = Result<SpotifyId, Error>;
|
||||||
pub type NamedSpotifyIdResult = Result<NamedSpotifyId, Error>;
|
|
||||||
|
|
||||||
const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef";
|
const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef";
|
||||||
|
@ -84,14 +36,6 @@ impl SpotifyId {
|
||||||
const SIZE_BASE16: usize = 32;
|
const SIZE_BASE16: usize = 32;
|
||||||
const SIZE_BASE62: usize = 22;
|
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`.
|
/// Parses a base16 (hex) encoded [Spotify ID] into a `SpotifyId`.
|
||||||
///
|
///
|
||||||
/// `src` is expected to be 32 bytes long and encoded using valid characters.
|
/// `src` is expected to be 32 bytes long and encoded using valid characters.
|
||||||
|
@ -114,10 +58,7 @@ impl SpotifyId {
|
||||||
dst += p;
|
dst += p;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self { id: dst })
|
||||||
id: dst,
|
|
||||||
item_type: SpotifyItemType::Unknown,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a base62 encoded [Spotify ID] into a `u128`.
|
/// 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
|
/// [Spotify ID]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids
|
||||||
pub fn from_base62(src: &str) -> SpotifyIdResult {
|
pub fn from_base62(src: &str) -> SpotifyIdResult {
|
||||||
if src.len() != 22 {
|
if src.len() != Self::SIZE_BASE62 {
|
||||||
return Err(SpotifyIdError::InvalidId.into());
|
return Err(SpotifyIdError::InvalidId.into());
|
||||||
}
|
}
|
||||||
let mut dst: u128 = 0;
|
let mut dst: u128 = 0;
|
||||||
|
@ -143,10 +84,7 @@ impl SpotifyId {
|
||||||
dst = dst.checked_add(p).ok_or(SpotifyIdError::InvalidId)?;
|
dst = dst.checked_add(p).ok_or(SpotifyIdError::InvalidId)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self { id: dst })
|
||||||
id: dst,
|
|
||||||
item_type: SpotifyItemType::Unknown,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a `u128` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order.
|
/// 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() {
|
match src.try_into() {
|
||||||
Ok(dst) => Ok(Self {
|
Ok(dst) => Ok(Self {
|
||||||
id: u128::from_be_bytes(dst),
|
id: u128::from_be_bytes(dst),
|
||||||
item_type: SpotifyItemType::Unknown,
|
|
||||||
}),
|
}),
|
||||||
Err(_) => Err(SpotifyIdError::InvalidId.into()),
|
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)
|
/// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE16` (32)
|
||||||
/// character long `String`.
|
/// character long `String`.
|
||||||
#[allow(clippy::wrong_self_convention)]
|
#[allow(clippy::wrong_self_convention)]
|
||||||
|
@ -274,124 +158,19 @@ impl SpotifyId {
|
||||||
pub fn to_raw(&self) -> [u8; Self::SIZE] {
|
pub fn to_raw(&self) -> [u8; Self::SIZE] {
|
||||||
self.id.to_be_bytes()
|
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 {
|
impl fmt::Debug for SpotifyId {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.debug_tuple("SpotifyId")
|
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()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for SpotifyId {
|
impl fmt::Display for SpotifyId {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(&self.to_uri().unwrap_or_else(|_| "invalid uri".into()))
|
f.write_str(&self.to_base62().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()),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
type Error = crate::Error;
|
||||||
fn try_from(album: &protocol::metadata::Album) -> Result<Self, Self::Error> {
|
fn try_from(value: &SpotifyUri) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
match value {
|
||||||
item_type: SpotifyItemType::Album,
|
SpotifyUri::Album { id }
|
||||||
..Self::from_raw(album.gid())?
|
| SpotifyUri::Artist { id }
|
||||||
})
|
| SpotifyUri::Episode { id }
|
||||||
}
|
| SpotifyUri::Playlist { id, .. }
|
||||||
}
|
| SpotifyUri::Show { id }
|
||||||
|
| SpotifyUri::Track { id } => Ok(*id),
|
||||||
impl TryFrom<&protocol::metadata::Artist> for SpotifyId {
|
SpotifyUri::Local { .. } | SpotifyUri::Unknown { .. } => {
|
||||||
type Error = crate::Error;
|
Err(SpotifyIdError::InvalidFormat.into())
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -541,8 +236,6 @@ mod tests {
|
||||||
|
|
||||||
struct ConversionCase {
|
struct ConversionCase {
|
||||||
id: u128,
|
id: u128,
|
||||||
kind: SpotifyItemType,
|
|
||||||
uri: &'static str,
|
|
||||||
base16: &'static str,
|
base16: &'static str,
|
||||||
base62: &'static str,
|
base62: &'static str,
|
||||||
raw: &'static [u8],
|
raw: &'static [u8],
|
||||||
|
@ -551,8 +244,6 @@ mod tests {
|
||||||
static CONV_VALID: [ConversionCase; 5] = [
|
static CONV_VALID: [ConversionCase; 5] = [
|
||||||
ConversionCase {
|
ConversionCase {
|
||||||
id: 238762092608182713602505436543891614649,
|
id: 238762092608182713602505436543891614649,
|
||||||
kind: SpotifyItemType::Track,
|
|
||||||
uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH",
|
|
||||||
base16: "b39fe8081e1f4c54be38e8d6f9f12bb9",
|
base16: "b39fe8081e1f4c54be38e8d6f9f12bb9",
|
||||||
base62: "5sWHDYs0csV6RS48xBl0tH",
|
base62: "5sWHDYs0csV6RS48xBl0tH",
|
||||||
raw: &[
|
raw: &[
|
||||||
|
@ -561,8 +252,6 @@ mod tests {
|
||||||
},
|
},
|
||||||
ConversionCase {
|
ConversionCase {
|
||||||
id: 204841891221366092811751085145916697048,
|
id: 204841891221366092811751085145916697048,
|
||||||
kind: SpotifyItemType::Track,
|
|
||||||
uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4",
|
|
||||||
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
|
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
|
||||||
base62: "4GNcXTGWmnZ3ySrqvol3o4",
|
base62: "4GNcXTGWmnZ3ySrqvol3o4",
|
||||||
raw: &[
|
raw: &[
|
||||||
|
@ -571,8 +260,6 @@ mod tests {
|
||||||
},
|
},
|
||||||
ConversionCase {
|
ConversionCase {
|
||||||
id: 204841891221366092811751085145916697048,
|
id: 204841891221366092811751085145916697048,
|
||||||
kind: SpotifyItemType::Episode,
|
|
||||||
uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4",
|
|
||||||
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
|
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
|
||||||
base62: "4GNcXTGWmnZ3ySrqvol3o4",
|
base62: "4GNcXTGWmnZ3ySrqvol3o4",
|
||||||
raw: &[
|
raw: &[
|
||||||
|
@ -581,8 +268,6 @@ mod tests {
|
||||||
},
|
},
|
||||||
ConversionCase {
|
ConversionCase {
|
||||||
id: 204841891221366092811751085145916697048,
|
id: 204841891221366092811751085145916697048,
|
||||||
kind: SpotifyItemType::Show,
|
|
||||||
uri: "spotify:show:4GNcXTGWmnZ3ySrqvol3o4",
|
|
||||||
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
|
base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
|
||||||
base62: "4GNcXTGWmnZ3ySrqvol3o4",
|
base62: "4GNcXTGWmnZ3ySrqvol3o4",
|
||||||
raw: &[
|
raw: &[
|
||||||
|
@ -591,8 +276,6 @@ mod tests {
|
||||||
},
|
},
|
||||||
ConversionCase {
|
ConversionCase {
|
||||||
id: 0,
|
id: 0,
|
||||||
kind: SpotifyItemType::Local,
|
|
||||||
uri: "spotify:local:0000000000000000000000",
|
|
||||||
base16: "00000000000000000000000000000000",
|
base16: "00000000000000000000000000000000",
|
||||||
base62: "0000000000000000000000",
|
base62: "0000000000000000000000",
|
||||||
raw: &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
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] = [
|
static CONV_INVALID: [ConversionCase; 5] = [
|
||||||
ConversionCase {
|
ConversionCase {
|
||||||
id: 0,
|
id: 0,
|
||||||
kind: SpotifyItemType::Unknown,
|
|
||||||
// Invalid ID in the URI.
|
|
||||||
uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH",
|
|
||||||
base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9",
|
base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9",
|
||||||
base62: "!!!!!Ys0csV6RS48xBl0tH",
|
base62: "!!!!!Ys0csV6RS48xBl0tH",
|
||||||
raw: &[
|
raw: &[
|
||||||
|
@ -614,9 +294,6 @@ mod tests {
|
||||||
},
|
},
|
||||||
ConversionCase {
|
ConversionCase {
|
||||||
id: 0,
|
id: 0,
|
||||||
kind: SpotifyItemType::Unknown,
|
|
||||||
// Missing colon between ID and type.
|
|
||||||
uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH",
|
|
||||||
base16: "--------------------",
|
base16: "--------------------",
|
||||||
base62: "....................",
|
base62: "....................",
|
||||||
raw: &[
|
raw: &[
|
||||||
|
@ -626,9 +303,6 @@ mod tests {
|
||||||
},
|
},
|
||||||
ConversionCase {
|
ConversionCase {
|
||||||
id: 0,
|
id: 0,
|
||||||
kind: SpotifyItemType::Unknown,
|
|
||||||
// Uri too short
|
|
||||||
uri: "spotify:azb:aRS48xBl0tH",
|
|
||||||
// too long, should return error but not panic overflow
|
// too long, should return error but not panic overflow
|
||||||
base16: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
base16: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
// too long, should return error but not panic overflow
|
// too long, should return error but not panic overflow
|
||||||
|
@ -640,9 +314,6 @@ mod tests {
|
||||||
},
|
},
|
||||||
ConversionCase {
|
ConversionCase {
|
||||||
id: 0,
|
id: 0,
|
||||||
kind: SpotifyItemType::Unknown,
|
|
||||||
// Uri too short
|
|
||||||
uri: "spotify:azb:aRS48xBl0tH",
|
|
||||||
base16: "--------------------",
|
base16: "--------------------",
|
||||||
// too short to encode a 128 bits int
|
// too short to encode a 128 bits int
|
||||||
base62: "aa",
|
base62: "aa",
|
||||||
|
@ -653,8 +324,6 @@ mod tests {
|
||||||
},
|
},
|
||||||
ConversionCase {
|
ConversionCase {
|
||||||
id: 0,
|
id: 0,
|
||||||
kind: SpotifyItemType::Unknown,
|
|
||||||
uri: "cleary invalid uri",
|
|
||||||
base16: "--------------------",
|
base16: "--------------------",
|
||||||
// too high of a value, this would need a 132 bits int
|
// too high of a value, this would need a 132 bits int
|
||||||
base62: "ZZZZZZZZZZZZZZZZZZZZZZ",
|
base62: "ZZZZZZZZZZZZZZZZZZZZZZ",
|
||||||
|
@ -679,10 +348,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn to_base62() {
|
fn to_base62() {
|
||||||
for c in &CONV_VALID {
|
for c in &CONV_VALID {
|
||||||
let id = SpotifyId {
|
let id = SpotifyId { id: c.id };
|
||||||
id: c.id,
|
|
||||||
item_type: c.kind,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(id.to_base62().unwrap(), c.base62);
|
assert_eq!(id.to_base62().unwrap(), c.base62);
|
||||||
}
|
}
|
||||||
|
@ -702,60 +368,12 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn to_base16() {
|
fn to_base16() {
|
||||||
for c in &CONV_VALID {
|
for c in &CONV_VALID {
|
||||||
let id = SpotifyId {
|
let id = SpotifyId { id: c.id };
|
||||||
id: c.id,
|
|
||||||
item_type: c.kind,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(id.to_base16().unwrap(), c.base16);
|
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]
|
#[test]
|
||||||
fn from_raw() {
|
fn from_raw() {
|
||||||
for c in &CONV_VALID {
|
for c in &CONV_VALID {
|
||||||
|
|
583
core/src/spotify_uri.rs
Normal file
583
core/src/spotify_uri.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,7 +32,7 @@ ctr = "0.9"
|
||||||
dns-sd = { version = "0.1", optional = true }
|
dns-sd = { version = "0.1", optional = true }
|
||||||
form_urlencoded = "1.2"
|
form_urlencoded = "1.2"
|
||||||
futures-core = "0.3"
|
futures-core = "0.3"
|
||||||
futures-util = "0.3"
|
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
http-body-util = "0.1"
|
http-body-util = "0.1"
|
||||||
hyper = { version = "1.6", features = ["http1"] }
|
hyper = { version = "1.6", features = ["http1"] }
|
||||||
|
@ -41,9 +41,9 @@ hyper-util = { version = "0.1", features = [
|
||||||
"server-graceful",
|
"server-graceful",
|
||||||
"service",
|
"service",
|
||||||
] }
|
] }
|
||||||
libmdns = { version = "0.9", optional = true }
|
libmdns = { version = "0.10", optional = true }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
rand = "0.9"
|
rand = { version = "0.9", default-features = false, features = ["thread_rng"] }
|
||||||
serde = { version = "1", default-features = false, features = [
|
serde = { version = "1", default-features = false, features = [
|
||||||
"derive",
|
"derive",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
|
@ -51,7 +51,7 @@ serde_repr = "0.1"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
sha1 = "0.10"
|
sha1 = "0.10"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tokio = { version = "1", features = ["parking_lot", "sync", "rt"] }
|
tokio = { version = "1", features = ["sync", "rt"] }
|
||||||
zbus = { version = "5", default-features = false, features = [
|
zbus = { version = "5", default-features = false, features = [
|
||||||
"tokio",
|
"tokio",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
|
@ -59,4 +59,4 @@ zbus = { version = "5", default-features = false, features = [
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
tokio = { version = "1", features = ["macros", "parking_lot", "rt"] }
|
tokio = { version = "1", features = ["macros", "rt"] }
|
||||||
|
|
|
@ -406,12 +406,7 @@ fn launch_libmdns(
|
||||||
}
|
}
|
||||||
.map_err(|e| DiscoveryError::DnsSdError(Box::new(e)))?;
|
.map_err(|e| DiscoveryError::DnsSdError(Box::new(e)))?;
|
||||||
|
|
||||||
let svc = responder.register(
|
let svc = responder.register(&DNS_SD_SERVICE_NAME, &name, port, &TXT_RECORD);
|
||||||
DNS_SD_SERVICE_NAME.to_owned(),
|
|
||||||
name.into_owned(),
|
|
||||||
port,
|
|
||||||
&TXT_RECORD,
|
|
||||||
);
|
|
||||||
|
|
||||||
let _ = shutdown_rx.blocking_recv();
|
let _ = shutdown_rx.blocking_recv();
|
||||||
|
|
||||||
|
|
36
examples/README.md
Normal file
36
examples/README.md
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
This folder contains examples of how to use the `librespot` library for various purposes.
|
||||||
|
|
||||||
|
## How to run the examples
|
||||||
|
|
||||||
|
In general, to invoke an example, clone down the repo and use `cargo` as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run --example [filename]
|
||||||
|
```
|
||||||
|
|
||||||
|
in which `filename` is the file name of the example, for instance `get_token` or `play`.
|
||||||
|
|
||||||
|
### Acquiring an access token
|
||||||
|
|
||||||
|
Most examples require an access token as the first positional argument. **Note that an access token
|
||||||
|
gained by the client credentials flow will not work**. `librespot-oauth` provides a utility to
|
||||||
|
acquire an access token using an OAuth flow, which will be able to run the examples. To invoke this,
|
||||||
|
run:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run --package librespot-oauth --example oauth_sync
|
||||||
|
```
|
||||||
|
|
||||||
|
A browser window will open and prompt you to authorize with Spotify. Once done, take the
|
||||||
|
`access_token` property from the dumped object response and proceed to use it in examples. You may
|
||||||
|
find it convenient to save it in a shell variable like `$ACCESS_TOKEN`.
|
||||||
|
|
||||||
|
Once you have obtained the token you can proceed to run the example. Check each individual
|
||||||
|
file to see what arguments are expected. As a demonstration, here is how to invoke the `play`
|
||||||
|
example to play a song -- the second argument is the URI of the track to play.
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run --example play "$ACCESS_TOKEN" 2WUy2Uywcj5cP0IXQagO3z
|
||||||
|
```
|
|
@ -2,10 +2,8 @@ use std::{env, process::exit};
|
||||||
|
|
||||||
use librespot::{
|
use librespot::{
|
||||||
core::{
|
core::{
|
||||||
authentication::Credentials,
|
SpotifyUri, authentication::Credentials, config::SessionConfig, session::Session,
|
||||||
config::SessionConfig,
|
spotify_id::SpotifyId,
|
||||||
session::Session,
|
|
||||||
spotify_id::{SpotifyId, SpotifyItemType},
|
|
||||||
},
|
},
|
||||||
playback::{
|
playback::{
|
||||||
audio_backend,
|
audio_backend,
|
||||||
|
@ -28,8 +26,9 @@ async fn main() {
|
||||||
}
|
}
|
||||||
let credentials = Credentials::with_access_token(&args[1]);
|
let credentials = Credentials::with_access_token(&args[1]);
|
||||||
|
|
||||||
let mut track = SpotifyId::from_base62(&args[2]).unwrap();
|
let track = SpotifyUri::Track {
|
||||||
track.item_type = SpotifyItemType::Track;
|
id: SpotifyId::from_base62(&args[2]).unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
let backend = audio_backend::find(None).unwrap();
|
let backend = audio_backend::find(None).unwrap();
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,8 @@ use std::{env, process::exit};
|
||||||
|
|
||||||
use librespot::{
|
use librespot::{
|
||||||
core::{
|
core::{
|
||||||
authentication::Credentials, config::SessionConfig, session::Session, spotify_id::SpotifyId,
|
authentication::Credentials, config::SessionConfig, session::Session,
|
||||||
|
spotify_uri::SpotifyUri,
|
||||||
},
|
},
|
||||||
metadata::{Metadata, Playlist, Track},
|
metadata::{Metadata, Playlist, Track},
|
||||||
};
|
};
|
||||||
|
@ -19,7 +20,7 @@ async fn main() {
|
||||||
}
|
}
|
||||||
let credentials = Credentials::with_access_token(&args[1]);
|
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!(
|
eprintln!(
|
||||||
"PLAYLIST should be a playlist URI such as: \
|
"PLAYLIST should be a playlist URI such as: \
|
||||||
\"spotify:playlist:37i9dQZF1DXec50AjHrNTq\""
|
\"spotify:playlist:37i9dQZF1DXec50AjHrNTq\""
|
||||||
|
|
|
@ -17,7 +17,7 @@ use crate::{
|
||||||
util::{impl_deref_wrapped, impl_try_from_repeated},
|
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 librespot_protocol as protocol;
|
||||||
use protocol::metadata::Disc as DiscMessage;
|
use protocol::metadata::Disc as DiscMessage;
|
||||||
|
@ -25,7 +25,7 @@ pub use protocol::metadata::album::Type as AlbumType;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Album {
|
pub struct Album {
|
||||||
pub id: SpotifyId,
|
pub id: SpotifyUri,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub artists: Artists,
|
pub artists: Artists,
|
||||||
pub album_type: AlbumType,
|
pub album_type: AlbumType,
|
||||||
|
@ -48,9 +48,9 @@ pub struct Album {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Disc {
|
pub struct Disc {
|
||||||
|
@ -65,7 +65,7 @@ pub struct Discs(pub Vec<Disc>);
|
||||||
impl_deref_wrapped!(Discs, Vec<Disc>);
|
impl_deref_wrapped!(Discs, Vec<Disc>);
|
||||||
|
|
||||||
impl Album {
|
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())
|
self.discs.iter().flat_map(|disc| disc.tracks.iter())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,11 +74,15 @@ impl Album {
|
||||||
impl Metadata for Album {
|
impl Metadata for Album {
|
||||||
type Message = protocol::metadata::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
|
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)
|
Self::try_from(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ use crate::{
|
||||||
util::{impl_deref_wrapped, impl_from_repeated, impl_try_from_repeated},
|
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;
|
use librespot_protocol as protocol;
|
||||||
pub use protocol::metadata::artist_with_role::ArtistRole;
|
pub use protocol::metadata::artist_with_role::ArtistRole;
|
||||||
|
@ -29,7 +29,7 @@ use protocol::metadata::TopTracks as TopTracksMessage;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Artist {
|
pub struct Artist {
|
||||||
pub id: SpotifyId,
|
pub id: SpotifyUri,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub popularity: i32,
|
pub popularity: i32,
|
||||||
pub top_tracks: CountryTopTracks,
|
pub top_tracks: CountryTopTracks,
|
||||||
|
@ -56,7 +56,7 @@ impl_deref_wrapped!(Artists, Vec<Artist>);
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ArtistWithRole {
|
pub struct ArtistWithRole {
|
||||||
pub id: SpotifyId,
|
pub id: SpotifyUri,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub role: ArtistRole,
|
pub role: ArtistRole,
|
||||||
}
|
}
|
||||||
|
@ -140,14 +140,14 @@ impl Artist {
|
||||||
/// Get the full list of albums, not containing duplicate variants of the same albums.
|
/// Get the full list of albums, not containing duplicate variants of the same albums.
|
||||||
///
|
///
|
||||||
/// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`]
|
/// 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()
|
self.albums.current_releases()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the full list of singles, not containing duplicate variants of the same singles.
|
/// Get the full list of singles, not containing duplicate variants of the same singles.
|
||||||
///
|
///
|
||||||
/// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`]
|
/// 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()
|
self.singles.current_releases()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,14 +155,14 @@ impl Artist {
|
||||||
/// compilations.
|
/// compilations.
|
||||||
///
|
///
|
||||||
/// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`]
|
/// 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()
|
self.compilations.current_releases()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the full list of albums, not containing duplicate variants of the same albums.
|
/// Get the full list of albums, not containing duplicate variants of the same albums.
|
||||||
///
|
///
|
||||||
/// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`]
|
/// 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()
|
self.appears_on_albums.current_releases()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -171,11 +171,15 @@ impl Artist {
|
||||||
impl Metadata for Artist {
|
impl Metadata for Artist {
|
||||||
type Message = protocol::metadata::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
|
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)
|
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
|
/// 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
|
/// multiple variants are available. This should be used if multiple variants of the same album
|
||||||
/// are not explicitely desired.
|
/// 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())
|
self.iter().filter_map(|agrp| agrp.first())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,7 @@ use crate::{
|
||||||
|
|
||||||
use super::file::AudioFiles;
|
use super::file::AudioFiles;
|
||||||
|
|
||||||
use librespot_core::{
|
use librespot_core::{Error, Session, SpotifyUri, date::Date, session::UserData};
|
||||||
Error, Session, SpotifyId, date::Date, session::UserData, spotify_id::SpotifyItemType,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub type AudioItemResult = Result<AudioItem, Error>;
|
pub type AudioItemResult = Result<AudioItem, Error>;
|
||||||
|
|
||||||
|
@ -29,7 +27,7 @@ pub struct CoverImage {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AudioItem {
|
pub struct AudioItem {
|
||||||
pub track_id: SpotifyId,
|
pub track_id: SpotifyUri,
|
||||||
pub uri: String,
|
pub uri: String,
|
||||||
pub files: AudioFiles,
|
pub files: AudioFiles,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -60,14 +58,14 @@ pub enum UniqueFields {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioItem {
|
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
|
let image_url = session
|
||||||
.get_user_attribute("image-url")
|
.get_user_attribute("image-url")
|
||||||
.unwrap_or_else(|| String::from("https://i.scdn.co/image/{file_id}"));
|
.unwrap_or_else(|| String::from("https://i.scdn.co/image/{file_id}"));
|
||||||
|
|
||||||
match id.item_type {
|
match uri {
|
||||||
SpotifyItemType::Track => {
|
SpotifyUri::Track { .. } => {
|
||||||
let track = Track::get(session, &id).await?;
|
let track = Track::get(session, &uri).await?;
|
||||||
|
|
||||||
if track.duration <= 0 {
|
if track.duration <= 0 {
|
||||||
return Err(Error::unavailable(MetadataError::InvalidDuration(
|
return Err(Error::unavailable(MetadataError::InvalidDuration(
|
||||||
|
@ -79,8 +77,7 @@ impl AudioItem {
|
||||||
return Err(Error::unavailable(MetadataError::ExplicitContentFiltered));
|
return Err(Error::unavailable(MetadataError::ExplicitContentFiltered));
|
||||||
}
|
}
|
||||||
|
|
||||||
let track_id = track.id;
|
let uri_string = uri.to_uri()?;
|
||||||
let uri = track_id.to_uri()?;
|
|
||||||
let album = track.album.name;
|
let album = track.album.name;
|
||||||
|
|
||||||
let album_artists = track
|
let album_artists = track
|
||||||
|
@ -123,8 +120,8 @@ impl AudioItem {
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
track_id,
|
track_id: uri,
|
||||||
uri,
|
uri: uri_string,
|
||||||
files: track.files,
|
files: track.files,
|
||||||
name: track.name,
|
name: track.name,
|
||||||
covers,
|
covers,
|
||||||
|
@ -136,8 +133,8 @@ impl AudioItem {
|
||||||
unique_fields,
|
unique_fields,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SpotifyItemType::Episode => {
|
SpotifyUri::Episode { .. } => {
|
||||||
let episode = Episode::get(session, &id).await?;
|
let episode = Episode::get(session, &uri).await?;
|
||||||
|
|
||||||
if episode.duration <= 0 {
|
if episode.duration <= 0 {
|
||||||
return Err(Error::unavailable(MetadataError::InvalidDuration(
|
return Err(Error::unavailable(MetadataError::InvalidDuration(
|
||||||
|
@ -149,8 +146,7 @@ impl AudioItem {
|
||||||
return Err(Error::unavailable(MetadataError::ExplicitContentFiltered));
|
return Err(Error::unavailable(MetadataError::ExplicitContentFiltered));
|
||||||
}
|
}
|
||||||
|
|
||||||
let track_id = episode.id;
|
let uri_string = uri.to_uri()?;
|
||||||
let uri = track_id.to_uri()?;
|
|
||||||
|
|
||||||
let covers = get_covers(episode.covers, image_url);
|
let covers = get_covers(episode.covers, image_url);
|
||||||
|
|
||||||
|
@ -167,8 +163,8 @@ impl AudioItem {
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
track_id,
|
track_id: uri,
|
||||||
uri,
|
uri: uri_string,
|
||||||
files: episode.audio,
|
files: episode.audio,
|
||||||
name: episode.name,
|
name: episode.name,
|
||||||
covers,
|
covers,
|
||||||
|
|
|
@ -15,14 +15,14 @@ use crate::{
|
||||||
video::VideoFiles,
|
video::VideoFiles,
|
||||||
};
|
};
|
||||||
|
|
||||||
use librespot_core::{Error, Session, SpotifyId, date::Date};
|
use librespot_core::{Error, Session, SpotifyUri, date::Date};
|
||||||
|
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
pub use protocol::metadata::episode::EpisodeType;
|
pub use protocol::metadata::episode::EpisodeType;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Episode {
|
pub struct Episode {
|
||||||
pub id: SpotifyId,
|
pub id: SpotifyUri,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub duration: i32,
|
pub duration: i32,
|
||||||
pub audio: AudioFiles,
|
pub audio: AudioFiles,
|
||||||
|
@ -49,19 +49,23 @@ pub struct Episode {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[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]
|
#[async_trait]
|
||||||
impl Metadata for Episode {
|
impl Metadata for Episode {
|
||||||
type Message = protocol::metadata::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
|
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)
|
Self::try_from(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
||||||
|
|
||||||
use crate::util::{impl_deref_wrapped, impl_from_repeated, impl_try_from_repeated};
|
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 librespot_protocol as protocol;
|
||||||
use protocol::metadata::Image as ImageMessage;
|
use protocol::metadata::Image as ImageMessage;
|
||||||
|
@ -47,7 +47,7 @@ impl_deref_wrapped!(PictureSizes, Vec<PictureSize>);
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TranscodedPicture {
|
pub struct TranscodedPicture {
|
||||||
pub target_name: String,
|
pub target_name: String,
|
||||||
pub uri: SpotifyId,
|
pub uri: SpotifyUri,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
@ -6,7 +6,7 @@ extern crate async_trait;
|
||||||
|
|
||||||
use protobuf::Message;
|
use protobuf::Message;
|
||||||
|
|
||||||
use librespot_core::{Error, Session, SpotifyId};
|
use librespot_core::{Error, Session, SpotifyUri};
|
||||||
|
|
||||||
pub mod album;
|
pub mod album;
|
||||||
pub mod artist;
|
pub mod artist;
|
||||||
|
@ -44,15 +44,15 @@ pub trait Metadata: Send + Sized + 'static {
|
||||||
type Message: protobuf::Message + std::fmt::Debug;
|
type Message: protobuf::Message + std::fmt::Debug;
|
||||||
|
|
||||||
// Request a protobuf
|
// Request a protobuf
|
||||||
async fn request(session: &Session, id: &SpotifyId) -> RequestResult;
|
async fn request(session: &Session, id: &SpotifyUri) -> RequestResult;
|
||||||
|
|
||||||
// Request a metadata struct
|
// 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 response = Self::request(session, id).await?;
|
||||||
let msg = Self::Message::parse_from_bytes(&response)?;
|
let msg = Self::Message::parse_from_bytes(&response)?;
|
||||||
trace!("Received metadata: {msg:#?}");
|
trace!("Received metadata: {msg:#?}");
|
||||||
Self::parse(&msg, id)
|
Self::parse(&msg, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(msg: &Self::Message, _: &SpotifyId) -> Result<Self, Error>;
|
fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,7 @@ use crate::{
|
||||||
request::{MercuryRequest, RequestResult},
|
request::{MercuryRequest, RequestResult},
|
||||||
};
|
};
|
||||||
|
|
||||||
use librespot_core::{Error, Session, SpotifyId};
|
use librespot_core::{Error, Session, SpotifyId, SpotifyUri};
|
||||||
|
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
pub use protocol::playlist_annotate3::AbuseReportState;
|
pub use protocol::playlist_annotate3::AbuseReportState;
|
||||||
|
|
||||||
|
@ -26,12 +25,20 @@ pub struct PlaylistAnnotation {
|
||||||
impl Metadata for PlaylistAnnotation {
|
impl Metadata for PlaylistAnnotation {
|
||||||
type Message = protocol::playlist_annotate3::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 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, ¤t_user, playlist_id).await
|
Self::request_for_user(session, ¤t_user, playlist_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(msg: &Self::Message, _: &SpotifyId) -> Result<Self, Error> {
|
fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
description: msg.description().to_owned(),
|
description: msg.description().to_owned(),
|
||||||
picture: msg.picture().to_owned(), // TODO: is this a URL or Spotify URI?
|
picture: msg.picture().to_owned(), // TODO: is this a URL or Spotify URI?
|
||||||
|
@ -60,11 +67,18 @@ impl PlaylistAnnotation {
|
||||||
async fn get_for_user(
|
async fn get_for_user(
|
||||||
session: &Session,
|
session: &Session,
|
||||||
username: &str,
|
username: &str,
|
||||||
playlist_id: &SpotifyId,
|
playlist_uri: &SpotifyUri,
|
||||||
) -> Result<Self, Error> {
|
) -> 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 response = Self::request_for_user(session, username, playlist_id).await?;
|
||||||
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
|
let msg = <Self as Metadata>::Message::parse_from_bytes(&response)?;
|
||||||
Self::parse(&msg, playlist_id)
|
Self::parse(&msg, playlist_uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ use super::{
|
||||||
permission::Capabilities,
|
permission::Capabilities,
|
||||||
};
|
};
|
||||||
|
|
||||||
use librespot_core::{SpotifyId, date::Date};
|
use librespot_core::{SpotifyUri, date::Date};
|
||||||
|
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
use protocol::playlist4_external::Item as PlaylistItemMessage;
|
use protocol::playlist4_external::Item as PlaylistItemMessage;
|
||||||
|
@ -19,7 +19,7 @@ use protocol::playlist4_external::MetaItem as PlaylistMetaItemMessage;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PlaylistItem {
|
pub struct PlaylistItem {
|
||||||
pub id: SpotifyId,
|
pub id: SpotifyUri,
|
||||||
pub attributes: PlaylistItemAttributes,
|
pub attributes: PlaylistItemAttributes,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ pub struct PlaylistItemList {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PlaylistMetaItem {
|
pub struct PlaylistMetaItem {
|
||||||
pub revision: SpotifyId,
|
pub revision: SpotifyUri,
|
||||||
pub attributes: PlaylistAttributes,
|
pub attributes: PlaylistAttributes,
|
||||||
pub length: i32,
|
pub length: i32,
|
||||||
pub timestamp: Date,
|
pub timestamp: Date,
|
||||||
|
|
|
@ -14,12 +14,7 @@ use super::{
|
||||||
permission::Capabilities,
|
permission::Capabilities,
|
||||||
};
|
};
|
||||||
|
|
||||||
use librespot_core::{
|
use librespot_core::{Error, Session, SpotifyUri, date::Date, spotify_id::SpotifyId};
|
||||||
Error, Session,
|
|
||||||
date::Date,
|
|
||||||
spotify_id::{NamedSpotifyId, SpotifyId},
|
|
||||||
};
|
|
||||||
|
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
use protocol::playlist4_external::GeoblockBlockingType as Geoblock;
|
use protocol::playlist4_external::GeoblockBlockingType as Geoblock;
|
||||||
|
|
||||||
|
@ -30,7 +25,7 @@ impl_deref_wrapped!(Geoblocks, Vec<Geoblock>);
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Playlist {
|
pub struct Playlist {
|
||||||
pub id: NamedSpotifyId,
|
pub id: SpotifyUri,
|
||||||
pub revision: Vec<u8>,
|
pub revision: Vec<u8>,
|
||||||
pub length: i32,
|
pub length: i32,
|
||||||
pub attributes: PlaylistAttributes,
|
pub attributes: PlaylistAttributes,
|
||||||
|
@ -72,7 +67,7 @@ pub struct SelectedListContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Playlist {
|
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 tracks = self.contents.items.iter().map(|item| &item.id);
|
||||||
|
|
||||||
let length = tracks.len();
|
let length = tracks.len();
|
||||||
|
@ -93,17 +88,35 @@ impl Playlist {
|
||||||
impl Metadata for Playlist {
|
impl Metadata for Playlist {
|
||||||
type Message = protocol::playlist4_external::SelectedListContent;
|
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
|
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
|
// the playlist proto doesn't contain the id so we decorate it
|
||||||
let playlist = SelectedListContent::try_from(msg)?;
|
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 {
|
Ok(Self {
|
||||||
id,
|
id: new_uri,
|
||||||
revision: playlist.revision,
|
revision: playlist.revision,
|
||||||
length: playlist.length,
|
length: playlist.length,
|
||||||
attributes: playlist.attributes,
|
attributes: playlist.attributes,
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
episode::Episodes, image::Images, restriction::Restrictions,
|
episode::Episodes, image::Images, restriction::Restrictions,
|
||||||
};
|
};
|
||||||
|
|
||||||
use librespot_core::{Error, Session, SpotifyId};
|
use librespot_core::{Error, Session, SpotifyUri};
|
||||||
|
|
||||||
use librespot_protocol as protocol;
|
use librespot_protocol as protocol;
|
||||||
pub use protocol::metadata::show::ConsumptionOrder as ShowConsumptionOrder;
|
pub use protocol::metadata::show::ConsumptionOrder as ShowConsumptionOrder;
|
||||||
|
@ -13,7 +13,7 @@ pub use protocol::metadata::show::MediaType as ShowMediaType;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Show {
|
pub struct Show {
|
||||||
pub id: SpotifyId,
|
pub id: SpotifyUri,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub publisher: String,
|
pub publisher: String,
|
||||||
|
@ -27,7 +27,7 @@ pub struct Show {
|
||||||
pub media_type: ShowMediaType,
|
pub media_type: ShowMediaType,
|
||||||
pub consumption_order: ShowConsumptionOrder,
|
pub consumption_order: ShowConsumptionOrder,
|
||||||
pub availability: Availabilities,
|
pub availability: Availabilities,
|
||||||
pub trailer_uri: Option<SpotifyId>,
|
pub trailer_uri: Option<SpotifyUri>,
|
||||||
pub has_music_and_talk: bool,
|
pub has_music_and_talk: bool,
|
||||||
pub is_audiobook: bool,
|
pub is_audiobook: bool,
|
||||||
}
|
}
|
||||||
|
@ -36,11 +36,15 @@ pub struct Show {
|
||||||
impl Metadata for Show {
|
impl Metadata for Show {
|
||||||
type Message = protocol::metadata::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
|
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)
|
Self::try_from(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,7 +71,7 @@ impl TryFrom<&<Self as Metadata>::Message> for Show {
|
||||||
.trailer_uri
|
.trailer_uri
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.map(SpotifyId::from_uri)
|
.map(SpotifyUri::from_uri)
|
||||||
.transpose()?,
|
.transpose()?,
|
||||||
has_music_and_talk: show.music_and_talk(),
|
has_music_and_talk: show.music_and_talk(),
|
||||||
is_audiobook: show.is_audiobook(),
|
is_audiobook: show.is_audiobook(),
|
||||||
|
|
|
@ -17,12 +17,12 @@ use crate::{
|
||||||
util::{impl_deref_wrapped, impl_try_from_repeated},
|
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 librespot_protocol as protocol;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Track {
|
pub struct Track {
|
||||||
pub id: SpotifyId,
|
pub id: SpotifyUri,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub album: Album,
|
pub album: Album,
|
||||||
pub artists: Artists,
|
pub artists: Artists,
|
||||||
|
@ -50,19 +50,23 @@ pub struct Track {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[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]
|
#[async_trait]
|
||||||
impl Metadata for Track {
|
impl Metadata for Track {
|
||||||
type Message = protocol::metadata::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
|
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)
|
Self::try_from(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,18 +51,12 @@ librespot-audio = { version = "0.7.1", path = "../audio", default-features = fal
|
||||||
librespot-core = { version = "0.7.1", path = "../core", default-features = false }
|
librespot-core = { version = "0.7.1", path = "../core", default-features = false }
|
||||||
librespot-metadata = { version = "0.7.1", path = "../metadata", default-features = false }
|
librespot-metadata = { version = "0.7.1", path = "../metadata", default-features = false }
|
||||||
|
|
||||||
portable-atomic = "1"
|
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
||||||
futures-util = "0.3"
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
|
portable-atomic = "1"
|
||||||
shell-words = "1.1"
|
shell-words = "1.1"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tokio = { version = "1", features = [
|
tokio = { version = "1", features = ["rt-multi-thread", "sync"] }
|
||||||
"parking_lot",
|
|
||||||
"rt",
|
|
||||||
"rt-multi-thread",
|
|
||||||
"sync",
|
|
||||||
] }
|
|
||||||
zerocopy = { version = "0.8", features = ["derive"] }
|
zerocopy = { version = "0.8", features = ["derive"] }
|
||||||
|
|
||||||
# Backends
|
# Backends
|
||||||
|
@ -97,5 +91,5 @@ symphonia = { version = "0.5", default-features = false, features = [
|
||||||
ogg = { version = "0.9", optional = true }
|
ogg = { version = "0.9", optional = true }
|
||||||
|
|
||||||
# Dithering
|
# Dithering
|
||||||
rand = { version = "0.9", features = ["small_rng"] }
|
rand = { version = "0.9", default-features = false, features = ["small_rng"] }
|
||||||
rand_distr = "0.5"
|
rand_distr = "0.5"
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use gstreamer::{
|
use gstreamer::{
|
||||||
State,
|
State,
|
||||||
event::{FlushStart, FlushStop},
|
event::{FlushStart, FlushStop},
|
||||||
|
@ -8,8 +10,7 @@ use gstreamer as gst;
|
||||||
use gstreamer_app as gst_app;
|
use gstreamer_app as gst_app;
|
||||||
use gstreamer_audio as gst_audio;
|
use gstreamer_audio as gst_audio;
|
||||||
|
|
||||||
use parking_lot::Mutex;
|
const GSTREAMER_ASYNC_ERROR_POISON_MSG: &str = "gstreamer async error mutex should not be poisoned";
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};
|
use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};
|
||||||
|
|
||||||
|
@ -97,7 +98,9 @@ impl Open for GstreamerSink {
|
||||||
gst::MessageView::Eos(_) => {
|
gst::MessageView::Eos(_) => {
|
||||||
println!("gst signaled end of stream");
|
println!("gst signaled end of stream");
|
||||||
|
|
||||||
let mut async_error_storage = async_error_clone.lock();
|
let mut async_error_storage = async_error_clone
|
||||||
|
.lock()
|
||||||
|
.expect(GSTREAMER_ASYNC_ERROR_POISON_MSG);
|
||||||
*async_error_storage = Some(String::from("gst signaled end of stream"));
|
*async_error_storage = Some(String::from("gst signaled end of stream"));
|
||||||
}
|
}
|
||||||
gst::MessageView::Error(err) => {
|
gst::MessageView::Error(err) => {
|
||||||
|
@ -108,7 +111,9 @@ impl Open for GstreamerSink {
|
||||||
err.debug()
|
err.debug()
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut async_error_storage = async_error_clone.lock();
|
let mut async_error_storage = async_error_clone
|
||||||
|
.lock()
|
||||||
|
.expect(GSTREAMER_ASYNC_ERROR_POISON_MSG);
|
||||||
*async_error_storage = Some(format!(
|
*async_error_storage = Some(format!(
|
||||||
"Error from {:?}: {} ({:?})",
|
"Error from {:?}: {} ({:?})",
|
||||||
err.src().map(|s| s.path_string()),
|
err.src().map(|s| s.path_string()),
|
||||||
|
@ -138,7 +143,10 @@ impl Open for GstreamerSink {
|
||||||
|
|
||||||
impl Sink for GstreamerSink {
|
impl Sink for GstreamerSink {
|
||||||
fn start(&mut self) -> SinkResult<()> {
|
fn start(&mut self) -> SinkResult<()> {
|
||||||
*self.async_error.lock() = None;
|
*self
|
||||||
|
.async_error
|
||||||
|
.lock()
|
||||||
|
.expect(GSTREAMER_ASYNC_ERROR_POISON_MSG) = None;
|
||||||
self.appsrc.send_event(FlushStop::new(true));
|
self.appsrc.send_event(FlushStop::new(true));
|
||||||
self.bufferpool
|
self.bufferpool
|
||||||
.set_active(true)
|
.set_active(true)
|
||||||
|
@ -150,7 +158,10 @@ impl Sink for GstreamerSink {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop(&mut self) -> SinkResult<()> {
|
fn stop(&mut self) -> SinkResult<()> {
|
||||||
*self.async_error.lock() = None;
|
*self
|
||||||
|
.async_error
|
||||||
|
.lock()
|
||||||
|
.expect(GSTREAMER_ASYNC_ERROR_POISON_MSG) = None;
|
||||||
self.appsrc.send_event(FlushStart::new());
|
self.appsrc.send_event(FlushStart::new());
|
||||||
self.pipeline
|
self.pipeline
|
||||||
.set_state(State::Paused)
|
.set_state(State::Paused)
|
||||||
|
@ -173,7 +184,11 @@ impl Drop for GstreamerSink {
|
||||||
impl SinkAsBytes for GstreamerSink {
|
impl SinkAsBytes for GstreamerSink {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
|
fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
|
||||||
if let Some(async_error) = &*self.async_error.lock() {
|
if let Some(async_error) = &*self
|
||||||
|
.async_error
|
||||||
|
.lock()
|
||||||
|
.expect(GSTREAMER_ASYNC_ERROR_POISON_MSG)
|
||||||
|
{
|
||||||
return Err(SinkError::OnWrite(async_error.to_string()));
|
return Err(SinkError::OnWrite(async_error.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ use std::{
|
||||||
mem,
|
mem,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
process::exit,
|
process::exit,
|
||||||
|
sync::Mutex,
|
||||||
sync::{
|
sync::{
|
||||||
Arc,
|
Arc,
|
||||||
atomic::{AtomicUsize, Ordering},
|
atomic::{AtomicUsize, Ordering},
|
||||||
|
@ -15,27 +16,25 @@ use std::{
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use futures_util::{
|
#[cfg(feature = "passthrough-decoder")]
|
||||||
StreamExt, TryFutureExt, future, future::FusedFuture,
|
use crate::decoder::PassthroughDecoder;
|
||||||
stream::futures_unordered::FuturesUnordered,
|
|
||||||
};
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use symphonia::core::io::MediaSource;
|
|
||||||
use tokio::sync::{mpsc, oneshot};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
audio::{AudioDecrypt, AudioFetchParams, AudioFile, StreamLoaderController},
|
audio::{AudioDecrypt, AudioFetchParams, AudioFile, StreamLoaderController},
|
||||||
audio_backend::Sink,
|
audio_backend::Sink,
|
||||||
config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig},
|
config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig},
|
||||||
convert::Converter,
|
convert::Converter,
|
||||||
core::{Error, Session, SpotifyId, util::SeqGenerator},
|
core::{Error, Session, SpotifyId, SpotifyUri, util::SeqGenerator},
|
||||||
decoder::{AudioDecoder, AudioPacket, AudioPacketPosition, SymphoniaDecoder},
|
decoder::{AudioDecoder, AudioPacket, AudioPacketPosition, SymphoniaDecoder},
|
||||||
metadata::audio::{AudioFileFormat, AudioFiles, AudioItem},
|
metadata::audio::{AudioFileFormat, AudioFiles, AudioItem},
|
||||||
mixer::VolumeGetter,
|
mixer::VolumeGetter,
|
||||||
};
|
};
|
||||||
|
use futures_util::{
|
||||||
#[cfg(feature = "passthrough-decoder")]
|
StreamExt, TryFutureExt, future, future::FusedFuture,
|
||||||
use crate::decoder::PassthroughDecoder;
|
stream::futures_unordered::FuturesUnordered,
|
||||||
|
};
|
||||||
|
use librespot_metadata::track::Tracks;
|
||||||
|
use symphonia::core::io::MediaSource;
|
||||||
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
|
||||||
use crate::SAMPLES_PER_SECOND;
|
use crate::SAMPLES_PER_SECOND;
|
||||||
|
|
||||||
|
@ -47,6 +46,8 @@ pub const PCM_AT_0DBFS: f64 = 1.0;
|
||||||
// otherwise expect in Vorbis comments. This packet isn't well-formed and players may balk at it.
|
// otherwise expect in Vorbis comments. This packet isn't well-formed and players may balk at it.
|
||||||
const SPOTIFY_OGG_HEADER_END: u64 = 0xa7;
|
const SPOTIFY_OGG_HEADER_END: u64 = 0xa7;
|
||||||
|
|
||||||
|
const LOAD_HANDLES_POISON_MSG: &str = "load handles mutex should not be poisoned";
|
||||||
|
|
||||||
pub type PlayerResult = Result<(), Error>;
|
pub type PlayerResult = Result<(), Error>;
|
||||||
|
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
|
@ -94,12 +95,12 @@ static PLAYER_COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
enum PlayerCommand {
|
enum PlayerCommand {
|
||||||
Load {
|
Load {
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
play: bool,
|
play: bool,
|
||||||
position_ms: u32,
|
position_ms: u32,
|
||||||
},
|
},
|
||||||
Preload {
|
Preload {
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
},
|
},
|
||||||
Play,
|
Play,
|
||||||
Pause,
|
Pause,
|
||||||
|
@ -142,17 +143,17 @@ pub enum PlayerEvent {
|
||||||
// Fired when the player is stopped (e.g. by issuing a "stop" command to the player).
|
// Fired when the player is stopped (e.g. by issuing a "stop" command to the player).
|
||||||
Stopped {
|
Stopped {
|
||||||
play_request_id: u64,
|
play_request_id: u64,
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
},
|
},
|
||||||
// The player is delayed by loading a track.
|
// The player is delayed by loading a track.
|
||||||
Loading {
|
Loading {
|
||||||
play_request_id: u64,
|
play_request_id: u64,
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
position_ms: u32,
|
position_ms: u32,
|
||||||
},
|
},
|
||||||
// The player is preloading a track.
|
// The player is preloading a track.
|
||||||
Preloading {
|
Preloading {
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
},
|
},
|
||||||
// The player is playing a track.
|
// The player is playing a track.
|
||||||
// This event is issued at the start of playback of whenever the position must be communicated
|
// This event is issued at the start of playback of whenever the position must be communicated
|
||||||
|
@ -163,31 +164,31 @@ pub enum PlayerEvent {
|
||||||
// after a buffer-underrun
|
// after a buffer-underrun
|
||||||
Playing {
|
Playing {
|
||||||
play_request_id: u64,
|
play_request_id: u64,
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
position_ms: u32,
|
position_ms: u32,
|
||||||
},
|
},
|
||||||
// The player entered a paused state.
|
// The player entered a paused state.
|
||||||
Paused {
|
Paused {
|
||||||
play_request_id: u64,
|
play_request_id: u64,
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
position_ms: u32,
|
position_ms: u32,
|
||||||
},
|
},
|
||||||
// The player thinks it's a good idea to issue a preload command for the next track now.
|
// 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.
|
// This event is intended for use within spirc.
|
||||||
TimeToPreloadNextTrack {
|
TimeToPreloadNextTrack {
|
||||||
play_request_id: u64,
|
play_request_id: u64,
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
},
|
},
|
||||||
// The player reached the end of a track.
|
// The player reached the end of a track.
|
||||||
// This event is intended for use within spirc. Spirc will respond by issuing another command.
|
// This event is intended for use within spirc. Spirc will respond by issuing another command.
|
||||||
EndOfTrack {
|
EndOfTrack {
|
||||||
play_request_id: u64,
|
play_request_id: u64,
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
},
|
},
|
||||||
// The player was unable to load the requested track.
|
// The player was unable to load the requested track.
|
||||||
Unavailable {
|
Unavailable {
|
||||||
play_request_id: u64,
|
play_request_id: u64,
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
},
|
},
|
||||||
// The mixer volume was set to a new level.
|
// The mixer volume was set to a new level.
|
||||||
VolumeChanged {
|
VolumeChanged {
|
||||||
|
@ -195,7 +196,7 @@ pub enum PlayerEvent {
|
||||||
},
|
},
|
||||||
PositionCorrection {
|
PositionCorrection {
|
||||||
play_request_id: u64,
|
play_request_id: u64,
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
position_ms: u32,
|
position_ms: u32,
|
||||||
},
|
},
|
||||||
/// Requires `PlayerConfig::position_update_interval` to be set to Some.
|
/// Requires `PlayerConfig::position_update_interval` to be set to Some.
|
||||||
|
@ -203,12 +204,12 @@ pub enum PlayerEvent {
|
||||||
/// current playback position
|
/// current playback position
|
||||||
PositionChanged {
|
PositionChanged {
|
||||||
play_request_id: u64,
|
play_request_id: u64,
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
position_ms: u32,
|
position_ms: u32,
|
||||||
},
|
},
|
||||||
Seeked {
|
Seeked {
|
||||||
play_request_id: u64,
|
play_request_id: u64,
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
position_ms: u32,
|
position_ms: u32,
|
||||||
},
|
},
|
||||||
TrackChanged {
|
TrackChanged {
|
||||||
|
@ -526,7 +527,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 {
|
self.command(PlayerCommand::Load {
|
||||||
track_id,
|
track_id,
|
||||||
play: start_playing,
|
play: start_playing,
|
||||||
|
@ -534,7 +535,7 @@ impl Player {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn preload(&self, track_id: SpotifyId) {
|
pub fn preload(&self, track_id: SpotifyUri) {
|
||||||
self.command(PlayerCommand::Preload { track_id });
|
self.command(PlayerCommand::Preload { track_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -660,11 +661,11 @@ struct PlayerLoadedTrackData {
|
||||||
enum PlayerPreload {
|
enum PlayerPreload {
|
||||||
None,
|
None,
|
||||||
Loading {
|
Loading {
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
loader: Pin<Box<dyn FusedFuture<Output = Result<PlayerLoadedTrackData, ()>> + Send>>,
|
loader: Pin<Box<dyn FusedFuture<Output = Result<PlayerLoadedTrackData, ()>> + Send>>,
|
||||||
},
|
},
|
||||||
Ready {
|
Ready {
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
loaded_track: Box<PlayerLoadedTrackData>,
|
loaded_track: Box<PlayerLoadedTrackData>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -674,13 +675,13 @@ type Decoder = Box<dyn AudioDecoder + Send>;
|
||||||
enum PlayerState {
|
enum PlayerState {
|
||||||
Stopped,
|
Stopped,
|
||||||
Loading {
|
Loading {
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
play_request_id: u64,
|
play_request_id: u64,
|
||||||
start_playback: bool,
|
start_playback: bool,
|
||||||
loader: Pin<Box<dyn FusedFuture<Output = Result<PlayerLoadedTrackData, ()>> + Send>>,
|
loader: Pin<Box<dyn FusedFuture<Output = Result<PlayerLoadedTrackData, ()>> + Send>>,
|
||||||
},
|
},
|
||||||
Paused {
|
Paused {
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
play_request_id: u64,
|
play_request_id: u64,
|
||||||
decoder: Decoder,
|
decoder: Decoder,
|
||||||
audio_item: AudioItem,
|
audio_item: AudioItem,
|
||||||
|
@ -694,7 +695,7 @@ enum PlayerState {
|
||||||
is_explicit: bool,
|
is_explicit: bool,
|
||||||
},
|
},
|
||||||
Playing {
|
Playing {
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
play_request_id: u64,
|
play_request_id: u64,
|
||||||
decoder: Decoder,
|
decoder: Decoder,
|
||||||
normalisation_data: NormalisationData,
|
normalisation_data: NormalisationData,
|
||||||
|
@ -709,7 +710,7 @@ enum PlayerState {
|
||||||
is_explicit: bool,
|
is_explicit: bool,
|
||||||
},
|
},
|
||||||
EndOfTrack {
|
EndOfTrack {
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
play_request_id: u64,
|
play_request_id: u64,
|
||||||
loaded_track: PlayerLoadedTrackData,
|
loaded_track: PlayerLoadedTrackData,
|
||||||
},
|
},
|
||||||
|
@ -893,10 +894,12 @@ impl PlayerTrackLoader {
|
||||||
None
|
None
|
||||||
} else if !audio_item.files.is_empty() {
|
} else if !audio_item.files.is_empty() {
|
||||||
Some(audio_item)
|
Some(audio_item)
|
||||||
} else if let Some(alternatives) = &audio_item.alternatives {
|
} else if let Some(alternatives) = audio_item.alternatives {
|
||||||
let alternatives: FuturesUnordered<_> = alternatives
|
let Tracks(alternatives_vec) = alternatives; // required to make `into_iter` able to move
|
||||||
.iter()
|
|
||||||
.map(|alt_id| AudioItem::get_file(&self.session, *alt_id))
|
let alternatives: FuturesUnordered<_> = alternatives_vec
|
||||||
|
.into_iter()
|
||||||
|
.map(|alt_id| AudioItem::get_file(&self.session, alt_id))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
alternatives
|
alternatives
|
||||||
|
@ -938,16 +941,40 @@ impl PlayerTrackLoader {
|
||||||
|
|
||||||
async fn load_track(
|
async fn load_track(
|
||||||
&self,
|
&self,
|
||||||
spotify_id: SpotifyId,
|
track_uri: SpotifyUri,
|
||||||
position_ms: u32,
|
position_ms: u32,
|
||||||
) -> Option<PlayerLoadedTrackData> {
|
) -> 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 {
|
Ok(audio) => match self.find_available_alternative(audio).await {
|
||||||
Some(audio) => audio,
|
Some(audio) => audio,
|
||||||
None => {
|
None => {
|
||||||
warn!(
|
warn!(
|
||||||
"<{}> is not available",
|
"spotify:track:<{}> is not available",
|
||||||
spotify_id.to_uri().unwrap_or_default()
|
track_id.to_base62().unwrap_or_default()
|
||||||
);
|
);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
@ -1033,13 +1060,14 @@ impl PlayerTrackLoader {
|
||||||
// Not all audio files are encrypted. If we can't get a key, try loading the track
|
// 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
|
// 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.
|
// 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),
|
Ok(key) => Some(key),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Unable to load key, continuing without decryption: {e}");
|
warn!("Unable to load key, continuing without decryption: {e}");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut decrypted_file = AudioDecrypt::new(key, encrypted_file);
|
let mut decrypted_file = AudioDecrypt::new(key, encrypted_file);
|
||||||
|
|
||||||
let is_ogg_vorbis = AudioFiles::is_ogg_vorbis(format);
|
let is_ogg_vorbis = AudioFiles::is_ogg_vorbis(format);
|
||||||
|
@ -1195,13 +1223,15 @@ impl Future for PlayerInternal {
|
||||||
// Handle loading of a new track to play
|
// Handle loading of a new track to play
|
||||||
if let PlayerState::Loading {
|
if let PlayerState::Loading {
|
||||||
ref mut loader,
|
ref mut loader,
|
||||||
track_id,
|
ref track_id,
|
||||||
start_playback,
|
start_playback,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
} = self.state
|
} = self.state
|
||||||
{
|
{
|
||||||
// The loader may be terminated if we are trying to load the same track
|
// The loader may be terminated if we are trying to load the same track
|
||||||
// as before, and that track failed to open before.
|
// as before, and that track failed to open before.
|
||||||
|
let track_id = track_id.clone();
|
||||||
|
|
||||||
if !loader.as_mut().is_terminated() {
|
if !loader.as_mut().is_terminated() {
|
||||||
match loader.as_mut().poll(cx) {
|
match loader.as_mut().poll(cx) {
|
||||||
Poll::Ready(Ok(loaded_track)) => {
|
Poll::Ready(Ok(loaded_track)) => {
|
||||||
|
@ -1233,12 +1263,15 @@ impl Future for PlayerInternal {
|
||||||
// handle pending preload requests.
|
// handle pending preload requests.
|
||||||
if let PlayerPreload::Loading {
|
if let PlayerPreload::Loading {
|
||||||
ref mut loader,
|
ref mut loader,
|
||||||
track_id,
|
ref track_id,
|
||||||
} = self.preload
|
} = self.preload
|
||||||
{
|
{
|
||||||
|
let track_id = track_id.clone();
|
||||||
match loader.as_mut().poll(cx) {
|
match loader.as_mut().poll(cx) {
|
||||||
Poll::Ready(Ok(loaded_track)) => {
|
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 {
|
self.preload = PlayerPreload::Ready {
|
||||||
track_id,
|
track_id,
|
||||||
loaded_track: Box::new(loaded_track),
|
loaded_track: Box::new(loaded_track),
|
||||||
|
@ -1269,7 +1302,7 @@ impl Future for PlayerInternal {
|
||||||
self.ensure_sink_running();
|
self.ensure_sink_running();
|
||||||
|
|
||||||
if let PlayerState::Playing {
|
if let PlayerState::Playing {
|
||||||
track_id,
|
ref track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
ref mut decoder,
|
ref mut decoder,
|
||||||
normalisation_factor,
|
normalisation_factor,
|
||||||
|
@ -1278,6 +1311,7 @@ impl Future for PlayerInternal {
|
||||||
..
|
..
|
||||||
} = self.state
|
} = self.state
|
||||||
{
|
{
|
||||||
|
let track_id = track_id.clone();
|
||||||
match decoder.next_packet() {
|
match decoder.next_packet() {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
if let Some((ref packet_position, ref packet)) = result {
|
if let Some((ref packet_position, ref packet)) = result {
|
||||||
|
@ -1338,7 +1372,7 @@ impl Future for PlayerInternal {
|
||||||
now.checked_sub(new_stream_position);
|
now.checked_sub(new_stream_position);
|
||||||
self.send_event(PlayerEvent::PositionCorrection {
|
self.send_event(PlayerEvent::PositionCorrection {
|
||||||
play_request_id,
|
play_request_id,
|
||||||
track_id,
|
track_id: track_id.clone(),
|
||||||
position_ms: new_stream_position_ms,
|
position_ms: new_stream_position_ms,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1391,7 +1425,7 @@ impl Future for PlayerInternal {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let PlayerState::Playing {
|
if let PlayerState::Playing {
|
||||||
track_id,
|
ref track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
duration_ms,
|
duration_ms,
|
||||||
stream_position_ms,
|
stream_position_ms,
|
||||||
|
@ -1400,7 +1434,7 @@ impl Future for PlayerInternal {
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
| PlayerState::Paused {
|
| PlayerState::Paused {
|
||||||
track_id,
|
ref track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
duration_ms,
|
duration_ms,
|
||||||
stream_position_ms,
|
stream_position_ms,
|
||||||
|
@ -1409,6 +1443,8 @@ impl Future for PlayerInternal {
|
||||||
..
|
..
|
||||||
} = self.state
|
} = self.state
|
||||||
{
|
{
|
||||||
|
let track_id = track_id.clone();
|
||||||
|
|
||||||
if (!*suggested_to_preload_next_track)
|
if (!*suggested_to_preload_next_track)
|
||||||
&& ((duration_ms as i64 - stream_position_ms as i64)
|
&& ((duration_ms as i64 - stream_position_ms as i64)
|
||||||
< PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS as i64)
|
< PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS as i64)
|
||||||
|
@ -1482,25 +1518,27 @@ impl PlayerInternal {
|
||||||
fn handle_player_stop(&mut self) {
|
fn handle_player_stop(&mut self) {
|
||||||
match self.state {
|
match self.state {
|
||||||
PlayerState::Playing {
|
PlayerState::Playing {
|
||||||
track_id,
|
ref track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
| PlayerState::Paused {
|
| PlayerState::Paused {
|
||||||
track_id,
|
ref track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
| PlayerState::EndOfTrack {
|
| PlayerState::EndOfTrack {
|
||||||
track_id,
|
ref track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
| PlayerState::Loading {
|
| PlayerState::Loading {
|
||||||
track_id,
|
ref track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
|
let track_id = track_id.clone();
|
||||||
|
|
||||||
self.ensure_sink_stopped(false);
|
self.ensure_sink_stopped(false);
|
||||||
self.send_event(PlayerEvent::Stopped {
|
self.send_event(PlayerEvent::Stopped {
|
||||||
track_id,
|
track_id,
|
||||||
|
@ -1519,11 +1557,13 @@ impl PlayerInternal {
|
||||||
fn handle_play(&mut self) {
|
fn handle_play(&mut self) {
|
||||||
match self.state {
|
match self.state {
|
||||||
PlayerState::Paused {
|
PlayerState::Paused {
|
||||||
track_id,
|
ref track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
stream_position_ms,
|
stream_position_ms,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
|
let track_id = track_id.clone();
|
||||||
|
|
||||||
self.state.paused_to_playing();
|
self.state.paused_to_playing();
|
||||||
self.send_event(PlayerEvent::Playing {
|
self.send_event(PlayerEvent::Playing {
|
||||||
track_id,
|
track_id,
|
||||||
|
@ -1546,11 +1586,13 @@ impl PlayerInternal {
|
||||||
match self.state {
|
match self.state {
|
||||||
PlayerState::Paused { .. } => self.ensure_sink_stopped(false),
|
PlayerState::Paused { .. } => self.ensure_sink_stopped(false),
|
||||||
PlayerState::Playing {
|
PlayerState::Playing {
|
||||||
track_id,
|
ref track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
stream_position_ms,
|
stream_position_ms,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
|
let track_id = track_id.clone();
|
||||||
|
|
||||||
self.state.playing_to_paused();
|
self.state.playing_to_paused();
|
||||||
|
|
||||||
self.ensure_sink_stopped(false);
|
self.ensure_sink_stopped(false);
|
||||||
|
@ -1681,13 +1723,13 @@ impl PlayerInternal {
|
||||||
None => {
|
None => {
|
||||||
self.state.playing_to_end_of_track();
|
self.state.playing_to_end_of_track();
|
||||||
if let PlayerState::EndOfTrack {
|
if let PlayerState::EndOfTrack {
|
||||||
track_id,
|
ref track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
..
|
..
|
||||||
} = self.state
|
} = self.state
|
||||||
{
|
{
|
||||||
self.send_event(PlayerEvent::EndOfTrack {
|
self.send_event(PlayerEvent::EndOfTrack {
|
||||||
track_id,
|
track_id: track_id.clone(),
|
||||||
play_request_id,
|
play_request_id,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -1700,7 +1742,7 @@ impl PlayerInternal {
|
||||||
|
|
||||||
fn start_playback(
|
fn start_playback(
|
||||||
&mut self,
|
&mut self,
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
play_request_id: u64,
|
play_request_id: u64,
|
||||||
loaded_track: PlayerLoadedTrackData,
|
loaded_track: PlayerLoadedTrackData,
|
||||||
start_playback: bool,
|
start_playback: bool,
|
||||||
|
@ -1725,7 +1767,7 @@ impl PlayerInternal {
|
||||||
if start_playback {
|
if start_playback {
|
||||||
self.ensure_sink_running();
|
self.ensure_sink_running();
|
||||||
self.send_event(PlayerEvent::Playing {
|
self.send_event(PlayerEvent::Playing {
|
||||||
track_id,
|
track_id: track_id.clone(),
|
||||||
play_request_id,
|
play_request_id,
|
||||||
position_ms,
|
position_ms,
|
||||||
});
|
});
|
||||||
|
@ -1750,7 +1792,7 @@ impl PlayerInternal {
|
||||||
self.ensure_sink_stopped(false);
|
self.ensure_sink_stopped(false);
|
||||||
|
|
||||||
self.state = PlayerState::Paused {
|
self.state = PlayerState::Paused {
|
||||||
track_id,
|
track_id: track_id.clone(),
|
||||||
play_request_id,
|
play_request_id,
|
||||||
decoder: loaded_track.decoder,
|
decoder: loaded_track.decoder,
|
||||||
audio_item: loaded_track.audio_item,
|
audio_item: loaded_track.audio_item,
|
||||||
|
@ -1774,7 +1816,7 @@ impl PlayerInternal {
|
||||||
|
|
||||||
fn handle_command_load(
|
fn handle_command_load(
|
||||||
&mut self,
|
&mut self,
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyUri,
|
||||||
play_request_id_option: Option<u64>,
|
play_request_id_option: Option<u64>,
|
||||||
play: bool,
|
play: bool,
|
||||||
position_ms: u32,
|
position_ms: u32,
|
||||||
|
@ -1803,9 +1845,9 @@ impl PlayerInternal {
|
||||||
if let PlayerState::EndOfTrack {
|
if let PlayerState::EndOfTrack {
|
||||||
track_id: previous_track_id,
|
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) {
|
let mut loaded_track = match mem::replace(&mut self.state, PlayerState::Invalid) {
|
||||||
PlayerState::EndOfTrack { loaded_track, .. } => loaded_track,
|
PlayerState::EndOfTrack { loaded_track, .. } => loaded_track,
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -1834,19 +1876,19 @@ impl PlayerInternal {
|
||||||
|
|
||||||
// Check if we are already playing the track. If so, just do a seek and update our info.
|
// Check if we are already playing the track. If so, just do a seek and update our info.
|
||||||
if let PlayerState::Playing {
|
if let PlayerState::Playing {
|
||||||
track_id: current_track_id,
|
track_id: ref current_track_id,
|
||||||
ref mut stream_position_ms,
|
ref mut stream_position_ms,
|
||||||
ref mut decoder,
|
ref mut decoder,
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
| PlayerState::Paused {
|
| PlayerState::Paused {
|
||||||
track_id: current_track_id,
|
track_id: ref current_track_id,
|
||||||
ref mut stream_position_ms,
|
ref mut stream_position_ms,
|
||||||
ref mut decoder,
|
ref mut decoder,
|
||||||
..
|
..
|
||||||
} = self.state
|
} = 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.
|
// we can use the current decoder. Ensure it's at the correct position.
|
||||||
if position_ms != *stream_position_ms {
|
if position_ms != *stream_position_ms {
|
||||||
// This may be blocking.
|
// This may be blocking.
|
||||||
|
@ -1915,9 +1957,9 @@ impl PlayerInternal {
|
||||||
if let PlayerPreload::Ready {
|
if let PlayerPreload::Ready {
|
||||||
track_id: loaded_track_id,
|
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);
|
let preload = std::mem::replace(&mut self.preload, PlayerPreload::None);
|
||||||
if let PlayerPreload::Ready {
|
if let PlayerPreload::Ready {
|
||||||
track_id,
|
track_id,
|
||||||
|
@ -1940,7 +1982,7 @@ impl PlayerInternal {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.send_event(PlayerEvent::Loading {
|
self.send_event(PlayerEvent::Loading {
|
||||||
track_id,
|
track_id: track_id.clone(),
|
||||||
play_request_id,
|
play_request_id,
|
||||||
position_ms,
|
position_ms,
|
||||||
});
|
});
|
||||||
|
@ -1949,9 +1991,9 @@ impl PlayerInternal {
|
||||||
let loader = if let PlayerPreload::Loading {
|
let loader = if let PlayerPreload::Loading {
|
||||||
track_id: loaded_track_id,
|
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;
|
let mut preload = PlayerPreload::None;
|
||||||
std::mem::swap(&mut preload, &mut self.preload);
|
std::mem::swap(&mut preload, &mut self.preload);
|
||||||
if let PlayerPreload::Loading { loader, .. } = preload {
|
if let PlayerPreload::Loading { loader, .. } = preload {
|
||||||
|
@ -1969,7 +2011,8 @@ impl PlayerInternal {
|
||||||
self.preload = PlayerPreload::None;
|
self.preload = PlayerPreload::None;
|
||||||
|
|
||||||
// If we don't have a loader yet, create one from scratch.
|
// 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.
|
// Set ourselves to a loading state.
|
||||||
self.state = PlayerState::Loading {
|
self.state = PlayerState::Loading {
|
||||||
|
@ -1982,7 +2025,7 @@ impl PlayerInternal {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_command_preload(&mut self, track_id: SpotifyId) {
|
fn handle_command_preload(&mut self, track_id: SpotifyUri) {
|
||||||
debug!("Preloading track");
|
debug!("Preloading track");
|
||||||
let mut preload_track = true;
|
let mut preload_track = true;
|
||||||
// check whether the track is already loaded somewhere or being loaded.
|
// check whether the track is already loaded somewhere or being loaded.
|
||||||
|
@ -1993,9 +2036,9 @@ impl PlayerInternal {
|
||||||
| PlayerPreload::Ready {
|
| PlayerPreload::Ready {
|
||||||
track_id: currently_loading,
|
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.
|
// we're already preloading the requested track.
|
||||||
preload_track = false;
|
preload_track = false;
|
||||||
} else {
|
} else {
|
||||||
|
@ -2015,9 +2058,9 @@ impl PlayerInternal {
|
||||||
| PlayerState::EndOfTrack {
|
| PlayerState::EndOfTrack {
|
||||||
track_id: current_track_id,
|
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.
|
// we already have the requested track loaded.
|
||||||
preload_track = false;
|
preload_track = false;
|
||||||
}
|
}
|
||||||
|
@ -2025,7 +2068,7 @@ impl PlayerInternal {
|
||||||
|
|
||||||
// schedule the preload of the current track if desired.
|
// schedule the preload of the current track if desired.
|
||||||
if preload_track {
|
if preload_track {
|
||||||
let loader = self.load_track(track_id, 0);
|
let loader = self.load_track(track_id.clone(), 0);
|
||||||
self.preload = PlayerPreload::Loading {
|
self.preload = PlayerPreload::Loading {
|
||||||
track_id,
|
track_id,
|
||||||
loader: Box::pin(loader),
|
loader: Box::pin(loader),
|
||||||
|
@ -2039,14 +2082,14 @@ impl PlayerInternal {
|
||||||
// that. In this case just restart the loading process but
|
// that. In this case just restart the loading process but
|
||||||
// with the requested position.
|
// with the requested position.
|
||||||
if let PlayerState::Loading {
|
if let PlayerState::Loading {
|
||||||
track_id,
|
ref track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
start_playback,
|
start_playback,
|
||||||
..
|
..
|
||||||
} = self.state
|
} = self.state
|
||||||
{
|
{
|
||||||
return self.handle_command_load(
|
return self.handle_command_load(
|
||||||
track_id,
|
track_id.clone(),
|
||||||
Some(play_request_id),
|
Some(play_request_id),
|
||||||
start_playback,
|
start_playback,
|
||||||
position_ms,
|
position_ms,
|
||||||
|
@ -2058,13 +2101,13 @@ impl PlayerInternal {
|
||||||
Ok(new_position_ms) => {
|
Ok(new_position_ms) => {
|
||||||
if let PlayerState::Playing {
|
if let PlayerState::Playing {
|
||||||
ref mut stream_position_ms,
|
ref mut stream_position_ms,
|
||||||
track_id,
|
ref track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
| PlayerState::Paused {
|
| PlayerState::Paused {
|
||||||
ref mut stream_position_ms,
|
ref mut stream_position_ms,
|
||||||
track_id,
|
ref track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
..
|
..
|
||||||
} = self.state
|
} = self.state
|
||||||
|
@ -2073,7 +2116,7 @@ impl PlayerInternal {
|
||||||
|
|
||||||
self.send_event(PlayerEvent::Seeked {
|
self.send_event(PlayerEvent::Seeked {
|
||||||
play_request_id,
|
play_request_id,
|
||||||
track_id,
|
track_id: track_id.clone(),
|
||||||
position_ms: new_position_ms,
|
position_ms: new_position_ms,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2177,18 +2220,20 @@ impl PlayerInternal {
|
||||||
|
|
||||||
if filter {
|
if filter {
|
||||||
if let PlayerState::Playing {
|
if let PlayerState::Playing {
|
||||||
track_id,
|
ref track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
is_explicit,
|
is_explicit,
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
| PlayerState::Paused {
|
| PlayerState::Paused {
|
||||||
track_id,
|
ref track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
is_explicit,
|
is_explicit,
|
||||||
..
|
..
|
||||||
} = self.state
|
} = self.state
|
||||||
{
|
{
|
||||||
|
let track_id = track_id.clone();
|
||||||
|
|
||||||
if is_explicit {
|
if is_explicit {
|
||||||
warn!(
|
warn!(
|
||||||
"Currently loaded track is explicit, which client setting forbids -- skipping to next track."
|
"Currently loaded track is explicit, which client setting forbids -- skipping to next track."
|
||||||
|
@ -2213,7 +2258,7 @@ impl PlayerInternal {
|
||||||
|
|
||||||
fn load_track(
|
fn load_track(
|
||||||
&mut self,
|
&mut self,
|
||||||
spotify_id: SpotifyId,
|
spotify_uri: SpotifyUri,
|
||||||
position_ms: u32,
|
position_ms: u32,
|
||||||
) -> impl FusedFuture<Output = Result<PlayerLoadedTrackData, ()>> + Send + 'static {
|
) -> impl FusedFuture<Output = Result<PlayerLoadedTrackData, ()>> + Send + 'static {
|
||||||
// This method creates a future that returns the loaded stream and associated info.
|
// This method creates a future that returns the loaded stream and associated info.
|
||||||
|
@ -2231,17 +2276,18 @@ impl PlayerInternal {
|
||||||
|
|
||||||
let load_handles_clone = self.load_handles.clone();
|
let load_handles_clone = self.load_handles.clone();
|
||||||
let handle = tokio::runtime::Handle::current();
|
let handle = tokio::runtime::Handle::current();
|
||||||
|
|
||||||
let load_handle = thread::spawn(move || {
|
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 {
|
if let Some(data) = data {
|
||||||
let _ = result_tx.send(data);
|
let _ = result_tx.send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut load_handles = load_handles_clone.lock();
|
let mut load_handles = load_handles_clone.lock().expect(LOAD_HANDLES_POISON_MSG);
|
||||||
load_handles.remove(&thread::current().id());
|
load_handles.remove(&thread::current().id());
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut load_handles = self.load_handles.lock();
|
let mut load_handles = self.load_handles.lock().expect(LOAD_HANDLES_POISON_MSG);
|
||||||
load_handles.insert(load_handle.thread().id(), load_handle);
|
load_handles.insert(load_handle.thread().id(), load_handle);
|
||||||
|
|
||||||
result_rx.map_err(|_| ())
|
result_rx.map_err(|_| ())
|
||||||
|
@ -2276,7 +2322,7 @@ impl Drop for PlayerInternal {
|
||||||
|
|
||||||
let handles: Vec<thread::JoinHandle<()>> = {
|
let handles: Vec<thread::JoinHandle<()>> = {
|
||||||
// waiting for the thread while holding the mutex would result in a deadlock
|
// waiting for the thread while holding the mutex would result in a deadlock
|
||||||
let mut load_handles = self.load_handles.lock();
|
let mut load_handles = self.load_handles.lock().expect(LOAD_HANDLES_POISON_MSG);
|
||||||
|
|
||||||
load_handles
|
load_handles
|
||||||
.drain()
|
.drain()
|
||||||
|
@ -2376,7 +2422,7 @@ impl fmt::Debug for PlayerCommand {
|
||||||
impl fmt::Debug for PlayerState {
|
impl fmt::Debug for PlayerState {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
use PlayerState::*;
|
use PlayerState::*;
|
||||||
match *self {
|
match self {
|
||||||
Stopped => f.debug_struct("Stopped").finish(),
|
Stopped => f.debug_struct("Stopped").finish(),
|
||||||
Loading {
|
Loading {
|
||||||
track_id,
|
track_id,
|
||||||
|
|
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[toolchain]
|
||||||
|
components = ["rustfmt", "clippy"]
|
|
@ -28,7 +28,7 @@ impl EventHandler {
|
||||||
env_vars.insert("PLAY_REQUEST_ID", play_request_id.to_string());
|
env_vars.insert("PLAY_REQUEST_ID", play_request_id.to_string());
|
||||||
}
|
}
|
||||||
PlayerEvent::TrackChanged { audio_item } => {
|
PlayerEvent::TrackChanged { audio_item } => {
|
||||||
match audio_item.track_id.to_base62() {
|
match audio_item.track_id.to_id() {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("PlayerEvent::TrackChanged: Invalid track id: {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}"),
|
Err(e) => warn!("PlayerEvent::Stopped: Invalid track id: {e}"),
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
env_vars.insert("PLAYER_EVENT", "stopped".to_string());
|
env_vars.insert("PLAYER_EVENT", "stopped".to_string());
|
||||||
|
@ -115,7 +115,7 @@ impl EventHandler {
|
||||||
track_id,
|
track_id,
|
||||||
position_ms,
|
position_ms,
|
||||||
..
|
..
|
||||||
} => match track_id.to_base62() {
|
} => match track_id.to_id() {
|
||||||
Err(e) => warn!("PlayerEvent::Playing: Invalid track id: {e}"),
|
Err(e) => warn!("PlayerEvent::Playing: Invalid track id: {e}"),
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
env_vars.insert("PLAYER_EVENT", "playing".to_string());
|
env_vars.insert("PLAYER_EVENT", "playing".to_string());
|
||||||
|
@ -127,7 +127,7 @@ impl EventHandler {
|
||||||
track_id,
|
track_id,
|
||||||
position_ms,
|
position_ms,
|
||||||
..
|
..
|
||||||
} => match track_id.to_base62() {
|
} => match track_id.to_id() {
|
||||||
Err(e) => warn!("PlayerEvent::Paused: Invalid track id: {e}"),
|
Err(e) => warn!("PlayerEvent::Paused: Invalid track id: {e}"),
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
env_vars.insert("PLAYER_EVENT", "paused".to_string());
|
env_vars.insert("PLAYER_EVENT", "paused".to_string());
|
||||||
|
@ -135,26 +135,24 @@ impl EventHandler {
|
||||||
env_vars.insert("POSITION_MS", position_ms.to_string());
|
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}"),
|
Err(e) => warn!("PlayerEvent::Loading: Invalid track id: {e}"),
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
env_vars.insert("PLAYER_EVENT", "loading".to_string());
|
env_vars.insert("PLAYER_EVENT", "loading".to_string());
|
||||||
env_vars.insert("TRACK_ID", id);
|
env_vars.insert("TRACK_ID", id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
PlayerEvent::Preloading { track_id, .. } => {
|
PlayerEvent::Preloading { track_id, .. } => match track_id.to_id() {
|
||||||
match track_id.to_base62() {
|
Err(e) => {
|
||||||
Err(e) => {
|
warn!("PlayerEvent::Preloading: Invalid track id: {e}")
|
||||||
warn!("PlayerEvent::Preloading: Invalid track id: {e}")
|
|
||||||
}
|
|
||||||
Ok(id) => {
|
|
||||||
env_vars.insert("PLAYER_EVENT", "preloading".to_string());
|
|
||||||
env_vars.insert("TRACK_ID", id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
Ok(id) => {
|
||||||
|
env_vars.insert("PLAYER_EVENT", "preloading".to_string());
|
||||||
|
env_vars.insert("TRACK_ID", id);
|
||||||
|
}
|
||||||
|
},
|
||||||
PlayerEvent::TimeToPreloadNextTrack { track_id, .. } => {
|
PlayerEvent::TimeToPreloadNextTrack { track_id, .. } => {
|
||||||
match track_id.to_base62() {
|
match track_id.to_id() {
|
||||||
Err(e) => warn!(
|
Err(e) => warn!(
|
||||||
"PlayerEvent::TimeToPreloadNextTrack: Invalid track id: {e}"
|
"PlayerEvent::TimeToPreloadNextTrack: Invalid track id: {e}"
|
||||||
),
|
),
|
||||||
|
@ -164,19 +162,16 @@ impl EventHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PlayerEvent::EndOfTrack { track_id, .. } => {
|
PlayerEvent::EndOfTrack { track_id, .. } => match track_id.to_id() {
|
||||||
match track_id.to_base62() {
|
Err(e) => {
|
||||||
Err(e) => {
|
warn!("PlayerEvent::EndOfTrack: Invalid track id: {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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
Ok(id) => {
|
||||||
PlayerEvent::Unavailable { track_id, .. } => match track_id.to_base62()
|
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}"),
|
Err(e) => warn!("PlayerEvent::Unavailable: Invalid track id: {e}"),
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
env_vars.insert("PLAYER_EVENT", "unavailable".to_string());
|
env_vars.insert("PLAYER_EVENT", "unavailable".to_string());
|
||||||
|
@ -191,7 +186,7 @@ impl EventHandler {
|
||||||
track_id,
|
track_id,
|
||||||
position_ms,
|
position_ms,
|
||||||
..
|
..
|
||||||
} => match track_id.to_base62() {
|
} => match track_id.to_id() {
|
||||||
Err(e) => warn!("PlayerEvent::Seeked: Invalid track id: {e}"),
|
Err(e) => warn!("PlayerEvent::Seeked: Invalid track id: {e}"),
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
env_vars.insert("PLAYER_EVENT", "seeked".to_string());
|
env_vars.insert("PLAYER_EVENT", "seeked".to_string());
|
||||||
|
@ -203,7 +198,7 @@ impl EventHandler {
|
||||||
track_id,
|
track_id,
|
||||||
position_ms,
|
position_ms,
|
||||||
..
|
..
|
||||||
} => match track_id.to_base62() {
|
} => match track_id.to_id() {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("PlayerEvent::PositionCorrection: Invalid track id: {e}")
|
warn!("PlayerEvent::PositionCorrection: Invalid track id: {e}")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue