Skip to content

Commit

Permalink
feat: add format info to hooks
Browse files Browse the repository at this point in the history
Adds:
- Audio format and bitrate info to track_changed hook event
- ToF32 implementations for u64 with range clamping
- Documentation updates for hook and utility functions
  • Loading branch information
roderickvd committed Dec 28, 2024
1 parent 21c2523 commit 33ebda6
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 14 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).

### Added
- [main] Support for SIGHUP to reload configuration
- [remote] Add audio format and bitrate to `track_changed` event
- [signal] New module for unified signal handling across platforms

### Changed
- [main] Improved signal handling and graceful shutdown
- [docs] Enhanced documentation for signal handling and lifecycle management
- [main] Improved signal handling and graceful shutdown

## [0.7.0] - 2024-12-28

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ Variables:
`{format}` is either `jpg` (smaller file size) or `png` (higher quality).
Deezer's default is 500x500.jpg
- `DURATION`: Track duration in seconds
- `FORMAT`: The audio format and bitrate (e.g., "MP3 320K" showing constant bitrate, or "FLAC 1.234M" showing variable bitrate)
#### `connected`
Emitted when a Deezer client connects to control playback
Expand Down
91 changes: 81 additions & 10 deletions src/protocol/connect/contents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1048,20 +1048,23 @@ impl fmt::Display for RepeatMode {
}

#[expect(clippy::doc_markdown)]
/// Audio quality levels in the Deezer Connect protocol.
///
/// Represents the different audio quality tiers available in Deezer,
/// corresponding to different bitrates and formats. Note that the remote
/// device cannot control the audio quality - it can only report it.
///
/// # Quality Levels
///
/// * `Basic` - 64 kbps MP3
/// * `Standard` - 128 kbps MP3 (default)
/// * `High` - 320 kbps MP3 (Premium subscription)
/// * `Lossless` - 1411 kbps FLAC (HiFi subscription)
/// * `Basic` - 64 kbps constant bitrate MP3
/// * `Standard` - 128 kbps constant bitrate MP3 (default)
/// * `High` - 320 kbps constant bitrate MP3 (Premium subscription)
/// * `Lossless` - FLAC lossless compression (HiFi subscription)
/// * `Unknown` - Unrecognized quality level
///
/// # Bitrates
///
/// Use the [`bitrate`](Self::bitrate) method to get the nominal bitrate
/// for each quality level in kbps. MP3 formats use constant bitrates,
/// while FLAC uses variable bitrate compression - its actual bitrate
/// varies depending on the audio content's complexity, typically much
/// lower than the theoretical maximum of 1411 kbps for 16-bit/44.1kHz
/// stereo audio.
///
/// # Subscription Requirements
///
/// Different quality levels require specific subscription tiers:
Expand Down Expand Up @@ -1162,6 +1165,74 @@ pub enum AudioQuality {
Unknown = -1,
}

impl AudioQuality {
#[expect(clippy::doc_markdown)]
/// Audio quality levels in the Deezer Connect protocol.
///
/// Represents the different audio quality tiers available in Deezer,
/// corresponding to different codecs and bitrates. Note that the remote
/// device cannot control the audio quality - it can only report it.
///
/// # Quality Levels
///
/// * `Basic` - MP3 at 64 kbps constant bitrate
/// * `Standard` - MP3 at 128 kbps constant bitrate (default)
/// * `High` - MP3 at 320 kbps constant bitrate (Premium subscription)
/// * `Lossless` - FLAC variable bitrate compression (HiFi subscription)
/// * `Unknown` - Unrecognized quality level
///
/// # Format Information
///
/// Use the following methods to get format details:
/// * [`codec`](Self::codec) - Get the audio codec (MP3 or FLAC)
/// * [`bitrate`](Self::bitrate) - Get the bitrate in kbps
///
/// Note that while MP3 formats use constant bitrates, FLAC uses variable
/// bitrate compression - its actual bitrate varies with audio content
/// complexity, typically much lower than the maximum of 1411 kbps for
/// 16-bit/44.1kHz stereo audio.
#[must_use]
pub fn bitrate(&self) -> Option<usize> {
let bitrate = match self {
AudioQuality::Unknown => return None,
AudioQuality::Basic => 64,
AudioQuality::Standard => 128,
AudioQuality::High => 320,
AudioQuality::Lossless => 1411,
};

Some(bitrate)
}

/// Returns the audio codec name for this quality level.
///
/// # Returns
///
/// * `Some("MP3")` - For Basic, Standard, and High quality (constant bitrate)
/// * `Some("FLAC")` - For Lossless quality (variable bitrate)
/// * `None` - For Unknown quality
///
/// # Examples
///
/// ```rust
/// assert_eq!(AudioQuality::Basic.codec(), Some("MP3"));
/// assert_eq!(AudioQuality::Standard.codec(), Some("MP3"));
/// assert_eq!(AudioQuality::High.codec(), Some("MP3"));
/// assert_eq!(AudioQuality::Lossless.codec(), Some("FLAC"));
/// assert_eq!(AudioQuality::Unknown.codec(), None);
/// ```
#[must_use]
pub fn codec(&self) -> Option<&str> {
let codec = match self {
AudioQuality::Unknown => return None,
AudioQuality::Lossless => "FLAC",
_ => "MP3",
};

Some(codec)
}
}

