Merge pull request #1053 from JasonLG1979/connect-event

Major events overhaul
This commit is contained in:
Roderick van Domburg 2022-09-22 21:43:09 +02:00 committed by GitHub
commit ccd501d22e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1239 additions and 639 deletions

View file

@ -79,9 +79,20 @@ https://github.com/librespot-org/librespot
disabled such content. Applications that use librespot as a library without
Connect should use the 'filter-explicit-content' user attribute in the session.
- [metadata] All metadata fields in the protobufs are now exposed (breaking)
- [connect] Add session events
- [playback] Add metadata support via a `TrackChanged` event
- [main] Add all player events to `player_event_handler.rs`
- [contrib] Add `event_handler_example.py`
- [connect] Add `repeat`, `set_position_ms` and `set_volume` to `spirc.rs`
- [main] Add an event worker thread that runs async to the main thread(s) but sync to itself to prevent potential data races for event consumers
### Fixed
- [connect] Set `PlayStatus` to the correct value when Player is loading to avoid blanking out the controls when `self.play_status` is `LoadingPlay` or `LoadingPause` in `spirc.rs`
- [connect] Handle attempts to play local files better by basically ignoring attempts to load them in `handle_remote_update` in `spirc.rs`
- [playback] Handle invalid track start positions by just starting the track from the beginning
- [playback, connect] Clean up and de-noise events and event firing
- [playback] Handle disappearing and invalid devices better
### Removed
- [main] `autoplay` is no longer a command-line option. Instead, librespot now

View file

