1use std::sync::mpsc::{self, Receiver};
2
3use base64::Engine;
4use souvlaki::{MediaControlEvent, MediaControls, MediaMetadata, MediaPlayback, PlatformConfig};
5use termusiclib::{
6 common::const_unknown::{UNKNOWN_ARTIST, UNKNOWN_TITLE},
7 track::Track,
8};
9
10use crate::{
11 GeneralPlayer, PlayerCmd, PlayerProgress, PlayerTimeUnit, PlayerTrait, RunningStatus, Volume,
12};
13
14pub struct Mpris {
15 controls: MediaControls,
16 pub rx: Receiver<MediaControlEvent>,
17}
18
19impl Mpris {
20 pub fn new(cmd_tx: crate::PlayerCmdSender) -> Self {
21 #[cfg(not(target_os = "windows"))]
33 let hwnd = None;
34
35 #[cfg(target_os = "windows")]
36 let (hwnd, _dummy_window) = {
37 let dummy_window = windows::DummyWindow::new().unwrap();
38 let handle = Some(dummy_window.handle.0);
39 (handle, dummy_window)
40 };
41
42 let config = PlatformConfig {
43 dbus_name: "termusic",
44 display_name: "Termusic in Rust",
45 hwnd,
46 };
47
48 let mut controls = MediaControls::new(config).unwrap();
49
50 let (tx, rx) = mpsc::sync_channel(32);
51 controls
53 .attach(move |event: MediaControlEvent| {
54 tx.send(event).ok();
55 cmd_tx.send(PlayerCmd::Tick).ok();
58 })
59 .ok();
60
61 Self { controls, rx }
62 }
63}
64
65impl Mpris {
66 pub fn add_and_play(&mut self, track: &Track) {
67 std::thread::sleep(std::time::Duration::from_millis(100));
69 self.controls
70 .set_playback(MediaPlayback::Playing { progress: None })
71 .ok();
72
73 let cover_art = match track.get_picture() {
74 Ok(v) => v.map(|v| {
75 format!(
76 "data:{};base64,{}",
77 v.mime_type().map_or_else(
78 || {
79 error!("Unknown mimetype for picture of track {track:#?}");
80 "application/octet-stream"
81 },
82 |v| v.as_str()
83 ),
84 base64::engine::general_purpose::STANDARD_NO_PAD.encode(v.data())
85 )
86 }),
87 Err(err) => {
88 error!("Fetching the cover failed: {err:#?}");
89 None
90 }
91 };
92
93 let album = track.as_track().and_then(|v| v.album());
94
95 self.controls
96 .set_metadata(MediaMetadata {
97 title: Some(track.title().unwrap_or(UNKNOWN_TITLE)),
98 artist: Some(track.artist().unwrap_or(UNKNOWN_ARTIST)),
99 album: Some(album.unwrap_or("")),
100 cover_url: cover_art.as_deref(),
101 duration: track.duration(),
102 })
103 .ok();
104 }
105
106 pub fn pause(&mut self) {
107 self.controls
108 .set_playback(MediaPlayback::Paused { progress: None })
109 .ok();
110 }
111 pub fn resume(&mut self) {
112 self.controls
113 .set_playback(MediaPlayback::Playing { progress: None })
114 .ok();
115 }
116
117 pub fn update_progress(
119 &mut self,
120 position: Option<PlayerTimeUnit>,
121 playlist_status: RunningStatus,
122 ) {
123 if let Some(position) = position {
124 match playlist_status {
125 RunningStatus::Running => self
126 .controls
127 .set_playback(MediaPlayback::Playing {
128 progress: Some(souvlaki::MediaPosition(position)),
129 })
130 .ok(),
131 RunningStatus::Paused | RunningStatus::Stopped => self
132 .controls
133 .set_playback(MediaPlayback::Paused {
134 progress: Some(souvlaki::MediaPosition(position)),
135 })
136 .ok(),
137 };
138 }
139 }
140
141 #[allow(unused_variables, clippy::unused_self)] pub fn update_volume(&mut self, volume: Volume) {
146 #[cfg(target_os = "linux")]
148 {
149 let vol = f64::from(volume) / 100.0;
151 let _ = self.controls.set_volume(vol);
152 }
153 }
154}
155
156impl GeneralPlayer {
157 pub fn mpris_handler(&mut self, e: MediaControlEvent) {
158 match e {
159 MediaControlEvent::Next => {
160 self.next();
161 }
162 MediaControlEvent::Previous => {
163 self.previous();
164 }
165 MediaControlEvent::Pause => {
166 self.pause();
167 }
168 MediaControlEvent::Toggle => {
169 self.toggle_pause();
170 }
171 MediaControlEvent::Play => {
172 self.play();
173 }
174 MediaControlEvent::Seek(direction) => {
176 let cmd = match direction {
177 souvlaki::SeekDirection::Forward => PlayerCmd::SeekForward,
178 souvlaki::SeekDirection::Backward => PlayerCmd::SeekBackward,
179 };
180
181 self.cmd_tx.send(cmd).ok();
183 }
184 MediaControlEvent::SetPosition(position) => {
185 self.seek_to(position.0);
186 }
187 MediaControlEvent::OpenUri(_uri) => {
188 info!("Unimplemented Event: OpenUri");
195 }
196 MediaControlEvent::SeekBy(direction, duration) => {
197 #[allow(clippy::cast_possible_wrap)]
198 let as_secs = duration.as_secs().min(i64::MAX as u64) as i64;
199
200 if as_secs == 0 {
202 warn!("can only seek in seconds, got less than 0 seconds");
203 return;
204 }
205
206 let offset = match direction {
207 souvlaki::SeekDirection::Forward => as_secs,
208 souvlaki::SeekDirection::Backward => -as_secs,
209 };
210
211 let _ = self.seek(offset);
214 }
215 MediaControlEvent::SetVolume(volume) => {
216 debug!("got souvlaki SetVolume: {volume:#}");
217 if volume > 1.0 {
220 error!("SetVolume above 1.0 will be clamped to 1.0!");
221 }
222 let clamp_100 = volume.clamp(0.0, 1.0) * 100.0;
224 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
229 let uvol = clamp_100.round() as u16;
230 self.cmd_tx.send(PlayerCmd::VolumeSet(uvol)).ok();
232 }
233 MediaControlEvent::Quit => {
234 self.cmd_tx.send(PlayerCmd::Quit).ok();
236 }
237 MediaControlEvent::Stop => {
238 info!("Unimplemented Event: Stop");
240 }
241 MediaControlEvent::Raise => {}
243 }
244 }
245
246 pub fn mpris_handle_events(&mut self) {
248 if let Some(ref mut mpris) = self.mpris {
249 if let Ok(m) = mpris.rx.try_recv() {
250 self.mpris_handler(m);
251 }
252 }
253 }
254
255 #[inline]
257 pub fn mpris_update_progress(&mut self, progress: &PlayerProgress) {
258 if let Some(ref mut mpris) = self.mpris {
259 mpris.update_progress(progress.position, self.playlist.read_recursive().status());
260 }
261 }
262
263 #[inline]
265 pub fn mpris_volume_update(&mut self) {
266 let volume = self.volume();
267 if let Some(ref mut mpris) = self.mpris {
268 mpris.update_volume(volume);
269 }
270 }
271}
272
273#[cfg(target_os = "windows")]
276#[allow(clippy::cast_possible_truncation, unsafe_code)]
277mod windows {
278 use std::io::Error;
279 use std::mem;
280
281 use windows::core::w;
282 use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
284 use windows::Win32::System::LibraryLoader::GetModuleHandleW;
285 use windows::Win32::UI::WindowsAndMessaging::{
286 CreateWindowExW, DefWindowProcW, DestroyWindow, RegisterClassExW, WINDOW_EX_STYLE,
287 WINDOW_STYLE, WNDCLASSEXW,
288 };
289
290 pub struct DummyWindow {
291 pub handle: HWND,
292 }
293
294 impl DummyWindow {
295 pub fn new() -> Result<DummyWindow, String> {
296 let class_name = w!("SimpleTray");
297
298 let handle_result = unsafe {
299 let instance = GetModuleHandleW(None)
300 .map_err(|e| format!("Getting module handle failed: {e}"))?;
301
302 let wnd_class = WNDCLASSEXW {
303 cbSize: mem::size_of::<WNDCLASSEXW>() as u32,
304 hInstance: instance.into(),
305 lpszClassName: class_name,
306 lpfnWndProc: Some(Self::wnd_proc),
307 ..Default::default()
308 };
309
310 if RegisterClassExW(&wnd_class) == 0 {
311 return Err(format!(
312 "Registering class failed: {}",
313 Error::last_os_error()
314 ));
315 }
316
317 let handle = match CreateWindowExW(
318 WINDOW_EX_STYLE::default(),
319 class_name,
320 w!(""),
321 WINDOW_STYLE::default(),
322 0,
323 0,
324 0,
325 0,
326 None,
327 None,
328 instance,
329 None,
330 ) {
331 Ok(v) => v,
332 Err(err) => {
333 return Err(format!("{err}"));
334 }
335 };
336
337 if handle.is_invalid() {
338 Err(format!(
339 "Message only window creation failed: {}",
340 Error::last_os_error()
341 ))
342 } else {
343 Ok(handle)
344 }
345 };
346
347 handle_result.map(|handle| DummyWindow { handle })
348 }
349 extern "system" fn wnd_proc(
350 hwnd: HWND,
351 msg: u32,
352 wparam: WPARAM,
353 lparam: LPARAM,
354 ) -> LRESULT {
355 unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
356 }
357 }
358
359 impl Drop for DummyWindow {
360 fn drop(&mut self) {
361 unsafe {
362 DestroyWindow(self.handle).unwrap();
363 }
364 }
365 }
366
367 }