1use std::{path::PathBuf, str::FromStr, time::Duration};
6
7use futures::executor::block_on;
8use mecomp_core::state::{RepeatMode, SeekType, Status};
9use mecomp_prost::{PlaybackSeekRequest, PlaybackSkipRequest, PlaybackVolumeSetRequest};
10use mpris_server::{
11 LoopStatus, Metadata, PlaybackRate, PlaybackStatus, PlayerInterface, Time, TrackId, Volume,
12 zbus::{Error as ZbusError, fdo},
13};
14
15use crate::{Mpris, interfaces::root::SUPPORTED_MIME_TYPES, metadata_from_opt_song};
16
17impl PlayerInterface for Mpris {
18 async fn next(&self) -> fdo::Result<()> {
19 block_on(
20 self.daemon
21 .clone()
22 .playback_skip_forward(PlaybackSkipRequest::new(1)),
23 )
24 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
25 Ok(())
26 }
27
28 async fn previous(&self) -> fdo::Result<()> {
29 block_on(
30 self.daemon
31 .clone()
32 .playback_skip_backward(PlaybackSkipRequest::new(1)),
33 )
34 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
35 Ok(())
36 }
37
38 async fn pause(&self) -> fdo::Result<()> {
39 block_on(self.daemon.clone().playback_pause(()))
40 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
41
42 Ok(())
43 }
44
45 async fn play_pause(&self) -> fdo::Result<()> {
46 block_on(self.daemon.clone().playback_toggle(()))
47 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
48 Ok(())
49 }
50
51 async fn stop(&self) -> fdo::Result<()> {
52 block_on(self.daemon.clone().playback_stop(()))
53 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
54 Ok(())
55 }
56
57 async fn play(&self) -> fdo::Result<()> {
58 block_on(self.daemon.clone().playback_play(()))
59 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
60 Ok(())
61 }
62
63 async fn open_uri(&self, uri: String) -> fdo::Result<()> {
64 log::info!("Opening URI: {uri}");
68
69 if !uri.starts_with("file://") {
71 return Err(fdo::Error::InvalidArgs(
72 "Only file:// URIs are supported".to_string(),
73 ));
74 }
75
76 let path = uri.strip_prefix("file://").unwrap();
78 let mut path = PathBuf::from_str(path)
79 .map_err(|_| fdo::Error::InvalidArgs("Invalid file path".to_string()))?;
80
81 path = percent_encoding::percent_decode_str(&path.to_string_lossy())
83 .decode_utf8()
84 .map_err(|_| fdo::Error::InvalidArgs("Invalid file path".to_string()))?
85 .into_owned()
86 .into();
87
88 if !SUPPORTED_MIME_TYPES
90 .iter()
91 .filter_map(|s| s.split('/').next_back())
92 .any(|ext| path.extension().is_some_and(|e| e == ext))
93 {
94 return Err(fdo::Error::InvalidArgs(
95 "File type not supported".to_string(),
96 ));
97 }
98
99 if path.starts_with("~") {
101 path = shellexpand::tilde(&path.to_string_lossy())
102 .into_owned()
103 .into();
104 }
105
106 log::debug!("Locating file: {}", path.display());
107
108 if !path.exists() {
110 return Err(fdo::Error::InvalidArgs("File does not exist".to_string()));
111 }
112
113 if !path.is_file() {
115 return Err(fdo::Error::InvalidArgs("Path is not a file".to_string()));
116 }
117
118 path = path.canonicalize().unwrap_or(path);
120
121 let mut daemon = self.daemon.clone();
123 if let Some(song) = block_on(daemon.library_song_get_by_path(mecomp_prost::Path::new(path)))
124 .map_err(|e| fdo::Error::Failed(e.to_string()))?
125 .into_inner()
126 .song
127 {
128 block_on(daemon.queue_add(song.id)).map_err(|e| fdo::Error::Failed(e.to_string()))?;
129 } else {
130 return Err(fdo::Error::Failed(
131 "Failed to find song in database".to_string(),
132 ));
133 }
134
135 Ok(())
136 }
137
138 async fn playback_status(&self) -> fdo::Result<PlaybackStatus> {
139 let status = self.state.read().await.status;
140 match status {
141 Status::Stopped => Ok(PlaybackStatus::Stopped),
142 Status::Paused => Ok(PlaybackStatus::Paused),
143 Status::Playing => Ok(PlaybackStatus::Playing),
144 }
145 }
146
147 async fn loop_status(&self) -> fdo::Result<LoopStatus> {
148 let repeat_mode = self.state.read().await.repeat_mode;
149 match repeat_mode {
150 RepeatMode::None => Ok(LoopStatus::None),
151 RepeatMode::One => Ok(LoopStatus::Track),
152 RepeatMode::All => Ok(LoopStatus::Playlist),
153 }
154 }
155
156 async fn set_loop_status(&self, loop_status: LoopStatus) -> Result<(), ZbusError> {
157 let repeat_mode = match loop_status {
158 LoopStatus::None => RepeatMode::None,
159 LoopStatus::Track => RepeatMode::One,
160 LoopStatus::Playlist => RepeatMode::All,
161 };
162
163 let mut daemon = self.daemon.clone();
164 block_on(daemon.playback_repeat(mecomp_prost::PlaybackRepeatRequest::new(repeat_mode)))
165 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
166
167 Ok(())
168 }
169
170 async fn rate(&self) -> fdo::Result<PlaybackRate> {
171 Ok(1.0)
172 }
173
174 async fn set_rate(&self, _: PlaybackRate) -> Result<(), ZbusError> {
175 Ok(())
176 }
177
178 async fn shuffle(&self) -> fdo::Result<bool> {
179 Ok(true)
183 }
184
185 async fn set_shuffle(&self, shuffle: bool) -> Result<(), ZbusError> {
186 if shuffle {
188 let mut daemon = self.daemon.clone();
189
190 block_on(daemon.playback_shuffle(())).map_err(|e| fdo::Error::Failed(e.to_string()))?;
191 }
192
193 Ok(())
194 }
195
196 async fn metadata(&self) -> fdo::Result<Metadata> {
197 let state = self.state.read().await;
198
199 Ok(metadata_from_opt_song(state.current_song.as_ref()))
200 }
201
202 async fn volume(&self) -> fdo::Result<Volume> {
203 let state = self.state.read().await;
204 if state.muted {
205 Ok(0.0)
206 } else {
207 Ok(f64::from(state.volume))
208 }
209 }
210
211 async fn set_volume(&self, volume: Volume) -> Result<(), ZbusError> {
212 let mut daemon = self.daemon.clone();
213 #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
214 block_on(daemon.playback_volume(PlaybackVolumeSetRequest::new(volume as f32)))
215 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
216
217 Ok(())
218 }
219
220 async fn position(&self) -> fdo::Result<Time> {
221 self.state.read().await.runtime.map_or_else(
222 || Ok(Time::from_micros(0)),
223 |runtime| {
224 Ok(Time::from_micros(
225 i64::try_from(runtime.seek_position.as_micros()).unwrap_or(i64::MAX),
226 ))
227 },
228 )
229 }
230
231 async fn seek(&self, offset: Time) -> fdo::Result<()> {
232 let seek_type = if offset.as_micros() > 0 {
234 SeekType::RelativeForwards
235 } else {
236 SeekType::RelativeBackwards
237 };
238
239 let offset = Duration::from_micros(offset.as_micros().unsigned_abs());
240
241 let mut daemon = self.daemon.clone();
242
243 block_on(daemon.playback_seek(PlaybackSeekRequest::new(seek_type, offset)))
244 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
245 Ok(())
246 }
247
248 async fn set_position(&self, track_id: TrackId, position: Time) -> fdo::Result<()> {
249 if Some(track_id) != self.metadata().await?.trackid() {
251 return Ok(());
252 }
253
254 if let Some(song) = self.state.read().await.current_song.as_ref() {
255 let position = position.as_micros();
257 if position < 0 || u128::from(position.unsigned_abs()) > song.runtime.as_micros() {
258 return Ok(());
259 }
260
261 block_on(self.daemon.clone().playback_seek(PlaybackSeekRequest::new(
262 SeekType::Absolute,
263 Duration::from_micros(u64::try_from(position).unwrap_or_default()),
264 )))
265 .map_err(|e| fdo::Error::Failed(e.to_string()))?;
266 }
267
268 Ok(())
269 }
270
271 async fn minimum_rate(&self) -> fdo::Result<PlaybackRate> {
272 Ok(1.0)
273 }
274
275 async fn maximum_rate(&self) -> fdo::Result<PlaybackRate> {
276 Ok(1.0)
277 }
278
279 async fn can_go_next(&self) -> fdo::Result<bool> {
280 Ok(true)
281 }
282
283 async fn can_go_previous(&self) -> fdo::Result<bool> {
284 Ok(true)
285 }
286
287 async fn can_play(&self) -> fdo::Result<bool> {
288 Ok(true)
289 }
290
291 async fn can_pause(&self) -> fdo::Result<bool> {
292 Ok(true)
293 }
294
295 async fn can_seek(&self) -> fdo::Result<bool> {
296 Ok(true)
297 }
298
299 async fn can_control(&self) -> fdo::Result<bool> {
300 Ok(true)
301 }
302}
303
304#[cfg(test)]
308mod tests {
309 use std::sync::{Arc, mpsc::Receiver};
310
311 use mecomp_core::{
312 audio::{
313 AudioKernelSender,
314 commands::{AudioCommand, QueueCommand},
315 },
316 test_utils::init,
317 udp::StateChange,
318 };
319
320 use mecomp_storage::db::schemas::song::SongBrief;
321 use one_or_many::OneOrMany;
322 use pretty_assertions::{assert_eq, assert_ne};
323 use rstest::rstest;
324 use tempfile::TempDir;
325
326 use super::*;
327 use crate::test_utils::fixtures;
328
329 #[rstest]
340 #[timeout(Duration::from_secs(20))]
341 #[tokio::test(flavor = "multi_thread", worker_threads = 3)]
342 async fn test_next(
343 #[future] fixtures: (
344 Mpris,
345 Receiver<StateChange>,
346 TempDir,
347 Arc<AudioKernelSender>,
348 ),
349 ) {
350 init();
351 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
352
353 assert_eq!(mpris.can_go_next().await.unwrap(), true);
354
355 let songs = mpris
357 .daemon
358 .clone()
359 .library_songs(())
360 .await
361 .unwrap()
362 .into_inner()
363 .songs;
364 assert_eq!(songs.len(), 4);
365 audio_kernel.send(AudioCommand::Queue(QueueCommand::AddToQueue(
367 songs.into_iter().map(Into::into).collect(),
368 )));
369 assert_eq!(event_rx.recv(), Ok(StateChange::QueueChanged));
370 let Ok(StateChange::TrackChanged(Some(first_song))) = event_rx.recv() else {
371 panic!("Expected a TrackChanged event, but got something else");
372 };
373 assert_eq!(
374 event_rx.recv(),
375 Ok(StateChange::StatusChanged(Status::Playing))
376 );
377
378 mpris.next().await.unwrap();
380
381 let Ok(StateChange::TrackChanged(Some(second_song))) = event_rx.recv() else {
382 panic!("Expected a TrackChanged event, but got something else");
383 };
384
385 assert_ne!(first_song, second_song);
387
388 drop(tempdir);
389 }
390
391 #[rstest]
392 #[timeout(Duration::from_secs(20))]
393 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
394 async fn test_next_maintains_status(
395 #[future] fixtures: (
396 Mpris,
397 Receiver<StateChange>,
398 TempDir,
399 Arc<AudioKernelSender>,
400 ),
401 ) {
402 init();
403 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
404
405 assert_eq!(mpris.can_go_next().await.unwrap(), true);
406
407 let songs = mpris
409 .daemon
410 .clone()
411 .library_songs(())
412 .await
413 .unwrap()
414 .into_inner()
415 .songs;
416 assert_eq!(songs.len(), 4);
417 audio_kernel.send(AudioCommand::Queue(QueueCommand::AddToQueue(
419 songs.into_iter().map(Into::into).collect(),
420 )));
421 assert_eq!(event_rx.recv(), Ok(StateChange::QueueChanged));
422 let Ok(StateChange::TrackChanged(Some(first_song))) = event_rx.recv() else {
423 panic!("Expected a TrackChanged event, but got something else");
424 };
425 assert_eq!(
426 event_rx.recv(),
427 Ok(StateChange::StatusChanged(Status::Playing))
428 );
429
430 mpris.stop().await.unwrap();
434 assert_eq!(
435 event_rx.recv(),
436 Ok(StateChange::Seeked(Duration::from_secs(0)))
437 );
438 assert_eq!(
439 event_rx.recv(),
440 Ok(StateChange::StatusChanged(Status::Stopped))
441 );
442
443 mpris.next().await.unwrap();
445 let Ok(StateChange::TrackChanged(Some(second_song))) = event_rx.recv() else {
446 panic!("Expected a TrackChanged event, but got something else");
447 };
448 assert!(event_rx.try_recv().is_err());
450
451 assert_ne!(first_song, second_song);
453
454 mpris.pause().await.unwrap();
456 assert_eq!(
457 event_rx.recv(),
458 Ok(StateChange::StatusChanged(Status::Paused))
459 );
460
461 mpris.next().await.unwrap();
463 let Ok(StateChange::TrackChanged(Some(third_song))) = event_rx.recv() else {
464 panic!("Expected a TrackChanged event, but got something else");
465 };
466 assert!(event_rx.try_recv().is_err());
468
469 assert_ne!(second_song, third_song);
471
472 drop(tempdir);
473 }
474
475 #[rstest]
476 #[timeout(Duration::from_secs(20))]
477 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
478 async fn test_next_no_next_track(
479 #[future] fixtures: (
480 Mpris,
481 Receiver<StateChange>,
482 TempDir,
483 Arc<AudioKernelSender>,
484 ),
485 ) {
486 init();
487 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
488
489 assert_eq!(mpris.can_go_next().await.unwrap(), true);
490
491 let songs = mpris
493 .daemon
494 .clone()
495 .library_songs(())
496 .await
497 .unwrap()
498 .into_inner()
499 .songs;
500 assert_eq!(songs.len(), 4);
501 audio_kernel.send(AudioCommand::Queue(QueueCommand::AddToQueue(
503 OneOrMany::One(Box::new(songs[0].clone().into())),
504 )));
505 let _ = event_rx.recv();
506 let _ = event_rx.recv();
507 let _ = event_rx.recv();
508
509 mpris.next().await.unwrap();
512 assert_eq!(
513 event_rx.recv(),
514 Ok(StateChange::QueueChanged),
515 "since it was the last song, the queue clears"
516 );
517 assert_eq!(event_rx.recv(), Ok(StateChange::TrackChanged(None)));
518 assert_eq!(
519 event_rx.recv(),
520 Ok(StateChange::StatusChanged(Status::Stopped))
521 );
522 drop(tempdir);
523 }
524
525 #[rstest]
536 #[timeout(Duration::from_secs(20))]
537 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
538 async fn test_prev(
539 #[future] fixtures: (
540 Mpris,
541 Receiver<StateChange>,
542 TempDir,
543 Arc<AudioKernelSender>,
544 ),
545 ) {
546 init();
547 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
548
549 assert_eq!(mpris.can_go_previous().await.unwrap(), true);
550
551 let songs = mpris
553 .daemon
554 .clone()
555 .library_songs(())
556 .await
557 .unwrap()
558 .into_inner()
559 .songs;
560 assert_eq!(songs.len(), 4);
561 let first_song: mecomp_storage::db::schemas::RecordId = songs[0].id.clone().into();
562 let third_song: mecomp_storage::db::schemas::RecordId = songs[2].id.clone().into();
563 let fourth_song: mecomp_storage::db::schemas::RecordId = songs[3].id.clone().into();
564
565 audio_kernel.send(AudioCommand::Queue(QueueCommand::AddToQueue(
567 songs.into_iter().map(Into::into).collect(),
568 )));
569 assert_eq!(event_rx.recv(), Ok(StateChange::QueueChanged));
570 assert_eq!(
571 event_rx.recv(),
572 Ok(StateChange::TrackChanged(Some(first_song)))
573 );
574 assert_eq!(
575 event_rx.recv(),
576 Ok(StateChange::StatusChanged(Status::Playing))
577 );
578
579 audio_kernel.send(AudioCommand::Queue(QueueCommand::SetPosition(3)));
581 assert_eq!(
582 event_rx.recv(),
583 Ok(StateChange::TrackChanged(Some(fourth_song.clone())))
584 );
585
586 mpris.previous().await.unwrap();
588 assert_eq!(
590 event_rx.recv(),
591 Ok(StateChange::TrackChanged(Some(third_song))),
592 );
593
594 drop(tempdir);
595 }
596 #[rstest]
597 #[timeout(Duration::from_secs(20))]
598 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
599 async fn test_prev_maintains_state(
600 #[future] fixtures: (
601 Mpris,
602 Receiver<StateChange>,
603 TempDir,
604 Arc<AudioKernelSender>,
605 ),
606 ) {
607 init();
608 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
609
610 assert_eq!(mpris.can_go_previous().await.unwrap(), true);
611
612 let songs = mpris
614 .daemon
615 .clone()
616 .library_songs(())
617 .await
618 .unwrap()
619 .into_inner()
620 .songs;
621 assert_eq!(songs.len(), 4);
622 let first_song: mecomp_storage::db::schemas::RecordId = songs[0].id.clone().into();
623 let second_song: mecomp_storage::db::schemas::RecordId = songs[1].id.clone().into();
624 let third_song: mecomp_storage::db::schemas::RecordId = songs[2].id.clone().into();
625 let fourth_song: mecomp_storage::db::schemas::RecordId = songs[3].id.clone().into();
626
627 audio_kernel.send(AudioCommand::Queue(QueueCommand::AddToQueue(
629 songs.into_iter().map(Into::into).collect(),
630 )));
631 assert_eq!(event_rx.recv(), Ok(StateChange::QueueChanged));
632 assert_eq!(
633 event_rx.recv(),
634 Ok(StateChange::TrackChanged(Some(first_song)))
635 );
636 assert_eq!(
637 event_rx.recv(),
638 Ok(StateChange::StatusChanged(Status::Playing))
639 );
640
641 audio_kernel.send(AudioCommand::Queue(QueueCommand::SetPosition(3)));
643 assert_eq!(
644 event_rx.recv(),
645 Ok(StateChange::TrackChanged(Some(fourth_song.clone())))
646 );
647
648 mpris.stop().await.unwrap();
652 assert_eq!(
653 event_rx.recv(),
654 Ok(StateChange::Seeked(Duration::from_secs(0)))
655 );
656 assert_eq!(
657 event_rx.recv(),
658 Ok(StateChange::StatusChanged(Status::Stopped))
659 );
660 mpris.previous().await.unwrap();
662 assert_eq!(
664 event_rx.recv(),
665 Ok(StateChange::TrackChanged(Some(third_song))),
666 );
667 assert!(event_rx.try_recv().is_err());
669
670 mpris.pause().await.unwrap();
672 assert!(matches!(
673 event_rx.recv(),
674 Ok(StateChange::StatusChanged(Status::Paused))
675 ));
676
677 mpris.previous().await.unwrap();
679 assert_eq!(
681 event_rx.recv(),
682 Ok(StateChange::TrackChanged(Some(second_song))),
683 );
684 assert!(event_rx.try_recv().is_err());
686
687 drop(tempdir);
688 }
689
690 #[rstest]
691 #[timeout(Duration::from_secs(20))]
692 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
693 async fn test_prev_no_prev_track(
694 #[future] fixtures: (
695 Mpris,
696 Receiver<StateChange>,
697 TempDir,
698 Arc<AudioKernelSender>,
699 ),
700 ) {
701 init();
702 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
703
704 assert_eq!(mpris.can_go_previous().await.unwrap(), true);
705
706 let songs = mpris
708 .daemon
709 .clone()
710 .library_songs(())
711 .await
712 .unwrap()
713 .into_inner()
714 .songs;
715 assert_eq!(songs.len(), 4);
716
717 audio_kernel.send(AudioCommand::Queue(QueueCommand::AddToQueue(
719 OneOrMany::One(Box::new(songs[0].clone().into())),
720 )));
721 assert_eq!(event_rx.recv(), Ok(StateChange::QueueChanged));
722 let _ = event_rx.recv();
723 let _ = event_rx.recv();
724
725 mpris.previous().await.unwrap();
728 assert_eq!(event_rx.recv(), Ok(StateChange::TrackChanged(None)),);
730 assert!(matches!(
732 event_rx.recv(),
733 Ok(StateChange::StatusChanged(Status::Stopped))
734 ));
735
736 drop(tempdir);
737 }
738
739 #[rstest]
782 #[timeout(Duration::from_secs(20))]
783 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
784 async fn test_play_pause_stop(
785 #[future] fixtures: (
786 Mpris,
787 Receiver<StateChange>,
788 TempDir,
789 Arc<AudioKernelSender>,
790 ),
791 ) {
792 init();
793 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
794
795 assert_eq!(mpris.can_pause().await.unwrap(), true);
796 assert_eq!(mpris.can_play().await.unwrap(), true);
797 assert_eq!(mpris.can_control().await.unwrap(), true);
798
799 let songs = mpris
801 .daemon
802 .clone()
803 .library_songs(())
804 .await
805 .unwrap()
806 .into_inner()
807 .songs;
808 assert_eq!(songs.len(), 4);
809 let first_song = songs[0].clone();
810
811 mpris.play().await.unwrap();
813 let event = event_rx.try_recv();
814 assert!(
815 event.is_err(),
816 "Expected not to receive an event, but got {event:?}"
817 );
818
819 audio_kernel.send(AudioCommand::Queue(QueueCommand::AddToQueue(
821 OneOrMany::One(Box::new(first_song.clone().into())),
822 )));
823 assert_eq!(event_rx.recv(), Ok(StateChange::QueueChanged));
824 assert_eq!(
825 event_rx.recv(),
826 Ok(StateChange::TrackChanged(Some(
827 first_song.id.clone().into()
828 )))
829 );
830 assert_eq!(
831 event_rx.recv(),
832 Ok(StateChange::StatusChanged(Status::Playing))
833 );
834
835 mpris.pause().await.unwrap();
837 assert_eq!(
838 event_rx.recv(),
839 Ok(StateChange::StatusChanged(Status::Paused))
840 );
841
842 mpris.pause().await.unwrap();
844 let event = event_rx.try_recv();
845 assert!(
846 event.is_err(),
847 "Expected not to receive an event, but got {event:?}"
848 );
849
850 mpris.play().await.unwrap();
855 assert_eq!(
856 event_rx.recv(),
857 Ok(StateChange::StatusChanged(Status::Playing))
858 );
859
860 mpris.play().await.unwrap();
862 let event = event_rx.try_recv();
863 assert!(
864 event.is_err(),
865 "Expected not to receive an event, but got {event:?}"
866 );
867
868 mpris.play_pause().await.unwrap();
876 assert_eq!(
877 event_rx.recv(),
878 Ok(StateChange::StatusChanged(Status::Paused))
879 );
880
881 mpris.play_pause().await.unwrap();
883 assert_eq!(
884 event_rx.recv(),
885 Ok(StateChange::StatusChanged(Status::Playing))
886 );
887
888 mpris.stop().await.unwrap();
890 assert_eq!(
891 event_rx.recv(),
892 Ok(StateChange::Seeked(Duration::from_secs(0)))
893 );
894 assert_eq!(
895 event_rx.recv(),
896 Ok(StateChange::StatusChanged(Status::Stopped))
897 );
898 mpris.play_pause().await.unwrap();
899 assert_eq!(
900 event_rx.recv(),
901 Ok(StateChange::StatusChanged(Status::Playing))
902 );
903
904 mpris.stop().await.unwrap();
906 assert_eq!(
907 event_rx.recv(),
908 Ok(StateChange::Seeked(Duration::from_secs(0)))
909 );
910 assert_eq!(
911 event_rx.recv(),
912 Ok(StateChange::StatusChanged(Status::Stopped))
913 );
914
915 mpris.stop().await.unwrap();
917 let event = event_rx.try_recv();
918 assert!(
919 event.is_err(),
920 "Expected not to receive an event, but got {event:?}"
921 );
922
923 drop(tempdir);
927 }
928
929 #[rstest]
945 #[timeout(Duration::from_secs(20))]
946 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
947 async fn test_open_uri(
948 #[future] fixtures: (
949 Mpris,
950 Receiver<StateChange>,
951 TempDir,
952 Arc<AudioKernelSender>,
953 ),
954 ) {
955 init();
956 let (mpris, event_rx, tempdir, _) = fixtures.await;
957
958 let songs = mpris
960 .daemon
961 .clone()
962 .library_songs(())
963 .await
964 .unwrap()
965 .into_inner()
966 .songs;
967 assert_eq!(songs.len(), 4);
968 let first_song = songs[0].clone();
969
970 let file_uri = format!("file://{}", first_song.path);
974 mpris.open_uri(file_uri).await.unwrap();
975 assert_eq!(event_rx.recv(), Ok(StateChange::QueueChanged));
976 assert_eq!(
977 event_rx.recv(),
978 Ok(StateChange::TrackChanged(Some(
979 first_song.id.clone().into()
980 )))
981 );
982 assert_eq!(
984 event_rx.recv(),
985 Ok(StateChange::StatusChanged(Status::Playing))
986 );
987
988 let file_uri = "http://example.com/song.mp3".to_string();
993 let result = mpris.open_uri(file_uri).await;
994 assert!(result.is_err());
995 let file_uri = "file://".to_string();
997 let result = mpris.open_uri(file_uri).await;
998 assert!(result.is_err());
999 let file_uri = format!("file://{}", tempdir.path().display());
1001 let result = mpris.open_uri(file_uri).await;
1002 assert!(result.is_err());
1003 let file_uri = "file:///nonexistent.mp3".to_string();
1005 let result = mpris.open_uri(file_uri).await;
1006 assert!(result.is_err());
1007 std::fs::write(tempdir.path().join("unsupported.txt"), "unsupported")
1009 .expect("Failed to write file");
1010 let file_uri = format!(
1011 "file:///{}",
1012 tempdir.path().join("unsupported.txt").display()
1013 );
1014 let result = mpris.open_uri(file_uri).await;
1015 assert!(result.is_err());
1016
1017 drop(tempdir);
1021 }
1022
1023 #[rstest]
1028 #[timeout(Duration::from_secs(20))]
1029 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1030 async fn test_playback_status(
1031 #[future] fixtures: (
1032 Mpris,
1033 Receiver<StateChange>,
1034 TempDir,
1035 Arc<AudioKernelSender>,
1036 ),
1037 ) {
1038 init();
1039 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
1040
1041 let songs = mpris
1043 .daemon
1044 .clone()
1045 .library_songs(())
1046 .await
1047 .unwrap()
1048 .into_inner()
1049 .songs;
1050 assert_eq!(songs.len(), 4);
1051 let first_song = songs[0].clone();
1052
1053 assert_eq!(
1056 mpris.playback_status().await.unwrap(),
1057 PlaybackStatus::Stopped
1058 );
1059
1060 audio_kernel.send(AudioCommand::Queue(QueueCommand::AddToQueue(
1062 OneOrMany::One(Box::new(first_song.clone().into())),
1063 )));
1064
1065 mpris.pause().await.unwrap();
1067
1068 let mut events = [false; 4];
1070 for _ in 0..4 {
1071 let event = event_rx.recv().unwrap();
1072
1073 match event {
1074 StateChange::QueueChanged => {
1075 events[0] = true;
1076 }
1077 StateChange::TrackChanged(Some(_)) => {
1078 mpris.state.write().await.current_song = Some(first_song.clone().into());
1079 events[1] = true;
1080 }
1081 StateChange::StatusChanged(Status::Playing) => {
1082 mpris.state.write().await.status = Status::Playing;
1083 assert_eq!(
1084 mpris.playback_status().await.unwrap(),
1085 PlaybackStatus::Playing
1086 );
1087 events[2] = true;
1088 }
1089 StateChange::StatusChanged(Status::Paused) => {
1090 mpris.state.write().await.status = Status::Paused;
1091 assert_eq!(
1092 mpris.playback_status().await.unwrap(),
1093 PlaybackStatus::Paused
1094 );
1095 events[3] = true;
1096 }
1097 _ => panic!("Unexpected event: {event:?}"),
1098 }
1099 }
1100
1101 assert!(events.iter().all(|&e| e));
1102
1103 drop(tempdir);
1104 }
1105
1106 #[rstest]
1118 #[timeout(Duration::from_secs(20))]
1119 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1120 async fn test_loop_status(
1121 #[future] fixtures: (
1122 Mpris,
1123 Receiver<StateChange>,
1124 TempDir,
1125 Arc<AudioKernelSender>,
1126 ),
1127 ) {
1128 init();
1129 let (mpris, event_rx, _, _) = fixtures.await;
1130
1131 assert_eq!(mpris.loop_status().await.unwrap(), LoopStatus::None);
1134
1135 mpris.set_loop_status(LoopStatus::Track).await.unwrap();
1137 if event_rx.recv() == Ok(StateChange::RepeatModeChanged(RepeatMode::One)) {
1138 mpris.state.write().await.repeat_mode = RepeatMode::One;
1139 } else {
1140 panic!("Expected a RepeatModeChanged event, but got something else");
1141 }
1142 assert_eq!(mpris.loop_status().await.unwrap(), LoopStatus::Track);
1143
1144 mpris.set_loop_status(LoopStatus::Playlist).await.unwrap();
1146 if event_rx.recv() == Ok(StateChange::RepeatModeChanged(RepeatMode::All)) {
1147 mpris.state.write().await.repeat_mode = RepeatMode::All;
1148 } else {
1149 panic!("Expected a RepeatModeChanged event, but got something else");
1150 }
1151 assert_eq!(mpris.loop_status().await.unwrap(), LoopStatus::Playlist);
1152
1153 mpris.set_loop_status(LoopStatus::None).await.unwrap();
1155 if event_rx.recv() == Ok(StateChange::RepeatModeChanged(RepeatMode::None)) {
1156 mpris.state.write().await.repeat_mode = RepeatMode::None;
1157 } else {
1158 panic!("Expected a RepeatModeChanged event, but got something else");
1159 }
1160 assert_eq!(mpris.loop_status().await.unwrap(), LoopStatus::None);
1161 }
1162
1163 #[rstest]
1179 #[timeout(Duration::from_secs(20))]
1180 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1181 async fn test_rate(
1182 #[future] fixtures: (
1183 Mpris,
1184 Receiver<StateChange>,
1185 TempDir,
1186 Arc<AudioKernelSender>,
1187 ),
1188 ) {
1189 init();
1190 let (mpris, event_rx, _, _) = fixtures.await;
1191
1192 let rate = mpris.rate().await.unwrap();
1194 assert!(f64::EPSILON > (rate - 1.0).abs(), "{rate} != 1.0");
1195
1196 let min_rate = mpris.minimum_rate().await.unwrap();
1198 assert!(f64::EPSILON > (min_rate - 1.0).abs(), "{min_rate} != 1.0");
1199
1200 let max_rate = mpris.maximum_rate().await.unwrap();
1202 assert!(f64::EPSILON > (max_rate - 1.0).abs(), "{max_rate} != 1.0");
1203
1204 let result = mpris.set_rate(1.0).await;
1207 assert!(result.is_ok());
1208 assert!(event_rx.try_recv().is_err());
1209 }
1210
1211 #[rstest]
1227 #[timeout(Duration::from_secs(20))]
1228 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1229 async fn test_shuffle(
1230 #[future] fixtures: (
1231 Mpris,
1232 Receiver<StateChange>,
1233 TempDir,
1234 Arc<AudioKernelSender>,
1235 ),
1236 ) {
1237 init();
1238 let (mpris, event_rx, _, _) = fixtures.await;
1239
1240 assert_eq!(mpris.shuffle().await.unwrap(), true);
1242
1243 mpris.set_shuffle(true).await.unwrap();
1246 assert_eq!(mpris.shuffle().await.unwrap(), true);
1247 assert_eq!(event_rx.recv(), Ok(StateChange::QueueChanged));
1248
1249 mpris.set_shuffle(false).await.unwrap();
1251 assert_eq!(mpris.shuffle().await.unwrap(), true);
1252 assert!(event_rx.recv_timeout(Duration::from_millis(100)).is_err());
1253 }
1254
1255 #[rstest]
1263 #[timeout(Duration::from_secs(20))]
1264 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1265 async fn test_metadata(
1266 #[future] fixtures: (
1267 Mpris,
1268 Receiver<StateChange>,
1269 TempDir,
1270 Arc<AudioKernelSender>,
1271 ),
1272 ) {
1273 init();
1274 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
1275
1276 let songs = mpris
1278 .daemon
1279 .clone()
1280 .library_songs(())
1281 .await
1282 .unwrap()
1283 .into_inner()
1284 .songs;
1285 assert_eq!(songs.len(), 4);
1286 let first_song: SongBrief = songs[0].clone().into();
1287
1288 assert_eq!(
1291 mpris.metadata().await.unwrap(),
1292 Metadata::builder().trackid(TrackId::NO_TRACK).build()
1293 );
1294
1295 audio_kernel.send(AudioCommand::Queue(QueueCommand::AddToQueue(
1297 first_song.clone().into(),
1298 )));
1299
1300 assert_eq!(event_rx.recv(), Ok(StateChange::QueueChanged));
1301 assert_eq!(
1302 event_rx.recv(),
1303 Ok(StateChange::TrackChanged(Some(
1304 first_song.id.clone().into()
1305 )))
1306 );
1307
1308 *mpris.state.write().await = mpris
1309 .daemon
1310 .clone()
1311 .state_audio(())
1312 .await
1313 .unwrap()
1314 .into_inner()
1315 .state
1316 .unwrap()
1317 .into();
1318
1319 let metadata = mpris.metadata().await.unwrap();
1321 assert_eq!(metadata, metadata_from_opt_song(Some(&first_song)));
1322 assert_ne!(
1323 metadata,
1324 Metadata::builder().trackid(TrackId::NO_TRACK).build()
1325 );
1326
1327 drop(tempdir);
1328 }
1329
1330 #[rstest]
1335 #[timeout(Duration::from_secs(20))]
1336 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1337 async fn test_volume(
1338 #[future] fixtures: (
1339 Mpris,
1340 Receiver<StateChange>,
1341 TempDir,
1342 Arc<AudioKernelSender>,
1343 ),
1344 ) {
1345 let (mpris, event_rx, _, _) = fixtures.await;
1346
1347 let volume = mpris.volume().await.unwrap();
1349 assert!(f64::EPSILON > (volume - 1.0).abs(), "{volume} != 1.0");
1350
1351 mpris.set_volume(-1.0).await.unwrap();
1353 if event_rx.recv() == Ok(StateChange::VolumeChanged(0.0)) {
1354 mpris.state.write().await.volume = 0.0;
1355 let volume = mpris.volume().await.unwrap();
1356 assert!(f64::EPSILON > volume.abs(), "{volume} != 0.0");
1357 } else {
1358 panic!("Expected a VolumeChanged event, but got something else");
1359 }
1360
1361 mpris.set_volume(1.0).await.unwrap();
1363 if event_rx.recv() == Ok(StateChange::VolumeChanged(1.0)) {
1364 mpris.state.write().await.volume = 1.0;
1365 let volume = mpris.volume().await.unwrap();
1366 assert!(f64::EPSILON > (volume - 1.0).abs(), "{volume} != 1.0");
1367 } else {
1368 panic!("Expected a VolumeChanged event, but got something else");
1369 }
1370
1371 mpris.set_volume(1.0).await.unwrap();
1373 assert!(event_rx.try_recv().is_err());
1374 }
1375
1376 #[rstest]
1389 #[timeout(Duration::from_secs(20))]
1390 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1391 async fn test_position(
1392 #[future] fixtures: (
1393 Mpris,
1394 Receiver<StateChange>,
1395 TempDir,
1396 Arc<AudioKernelSender>,
1397 ),
1398 ) {
1399 init();
1400 let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
1401
1402 assert!(mpris.can_seek().await.unwrap());
1403
1404 let songs = mpris
1406 .daemon
1407 .clone()
1408 .library_songs(())
1409 .await
1410 .unwrap()
1411 .into_inner()
1412 .songs;
1413 assert_eq!(songs.len(), 4);
1414 let first_song: SongBrief = songs[0].clone().into();
1415
1416 assert_eq!(mpris.position().await.unwrap(), Time::from_micros(0));
1419
1420 audio_kernel.send(AudioCommand::Queue(QueueCommand::AddToQueue(
1422 first_song.clone().into(),
1423 )));
1424 audio_kernel.send(AudioCommand::Pause);
1425 let _ = event_rx.recv().unwrap();
1426 let _ = event_rx.recv().unwrap();
1427 let _ = event_rx.recv().unwrap();
1428 let _ = event_rx.recv().unwrap();
1429 *mpris.state.write().await = mpris
1431 .daemon
1432 .clone()
1433 .state_audio(())
1434 .await
1435 .unwrap()
1436 .into_inner()
1437 .state
1438 .unwrap()
1439 .into();
1440
1441 let first_song_track_id = mpris.metadata().await.unwrap().trackid().unwrap();
1442
1443 mpris
1445 .set_position(first_song_track_id.clone(), Time::from_secs(2))
1446 .await
1447 .unwrap();
1448 assert_eq!(
1449 event_rx.recv(),
1450 Ok(StateChange::Seeked(Duration::from_secs(2)))
1451 );
1452
1453 mpris
1455 .set_position(first_song_track_id.clone(), Time::from_secs(-1))
1456 .await
1457 .unwrap();
1458 assert!(event_rx.try_recv().is_err());
1459
1460 mpris
1462 .set_position(first_song_track_id.clone(), Time::from_secs(100))
1463 .await
1464 .unwrap();
1465 assert!(event_rx.try_recv().is_err());
1466
1467 assert!(mpris.can_seek().await.unwrap());
1470
1471 mpris.seek(Time::from_secs(1)).await.unwrap();
1472 assert_eq!(
1473 event_rx.recv(),
1474 Ok(StateChange::Seeked(Duration::from_secs(3)))
1475 );
1476
1477 mpris.seek(Time::from_secs(-1)).await.unwrap();
1478 assert_eq!(
1479 event_rx.recv(),
1480 Ok(StateChange::Seeked(Duration::from_secs(2)))
1481 );
1482
1483 drop(tempdir);
1484 }
1485}