mirror of
https://github.com/librespot-org/librespot.git
synced 2025-10-03 01:39:28 +02:00
Add rustdoc to connect crate (#1455)
* restructure connect and add initial docs * replace inline crate rustdoc with README.md * connect: make metadata trait less visible * connect: add some more docs * chore: remove clippy warning * update CHANGELOG.md * revert unrelated changes * enforce separation of autoplay and options * hide and improve docs of uid * remove/rename remarks * update connect example and link in docs * fixup rebase and clippy warnings
This commit is contained in:
parent
f497806fb1
commit
09b4aa41e5
14 changed files with 429 additions and 209 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,5 +4,6 @@ spotify_appkey.key
|
||||||
.vagrant/
|
.vagrant/
|
||||||
.project
|
.project
|
||||||
.history
|
.history
|
||||||
|
.cache
|
||||||
*.save
|
*.save
|
||||||
*.*~
|
*.*~
|
||||||
|
|
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -11,16 +11,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
- [core] MSRV is now 1.81 (breaking)
|
- [core] MSRV is now 1.81 (breaking)
|
||||||
- [core] AP connect and handshake have a combined 5 second timeout.
|
- [core] AP connect and handshake have a combined 5 second timeout.
|
||||||
- [connect] Replaced `ConnectConfig` with `ConnectStateConfig` (breaking)
|
- [connect] Replaced `has_volume_ctrl` with `disable_volume` in `ConnectConfig` (breaking)
|
||||||
- [connect] Replaced `playing_track_index` field of `SpircLoadCommand` with `playing_track` (breaking)
|
- [connect] Changed `initial_volume` from `Option<u16>` to `u16` in `ConnectConfig` (breaking)
|
||||||
|
- [connect] Replaced `SpircLoadCommand` with `LoadRequest`, `LoadRequestOptions` and `LoadContextOptions` (breaking)
|
||||||
|
- [connect] Moved all public items to the highest level (breaking)
|
||||||
- [connect] Replaced Mercury usage in `Spirc` with Dealer
|
- [connect] Replaced Mercury usage in `Spirc` with Dealer
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- [connect] Add `seek_to` field to `SpircLoadCommand` (breaking)
|
- [connect] Add support for `seek_to`, `repeat_track` and `autoplay` for `Spirc` loading
|
||||||
- [connect] Add `repeat_track` field to `SpircLoadCommand` (breaking)
|
|
||||||
- [connect] Add `autoplay` field to `SpircLoadCommand` (breaking)
|
|
||||||
- [connect] Add `pause` parameter to `Spirc::disconnect` method (breaking)
|
- [connect] Add `pause` parameter to `Spirc::disconnect` method (breaking)
|
||||||
|
- [connect] Add `volume_steps` to `ConnectConfig` (breaking)
|
||||||
|
- [connect] Add and enforce rustdoc
|
||||||
- [playback] Add `track` field to `PlayerEvent::RepeatChanged` (breaking)
|
- [playback] Add `track` field to `PlayerEvent::RepeatChanged` (breaking)
|
||||||
- [core] Add `request_with_options` and `request_with_protobuf_and_options` to `SpClient`
|
- [core] Add `request_with_options` and `request_with_protobuf_and_options` to `SpClient`
|
||||||
- [oauth] Add `OAuthClient` and `OAuthClientBuilder` structs to achieve a more customizable login process
|
- [oauth] Add `OAuthClient` and `OAuthClientBuilder` structs to achieve a more customizable login process
|
||||||
|
|
63
connect/README.md
Normal file
63
connect/README.md
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
[//]: # (This readme is optimized for inline rustdoc, if some links don't work, they will when included in lib.rs)
|
||||||
|
|
||||||
|
# Connect
|
||||||
|
|
||||||
|
The connect module of librespot. Provides the option to create your own connect device
|
||||||
|
and stream to it like any other official spotify client.
|
||||||
|
|
||||||
|
The [`Spirc`] is the entrypoint to creating your own connect device. It can be
|
||||||
|
configured with the given [`ConnectConfig`] options and requires some additional data
|
||||||
|
to start up the device.
|
||||||
|
|
||||||
|
When creating a new [`Spirc`] it returns two items. The [`Spirc`] itself, which is can
|
||||||
|
be used as to control the local connect device. And a [`Future`](std::future::Future),
|
||||||
|
lets name it `SpircTask`, that starts and executes the event loop of the connect device
|
||||||
|
when awaited.
|
||||||
|
|
||||||
|
A basic example in which the `Spirc` and `SpircTask` is used can be found here:
|
||||||
|
[`examples/play_connect.rs`](../examples/play_connect.rs).
|
||||||
|
|
||||||
|
# Example
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use std::{future::Future, thread};
|
||||||
|
|
||||||
|
use librespot_connect::{ConnectConfig, Spirc};
|
||||||
|
use librespot_core::{authentication::Credentials, Error, Session, SessionConfig};
|
||||||
|
use librespot_playback::{
|
||||||
|
audio_backend, mixer,
|
||||||
|
config::{AudioFormat, PlayerConfig},
|
||||||
|
mixer::{MixerConfig, NoOpVolume},
|
||||||
|
player::Player
|
||||||
|
};
|
||||||
|
|
||||||
|
async fn create_basic_spirc() -> Result<(), Error> {
|
||||||
|
let credentials = Credentials::with_access_token("access-token-here");
|
||||||
|
let session = Session::new(SessionConfig::default(), None);
|
||||||
|
|
||||||
|
let backend = audio_backend::find(None).expect("will default to rodio");
|
||||||
|
|
||||||
|
let player = Player::new(
|
||||||
|
PlayerConfig::default(),
|
||||||
|
session.clone(),
|
||||||
|
Box::new(NoOpVolume),
|
||||||
|
move || {
|
||||||
|
let format = AudioFormat::default();
|
||||||
|
let device = None;
|
||||||
|
backend(device, format)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let mixer = mixer::find(None).expect("will default to SoftMixer");
|
||||||
|
|
||||||
|
let (spirc, spirc_task): (Spirc, _) = Spirc::new(
|
||||||
|
ConnectConfig::default(),
|
||||||
|
session,
|
||||||
|
credentials,
|
||||||
|
player,
|
||||||
|
mixer(MixerConfig::default())
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
|
@ -1,3 +1,6 @@
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
#![doc=include_str!("../README.md")]
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
|
||||||
|
@ -7,6 +10,10 @@ use librespot_protocol as protocol;
|
||||||
|
|
||||||
mod context_resolver;
|
mod context_resolver;
|
||||||
mod model;
|
mod model;
|
||||||
pub mod shuffle_vec;
|
mod shuffle_vec;
|
||||||
pub mod spirc;
|
mod spirc;
|
||||||
pub mod state;
|
mod state;
|
||||||
|
|
||||||
|
pub use model::*;
|
||||||
|
pub use spirc::*;
|
||||||
|
pub use state::*;
|
||||||
|
|
|
@ -1,30 +1,111 @@
|
||||||
use librespot_core::dealer::protocol::SkipTo;
|
use crate::{
|
||||||
|
core::dealer::protocol::SkipTo, protocol::context_player_options::ContextPlayerOptionOverrides,
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
/// Request for loading playback
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SpircLoadCommand {
|
pub struct LoadRequest {
|
||||||
pub context_uri: String,
|
pub(super) context_uri: String,
|
||||||
|
pub(super) options: LoadRequestOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for LoadRequest {
|
||||||
|
type Target = LoadRequestOptions;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The parameters for creating a load request
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct LoadRequestOptions {
|
||||||
/// Whether the given tracks should immediately start playing, or just be initially loaded.
|
/// Whether the given tracks should immediately start playing, or just be initially loaded.
|
||||||
pub start_playing: bool,
|
pub start_playing: bool,
|
||||||
|
/// Start the playback at a specific point of the track.
|
||||||
|
///
|
||||||
|
/// The provided value is used as milliseconds. Providing a value greater
|
||||||
|
/// than the track duration will start the track at the beginning.
|
||||||
pub seek_to: u32,
|
pub seek_to: u32,
|
||||||
pub shuffle: bool,
|
/// Options that decide how the context starts playing
|
||||||
pub repeat: bool,
|
pub context_options: Option<LoadContextOptions>,
|
||||||
pub repeat_track: bool,
|
/// Decides the starting position in the given context.
|
||||||
/// Decides if the context or the autoplay of the context is played
|
|
||||||
///
|
///
|
||||||
/// ## Remarks:
|
/// If the provided item doesn't exist or is out of range,
|
||||||
/// If `true` is provided, the option values (`shuffle`, `repeat` and `repeat_track`) are ignored
|
/// the playback starts at the beginning of the context.
|
||||||
pub autoplay: bool,
|
|
||||||
/// Decides the starting position in the given context
|
|
||||||
///
|
///
|
||||||
/// ## Remarks:
|
|
||||||
/// If `None` is provided and `shuffle` is `true`, a random track is played, otherwise the first
|
/// If `None` is provided and `shuffle` is `true`, a random track is played, otherwise the first
|
||||||
pub playing_track: Option<PlayingTrack>,
|
pub playing_track: Option<PlayingTrack>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The options which decide how the playback is started
|
||||||
|
///
|
||||||
|
/// Separated into an `enum` to exclude the other variants from being used
|
||||||
|
/// simultaneously, as they are not compatible.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum LoadContextOptions {
|
||||||
|
/// Starts the context with options
|
||||||
|
Options(Options),
|
||||||
|
/// Starts the playback as the autoplay variant of the context
|
||||||
|
///
|
||||||
|
/// This is the same as finishing a context and
|
||||||
|
/// automatically continuing playback of similar tracks
|
||||||
|
Autoplay,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The available options that indicate how to start the context
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Options {
|
||||||
|
/// Start the context in shuffle mode
|
||||||
|
pub shuffle: bool,
|
||||||
|
/// Start the context in repeat mode
|
||||||
|
pub repeat: bool,
|
||||||
|
/// Start the context, repeating the first track until skipped or manually disabled
|
||||||
|
pub repeat_track: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ContextPlayerOptionOverrides> for Options {
|
||||||
|
fn from(value: ContextPlayerOptionOverrides) -> Self {
|
||||||
|
Self {
|
||||||
|
shuffle: value.shuffling_context.unwrap_or_default(),
|
||||||
|
repeat: value.repeating_context.unwrap_or_default(),
|
||||||
|
repeat_track: value.repeating_track.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoadRequest {
|
||||||
|
/// Create a load request from a `context_uri`
|
||||||
|
///
|
||||||
|
/// For supported `context_uri` see [`SpClient::get_context`](librespot_core::spclient::SpClient::get_context)
|
||||||
|
pub fn from_context_uri(context_uri: String, options: LoadRequestOptions) -> Self {
|
||||||
|
Self {
|
||||||
|
context_uri,
|
||||||
|
options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An item that represent a track to play
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PlayingTrack {
|
pub enum PlayingTrack {
|
||||||
|
/// Represent the track at a given index.
|
||||||
Index(u32),
|
Index(u32),
|
||||||
|
/// Represent the uri of a track.
|
||||||
Uri(String),
|
Uri(String),
|
||||||
|
#[doc(hidden)]
|
||||||
|
/// Represent an internal identifier from spotify.
|
||||||
|
///
|
||||||
|
/// The internal identifier is not the id contained in the uri. And rather
|
||||||
|
/// an unrelated id probably unique in spotify's internal database. But that's
|
||||||
|
/// just speculation.
|
||||||
|
///
|
||||||
|
/// This identifier is not available by any public api. It's used for varies in
|
||||||
|
/// any spotify client, like sorting, displaying which track is currently played
|
||||||
|
/// and skipping to a track. Mobile uses it pretty intensively but also web and
|
||||||
|
/// desktop seem to make use of it.
|
||||||
Uid(String),
|
Uid(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,13 +46,6 @@ impl<T> From<Vec<T>> for ShuffleVec<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> ShuffleVec<T> {
|
impl<T> ShuffleVec<T> {
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
vec: Vec::new(),
|
|
||||||
indices: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn shuffle_with_seed(&mut self, seed: u64) {
|
pub fn shuffle_with_seed(&mut self, seed: u64) {
|
||||||
self.shuffle_with_rng(SmallRng::seed_from_u64(seed))
|
self.shuffle_with_rng(SmallRng::seed_from_u64(seed))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
pub use crate::model::{PlayingTrack, SpircLoadCommand};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
context_resolver::{ContextAction, ContextResolver, ResolveContext},
|
context_resolver::{ContextAction, ContextResolver, ResolveContext},
|
||||||
core::{
|
core::{
|
||||||
|
@ -10,7 +9,7 @@ use crate::{
|
||||||
session::UserAttributes,
|
session::UserAttributes,
|
||||||
Error, Session, SpotifyId,
|
Error, Session, SpotifyId,
|
||||||
},
|
},
|
||||||
model::SpircPlayStatus,
|
model::{LoadRequest, PlayingTrack, SpircPlayStatus},
|
||||||
playback::{
|
playback::{
|
||||||
mixer::Mixer,
|
mixer::Mixer,
|
||||||
player::{Player, PlayerEvent, PlayerEventChannel},
|
player::{Player, PlayerEvent, PlayerEventChannel},
|
||||||
|
@ -26,10 +25,10 @@ use crate::{
|
||||||
},
|
},
|
||||||
state::{
|
state::{
|
||||||
context::{ContextType, ResetContext},
|
context::{ContextType, ResetContext},
|
||||||
metadata::Metadata,
|
|
||||||
provider::IsProvider,
|
provider::IsProvider,
|
||||||
{ConnectState, ConnectStateConfig},
|
{ConnectConfig, ConnectState},
|
||||||
},
|
},
|
||||||
|
LoadContextOptions, LoadRequestOptions,
|
||||||
};
|
};
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use protobuf::MessageField;
|
use protobuf::MessageField;
|
||||||
|
@ -43,15 +42,13 @@ use thiserror::Error;
|
||||||
use tokio::{sync::mpsc, time::sleep};
|
use tokio::{sync::mpsc, time::sleep};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum SpircError {
|
enum SpircError {
|
||||||
#[error("response payload empty")]
|
#[error("response payload empty")]
|
||||||
NoData,
|
NoData,
|
||||||
#[error("{0} had no uri")]
|
#[error("{0} had no uri")]
|
||||||
NoUri(&'static str),
|
NoUri(&'static str),
|
||||||
#[error("message pushed for another URI")]
|
#[error("message pushed for another URI")]
|
||||||
InvalidUri(String),
|
InvalidUri(String),
|
||||||
#[error("tried resolving not allowed context: {0:?}")]
|
|
||||||
NotAllowedContext(String),
|
|
||||||
#[error("failed to put connect state for new device")]
|
#[error("failed to put connect state for new device")]
|
||||||
FailedDealerSetup,
|
FailedDealerSetup,
|
||||||
#[error("unknown endpoint: {0:#?}")]
|
#[error("unknown endpoint: {0:#?}")]
|
||||||
|
@ -62,7 +59,7 @@ impl From<SpircError> for Error {
|
||||||
fn from(err: SpircError) -> Self {
|
fn from(err: SpircError) -> Self {
|
||||||
use SpircError::*;
|
use SpircError::*;
|
||||||
match err {
|
match err {
|
||||||
NoData | NoUri(_) | NotAllowedContext(_) => Error::unavailable(err),
|
NoData | NoUri(_) => Error::unavailable(err),
|
||||||
InvalidUri(_) | FailedDealerSetup => Error::aborted(err),
|
InvalidUri(_) | FailedDealerSetup => Error::aborted(err),
|
||||||
UnknownEndpoint(_) => Error::unimplemented(err),
|
UnknownEndpoint(_) => Error::unimplemented(err),
|
||||||
}
|
}
|
||||||
|
@ -130,25 +127,30 @@ enum SpircCommand {
|
||||||
SetPosition(u32),
|
SetPosition(u32),
|
||||||
SetVolume(u16),
|
SetVolume(u16),
|
||||||
Activate,
|
Activate,
|
||||||
Load(SpircLoadCommand),
|
Load(LoadRequest),
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONTEXT_FETCH_THRESHOLD: usize = 2;
|
const CONTEXT_FETCH_THRESHOLD: usize = 2;
|
||||||
|
|
||||||
const VOLUME_STEP_SIZE: u16 = 1024; // (u16::MAX + 1) / VOLUME_STEPS
|
|
||||||
|
|
||||||
// delay to update volume after a certain amount of time, instead on each update request
|
// delay to update volume after a certain amount of time, instead on each update request
|
||||||
const VOLUME_UPDATE_DELAY: Duration = Duration::from_secs(2);
|
const VOLUME_UPDATE_DELAY: Duration = Duration::from_secs(2);
|
||||||
// to reduce updates to remote, we group some request by waiting for a set amount of time
|
// to reduce updates to remote, we group some request by waiting for a set amount of time
|
||||||
const UPDATE_STATE_DELAY: Duration = Duration::from_millis(200);
|
const UPDATE_STATE_DELAY: Duration = Duration::from_millis(200);
|
||||||
|
|
||||||
|
/// The spotify connect handle
|
||||||
pub struct Spirc {
|
pub struct Spirc {
|
||||||
commands: mpsc::UnboundedSender<SpircCommand>,
|
commands: mpsc::UnboundedSender<SpircCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Spirc {
|
impl Spirc {
|
||||||
|
/// Initializes a new spotify connect device
|
||||||
|
///
|
||||||
|
/// The returned tuple consists out of a handle to the [`Spirc`] that
|
||||||
|
/// can control the local connect device when active. And a [`Future`]
|
||||||
|
/// which represents the [`Spirc`] event loop that processes the whole
|
||||||
|
/// connect device logic.
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
config: ConnectStateConfig,
|
config: ConnectConfig,
|
||||||
session: Session,
|
session: Session,
|
||||||
credentials: Credentials,
|
credentials: Credentials,
|
||||||
player: Arc<Player>,
|
player: Arc<Player>,
|
||||||
|
@ -268,54 +270,132 @@ impl Spirc {
|
||||||
Ok((spirc, task.run()))
|
Ok((spirc, task.run()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn play(&self) -> Result<(), Error> {
|
/// Safely shutdowns the spirc.
|
||||||
Ok(self.commands.send(SpircCommand::Play)?)
|
///
|
||||||
}
|
/// This pauses the playback, disconnects the connect device and
|
||||||
pub fn play_pause(&self) -> Result<(), Error> {
|
/// bring the future initially returned to an end.
|
||||||
Ok(self.commands.send(SpircCommand::PlayPause)?)
|
|
||||||
}
|
|
||||||
pub fn pause(&self) -> Result<(), Error> {
|
|
||||||
Ok(self.commands.send(SpircCommand::Pause)?)
|
|
||||||
}
|
|
||||||
pub fn prev(&self) -> Result<(), Error> {
|
|
||||||
Ok(self.commands.send(SpircCommand::Prev)?)
|
|
||||||
}
|
|
||||||
pub fn next(&self) -> Result<(), Error> {
|
|
||||||
Ok(self.commands.send(SpircCommand::Next)?)
|
|
||||||
}
|
|
||||||
pub fn volume_up(&self) -> Result<(), Error> {
|
|
||||||
Ok(self.commands.send(SpircCommand::VolumeUp)?)
|
|
||||||
}
|
|
||||||
pub fn volume_down(&self) -> Result<(), Error> {
|
|
||||||
Ok(self.commands.send(SpircCommand::VolumeDown)?)
|
|
||||||
}
|
|
||||||
pub fn shutdown(&self) -> Result<(), Error> {
|
pub fn shutdown(&self) -> Result<(), Error> {
|
||||||
Ok(self.commands.send(SpircCommand::Shutdown)?)
|
Ok(self.commands.send(SpircCommand::Shutdown)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resumes the playback
|
||||||
|
///
|
||||||
|
/// Does nothing if we are not the active device, or it isn't paused.
|
||||||
|
pub fn play(&self) -> Result<(), Error> {
|
||||||
|
Ok(self.commands.send(SpircCommand::Play)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resumes or pauses the playback
|
||||||
|
///
|
||||||
|
/// Does nothing if we are not the active device.
|
||||||
|
pub fn play_pause(&self) -> Result<(), Error> {
|
||||||
|
Ok(self.commands.send(SpircCommand::PlayPause)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pauses the playback
|
||||||
|
///
|
||||||
|
/// Does nothing if we are not the active device, or if it isn't playing.
|
||||||
|
pub fn pause(&self) -> Result<(), Error> {
|
||||||
|
Ok(self.commands.send(SpircCommand::Pause)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Seeks to the beginning or skips to the previous track.
|
||||||
|
///
|
||||||
|
/// Seeks to the beginning when the current track position
|
||||||
|
/// is greater than 3 seconds.
|
||||||
|
///
|
||||||
|
/// Does nothing if we are not the active device.
|
||||||
|
pub fn prev(&self) -> Result<(), Error> {
|
||||||
|
Ok(self.commands.send(SpircCommand::Prev)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skips to the next track.
|
||||||
|
///
|
||||||
|
/// Does nothing if we are not the active device.
|
||||||
|
pub fn next(&self) -> Result<(), Error> {
|
||||||
|
Ok(self.commands.send(SpircCommand::Next)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increases the volume by configured steps of [ConnectConfig].
|
||||||
|
///
|
||||||
|
/// Does nothing if we are not the active device.
|
||||||
|
pub fn volume_up(&self) -> Result<(), Error> {
|
||||||
|
Ok(self.commands.send(SpircCommand::VolumeUp)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decreases the volume by configured steps of [ConnectConfig].
|
||||||
|
///
|
||||||
|
/// Does nothing if we are not the active device.
|
||||||
|
pub fn volume_down(&self) -> Result<(), Error> {
|
||||||
|
Ok(self.commands.send(SpircCommand::VolumeDown)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shuffles the playback according to the value.
|
||||||
|
///
|
||||||
|
/// If true shuffles/reshuffles the playback. Otherwise, does
|
||||||
|
/// nothing (if not shuffled) or unshuffles the playback while
|
||||||
|
/// resuming at the position of the current track.
|
||||||
|
///
|
||||||
|
/// Does nothing if we are not the active device.
|
||||||
pub fn shuffle(&self, shuffle: bool) -> Result<(), Error> {
|
pub fn shuffle(&self, shuffle: bool) -> Result<(), Error> {
|
||||||
Ok(self.commands.send(SpircCommand::Shuffle(shuffle))?)
|
Ok(self.commands.send(SpircCommand::Shuffle(shuffle))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Repeats the playback context according to the value.
|
||||||
|
///
|
||||||
|
/// Does nothing if we are not the active device.
|
||||||
pub fn repeat(&self, repeat: bool) -> Result<(), Error> {
|
pub fn repeat(&self, repeat: bool) -> Result<(), Error> {
|
||||||
Ok(self.commands.send(SpircCommand::Repeat(repeat))?)
|
Ok(self.commands.send(SpircCommand::Repeat(repeat))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Repeats the current track if true.
|
||||||
|
///
|
||||||
|
/// Does nothing if we are not the active device.
|
||||||
|
///
|
||||||
|
/// Skipping to the next track disables the repeating.
|
||||||
pub fn repeat_track(&self, repeat: bool) -> Result<(), Error> {
|
pub fn repeat_track(&self, repeat: bool) -> Result<(), Error> {
|
||||||
Ok(self.commands.send(SpircCommand::RepeatTrack(repeat))?)
|
Ok(self.commands.send(SpircCommand::RepeatTrack(repeat))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the volume to the given value.
|
||||||
|
///
|
||||||
|
/// Does nothing if we are not the active device.
|
||||||
pub fn set_volume(&self, volume: u16) -> Result<(), Error> {
|
pub fn set_volume(&self, volume: u16) -> Result<(), Error> {
|
||||||
Ok(self.commands.send(SpircCommand::SetVolume(volume))?)
|
Ok(self.commands.send(SpircCommand::SetVolume(volume))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates the position to the given value.
|
||||||
|
///
|
||||||
|
/// Does nothing if we are not the active device.
|
||||||
|
///
|
||||||
|
/// If value is greater than the track duration,
|
||||||
|
/// the update is ignored.
|
||||||
pub fn set_position_ms(&self, position_ms: u32) -> Result<(), Error> {
|
pub fn set_position_ms(&self, position_ms: u32) -> Result<(), Error> {
|
||||||
Ok(self.commands.send(SpircCommand::SetPosition(position_ms))?)
|
Ok(self.commands.send(SpircCommand::SetPosition(position_ms))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load a new context and replace the current.
|
||||||
|
///
|
||||||
|
/// Does nothing if we are not the active device.
|
||||||
|
///
|
||||||
|
/// Does not overwrite the queue.
|
||||||
|
pub fn load(&self, command: LoadRequest) -> Result<(), Error> {
|
||||||
|
Ok(self.commands.send(SpircCommand::Load(command))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disconnects the current device and pauses the playback according the value.
|
||||||
|
///
|
||||||
|
/// Does nothing if we are not the active device.
|
||||||
pub fn disconnect(&self, pause: bool) -> Result<(), Error> {
|
pub fn disconnect(&self, pause: bool) -> Result<(), Error> {
|
||||||
Ok(self.commands.send(SpircCommand::Disconnect { pause })?)
|
Ok(self.commands.send(SpircCommand::Disconnect { pause })?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Acquires the control as active connect device.
|
||||||
|
///
|
||||||
|
/// Does nothing if we are not the active device.
|
||||||
pub fn activate(&self) -> Result<(), Error> {
|
pub fn activate(&self) -> Result<(), Error> {
|
||||||
Ok(self.commands.send(SpircCommand::Activate)?)
|
Ok(self.commands.send(SpircCommand::Activate)?)
|
||||||
}
|
}
|
||||||
pub fn load(&self, command: SpircLoadCommand) -> Result<(), Error> {
|
|
||||||
Ok(self.commands.send(SpircCommand::Load(command))?)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpircTask {
|
impl SpircTask {
|
||||||
|
@ -529,6 +609,7 @@ impl SpircTask {
|
||||||
match cmd {
|
match cmd {
|
||||||
SpircCommand::Shutdown => {
|
SpircCommand::Shutdown => {
|
||||||
trace!("Received SpircCommand::Shutdown");
|
trace!("Received SpircCommand::Shutdown");
|
||||||
|
self.handle_pause();
|
||||||
self.handle_disconnect().await?;
|
self.handle_disconnect().await?;
|
||||||
self.shutdown = true;
|
self.shutdown = true;
|
||||||
if let Some(rx) = self.commands.as_mut() {
|
if let Some(rx) = self.commands.as_mut() {
|
||||||
|
@ -895,42 +976,28 @@ impl SpircTask {
|
||||||
return self.notify().await;
|
return self.notify().await;
|
||||||
}
|
}
|
||||||
Play(play) => {
|
Play(play) => {
|
||||||
let shuffle = play
|
|
||||||
.options
|
|
||||||
.player_options_override
|
|
||||||
.as_ref()
|
|
||||||
.map(|o| o.shuffling_context.unwrap_or_default())
|
|
||||||
.unwrap_or_default();
|
|
||||||
let repeat = play
|
|
||||||
.options
|
|
||||||
.player_options_override
|
|
||||||
.as_ref()
|
|
||||||
.map(|o| o.repeating_context.unwrap_or_default())
|
|
||||||
.unwrap_or_default();
|
|
||||||
let repeat_track = play
|
|
||||||
.options
|
|
||||||
.player_options_override
|
|
||||||
.as_ref()
|
|
||||||
.map(|o| o.repeating_track.unwrap_or_default())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let context_uri = play
|
let context_uri = play
|
||||||
.context
|
.context
|
||||||
.uri
|
.uri
|
||||||
.clone()
|
.clone()
|
||||||
.ok_or(SpircError::NoUri("context"))?;
|
.ok_or(SpircError::NoUri("context"))?;
|
||||||
|
|
||||||
|
let context_options = play
|
||||||
|
.options
|
||||||
|
.player_options_override
|
||||||
|
.map(Into::into)
|
||||||
|
.map(LoadContextOptions::Options);
|
||||||
|
|
||||||
self.handle_load(
|
self.handle_load(
|
||||||
SpircLoadCommand {
|
LoadRequest::from_context_uri(
|
||||||
context_uri,
|
context_uri,
|
||||||
start_playing: true,
|
LoadRequestOptions {
|
||||||
seek_to: play.options.seek_to.unwrap_or_default(),
|
start_playing: true,
|
||||||
playing_track: play.options.skip_to.and_then(|s| s.try_into().ok()),
|
seek_to: play.options.seek_to.unwrap_or_default(),
|
||||||
shuffle,
|
playing_track: play.options.skip_to.and_then(|s| s.try_into().ok()),
|
||||||
repeat,
|
context_options,
|
||||||
repeat_track,
|
},
|
||||||
autoplay: false,
|
),
|
||||||
},
|
|
||||||
Some(play.context),
|
Some(play.context),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -991,7 +1058,7 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let autoplay = self.connect_state.current_track(|t| t.is_from_autoplay());
|
let autoplay = self.connect_state.current_track(|t| t.is_autoplay());
|
||||||
if autoplay {
|
if autoplay {
|
||||||
ctx_uri = ctx_uri.replace("station:", "");
|
ctx_uri = ctx_uri.replace("station:", "");
|
||||||
}
|
}
|
||||||
|
@ -1111,7 +1178,7 @@ impl SpircTask {
|
||||||
|
|
||||||
async fn handle_load(
|
async fn handle_load(
|
||||||
&mut self,
|
&mut self,
|
||||||
cmd: SpircLoadCommand,
|
cmd: LoadRequest,
|
||||||
context: Option<Context>,
|
context: Option<Context>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
self.connect_state
|
self.connect_state
|
||||||
|
@ -1132,7 +1199,7 @@ impl SpircTask {
|
||||||
&cmd.context_uri
|
&cmd.context_uri
|
||||||
};
|
};
|
||||||
|
|
||||||
let update_context = if cmd.autoplay {
|
let update_context = if matches!(cmd.context_options, Some(LoadContextOptions::Autoplay)) {
|
||||||
ContextType::Autoplay
|
ContextType::Autoplay
|
||||||
} else {
|
} else {
|
||||||
ContextType::Default
|
ContextType::Default
|
||||||
|
@ -1168,31 +1235,31 @@ impl SpircTask {
|
||||||
|
|
||||||
let index = match cmd.playing_track {
|
let index = match cmd.playing_track {
|
||||||
None => None,
|
None => None,
|
||||||
Some(playing_track) => Some(match playing_track {
|
Some(ref playing_track) => Some(match playing_track {
|
||||||
PlayingTrack::Index(i) => i as usize,
|
PlayingTrack::Index(i) => *i as usize,
|
||||||
PlayingTrack::Uri(uri) => {
|
PlayingTrack::Uri(uri) => {
|
||||||
let ctx = self.connect_state.get_context(ContextType::Default)?;
|
let ctx = self.connect_state.get_context(ContextType::Default)?;
|
||||||
ConnectState::find_index_in_context(ctx, |t| t.uri == uri)?
|
ConnectState::find_index_in_context(ctx, |t| &t.uri == uri)?
|
||||||
}
|
}
|
||||||
PlayingTrack::Uid(uid) => {
|
PlayingTrack::Uid(uid) => {
|
||||||
let ctx = self.connect_state.get_context(ContextType::Default)?;
|
let ctx = self.connect_state.get_context(ContextType::Default)?;
|
||||||
ConnectState::find_index_in_context(ctx, |t| t.uid == uid)?
|
ConnectState::find_index_in_context(ctx, |t| &t.uid == uid)?
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!(
|
if let Some(LoadContextOptions::Options(ref options)) = cmd.context_options {
|
||||||
"loading with shuffle: <{}>, repeat track: <{}> context: <{}>",
|
debug!(
|
||||||
cmd.shuffle, cmd.repeat, cmd.repeat_track
|
"loading with shuffle: <{}>, repeat track: <{}> context: <{}>",
|
||||||
);
|
options.shuffle, options.repeat, options.repeat_track
|
||||||
|
);
|
||||||
|
|
||||||
self.connect_state.set_shuffle(!cmd.autoplay && cmd.shuffle);
|
self.connect_state.set_shuffle(options.shuffle);
|
||||||
self.connect_state
|
self.connect_state.set_repeat_context(options.repeat);
|
||||||
.set_repeat_context(!cmd.autoplay && cmd.repeat);
|
self.connect_state.set_repeat_track(options.repeat_track);
|
||||||
self.connect_state
|
}
|
||||||
.set_repeat_track(!cmd.autoplay && cmd.repeat_track);
|
|
||||||
|
|
||||||
if cmd.shuffle {
|
if matches!(cmd.context_options, Some(LoadContextOptions::Options(ref o)) if o.shuffle) {
|
||||||
if let Some(index) = index {
|
if let Some(index) = index {
|
||||||
self.connect_state.set_current_track(index)?;
|
self.connect_state.set_current_track(index)?;
|
||||||
} else {
|
} else {
|
||||||
|
@ -1285,6 +1352,12 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_seek(&mut self, position_ms: u32) {
|
fn handle_seek(&mut self, position_ms: u32) {
|
||||||
|
let duration = self.connect_state.player().duration;
|
||||||
|
if i64::from(position_ms) > duration {
|
||||||
|
warn!("tried to seek to {position_ms}ms of {duration}ms");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.connect_state
|
self.connect_state
|
||||||
.update_position(position_ms, self.now_ms());
|
.update_position(position_ms, self.now_ms());
|
||||||
self.player.seek(position_ms);
|
self.player.seek(position_ms);
|
||||||
|
@ -1422,14 +1495,16 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_volume_up(&mut self) {
|
fn handle_volume_up(&mut self) {
|
||||||
let volume =
|
let volume_steps = self.connect_state.device_info().capabilities.volume_steps as u16;
|
||||||
(self.connect_state.device_info().volume as u16).saturating_add(VOLUME_STEP_SIZE);
|
|
||||||
|
let volume = (self.connect_state.device_info().volume as u16).saturating_add(volume_steps);
|
||||||
self.set_volume(volume);
|
self.set_volume(volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_volume_down(&mut self) {
|
fn handle_volume_down(&mut self) {
|
||||||
let volume =
|
let volume_steps = self.connect_state.device_info().capabilities.volume_steps as u16;
|
||||||
(self.connect_state.device_info().volume as u16).saturating_sub(VOLUME_STEP_SIZE);
|
|
||||||
|
let volume = (self.connect_state.device_info().volume as u16).saturating_sub(volume_steps);
|
||||||
self.set_volume(volume);
|
self.set_volume(volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
pub(super) mod context;
|
pub(super) mod context;
|
||||||
mod handle;
|
mod handle;
|
||||||
pub mod metadata;
|
mod metadata;
|
||||||
mod options;
|
mod options;
|
||||||
pub(super) mod provider;
|
pub(super) mod provider;
|
||||||
mod restrictions;
|
mod restrictions;
|
||||||
|
@ -40,7 +40,7 @@ const SPOTIFY_MAX_PREV_TRACKS_SIZE: usize = 10;
|
||||||
const SPOTIFY_MAX_NEXT_TRACKS_SIZE: usize = 80;
|
const SPOTIFY_MAX_NEXT_TRACKS_SIZE: usize = 80;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum StateError {
|
pub(super) enum StateError {
|
||||||
#[error("the current track couldn't be resolved from the transfer state")]
|
#[error("the current track couldn't be resolved from the transfer state")]
|
||||||
CouldNotResolveTrackFromTransfer,
|
CouldNotResolveTrackFromTransfer,
|
||||||
#[error("context is not available. type: {0:?}")]
|
#[error("context is not available. type: {0:?}")]
|
||||||
|
@ -74,33 +74,38 @@ impl From<StateError> for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configuration of the connect device
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ConnectStateConfig {
|
pub struct ConnectConfig {
|
||||||
pub session_id: String,
|
/// The name of the connect device (default: librespot)
|
||||||
pub initial_volume: u32,
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
/// The icon type of the connect device (default: [DeviceType::Speaker])
|
||||||
pub device_type: DeviceType,
|
pub device_type: DeviceType,
|
||||||
pub volume_steps: i32,
|
/// Displays the [DeviceType] twice in the ui to show up as a group (default: false)
|
||||||
pub is_group: bool,
|
pub is_group: bool,
|
||||||
|
/// The volume with which the connect device will be initialized (default: 50%)
|
||||||
|
pub initial_volume: u16,
|
||||||
|
/// Disables the option to control the volume remotely (default: false)
|
||||||
pub disable_volume: bool,
|
pub disable_volume: bool,
|
||||||
|
/// The steps in which the volume is incremented (default: 1024)
|
||||||
|
pub volume_steps: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ConnectStateConfig {
|
impl Default for ConnectConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
session_id: String::new(),
|
|
||||||
initial_volume: u32::from(u16::MAX) / 2,
|
|
||||||
name: "librespot".to_string(),
|
name: "librespot".to_string(),
|
||||||
device_type: DeviceType::Speaker,
|
device_type: DeviceType::Speaker,
|
||||||
volume_steps: 64,
|
|
||||||
is_group: false,
|
is_group: false,
|
||||||
|
initial_volume: u16::MAX / 2,
|
||||||
disable_volume: false,
|
disable_volume: false,
|
||||||
|
volume_steps: 1024,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct ConnectState {
|
pub(super) struct ConnectState {
|
||||||
/// the entire state that is updated to the remote server
|
/// the entire state that is updated to the remote server
|
||||||
request: PutStateRequest,
|
request: PutStateRequest,
|
||||||
|
|
||||||
|
@ -125,10 +130,10 @@ pub struct ConnectState {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConnectState {
|
impl ConnectState {
|
||||||
pub fn new(cfg: ConnectStateConfig, session: &Session) -> Self {
|
pub fn new(cfg: ConnectConfig, session: &Session) -> Self {
|
||||||
let device_info = DeviceInfo {
|
let device_info = DeviceInfo {
|
||||||
can_play: true,
|
can_play: true,
|
||||||
volume: cfg.initial_volume,
|
volume: cfg.initial_volume.into(),
|
||||||
name: cfg.name,
|
name: cfg.name,
|
||||||
device_id: session.device_id().to_string(),
|
device_id: session.device_id().to_string(),
|
||||||
device_type: EnumOrUnknown::new(cfg.device_type.into()),
|
device_type: EnumOrUnknown::new(cfg.device_type.into()),
|
||||||
|
@ -137,7 +142,7 @@ impl ConnectState {
|
||||||
client_id: session.client_id(),
|
client_id: session.client_id(),
|
||||||
is_group: cfg.is_group,
|
is_group: cfg.is_group,
|
||||||
capabilities: MessageField::some(Capabilities {
|
capabilities: MessageField::some(Capabilities {
|
||||||
volume_steps: cfg.volume_steps,
|
volume_steps: cfg.volume_steps.into(),
|
||||||
disable_volume: cfg.disable_volume,
|
disable_volume: cfg.disable_volume,
|
||||||
|
|
||||||
gaia_eq_connect_id: true,
|
gaia_eq_connect_id: true,
|
||||||
|
@ -183,7 +188,7 @@ impl ConnectState {
|
||||||
device: MessageField::some(Device {
|
device: MessageField::some(Device {
|
||||||
device_info: MessageField::some(device_info),
|
device_info: MessageField::some(device_info),
|
||||||
player_state: MessageField::some(PlayerState {
|
player_state: MessageField::some(PlayerState {
|
||||||
session_id: cfg.session_id,
|
session_id: session.session_id(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
|
@ -24,6 +24,7 @@ macro_rules! metadata_entry {
|
||||||
self.$get($entry)
|
self.$get($entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn $set (&mut self, $key: impl Display) {
|
fn $set (&mut self, $key: impl Display) {
|
||||||
self.metadata_mut().insert($entry.to_string(), $key.to_string());
|
self.metadata_mut().insert($entry.to_string(), $key.to_string());
|
||||||
}
|
}
|
||||||
|
@ -34,9 +35,11 @@ macro_rules! metadata_entry {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Allows easy access of known metadata fields
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub trait Metadata {
|
pub(super) trait Metadata {
|
||||||
fn metadata(&self) -> &HashMap<String, String>;
|
fn metadata(&self) -> &HashMap<String, String>;
|
||||||
|
|
||||||
fn metadata_mut(&mut self) -> &mut HashMap<String, String>;
|
fn metadata_mut(&mut self) -> &mut HashMap<String, String>;
|
||||||
|
|
||||||
fn get_bool(&self, entry: &str) -> bool {
|
fn get_bool(&self, entry: &str) -> bool {
|
||||||
|
|
|
@ -84,7 +84,7 @@ impl Credentials {
|
||||||
}
|
}
|
||||||
|
|
||||||
let hi = read_u8(stream)? as u32;
|
let hi = read_u8(stream)? as u32;
|
||||||
Ok(lo & 0x7f | hi << 7)
|
Ok(lo & 0x7f | (hi << 7))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_bytes<R: Read>(stream: &mut R) -> io::Result<Vec<u8>> {
|
fn read_bytes<R: Read>(stream: &mut R) -> io::Result<Vec<u8>> {
|
||||||
|
|
|
@ -804,20 +804,27 @@ impl SpClient {
|
||||||
|
|
||||||
/// Request the context for an uri
|
/// Request the context for an uri
|
||||||
///
|
///
|
||||||
/// ## Query entry found in the wild:
|
/// All [SpotifyId] uris are supported in addition to the following special uris:
|
||||||
|
/// - liked songs:
|
||||||
|
/// - all: `spotify:user:<user_id>:collection`
|
||||||
|
/// - of artist: `spotify:user:<user_id>:collection:artist:<artist_id>`
|
||||||
|
/// - search: `spotify:search:<search+query>` (whitespaces are replaced with `+`)
|
||||||
|
///
|
||||||
|
/// ## Query params found in the wild:
|
||||||
/// - include_video=true
|
/// - include_video=true
|
||||||
/// ## Remarks:
|
///
|
||||||
/// - track
|
/// ## Known results of uri types:
|
||||||
|
/// - uris of type `track`
|
||||||
/// - returns a single page with a single track
|
/// - returns a single page with a single track
|
||||||
/// - when requesting a single track with a query in the request, the returned track uri
|
/// - when requesting a single track with a query in the request, the returned track uri
|
||||||
/// **will** contain the query
|
/// **will** contain the query
|
||||||
/// - artists
|
/// - uris of type `artist`
|
||||||
/// - returns 2 pages with tracks: 10 most popular tracks and latest/popular album
|
/// - returns 2 pages with tracks: 10 most popular tracks and latest/popular album
|
||||||
/// - remaining pages are artist albums sorted by popularity (only provided as page_url)
|
/// - remaining pages are artist albums sorted by popularity (only provided as page_url)
|
||||||
/// - search
|
/// - uris of type `search`
|
||||||
/// - is massively influenced by the provided query
|
/// - is massively influenced by the provided query
|
||||||
/// - the query result shown by the search expects no query at all
|
/// - the query result shown by the search expects no query at all
|
||||||
/// - uri looks like "spotify:search:never+gonna"
|
/// - uri looks like `spotify:search:never+gonna`
|
||||||
pub async fn get_context(&self, uri: &str) -> Result<Context, Error> {
|
pub async fn get_context(&self, uri: &str) -> Result<Context, Error> {
|
||||||
let uri = format!("/context-resolve/v1/{uri}");
|
let uri = format!("/context-resolve/v1/{uri}");
|
||||||
|
|
||||||
|
|
|
@ -1,92 +1,77 @@
|
||||||
use librespot::{
|
use librespot::{
|
||||||
|
connect::{ConnectConfig, LoadRequest, LoadRequestOptions, Spirc},
|
||||||
core::{
|
core::{
|
||||||
authentication::Credentials, config::SessionConfig, session::Session, spotify_id::SpotifyId,
|
authentication::Credentials, cache::Cache, config::SessionConfig, session::Session, Error,
|
||||||
},
|
},
|
||||||
|
playback::mixer::MixerConfig,
|
||||||
playback::{
|
playback::{
|
||||||
audio_backend,
|
audio_backend,
|
||||||
config::{AudioFormat, PlayerConfig},
|
config::{AudioFormat, PlayerConfig},
|
||||||
mixer::NoOpVolume,
|
mixer,
|
||||||
player::Player,
|
player::Player,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use librespot_connect::spirc::PlayingTrack;
|
|
||||||
use librespot_connect::{
|
use log::LevelFilter;
|
||||||
spirc::{Spirc, SpircLoadCommand},
|
|
||||||
state::ConnectStateConfig,
|
const CACHE: &str = ".cache";
|
||||||
};
|
const CACHE_FILES: &str = ".cache/files";
|
||||||
use librespot_metadata::{Album, Metadata};
|
|
||||||
use librespot_playback::mixer::{softmixer::SoftMixer, Mixer, MixerConfig};
|
|
||||||
use std::env;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::join;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() -> Result<(), Error> {
|
||||||
|
env_logger::builder()
|
||||||
|
.filter_module("librespot", LevelFilter::Debug)
|
||||||
|
.init();
|
||||||
|
|
||||||
let session_config = SessionConfig::default();
|
let session_config = SessionConfig::default();
|
||||||
let player_config = PlayerConfig::default();
|
let player_config = PlayerConfig::default();
|
||||||
let audio_format = AudioFormat::default();
|
let audio_format = AudioFormat::default();
|
||||||
let connect_config = ConnectStateConfig::default();
|
let connect_config = ConnectConfig::default();
|
||||||
|
let mixer_config = MixerConfig::default();
|
||||||
|
let request_options = LoadRequestOptions::default();
|
||||||
|
|
||||||
let mut args: Vec<_> = env::args().collect();
|
let sink_builder = audio_backend::find(None).unwrap();
|
||||||
let context_uri = if args.len() == 3 {
|
let mixer_builder = mixer::find(None).unwrap();
|
||||||
args.pop().unwrap()
|
|
||||||
} else if args.len() == 2 {
|
|
||||||
String::from("spotify:album:79dL7FLiJFOO0EoehUHQBv")
|
|
||||||
} else {
|
|
||||||
eprintln!("Usage: {} ACCESS_TOKEN (ALBUM URI)", args[0]);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let credentials = Credentials::with_access_token(&args[1]);
|
let cache = Cache::new(Some(CACHE), Some(CACHE), Some(CACHE_FILES), None)?;
|
||||||
let backend = audio_backend::find(None).unwrap();
|
let credentials = cache
|
||||||
|
.credentials()
|
||||||
|
.ok_or(Error::unavailable("credentials not cached"))
|
||||||
|
.or_else(|_| {
|
||||||
|
librespot_oauth::OAuthClientBuilder::new(
|
||||||
|
&session_config.client_id,
|
||||||
|
"http://127.0.0.1:8898/login",
|
||||||
|
vec!["streaming"],
|
||||||
|
)
|
||||||
|
.open_in_browser()
|
||||||
|
.build()?
|
||||||
|
.get_access_token()
|
||||||
|
.map(|t| Credentials::with_access_token(t.access_token))
|
||||||
|
})?;
|
||||||
|
|
||||||
println!("Connecting...");
|
let session = Session::new(session_config, Some(cache));
|
||||||
let session = Session::new(session_config, None);
|
let mixer = mixer_builder(mixer_config);
|
||||||
|
|
||||||
let player = Player::new(
|
let player = Player::new(
|
||||||
player_config,
|
player_config,
|
||||||
session.clone(),
|
session.clone(),
|
||||||
Box::new(NoOpVolume),
|
mixer.get_soft_volume(),
|
||||||
move || backend(None, audio_format),
|
move || sink_builder(None, audio_format),
|
||||||
);
|
);
|
||||||
|
|
||||||
let (spirc, spirc_task) = Spirc::new(
|
let (spirc, spirc_task) =
|
||||||
connect_config,
|
Spirc::new(connect_config, session.clone(), credentials, player, mixer).await?;
|
||||||
session.clone(),
|
|
||||||
credentials,
|
|
||||||
player,
|
|
||||||
Arc::new(SoftMixer::open(MixerConfig::default())),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
join!(spirc_task, async {
|
// these calls can be seen as "queued"
|
||||||
let album = Album::get(&session, &SpotifyId::from_uri(&context_uri).unwrap())
|
spirc.activate()?;
|
||||||
.await
|
spirc.load(LoadRequest::from_context_uri(
|
||||||
.unwrap();
|
format!("spotify:user:{}:collection", session.username()),
|
||||||
|
request_options,
|
||||||
|
))?;
|
||||||
|
spirc.play()?;
|
||||||
|
|
||||||
println!(
|
// starting the connect device and processing the previously "queued" calls
|
||||||
"Playing album: {} by {}",
|
spirc_task.await;
|
||||||
&album.name,
|
|
||||||
album
|
|
||||||
.artists
|
|
||||||
.first()
|
|
||||||
.map_or("unknown", |artist| &artist.name)
|
|
||||||
);
|
|
||||||
|
|
||||||
spirc.activate().unwrap();
|
Ok(())
|
||||||
spirc
|
|
||||||
.load(SpircLoadCommand {
|
|
||||||
context_uri,
|
|
||||||
start_playing: true,
|
|
||||||
seek_to: 0,
|
|
||||||
shuffle: false,
|
|
||||||
repeat: false,
|
|
||||||
repeat_track: false,
|
|
||||||
autoplay: false,
|
|
||||||
// the index specifies which track in the context starts playing, in this case the first in the album
|
|
||||||
playing_track: PlayingTrack::Index(0).into(),
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2238,9 +2238,7 @@ impl PlayerInternal {
|
||||||
let wait_for_data_length =
|
let wait_for_data_length =
|
||||||
(read_ahead_during_playback.as_secs_f32() * bytes_per_second as f32) as usize;
|
(read_ahead_during_playback.as_secs_f32() * bytes_per_second as f32) as usize;
|
||||||
|
|
||||||
stream_loader_controller
|
stream_loader_controller.fetch_next_and_wait(request_data_length, wait_for_data_length)
|
||||||
.fetch_next_and_wait(request_data_length, wait_for_data_length)
|
|
||||||
.map_err(Into::into)
|
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
12
src/main.rs
12
src/main.rs
|
@ -14,7 +14,7 @@ use futures_util::StreamExt;
|
||||||
#[cfg(feature = "alsa-backend")]
|
#[cfg(feature = "alsa-backend")]
|
||||||
use librespot::playback::mixer::alsamixer::AlsaMixer;
|
use librespot::playback::mixer::alsamixer::AlsaMixer;
|
||||||
use librespot::{
|
use librespot::{
|
||||||
connect::{spirc::Spirc, state::ConnectStateConfig},
|
connect::{ConnectConfig, Spirc},
|
||||||
core::{
|
core::{
|
||||||
authentication::Credentials, cache::Cache, config::DeviceType, version, Session,
|
authentication::Credentials, cache::Cache, config::DeviceType, version, Session,
|
||||||
SessionConfig,
|
SessionConfig,
|
||||||
|
@ -208,7 +208,7 @@ struct Setup {
|
||||||
cache: Option<Cache>,
|
cache: Option<Cache>,
|
||||||
player_config: PlayerConfig,
|
player_config: PlayerConfig,
|
||||||
session_config: SessionConfig,
|
session_config: SessionConfig,
|
||||||
connect_config: ConnectStateConfig,
|
connect_config: ConnectConfig,
|
||||||
mixer_config: MixerConfig,
|
mixer_config: MixerConfig,
|
||||||
credentials: Option<Credentials>,
|
credentials: Option<Credentials>,
|
||||||
enable_oauth: bool,
|
enable_oauth: bool,
|
||||||
|
@ -1371,7 +1371,7 @@ fn get_setup() -> Setup {
|
||||||
});
|
});
|
||||||
|
|
||||||
let connect_config = {
|
let connect_config = {
|
||||||
let connect_default_config = ConnectStateConfig::default();
|
let connect_default_config = ConnectConfig::default();
|
||||||
|
|
||||||
let name = opt_str(NAME).unwrap_or_else(|| connect_default_config.name.clone());
|
let name = opt_str(NAME).unwrap_or_else(|| connect_default_config.name.clone());
|
||||||
|
|
||||||
|
@ -1483,15 +1483,15 @@ fn get_setup() -> Setup {
|
||||||
let is_group = opt_present(DEVICE_IS_GROUP);
|
let is_group = opt_present(DEVICE_IS_GROUP);
|
||||||
|
|
||||||
if let Some(initial_volume) = initial_volume {
|
if let Some(initial_volume) = initial_volume {
|
||||||
ConnectStateConfig {
|
ConnectConfig {
|
||||||
name,
|
name,
|
||||||
device_type,
|
device_type,
|
||||||
is_group,
|
is_group,
|
||||||
initial_volume: initial_volume.into(),
|
initial_volume,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ConnectStateConfig {
|
ConnectConfig {
|
||||||
name,
|
name,
|
||||||
device_type,
|
device_type,
|
||||||
is_group,
|
is_group,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue