From 00b2b1f64c8f41b108840363a2eda6fca93fff27 Mon Sep 17 00:00:00 2001 From: jmb05 Date: Mon, 2 Nov 2026 14:36:53 +0100 Subject: [PATCH] Replace mpris crate with own implementation --- Cargo.lock | 43 --- Cargo.toml | 4 +- README.md | 3 +- src/backend.rs | 54 ++-- src/backend/audio.rs | 645 ++++++++++++++++++++++++++++++++------ src/ui.rs | 49 ++- src/ui/title_panel.rs | 46 ++- src/ui/utilities_panel.rs | 16 +- 8 files changed, 634 insertions(+), 226 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d535a7..5cdae15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1170,17 +1170,6 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" -[[package]] -name = "dbus" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" -dependencies = [ - "libc", - "libdbus-sys", - "windows-sys 0.61.2", -] - [[package]] name = "ddc" version = "0.2.2" @@ -1493,17 +1482,6 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" -[[package]] -name = "enum-kinds" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e40a16955681d469ab3da85aaa6b42ff656b3c67b52e1d8d3dd36afe97fd462" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "enum_dispatch" version = "0.3.13" @@ -2777,7 +2755,6 @@ dependencies = [ "libc", "libpulse-binding", "meval", - "mpris", "nmrs", "notify", "nucleo-matcher", @@ -2946,15 +2923,6 @@ version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" -[[package]] -name = "libdbus-sys" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" -dependencies = [ - "pkg-config", -] - [[package]] name = "libloading" version = "0.8.9" @@ -3338,17 +3306,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "mpris" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b72fbda9340cad2ca8181eabd8b96a2d8f4386e1ac9eefcf4045fc4e97d1ab7b" -dependencies = [ - "dbus", - "enum-kinds", - "thiserror 2.0.21", -] - [[package]] name = "multimap" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index 130145a..203dc91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ gtk4-layer-shell = "0.8.1" hyprland = { git = "https://github.com/romanstingler/hyprland-rs-next", branch = "master" } notify = "8.2.0" glib = "0.22.10" -mpris = "2.1.0" time-format = "1.2.4" nmrs = "3.5.7" tokio = { version = "1.53.3", features = ["rt", "time", "net", "sync", "rt-multi-thread", "io-util", "macros", "signal"] } @@ -42,13 +41,12 @@ battery = "0.7.8" ddc-hi = { git = "https://github.com/jmb05/ddc-hi-rs" } wayland-protocols = { version = "0.32.13", features = ["staging", "client"] } wayland-client = "0.31.15" -zbus = "5.19.0" +zbus = "5.19.0" # Keep at this version for now because other dependencies also have it serde = { version = "1.0.229", features = ["derive"] } freedesktop-desktop-entry = "0.8.4" nucleo-matcher = "0.3.1" meval = "0.2.0" toml = "1.1.8" -#zbus = "6.1.0" [build-dependencies] glib-build-tools = "0.22.8" diff --git a/README.md b/README.md index 40884ce..5d6f6bc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ Additionally, it acts as an idle daemon. It is written in rust and uses Gtk 4 an gtk-layer-shell. **This is a hobby project and for my own personal use. It might be unoptimized in some places and naively implemented. It is also most definitely not very customizable -and highly opinionated. Use at your own risk.** +and highly opinionated. It is built with the specifics of my system in mind and +will probably not work on your machine. Use at your own risk.** ### Building and Running To build the project you need the rust compiler and cargo installed. diff --git a/src/backend.rs b/src/backend.rs index d72075a..4a6cf09 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -8,7 +8,7 @@ pub mod perf_stats; pub mod window_manager_hypr; use crate::LOGGER; -use crate::backend::audio::{MediaBackend, MediaCommand}; +use crate::backend::audio::{AudioBackend, AudioMessage, MediaBackend, MediaMessage}; use crate::backend::brightness_control::{BrightnessManager, BrightnessMessage}; use crate::backend::commands::ControlSocket; use crate::backend::idle::{IdleDaemon, IdleMessage, IdleState}; @@ -74,7 +74,8 @@ pub struct Backend { net_registrator: Registrator, br_registrator: Registrator, stats_registrator: Registrator, - media_registrator: Registrator, + audio_registrator: Registrator, + media_registration: Registrator, netbird_registrator: Registrator, idle_registrator: Registrator, control_registrator: Registrator, @@ -85,6 +86,7 @@ pub enum ChannelType { Hypr, Br, Stats, + Audio, Media, Idle, Netbird, @@ -98,8 +100,9 @@ impl Backend { let net_registrator = NetworkBackendHandle::start(); let br_registrator = BrightnessManager::start(); let stats_registrator = StatsBackend::start(); - let media_registrator = MediaBackend::start(); - let tailscale_registrator = JsonSocketBackend::start(); + let audio_registrator = AudioBackend::start(); + let media_registration = MediaBackend::start(); + let netbird_registrator = JsonSocketBackend::start(); let idle_registrator = IdleDaemon::start(); let control_registrator = ControlSocket::start(); debug!(&LOGGER, "Started Backend threads"); @@ -108,8 +111,9 @@ impl Backend { net_registrator, br_registrator, stats_registrator, - media_registrator, - netbird_registrator: tailscale_registrator, + audio_registrator, + media_registration, + netbird_registrator, idle_registrator, control_registrator, } @@ -124,21 +128,18 @@ impl Backend { channels.net_tx = Some(tx); channels.net_rx = Some(rx); } - ChannelType::Hypr => { - channels.hypr_rx = - Some(HyprlandComms::subscribe(self.hypr_registrator.clone()).0); - } ChannelType::Br => { let (rx, tx) = BrightnessManager::subscribe(self.br_registrator.clone()); channels.br_rx = Some(rx); channels.br_tx = Some(tx); } - ChannelType::Stats => { - channels.stats_rx = - Some(StatsBackend::subscribe(self.stats_registrator.clone()).0); + ChannelType::Audio => { + let (rx, tx) = AudioBackend::subscribe(self.audio_registrator.clone()); + channels.audio_rx = Some(rx); + channels.audio_tx = Some(tx); } ChannelType::Media => { - let (rx, tx) = MediaBackend::subscribe(self.media_registrator.clone()); + let (rx, tx) = MediaBackend::subscribe(self.media_registration.clone()); channels.media_rx = Some(rx); channels.media_tx = Some(tx); } @@ -155,6 +156,14 @@ impl Backend { channels.ctl_rx = Some(ControlSocket::subscribe(self.control_registrator.clone()).0); } + ChannelType::Stats => { + channels.stats_rx = + Some(StatsBackend::subscribe(self.stats_registrator.clone()).0); + } + ChannelType::Hypr => { + channels.hypr_rx = + Some(HyprlandComms::subscribe(self.hypr_registrator.clone()).0); + } } } channels @@ -168,6 +177,7 @@ impl Backend { ChannelType::Br, ChannelType::Stats, ChannelType::Control, + ChannelType::Audio, ChannelType::Media, ChannelType::Hypr, ]) @@ -182,8 +192,10 @@ pub struct BackendChannels { br_rx: Option>, br_tx: Option>, stats_rx: Option>, - media_rx: Option>, - media_tx: Option>, + audio_rx: Option>, + audio_tx: Option>, + media_rx: Option>, + media_tx: Option>, netbird_rx: Option>, idle_rx: Option>, idle_tx: Option>, @@ -206,7 +218,10 @@ impl BackendChannels { pub fn take_netbird_rx(&mut self) -> Option> { self.netbird_rx.take() } - pub fn take_media_rx(&mut self) -> Option> { + pub fn take_audio_rx(&mut self) -> Option> { + self.audio_rx.take() + } + pub fn take_media_rx(&mut self) -> Option> { self.media_rx.take() } pub fn take_idle_rx(&mut self) -> Option> { @@ -221,7 +236,10 @@ impl BackendChannels { pub fn br_tx(&self) -> Option> { self.br_tx.clone() } - pub fn media_tx(&self) -> Option> { + pub fn audio_tx(&self) -> Option> { + self.audio_tx.clone() + } + pub fn media_tx(&self) -> Option> { self.media_tx.clone() } pub fn idle_tx(&self) -> Option> { diff --git a/src/backend/audio.rs b/src/backend/audio.rs index 66ec91a..2363104 100644 --- a/src/backend/audio.rs +++ b/src/backend/audio.rs @@ -15,12 +15,19 @@ use pulse::volume::{ChannelVolumes, Volume}; use pulse::{mainloop, operation}; use std::boxed::Box; use std::cell::Cell; +use std::collections::HashMap; use std::os::fd::{AsRawFd, RawFd}; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::thread; +use zbus::export::ordered_stream::OrderedStreamExt; +use zbus::fdo::{NameOwnerChanged, PropertiesProxy}; +use zbus::message::Type; +use zbus::names::InterfaceName; +use zbus::zvariant::OwnedValue; +use zbus::{MatchRule, MessageStream}; -pub struct AudioController { +pub struct PulseController { pub mainloop: Mainloop, context: Context, device_index: Option, @@ -37,7 +44,7 @@ pub enum ControllerMsg { Set(u8, bool), } -impl AudioController { +impl PulseController { pub fn new(tx: Sender) -> Self { let mut mainloop = Mainloop::new().unwrap(); let mut prop_list = Proplist::new().unwrap(); @@ -95,7 +102,7 @@ impl AudioController { trace!(&LOGGER, "Subscribed to pulseaudio events: {success}"); }); - let mut controller = AudioController { + let mut controller = PulseController { context, mainloop, device_index: None, @@ -251,147 +258,151 @@ impl AudioController { } #[derive(Clone, Debug)] -pub enum MediaCommand { - Init(Sender), +pub enum AudioMessage { + Init(Sender), Request, Cmd(u8, bool, bool), } -pub struct MediaBackend { - subscribers: Vec>, - command_tx: Sender, +pub struct AudioBackend { + subscribers: Vec>, + command_tx: Sender, } -impl BackendThread> for MediaBackend { +impl BackendThread> for AudioBackend { fn subscribe( - registrator: Registrator, - ) -> (Receiver, Sender) { - let (result_tx, result_rx) = crossbeam_channel::unbounded::(); + registrator: Registrator, + ) -> (Receiver, Sender) { + let (result_tx, result_rx) = crossbeam_channel::unbounded::(); let _ = registrator.send(result_tx); while let Ok(state) = result_rx.recv() { return match state { - MediaCommand::Init(sender) => (result_rx, sender), + AudioMessage::Init(sender) => (result_rx, sender), _ => continue, }; } panic!("Channel closed unexpectedly, without returning anything") } - fn subscribers(&self) -> &Vec> { + fn subscribers(&self) -> &Vec> { &self.subscribers } - fn init(registration_rx: Receiver>) { - let (command_tx, command_rx) = crossbeam_channel::unbounded::(); + fn init(registration_rx: Receiver>) { + let (command_tx, command_rx) = crossbeam_channel::unbounded::(); let instance = Arc::new(Mutex::new(Self { subscribers: vec![], command_tx, })); - let instance_clone = instance.clone(); let (controller_tx, controller_rx) = crossbeam_channel::unbounded::(); - let controller_tx_clone = controller_tx.clone(); let notify = Arc::new(EventFD::new(0, EfdFlags::EFD_CLOEXEC).unwrap()); - let notify_clone = notify.clone(); - thread::spawn(move || { - let mut controller = AudioController::new(controller_tx_clone); - let _event = api::Mainloop::new_io_event( - &mut controller.mainloop, - notify_clone.as_raw_fd(), - io::FlagSet::INPUT, - Box::new(move |_, fd, _flags| match drain_eventfd(fd) { - Ok(_) => trace!(&LOGGER, "Woke pulseaudio loop using EventFD"), - Err(e) => warn!(&LOGGER, "Error draining EventFD {}", e.to_string()), - }), - ) - .unwrap(); - loop { - while let Ok(msg) = controller_rx.try_recv() { - match msg { - Request => { - instance_clone.lock().expect("Couldn't get lock").publish( - MediaCommand::Cmd( - controller.current_vol, - controller.current_mute, - controller.device_bt.unwrap_or(false), - ), - ); - } - ControllerMsg::NotifyServer => { - controller.query_default_device_blocking(); - let _ = controller.query_state_blocking(); - instance_clone.lock().expect("Couldn't get lock").publish( - MediaCommand::Cmd( - controller.current_vol, - controller.current_mute, - controller.device_bt.unwrap_or(false), - ), - ); - } - ControllerMsg::NotifySink(idx) => { - if controller.device_index.is_some_and(|i| i == idx) { - let changed = controller.query_state_blocking(); - if changed { - instance_clone.lock().expect("Couldn't get lock").publish( - MediaCommand::Cmd( - controller.current_vol, - controller.current_mute, - controller.device_bt.unwrap_or(false), - ), - ); - trace!( - &LOGGER, - "Change: {} muted: {}", + thread::spawn({ + let instance = instance.clone(); + let controller_tx = controller_tx.clone(); + let notify = notify.clone(); + move || { + let mut controller = PulseController::new(controller_tx); + let _event = api::Mainloop::new_io_event( + &mut controller.mainloop, + notify.as_raw_fd(), + io::FlagSet::INPUT, + Box::new(move |_, fd, _flags| match drain_eventfd(fd) { + Ok(_) => trace!(&LOGGER, "Woke pulseaudio loop using EventFD"), + Err(e) => warn!(&LOGGER, "Error draining EventFD {}", e.to_string()), + }), + ) + .unwrap(); + loop { + while let Ok(msg) = controller_rx.try_recv() { + match msg { + Request => { + instance.lock().expect("Couldn't get lock").publish( + AudioMessage::Cmd( controller.current_vol, - controller.current_mute - ); + controller.current_mute, + controller.device_bt.unwrap_or(false), + ), + ); + } + ControllerMsg::NotifyServer => { + controller.query_default_device_blocking(); + let _ = controller.query_state_blocking(); + instance.lock().expect("Couldn't get lock").publish( + AudioMessage::Cmd( + controller.current_vol, + controller.current_mute, + controller.device_bt.unwrap_or(false), + ), + ); + } + ControllerMsg::NotifySink(idx) => { + if controller.device_index.is_some_and(|i| i == idx) { + let changed = controller.query_state_blocking(); + if changed { + instance.lock().expect("Couldn't get lock").publish( + AudioMessage::Cmd( + controller.current_vol, + controller.current_mute, + controller.device_bt.unwrap_or(false), + ), + ); + trace!( + &LOGGER, + "Change: {} muted: {}", + controller.current_vol, + controller.current_mute + ); + } } } - } - ControllerMsg::Set(volume, mute) => { - controller.set_state(volume, mute); - instance_clone.lock().expect("Couldn't get lock").publish( - MediaCommand::Cmd( - controller.current_vol, - controller.current_mute, - controller.device_bt.unwrap_or(false), - ), - ); + ControllerMsg::Set(volume, mute) => { + controller.set_state(volume, mute); + instance.lock().expect("Couldn't get lock").publish( + AudioMessage::Cmd( + controller.current_vol, + controller.current_mute, + controller.device_bt.unwrap_or(false), + ), + ); + } } } + controller.mainloop.iterate(true); } - controller.mainloop.iterate(true); } }); - let instance_clone = instance.clone(); - thread::spawn(move || { - while let Ok(sender) = registration_rx.recv() { - if sender - .send(MediaCommand::Init( - instance_clone + thread::spawn({ + let instance = instance.clone(); + move || { + while let Ok(sender) = registration_rx.recv() { + if sender + .send(AudioMessage::Init( + instance + .lock() + .expect("Couldn't get lock") + .command_tx + .clone(), + )) + .is_ok() + { + let _ = sender.send(AudioMessage::Request); + instance .lock() - .expect("Couldn't get lock") - .command_tx - .clone(), - )) - .is_ok() - { - let _ = sender.send(MediaCommand::Request); - instance_clone - .lock() - .expect("Couldn't get subscribers") - .subscribers - .push(sender); + .expect("Couldn't get subscribers") + .subscribers + .push(sender); + } } } }); thread::spawn(move || { while let Ok(command) = command_rx.recv() { match command { - MediaCommand::Cmd(vol, mute, _) => { + AudioMessage::Cmd(vol, mute, _) => { let _ = controller_tx.send(ControllerMsg::Set(vol, mute)); notify.write(1).expect("Error writing notify"); } - MediaCommand::Request => { + AudioMessage::Request => { let _ = controller_tx.send(Request); notify.write(1).expect("Error writing notify"); } @@ -404,6 +415,438 @@ impl BackendThread> for MediaBackend { } } +const MPRIS_PREFIX: &str = "org.mpris.MediaPlayer2"; +const MPRIS_PATH: &str = "/org/mpris/MediaPlayer2"; +const MPRIS_PLAYER_INTERFACE: &str = "org.mpris.MediaPlayer2.Player"; + +#[derive(Clone, Debug)] +pub enum PlayerStatus { + Playing(String), + Paused(String), + Stopped, +} + +#[derive(Clone, Debug)] +pub enum MediaMessage { + Init(Sender), + PlayerStatus(PlayerStatus), + PlayerPlay, + PlayerPause, + PlayerStop, +} + +pub struct MediaBackend { + subscribers: Vec>, + players: HashMap, + active_player: Option, +} + +impl BackendThread> for MediaBackend { + fn subscribe( + registrator: Registrator, + ) -> (Receiver, Sender) { + let (result_tx, result_rx) = crossbeam_channel::unbounded::(); + let _ = registrator.send(result_tx); + while let Ok(state) = result_rx.recv() { + return match state { + MediaMessage::Init(sender) => (result_rx, sender), + _ => continue, + }; + } + panic!("Channel closed unexpectedly, without returning anything") + } + + fn subscribers(&self) -> &Vec> { + &self.subscribers + } + + fn init(registration_rx: Receiver>) { + let (tx, rx) = crossbeam_channel::unbounded::(); + let instance = Arc::new(Mutex::new(Self { + subscribers: vec![], + players: HashMap::new(), + active_player: None, + })); + thread::spawn({ + let instance = instance.clone(); + move || { + while let Ok(sender) = registration_rx.recv() { + if let Ok(_) = sender.send(MediaMessage::Init(tx.clone())) { + let mut instance_guard = instance.lock().unwrap(); + if let Some(name) = &instance_guard.active_player + && let Some(status) = instance_guard.players.get(name) + { + let _ = sender.send(MediaMessage::PlayerStatus(status.clone())); + } + instance_guard.subscribers.push(sender); + } + } + } + }); + thread::spawn({ + let instance = instance.clone(); + move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create Tokio runtime"); + + runtime.block_on(async move { + if let Err(e) = Self::wait_for_players(instance).await { + error!(&LOGGER, "Error waiting for players: {e}"); + } + }); + } + }); + thread::spawn(move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create Tokio runtime"); + + runtime.block_on({ + let instance = instance.clone(); + async move { + let Ok(connection) = zbus::Connection::session().await else { + error!(&LOGGER, "Couldn't create D-Bus connection"); + return; + }; + while let Ok(msg) = rx.recv() { + let mut instance_guard = instance.lock().unwrap(); + if let Some(name) = &instance_guard.active_player { + if let Some(status) = instance_guard.players.get(name) { + match msg { + MediaMessage::PlayerPlay => { + if let PlayerStatus::Paused(_) = status { + instance_guard.active_player_play(&connection).await; + } + } + MediaMessage::PlayerPause => { + if let PlayerStatus::Playing(_) = status { + instance_guard.active_player_pause(&connection).await; + } + } + MediaMessage::PlayerStop => { + if let PlayerStatus::Playing(_) | PlayerStatus::Paused(_) = + status + { + instance_guard.active_player_stop(&connection).await; + } + } + _ => {} + } + } + } + } + } + }); + }); + } +} + +impl MediaBackend { + async fn find_existing_players( + connection: &zbus::Connection, + instance: Arc>, + ) -> zbus::fdo::Result<()> { + let dbus = zbus::Proxy::new( + connection, + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + ) + .await?; + + let names: Vec = dbus.call("ListNames", &()).await?; + let players: Vec = names + .into_iter() + .filter(|name| name.starts_with(&format!("{MPRIS_PREFIX}."))) + .collect(); + + for player in players { + Self::spawn_new_player_watcher(&connection, &instance, &player); + } + + Ok(()) + } + + async fn wait_for_players(instance: Arc>) -> zbus::fdo::Result<()> { + let connection = zbus::Connection::session().await?; + + if let Err(e) = Self::find_existing_players(&connection, instance.clone()).await { + warn!(&LOGGER, "Error finding existing Media Players: {e}"); + } + + let rule = MatchRule::builder() + .msg_type(Type::Signal) + .sender("org.freedesktop.DBus")? + .interface("org.freedesktop.DBus")? + .member("NameOwnerChanged")? + .build(); + + let mut stream = MessageStream::for_match_rule(rule, &connection, Some(32)).await?; + + while let Some(Ok(message)) = stream.next().await { + let Some(signal) = NameOwnerChanged::from_message(message) else { + continue; + }; + + let Ok(args) = signal.args() else { + continue; + }; + let name = args.name.as_str(); + + if !name.starts_with(&format!("{MPRIS_PREFIX}.")) { + continue; + } + + let old_owner = args.old_owner(); + let new_owner = args.new_owner(); + + let map = &mut instance.lock().unwrap().players; + if old_owner.is_none() && new_owner.is_some() { + Self::spawn_new_player_watcher(&connection, &instance, name); + } else if old_owner.is_some() && new_owner.is_none() { + map.remove(name); + debug!(&LOGGER, "(1) Player removed: {name}"); + } else { + debug!(&LOGGER, "Player owner changed: {name}"); + } + } + warn!(&LOGGER, "Player Finder ended prematurely"); + Ok(()) + } + + fn spawn_new_player_watcher( + connection: &zbus::Connection, + instance: &Arc>, + name: &str, + ) { + tokio::spawn({ + let instance = instance.clone(); + let connection = connection.clone(); + let name = name.to_string(); + async move { + let _ = Self::watch_player_status(&connection, instance, &name).await; + } + }); + } + + async fn watch_player_status( + connection: &zbus::Connection, + instance: Arc>, + name: &str, + ) -> zbus::fdo::Result<()> { + let props = PropertiesProxy::builder(connection) + .destination(name)? + .path(MPRIS_PATH)? + .build() + .await?; + + let mut changes = props.receive_properties_changed().await?; + + let try_string: Result = props + .get( + InterfaceName::from_static_str_unchecked(MPRIS_PLAYER_INTERFACE), + "PlaybackStatus", + ) + .await? + .try_into(); + + let initial: String = if let Ok(s) = try_string { + s + } else { + String::from("Stopped") + }; + + debug!(&LOGGER, "Player added: {name} {initial}"); + let _ = Self::insert_or_update_player(&instance, &props, name.into(), initial).await; + + while let Some(change) = changes.next().await { + let args = change.args()?; + + if !args.interface_name().eq(MPRIS_PLAYER_INTERFACE) { + continue; + } + + if let Some(value) = args.changed_properties().get("PlaybackStatus") { + if let Ok(status) = ::try_from(value.clone()) { + debug!(&LOGGER, "Player ({name}) status has changed to: '{status}'"); + Self::insert_or_update_player(&instance, &props, name.into(), status).await; + } + } + } + debug!(&LOGGER, "(2) Removing player: {name}"); + instance.lock().unwrap().players.remove(name); + + Ok(()) + } + + async fn insert_or_update_player( + instance: &Arc>, + props: &PropertiesProxy<'_>, + player_name: String, + player_status: String, + ) { + match Self::get_player_status_from_string(&props, player_status).await { + Ok(status) => { + let mut instance_guard = instance.lock().unwrap(); + instance_guard.players.insert(player_name.clone(), status); + instance_guard.select_active_player(Some(player_name)); + } + Err(e) => { + warn!(&LOGGER, "Error parsing player status: {e}"); + } + } + } + + async fn get_title(props: &PropertiesProxy<'_>) -> zbus::fdo::Result { + let metadata = props + .get( + InterfaceName::from_static_str_unchecked(MPRIS_PLAYER_INTERFACE), + "Metadata", + ) + .await?; + let metadata = >::try_from(metadata); + match metadata { + Ok(metadata) => { + if let Some(title) = metadata.get("xesam:title") { + ::try_from(title.clone()) + .map_err(|e| zbus::fdo::Error::Failed(e.to_string())) + } else { + Err(zbus::fdo::Error::Failed("Couldn't parse title".into())) + } + } + Err(err) => { + warn!(&LOGGER, "Error parsing metadata {}", err); + Err(zbus::fdo::Error::Failed("Couldn't parse metadata".into())) + } + } + } + + async fn get_player_status_from_string( + props: &PropertiesProxy<'_>, + status: String, + ) -> zbus::fdo::Result { + match status.as_str() { + "Playing" => match Self::get_title(props).await { + Ok(title) => Ok(PlayerStatus::Playing(title)), + Err(e) => Err(e), + }, + "Paused" => match Self::get_title(props).await { + Ok(title) => Ok(PlayerStatus::Paused(title)), + Err(e) => Err(e), + }, + "Stopped" => Ok(PlayerStatus::Stopped), + other => Err(zbus::fdo::Error::Failed(format!( + "Invalid status string: '{other}' {:?}", + other.as_bytes() + ))), + } + } + + async fn active_player_play(&self, connection: &zbus::Connection) { + if let Some(player_name) = &self.active_player { + if let Ok(proxy) = zbus::Proxy::new( + &connection, + player_name.as_str(), + MPRIS_PATH, + MPRIS_PLAYER_INTERFACE, + ) + .await + { + if let Err(e) = proxy.call_method("Play", &()).await { + warn!( + &LOGGER, + "Error starting play for player: {player_name}, {e}" + ); + } + } + } + } + + async fn active_player_pause(&mut self, connection: &zbus::Connection) { + if let Some(player_name) = &self.active_player { + if let Ok(proxy) = zbus::Proxy::new( + &connection, + player_name.as_str(), + MPRIS_PATH, + MPRIS_PLAYER_INTERFACE, + ) + .await + { + if let Err(e) = proxy.call_method("Pause", &()).await { + warn!(&LOGGER, "Error pausing player: {player_name}, {e}"); + } + } + self.select_active_player(self.active_player.clone()); + } + } + + async fn active_player_stop(&mut self, connection: &zbus::Connection) { + if let Some(player_name) = &self.active_player { + if let Ok(proxy) = zbus::Proxy::new( + &connection, + player_name.as_str(), + MPRIS_PATH, + MPRIS_PLAYER_INTERFACE, + ) + .await + { + if let Err(e) = proxy.call_method("Stop", &()).await { + warn!(&LOGGER, "Error stopping player: {player_name}, {e}"); + } + } + } + self.select_active_player(None); + } + + fn select_active_player(&mut self, changed_player: Option) { + let mut changed_also_playing = false; + let mut new_active: Option = None; + let mut send_status = PlayerStatus::Stopped; + if let Some(player) = changed_player + && let Some(status) = self.players.get(&player) + { + match status { + PlayerStatus::Playing(_) => { + new_active = Some(player); + send_status = status.clone(); + changed_also_playing = true; + } + PlayerStatus::Paused(_) => { + new_active = Some(player); + send_status = status.clone(); + } + PlayerStatus::Stopped => {} // Stopped players shouldn't be active + } + } + + if !changed_also_playing { + for (name, status) in &self.players { + match status { + PlayerStatus::Playing(_) => { + // Found a player that is playing media -> prioritize this one + new_active = Some(name.clone()); + send_status = status.clone(); + break; + } + PlayerStatus::Paused(_) => { + if new_active.is_none() { + new_active = Some(name.clone()); + send_status = status.clone(); + } + } + PlayerStatus::Stopped => {} // Stopped players shouldn't be active + } + } + } + + self.active_player = new_active; + self.publish(MediaMessage::PlayerStatus(send_status)); + } +} + fn drain_eventfd(fd: RawFd) -> std::io::Result { let mut value: u64 = 0; diff --git a/src/ui.rs b/src/ui.rs index 54e19e0..d2a8c1a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,10 +1,10 @@ use crate::LOGGER; +use crate::backend::audio::MediaMessage; use crate::backend::window_manager_hypr::{HyprlandComms, WindowManagerEvent}; use crate::backend::{Backend, BackendChannels, ChannelType}; use crate::ctl::ControlCommand; use crate::ui::action_centre::ActionCentre; use crate::ui::run_menu::RunMenu; -use crossbeam_channel::TryRecvError; use glib::ControlFlow; use glib::prelude::Cast; use gtk::gdk::Display; @@ -12,7 +12,6 @@ use gtk::prelude::{BoxExt, ButtonExt, DisplayExt, GtkWindowExt, ListModelExt, Wi use gtk::{Align, Application, ApplicationWindow, Button, Orientation, gdk}; use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; use jmb_logger_rs::*; -use mpris::PlayerFinder; use std::collections::HashMap; use std::rc::Rc; use std::sync::Mutex; @@ -98,12 +97,10 @@ fn create_and_show_bar( let hypr_rx = channels .take_hypr_rx() .expect("Tried taking unavailable Hyprland Channel Receiver"); - - let player_opt = if let Ok(player_finder) = PlayerFinder::new() { - player_finder.find_active().ok() - } else { - None - }; + let media_rx = channels + .take_media_rx() + .expect("Tried taking unavailable Media Channel Receiver"); + let media_tx = channels.media_tx().unwrap(); let window = ApplicationWindow::builder() .application(&app) @@ -136,7 +133,7 @@ fn create_and_show_bar( ); let title_button: Option