@ -44,6 +44,8 @@ use crate::{
pub enum SpircError {
#[error("response payload empty")]
NoData,
#[error("playback of local files is not supported")]
UnsupportedLocalPlayBack,
#[error("message addressed at another ident: {0}")]
Ident(String),
#[error("message pushed for another URI")]
@ -52,10 +54,10 @@ pub enum SpircError {
impl From<SpircError> for Error {
fn from(err: SpircError) -> Self {
use SpircError::*;
match err {
SpircError::NoData => Error::unavailable(err),
SpircError::Ident(_) => Error::aborted(err),
SpircError::InvalidUri(_) => Error::aborted(err),
NoData | UnsupportedLocalPlayBack => Error::unavailable(err),
Ident(_) | InvalidUri(_) => Error::aborted(err),
}
}
}
@ -113,6 +115,7 @@ struct SpircTask {
static SPIRC_COUNTER: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug)]
pub enum SpircCommand {
Play,
PlayPause,
@ -122,7 +125,11 @@ pub enum SpircCommand {
VolumeUp,
VolumeDown,
Shutdown,
Shuffle,
Shuffle(bool),
Repeat(bool),
Disconnect,
SetPosition(u32),
SetVolume(u16),
}
const CONTEXT_TRACKS_HISTORY: usize = 10;
@ -243,10 +250,8 @@ fn initial_device_state(config: ConnectConfig) -> DeviceState {
msg.set_typ(protocol::spirc::CapabilityType::kSupportedTypes);
{
let repeated = msg.mut_stringValue();
repeated.push(::std::convert::Into::into("audio/local"));
repeated.push(::std::convert::Into::into("audio/track"));
repeated.push(::std::convert::Into::into("audio/episode"));
repeated.push(::std::convert::Into::into("local"));
repeated.push(::std::convert::Into::into("track"))
};
msg
@ -416,8 +421,20 @@ impl Spirc {
pub fn shutdown(&self) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::Shutdown)?)
}
pub fn shuffle(&self) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::Shuffle)?)
pub fn shuffle(&self, shuffle: bool) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::Shuffle(shuffle))?)
}
pub fn repeat(&self, repeat: bool) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::Repeat(repeat))?)
}
pub fn set_volume(&self, volume: u16) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::SetVolume(volume))?)
}
pub fn set_position_ms(&self, position_ms: u32) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::SetPosition(position_ms))?)
}
pub fn disconnect(&self) -> Result<(), Error> {
Ok(self.commands.send(SpircCommand::Disconnect)?)
}
}
@ -552,76 +569,71 @@ impl SpircTask {
}
fn handle_command(&mut self, cmd: SpircCommand) -> Result<(), Error> {
let active = self.device.get_is_active();
match cmd {
SpircCommand::Play => {
if active {
self.handle_play();
self.notify(None)
} else {
CommandSender::new(self, MessageType::kMessageTypePlay).send()
}
}
SpircCommand::PlayPause => {
if active {
self.handle_play_pause();
self.notify(None)
} else {
CommandSender::new(self, MessageType::kMessageTypePlayPause).send()
}
}
SpircCommand::Pause => {
if active {
self.handle_pause();
self.notify(None)
} else {
CommandSender::new(self, MessageType::kMessageTypePause).send()
}
}
SpircCommand::Prev => {
if active {
self.handle_prev();
self.notify(None)
} else {
CommandSender::new(self, MessageType::kMessageTypePrev).send()
}
}
SpircCommand::Next => {
if active {
self.handle_next();
self.notify(None)
} else {
CommandSender::new(self, MessageType::kMessageTypeNext).send()
}
}
SpircCommand::VolumeUp => {
if active {
self.handle_volume_up();
self.notify(None)
} else {
CommandSender::new(self, MessageType::kMessageTypeVolumeUp).send()
}
}
SpircCommand::VolumeDown => {
if active {
self.handle_volume_down();
self.notify(None)
} else {
CommandSender::new(self, MessageType::kMessageTypeVolumeDown).send()
}
}
SpircCommand::Shutdown => {
if matches!(cmd, SpircCommand::Shutdown) {
trace!("Received SpircCommand::Shutdown");
CommandSender::new(self, MessageType::kMessageTypeGoodbye).send()?;
self.player.stop();
self.handle_disconnect();
self.shutdown = true;
if let Some(rx) = self.commands.as_mut() {
rx.close()
}
Ok(())
} else if self.device.get_is_active() {
trace!("Received SpircCommand::{:?}", cmd);
match cmd {
SpircCommand::Play => {
self.handle_play();
self.notify(None)
}
SpircCommand::Shuffle => {
CommandSender::new(self, MessageType::kMessageTypeShuffle).send()
SpircCommand::PlayPause => {
self.handle_play_pause();
self.notify(None)
}
SpircCommand::Pause => {
self.handle_pause();
self.notify(None)
}
SpircCommand::Prev => {
self.handle_prev();
self.notify(None)
}
SpircCommand::Next => {
self.handle_next();
self.notify(None)
}
SpircCommand::VolumeUp => {
self.handle_volume_up();
self.notify(None)
}
SpircCommand::VolumeDown => {
self.handle_volume_down();
self.notify(None)
}
SpircCommand::Disconnect => {
self.handle_disconnect();
self.notify(None)
}
SpircCommand::Shuffle(shuffle) => {
self.state.set_shuffle(shuffle);
self.notify(None)
}
SpircCommand::Repeat(repeat) => {
self.state.set_repeat(repeat);
self.notify(None)
}
SpircCommand::SetPosition(position) => {
self.handle_seek(position);
self.notify(None)
}
SpircCommand::SetVolume(volume) => {
self.set_volume(volume);
self.notify(None)
}
_ => Ok(()),
}
} else {
warn!("SpircCommand::{:?} will be ignored while Not Active", cmd);
Ok(())
}
}
@ -635,11 +647,28 @@ impl SpircTask {
match event {
PlayerEvent::EndOfTrack { .. } => self.handle_end_of_track(),
PlayerEvent::Loading { .. } => {
trace!("==> kPlayStatusLoading");
match self.play_status {
SpircPlayStatus::LoadingPlay { position_ms } => {
self.update_state_position(position_ms);
self.state.set_status(PlayStatus::kPlayStatusPlay);
trace!("==> kPlayStatusPlay");
}
SpircPlayStatus::LoadingPause { position_ms } => {
self.update_state_position(position_ms);
self.state.set_status(PlayStatus::kPlayStatusPause);
trace!("==> kPlayStatusPause");
}
_ => {
self.state.set_status(PlayStatus::kPlayStatusLoading);
self.update_state_position(0);
trace!("==> kPlayStatusLoading");
}
}
self.notify(None)
}
PlayerEvent::Playing { position_ms, .. } => {
PlayerEvent::Playing { position_ms, .. }
| PlayerEvent::PositionCorrection { position_ms, .. }
| PlayerEvent::Seeked { position_ms, .. } => {
trace!("==> kPlayStatusPlay");
let new_nominal_start_time = self.now_ms() - position_ms as i64;
match self.play_status {
@ -674,17 +703,14 @@ impl SpircTask {
} => {
trace!("==> kPlayStatusPause");
match self.play_status {
SpircPlayStatus::Paused {
ref mut position_ms,
..
} => {
if *position_ms != new_position_ms {
*position_ms = new_position_ms;
SpircPlayStatus::Paused { .. } | SpircPlayStatus::Playing { .. } => {
self.state.set_status(PlayStatus::kPlayStatusPause);
self.update_state_position(new_position_ms);
self.play_status = SpircPlayStatus::Paused {
position_ms: new_position_ms,
preloading_of_next_track_triggered: false,
};
self.notify(None)
} else {
Ok(())
}
}
SpircPlayStatus::LoadingPlay { .. }
| SpircPlayStatus::LoadingPause { .. } => {
@ -762,7 +788,13 @@ impl SpircTask {
);
if key == "filter-explicit-content" && new_value == "1" {
self.player.skip_explicit_content();
self.player
.emit_filter_explicit_content_changed_event(matches!(new_value, "1"));
}
if key == "autoplay" && old_value != new_value {
self.player
.emit_auto_play_changed_event(matches!(new_value, "1"));
}
} else {
trace!(
@ -785,13 +817,31 @@ impl SpircTask {
return Err(SpircError::Ident(ident.to_string()).into());
}
let old_client_id = self.session.client_id();
for entry in update.get_device_state().get_metadata().iter() {
if entry.get_field_type() == "client_id" {
self.session.set_client_id(entry.get_metadata());
break;
match entry.get_field_type() {
"client-id" => self.session.set_client_id(entry.get_metadata()),
"brand_display_name" => self.session.set_client_brand_name(entry.get_metadata()),
"model_display_name" => self.session.set_client_model_name(entry.get_metadata()),
_ => (),
}
}
self.session
.set_client_name(update.get_device_state().get_name());
let new_client_id = self.session.client_id();
if self.device.get_is_active() && new_client_id != old_client_id {
self.player.emit_session_client_changed_event(
new_client_id,
self.session.client_name(),
self.session.client_brand_name(),
self.session.client_model_name(),
);
}
match update.get_typ() {
MessageType::kMessageTypeHello => self.notify(Some(ident)),
@ -800,6 +850,40 @@ impl SpircTask {
let now = self.now_ms();
self.device.set_is_active(true);
self.device.set_became_active_at(now);
self.player.emit_session_connected_event(
self.session.connection_id(),
self.session.username(),
);
self.player.emit_session_client_changed_event(
self.session.client_id(),
self.session.client_name(),
self.session.client_brand_name(),
self.session.client_model_name(),
);
self.player
.emit_volume_changed_event(self.device.get_volume() as u16);
self.player
.emit_auto_play_changed_event(self.session.autoplay());
self.player.emit_filter_explicit_content_changed_event(
self.session.filter_explicit_content(),
);
self.player
.emit_shuffle_changed_event(self.state.get_shuffle());
self.player
.emit_repeat_changed_event(self.state.get_repeat());
}
let context_uri = update.get_state().get_context_uri().to_owned();
// completely ignore local playback.
if context_uri.starts_with("spotify:local-files") {
self.notify(None)?;
return Err(SpircError::UnsupportedLocalPlayBack.into());
}
self.update_tracks(&update);
@ -852,12 +936,17 @@ impl SpircTask {
}
MessageType::kMessageTypeRepeat => {
self.state.set_repeat(update.get_state().get_repeat());
let repeat = update.get_state().get_repeat();
self.state.set_repeat(repeat);
self.player.emit_repeat_changed_event(repeat);
self.notify(None)
}
MessageType::kMessageTypeShuffle => {
self.state.set_shuffle(update.get_state().get_shuffle());
let shuffle = update.get_state().get_shuffle();
self.state.set_shuffle(shuffle);
if self.state.get_shuffle() {
let current_index = self.state.get_playing_track_index();
let tracks = self.state.mut_track();
@ -873,6 +962,9 @@ impl SpircTask {
let context = self.state.get_context_uri();
debug!("{:?}", context);
}
self.player.emit_shuffle_changed_event(shuffle);
self.notify(None)
}
@ -882,6 +974,14 @@ impl SpircTask {
}
MessageType::kMessageTypeReplace => {
let context_uri = update.get_state().get_context_uri().to_owned();
// completely ignore local playback.
if context_uri.starts_with("spotify:local-files") {
self.notify(None)?;
return Err(SpircError::UnsupportedLocalPlayBack.into());
}
self.update_tracks(&update);
if let SpircPlayStatus::Playing {
@ -915,16 +1015,23 @@ impl SpircTask {
&& self.device.get_became_active_at()
<= update.get_device_state().get_became_active_at()
{
self.device.set_is_active(false);
self.handle_stop();
self.handle_disconnect();
}
Ok(())
self.notify(None)
}
_ => Ok(()),
}
}
fn handle_disconnect(&mut self) {
self.device.set_is_active(false);
self.handle_stop();
self.player
.emit_session_disconnected_event(self.session.connection_id(), self.session.username());
}
fn handle_stop(&mut self) {
self.player.stop();
}
@ -1100,17 +1207,7 @@ impl SpircTask {
}
if new_index >= tracks_len {
let autoplay = self
.session
.get_user_attribute("autoplay")
.unwrap_or_else(|| {
warn!(
"Unable to get autoplay user attribute. Continuing with autoplay disabled."
);
"0".into()
});
if autoplay == "1" {
if self.session.autoplay() {
// Extend the playlist
debug!("Extending playlist <{}>", context_uri);
self.update_tracks_from_context();
@ -1282,13 +1379,11 @@ impl SpircTask {
|| context_uri.starts_with("spotify:dailymix:")
{
self.context_fut = self.resolve_station(&context_uri);
} else if let Some(autoplay) = self.session.get_user_attribute("autoplay") {
if &autoplay == "1" {
} else if self.session.autoplay() {
info!("Fetching autoplay context uri");
// Get autoplay_station_uri for regular playlists
self.autoplay_fut = self.resolve_autoplay_uri(&context_uri);
}
}
self.player
.set_auto_normalise_as_album(context_uri.starts_with("spotify:album:"));
@ -1422,12 +1517,18 @@ impl SpircTask {
}
fn set_volume(&mut self, volume: u16) {
self.device.set_volume(volume as u32);
let old_volume = self.device.get_volume();
let new_volume = volume as u32;
if old_volume != new_volume {
self.device.set_volume(new_volume);
self.mixer.set_volume(volume);
if let Some(cache) = self.session.cache() {
cache.save_volume(volume)
}
self.player.emit_volume_set_event(volume);
if self.device.get_is_active() {
self.player.emit_volume_changed_event(volume);
}
}
}
}

View file

@ -0,0 +1,77 @@
#!/usr/bin/python3
import os
import json
from datetime import datetime
player_event = os.getenv('PLAYER_EVENT')
json_dict = {
'event_time': str(datetime.now()),
'event': player_event,
}
if player_event in ('session_connected', 'session_disconnected'):
json_dict['user_name'] = os.environ['USER_NAME']
json_dict['connection_id'] = os.environ['CONNECTION_ID']
elif player_event == 'session_client_changed':
json_dict['client_id'] = os.environ['CLIENT_ID']
json_dict['client_name'] = os.environ['CLIENT_NAME']
json_dict['client_brand_name'] = os.environ['CLIENT_BRAND_NAME']
json_dict['client_model_name'] = os.environ['CLIENT_MODEL_NAME']
elif player_event == 'shuffle_changed':
json_dict['shuffle'] = os.environ['SHUFFLE']
elif player_event == 'repeat_changed':
json_dict['repeat'] = os.environ['REPEAT']
elif player_event == 'auto_play_changed':
json_dict['auto_play'] = os.environ['AUTO_PLAY']
elif player_event == 'filter_explicit_content_changed':
json_dict['filter'] = os.environ['FILTER']
elif player_event == 'volume_changed':
json_dict['volume'] = os.environ['VOLUME']
elif player_event in ('seeked', 'position_correction', 'playing', 'paused'):
json_dict['track_id'] = os.environ['TRACK_ID']
json_dict['position_ms'] = os.environ['POSITION_MS']
elif player_event in ('unavailable', 'end_of_track', 'preload_next', 'preloading', 'loading', 'stopped'):
json_dict['track_id'] = os.environ['TRACK_ID']
elif player_event == 'track_changed':
common_metadata_fields = {}
item_type = os.environ['ITEM_TYPE']
common_metadata_fields['item_type'] = item_type
common_metadata_fields['track_id'] = os.environ['TRACK_ID']
common_metadata_fields['uri'] = os.environ['URI']
common_metadata_fields['name'] = os.environ['NAME']
common_metadata_fields['duration_ms'] = os.environ['DURATION_MS']
common_metadata_fields['is_explicit'] = os.environ['IS_EXPLICIT']
common_metadata_fields['language'] = os.environ['LANGUAGE'].split('\n')
common_metadata_fields['covers'] = os.environ['COVERS'].split('\n')
json_dict['common_metadata_fields'] = common_metadata_fields
if item_type == 'Track':
track_metadata_fields = {}
track_metadata_fields['number'] = os.environ['NUMBER']
track_metadata_fields['disc_number'] = os.environ['DISC_NUMBER']
track_metadata_fields['popularity'] = os.environ['POPULARITY']
track_metadata_fields['album'] = os.environ['ALBUM']
track_metadata_fields['artists'] = os.environ['ARTISTS'].split('\n')
track_metadata_fields['album_artists'] = os.environ['ALBUM_ARTISTS'].split('\n')
json_dict['track_metadata_fields'] = track_metadata_fields
elif item_type == 'Episode':
episode_metadata_fields = {}
episode_metadata_fields['show_name'] = os.environ['SHOW_NAME']
publish_time = datetime.utcfromtimestamp(int(os.environ['PUBLISH_TIME'])).strftime('%Y-%m-%d')
episode_metadata_fields['publish_time'] = publish_time
episode_metadata_fields['description'] = os.environ['DESCRIPTION']
json_dict['episode_metadata_fields'] = episode_metadata_fields
print(json.dumps(json_dict, indent = 4))

View file

@ -26,7 +26,7 @@ impl From<AuthenticationError> for Error {
}
/// The credentials are used to log into the Spotify API.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Credentials {
pub username: String,

View file

@ -73,6 +73,9 @@ pub struct UserData {
#[derive(Debug, Clone, Default)]
struct SessionData {
client_id: String,
client_name: String,
client_brand_name: String,
client_model_name: String,
connection_id: String,
time_delta: i64,
invalid: bool,
@ -383,6 +386,30 @@ impl Session {
self.0.data.write().client_id = client_id.to_owned();
}
pub fn client_name(&self) -> String {
self.0.data.read().client_name.clone()
}
pub fn set_client_name(&self, client_name: &str) {
self.0.data.write().client_name = client_name.to_owned();
}
pub fn client_brand_name(&self) -> String {
self.0.data.read().client_brand_name.clone()
}
pub fn set_client_brand_name(&self, client_brand_name: &str) {
self.0.data.write().client_brand_name = client_brand_name.to_owned();
}
pub fn client_model_name(&self) -> String {
self.0.data.read().client_model_name.clone()
}
pub fn set_client_model_name(&self, client_model_name: &str) {
self.0.data.write().client_model_name = client_model_name.to_owned();
}
pub fn connection_id(&self) -> String {
self.0.data.read().connection_id.clone()
}
@ -403,6 +430,20 @@ impl Session {
self.0.data.read().user_data.country.clone()
}
pub fn filter_explicit_content(&self) -> bool {
match self.get_user_attribute("filter-explicit-content") {
Some(value) => matches!(&*value, "1"),
None => false,
}
}
pub fn autoplay(&self) -> bool {
match self.get_user_attribute("autoplay") {
Some(value) => matches!(&*value, "1"),
None => false,
}
}
pub fn set_user_attribute(&self, key: &str, value: &str) -> Option<String> {
let mut dummy_attributes = UserAttributes::new();
dummy_attributes.insert(key.to_owned(), value.to_owned());

View file

@ -36,7 +36,7 @@ async fn main() {
exit(1);
}
let (mut player, _) = Player::new(player_config, session, Box::new(NoOpVolume), move || {
let mut player = Player::new(player_config, session, Box::new(NoOpVolume), move || {
backend(None, audio_format)
});

View file

@ -97,7 +97,7 @@ impl TryFrom<&<Self as Metadata>::Message> for Album {
date: album.get_date().try_into()?,
popularity: album.get_popularity(),
genres: album.get_genre().to_vec(),
covers: album.get_cover().into(),
covers: album.get_cover_group().into(),
external_ids: album.get_external_id().into(),
discs: album.get_disc().try_into()?,
reviews: album.get_review().to_vec(),

View file

@ -1,11 +1,14 @@
use std::fmt::Debug;
use crate::{
artist::ArtistsWithRole,
availability::{AudioItemAvailability, Availabilities, UnavailabilityReason},
episode::Episode,
error::MetadataError,
image::{ImageSize, Images},
restriction::Restrictions,
track::{Track, Tracks},
Metadata,
};
use super::file::AudioFiles;
@ -16,37 +19,199 @@ use librespot_core::{
pub type AudioItemResult = Result<AudioItem, Error>;
// A wrapper with fields the player needs
#[derive(Debug, Clone)]
pub struct CoverImage {
pub url: String,
pub size: ImageSize,
pub width: i32,
pub height: i32,
}
#[derive(Debug, Clone)]
pub struct AudioItem {
pub id: SpotifyId,
pub spotify_uri: String,
pub track_id: SpotifyId,
pub uri: String,
pub files: AudioFiles,
pub name: String,
pub duration: i32,
pub covers: Vec<CoverImage>,
pub language: Vec<String>,
pub duration_ms: u32,
pub is_explicit: bool,
pub availability: AudioItemAvailability,
pub alternatives: Option<Tracks>,
pub is_explicit: bool,
pub unique_fields: UniqueFields,
}
#[derive(Debug, Clone)]
pub enum UniqueFields {
Track {
artists: ArtistsWithRole,
album: String,
album_artists: Vec<String>,
popularity: u8,
number: u32,
disc_number: u32,
},
Episode {
description: String,
publish_time: Date,
show_name: String,
},
}
impl AudioItem {
pub async fn get_file(session: &Session, id: SpotifyId) -> AudioItemResult {
let image_url = session
.get_user_attribute("image-url")
.unwrap_or_else(|| String::from("https://i.scdn.co/image/{file_id}"));
match id.item_type {
SpotifyItemType::Track => Track::get_audio_item(session, id).await,
SpotifyItemType::Episode => Episode::get_audio_item(session, id).await,
SpotifyItemType::Track => {
let track = Track::get(session, &id).await?;
if track.duration <= 0 {
return Err(Error::unavailable(MetadataError::InvalidDuration(
track.duration,
)));
}
if track.is_explicit && session.filter_explicit_content() {
return Err(Error::unavailable(MetadataError::ExplicitContentFiltered));
}
let track_id = track.id;
let uri = track_id.to_uri()?;
let album = track.album.name;
let album_artists = track
.album
.artists
.0
.into_iter()
.map(|a| a.name)
.collect::<Vec<String>>();
let covers = get_covers(track.album.covers, image_url);
let alternatives = if track.alternatives.is_empty() {
None
} else {
Some(track.alternatives)
};
let availability = if Date::now_utc() < track.earliest_live_timestamp {
Err(UnavailabilityReason::Embargo)
} else {
available_for_user(
&session.user_data(),
&track.availability,
&track.restrictions,
)
};
let popularity = track.popularity.max(0).min(100) as u8;
let number = track.number.max(0) as u32;
let disc_number = track.disc_number.max(0) as u32;
let unique_fields = UniqueFields::Track {
artists: track.artists_with_role,
album,
album_artists,
popularity,
number,
disc_number,
};
Ok(Self {
track_id,
uri,
files: track.files,
name: track.name,
covers,
language: track.language_of_performance,
duration_ms: track.duration as u32,
is_explicit: track.is_explicit,
availability,
alternatives,
unique_fields,
})
}
SpotifyItemType::Episode => {
let episode = Episode::get(session, &id).await?;
if episode.duration <= 0 {
return Err(Error::unavailable(MetadataError::InvalidDuration(
episode.duration,
)));
}
if episode.is_explicit && session.filter_explicit_content() {
return Err(Error::unavailable(MetadataError::ExplicitContentFiltered));
}
let track_id = episode.id;
let uri = track_id.to_uri()?;
let covers = get_covers(episode.covers, image_url);
let availability = available_for_user(
&session.user_data(),
&episode.availability,
&episode.restrictions,
);
let unique_fields = UniqueFields::Episode {
description: episode.description,
publish_time: episode.publish_time,
show_name: episode.show_name,
};
Ok(Self {
track_id,
uri,
files: episode.audio,
name: episode.name,
covers,
language: vec![episode.language],
duration_ms: episode.duration as u32,
is_explicit: episode.is_explicit,
availability,
alternatives: None,
unique_fields,
})
}
_ => Err(Error::unavailable(MetadataError::NonPlayable)),
}
}
}
#[async_trait]
pub trait InnerAudioItem {
async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult;
fn get_covers(covers: Images, image_url: String) -> Vec<CoverImage> {
let mut covers = covers;
fn allowed_for_user(
user_data: &UserData,
restrictions: &Restrictions,
) -> AudioItemAvailability {
covers.sort_by(|a, b| b.width.cmp(&a.width));
covers
.iter()
.filter_map(|cover| {
let cover_id = cover.id.to_string();
if !cover_id.is_empty() {
let cover_image = CoverImage {
url: image_url.replace("{file_id}", &cover_id),
size: cover.size,
width: cover.width,
height: cover.height,
};
Some(cover_image)
} else {
None
}
})
.collect()
}
fn allowed_for_user(user_data: &UserData, restrictions: &Restrictions) -> AudioItemAvailability {
let country = &user_data.country;
let user_catalogue = match user_data.attributes.get("catalogue") {
Some(catalogue) => catalogue,
@ -106,8 +271,7 @@ pub trait InnerAudioItem {
availability: &Availabilities,
restrictions: &Restrictions,
) -> AudioItemAvailability {
Self::available(availability)?;
Self::allowed_for_user(user_data, restrictions)?;
available(availability)?;
allowed_for_user(user_data, restrictions)?;
Ok(())
}
}

View file

@ -2,4 +2,4 @@ pub mod file;
pub mod item;
pub use file::{AudioFileFormat, AudioFiles};
pub use item::AudioItem;
pub use item::{AudioItem, UniqueFields};

View file

@ -5,10 +5,7 @@ use std::{
};
use crate::{
audio::{
file::AudioFiles,
item::{AudioItem, AudioItemResult, InnerAudioItem},
},
audio::file::AudioFiles,
availability::Availabilities,
content_rating::ContentRatings,
image::Images,
@ -36,7 +33,7 @@ pub struct Episode {
pub covers: Images,
pub language: String,
pub is_explicit: bool,
pub show: SpotifyId,
pub show_name: String,
pub videos: VideoFiles,
pub video_previews: VideoFiles,
pub audio_previews: AudioFiles,
@ -57,29 +54,6 @@ pub struct Episodes(pub Vec<SpotifyId>);
impl_deref_wrapped!(Episodes, Vec<SpotifyId>);
#[async_trait]
impl InnerAudioItem for Episode {
async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult {
let episode = Self::get(session, &id).await?;
let availability = Self::available_for_user(
&session.user_data(),
&episode.availability,
&episode.restrictions,
);
Ok(AudioItem {
id,
spotify_uri: id.to_uri()?,
files: episode.audio,
name: episode.name,
duration: episode.duration,
availability,
alternatives: None,
is_explicit: episode.is_explicit,
})
}
}
#[async_trait]
impl Metadata for Episode {
type Message = protocol::metadata::Episode;
@ -107,7 +81,7 @@ impl TryFrom<&<Self as Metadata>::Message> for Episode {
covers: episode.get_cover_image().get_image().into(),
language: episode.get_language().to_owned(),
is_explicit: episode.get_explicit().to_owned(),
show: episode.get_show().try_into()?,
show_name: episode.get_show().get_name().to_owned(),
videos: episode.get_video().into(),
video_previews: episode.get_video_preview().into(),
audio_previews: episode.get_audio_preview().into(),

View file

@ -7,4 +7,8 @@ pub enum MetadataError {
Empty,
#[error("audio item is non-playable when it should be")]
NonPlayable,
#[error("audio item duration can not be: {0}")]
InvalidDuration(i32),
#[error("track is marked as explicit, which client setting forbids")]
ExplicitContentFiltered,
}

View file

@ -10,6 +10,7 @@ use librespot_core::{FileId, SpotifyId};
use librespot_protocol as protocol;
use protocol::metadata::Image as ImageMessage;
use protocol::metadata::ImageGroup;
pub use protocol::metadata::Image_Size as ImageSize;
use protocol::playlist4_external::PictureSize as PictureSizeMessage;
use protocol::playlist_annotate3::TranscodedPicture as TranscodedPictureMessage;
@ -25,6 +26,12 @@ pub struct Image {
#[derive(Debug, Clone, Default)]
pub struct Images(pub Vec<Image>);
impl From<&ImageGroup> for Images {
fn from(image_group: &ImageGroup) -> Self {
Self(image_group.image.iter().map(|i| i.into()).collect())
}
}
impl_deref_wrapped!(Images, Vec<Image>);
#[derive(Debug, Clone)]

View file

@ -8,11 +8,8 @@ use uuid::Uuid;
use crate::{
artist::{Artists, ArtistsWithRole},
audio::{
file::AudioFiles,
item::{AudioItem, AudioItemResult, InnerAudioItem},
},
availability::{Availabilities, UnavailabilityReason},
audio::file::AudioFiles,
availability::Availabilities,
content_rating::ContentRatings,
external_id::ExternalIds,
restriction::Restrictions,
@ -58,42 +55,6 @@ pub struct Tracks(pub Vec<SpotifyId>);
impl_deref_wrapped!(Tracks, Vec<SpotifyId>);
#[async_trait]
impl InnerAudioItem for Track {
async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult {
let track = Self::get(session, &id).await?;
let alternatives = {
if track.alternatives.is_empty() {
None
} else {
Some(track.alternatives.clone())
}
};
// TODO: check meaning of earliest_live_timestamp in
let availability = if Date::now_utc() < track.earliest_live_timestamp {
Err(UnavailabilityReason::Embargo)
} else {
Self::available_for_user(
&session.user_data(),
&track.availability,
&track.restrictions,
)
};
Ok(AudioItem {
id,
spotify_uri: id.to_uri()?,
files: track.files,
name: track.name,
duration: track.duration,
availability,
alternatives,
is_explicit: track.is_explicit,
})
}
}
#[async_trait]
impl Metadata for Track {
type Message = protocol::metadata::Track;

View file

@ -442,6 +442,7 @@ impl Sink for AlsaSink {
}
fn stop(&mut self) -> SinkResult<()> {
if self.pcm.is_some() {
// Zero fill the remainder of the period buffer and
// write any leftover data before draining the actual PCM buffer.
self.period_buffer.resize(self.period_buffer.capacity(), 0);
@ -450,6 +451,7 @@ impl Sink for AlsaSink {
let pcm = self.pcm.take().ok_or(AlsaError::NotConnected)?;
pcm.drain().map_err(AlsaError::DrainFailure)?;
}
Ok(())
}
@ -489,9 +491,13 @@ impl AlsaSink {
pub const NAME: &'static str = "alsa";
fn write_buf(&mut self) -> SinkResult<()> {
if self.pcm.is_some() {
let write_result = {
let pcm = self.pcm.as_mut().ok_or(AlsaError::NotConnected)?;
if let Err(e) = pcm.io_bytes().writei(&self.period_buffer) {
match pcm.io_bytes().writei(&self.period_buffer) {
Ok(_) => Ok(()),
Err(e) => {
// Capture and log the original error as a warning, and then try to recover.
// If recovery fails then forward that error back to player.
warn!(
@ -499,7 +505,15 @@ impl AlsaSink {
e
);
pcm.try_recover(e, false).map_err(AlsaError::OnWrite)?
pcm.try_recover(e, false).map_err(AlsaError::OnWrite)
}
}
};
if let Err(e) = write_result {
self.pcm = None;
return Err(e.into());
}
}
self.period_buffer.clear();

View file

@ -111,9 +111,26 @@ enum PlayerCommand {
Seek(u32),
AddEventSender(mpsc::UnboundedSender<PlayerEvent>),
SetSinkEventCallback(Option<SinkEventCallback>),
EmitVolumeSetEvent(u16),
EmitVolumeChangedEvent(u16),
SetAutoNormaliseAsAlbum(bool),
SkipExplicitContent(),
EmitSessionDisconnectedEvent {
connection_id: String,
user_name: String,
},
EmitSessionConnectedEvent {
connection_id: String,
user_name: String,
},
EmitSessionClientChangedEvent {
client_id: String,
client_name: String,
client_brand_name: String,
client_model_name: String,
},
EmitFilterExplicitContentChangedEvent(bool),
EmitShuffleChangedEvent(bool),
EmitRepeatChangedEvent(bool),
EmitAutoPlayChangedEvent(bool),
}
#[derive(Debug, Clone)]
@ -123,19 +140,6 @@ pub enum PlayerEvent {
play_request_id: u64,
track_id: SpotifyId,
},
// The player started working on playback of a track while it was in a stopped state.
// This is always immediately followed up by a "Loading" or "Playing" event.
Started {
play_request_id: u64,
track_id: SpotifyId,
position_ms: u32,
},
// Same as started but in the case that the player already had a track loaded.
// The player was either playing the loaded track or it was paused.
Changed {
old_track_id: SpotifyId,
new_track_id: SpotifyId,
},
// The player is delayed by loading a track.
Loading {
play_request_id: u64,
@ -157,14 +161,12 @@ pub enum PlayerEvent {
play_request_id: u64,
track_id: SpotifyId,
position_ms: u32,
duration_ms: u32,
},
// The player entered a paused state.
Paused {
play_request_id: u64,
track_id: SpotifyId,
position_ms: u32,
duration_ms: u32,
},
// The player thinks it's a good idea to issue a preload command for the next track now.
// This event is intended for use within spirc.
@ -173,8 +175,7 @@ pub enum PlayerEvent {
track_id: SpotifyId,
},
// The player reached the end of a track.
// This event is intended for use within spirc. Spirc will respond by issuing another command
// which will trigger another event (e.g. Changed or Stopped)
// This event is intended for use within spirc. Spirc will respond by issuing another command.
EndOfTrack {
play_request_id: u64,
track_id: SpotifyId,
@ -185,9 +186,48 @@ pub enum PlayerEvent {
track_id: SpotifyId,
},
// The mixer volume was set to a new level.
VolumeSet {
VolumeChanged {
volume: u16,
},
PositionCorrection {
play_request_id: u64,
track_id: SpotifyId,
position_ms: u32,
},
Seeked {
play_request_id: u64,
track_id: SpotifyId,
position_ms: u32,
},
TrackChanged {
audio_item: Box<AudioItem>,
},
SessionConnected {
connection_id: String,
user_name: String,
},
SessionDisconnected {
connection_id: String,
user_name: String,
},
SessionClientChanged {
client_id: String,
client_name: String,
client_brand_name: String,
client_model_name: String,
},
ShuffleChanged {
shuffle: bool,
},
RepeatChanged {
repeat: bool,
},
AutoPlayChanged {
auto_play: bool,
},
FilterExplicitContentChanged {
filter: bool,
},
}
impl PlayerEvent {
@ -200,9 +240,6 @@ impl PlayerEvent {
| Unavailable {
play_request_id, ..
}
| Started {
play_request_id, ..
}
| Playing {
play_request_id, ..
}
@ -217,8 +254,14 @@ impl PlayerEvent {
}
| Stopped {
play_request_id, ..
}
| PositionCorrection {
play_request_id, ..
}
| Seeked {
play_request_id, ..
} => Some(*play_request_id),
Changed { .. } | Preloading { .. } | VolumeSet { .. } => None,
_ => None,
}
}
}
@ -370,12 +413,11 @@ impl Player {
session: Session,
volume_getter: Box<dyn VolumeGetter + Send>,
sink_builder: F,
) -> (Player, PlayerEventChannel)
) -> Self
where
F: FnOnce() -> Box<dyn Sink> + Send + 'static,
{
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
let (event_sender, event_receiver) = mpsc::unbounded_channel();
if config.normalisation {
debug!("Normalisation Type: {:?}", config.normalisation_type);
@ -421,7 +463,7 @@ impl Player {
sink_status: SinkStatus::Closed,
sink_event_callback: None,
volume_getter,
event_senders: [event_sender].to_vec(),
event_senders: vec![],
converter,
normalisation_peak: 0.0,
@ -440,14 +482,11 @@ impl Player {
debug!("PlayerInternal thread finished.");
});
(
Player {
Self {
commands: Some(cmd_tx),
thread_handle: Some(handle),
play_request_id_generator: SeqGenerator::new(0),
},
event_receiver,
)
}
}
fn command(&self, cmd: PlayerCommand) {
@ -512,16 +551,57 @@ impl Player {
self.command(PlayerCommand::SetSinkEventCallback(callback));
}
pub fn emit_volume_set_event(&self, volume: u16) {
self.command(PlayerCommand::EmitVolumeSetEvent(volume));
pub fn emit_volume_changed_event(&self, volume: u16) {
self.command(PlayerCommand::EmitVolumeChangedEvent(volume));
}
pub fn set_auto_normalise_as_album(&self, setting: bool) {
self.command(PlayerCommand::SetAutoNormaliseAsAlbum(setting));
}
pub fn skip_explicit_content(&self) {
self.command(PlayerCommand::SkipExplicitContent());
pub fn emit_filter_explicit_content_changed_event(&self, filter: bool) {
self.command(PlayerCommand::EmitFilterExplicitContentChangedEvent(filter));
}
pub fn emit_session_connected_event(&self, connection_id: String, user_name: String) {
self.command(PlayerCommand::EmitSessionConnectedEvent {
connection_id,
user_name,
});
}
pub fn emit_session_disconnected_event(&self, connection_id: String, user_name: String) {
self.command(PlayerCommand::EmitSessionDisconnectedEvent {
connection_id,
user_name,
});
}
pub fn emit_session_client_changed_event(
&self,
client_id: String,
client_name: String,
client_brand_name: String,
client_model_name: String,
) {
self.command(PlayerCommand::EmitSessionClientChangedEvent {
client_id,
client_name,
client_brand_name,
client_model_name,
});
}
pub fn emit_shuffle_changed_event(&self, shuffle: bool) {
self.command(PlayerCommand::EmitShuffleChangedEvent(shuffle));
}
pub fn emit_repeat_changed_event(&self, repeat: bool) {
self.command(PlayerCommand::EmitRepeatChangedEvent(repeat));
}
pub fn emit_auto_play_changed_event(&self, auto_play: bool) {
self.command(PlayerCommand::EmitAutoPlayChangedEvent(auto_play));
}
}
@ -541,6 +621,7 @@ struct PlayerLoadedTrackData {
decoder: Decoder,
normalisation_data: NormalisationData,
stream_loader_controller: StreamLoaderController,
audio_item: AudioItem,
bytes_per_second: usize,
duration_ms: u32,
stream_position_ms: u32,
@ -573,6 +654,7 @@ enum PlayerState {
track_id: SpotifyId,
play_request_id: u64,
decoder: Decoder,
audio_item: AudioItem,
normalisation_data: NormalisationData,
normalisation_factor: f64,
stream_loader_controller: StreamLoaderController,
@ -587,6 +669,7 @@ enum PlayerState {
play_request_id: u64,
decoder: Decoder,
normalisation_data: NormalisationData,
audio_item: AudioItem,
normalisation_factor: f64,
stream_loader_controller: StreamLoaderController,
bytes_per_second: usize,
@ -660,6 +743,7 @@ impl PlayerState {
stream_loader_controller,
stream_position_ms,
is_explicit,
audio_item,
..
} => {
*self = EndOfTrack {
@ -669,6 +753,7 @@ impl PlayerState {
decoder,
normalisation_data,
stream_loader_controller,
audio_item,
bytes_per_second,
duration_ms,
stream_position_ms,
@ -694,6 +779,7 @@ impl PlayerState {
track_id,
play_request_id,
decoder,
audio_item,
normalisation_data,
normalisation_factor,
stream_loader_controller,
@ -707,13 +793,15 @@ impl PlayerState {
track_id,
play_request_id,
decoder,
audio_item,
normalisation_data,
normalisation_factor,
stream_loader_controller,
duration_ms,
bytes_per_second,
stream_position_ms,
reported_nominal_start_time: None,
reported_nominal_start_time: Instant::now()
.checked_sub(Duration::from_millis(stream_position_ms as u64)),
suggested_to_preload_next_track,
is_explicit,
};
@ -736,20 +824,22 @@ impl PlayerState {
track_id,
play_request_id,
decoder,
audio_item,
normalisation_data,
normalisation_factor,
stream_loader_controller,
duration_ms,
bytes_per_second,
stream_position_ms,
reported_nominal_start_time: _,
suggested_to_preload_next_track,
is_explicit,
..
} => {
*self = Paused {
track_id,
play_request_id,
decoder,
audio_item,
normalisation_data,
normalisation_factor,
stream_loader_controller,
@ -777,13 +867,13 @@ struct PlayerTrackLoader {
}
impl PlayerTrackLoader {
async fn find_available_alternative(&self, audio: AudioItem) -> Option<AudioItem> {
if let Err(e) = audio.availability {
async fn find_available_alternative(&self, audio_item: AudioItem) -> Option<AudioItem> {
if let Err(e) = audio_item.availability {
error!("Track is unavailable: {}", e);
None
} else if !audio.files.is_empty() {
Some(audio)
} else if let Some(alternatives) = &audio.alternatives {
} else if !audio_item.files.is_empty() {
Some(audio_item)
} else if let Some(alternatives) = &audio_item.alternatives {
let alternatives: FuturesUnordered<_> = alternatives
.iter()
.map(|alt_id| AudioItem::get_file(&self.session, *alt_id))
@ -822,7 +912,7 @@ impl PlayerTrackLoader {
spotify_id: SpotifyId,
position_ms: u32,
) -> Option<PlayerLoadedTrackData> {
let audio = match AudioItem::get_file(&self.session, spotify_id).await {
let audio_item = match AudioItem::get_file(&self.session, spotify_id).await {
Ok(audio) => match self.find_available_alternative(audio).await {
Some(audio) => audio,
None => {
@ -841,31 +931,9 @@ impl PlayerTrackLoader {
info!(
"Loading <{}> with Spotify URI <{}>",
audio.name, audio.spotify_uri
audio_item.name, audio_item.uri
);
let is_explicit = audio.is_explicit;
if is_explicit {
if let Some(value) = self.session.get_user_attribute("filter-explicit-content") {
if &value == "1" {
warn!("Track is marked as explicit, which client setting forbids.");
return None;
}
}
}
if audio.duration < 0 {
error!(
"Track duration for <{}> cannot be {}",
spotify_id.to_uri().unwrap_or_default(),
audio.duration
);
return None;
}
let duration_ms = audio.duration as u32;
// (Most) podcasts seem to support only 96 kbps Ogg Vorbis, so fall back to it
let formats = match self.config.bitrate {
Bitrate::Bitrate96 => [
@ -900,13 +968,16 @@ impl PlayerTrackLoader {
let (format, file_id) =
match formats
.iter()
.find_map(|format| match audio.files.get(format) {
.find_map(|format| match audio_item.files.get(format) {
Some(&file_id) => Some((*format, file_id)),
_ => None,
}) {
Some(t) => t,
None => {
warn!("<{}> is not available in any supported format", audio.name);
warn!(
"<{}> is not available in any supported format",
audio_item.name
);
return None;
}
};
@ -1020,6 +1091,17 @@ impl PlayerTrackLoader {
}
};
let duration_ms = audio_item.duration_ms;
// Don't try to seek past the track's duration.
// If the position is invalid just start from
// the beginning of the track.
let position_ms = if position_ms > duration_ms {
warn!("Invalid start position of {}ms exceeds track's duration of {}ms, starting track from the beginning", position_ms, duration_ms);
0
} else {
position_ms
};
// Ensure the starting position. Even when we want to play from the beginning,
// the cursor may have been moved by parsing normalisation data. This may not
// matter for playback (but won't hurt either), but may be useful for the
@ -1038,12 +1120,15 @@ impl PlayerTrackLoader {
// Ensure streaming mode now that we are ready to play from the requested position.
stream_loader_controller.set_stream_mode();
info!("<{}> ({} ms) loaded", audio.name, audio.duration);
let is_explicit = audio_item.is_explicit;
info!("<{}> ({} ms) loaded", audio_item.name, duration_ms);
return Some(PlayerLoadedTrackData {
decoder,
normalisation_data,
stream_loader_controller,
audio_item,
bytes_per_second,
duration_ms,
stream_position_ms,
@ -1164,7 +1249,6 @@ impl Future for PlayerInternal {
normalisation_factor,
ref mut stream_position_ms,
ref mut reported_nominal_start_time,
duration_ms,
..
} = self.state
{
@ -1226,11 +1310,10 @@ impl Future for PlayerInternal {
if notify_about_position {
*reported_nominal_start_time =
now.checked_sub(new_stream_position);
self.send_event(PlayerEvent::Playing {
track_id,
self.send_event(PlayerEvent::PositionCorrection {
play_request_id,
track_id,
position_ms: new_stream_position_ms as u32,
duration_ms,
});
}
}
@ -1315,7 +1398,7 @@ impl PlayerInternal {
Ok(()) => self.sink_status = SinkStatus::Running,
Err(e) => {
error!("{}", e);
exit(1);
self.handle_pause();
}
}
}
@ -1396,7 +1479,6 @@ impl PlayerInternal {
track_id,
play_request_id,
stream_position_ms,
duration_ms,
..
} = self.state
{
@ -1405,7 +1487,6 @@ impl PlayerInternal {
track_id,
play_request_id,
position_ms: stream_position_ms,
duration_ms,
});
self.ensure_sink_running();
} else {
@ -1414,14 +1495,14 @@ impl PlayerInternal {
}
fn handle_pause(&mut self) {
if let PlayerState::Playing {
match self.state {
PlayerState::Paused { .. } => self.ensure_sink_stopped(false),
PlayerState::Playing {
track_id,
play_request_id,
stream_position_ms,
duration_ms,
..
} = self.state
{
} => {
self.state.playing_to_paused();
self.ensure_sink_stopped(false);
@ -1429,10 +1510,9 @@ impl PlayerInternal {
track_id,
play_request_id,
position_ms: stream_position_ms,
duration_ms,
});
} else {
error!("Player::pause called from invalid state: {:?}", self.state);
}
_ => error!("Player::pause called from invalid state: {:?}", self.state),
}
}
@ -1555,7 +1635,7 @@ impl PlayerInternal {
if let Err(e) = self.sink.write(packet, &mut self.converter) {
error!("{}", e);
exit(1);
self.handle_pause();
}
}
}
@ -1587,6 +1667,10 @@ impl PlayerInternal {
loaded_track: PlayerLoadedTrackData,
start_playback: bool,
) {
let audio_item = Box::new(loaded_track.audio_item.clone());
self.send_event(PlayerEvent::TrackChanged { audio_item });
let position_ms = loaded_track.stream_position_ms;
let mut config = self.config.clone();
@ -1602,18 +1686,17 @@ impl PlayerInternal {
if start_playback {
self.ensure_sink_running();
self.send_event(PlayerEvent::Playing {
track_id,
play_request_id,
position_ms,
duration_ms: loaded_track.duration_ms,
});
self.state = PlayerState::Playing {
track_id,
play_request_id,
decoder: loaded_track.decoder,
audio_item: loaded_track.audio_item,
normalisation_data: loaded_track.normalisation_data,
normalisation_factor,
stream_loader_controller: loaded_track.stream_loader_controller,
@ -1632,6 +1715,7 @@ impl PlayerInternal {
track_id,
play_request_id,
decoder: loaded_track.decoder,
audio_item: loaded_track.audio_item,
normalisation_data: loaded_track.normalisation_data,
normalisation_factor,
stream_loader_controller: loaded_track.stream_loader_controller,
@ -1646,7 +1730,6 @@ impl PlayerInternal {
track_id,
play_request_id,
position_ms,
duration_ms: loaded_track.duration_ms,
});
}
}
@ -1661,39 +1744,13 @@ impl PlayerInternal {
if !self.config.gapless {
self.ensure_sink_stopped(play);
}
// emit the correct player event
match self.state {
PlayerState::Playing {
track_id: old_track_id,
..
}
| PlayerState::Paused {
track_id: old_track_id,
..
}
| PlayerState::EndOfTrack {
track_id: old_track_id,
..
}
| PlayerState::Loading {
track_id: old_track_id,
..
} => self.send_event(PlayerEvent::Changed {
old_track_id,
new_track_id: track_id,
}),
PlayerState::Stopped => self.send_event(PlayerEvent::Started {
track_id,
play_request_id,
position_ms,
}),
PlayerState::Invalid { .. } => {
if matches!(self.state, PlayerState::Invalid { .. }) {
return Err(Error::internal(format!(
"Player::handle_command_load called from invalid state: {:?}",
self.state
)));
}
}
// Now we check at different positions whether we already have a pre-loaded version
// of this track somewhere. If so, use it and return.
@ -1754,6 +1811,7 @@ impl PlayerInternal {
if let PlayerState::Playing {
stream_position_ms,
decoder,
audio_item,
stream_loader_controller,
bytes_per_second,
duration_ms,
@ -1764,6 +1822,7 @@ impl PlayerInternal {
| PlayerState::Paused {
stream_position_ms,
decoder,
audio_item,
stream_loader_controller,
bytes_per_second,
duration_ms,
@ -1776,6 +1835,7 @@ impl PlayerInternal {
decoder,
normalisation_data,
stream_loader_controller,
audio_item,
bytes_per_second,
duration_ms,
stream_position_ms,
@ -1925,14 +1985,24 @@ impl PlayerInternal {
Ok(new_position_ms) => {
if let PlayerState::Playing {
ref mut stream_position_ms,
track_id,
play_request_id,
..
}
| PlayerState::Paused {
ref mut stream_position_ms,
track_id,
play_request_id,
..
} = self.state
{
*stream_position_ms = new_position_ms;
self.send_event(PlayerEvent::Seeked {
play_request_id,
track_id,
position_ms: new_position_ms,
});
}
}
Err(e) => error!("PlayerInternal::handle_command_seek error: {}", e),
@ -1945,35 +2015,12 @@ impl PlayerInternal {
self.preload_data_before_playback()?;
if let PlayerState::Playing {
track_id,
play_request_id,
ref mut reported_nominal_start_time,
duration_ms,
..
} = self.state
{
*reported_nominal_start_time =
Instant::now().checked_sub(Duration::from_millis(position_ms as u64));
self.send_event(PlayerEvent::Playing {
track_id,
play_request_id,
position_ms,
duration_ms,
});
}
if let PlayerState::Paused {
track_id,
play_request_id,
duration_ms,
..
} = self.state
{
self.send_event(PlayerEvent::Paused {
track_id,
play_request_id,
position_ms,
duration_ms,
});
}
Ok(())
@ -2003,15 +2050,58 @@ impl PlayerInternal {
PlayerCommand::SetSinkEventCallback(callback) => self.sink_event_callback = callback,
PlayerCommand::EmitVolumeSetEvent(volume) => {
self.send_event(PlayerEvent::VolumeSet { volume })
PlayerCommand::EmitVolumeChangedEvent(volume) => {
self.send_event(PlayerEvent::VolumeChanged { volume })
}
PlayerCommand::EmitRepeatChangedEvent(repeat) => {
self.send_event(PlayerEvent::RepeatChanged { repeat })
}
PlayerCommand::EmitShuffleChangedEvent(shuffle) => {
self.send_event(PlayerEvent::ShuffleChanged { shuffle })
}
PlayerCommand::EmitAutoPlayChangedEvent(auto_play) => {
self.send_event(PlayerEvent::AutoPlayChanged { auto_play })
}
PlayerCommand::EmitSessionClientChangedEvent {
client_id,
client_name,
client_brand_name,
client_model_name,
} => self.send_event(PlayerEvent::SessionClientChanged {
client_id,
client_name,
client_brand_name,
client_model_name,
}),
PlayerCommand::EmitSessionConnectedEvent {
connection_id,
user_name,
} => self.send_event(PlayerEvent::SessionConnected {
connection_id,
user_name,
}),
PlayerCommand::EmitSessionDisconnectedEvent {
connection_id,
user_name,
} => self.send_event(PlayerEvent::SessionDisconnected {
connection_id,
user_name,
}),
PlayerCommand::SetAutoNormaliseAsAlbum(setting) => {
self.auto_normalise_as_album = setting
}
PlayerCommand::SkipExplicitContent() => {
PlayerCommand::EmitFilterExplicitContentChangedEvent(filter) => {
self.send_event(PlayerEvent::FilterExplicitContentChanged { filter });
if filter {
if let PlayerState::Playing {
track_id,
play_request_id,
@ -2034,6 +2124,7 @@ impl PlayerInternal {
}
}
}
}
};
Ok(())
@ -2133,7 +2224,7 @@ impl Drop for PlayerInternal {
impl fmt::Debug for PlayerCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
match self {
PlayerCommand::Load {
track_id,
play,
@ -2156,14 +2247,58 @@ impl fmt::Debug for PlayerCommand {
PlayerCommand::SetSinkEventCallback(_) => {
f.debug_tuple("SetSinkEventCallback").finish()
}
PlayerCommand::EmitVolumeSetEvent(volume) => {
f.debug_tuple("VolumeSet").field(&volume).finish()
}
PlayerCommand::EmitVolumeChangedEvent(volume) => f
.debug_tuple("EmitVolumeChangedEvent")
.field(&volume)
.finish(),
PlayerCommand::SetAutoNormaliseAsAlbum(setting) => f
.debug_tuple("SetAutoNormaliseAsAlbum")
.field(&setting)
.finish(),
PlayerCommand::SkipExplicitContent() => f.debug_tuple("SkipExplicitContent").finish(),
PlayerCommand::EmitFilterExplicitContentChangedEvent(filter) => f
.debug_tuple("EmitFilterExplicitContentChangedEvent")
.field(&filter)
.finish(),
PlayerCommand::EmitSessionConnectedEvent {
connection_id,
user_name,
} => f
.debug_tuple("EmitSessionConnectedEvent")
.field(&connection_id)
.field(&user_name)
.finish(),
PlayerCommand::EmitSessionDisconnectedEvent {
connection_id,
user_name,
} => f
.debug_tuple("EmitSessionDisconnectedEvent")
.field(&connection_id)
.field(&user_name)
.finish(),
PlayerCommand::EmitSessionClientChangedEvent {
client_id,
client_name,
client_brand_name,
client_model_name,
} => f
.debug_tuple("EmitSessionClientChangedEvent")
.field(&client_id)
.field(&client_name)
.field(&client_brand_name)
.field(&client_model_name)
.finish(),
PlayerCommand::EmitShuffleChangedEvent(shuffle) => f
.debug_tuple("EmitShuffleChangedEvent")
.field(&shuffle)
.finish(),
PlayerCommand::EmitRepeatChangedEvent(repeat) => f
.debug_tuple("EmitRepeatChangedEvent")
.field(&repeat)
.finish(),
PlayerCommand::EmitAutoPlayChangedEvent(auto_play) => f
.debug_tuple("EmitAutoPlayChangedEvent")
.field(&auto_play)
.finish(),
}
}
}

View file

@ -13,7 +13,6 @@ use futures_util::StreamExt;
use log::{error, info, trace, warn};
use sha1::{Digest, Sha1};
use thiserror::Error;
use tokio::sync::mpsc::UnboundedReceiver;
use url::Url;
use librespot::{
@ -29,7 +28,7 @@ use librespot::{
},
dither,
mixer::{self, MixerConfig, MixerFn},
player::{coefficient_to_duration, duration_to_coefficient, Player, PlayerEvent},
player::{coefficient_to_duration, duration_to_coefficient, Player},
},
};
@ -37,7 +36,7 @@ use librespot::{
use librespot::playback::mixer::alsamixer::AlsaMixer;
mod player_event_handler;
use player_event_handler::{emit_sink_event, run_program_on_events};
use player_event_handler::{run_program_on_sink_events, EventHandler};
fn device_id(name: &str) -> String {
hex::encode(Sha1::digest(name.as_bytes()))
@ -1598,10 +1597,10 @@ async fn main() {
let mut last_credentials = None;
let mut spirc: Option<Spirc> = None;
let mut spirc_task: Option<Pin<_>> = None;
let mut player_event_channel: Option<UnboundedReceiver<PlayerEvent>> = None;
let mut auto_connect_times: Vec<Instant> = vec![];
let mut discovery = None;
let mut connecting = false;
let mut _event_handler: Option<EventHandler> = None;
let session = Session::new(setup.session_config.clone(), setup.cache.clone());
@ -1669,32 +1668,21 @@ async fn main() {
let format = setup.format;
let backend = setup.backend;
let device = setup.device.clone();
let (player, event_channel) =
Player::new(player_config, session.clone(), soft_volume, move || {
let player = Player::new(player_config, session.clone(), soft_volume, move || {
(backend)(device, format)
});
if setup.emit_sink_events {
if let Some(player_event_program) = setup.player_event_program.clone() {
_event_handler = Some(EventHandler::new(player.get_player_event_channel(), &player_event_program));
if setup.emit_sink_events {
player.set_sink_event_callback(Some(Box::new(move |sink_status| {
match emit_sink_event(sink_status, &player_event_program) {
Ok(e) if e.success() => (),
Ok(e) => {
if let Some(code) = e.code() {
warn!("Sink event program returned exit code {}", code);
} else {
warn!("Sink event program returned failure");
}
},
Err(e) => {
warn!("Emitting sink event failed: {}", e);
},
}
run_program_on_sink_events(sink_status, &player_event_program)
})));
}
};
let (spirc_, spirc_task_) = match Spirc::new(connect_config, session.clone(), last_credentials.clone().unwrap(), player, mixer).await {
let (spirc_, spirc_task_) = match Spirc::new(connect_config, session.clone(), last_credentials.clone().unwrap_or_default(), player, mixer).await {
Ok((spirc_, spirc_task_)) => (spirc_, spirc_task_),
Err(e) => {
error!("could not initialize spirc: {}", e);
@ -1703,7 +1691,6 @@ async fn main() {
};
spirc = Some(spirc_);
spirc_task = Some(Box::pin(spirc_task_));
player_event_channel = Some(event_channel);
connecting = false;
},
@ -1732,41 +1719,6 @@ async fn main() {
},
}
},
event = async {
match player_event_channel.as_mut() {
Some(p) => p.recv().await,
_ => None
}
}, if player_event_channel.is_some() => match event {
Some(event) => {
if let Some(program) = &setup.player_event_program {
if let Some(child) = run_program_on_events(event, program) {
if let Ok(mut child) = child {
tokio::spawn(async move {
match child.wait().await {
Ok(e) if e.success() => (),
Ok(e) => {
if let Some(code) = e.code() {
warn!("On event program returned exit code {}", code);
} else {
warn!("On event program returned failure");
}
},
Err(e) => {
warn!("On event program failed: {}", e);
},
}
});
} else {
warn!("On event program failed to start");
}
}
}
},
None => {
player_event_channel = None;
}
},
_ = tokio::signal::ctrl_c() => {
break;
},

View file

@ -1,61 +1,96 @@
use log::info;
use log::{debug, error, warn};
use std::{
collections::HashMap,
io::{Error, ErrorKind, Result},
process::{Command, ExitStatus},
use std::{collections::HashMap, process::Command, thread};
use librespot::{
metadata::audio::UniqueFields,
playback::player::{PlayerEvent, PlayerEventChannel, SinkStatus},
};
use tokio::process::{Child as AsyncChild, Command as AsyncCommand};
pub struct EventHandler {
thread_handle: Option<thread::JoinHandle<()>>,
}
use librespot::playback::player::{PlayerEvent, SinkStatus};
pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<Result<AsyncChild>> {
impl EventHandler {
pub fn new(mut player_events: PlayerEventChannel, onevent: &str) -> Self {
let on_event = onevent.to_string();
let thread_handle = Some(thread::spawn(move || loop {
match player_events.blocking_recv() {
None => break,
Some(event) => {
let mut env_vars = HashMap::new();
match event {
PlayerEvent::Changed {
old_track_id,
new_track_id,
} => match old_track_id.to_base62() {
PlayerEvent::TrackChanged { audio_item } => {
match audio_item.track_id.to_base62() {
Err(e) => {
return Some(Err(Error::new(
ErrorKind::InvalidData,
format!("PlayerEvent::Changed: Invalid old track id: {}", e),
)))
}
Ok(old_id) => match new_track_id.to_base62() {
Err(e) => {
return Some(Err(Error::new(
ErrorKind::InvalidData,
format!("PlayerEvent::Changed: Invalid old track id: {}", e),
)))
}
Ok(new_id) => {
env_vars.insert("PLAYER_EVENT", "changed".to_string());
env_vars.insert("OLD_TRACK_ID", old_id);
env_vars.insert("TRACK_ID", new_id);
}
},
},
PlayerEvent::Started { track_id, .. } => match track_id.to_base62() {
Err(e) => {
return Some(Err(Error::new(
ErrorKind::InvalidData,
format!("PlayerEvent::Started: Invalid track id: {}", e),
)))
warn!("PlayerEvent::TrackChanged: Invalid track id: {}", e)
}
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "started".to_string());
env_vars.insert("PLAYER_EVENT", "track_changed".to_string());
env_vars.insert("TRACK_ID", id);
env_vars.insert("URI", audio_item.uri);
env_vars.insert("NAME", audio_item.name);
env_vars.insert(
"COVERS",
audio_item
.covers
.into_iter()
.map(|c| c.url)
.collect::<Vec<String>>()
.join("\n"),
);
env_vars.insert("LANGUAGE", audio_item.language.join("\n"));
env_vars
.insert("DURATION_MS", audio_item.duration_ms.to_string());
env_vars
.insert("IS_EXPLICIT", audio_item.is_explicit.to_string());
match audio_item.unique_fields {
UniqueFields::Track {
artists,
album,
album_artists,
popularity,
number,
disc_number,
} => {
env_vars.insert("ITEM_TYPE", "Track".to_string());
env_vars.insert(
"ARTISTS",
artists
.0
.into_iter()
.map(|a| a.name)
.collect::<Vec<String>>()
.join("\n"),
);
env_vars
.insert("ALBUM_ARTISTS", album_artists.join("\n"));
env_vars.insert("ALBUM", album);
env_vars.insert("POPULARITY", popularity.to_string());
env_vars.insert("NUMBER", number.to_string());
env_vars.insert("DISC_NUMBER", disc_number.to_string());
}
UniqueFields::Episode {
description,
publish_time,
show_name,
} => {
env_vars.insert("ITEM_TYPE", "Episode".to_string());
env_vars.insert("DESCRIPTION", description);
env_vars.insert(
"PUBLISH_TIME",
publish_time.unix_timestamp().to_string(),
);
env_vars.insert("SHOW_NAME", show_name);
}
}
}
}
}
},
PlayerEvent::Stopped { track_id, .. } => match track_id.to_base62() {
Err(e) => {
return Some(Err(Error::new(
ErrorKind::InvalidData,
format!("PlayerEvent::Stopped: Invalid track id: {}", e),
)))
}
Err(e) => warn!("PlayerEvent::Stopped: Invalid track id: {}", e),
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "stopped".to_string());
env_vars.insert("TRACK_ID", id);
@ -63,86 +98,210 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<Result
},
PlayerEvent::Playing {
track_id,
duration_ms,
position_ms,
..
} => match track_id.to_base62() {
Err(e) => {
return Some(Err(Error::new(
ErrorKind::InvalidData,
format!("PlayerEvent::Playing: Invalid track id: {}", e),
)))
}
Err(e) => warn!("PlayerEvent::Playing: Invalid track id: {}", e),
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "playing".to_string());
env_vars.insert("TRACK_ID", id);
env_vars.insert("DURATION_MS", duration_ms.to_string());
env_vars.insert("POSITION_MS", position_ms.to_string());
}
},
PlayerEvent::Paused {
track_id,
duration_ms,
position_ms,
..
} => match track_id.to_base62() {
Err(e) => {
return Some(Err(Error::new(
ErrorKind::InvalidData,
format!("PlayerEvent::Paused: Invalid track id: {}", e),
)))
}
Err(e) => warn!("PlayerEvent::Paused: Invalid track id: {}", e),
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "paused".to_string());
env_vars.insert("TRACK_ID", id);
env_vars.insert("DURATION_MS", duration_ms.to_string());
env_vars.insert("POSITION_MS", position_ms.to_string());
}
},
PlayerEvent::Preloading { track_id, .. } => match track_id.to_base62() {
Err(e) => {
return Some(Err(Error::new(
ErrorKind::InvalidData,
format!("PlayerEvent::Preloading: Invalid track id: {}", e),
)))
PlayerEvent::Loading { track_id, .. } => match track_id.to_base62() {
Err(e) => warn!("PlayerEvent::Loading: Invalid track id: {}", e),
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "loading".to_string());
env_vars.insert("TRACK_ID", id);
}
},
PlayerEvent::Preloading { track_id, .. } => match track_id.to_base62() {
Err(e) => warn!("PlayerEvent::Preloading: Invalid track id: {}", e),
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "preloading".to_string());
env_vars.insert("TRACK_ID", id);
}
},
PlayerEvent::VolumeSet { volume } => {
env_vars.insert("PLAYER_EVENT", "volume_set".to_string());
PlayerEvent::TimeToPreloadNextTrack { track_id, .. } => {
match track_id.to_base62() {
Err(e) => warn!(
"PlayerEvent::TimeToPreloadNextTrack: Invalid track id: {}",
e
),
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "preload_next".to_string());
env_vars.insert("TRACK_ID", id);
}
}
}
PlayerEvent::EndOfTrack { track_id, .. } => match track_id.to_base62() {
Err(e) => warn!("PlayerEvent::EndOfTrack: Invalid track id: {}", e),
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "end_of_track".to_string());
env_vars.insert("TRACK_ID", id);
}
},
PlayerEvent::Unavailable { track_id, .. } => match track_id.to_base62() {
Err(e) => warn!("PlayerEvent::Unavailable: Invalid track id: {}", e),
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "unavailable".to_string());
env_vars.insert("TRACK_ID", id);
}
},
PlayerEvent::VolumeChanged { volume } => {
env_vars.insert("PLAYER_EVENT", "volume_changed".to_string());
env_vars.insert("VOLUME", volume.to_string());
}
_ => return None,
PlayerEvent::Seeked {
track_id,
position_ms,
..
} => match track_id.to_base62() {
Err(e) => warn!("PlayerEvent::Seeked: Invalid track id: {}", e),
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "seeked".to_string());
env_vars.insert("TRACK_ID", id);
env_vars.insert("POSITION_MS", position_ms.to_string());
}
},
PlayerEvent::PositionCorrection {
track_id,
position_ms,
..
} => match track_id.to_base62() {
Err(e) => {
warn!("PlayerEvent::PositionCorrection: Invalid track id: {}", e)
}
Ok(id) => {
env_vars.insert("PLAYER_EVENT", "position_correction".to_string());
env_vars.insert("TRACK_ID", id);
env_vars.insert("POSITION_MS", position_ms.to_string());
}
},
PlayerEvent::SessionConnected {
connection_id,
user_name,
} => {
env_vars.insert("PLAYER_EVENT", "session_connected".to_string());
env_vars.insert("CONNECTION_ID", connection_id);
env_vars.insert("USER_NAME", user_name);
}
PlayerEvent::SessionDisconnected {
connection_id,
user_name,
} => {
env_vars.insert("PLAYER_EVENT", "session_disconnected".to_string());
env_vars.insert("CONNECTION_ID", connection_id);
env_vars.insert("USER_NAME", user_name);
}
PlayerEvent::SessionClientChanged {
client_id,
client_name,
client_brand_name,
client_model_name,
} => {
env_vars.insert("PLAYER_EVENT", "session_client_changed".to_string());
env_vars.insert("CLIENT_ID", client_id);
env_vars.insert("CLIENT_NAME", client_name);
env_vars.insert("CLIENT_BRAND_NAME", client_brand_name);
env_vars.insert("CLIENT_MODEL_NAME", client_model_name);
}
PlayerEvent::ShuffleChanged { shuffle } => {
env_vars.insert("PLAYER_EVENT", "shuffle_changed".to_string());
env_vars.insert("SHUFFLE", shuffle.to_string());
}
PlayerEvent::RepeatChanged { repeat } => {
env_vars.insert("PLAYER_EVENT", "repeat_changed".to_string());
env_vars.insert("REPEAT", repeat.to_string());
}
PlayerEvent::AutoPlayChanged { auto_play } => {
env_vars.insert("PLAYER_EVENT", "auto_play_changed".to_string());
env_vars.insert("AUTO_PLAY", auto_play.to_string());
}
let mut v: Vec<&str> = onevent.split_whitespace().collect();
info!("Running {:?} with environment variables {:?}", v, env_vars);
Some(
AsyncCommand::new(&v.remove(0))
.args(&v)
.envs(env_vars.iter())
.spawn(),
)
PlayerEvent::FilterExplicitContentChanged { filter } => {
env_vars.insert(
"PLAYER_EVENT",
"filter_explicit_content_changed".to_string(),
);
env_vars.insert("FILTER", filter.to_string());
}
}
pub fn emit_sink_event(sink_status: SinkStatus, onevent: &str) -> Result<ExitStatus> {
if !env_vars.is_empty() {
run_program(env_vars, &on_event);
}
}
}
}));
Self { thread_handle }
}
}
impl Drop for EventHandler {
fn drop(&mut self) {
debug!("Shutting down EventHandler thread ...");
if let Some(handle) = self.thread_handle.take() {
if let Err(e) = handle.join() {
error!("EventHandler thread Error: {:?}", e);
}
}
}
}
pub fn run_program_on_sink_events(sink_status: SinkStatus, onevent: &str) {
let mut env_vars = HashMap::new();
env_vars.insert("PLAYER_EVENT", "sink".to_string());
let sink_status = match sink_status {
SinkStatus::Running => "running",
SinkStatus::TemporarilyClosed => "temporarily_closed",
SinkStatus::Closed => "closed",
};
env_vars.insert("SINK_STATUS", sink_status.to_string());
let mut v: Vec<&str> = onevent.split_whitespace().collect();
info!("Running {:?} with environment variables {:?}", v, env_vars);
Command::new(&v.remove(0))
env_vars.insert("SINK_STATUS", sink_status.to_string());
run_program(env_vars, onevent);
}
fn run_program(env_vars: HashMap<&str, String>, onevent: &str) {
let mut v: Vec<&str> = onevent.split_whitespace().collect();
debug!(
"Running {} with environment variables:\n{:#?}",
onevent, env_vars
);
match Command::new(&v.remove(0))
.args(&v)
.envs(env_vars.iter())
.spawn()?
.wait()
.spawn()
{
Err(e) => warn!("On event program {} failed to start: {}", onevent, e),
Ok(mut child) => match child.wait() {
Err(e) => warn!("On event program {} failed: {}", onevent, e),
Ok(e) if e.success() => (),
Ok(e) => {
if let Some(code) = e.code() {
warn!("On event program {} returned exit code {}", onevent, code);
} else {
warn!("On event program {} returned failure: {}", onevent, e);
}
}
},
}
}