1#[cfg(test)]
17mod tests;
18
19use std::cell::RefCell;
20use std::future::poll_fn;
21use std::pin::Pin;
22use std::rc::Rc;
23use std::sync::Once;
24use std::time::{Duration, Instant};
25
26use crabshell_reactive::{ReadSignal, Signal};
27use crabshell_ui::Cx;
28use zbus::export::futures_core::Stream;
29use zbus::message::Type as Kind;
30use zbus::{
31 Array, Connection, DynamicType, Message, MessageStream, ObjectPath, OwnedObjectPath,
32 OwnedVariant, Variant,
33};
34
35use crate::Shared;
36use crate::dbus::{self, BUS, BUS_PATH, CALL_TIMEOUT, PROPERTIES, Props, set_changed};
37
38const PREFIX: &str = "org.mpris.MediaPlayer2.";
40const PLAYERCTLD: &str = "org.mpris.MediaPlayer2.playerctld";
43const PATH: &str = "/org/mpris/MediaPlayer2";
44const ROOT: &str = "org.mpris.MediaPlayer2";
46const PLAYER: &str = "org.mpris.MediaPlayer2.Player";
47
48const MAX_PLAYERS: usize = 32;
49const MAX_ARTISTS: usize = 32;
50const MAX_TEXT: usize = 4096;
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum PlaybackStatus {
56 Playing,
58 Paused,
60 Stopped,
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum LoopStatus {
67 None,
69 Track,
71 Playlist,
73}
74
75impl LoopStatus {
76 fn parse(name: &str) -> Option<LoopStatus> {
77 match name {
78 "None" => Some(LoopStatus::None),
79 "Track" => Some(LoopStatus::Track),
80 "Playlist" => Some(LoopStatus::Playlist),
81 _ => None,
82 }
83 }
84
85 fn name(self) -> &'static str {
86 match self {
87 LoopStatus::None => "None",
88 LoopStatus::Track => "Track",
89 LoopStatus::Playlist => "Playlist",
90 }
91 }
92}
93
94#[derive(Clone, Debug, PartialEq)]
97pub struct Player {
98 pub id: String,
102 pub identity: String,
104 pub desktop_entry: String,
106 pub status: PlaybackStatus,
108 pub title: String,
110 pub artists: Vec<String>,
112 pub album: String,
114 pub art_url: String,
116 pub length: Option<Duration>,
118 pub can_play: bool,
120 pub can_pause: bool,
122 pub can_go_next: bool,
124 pub can_go_previous: bool,
126 pub can_seek: bool,
129 pub can_control: bool,
131 pub volume: Option<f64>,
133 pub shuffle: Option<bool>,
135 pub loop_status: Option<LoopStatus>,
137 pub rate: f64,
139 pub position: Duration,
141 pub position_known_at: Instant,
143}
144
145impl Player {
146 pub fn position_at(&self, now: Instant) -> Duration {
151 let mut position = self.position;
152 if self.status == PlaybackStatus::Playing {
153 let elapsed = now.saturating_duration_since(self.position_known_at);
154 let moved = position.as_secs_f64() + elapsed.as_secs_f64() * self.rate;
155 position = Duration::try_from_secs_f64(moved.max(0.0)).unwrap_or(position);
156 }
157 self.length.map_or(position, |length| position.min(length))
158 }
159
160 fn from_props(
161 id: String,
162 (root, props): &(Props, Props),
163 position: Duration,
164 at: Instant,
165 ) -> Player {
166 let metadata = metadata(props);
167 let flag = |name| dbus::get(props, name).unwrap_or(false);
168 Player {
169 id,
170 identity: text(root, "Identity"),
171 desktop_entry: text(root, "DesktopEntry"),
172 status: match dbus::get::<&str>(props, "PlaybackStatus") {
173 Some("Playing") => PlaybackStatus::Playing,
174 Some("Paused") => PlaybackStatus::Paused,
175 _ => PlaybackStatus::Stopped,
176 },
177 title: text(&metadata, "xesam:title"),
178 artists: dbus::get::<&Array>(&metadata, "xesam:artist").map_or(Vec::new(), |artists| {
179 let names = artists
180 .inner()
181 .iter()
182 .filter_map(|name| <&str>::try_from(name).ok());
183 names.take(MAX_ARTISTS).map(cut).collect()
184 }),
185 album: text(&metadata, "xesam:album"),
186 art_url: dbus::get::<&str>(&metadata, "mpris:artUrl")
188 .filter(|url| url.len() <= MAX_TEXT)
189 .unwrap_or_default()
190 .to_owned(),
191 length: metadata
193 .get("mpris:length")
194 .and_then(|length| micros(length))
195 .filter(|length| !length.is_zero()),
196 can_play: flag("CanPlay"),
197 can_pause: flag("CanPause"),
198 can_go_next: flag("CanGoNext"),
199 can_go_previous: flag("CanGoPrevious"),
200 can_seek: flag("CanSeek"),
201 can_control: flag("CanControl"),
202 volume: dbus::get::<f64>(props, "Volume").filter(|volume| volume.is_finite()),
203 shuffle: dbus::get(props, "Shuffle"),
204 loop_status: dbus::get::<&str>(props, "LoopStatus").and_then(LoopStatus::parse),
205 rate: dbus::get::<f64>(props, "Rate")
206 .filter(|rate| rate.is_finite())
207 .unwrap_or(1.0),
208 position,
209 position_known_at: at,
210 }
211 }
212}
213
214fn metadata(props: &Props) -> Props {
217 let metadata = props
218 .get("Metadata")
219 .and_then(|value| value.try_clone().ok());
220 metadata
221 .and_then(|value| Props::try_from(value).ok())
222 .unwrap_or_default()
223}
224
225fn text(props: &Props, name: &str) -> String {
227 cut(dbus::get::<&str>(props, name).unwrap_or_default())
228}
229
230fn cut(text: &str) -> String {
232 text[..text.floor_char_boundary(MAX_TEXT)].to_owned()
233}
234
235fn micros(value: &Variant<'_>) -> Option<Duration> {
238 let micros = match *value {
239 Variant::I64(n) => u64::try_from(n).ok()?,
240 Variant::U64(n) => n,
241 Variant::I32(n) => u64::try_from(n).ok()?,
242 Variant::U32(n) => n.into(),
243 Variant::I16(n) => u64::try_from(n).ok()?,
244 Variant::U16(n) => n.into(),
245 Variant::U8(n) => n.into(),
246 _ => return None,
247 };
248 Some(Duration::from_micros(micros))
249}
250
251#[derive(Clone)]
254pub struct Mpris {
255 service: Option<Rc<Service>>,
257 players: ReadSignal<Vec<Player>>,
258}
259
260impl Mpris {
261 pub fn players(&self) -> ReadSignal<Vec<Player>> {
264 self.players
265 }
266
267 pub fn play_pause(&self, id: &str) {
271 self.call(id, PLAYER, "PlayPause", ());
272 }
273
274 pub fn play(&self, id: &str) {
276 self.call(id, PLAYER, "Play", ());
277 }
278
279 pub fn pause(&self, id: &str) {
281 self.call(id, PLAYER, "Pause", ());
282 }
283
284 pub fn stop(&self, id: &str) {
286 self.call(id, PLAYER, "Stop", ());
287 }
288
289 pub fn next(&self, id: &str) {
291 self.call(id, PLAYER, "Next", ());
292 }
293
294 pub fn previous(&self, id: &str) {
296 self.call(id, PLAYER, "Previous", ());
297 }
298
299 pub fn seek(&self, id: &str, seconds: f64) {
302 if !seconds.is_finite() {
303 return tracing::warn!(id, seconds, "a seek by no finite time; not sent");
304 }
305 let offset = (seconds * 1e6).round() as i64;
306 self.call(id, PLAYER, "Seek", offset);
307 }
308
309 pub fn set_position(&self, id: &str, position: Duration) {
312 let track = self.service.as_ref().and_then(|service| service.track(id));
313 let Some(track) = track else {
314 return tracing::warn!(id, "the media player names no track; no position set");
315 };
316 let at = i64::try_from(position.as_micros()).unwrap_or(i64::MAX);
317 self.call(id, PLAYER, "SetPosition", (track, at));
318 }
319
320 pub fn set_volume(&self, id: &str, volume: f64) {
323 if !volume.is_finite() {
324 return tracing::warn!(id, volume, "a volume that isn't finite; not sent");
325 }
326 self.set(id, "Volume", Variant::from(volume));
327 }
328
329 pub fn set_shuffle(&self, id: &str, shuffle: bool) {
331 self.set(id, "Shuffle", Variant::from(shuffle));
332 }
333
334 pub fn set_loop(&self, id: &str, status: LoopStatus) {
336 self.set(id, "LoopStatus", Variant::from(status.name()));
337 }
338
339 pub fn raise(&self, id: &str) {
341 self.call(id, ROOT, "Raise", ());
342 }
343
344 fn target(&self, id: &str, method: &str) -> Option<(Rc<Shared>, Connection)> {
347 let service = self.service.as_ref()?;
348 let state = service.state.borrow();
349 let target = state
350 .as_ref()
351 .filter(|state| state.entries.iter().any(|entry| entry.id == id))
352 .and_then(|state| state.connection.clone());
353 if target.is_none() {
354 tracing::warn!(id, method, "no such media player");
355 }
356 Some((Rc::clone(&service.shared), target?))
357 }
358
359 fn call<B>(&self, id: &str, interface: &'static str, method: &'static str, body: B)
361 where
362 B: serde::Serialize + DynamicType + 'static,
363 {
364 let Some((shared, connection)) = self.target(id, method) else {
365 return;
366 };
367 let id = id.to_owned();
368 let spawner = Rc::clone(&shared);
369 spawner.spawn(async move {
370 let call = connection.call_method(Some(id.as_str()), PATH, Some(interface), method, &body);
371 if let Err(error) = shared.timeout(CALL_TIMEOUT, call).await {
372 tracing::info!(id, method, %error, "the media player refused");
373 }
374 });
375 }
376
377 fn set(&self, id: &str, name: &'static str, value: Variant<'static>) {
379 let Some((shared, connection)) = self.target(id, name) else {
380 return;
381 };
382 let id = id.to_owned();
383 let spawner = Rc::clone(&shared);
384 spawner.spawn(async move {
385 let set = dbus::set(&shared, &connection, &id, (PATH, PLAYER), name, value);
386 if let Err(error) = set.await {
387 tracing::info!(id, name, %error, "the media player refused");
388 }
389 });
390 }
391}
392
393pub(crate) fn mpris(cx: &Cx) -> Mpris {
395 let Some(service) = cx.service::<Service>() else {
396 static LOGGED: Once = Once::new();
397 LOGGED.call_once(|| {
398 tracing::warn!("cx.mpris() needs the services the shell provides; it stays empty");
399 });
400 return Mpris {
401 service: None,
402 players: cx.signal(Vec::new()).read_only(),
403 };
404 };
405 let players = service.start();
406 Mpris {
407 service: Some(service),
408 players,
409 }
410}
411
412pub(crate) struct Service {
414 shared: Rc<Shared>,
415 state: RefCell<Option<State>>,
416}
417
418struct State {
419 players: Signal<Vec<Player>>,
420 connection: Option<Connection>,
422 entries: Vec<Entry>,
424}
425
426struct Entry {
427 id: String,
429 owner: Option<String>,
432 reads: u64,
434 props: Option<(Props, Props)>,
437 position: (Duration, Instant),
439}
440
441impl Service {
442 pub(crate) fn new(shared: Rc<Shared>) -> Service {
443 Service {
444 shared,
445 state: RefCell::new(None),
446 }
447 }
448
449 fn start(self: &Rc<Self>) -> ReadSignal<Vec<Player>> {
451 if let Some(state) = &*self.state.borrow() {
452 return state.players.read_only();
453 }
454 let players = self.shared.root.signal(Vec::new());
455 *self.state.borrow_mut() = Some(State {
456 players,
457 connection: None,
458 entries: Vec::new(),
459 });
460 match self.shared.config.session_bus.clone() {
461 Some(address) => self.shared.spawn(run(Rc::clone(self), address)),
462 None => tracing::info!("no session bus, so there are no media players"),
463 }
464 players.read_only()
465 }
466
467 fn alive(&self) -> bool {
469 let state = self.state.borrow();
470 let alive = state
471 .as_ref()
472 .and_then(|state| state.players.try_with(|_| ()));
473 alive.is_some()
474 }
475
476 fn with_entry<R>(&self, id: &str, f: impl FnOnce(&mut Entry) -> R) -> Option<R> {
477 let mut state = self.state.borrow_mut();
478 let entry = state
479 .as_mut()?
480 .entries
481 .iter_mut()
482 .find(|entry| entry.id == id)?;
483 Some(f(entry))
484 }
485
486 fn track(&self, id: &str) -> Option<OwnedObjectPath> {
488 self
489 .with_entry(id, |entry| {
490 let (_, props) = entry.props.as_ref()?;
491 let track = dbus::get::<&ObjectPath>(&metadata(props), "mpris:trackid")?.to_owned();
492 Some(OwnedObjectPath::from(track))
493 })
494 .flatten()
495 }
496
497 async fn join(self: &Rc<Self>, connection: &Connection) -> zbus::Result<()> {
500 let names = format!(
501 "type='signal',sender='{BUS}',interface='{BUS}',member='NameOwnerChanged',\
502 arg0namespace='{ROOT}'"
503 );
504 let signals = format!("type='signal',path='{PATH}'");
506 for rule in [names, signals] {
507 dbus::bus_call(connection, "AddMatch", &rule).await?;
508 }
509 let call = connection.call_method(Some(BUS), BUS_PATH, Some(BUS), "ListNames", &());
510 let names: Vec<String> = call.await?.body().deserialize()?;
511 for name in names.into_iter().filter(|name| is_player(name)) {
514 self.appeared(connection, name, None);
515 }
516 Ok(())
517 }
518
519 fn appeared(self: &Rc<Self>, connection: &Connection, id: String, owner: Option<String>) {
521 {
522 let mut state = self.state.borrow_mut();
523 let Some(state) = state.as_mut() else {
524 return;
525 };
526 let full = state.entries.len() >= MAX_PLAYERS;
527 match state.entries.iter_mut().find(|entry| entry.id == id) {
528 Some(entry) if entry.owner == owner => return,
529 Some(entry) => entry.owner = owner,
530 None if full => {
531 return tracing::warn!(id, "too many media players; leaving one out");
532 }
533 None => state.entries.push(Entry {
534 id: id.clone(),
535 owner,
536 reads: 0,
537 props: None,
538 position: (Duration::ZERO, Instant::now()),
539 }),
540 }
541 }
542 self.read(connection, id);
543 }
544
545 fn read(self: &Rc<Self>, connection: &Connection, id: String) {
548 let Some(read) = self.with_entry(&id, |entry| {
549 entry.reads += 1;
550 entry.reads
551 }) else {
552 return;
553 };
554 let (service, connection) = (Rc::clone(self), connection.clone());
555 self.shared.spawn(async move {
556 let shared = &service.shared;
557 let all = async {
558 let owner = dbus::owner(&connection, &id).await;
560 let root = dbus::get_all(shared, &connection, &id, PATH, ROOT).await?;
561 let props = dbus::get_all(shared, &connection, &id, PATH, PLAYER).await?;
562 zbus::Result::Ok((owner, (root, props)))
563 };
564 let (owner, props) = match all.await {
565 Ok(read) => read,
566 Err(error) => return tracing::warn!(id, %error, "can't read a media player"),
567 };
568 let position = props.1.get("Position").and_then(|at| micros(at));
569 let newest = service.with_entry(&id, |entry| {
570 if entry.reads == read {
571 entry.owner = owner;
572 entry.position = (position.unwrap_or_default(), Instant::now());
573 entry.props = Some(props);
574 }
575 });
576 if newest.is_some() {
577 service.publish();
578 }
579 });
580 }
581
582 fn read_position(self: &Rc<Self>, connection: &Connection, id: String) {
584 let Some(read) = self.with_entry(&id, |entry| entry.reads) else {
585 return;
586 };
587 let (service, connection) = (Rc::clone(self), connection.clone());
588 self.shared.spawn(async move {
589 let get = (PLAYER, "Position");
590 let call = connection.call_method(Some(id.as_str()), PATH, Some(PROPERTIES), "Get", &get);
591 let reply = service.shared.timeout(CALL_TIMEOUT, call).await;
592 let position = match reply.and_then(|reply| reply.body().deserialize::<OwnedVariant>()) {
593 Ok(position) => micros(&position).unwrap_or_default(),
594 Err(error) => return tracing::info!(id, %error, "can't read a media player's position"),
595 };
596 let newest = service.with_entry(&id, |entry| {
598 if entry.reads == read {
599 entry.position = (position, Instant::now());
600 }
601 });
602 if newest.is_some() {
603 service.publish();
604 }
605 });
606 }
607
608 fn on_signal(self: &Rc<Self>, connection: &Connection, message: &Message) {
609 if let Some((name, owner)) = dbus::owner_changed(message) {
610 match (is_player(&name), owner.is_empty()) {
611 (false, _) => {}
612 (true, true) => self.left(&name),
613 (true, false) => self.appeared(connection, name, Some(owner)),
614 }
615 return;
616 }
617 let header = message.header();
618 let (Some(sender), None) = (header.sender(), header.destination()) else {
621 return;
622 };
623 if header.path().map(|path| path.as_str()) != Some(PATH) {
624 return;
625 }
626 let ids: Vec<String> = {
627 let state = self.state.borrow();
628 let entries = state.as_ref().map(|state| state.entries.as_slice());
629 let from = entries
630 .unwrap_or_default()
631 .iter()
632 .filter(|entry| entry.owner.as_deref() == Some(sender.as_str()));
633 from.map(|entry| entry.id.clone()).collect()
634 };
635 if let Some((_, interface, changed, dropped)) = dbus::properties_changed(message) {
636 let moved = interface == PLAYER
637 && ["PlaybackStatus", "Rate", "Metadata"]
638 .iter()
639 .any(|name| changed.contains_key(*name));
640 for id in ids {
641 self.with_entry(&id, |entry| {
642 if moved && let Some(props) = &entry.props {
645 let (position, at) = entry.position;
646 let now = Instant::now();
647 let player = Player::from_props(entry.id.clone(), props, position, at);
648 entry.position = (player.position_at(now), now);
649 }
650 let Some((root, props)) = &mut entry.props else {
651 return;
652 };
653 let props = match interface.as_str() {
654 ROOT => root,
655 PLAYER => props,
656 _ => return,
657 };
658 dbus::apply(props, changed.clone(), dropped.clone());
659 });
660 if !dropped.is_empty() {
662 self.read(connection, id);
663 } else if moved {
664 self.read_position(connection, id);
665 }
666 }
667 return self.publish();
668 }
669 let member = header.member().map(|member| member.as_str());
670 let seeked = header.interface().map(|interface| interface.as_str()) == Some(PLAYER)
671 && member == Some("Seeked");
672 if let (true, Ok(at)) = (seeked, message.body().deserialize::<i64>()) {
673 let position = micros(&Variant::I64(at)).unwrap_or_default();
674 for id in ids {
675 self.with_entry(&id, |entry| {
676 entry.position = (position, Instant::now());
677 entry.reads += 1;
679 });
680 }
681 self.publish();
682 }
683 }
684
685 fn left(&self, id: &str) {
687 if let Some(state) = self.state.borrow_mut().as_mut() {
688 state.entries.retain(|entry| entry.id != id);
689 }
690 self.publish();
691 }
692
693 fn publish(&self) {
695 let shown = self.state.borrow().as_ref().map(|state| {
697 let players = state.entries.iter().filter_map(|entry| {
698 let props = entry.props.as_ref()?;
699 let (position, at) = entry.position;
700 Some(Player::from_props(entry.id.clone(), props, position, at))
701 });
702 (state.players, players.collect::<Vec<_>>())
703 });
704 if let Some((signal, players)) = shown {
705 set_changed(signal, players);
706 }
707 }
708
709 fn clear(&self) {
711 if let Some(state) = self.state.borrow_mut().as_mut() {
712 state.entries.clear();
713 state.connection = None;
714 }
715 self.publish();
716 }
717}
718
719fn is_player(name: &str) -> bool {
721 name.starts_with(PREFIX) && name != PLAYERCTLD
722}
723
724async fn run(service: Rc<Service>, address: zbus::Address) {
727 let connection = match zbus::connection::Builder::address(address).build().await {
728 Ok(connection) => connection,
729 Err(error) => {
730 return tracing::info!(%error, "can't reach the session bus, so there are no media players");
731 }
732 };
733 let mut messages = MessageStream::from(&connection);
735 if let Some(state) = service.state.borrow_mut().as_mut() {
736 state.connection = Some(connection.clone());
737 }
738 if let Err(error) = service.join(&connection).await {
739 return tracing::warn!(%error, "couldn't follow the media players on the session bus");
740 }
741 loop {
742 let next = poll_fn(|cx| Pin::new(&mut messages).poll_next(cx)).await;
743 if !service.alive() {
744 return service.clear();
745 }
746 match next {
747 Some(Ok(message)) if message.header().message_type() == Kind::Signal => {
748 service.on_signal(&connection, &message);
749 }
750 Some(Ok(_)) => {}
751 Some(Err(error)) => tracing::warn!(%error, "a bad message on the session bus"),
752 None => break,
753 }
754 }
755 tracing::warn!("the session bus closed, so there are no media players");
756 service.clear();
757}