player/systemint/
linux.rs1use mpris_server::{
2 Metadata, PlaybackStatus, PlayerInterface, Property, RootInterface, Server, Time, zbus::fdo,
3};
4use std::sync::{
5 Arc, Mutex, OnceLock,
6 mpsc::{self, Receiver, Sender},
7};
8
9#[derive(Debug)]
10pub enum SystemEvent {
11 Play,
12 Pause,
13 Toggle,
14 Next,
15 Prev,
16}
17
18static TX: OnceLock<Sender<SystemEvent>> = OnceLock::new();
19static RX: OnceLock<Mutex<Receiver<SystemEvent>>> = OnceLock::new();
20static STATE: OnceLock<Arc<Mutex<(Metadata, PlaybackStatus, Time)>>> = OnceLock::new();
21static NOTIFY: OnceLock<tokio::sync::mpsc::UnboundedSender<bool>> = OnceLock::new();
22
23fn tx() -> Sender<SystemEvent> {
24 TX.get_or_init(|| {
25 let (tx, rx) = mpsc::channel();
26 RX.set(Mutex::new(rx)).ok();
27 tx
28 })
29 .clone()
30}
31
32fn state() -> Arc<Mutex<(Metadata, PlaybackStatus, Time)>> {
33 STATE
34 .get_or_init(|| {
35 Arc::new(Mutex::new((
36 Metadata::new(),
37 PlaybackStatus::Stopped,
38 Time::ZERO,
39 )))
40 })
41 .clone()
42}
43
44struct P(
45 Arc<Mutex<(Metadata, PlaybackStatus, Time)>>,
46 Sender<SystemEvent>,
47);
48
49impl RootInterface for P {
50 async fn raise(&self) -> fdo::Result<()> {
51 Ok(())
52 }
53 async fn quit(&self) -> fdo::Result<()> {
54 Ok(())
55 }
56 async fn can_quit(&self) -> fdo::Result<bool> {
57 Ok(false)
58 }
59 async fn fullscreen(&self) -> fdo::Result<bool> {
60 Ok(false)
61 }
62 async fn set_fullscreen(&self, _: bool) -> mpris_server::zbus::Result<()> {
63 Ok(())
64 }
65 async fn can_set_fullscreen(&self) -> fdo::Result<bool> {
66 Ok(false)
67 }
68 async fn can_raise(&self) -> fdo::Result<bool> {
69 Ok(false)
70 }
71 async fn has_track_list(&self) -> fdo::Result<bool> {
72 Ok(false)
73 }
74 async fn identity(&self) -> fdo::Result<String> {
75 Ok("Kopuz".into())
76 }
77 async fn desktop_entry(&self) -> fdo::Result<String> {
78 Ok("kopuz".into())
79 }
80 async fn supported_uri_schemes(&self) -> fdo::Result<Vec<String>> {
81 Ok(vec![])
82 }
83 async fn supported_mime_types(&self) -> fdo::Result<Vec<String>> {
84 Ok(vec![])
85 }
86}
87
88impl PlayerInterface for P {
89 async fn next(&self) -> fdo::Result<()> {
90 self.1.send(SystemEvent::Next).ok();
91 Ok(())
92 }
93 async fn previous(&self) -> fdo::Result<()> {
94 self.1.send(SystemEvent::Prev).ok();
95 Ok(())
96 }
97 async fn pause(&self) -> fdo::Result<()> {
98 self.1.send(SystemEvent::Pause).ok();
99 Ok(())
100 }
101 async fn play_pause(&self) -> fdo::Result<()> {
102 self.1.send(SystemEvent::Toggle).ok();
103 Ok(())
104 }
105 async fn stop(&self) -> fdo::Result<()> {
106 self.1.send(SystemEvent::Pause).ok();
107 Ok(())
108 }
109 async fn play(&self) -> fdo::Result<()> {
110 self.1.send(SystemEvent::Play).ok();
111 Ok(())
112 }
113 async fn seek(&self, _: Time) -> fdo::Result<()> {
114 Ok(())
115 }
116 async fn set_position(&self, _: mpris_server::TrackId, _: Time) -> fdo::Result<()> {
117 Ok(())
118 }
119 async fn open_uri(&self, _: String) -> fdo::Result<()> {
120 Ok(())
121 }
122 async fn playback_status(&self) -> fdo::Result<PlaybackStatus> {
123 Ok(self
124 .0
125 .lock()
126 .map(|s| s.1)
127 .unwrap_or(PlaybackStatus::Stopped))
128 }
129 async fn loop_status(&self) -> fdo::Result<mpris_server::LoopStatus> {
130 Ok(mpris_server::LoopStatus::None)
131 }
132 async fn set_loop_status(&self, _: mpris_server::LoopStatus) -> mpris_server::zbus::Result<()> {
133 Ok(())
134 }
135 async fn rate(&self) -> fdo::Result<f64> {
136 Ok(1.0)
137 }
138 async fn set_rate(&self, _: f64) -> mpris_server::zbus::Result<()> {
139 Ok(())
140 }
141 async fn shuffle(&self) -> fdo::Result<bool> {
142 Ok(false)
143 }
144 async fn set_shuffle(&self, _: bool) -> mpris_server::zbus::Result<()> {
145 Ok(())
146 }
147 async fn metadata(&self) -> fdo::Result<Metadata> {
148 Ok(self.0.lock().map(|s| s.0.clone()).unwrap_or_default())
149 }
150 async fn volume(&self) -> fdo::Result<f64> {
151 Ok(1.0)
152 }
153 async fn set_volume(&self, _: f64) -> mpris_server::zbus::Result<()> {
154 Ok(())
155 }
156 async fn position(&self) -> fdo::Result<Time> {
157 Ok(self.0.lock().map(|s| s.2).unwrap_or(Time::ZERO))
158 }
159 async fn minimum_rate(&self) -> fdo::Result<f64> {
160 Ok(1.0)
161 }
162 async fn maximum_rate(&self) -> fdo::Result<f64> {
163 Ok(1.0)
164 }
165 async fn can_go_next(&self) -> fdo::Result<bool> {
166 Ok(true)
167 }
168 async fn can_go_previous(&self) -> fdo::Result<bool> {
169 Ok(true)
170 }
171 async fn can_play(&self) -> fdo::Result<bool> {
172 Ok(true)
173 }
174 async fn can_pause(&self) -> fdo::Result<bool> {
175 Ok(true)
176 }
177 async fn can_seek(&self) -> fdo::Result<bool> {
178 Ok(false)
179 }
180 async fn can_control(&self) -> fdo::Result<bool> {
181 Ok(true)
182 }
183}
184
185pub fn update_position(position: f64) {
186 setup();
187 if let Ok(mut s) = state().lock() {
188 s.2 = Time::from_micros((position * 1e6) as i64);
189 }
190}
191
192fn setup() {
193 static ONCE: OnceLock<()> = OnceLock::new();
194 ONCE.get_or_init(|| {
195 let (ntx, mut nrx) = tokio::sync::mpsc::unbounded_channel();
196 NOTIFY.set(ntx).ok();
197 let st = state();
198 std::thread::spawn(move || {
199 tokio::runtime::Builder::new_current_thread()
200 .enable_all()
201 .build()
202 .unwrap()
203 .block_on(async {
204 if let Ok(srv) = Server::new("kopuz", P(st.clone(), tx())).await {
205 while let Some(seeked) = nrx.recv().await {
206 if seeked {
207 let (metadata, status, position) = match st.lock() {
208 Ok(s) => (s.0.clone(), s.1, s.2),
209 Err(_) => continue,
210 };
211 srv.properties_changed([
212 Property::Metadata(metadata),
213 Property::PlaybackStatus(status),
214 ])
215 .await
216 .ok();
217 srv.emit(mpris_server::Signal::Seeked { position })
218 .await
219 .ok();
220 }
221 }
222 }
223 });
224 });
225 });
226}
227
228pub fn poll_event() -> Option<SystemEvent> {
229 setup();
230 RX.get()?.lock().ok()?.try_recv().ok()
231}
232
233pub fn update_now_playing(
234 title: &str,
235 artist: &str,
236 album: &str,
237 duration: f64,
238 position: f64,
239 playing: bool,
240 artwork_path: Option<&str>,
241) {
242 setup();
243 if let Ok(mut s) = state().lock() {
244 let mut b = Metadata::builder()
245 .title(title)
246 .artist([artist])
247 .album(album)
248 .length(Time::from_micros((duration * 1e6) as i64));
249 if let Some(art) = artwork_path {
250 b = b.art_url(
255 if art.starts_with("http://")
256 || art.starts_with("https://")
257 || art.starts_with("file://")
258 {
259 art.to_string()
260 } else if art.starts_with('/') {
261 format!("file://{art}")
262 } else {
263 format!(
264 "file://{}/{art}",
265 std::env::current_dir().unwrap_or_default().display()
266 )
267 },
268 );
269 }
270 *s = (
271 b.build(),
272 if playing {
273 PlaybackStatus::Playing
274 } else {
275 PlaybackStatus::Paused
276 },
277 Time::from_micros((position * 1e6) as i64),
278 );
279 }
280 NOTIFY.get().map(|tx| tx.send(true));
281}