feat: add MPRIS media controls integration

Backend (Rust):
- Add souvlaki crate for MPRIS support on Linux
- Create media_controls module with MediaControlsManager
- Track metadata (title, artist, album, duration, cover)
- Playback state (playing/paused/stopped)
- Wire pause/resume/stop commands to update MPRIS
- Add set_media_metadata Tauri command

Frontend (Svelte):
- Call set_media_metadata when track starts playing
- Send track info to MPRIS for system integration

Now shows track info in system media controls and
supports media key events (logged for now).
This commit is contained in:
vicrodh committed 2026-01-09 09:30:26 -06:00
1 parent d1a13110ea
commit 62e59f599a
5 files changed
+213

No files matched your search

+142
View File
@@ -0,0 +1,142 @@
//! MPRIS/Media controls integration
//!
//! Provides system-level media control integration:
//! - MPRIS on Linux (D-Bus based)
//! - Media key support
//! - Now playing notifications
use souvlaki::{MediaControlEvent, MediaControls, MediaMetadata, MediaPlayback, PlatformConfig};
use std::sync::{Arc, Mutex};
use std::thread;
/// Track metadata for media controls
#[derive(Debug, Clone, Default)]
pub struct TrackInfo {
pub title: String,
pub artist: String,
pub album: String,
pub duration_secs: Option<u64>,
pub cover_url: Option<String>,
}
/// Media controls manager
pub struct MediaControlsManager {
controls: Arc<Mutex<Option<MediaControls>>>,
}
impl MediaControlsManager {
/// Create a new media controls manager
pub fn new() -> Self {
let controls = Arc::new(Mutex::new(None));
let controls_clone = controls.clone();
// Initialize media controls in a separate thread
// (souvlaki requires a window handle on some platforms)
thread::spawn(move || {
let config = PlatformConfig {
dbus_name: "qbz",
display_name: "QBZ",
hwnd: None, // Not needed on Linux
};
match MediaControls::new(config) {
Ok(mut mc) => {
// Set up event handler for media key presses
// For now, just log events - we'll wire them up later
if let Err(e) = mc.attach(move |event: MediaControlEvent| {
log::info!("Media control event: {:?}", event);
// TODO: Wire these to player commands via Tauri events
}) {
log::error!("Failed to attach media controls handler: {}", e);
return;
}
log::info!("Media controls initialized successfully (MPRIS)");
// Store the controls
if let Ok(mut guard) = controls_clone.lock() {
*guard = Some(mc);
}
}
Err(e) => {
log::warn!("Failed to initialize media controls: {}. Media keys won't work.", e);
}
}
});
Self {
controls,
}
}
/// Update the currently playing track metadata
pub fn set_metadata(&self, track: &TrackInfo) {
if let Ok(mut guard) = self.controls.lock() {
if let Some(controls) = guard.as_mut() {
let metadata = MediaMetadata {
title: Some(track.title.as_str()),
artist: Some(track.artist.as_str()),
album: Some(track.album.as_str()),
duration: track.duration_secs.map(|d| std::time::Duration::from_secs(d)),
cover_url: track.cover_url.as_deref(),
};
if let Err(e) = controls.set_metadata(metadata) {
log::debug!("Failed to set media metadata: {}", e);
}
}
}
}
/// Update playback state
pub fn set_playback(&self, playing: bool) {
if let Ok(mut guard) = self.controls.lock() {
if let Some(controls) = guard.as_mut() {
let playback = if playing {
MediaPlayback::Playing { progress: None }
} else {
MediaPlayback::Paused { progress: None }
};
if let Err(e) = controls.set_playback(playback) {
log::debug!("Failed to set playback state: {}", e);
}
}
}
}
/// Update playback state with progress
pub fn set_playback_with_progress(&self, playing: bool, position_secs: u64) {
if let Ok(mut guard) = self.controls.lock() {
if let Some(controls) = guard.as_mut() {
let progress = Some(souvlaki::MediaPosition(std::time::Duration::from_secs(position_secs)));
let playback = if playing {
MediaPlayback::Playing { progress }
} else {
MediaPlayback::Paused { progress }
};
if let Err(e) = controls.set_playback(playback) {
log::debug!("Failed to set playback state: {}", e);
}
}
}
}
/// Set stopped state (no track playing)
pub fn set_stopped(&self) {
if let Ok(mut guard) = self.controls.lock() {
if let Some(controls) = guard.as_mut() {
if let Err(e) = controls.set_playback(MediaPlayback::Stopped) {
log::debug!("Failed to set stopped state: {}", e);
}
}
}
}
}
impl Default for MediaControlsManager {
fn default() -> Self {
Self::new()
}
}