/// Formats the audio quality for human-readable output.
///
/// # Examples
Expand Down
31 changes: 28 additions & 3 deletions src/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,13 @@ use crate::{
player::Player,
protocol::connect::{
queue::{self, ContainerType, MixType},
stream, Body, Channel, Contents, DeviceId, DeviceType, Headers, Ident, Message, Percentage,
QueueItem, RepeatMode, Status, UserId,
stream, AudioQuality, Body, Channel, Contents, DeviceId, DeviceType, Headers, Ident,
Message, Percentage, QueueItem, RepeatMode, Status, UserId,
},
proxy,
tokens::UserToken,
track::{Track, TrackId},
util::ToF32,
};

/// A client on the Deezer Connect protocol.
Expand Down Expand Up @@ -714,6 +715,29 @@ impl Client {
Event::TrackChanged => {
if let Some(track) = self.player.track() {
if let Some(command) = command.as_mut() {
let quality = track.quality();
let codec = quality.codec().unwrap_or("Unknown");
let bitrate = match quality {
AudioQuality::Lossless | AudioQuality::Unknown => track
.file_size()
.unwrap_or_default()
.checked_div(track.duration().as_secs())
.map(|bytes| bytes * 8 / 1024),
_ => quality.bitrate().map(|kbps| kbps as u64),
};

let bitrate = match bitrate {
Some(bitrate) => {
if bitrate >= 1000 {
format!("{}M", bitrate.to_f32_lossy() / 1000.)
} else {
format!("{bitrate}K")
}
}
// If bitrate is unknown, show codec only.
None => String::default(),
};

command
.env("EVENT", "track_changed")
.env("TRACK_ID", shell_escape(&track.id().to_string()))
Expand All @@ -724,7 +748,8 @@ impl Client {
.env(
"DURATION",
shell_escape(&track.duration().as_secs().to_string()),
);
)
.env("FORMAT", shell_escape(&format!("{codec} {bitrate}")));
}
}
}
Expand Down
28 changes: 28 additions & 0 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,34 @@ impl ToF32 for f64 {
}
}

/// Implements conversion from `u64` to `f32` with range clamping.
///
/// Clamps the value to the valid `f32` range before truncating:
/// * `u64` values beyond `f32::MAX` become `f32::MAX`
/// * `u64` values below `f32::MIN` (0) are impossible due to unsigned type
///
/// # Example
///
/// ```rust
/// use pleezer::util::ToF32;
///
/// let too_large = u64::MAX;
/// let clamped = too_large.to_f32_lossy();
/// assert!(clamped == f32::MAX);
/// ```
impl ToF32 for u64 {
#[expect(clippy::cast_possible_truncation)]
#[expect(clippy::cast_precision_loss)]
#[expect(clippy::cast_sign_loss)]
fn to_f32_lossy(self) -> f32 {
if self > f32::MAX as u64 {
f32::MAX
} else {
self as f32
}
}
}

/// Implements conversion from `u128` to `f32` with range clamping.
///
/// Clamps the value to the valid `f32` range before truncating:
Expand Down

0 comments on commit 33ebda6

Please sign in to comment.