mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Merge pull request #1053 from JasonLG1979/connect-event
Major events overhaul
This commit is contained in:
commit
ccd501d22e
17 changed files with 1239 additions and 639 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
if matches!(cmd, SpircCommand::Shutdown) {
|
||||
trace!("Received SpircCommand::Shutdown");
|
||||
CommandSender::new(self, MessageType::kMessageTypeGoodbye).send()?;
|
||||
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)
|
||||
} else {
|
||||
CommandSender::new(self, MessageType::kMessageTypePlay).send()
|
||||
}
|
||||
}
|
||||
SpircCommand::PlayPause => {
|
||||
if active {
|
||||
SpircCommand::PlayPause => {
|
||||
self.handle_play_pause();
|
||||
self.notify(None)
|
||||
} else {
|
||||
CommandSender::new(self, MessageType::kMessageTypePlayPause).send()
|
||||
}
|
||||
}
|
||||
SpircCommand::Pause => {
|
||||
if active {
|
||||
SpircCommand::Pause => {
|
||||
self.handle_pause();
|
||||
self.notify(None)
|
||||
} else {
|
||||
CommandSender::new(self, MessageType::kMessageTypePause).send()
|
||||
}
|
||||
}
|
||||
SpircCommand::Prev => {
|
||||
if active {
|
||||
SpircCommand::Prev => {
|
||||
self.handle_prev();
|
||||
self.notify(None)
|
||||
} else {
|
||||
CommandSender::new(self, MessageType::kMessageTypePrev).send()
|
||||
}
|
||||
}
|
||||
SpircCommand::Next => {
|
||||
if active {
|
||||
SpircCommand::Next => {
|
||||
self.handle_next();
|
||||
self.notify(None)
|
||||
} else {
|
||||
CommandSender::new(self, MessageType::kMessageTypeNext).send()
|
||||
}
|
||||
}
|
||||
SpircCommand::VolumeUp => {
|
||||
if active {
|
||||
SpircCommand::VolumeUp => {
|
||||
self.handle_volume_up();
|
||||
self.notify(None)
|
||||
} else {
|
||||
CommandSender::new(self, MessageType::kMessageTypeVolumeUp).send()
|
||||
}
|
||||
}
|
||||
SpircCommand::VolumeDown => {
|
||||
if active {
|
||||
SpircCommand::VolumeDown => {
|
||||
self.handle_volume_down();
|
||||
self.notify(None)
|
||||
} else {
|
||||
CommandSender::new(self, MessageType::kMessageTypeVolumeDown).send()
|
||||
}
|
||||
}
|
||||
SpircCommand::Shutdown => {
|
||||
CommandSender::new(self, MessageType::kMessageTypeGoodbye).send()?;
|
||||
self.player.stop();
|
||||
self.shutdown = true;
|
||||
if let Some(rx) = self.commands.as_mut() {
|
||||
rx.close()
|
||||
SpircCommand::Disconnect => {
|
||||
self.handle_disconnect();
|
||||
self.notify(None)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
SpircCommand::Shuffle => {
|
||||
CommandSender::new(self, MessageType::kMessageTypeShuffle).send()
|
||||
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");
|
||||
self.state.set_status(PlayStatus::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;
|
||||
self.update_state_position(new_position_ms);
|
||||
self.notify(None)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
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)
|
||||
}
|
||||
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,12 +1379,10 @@ 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" {
|
||||
info!("Fetching autoplay context uri");
|
||||
// Get autoplay_station_uri for regular playlists
|
||||
self.autoplay_fut = self.resolve_autoplay_uri(&context_uri);
|
||||
}
|
||||
} 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
|
||||
|
@ -1422,12 +1517,18 @@ impl SpircTask {
|
|||
}
|
||||
|
||||
fn set_volume(&mut self, volume: u16) {
|
||||
self.device.set_volume(volume as u32);
|
||||
self.mixer.set_volume(volume);
|
||||
if let Some(cache) = self.session.cache() {
|
||||
cache.save_volume(volume)
|
||||
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)
|
||||
}
|
||||
if self.device.get_is_active() {
|
||||
self.player.emit_volume_changed_event(volume);
|
||||
}
|
||||
}
|
||||
self.player.emit_volume_set_event(volume);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
77
contrib/event_handler_example.py
Normal file
77
contrib/event_handler_example.py
Normal 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))
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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)
|
||||
});
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,98 +19,259 @@ 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 {
|
||||
let country = &user_data.country;
|
||||
let user_catalogue = match user_data.attributes.get("catalogue") {
|
||||
Some(catalogue) => catalogue,
|
||||
None => "premium",
|
||||
};
|
||||
covers.sort_by(|a, b| b.width.cmp(&a.width));
|
||||
|
||||
for premium_restriction in restrictions.iter().filter(|restriction| {
|
||||
restriction
|
||||
.catalogue_strs
|
||||
.iter()
|
||||
.any(|restricted_catalogue| restricted_catalogue == user_catalogue)
|
||||
}) {
|
||||
if let Some(allowed_countries) = &premium_restriction.countries_allowed {
|
||||
// A restriction will specify either a whitelast *or* a blacklist,
|
||||
// but not both. So restrict availability if there is a whitelist
|
||||
// and the country isn't on it.
|
||||
if allowed_countries.iter().any(|allowed| country == allowed) {
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(UnavailabilityReason::NotWhitelisted);
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
if let Some(forbidden_countries) = &premium_restriction.countries_forbidden {
|
||||
if forbidden_countries
|
||||
.iter()
|
||||
.any(|forbidden| country == forbidden)
|
||||
{
|
||||
return Err(UnavailabilityReason::Blacklisted);
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(()) // no restrictions in place
|
||||
}
|
||||
|
||||
fn available(availability: &Availabilities) -> AudioItemAvailability {
|
||||
if availability.is_empty() {
|
||||
// not all items have availability specified
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !(availability
|
||||
.iter()
|
||||
.any(|availability| Date::now_utc() >= availability.start))
|
||||
{
|
||||
return Err(UnavailabilityReason::Embargo);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn available_for_user(
|
||||
user_data: &UserData,
|
||||
availability: &Availabilities,
|
||||
restrictions: &Restrictions,
|
||||
) -> AudioItemAvailability {
|
||||
Self::available(availability)?;
|
||||
Self::allowed_for_user(user_data, restrictions)?;
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.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,
|
||||
None => "premium",
|
||||
};
|
||||
|
||||
for premium_restriction in restrictions.iter().filter(|restriction| {
|
||||
restriction
|
||||
.catalogue_strs
|
||||
.iter()
|
||||
.any(|restricted_catalogue| restricted_catalogue == user_catalogue)
|
||||
}) {
|
||||
if let Some(allowed_countries) = &premium_restriction.countries_allowed {
|
||||
// A restriction will specify either a whitelast *or* a blacklist,
|
||||
// but not both. So restrict availability if there is a whitelist
|
||||
// and the country isn't on it.
|
||||
if allowed_countries.iter().any(|allowed| country == allowed) {
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(UnavailabilityReason::NotWhitelisted);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(forbidden_countries) = &premium_restriction.countries_forbidden {
|
||||
if forbidden_countries
|
||||
.iter()
|
||||
.any(|forbidden| country == forbidden)
|
||||
{
|
||||
return Err(UnavailabilityReason::Blacklisted);
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(()) // no restrictions in place
|
||||
}
|
||||
|
||||
fn available(availability: &Availabilities) -> AudioItemAvailability {
|
||||
if availability.is_empty() {
|
||||
// not all items have availability specified
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !(availability
|
||||
.iter()
|
||||
.any(|availability| Date::now_utc() >= availability.start))
|
||||
{
|
||||
return Err(UnavailabilityReason::Embargo);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn available_for_user(
|
||||
user_data: &UserData,
|
||||
availability: &Availabilities,
|
||||
restrictions: &Restrictions,
|
||||
) -> AudioItemAvailability {
|
||||
available(availability)?;
|
||||
allowed_for_user(user_data, restrictions)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ pub mod file;
|
|||
pub mod item;
|
||||
|
||||
pub use file::{AudioFileFormat, AudioFiles};
|
||||
pub use item::AudioItem;
|
||||
pub use item::{AudioItem, UniqueFields};
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -442,14 +442,16 @@ impl Sink for AlsaSink {
|
|||
}
|
||||
|
||||
fn stop(&mut self) -> SinkResult<()> {
|
||||
// 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);
|
||||
self.write_buf()?;
|
||||
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);
|
||||
self.write_buf()?;
|
||||
|
||||
let pcm = self.pcm.take().ok_or(AlsaError::NotConnected)?;
|
||||
let pcm = self.pcm.take().ok_or(AlsaError::NotConnected)?;
|
||||
|
||||
pcm.drain().map_err(AlsaError::DrainFailure)?;
|
||||
pcm.drain().map_err(AlsaError::DrainFailure)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -489,17 +491,29 @@ impl AlsaSink {
|
|||
pub const NAME: &'static str = "alsa";
|
||||
|
||||
fn write_buf(&mut self) -> SinkResult<()> {
|
||||
let pcm = self.pcm.as_mut().ok_or(AlsaError::NotConnected)?;
|
||||
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) {
|
||||
// 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!(
|
||||
"Error writing from AlsaSink buffer to PCM, trying to recover, {}",
|
||||
e
|
||||
);
|
||||
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!(
|
||||
"Error writing from AlsaSink buffer to PCM, trying to recover, {}",
|
||||
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();
|
||||
|
|
|
@ -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 {
|
||||
commands: Some(cmd_tx),
|
||||
thread_handle: Some(handle),
|
||||
play_request_id_generator: SeqGenerator::new(0),
|
||||
},
|
||||
event_receiver,
|
||||
)
|
||||
Self {
|
||||
commands: Some(cmd_tx),
|
||||
thread_handle: Some(handle),
|
||||
play_request_id_generator: SeqGenerator::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
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,25 +1495,24 @@ impl PlayerInternal {
|
|||
}
|
||||
|
||||
fn handle_pause(&mut self) {
|
||||
if let PlayerState::Playing {
|
||||
track_id,
|
||||
play_request_id,
|
||||
stream_position_ms,
|
||||
duration_ms,
|
||||
..
|
||||
} = self.state
|
||||
{
|
||||
self.state.playing_to_paused();
|
||||
|
||||
self.ensure_sink_stopped(false);
|
||||
self.send_event(PlayerEvent::Paused {
|
||||
match self.state {
|
||||
PlayerState::Paused { .. } => self.ensure_sink_stopped(false),
|
||||
PlayerState::Playing {
|
||||
track_id,
|
||||
play_request_id,
|
||||
position_ms: stream_position_ms,
|
||||
duration_ms,
|
||||
});
|
||||
} else {
|
||||
error!("Player::pause called from invalid state: {:?}", self.state);
|
||||
stream_position_ms,
|
||||
..
|
||||
} => {
|
||||
self.state.playing_to_paused();
|
||||
|
||||
self.ensure_sink_stopped(false);
|
||||
self.send_event(PlayerEvent::Paused {
|
||||
track_id,
|
||||
play_request_id,
|
||||
position_ms: stream_position_ms,
|
||||
});
|
||||
}
|
||||
_ => 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,38 +1744,12 @@ 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 { .. } => {
|
||||
return Err(Error::internal(format!(
|
||||
"Player::handle_command_load called from invalid state: {:?}",
|
||||
self.state
|
||||
)));
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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,34 +2050,78 @@ 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() => {
|
||||
if let PlayerState::Playing {
|
||||
track_id,
|
||||
play_request_id,
|
||||
is_explicit,
|
||||
..
|
||||
}
|
||||
| PlayerState::Paused {
|
||||
track_id,
|
||||
play_request_id,
|
||||
is_explicit,
|
||||
..
|
||||
} = self.state
|
||||
{
|
||||
if is_explicit {
|
||||
warn!("Currently loaded track is explicit, which client setting forbids -- skipping to next track.");
|
||||
self.send_event(PlayerEvent::EndOfTrack {
|
||||
track_id,
|
||||
play_request_id,
|
||||
})
|
||||
PlayerCommand::EmitFilterExplicitContentChangedEvent(filter) => {
|
||||
self.send_event(PlayerEvent::FilterExplicitContentChanged { filter });
|
||||
|
||||
if filter {
|
||||
if let PlayerState::Playing {
|
||||
track_id,
|
||||
play_request_id,
|
||||
is_explicit,
|
||||
..
|
||||
}
|
||||
| PlayerState::Paused {
|
||||
track_id,
|
||||
play_request_id,
|
||||
is_explicit,
|
||||
..
|
||||
} = self.state
|
||||
{
|
||||
if is_explicit {
|
||||
warn!("Currently loaded track is explicit, which client setting forbids -- skipping to next track.");
|
||||
self.send_event(PlayerEvent::EndOfTrack {
|
||||
track_id,
|
||||
play_request_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
72
src/main.rs
72
src/main.rs
|
@ -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 || {
|
||||
(backend)(device, format)
|
||||
});
|
||||
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() {
|
||||
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;
|
||||
},
|
||||
|
|
|
@ -1,148 +1,307 @@
|
|||
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};
|
||||
|
||||
use librespot::playback::player::{PlayerEvent, SinkStatus};
|
||||
|
||||
pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<Result<AsyncChild>> {
|
||||
let mut env_vars = HashMap::new();
|
||||
match event {
|
||||
PlayerEvent::Changed {
|
||||
old_track_id,
|
||||
new_track_id,
|
||||
} => match old_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),
|
||||
)))
|
||||
}
|
||||
Ok(id) => {
|
||||
env_vars.insert("PLAYER_EVENT", "started".to_string());
|
||||
env_vars.insert("TRACK_ID", id);
|
||||
}
|
||||
},
|
||||
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),
|
||||
)))
|
||||
}
|
||||
Ok(id) => {
|
||||
env_vars.insert("PLAYER_EVENT", "stopped".to_string());
|
||||
env_vars.insert("TRACK_ID", id);
|
||||
}
|
||||
},
|
||||
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),
|
||||
)))
|
||||
}
|
||||
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),
|
||||
)))
|
||||
}
|
||||
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),
|
||||
)))
|
||||
}
|
||||
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());
|
||||
env_vars.insert("VOLUME", volume.to_string());
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
|
||||
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(),
|
||||
)
|
||||
pub struct EventHandler {
|
||||
thread_handle: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
pub fn emit_sink_event(sink_status: SinkStatus, onevent: &str) -> Result<ExitStatus> {
|
||||
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::TrackChanged { audio_item } => {
|
||||
match audio_item.track_id.to_base62() {
|
||||
Err(e) => {
|
||||
warn!("PlayerEvent::TrackChanged: Invalid track id: {}", e)
|
||||
}
|
||||
Ok(id) => {
|
||||
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) => warn!("PlayerEvent::Stopped: Invalid track id: {}", e),
|
||||
Ok(id) => {
|
||||
env_vars.insert("PLAYER_EVENT", "stopped".to_string());
|
||||
env_vars.insert("TRACK_ID", id);
|
||||
}
|
||||
},
|
||||
PlayerEvent::Playing {
|
||||
track_id,
|
||||
position_ms,
|
||||
..
|
||||
} => match track_id.to_base62() {
|
||||
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("POSITION_MS", position_ms.to_string());
|
||||
}
|
||||
},
|
||||
PlayerEvent::Paused {
|
||||
track_id,
|
||||
position_ms,
|
||||
..
|
||||
} => match track_id.to_base62() {
|
||||
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("POSITION_MS", position_ms.to_string());
|
||||
}
|
||||
},
|
||||
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::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());
|
||||
}
|
||||
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());
|
||||
}
|
||||
|
||||
PlayerEvent::FilterExplicitContentChanged { filter } => {
|
||||
env_vars.insert(
|
||||
"PLAYER_EVENT",
|
||||
"filter_explicit_content_changed".to_string(),
|
||||
);
|
||||
env_vars.insert("FILTER", filter.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue