Skip to content

Commit

Permalink
feat: add hook script support for playback and connection events
Browse files Browse the repository at this point in the history
  • Loading branch information
roderickvd committed Nov 22, 2024
1 parent a0630d6 commit 8ce6374
Show file tree
Hide file tree
Showing 13 changed files with 280 additions and 72 deletions.
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
- [main] Support for configuring all command-line options via environment variables with `PLEEZER_` prefix
- [proxy] HTTPS proxy support via the `HTTPS_PROXY` environment variable
- [remote] Websocket monitoring mode for Deezer Connect protocol analysis
- [remote] Hook script support to execute commands on playback and connection events

### Changed
- [docs] Enhanced documentation clarity and consistency across all policy documents
Expand All @@ -27,8 +28,6 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
### Security
- [arl] Prevent ARL token exposure in debug logs

[Unreleased]: https://github.com/roderickvd/pleezer/compare/v0.1.0...HEAD

## [0.1.0] - 2024-02-20

Initial release of pleezer, a headless streaming player for the Deezer Connect protocol.
Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ serde_with = { version = "3.11", default-features = false, features = [
"macros",
"std",
] }
shell-escape = "0.1"
stream-download = { version = "0.13", features = ["reqwest-rustls"] }
sysinfo = { version = "0.32", default-features = false, features = ["system"] }
thiserror = "2"
Expand Down
75 changes: 69 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- [Command-Line Arguments](#command-line-arguments)
- [Environment Variables](#environment-variables)
- [Proxy Configuration](#proxy-configuration)
- [Hook Scripts](#hook-scripts)
- [Stateless Configuration](#stateless-configuration)
- [Configuring the Secrets File](#configuring-the-secrets-file)
- [Troubleshooting](#troubleshooting)
Expand Down Expand Up @@ -140,6 +141,12 @@ Your music will start playing on the selected device.
pleezer --no-interruptions
```

- `--hook`: Specify a script to execute when events occur (see [Hook Scripts](#hook-scripts) for details). Example:
```bash
pleezer --hook /path/to/script.sh
```
**Note:** The script must be executable and have a shebang line.

- `-q` or `--quiet`: Suppresses all output except warnings and errors. Example:
```bash
pleezer -q
Expand Down Expand Up @@ -190,25 +197,81 @@ Command-line arguments take precedence over environment variables if both are se

### Proxy Configuration

**pleezer** supports proxy connections through the `HTTPS_PROXY` environment variable. he value must include either the `http://` or `https://` schema prefix.
**pleezer** supports proxy connections through the `HTTPS_PROXY` environment variable. The value must include either the `http://` or `https://` schema prefix. HTTPS can be tunneled over either HTTP or HTTPS proxies.

Examples:

```bash
# Linux/macOS
export HTTPS_PROXY="http://proxy.example.com:8080"
export HTTPS_PROXY="https://proxy.example.com:8080"
export HTTPS_PROXY="http://proxy.example.com:8080" # HTTPS over HTTP proxy
export HTTPS_PROXY="https://proxy.example.com:8080" # HTTPS over HTTPS proxy
# Windows (Command Prompt)
set HTTPS_PROXY=https://proxy.company.com:8080
set HTTPS_PROXY=https://proxy.example.com:8080
# Windows (PowerShell)
$env:HTTPS_PROXY="https://proxy.company.com:8080"
$env:HTTPS_PROXY="https://proxy.example.com:8080"
```

The proxy settings will be automatically detected and used for all Deezer Connect connections.

### Hook Scripts

You can use the `--hook` option to specify a script that will be executed when certain events occur. The script receives event information through environment variables.

For all events, the `EVENT` variable contains the event name. Additional variables depend on the specific event:

`playing`
Emitted when playback starts
Variables:
- `TRACK_ID`: The ID of the track being played

`paused`
Emitted when playback is paused
No additional variables

`track_changed`
Emitted when the track changes
Variables:
- `TRACK_ID`: The ID of the track
- `TITLE`: The track title
- `ARTIST`: The main artist name
- `ALBUM_TITLE`: The album title
- `ALBUM_COVER`: The album cover ID, which can be used to construct image URLs:
```
https://e-cdns-images.dzcdn.net/images/cover/{album_cover}/{resolution}x{resolution}.{format}
```
where `{resolution}` is the desired resolution in pixels (up to 1920) and
`{format}` is either `jpg` (smaller file size) or `png` (higher quality).
Deezer's default is 500x500.jpg
- `DURATION`: Track duration in seconds
`connected`
Emitted when a Deezer client connects to control playback
Variables:
- `USER_ID`: The Deezer user ID
- `USER_NAME`: The Deezer account name
`disconnected`
Emitted when the controlling Deezer client disconnects
No additional variables
All string values are properly escaped for shell safety. Example usage:
```bash
#!/bin/bash
# example-hook.sh
echo "Event: $EVENT"
case "$EVENT" in
"track_changed")
echo "Track changed to: $TITLE by $ARTIST"
;;
"connected")
echo "Connected as: $USER_NAME"
;;
esac
```
### Stateless Configuration
**pleezer** operates statelessly and loads user settings, such as normalization and audio quality, when it connects. To apply changes, disconnect and reconnect. This limitation is due to the Deezer Connect protocol.
Expand Down
3 changes: 3 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ pub struct Config {
/// By default this is `true`.
pub interruptions: bool,

/// Script to execute when events occur
pub hook: Option<String>,

/// The client ID used in API requests.
///
/// By default this is a random number of 9 digits.
Expand Down
27 changes: 16 additions & 11 deletions src/events.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
use crate::track::TrackId;

#[derive(Clone, Debug)]
/// Events that can be emitted by the Deezer Connect player or remote.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Event {
Play(TrackId),
// TODO - proposals:
// QueueChanged(Queue),
// PlayingChanged(bool),
// ShuffleChanged(bool),
// RepeatModeChanged(RepeatMode),
// VolumeChanged(Percentage),
// ProgressChanged(Percentage),
/// Event emitted when the player has started playing a track.
Play,

/// Event emitted when the player has paused a track.
Pause,

/// Event emitted when the player has changed the track.
TrackChanged,

/// Event emitted when a remote control has connected.
Connected,

/// Event emitted when a remote control has disconnected.
Disconnected,
}
11 changes: 10 additions & 1 deletion src/gateway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,19 @@ impl Gateway {
.map(|data| data.user.settings.site.player_normalize)
}

pub fn target_gain(&self) -> Option<f32> {
/// The reference level for normalization.
pub fn target_gain(&self) -> Option<i8> {
self.user_data.as_ref().map(|data| data.gain.target)
}

/// The user's account name.
pub fn user_name(&self) -> &str {
self.user_data
.as_ref()
.map(|data| data.user.name.as_str())
.unwrap_or_default()
}

/// Converts a list of tracks from the Deezer API to a [`Queue`].
///
/// # Errors
Expand Down
5 changes: 5 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ struct Args {
#[arg(long, default_value_t = false, env = "PLEEZER_NO_INTERRUPTIONS")]
no_interruptions: bool,

/// Script to execute when events occur
#[arg(long, value_hint = ValueHint::ExecutablePath, env = "PLEEZER_HOOK")]
hook: Option<String>,

/// Suppress all output except warnings and errors
#[arg(short, long, default_value_t = false, group = ARGS_GROUP_LOGGING, env = "PLEEZER_QUIET")]
quiet: bool,
Expand Down Expand Up @@ -277,6 +281,7 @@ async fn run(args: Args) -> Result<()> {
device_id,

interruptions: !args.no_interruptions,
hook: args.hook,

client_id,
user_agent,
Expand Down
64 changes: 35 additions & 29 deletions src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ pub struct Player {
normalization: bool,

/// The target volume to normalize to in dB.
gain_target_db: f32,
gain_target_db: i8,

/// The channel to send playback events to.
event_tx: Option<tokio::sync::mpsc::UnboundedSender<Event>>,
Expand All @@ -82,7 +82,7 @@ pub struct Player {
}

/// The default target volume to normalize to in dB LUFS.
pub const DEFAULT_GAIN_TARGET_DB: f32 = -15.0;
pub const DEFAULT_GAIN_TARGET_DB: i8 = -15;

impl Player {
/// Creates a new `Player` with the given `Config`.
Expand Down Expand Up @@ -294,23 +294,30 @@ impl Player {
}

fn go_next(&mut self) {
let old_position = self.position;
let repeat_mode = self.repeat_mode();
if repeat_mode != RepeatMode::One {
let next = self.position.saturating_add(1);
if next < self.queue.len() {
// Move to the next track.
self.position = next;
self.notify_play();
} else {
// Reached the end of the queue: rewind to the beginning.
if repeat_mode != RepeatMode::All {
// Using this instead of `pause()` ensures that we only get a notification
// if the player was actually playing.
self.set_playing(false);
self.pause();
};
self.position = 0;
}
}

if self.position() != old_position {
self.notify(Event::TrackChanged);
}

// Even if we were already playing, we need to report another playback stream.
if self.is_playing() {
self.notify(Event::Play);
}
}

/// The audio gain control (AGC) attack time.
Expand Down Expand Up @@ -366,7 +373,7 @@ impl Player {
if self.normalization {
match track.gain() {
Some(gain) => {
difference = self.gain_target_db - gain;
difference = f32::from(self.gain_target_db) - gain;

// Keep -1 dBTP of headroom on tracks with lossy decoding to avoid
// clipping due to inter-sample peaks.
Expand Down Expand Up @@ -470,7 +477,7 @@ impl Player {
Ok(rx) => {
if let Some(rx) = rx {
self.current_rx = Some(rx);
self.notify_play();
self.notify(Event::TrackChanged);
}
}
Err(e) => {
Expand All @@ -495,14 +502,10 @@ impl Player {
}
}

fn notify_play(&self) {
if self.is_playing() {
if let Some(track) = self.track() {
if let Some(event_tx) = &self.event_tx {
if let Err(e) = event_tx.send(Event::Play(track.id())) {
error!("failed to send track changed event: {e}");
}
}
fn notify(&self, event: Event) {
if let Some(event_tx) = &self.event_tx {
if let Err(e) = event_tx.send(event) {
error!("failed to send event: {e}");
}
}
}
Expand All @@ -512,16 +515,21 @@ impl Player {
}

pub fn play(&mut self) {
debug!("starting playback");
self.sink.play();
if !self.is_playing() {
debug!("starting playback");
self.sink.play();

// Playback reporting happens every time a track starts playing or is unpaused.
self.notify_play();
// Playback reporting happens every time a track starts playing or is unpaused.
self.notify(Event::Play);
}
}

pub fn pause(&mut self) {
debug!("pausing playback");
self.sink.pause();
if self.is_playing() {
debug!("pausing playback");
self.sink.pause();
self.notify(Event::Pause);
}
}

#[must_use]
Expand All @@ -530,12 +538,10 @@ impl Player {
}

pub fn set_playing(&mut self, should_play: bool) {
if self.is_playing() {
if !should_play {
self.pause();
}
} else if should_play {
if should_play {
self.play();
} else {
self.pause();
}
}

Expand Down Expand Up @@ -706,7 +712,7 @@ impl Player {
self.normalization = normalization;
}

pub fn set_gain_target_db(&mut self, gain_target_db: f32) {
pub fn set_gain_target_db(&mut self, gain_target_db: i8) {
self.gain_target_db = gain_target_db;
}

Expand All @@ -730,7 +736,7 @@ impl Player {
}

#[must_use]
pub fn gain_target_db(&self) -> f32 {
pub fn gain_target_db(&self) -> i8 {
self.gain_target_db
}
}
4 changes: 4 additions & 0 deletions src/protocol/gateway/list_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ pub struct ListData {
pub track_id: TrackId,
#[serde(rename = "ART_NAME")]
pub artist: String,
#[serde(rename = "ALB_TITLE")]
pub album_title: String,
#[serde(rename = "ALB_PICTURE")]
pub album_cover: String,
#[serde_as(as = "DurationSeconds<String>")]
pub duration: Duration,
#[serde(rename = "SNG_TITLE")]
Expand Down
Loading

0 comments on commit 8ce6374

Please sign in to comment.