1use std::{path::PathBuf, str::FromStr, time::Duration};
6
7use mecomp_core::state::{RepeatMode, SeekType, Status};
8use mpris_server::{
9 LoopStatus, Metadata, PlaybackRate, PlaybackStatus, PlayerInterface, Time, TrackId, Volume,
10 zbus::{Error as ZbusError, fdo},
11};
12use tarpc::context::Context;
13
14use crate::{Mpris, interfaces::root::SUPPORTED_MIME_TYPES, metadata_from_opt_song};
15
16impl PlayerInterface for Mpris {
17 async fn next(&self) -> fdo::Result<()> {
18 let context = Context::current();
19 let daemon_read_lock = self.daemon().await;
20 if let Some(daemon) = daemon_read_lock.as_ref() {
21 daemon
22 .playback_skip_forward(context, 1)
23 .await
24 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
25 }
26 drop(daemon_read_lock);
27 Ok(())
28 }
29
30 async fn previous(&self) -> fdo::Result<()> {
31 let context = Context::current();
32 let daemon_read_lock = self.daemon().await;
33 if let Some(daemon) = daemon_read_lock.as_ref() {
34 daemon
35 .playback_skip_backward(context, 1)
36 .await
37 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
38 }
39 drop(daemon_read_lock);
40 Ok(())
41 }
42
43 async fn pause(&self) -> fdo::Result<()> {
44 let context = Context::current();
45 let daemon_read_lock = self.daemon().await;
46 if let Some(daemon) = daemon_read_lock.as_ref() {
47 daemon
48 .playback_pause(context)
49 .await
50 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
51 }
52 drop(daemon_read_lock);
53 Ok(())
54 }
55
56 async fn play_pause(&self) -> fdo::Result<()> {
57 let context = Context::current();
58 let daemon_read_lock = self.daemon().await;
59 if let Some(daemon) = daemon_read_lock.as_ref() {
60 daemon
61 .playback_toggle(context)
62 .await
63 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
64 }
65 drop(daemon_read_lock);
66 Ok(())
67 }
68
69 async fn stop(&self) -> fdo::Result<()> {
70 let daemon_read_lock = self.daemon().await;
71 if let Some(daemon) = daemon_read_lock.as_ref() {
72 let context = Context::current();
73 daemon
74 .playback_stop(context)
75 .await
76 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
77 }
78 drop(daemon_read_lock);
79
80 Ok(())
81 }
82
83 async fn play(&self) -> fdo::Result<()> {
84 let context = Context::current();
85 let daemon_read_lock = self.daemon().await;
86 if let Some(daemon) = daemon_read_lock.as_ref() {
87 daemon
88 .playback_play(context)
89 .await
90 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
91 }
92 drop(daemon_read_lock);
93 Ok(())
94 }
95
96 async fn open_uri(&self, uri: String) -> fdo::Result<()> {
97 log::info!("Opening URI: {uri}");
101
102 if !uri.starts_with("file://") {
104 return Err(fdo::Error::InvalidArgs(
105 "Only file:// URIs are supported".to_string(),
106 ));
107 }
108
109 let path = uri.strip_prefix("file://").unwrap();
111 let mut path = PathBuf::from_str(path)
112 .map_err(|_| fdo::Error::InvalidArgs("Invalid file path".to_string()))?;
113
114 path = percent_encoding::percent_decode_str(&path.to_string_lossy())
116 .decode_utf8()
117 .map_err(|_| fdo::Error::InvalidArgs("Invalid file path".to_string()))?
118 .into_owned()
119 .into();
120
121 if !SUPPORTED_MIME_TYPES
123 .iter()
124 .filter_map(|s| s.split('/').next_back())
125 .any(|ext| path.extension().is_some_and(|e| e == ext))
126 {
127 return Err(fdo::Error::InvalidArgs(
128 "File type not supported".to_string(),
129 ));
130 }
131
132 if path.starts_with("~") {
134 path = shellexpand::tilde(&path.to_string_lossy())
135 .into_owned()
136 .into();
137 }
138
139 log::debug!("Locating file: {}", path.display());
140
141 if !path.exists() {
143 return Err(fdo::Error::InvalidArgs("File does not exist".to_string()));
144 }
145
146 if !path.is_file() {
148 return Err(fdo::Error::InvalidArgs("Path is not a file".to_string()));
149 }
150
151 path = path.canonicalize().unwrap_or(path);
153
154 let daemon_read_lock = self.daemon().await;
156 if let Some(daemon) = daemon_read_lock.as_ref() {
157 let context = Context::current();
158 if let Some(song) = daemon
159 .library_song_get_by_path(context, path)
160 .await
161 .map_err(|e| fdo::Error::Failed(e.to_string()))?
162 {
163 let context = Context::current();
164 daemon
165 .queue_add(context, song.id.clone().into())
166 .await
167 .map_err(|e| fdo::Error::Failed(e.to_string()))?
168 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
169 } else {
170 return Err(fdo::Error::Failed(
171 "Failed to find song in database".to_string(),
172 ));
173 }
174 }
175 drop(daemon_read_lock);
176
177 Ok(())
178 }
179
180 async fn playback_status(&self) -> fdo::Result<PlaybackStatus> {
181 let status = self.state.read().await.status;
182 match status {
183 Status::Stopped => Ok(PlaybackStatus::Stopped),
184 Status::Paused => Ok(PlaybackStatus::Paused),
185 Status::Playing => Ok(PlaybackStatus::Playing),
186 }
187 }
188
189 async fn loop_status(&self) -> fdo::Result<LoopStatus> {
190 let repeat_mode = self.state.read().await.repeat_mode;
191 match repeat_mode {
192 RepeatMode::None => Ok(LoopStatus::None),
193 RepeatMode::One => Ok(LoopStatus::Track),
194 RepeatMode::All => Ok(LoopStatus::Playlist),
195 }
196 }
197
198 async fn set_loop_status(&self, loop_status: LoopStatus) -> Result<(), ZbusError> {
199 let repeat_mode = match loop_status {
200 LoopStatus::None => RepeatMode::None,
201 LoopStatus::Track => RepeatMode::One,
202 LoopStatus::Playlist => RepeatMode::All,
203 };
204
205 let context = Context::current();
206
207 let daemon_read_lock = self.daemon().await;
208 if let Some(daemon) = daemon_read_lock.as_ref() {
209 daemon
210 .playback_repeat(context, repeat_mode)
211 .await
212 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
213 }
214 drop(daemon_read_lock);
215
216 Ok(())
217 }
218
219 async fn rate(&self) -> fdo::Result<PlaybackRate> {
220 Ok(1.0)
221 }
222
223 async fn set_rate(&self, _: PlaybackRate) -> Result<(), ZbusError> {
224 Ok(())
225 }
226
227 async fn shuffle(&self) -> fdo::Result<bool> {
228 Ok(true)
232 }
233
234 async fn set_shuffle(&self, shuffle: bool) -> Result<(), ZbusError> {
235 if shuffle {
237 let context = Context::current();
238 let daemon_read_lock = self.daemon().await;
239 if let Some(daemon) = daemon_read_lock.as_ref() {
240 daemon
241 .playback_shuffle(context)
242 .await
243 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
244 }
245 drop(daemon_read_lock);
246 }
247
248 Ok(())
249 }
250
251 async fn metadata(&self) -> fdo::Result<Metadata> {
252 let state = self.state.read().await;
253
254 Ok(metadata_from_opt_song(state.current_song.as_ref()))
255 }
256
257 async fn volume(&self) -> fdo::Result<Volume> {
258 let state = self.state.read().await;
259 if state.muted {
260 Ok(0.0)
261 } else {
262 Ok(f64::from(state.volume))
263 }
264 }
265
266 async fn set_volume(&self, volume: Volume) -> Result<(), ZbusError> {
267 let context = Context::current();
268 let daemon_read_lock = self.daemon().await;
269 if let Some(daemon) = daemon_read_lock.as_ref() {
270 #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
271 daemon
272 .playback_volume(context, volume as f32)
273 .await
274 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
275 }
276 drop(daemon_read_lock);
277 Ok(())
278 }
279
280 async fn position(&self) -> fdo::Result<Time> {
281 self.state.read().await.runtime.map_or_else(
282 || Ok(Time::from_micros(0)),
283 |runtime| {
284 Ok(Time::from_micros(
285 i64::try_from(runtime.seek_position.as_micros()).unwrap_or(i64::MAX),
286 ))
287 },
288 )
289 }
290
291 async fn seek(&self, offset: Time) -> fdo::Result<()> {
292 let context = Context::current();
294 let daemon_read_lock = self.daemon().await;
295 if let Some(daemon) = daemon_read_lock.as_ref() {
296 let seek_type = if offset.as_micros() > 0 {
297 SeekType::RelativeForwards
298 } else {
299 SeekType::RelativeBackwards
300 };
301
302 let offset = Duration::from_micros(offset.as_micros().unsigned_abs());
303
304 daemon
305 .playback_seek(context, seek_type, offset)
306 .await
307 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
308 }
309 drop(daemon_read_lock);
310 Ok(())
311 }
312
313 async fn set_position(&self, track_id: TrackId, position: Time) -> fdo::Result<()> {
314 if Some(track_id) != self.metadata().await?.trackid() {
316 return Ok(());
317 }
318
319 let daemon_read_lock = self.daemon().await;
320 if let Some(daemon) = daemon_read_lock.as_ref() {
321 let maybe_state = self.state.read().await;
322 if let Some(song) = maybe_state.current_song.as_ref() {
323 let position = position.as_micros();
325 if position < 0 || u128::from(position.unsigned_abs()) > song.runtime.as_micros() {
326 return Ok(());
327 }
328
329 let context = Context::current();
330
331 daemon
332 .playback_seek(
333 context,
334 SeekType::Absolute,
335 Duration::from_micros(u64::try_from(position).unwrap_or_default()),
336 )
337 .await
338 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
339 }
340 drop(maybe_state);
341 }
342 drop(daemon_read_lock);
343
344 Ok(())
345 }
346
347 async fn minimum_rate(&self) -> fdo::Result<PlaybackRate> {
348 Ok(1.0)
349 }
350
351 async fn maximum_rate(&self) -> fdo::Result<PlaybackRate> {
352 Ok(1.0)
353 }
354
355 async fn can_go_next(&self) -> fdo::Result<bool> {
356 Ok(true)
357 }
358
359 async fn can_go_previous(&self) -> fdo::Result<bool> {
360 Ok(true)
361 }
362
363 async fn can_play(&self) -> fdo::Result<bool> {
364 Ok(true)
365 }
366
367 async fn can_pause(&self) -> fdo::Result<bool> {
368 Ok(true)
369 }
370
371 async fn can_seek(&self) -> fdo::Result<bool> {
372 Ok(true)
373 }
374
375 async fn can_control(&self) -> fdo::Result<bool> {
376 Ok(true)
377 }
378}
379
380#[cfg(test)]
384mod tests {
385 use std::sync::{Arc, mpsc::Receiver};
386
387 use mecomp_core::{
388 audio::{AudioKernelSender, commands::AudioCommand},
389 test_utils::init,
390 udp::StateChange,
391 };
392
393 use mecomp_storage::db::schemas::song::Song;
394 use pretty_assertions::{assert_eq, assert_ne};
395 use rstest::rstest;
396 use tempfile::TempDir;
397
398 use super::*;
399 use crate::test_utils::fixtures;
400
401 #[rstest]
412 #[timeout(Duration::from_secs(10))]
413 #[tokio::test(flavor = "multi_thread", worker_threads = 3)]
414 async fn test_next(
415 #[future] fixtures: (
416 Mpris,
417 Receiver<StateChange>,
418 TempDir,
419 Arc<AudioKernelSender>,
420 ),
421 ) {
422 init();
423 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
424
425 assert_eq!(mpris.can_go_next().await.unwrap(), true);
426
427 let context = Context::current();
429 let songs: Vec<Song> = mpris
430 .daemon
431 .read()
432 .await
433 .as_ref()
434 .unwrap()
435 .library_songs_full(context)
436 .await
437 .unwrap()
438 .unwrap()
439 .to_vec();
440 assert_eq!(songs.len(), 4);
441 audio_kernel.send(AudioCommand::Queue(
443 mecomp_core::audio::commands::QueueCommand::AddToQueue(Box::new(songs.into())),
444 ));
445
446 let Ok(StateChange::TrackChanged(Some(first_song))) = event_rx.recv() else {
447 panic!("Expected a TrackChanged event, but got something else");
448 };
449 assert_eq!(
450 event_rx.recv(),
451 Ok(StateChange::StatusChanged(Status::Playing))
452 );
453
454 mpris.next().await.unwrap();
456
457 let Ok(StateChange::TrackChanged(Some(second_song))) = event_rx.recv() else {
458 panic!("Expected a TrackChanged event, but got something else");
459 };
460
461 assert_ne!(first_song, second_song);
463
464 drop(tempdir);
465 }
466
467 #[rstest]
468 #[timeout(Duration::from_secs(10))]
469 #[tokio::test]
470 async fn test_next_maintains_status(
471 #[future] fixtures: (
472 Mpris,
473 Receiver<StateChange>,
474 TempDir,
475 Arc<AudioKernelSender>,
476 ),
477 ) {
478 init();
479 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
480
481 assert_eq!(mpris.can_go_next().await.unwrap(), true);
482
483 let context = Context::current();
485 let songs: Vec<Song> = mpris
486 .daemon
487 .read()
488 .await
489 .as_ref()
490 .unwrap()
491 .library_songs_full(context)
492 .await
493 .unwrap()
494 .unwrap()
495 .to_vec();
496 assert_eq!(songs.len(), 4);
497 audio_kernel.send(AudioCommand::Queue(
499 mecomp_core::audio::commands::QueueCommand::AddToQueue(Box::new(songs.into())),
500 ));
501 let Ok(StateChange::TrackChanged(Some(first_song))) = event_rx.recv() else {
502 panic!("Expected a TrackChanged event, but got something else");
503 };
504 assert_eq!(
505 event_rx.recv(),
506 Ok(StateChange::StatusChanged(Status::Playing))
507 );
508
509 mpris.stop().await.unwrap();
513 assert_eq!(
514 event_rx.recv(),
515 Ok(StateChange::Seeked(Duration::from_secs(0)))
516 );
517 assert_eq!(
518 event_rx.recv(),
519 Ok(StateChange::StatusChanged(Status::Stopped))
520 );
521
522 mpris.next().await.unwrap();
524 let Ok(StateChange::TrackChanged(Some(second_song))) = event_rx.recv() else {
525 panic!("Expected a TrackChanged event, but got something else");
526 };
527 assert!(event_rx.try_recv().is_err());
529
530 assert_ne!(first_song, second_song);
532
533 mpris.pause().await.unwrap();
535 assert_eq!(
536 event_rx.recv(),
537 Ok(StateChange::StatusChanged(Status::Paused))
538 );
539
540 mpris.next().await.unwrap();
542 let Ok(StateChange::TrackChanged(Some(third_song))) = event_rx.recv() else {
543 panic!("Expected a TrackChanged event, but got something else");
544 };
545 assert!(event_rx.try_recv().is_err());
547
548 assert_ne!(second_song, third_song);
550
551 drop(tempdir);
552 }
553
554 #[rstest]
555 #[timeout(Duration::from_secs(10))]
556 #[tokio::test]
557 async fn test_next_no_next_track(
558 #[future] fixtures: (
559 Mpris,
560 Receiver<StateChange>,
561 TempDir,
562 Arc<AudioKernelSender>,
563 ),
564 ) {
565 init();
566 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
567
568 assert_eq!(mpris.can_go_next().await.unwrap(), true);
569
570 let context = Context::current();
572 let songs: Vec<Song> = mpris
573 .daemon
574 .read()
575 .await
576 .as_ref()
577 .unwrap()
578 .library_songs_full(context)
579 .await
580 .unwrap()
581 .unwrap()
582 .to_vec();
583 assert_eq!(songs.len(), 4);
584 audio_kernel.send(AudioCommand::Queue(
586 mecomp_core::audio::commands::QueueCommand::AddToQueue(Box::new(
587 songs[0].clone().into(),
588 )),
589 ));
590 let _ = event_rx.recv();
591 let _ = event_rx.recv();
592
593 mpris.next().await.unwrap();
596 assert_eq!(event_rx.recv(), Ok(StateChange::TrackChanged(None)));
597 assert_eq!(
598 event_rx.recv(),
599 Ok(StateChange::StatusChanged(Status::Stopped))
600 );
601 drop(tempdir);
602 }
603
604 #[rstest]
615 #[timeout(Duration::from_secs(10))]
616 #[tokio::test]
617 async fn test_prev(
618 #[future] fixtures: (
619 Mpris,
620 Receiver<StateChange>,
621 TempDir,
622 Arc<AudioKernelSender>,
623 ),
624 ) {
625 init();
626 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
627
628 assert_eq!(mpris.can_go_previous().await.unwrap(), true);
629
630 let context = Context::current();
632 let songs: Vec<Song> = mpris
633 .daemon
634 .read()
635 .await
636 .as_ref()
637 .unwrap()
638 .library_songs_full(context)
639 .await
640 .unwrap()
641 .unwrap()
642 .to_vec();
643 assert_eq!(songs.len(), 4);
644 let third_song: mecomp_storage::db::schemas::RecordId = songs[2].id.clone().into();
645 let fourth_song: mecomp_storage::db::schemas::RecordId = songs[3].id.clone().into();
646
647 audio_kernel.send(AudioCommand::Queue(
649 mecomp_core::audio::commands::QueueCommand::AddToQueue(Box::new(songs.into())),
650 ));
651 let _ = event_rx.recv();
652 let _ = event_rx.recv();
653
654 audio_kernel.send(AudioCommand::Queue(
656 mecomp_core::audio::commands::QueueCommand::SetPosition(3),
657 ));
658 assert_eq!(
659 event_rx.recv(),
660 Ok(StateChange::TrackChanged(Some(fourth_song.clone())))
661 );
662
663 mpris.previous().await.unwrap();
665 assert_eq!(
667 event_rx.recv(),
668 Ok(StateChange::TrackChanged(Some(third_song))),
669 );
670
671 drop(tempdir);
672 }
673 #[rstest]
674 #[timeout(Duration::from_secs(10))]
675 #[tokio::test]
676 async fn test_prev_maintains_state(
677 #[future] fixtures: (
678 Mpris,
679 Receiver<StateChange>,
680 TempDir,
681 Arc<AudioKernelSender>,
682 ),
683 ) {
684 init();
685 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
686
687 assert_eq!(mpris.can_go_previous().await.unwrap(), true);
688
689 let context = Context::current();
691 let songs: Vec<Song> = mpris
692 .daemon
693 .read()
694 .await
695 .as_ref()
696 .unwrap()
697 .library_songs_full(context)
698 .await
699 .unwrap()
700 .unwrap()
701 .to_vec();
702 assert_eq!(songs.len(), 4);
703 let second_song: mecomp_storage::db::schemas::RecordId = songs[1].id.clone().into();
704 let third_song: mecomp_storage::db::schemas::RecordId = songs[2].id.clone().into();
705 let fourth_song: mecomp_storage::db::schemas::RecordId = songs[3].id.clone().into();
706
707 audio_kernel.send(AudioCommand::Queue(
709 mecomp_core::audio::commands::QueueCommand::AddToQueue(Box::new(songs.into())),
710 ));
711 let _ = event_rx.recv();
712 let _ = event_rx.recv();
713
714 audio_kernel.send(AudioCommand::Queue(
716 mecomp_core::audio::commands::QueueCommand::SetPosition(3),
717 ));
718 assert_eq!(
719 event_rx.recv(),
720 Ok(StateChange::TrackChanged(Some(fourth_song.clone())))
721 );
722
723 mpris.stop().await.unwrap();
727 assert_eq!(
728 event_rx.recv(),
729 Ok(StateChange::Seeked(Duration::from_secs(0)))
730 );
731 assert_eq!(
732 event_rx.recv(),
733 Ok(StateChange::StatusChanged(Status::Stopped))
734 );
735 mpris.previous().await.unwrap();
737 assert_eq!(
739 event_rx.recv(),
740 Ok(StateChange::TrackChanged(Some(third_song))),
741 );
742 assert!(event_rx.try_recv().is_err());
744
745 mpris.pause().await.unwrap();
747 assert!(matches!(
748 event_rx.recv(),
749 Ok(StateChange::StatusChanged(Status::Paused))
750 ));
751
752 mpris.previous().await.unwrap();
754 assert_eq!(
756 event_rx.recv(),
757 Ok(StateChange::TrackChanged(Some(second_song))),
758 );
759 assert!(event_rx.try_recv().is_err());
761
762 drop(tempdir);
763 }
764
765 #[rstest]
766 #[timeout(Duration::from_secs(10))]
767 #[tokio::test]
768 async fn test_prev_no_prev_track(
769 #[future] fixtures: (
770 Mpris,
771 Receiver<StateChange>,
772 TempDir,
773 Arc<AudioKernelSender>,
774 ),
775 ) {
776 init();
777 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
778
779 assert_eq!(mpris.can_go_previous().await.unwrap(), true);
780
781 let context = Context::current();
783 let songs: Vec<Song> = mpris
784 .daemon
785 .read()
786 .await
787 .as_ref()
788 .unwrap()
789 .library_songs_full(context)
790 .await
791 .unwrap()
792 .unwrap()
793 .to_vec();
794 assert_eq!(songs.len(), 4);
795
796 audio_kernel.send(AudioCommand::Queue(
798 mecomp_core::audio::commands::QueueCommand::AddToQueue(Box::new(
799 songs[0].clone().into(),
800 )),
801 ));
802 let _ = event_rx.recv();
803 let _ = event_rx.recv();
804
805 mpris.previous().await.unwrap();
808 assert_eq!(event_rx.recv(), Ok(StateChange::TrackChanged(None)),);
810 assert!(matches!(
812 event_rx.recv(),
813 Ok(StateChange::StatusChanged(Status::Stopped))
814 ));
815
816 drop(tempdir);
817 }
818
819 #[rstest]
862 #[timeout(Duration::from_secs(10))]
863 #[tokio::test]
864 async fn test_play_pause_stop(
865 #[future] fixtures: (
866 Mpris,
867 Receiver<StateChange>,
868 TempDir,
869 Arc<AudioKernelSender>,
870 ),
871 ) {
872 init();
873 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
874
875 assert_eq!(mpris.can_pause().await.unwrap(), true);
876 assert_eq!(mpris.can_play().await.unwrap(), true);
877 assert_eq!(mpris.can_control().await.unwrap(), true);
878
879 let context = Context::current();
881 let songs: Vec<Song> = mpris
882 .daemon
883 .read()
884 .await
885 .as_ref()
886 .unwrap()
887 .library_songs_full(context)
888 .await
889 .unwrap()
890 .unwrap()
891 .to_vec();
892 assert_eq!(songs.len(), 4);
893 let first_song = songs[0].clone();
894
895 mpris.play().await.unwrap();
897 let event = event_rx.try_recv();
898 assert!(
899 event.is_err(),
900 "Expected not to receive an event, but got {event:?}"
901 );
902
903 audio_kernel.send(AudioCommand::Queue(
905 mecomp_core::audio::commands::QueueCommand::AddToQueue(Box::new(
906 first_song.clone().into(),
907 )),
908 ));
909
910 assert_eq!(
911 event_rx.recv(),
912 Ok(StateChange::TrackChanged(Some(
913 first_song.id.clone().into()
914 )))
915 );
916 assert_eq!(
917 event_rx.recv(),
918 Ok(StateChange::StatusChanged(Status::Playing))
919 );
920
921 mpris.pause().await.unwrap();
923 assert_eq!(
924 event_rx.recv(),
925 Ok(StateChange::StatusChanged(Status::Paused))
926 );
927
928 mpris.pause().await.unwrap();
930 let event = event_rx.try_recv();
931 assert!(
932 event.is_err(),
933 "Expected not to receive an event, but got {event:?}"
934 );
935
936 mpris.play().await.unwrap();
941 assert_eq!(
942 event_rx.recv(),
943 Ok(StateChange::StatusChanged(Status::Playing))
944 );
945
946 mpris.play().await.unwrap();
948 let event = event_rx.try_recv();
949 assert!(
950 event.is_err(),
951 "Expected not to receive an event, but got {event:?}"
952 );
953
954 mpris.play_pause().await.unwrap();
962 assert_eq!(
963 event_rx.recv(),
964 Ok(StateChange::StatusChanged(Status::Paused))
965 );
966
967 mpris.play_pause().await.unwrap();
969 assert_eq!(
970 event_rx.recv(),
971 Ok(StateChange::StatusChanged(Status::Playing))
972 );
973
974 mpris.stop().await.unwrap();
976 assert_eq!(
977 event_rx.recv(),
978 Ok(StateChange::Seeked(Duration::from_secs(0)))
979 );
980 assert_eq!(
981 event_rx.recv(),
982 Ok(StateChange::StatusChanged(Status::Stopped))
983 );
984 mpris.play_pause().await.unwrap();
985 assert_eq!(
986 event_rx.recv(),
987 Ok(StateChange::StatusChanged(Status::Playing))
988 );
989
990 mpris.stop().await.unwrap();
992 assert_eq!(
993 event_rx.recv(),
994 Ok(StateChange::Seeked(Duration::from_secs(0)))
995 );
996 assert_eq!(
997 event_rx.recv(),
998 Ok(StateChange::StatusChanged(Status::Stopped))
999 );
1000
1001 mpris.stop().await.unwrap();
1003 let event = event_rx.try_recv();
1004 assert!(
1005 event.is_err(),
1006 "Expected not to receive an event, but got {event:?}"
1007 );
1008
1009 drop(tempdir);
1013 }
1014
1015 #[rstest]
1031 #[timeout(Duration::from_secs(10))]
1032 #[tokio::test]
1033 async fn test_open_uri(
1034 #[future] fixtures: (
1035 Mpris,
1036 Receiver<StateChange>,
1037 TempDir,
1038 Arc<AudioKernelSender>,
1039 ),
1040 ) {
1041 init();
1042 let (mpris, event_rx, tempdir, _) = fixtures.await;
1043
1044 let context = Context::current();
1046 let songs: Vec<Song> = mpris
1047 .daemon
1048 .read()
1049 .await
1050 .as_ref()
1051 .unwrap()
1052 .library_songs_full(context)
1053 .await
1054 .unwrap()
1055 .unwrap()
1056 .to_vec();
1057 assert_eq!(songs.len(), 4);
1058 let first_song = songs[0].clone();
1059
1060 let file_uri = format!("file://{}", first_song.path.display());
1064 mpris.open_uri(file_uri).await.unwrap();
1065 assert_eq!(
1066 event_rx.recv(),
1067 Ok(StateChange::TrackChanged(Some(
1068 first_song.id.clone().into()
1069 )))
1070 );
1071 assert_eq!(
1073 event_rx.recv(),
1074 Ok(StateChange::StatusChanged(Status::Playing))
1075 );
1076
1077 let file_uri = "http://example.com/song.mp3".to_string();
1082 let result = mpris.open_uri(file_uri).await;
1083 assert!(result.is_err());
1084 let file_uri = "file://".to_string();
1086 let result = mpris.open_uri(file_uri).await;
1087 assert!(result.is_err());
1088 let file_uri = format!("file://{}", tempdir.path().display());
1090 let result = mpris.open_uri(file_uri).await;
1091 assert!(result.is_err());
1092 let file_uri = "file:///nonexistent.mp3".to_string();
1094 let result = mpris.open_uri(file_uri).await;
1095 assert!(result.is_err());
1096 std::fs::write(tempdir.path().join("unsupported.txt"), "unsupported")
1098 .expect("Failed to write file");
1099 let file_uri = format!(
1100 "file:///{}",
1101 tempdir.path().join("unsupported.txt").display()
1102 );
1103 let result = mpris.open_uri(file_uri).await;
1104 assert!(result.is_err());
1105
1106 drop(tempdir);
1110 }
1111
1112 #[rstest]
1117 #[timeout(Duration::from_secs(10))]
1118 #[tokio::test]
1119 async fn test_playback_status(
1120 #[future] fixtures: (
1121 Mpris,
1122 Receiver<StateChange>,
1123 TempDir,
1124 Arc<AudioKernelSender>,
1125 ),
1126 ) {
1127 init();
1128 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
1129
1130 let context = Context::current();
1132 let songs: Vec<Song> = mpris
1133 .daemon
1134 .read()
1135 .await
1136 .as_ref()
1137 .unwrap()
1138 .library_songs_full(context)
1139 .await
1140 .unwrap()
1141 .unwrap()
1142 .to_vec();
1143 assert_eq!(songs.len(), 4);
1144 let first_song = songs[0].clone();
1145
1146 assert_eq!(
1149 mpris.playback_status().await.unwrap(),
1150 PlaybackStatus::Stopped
1151 );
1152
1153 audio_kernel.send(AudioCommand::Queue(
1155 mecomp_core::audio::commands::QueueCommand::AddToQueue(Box::new(
1156 first_song.clone().into(),
1157 )),
1158 ));
1159
1160 mpris.pause().await.unwrap();
1162
1163 let mut events = [false; 3];
1165 for _ in 0..3 {
1166 let event = event_rx.recv().unwrap();
1167
1168 match event {
1169 StateChange::TrackChanged(Some(_)) => {
1170 mpris.state.write().await.current_song = Some(first_song.clone());
1171 events[0] = true;
1172 }
1173 StateChange::StatusChanged(Status::Playing) => {
1174 mpris.state.write().await.status = Status::Playing;
1175 assert_eq!(
1176 mpris.playback_status().await.unwrap(),
1177 PlaybackStatus::Playing
1178 );
1179 events[1] = true;
1180 }
1181 StateChange::StatusChanged(Status::Paused) => {
1182 mpris.state.write().await.status = Status::Paused;
1183 assert_eq!(
1184 mpris.playback_status().await.unwrap(),
1185 PlaybackStatus::Paused
1186 );
1187 events[2] = true;
1188 }
1189 _ => panic!("Unexpected event: {event:?}"),
1190 }
1191 }
1192
1193 assert!(events.iter().all(|&e| e));
1194
1195 drop(tempdir);
1196 }
1197
1198 #[rstest]
1210 #[timeout(Duration::from_secs(10))]
1211 #[tokio::test]
1212 async fn test_loop_status(
1213 #[future] fixtures: (
1214 Mpris,
1215 Receiver<StateChange>,
1216 TempDir,
1217 Arc<AudioKernelSender>,
1218 ),
1219 ) {
1220 init();
1221 let (mpris, event_rx, _, _) = fixtures.await;
1222
1223 assert_eq!(mpris.loop_status().await.unwrap(), LoopStatus::None);
1226
1227 mpris.set_loop_status(LoopStatus::Track).await.unwrap();
1229 if event_rx.recv() == Ok(StateChange::RepeatModeChanged(RepeatMode::One)) {
1230 mpris.state.write().await.repeat_mode = RepeatMode::One;
1231 } else {
1232 panic!("Expected a RepeatModeChanged event, but got something else");
1233 };
1234 assert_eq!(mpris.loop_status().await.unwrap(), LoopStatus::Track);
1235
1236 mpris.set_loop_status(LoopStatus::Playlist).await.unwrap();
1238 if event_rx.recv() == Ok(StateChange::RepeatModeChanged(RepeatMode::All)) {
1239 mpris.state.write().await.repeat_mode = RepeatMode::All;
1240 } else {
1241 panic!("Expected a RepeatModeChanged event, but got something else");
1242 };
1243 assert_eq!(mpris.loop_status().await.unwrap(), LoopStatus::Playlist);
1244
1245 mpris.set_loop_status(LoopStatus::None).await.unwrap();
1247 if event_rx.recv() == Ok(StateChange::RepeatModeChanged(RepeatMode::None)) {
1248 mpris.state.write().await.repeat_mode = RepeatMode::None;
1249 } else {
1250 panic!("Expected a RepeatModeChanged event, but got something else");
1251 };
1252 assert_eq!(mpris.loop_status().await.unwrap(), LoopStatus::None);
1253 }
1254
1255 #[rstest]
1271 #[timeout(Duration::from_secs(10))]
1272 #[tokio::test]
1273 async fn test_rate(
1274 #[future] fixtures: (
1275 Mpris,
1276 Receiver<StateChange>,
1277 TempDir,
1278 Arc<AudioKernelSender>,
1279 ),
1280 ) {
1281 init();
1282 let (mpris, event_rx, _, _) = fixtures.await;
1283
1284 let rate = mpris.rate().await.unwrap();
1286 assert!(f64::EPSILON > (rate - 1.0).abs(), "{rate} != 1.0");
1287
1288 let min_rate = mpris.minimum_rate().await.unwrap();
1290 assert!(f64::EPSILON > (min_rate - 1.0).abs(), "{min_rate} != 1.0");
1291
1292 let max_rate = mpris.maximum_rate().await.unwrap();
1294 assert!(f64::EPSILON > (max_rate - 1.0).abs(), "{max_rate} != 1.0");
1295
1296 let result = mpris.set_rate(1.0).await;
1299 assert!(result.is_ok());
1300 assert!(event_rx.try_recv().is_err());
1301 }
1302
1303 #[rstest]
1319 #[timeout(Duration::from_secs(10))]
1320 #[tokio::test]
1321 async fn test_shuffle(
1322 #[future] fixtures: (
1323 Mpris,
1324 Receiver<StateChange>,
1325 TempDir,
1326 Arc<AudioKernelSender>,
1327 ),
1328 ) {
1329 init();
1330 let (mpris, event_rx, _, _) = fixtures.await;
1331
1332 assert_eq!(mpris.shuffle().await.unwrap(), true);
1334
1335 mpris.set_shuffle(true).await.unwrap();
1338 assert_eq!(mpris.shuffle().await.unwrap(), true);
1339 assert!(event_rx.try_recv().is_err());
1340
1341 mpris.set_shuffle(false).await.unwrap();
1343 assert_eq!(mpris.shuffle().await.unwrap(), true);
1344 assert!(event_rx.try_recv().is_err());
1345 }
1346
1347 #[rstest]
1355 #[timeout(Duration::from_secs(10))]
1356 #[tokio::test]
1357 async fn test_metadata(
1358 #[future] fixtures: (
1359 Mpris,
1360 Receiver<StateChange>,
1361 TempDir,
1362 Arc<AudioKernelSender>,
1363 ),
1364 ) {
1365 init();
1366 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
1367
1368 let context = Context::current();
1370 let songs: Vec<Song> = mpris
1371 .daemon
1372 .read()
1373 .await
1374 .as_ref()
1375 .unwrap()
1376 .library_songs_full(context)
1377 .await
1378 .unwrap()
1379 .unwrap()
1380 .to_vec();
1381 assert_eq!(songs.len(), 4);
1382 let first_song = songs[0].clone();
1383
1384 assert_eq!(
1387 mpris.metadata().await.unwrap(),
1388 Metadata::builder().trackid(TrackId::NO_TRACK).build()
1389 );
1390
1391 audio_kernel.send(AudioCommand::Queue(
1393 mecomp_core::audio::commands::QueueCommand::AddToQueue(Box::new(
1394 first_song.clone().into(),
1395 )),
1396 ));
1397
1398 assert_eq!(
1399 event_rx.recv(),
1400 Ok(StateChange::TrackChanged(Some(
1401 first_song.id.clone().into()
1402 )))
1403 );
1404
1405 *mpris.state.write().await = mpris
1406 .daemon
1407 .read()
1408 .await
1409 .as_ref()
1410 .unwrap()
1411 .state_audio(Context::current())
1412 .await
1413 .unwrap()
1414 .unwrap();
1415
1416 let metadata = mpris.metadata().await.unwrap();
1418 assert_eq!(metadata, metadata_from_opt_song(Some(&first_song)));
1419 assert_ne!(
1420 metadata,
1421 Metadata::builder().trackid(TrackId::NO_TRACK).build()
1422 );
1423
1424 drop(tempdir);
1425 }
1426
1427 #[rstest]
1432 #[timeout(Duration::from_secs(10))]
1433 #[tokio::test]
1434 async fn test_volume(
1435 #[future] fixtures: (
1436 Mpris,
1437 Receiver<StateChange>,
1438 TempDir,
1439 Arc<AudioKernelSender>,
1440 ),
1441 ) {
1442 let (mpris, event_rx, _, _) = fixtures.await;
1443
1444 let volume = mpris.volume().await.unwrap();
1446 assert!(f64::EPSILON > (volume - 1.0).abs(), "{volume} != 1.0");
1447
1448 mpris.set_volume(-1.0).await.unwrap();
1450 if event_rx.recv() == Ok(StateChange::VolumeChanged(0.0)) {
1451 mpris.state.write().await.volume = 0.0;
1452 let volume = mpris.volume().await.unwrap();
1453 assert!(f64::EPSILON > volume.abs(), "{volume} != 0.0");
1454 } else {
1455 panic!("Expected a VolumeChanged event, but got something else");
1456 }
1457
1458 mpris.set_volume(1.0).await.unwrap();
1460 if event_rx.recv() == Ok(StateChange::VolumeChanged(1.0)) {
1461 mpris.state.write().await.volume = 1.0;
1462 let volume = mpris.volume().await.unwrap();
1463 assert!(f64::EPSILON > (volume - 1.0).abs(), "{volume} != 1.0");
1464 } else {
1465 panic!("Expected a VolumeChanged event, but got something else");
1466 }
1467
1468 mpris.set_volume(1.0).await.unwrap();
1470 assert!(event_rx.try_recv().is_err());
1471 }
1472
1473 #[rstest]
1486 #[timeout(Duration::from_secs(10))]
1487 #[tokio::test]
1488 async fn test_position(
1489 #[future] fixtures: (
1490 Mpris,
1491 Receiver<StateChange>,
1492 TempDir,
1493 Arc<AudioKernelSender>,
1494 ),
1495 ) {
1496 init();
1497 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
1498
1499 assert!(mpris.can_seek().await.unwrap());
1500
1501 let context = Context::current();
1503 let songs: Vec<Song> = mpris
1504 .daemon
1505 .read()
1506 .await
1507 .as_ref()
1508 .unwrap()
1509 .library_songs_full(context)
1510 .await
1511 .unwrap()
1512 .unwrap()
1513 .to_vec();
1514 assert_eq!(songs.len(), 4);
1515 let first_song = songs[0].clone();
1516
1517 assert_eq!(mpris.position().await.unwrap(), Time::from_micros(0));
1520
1521 audio_kernel.send(AudioCommand::Queue(
1523 mecomp_core::audio::commands::QueueCommand::AddToQueue(Box::new(
1524 first_song.clone().into(),
1525 )),
1526 ));
1527 audio_kernel.send(AudioCommand::Pause);
1528 let _ = event_rx.recv().unwrap();
1529 let _ = event_rx.recv().unwrap();
1530 let _ = event_rx.recv().unwrap();
1531 *mpris.state.write().await = mpris
1533 .daemon
1534 .read()
1535 .await
1536 .as_ref()
1537 .unwrap()
1538 .state_audio(Context::current())
1539 .await
1540 .unwrap()
1541 .unwrap();
1542
1543 let first_song_track_id = mpris.metadata().await.unwrap().trackid().unwrap();
1544
1545 mpris
1547 .set_position(first_song_track_id.clone(), Time::from_secs(2))
1548 .await
1549 .unwrap();
1550 assert_eq!(
1551 event_rx.recv(),
1552 Ok(StateChange::Seeked(Duration::from_secs(2)))
1553 );
1554
1555 mpris
1557 .set_position(first_song_track_id.clone(), Time::from_secs(-1))
1558 .await
1559 .unwrap();
1560 assert!(event_rx.try_recv().is_err());
1561
1562 mpris
1564 .set_position(first_song_track_id.clone(), Time::from_secs(100))
1565 .await
1566 .unwrap();
1567 assert!(event_rx.try_recv().is_err());
1568
1569 assert!(mpris.can_seek().await.unwrap());
1572
1573 mpris.seek(Time::from_secs(1)).await.unwrap();
1574 assert_eq!(
1575 event_rx.recv(),
1576 Ok(StateChange::Seeked(Duration::from_secs(3)))
1577 );
1578
1579 mpris.seek(Time::from_secs(-1)).await.unwrap();
1580 assert_eq!(
1581 event_rx.recv(),
1582 Ok(StateChange::Seeked(Duration::from_secs(2)))
1583 );
1584
1585 drop(tempdir);
1586 }
1587}