1
0
Fork 0
mirror of https://github.com/librespot-org/librespot.git synced 2025-10-03 01:39:28 +02:00
librespot/core/src/mercury/mod.rs
Felix Prillwitz 5839b36192
Spirc: Replace Mecury with Dealer (#1356)
This was a huge effort by photovoltex@gmail.com with help from the community.
Over 140 commits were squashed. Below, their commit messages are kept unchanged.

---

* dealer wrapper for ease of use

* improve sending protobuf requests

* replace connect config with connect_state config

* start integrating dealer into spirc

* payload handling, gzip support

* put connect state consistent

* formatting

* request payload handling, gzip support

* expose dealer::protocol, move request in own file

* integrate handle of connect-state commands

* spirc: remove ident field

* transfer playing state better

* spirc: remove remote_update stream

* spirc: replace command sender with connect state update

* spirc: remove device state and remaining unused methods

* spirc: remove mercury sender

* add repeat track state

* ConnectState: add methods to replace state in spirc

* spirc: move context into connect_state, update load and next

* spirc: remove state, adjust remaining methods

* spirc: handle more dealer request commands

* revert rustfmt.toml

* spirc: impl shuffle

- impl shuffle again
- extracted fill up of next tracks in own method
- moved queue revision update into next track fill up
- removed unused method `set_playing_track_index`
- added option to specify index when resetting the playback context
- reshuffle after repeat context

* spirc: handle device became inactive

* dealer: adjust payload handling

* spirc: better set volume handling

* dealer: box PlayCommand (clippy warning)

* dealer: always respect queued tracks

* spirc: update duration of track

* ConnectState: update more restrictions

* cleanup

* spirc: handle queue requests

* spirc: skip next with track

* proto: exclude spirc.proto
- move "deserialize_with" functions into own file
- replace TrackRef with ProvidedTrack

* spirc: stabilize transfer/context handling

* core: cleanup some remains

* connect: improvements to code structure and performance

- use VecDeque for next and prev tracks

* connect: delayed volume update

* connect: move context resolve into own function

* connect: load context asynchronous

* connect: handle reconnect

- might currently steal the active devices playback

* connect: some fixes and adjustments

- fix wrong offset when transferring playback
- fix missing displayed context in web-player
- remove access_token from log
- send correct state reason when updating volume
- queue track correctly
- fix wrong assumption for skip_to

* connect: replace error case with option

* connect: use own context state

* connect: more stabilising

- handle SkipTo having no Index
- handle no transferred restrictions
- handle no transferred index
- update state before shutdown, for smoother reacquiring

* connect: working autoplay

* connect: handle repeat context/track

* connect: some quick fixes

- found self-named uid in collection after reconnecting

* connect: handle add_to_queue via set_queue

* fix clippy warnings

* fix check errors, fix/update example

* fix 1.75 specific error

* connect: position update improvements

* connect: handle unavailable

* connect: fix incorrect status handling for desktop and mobile

* core: fix dealer reconnect

- actually acquire new token
- use login5 token retrieval

* connect: split state into multiple files

* connect: encapsulate provider logic

* connect: remove public access to next and prev tracks

* connect: remove public access to player

* connect: move state only commands into own file

* connect: improve logging

* connect: handle transferred queue again

* connect: fix all-features specific error

* connect: extract transfer  handling into own file

* connect: remove old context model

* connect: handle more transfer cases correctly

* connect: do auth_token pre-acquiring earlier

* connect: handle play with skip_to by uid

* connect: simplified cluster update log

* core/connect: add remaining set value commands

* connect: position update workaround/fix

* connect: some queue cleanups

* connect: add uid to queue

* connect: duration as volume delay const

* connect: some adjustments and todo cleanups

- send volume update before general update
- simplify queue revision to use the track uri
- argument why copying the prev/next tracks is fine

* connect: handle shuffle from set_options

* connect: handle context update

* connect: move other structs into model.rs

* connect: reduce SpircCommand visibility

* connect: fix visibility of model

* connect: fix: shuffle on startup isn't applied

* connect: prevent loading a context with no tracks

* connect: use the first page of a context

* connect: improve context resolving

- support multiple pages
- support page_url of context
- handle single track

* connect: prevent integer underflow

* connect: rename method for better clarity

* connect: handle mutate and update messages

* connect: fix 1.75 problems

* connect: fill, instead of replace next page

* connect: reduce context update to single method

* connect: remove unused SpircError, handle local files

* connect: reduce nesting, adjust initial transfer handling

* connect: don't update volume initially

* core: disable trace logging of handled mercury responses

* core/connect: prevent takeover from other clients, handle session-update

* connect: add queue-uid for set_queue command

* connect: adjust fields for PlayCommand

* connect: preserve context position after update_context

* connect: unify metadata modification

- only handle `is_queued` `true` items for queue

* connect: polish request command handling

 - reply to all request endpoints
 - adjust some naming
 - add some docs

* connect: add uid to tracks without

* connect: simpler update of current index

* core/connect: update log msg, fix wrong behavior

- handle became inactive separately
- remove duplicate stop
- adjust docs for websocket request

* core: add option to request without metrics and salt

* core/context: adjust context requests and update

- search should now return the expected context
- removed workaround for single track playback
- move local playback check into update_context
- check track uri for invalid characters
- early return with `?`

* connect: handle possible search context uri

* connect: remove logout support

- handle logout command
- disable support for logout
- add todos for logout

* connect: adjust detailed tracks/context handling

- always allow next
- handle no prev track available
- separate active and fill up context

* connect: adjust context resolve handling, again

* connect: add autoplay metadata to tracks

- transfer into autoplay again

* core/connect: cleanup session after spirc stops

* update CHANGELOG.md

* playback: fix clippy warnings

* connect: adjust metadata

- unify naming
- move more metadata infos into metadata.rs

* connect: add delimiter between context and autoplay playback

* connect: stop and resume correctly

* connect: adjust context resolving

- improved certain logging parts
- preload autoplay when autoplay attribute mutates
- fix transfer context uri
- fix typo
- handle empty strings for resolve uri
- fix unexpected stop of playback

* connect: ignore failure during stop

* connect: revert resolve_uri changes

* connect: correct context reset

* connect: reduce boiler code

* connect: fix some incorrect states

- uid getting replaced by empty value
- shuffle/repeat clearing autoplay context
- fill_up updating and using incorrect index

* core: adjust incorrect separator

* connect: move `add_to_queue` and `mark_unavailable` into tracks.rs

* connect: refactor - directly modify PutStateRequest

- replace `next_tracks`, `prev_tracks`, `player` and `device` with `request`
- provide helper methods for the removed fields

* connect: adjust handling of context metadata/restrictions

* connect: fix incorrect context states

* connect: become inactive when no cluster is reported

* update CHANGELOG.md

* core/playback: preemptively fix clippy warnings

* connect: minor adjustment to session changed

* connect: change return type changing active context

* connect: handle unavailable contexts

* connect: fix previous restrictions blocking load with shuffle

* connect: update comments and logging

* core/connect: reduce some more duplicate code

* more docs around the dealer
2024-12-10 20:36:09 +01:00

306 lines
10 KiB
Rust

use std::{
collections::HashMap,
future::Future,
pin::Pin,
task::{Context, Poll},
};
use byteorder::{BigEndian, ByteOrder};
use bytes::Bytes;
use futures_util::FutureExt;
use protobuf::Message;
use tokio::sync::{mpsc, oneshot};
use crate::{packet::PacketType, protocol, util::SeqGenerator, Error};
mod types;
pub use self::types::*;
mod sender;
pub use self::sender::MercurySender;
component! {
MercuryManager : MercuryManagerInner {
sequence: SeqGenerator<u64> = SeqGenerator::new(0),
pending: HashMap<Vec<u8>, MercuryPending> = HashMap::new(),
subscriptions: Vec<(String, mpsc::UnboundedSender<MercuryResponse>)> = Vec::new(),
invalid: bool = false,
}
}
pub struct MercuryPending {
parts: Vec<Vec<u8>>,
partial: Option<Vec<u8>>,
callback: Option<oneshot::Sender<Result<MercuryResponse, Error>>>,
}
pub struct MercuryFuture<T> {
receiver: oneshot::Receiver<Result<T, Error>>,
}
impl<T> Future for MercuryFuture<T> {
type Output = Result<T, Error>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.receiver.poll_unpin(cx)?
}
}
impl MercuryManager {
fn next_seq(&self) -> Vec<u8> {
let mut seq = vec![0u8; 8];
BigEndian::write_u64(&mut seq, self.lock(|inner| inner.sequence.get()));
seq
}
fn request(&self, req: MercuryRequest) -> Result<MercuryFuture<MercuryResponse>, Error> {
let (tx, rx) = oneshot::channel();
let pending = MercuryPending {
parts: Vec::new(),
partial: None,
callback: Some(tx),
};
let seq = self.next_seq();
self.lock(|inner| {
if !inner.invalid {
inner.pending.insert(seq.clone(), pending);
}
});
let cmd = req.method.command();
let data = req.encode(&seq)?;
self.session().send_packet(cmd, data)?;
Ok(MercuryFuture { receiver: rx })
}
pub fn get<T: Into<String>>(&self, uri: T) -> Result<MercuryFuture<MercuryResponse>, Error> {
self.request(MercuryRequest {
method: MercuryMethod::Get,
uri: uri.into(),
content_type: None,
payload: Vec::new(),
})
}
pub fn send<T: Into<String>>(
&self,
uri: T,
data: Vec<u8>,
) -> Result<MercuryFuture<MercuryResponse>, Error> {
self.request(MercuryRequest {
method: MercuryMethod::Send,
uri: uri.into(),
content_type: None,
payload: vec![data],
})
}
pub fn sender<T: Into<String>>(&self, uri: T) -> MercurySender {
MercurySender::new(self.clone(), uri.into())
}
pub fn subscribe<T: Into<String>>(
&self,
uri: T,
) -> impl Future<Output = Result<mpsc::UnboundedReceiver<MercuryResponse>, Error>> + 'static
{
let uri = uri.into();
let request = self.request(MercuryRequest {
method: MercuryMethod::Sub,
uri: uri.clone(),
content_type: None,
payload: Vec::new(),
});
let manager = self.clone();
async move {
let response = request?.await?;
let (tx, rx) = mpsc::unbounded_channel();
manager.lock(move |inner| {
if !inner.invalid {
debug!("subscribed uri={} count={}", uri, response.payload.len());
if !response.payload.is_empty() {
// Old subscription protocol, watch the provided list of URIs
for sub in response.payload {
match protocol::pubsub::Subscription::parse_from_bytes(&sub) {
Ok(mut sub) => {
let sub_uri = sub.take_uri();
debug!("subscribed sub_uri={}", sub_uri);
inner.subscriptions.push((sub_uri, tx.clone()));
}
Err(e) => {
error!("could not subscribe to {}: {}", uri, e);
}
}
}
} else {
// New subscription protocol, watch the requested URI
inner.subscriptions.push((uri, tx));
}
}
});
Ok(rx)
}
}
pub fn listen_for<T: Into<String>>(
&self,
uri: T,
) -> impl Future<Output = mpsc::UnboundedReceiver<MercuryResponse>> + 'static {
let uri = uri.into();
let manager = self.clone();
async move {
let (tx, rx) = mpsc::unbounded_channel();
manager.lock(move |inner| {
if !inner.invalid {
debug!("listening to uri={}", uri);
inner.subscriptions.push((uri, tx));
}
});
rx
}
}
pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> {
let seq_len = BigEndian::read_u16(data.split_to(2).as_ref()) as usize;
let seq = data.split_to(seq_len).as_ref().to_owned();
let flags = data.split_to(1).as_ref()[0];
let count = BigEndian::read_u16(data.split_to(2).as_ref()) as usize;
let pending = self.lock(|inner| inner.pending.remove(&seq));
let mut pending = match pending {
Some(pending) => pending,
None => {
if let PacketType::MercuryEvent = cmd {
MercuryPending {
parts: Vec::new(),
partial: None,
callback: None,
}
} else {
warn!("Ignore seq {:?} cmd {:x}", seq, cmd as u8);
return Err(MercuryError::Command(cmd).into());
}
}
};
for i in 0..count {
let mut part = Self::parse_part(&mut data);
if let Some(mut partial) = pending.partial.take() {
partial.extend_from_slice(&part);
part = partial;
}
if i == count - 1 && (flags == 2) {
pending.partial = Some(part)
} else {
pending.parts.push(part);
}
}
if flags == 0x1 {
self.complete_request(cmd, pending)?;
} else {
self.lock(move |inner| inner.pending.insert(seq, pending));
}
Ok(())
}
fn parse_part(data: &mut Bytes) -> Vec<u8> {
let size = BigEndian::read_u16(data.split_to(2).as_ref()) as usize;
data.split_to(size).as_ref().to_owned()
}
fn complete_request(&self, cmd: PacketType, mut pending: MercuryPending) -> Result<(), Error> {
let header_data = pending.parts.remove(0);
let header = protocol::mercury::Header::parse_from_bytes(&header_data)?;
let response = MercuryResponse {
uri: header.uri().to_string(),
status_code: header.status_code(),
payload: pending.parts,
};
let status_code = response.status_code;
if status_code >= 500 {
error!("error {} for uri {}", status_code, &response.uri);
Err(MercuryError::Response(response).into())
} else if status_code >= 400 {
error!("error {} for uri {}", status_code, &response.uri);
if let Some(cb) = pending.callback {
cb.send(Err(MercuryError::Response(response.clone()).into()))
.map_err(|_| MercuryError::Channel)?;
}
Err(MercuryError::Response(response).into())
} else if let PacketType::MercuryEvent = cmd {
// TODO: This is just a workaround to make utf-8 encoded usernames work.
// A better solution would be to use an uri struct and urlencode it directly
// before sending while saving the subscription under its unencoded form.
let mut uri_split = response.uri.split('/');
let encoded_uri = std::iter::once(uri_split.next().unwrap_or_default().to_string())
.chain(uri_split.map(|component| {
form_urlencoded::byte_serialize(component.as_bytes()).collect::<String>()
}))
.collect::<Vec<String>>()
.join("/");
let mut found = false;
self.lock(|inner| {
inner.subscriptions.retain(|(prefix, sub)| {
if encoded_uri.starts_with(prefix) {
found = true;
// if send fails, remove from list of subs
// TODO: send unsub message
sub.send(response.clone()).is_ok()
} else {
// URI doesn't match
true
}
});
});
if found {
Ok(())
} else if self.session().dealer().handles(&response.uri) {
trace!("mercury response <{}> is handled by dealer", response.uri);
Ok(())
} else {
debug!("unknown subscription uri={}", &response.uri);
trace!("response pushed over Mercury: {:?}", response);
Err(MercuryError::Response(response).into())
}
} else if let Some(cb) = pending.callback {
cb.send(Ok(response)).map_err(|_| MercuryError::Channel)?;
Ok(())
} else {
error!("can't handle Mercury response: {:?}", response);
Err(MercuryError::Response(response).into())
}
}
pub(crate) fn shutdown(&self) {
self.lock(|inner| {
inner.invalid = true;
// destroy the sending halves of the channels to signal everyone who is waiting for something.
inner.pending.clear();
inner.subscriptions.clear();
});
}
}