mecomp_mpris/interfaces/
player.rs

1//! Implements the player interface of the MPRIS specification.
2//!
3//! [org.mpris.MediaPlayer2.Player](https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html)
4
5use 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        // the uri should be in the format:
98        // file:///path/to/file
99        // TODO: Support loading a playlist file when we've implemented importing/exporting external playlists
100        log::info!("Opening URI: {uri}");
101
102        // ensure the URI is a supported URI
103        if !uri.starts_with("file://") {
104            return Err(fdo::Error::InvalidArgs(
105                "Only file:// URIs are supported".to_string(),
106            ));
107        }
108
109        // extract the path from the URI, and ensure it's a valid file path
110        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        // parse out escaped characters (e.g. %20 for space)
115        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        // ensure the file type is supported
122        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        // expand the tilde if present
133        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        // ensure the path exists
142        if !path.exists() {
143            return Err(fdo::Error::InvalidArgs("File does not exist".to_string()));
144        }
145
146        // ensure the path is a file
147        if !path.is_file() {
148            return Err(fdo::Error::InvalidArgs("Path is not a file".to_string()));
149        }
150
151        // canonicalize the path
152        path = path.canonicalize().unwrap_or(path);
153
154        // add the song to the queue
155        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        // Mecomp has no distinction between shuffle and non-shuffle playback
229        // shuffling is done by actually shuffling the queue and is not reversible
230        // therefore, we always return true
231        Ok(true)
232    }
233
234    async fn set_shuffle(&self, shuffle: bool) -> Result<(), ZbusError> {
235        // if called with false, does nothing, if called with true, shuffles the queue
236        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        //TODO: if the value passed in would mean seeking beyond the end of the track, act like a call to Next
293        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        // "track_id - The currently playing track's identifier. If this does not match the id of the currently-playing track, the call is ignored as 'stale'"
315        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                // if the position not in the range of the song, ignore the call
324                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// NOTE: these tests do not run the event `Subscriber` main loop so events from the audio kernel are not
381// actually going to be applied to the state, so we have to manually set the state when testing methods
382// that simply report the state.
383#[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    /// """
402    /// Skips to the next track in the tracklist.
403    /// If there is no next track (and endless playback and track repeat are both off), stop playback.
404    /// If playback is paused or stopped, it remains that way.
405    /// If [CanGoNext] is false, attempting to call this method should have no effect.
406    /// """
407    ///
408    /// Mecomp supports skipping to the next track in the queue.
409    ///
410    /// the last case is irrelevant here, as we always return true for [CanGoNext]
411    #[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        // setup
428        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        // send all the songs to the audio kernel (adding them to the queue and starting playback)
442        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        // it skips to the next track //
455        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        // the current song should be different from the previous song
462        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        // setup
484        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        // send all the songs to the audio kernel (adding them to the queue and starting playback)
498        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        // if playback is paused or stopped, it remains that way //
510
511        // stop playback
512        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        // skip to the next track
523        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        // playback should remain stopped
528        assert!(event_rx.try_recv().is_err());
529
530        // the current song should be different from the previous song
531        assert_ne!(first_song, second_song);
532
533        // pause playback
534        mpris.pause().await.unwrap();
535        assert_eq!(
536            event_rx.recv(),
537            Ok(StateChange::StatusChanged(Status::Paused))
538        );
539
540        // skip to the next track
541        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        // playback should remain paused
546        assert!(event_rx.try_recv().is_err());
547
548        // the current song should be different from the previous song
549        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        // setup
571        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        // send one song to the audio kernel (adding them to the queue and starting playback)
585        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        // if there is no next track (and endless playback and track repeat are both off), stop playback. //
594        // skip to the next track (which should be nothing)
595        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    /// """
605    /// Skips to the previous track in the tracklist.
606    /// If there is no previous track (and endless playback and track repeat are both off), stop playback.
607    /// If playback is paused or stopped, it remains that way.
608    /// If [CanGoPrevious] is false, attempting to call this method should have no effect.
609    /// """
610    ///
611    /// Mecomp supports skipping to the previous track in the queue.
612    ///
613    /// the last case is irrelevant here, as we always return true for [CanGoPrevious]
614    #[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        // setup
631        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        // send all the songs to the audio kernel (adding them to the queue and starting playback)
648        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        // skip to the last song in the queue
655        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        // it skips to the previous track //
664        mpris.previous().await.unwrap();
665        // should go back to the fourth song
666        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        // setup
690        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        // send all the songs to the audio kernel (adding them to the queue and starting playback)
708        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        // skip to the last song in the queue
715        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        // if playback is paused or stopped, it remains that way //
724
725        // stop playback
726        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        // skip to the previous track
736        mpris.previous().await.unwrap();
737        // should go back to the third
738        assert_eq!(
739            event_rx.recv(),
740            Ok(StateChange::TrackChanged(Some(third_song))),
741        );
742        // playback should remain stopped
743        assert!(event_rx.try_recv().is_err());
744
745        // pause playback
746        mpris.pause().await.unwrap();
747        assert!(matches!(
748            event_rx.recv(),
749            Ok(StateChange::StatusChanged(Status::Paused))
750        ));
751
752        // skip to the previous track
753        mpris.previous().await.unwrap();
754        // should go back to the second song
755        assert_eq!(
756            event_rx.recv(),
757            Ok(StateChange::TrackChanged(Some(second_song))),
758        );
759        // playback should remain paused
760        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        // setup
782        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        // send all the songs to the audio kernel (adding them to the queue and starting playback)
797        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        // if there is no previous track (and endless playback and track repeat are both off), stop playback. //
806        // skip to the previous track
807        mpris.previous().await.unwrap();
808        // should go back to nothing
809        assert_eq!(event_rx.recv(), Ok(StateChange::TrackChanged(None)),);
810        // playback should be stopped
811        assert!(matches!(
812            event_rx.recv(),
813            Ok(StateChange::StatusChanged(Status::Stopped))
814        ));
815
816        drop(tempdir);
817    }
818
819    /// """
820    /// Pauses playback.
821    /// If playback is already paused, this has no effect.
822    /// Calling [Play] after this should cause playback to start again from the same position.
823    /// If [CanPause] is false, attempting to call this method should have no effect.
824    /// """
825    ///
826    /// Mecomp supports pausing playback.
827    ///
828    /// the last case is irrelevant here, as we always return true for [CanPause]
829    ///
830    /// """
831    /// Starts or resumes playback.
832    /// If already playing, this has no effect.
833    /// If paused, playback resumes from the current position.
834    /// If there is no track to play, this has no effect.
835    /// If [CanPlay] is false, attempting to call this method should have no effect.
836    /// """
837    ///
838    /// Mecomp supports starting or resuming playback.
839    ///
840    /// the last case is irrelevant here, as we always return true for [CanPlay]
841    ///
842    /// """
843    /// Pauses playback.
844    /// If playback is already paused, resumes playback.
845    /// If playback is stopped, starts playback.
846    /// If [CanPause] is false, attempting to call this method should have no effect and raise an error.
847    /// """
848    ///
849    /// Mecomp supports toggling between playing and pausing playback.
850    ///
851    /// """
852    /// Stops playback.
853    /// If playback is already stopped, this has no effect.
854    /// Calling Play after this should cause playback to start again from the beginning of the track.
855    /// If [CanControl] is false, attempting to call this method should have no effect and raise an error.
856    /// """
857    ///
858    /// Mecomp supports stopping playback.
859    ///
860    /// the last case is irrelevant here, as we always return true for [CanControl]
861    #[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        // setup
880        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        // Play: if there is no track to play, this has no effect //
896        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        // send all the songs to the audio kernel (adding them to the queue and starting playback)
904        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        // Pause: pauses playback //
922        mpris.pause().await.unwrap();
923        assert_eq!(
924            event_rx.recv(),
925            Ok(StateChange::StatusChanged(Status::Paused))
926        );
927
928        // Pause: if playback is already paused, this has no effect //
929        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        // Pause: calling [Play] after this should cause playback to start again from the same position. //
937        // not easily testable
938
939        // Play: Starts or resumes playback. //
940        mpris.play().await.unwrap();
941        assert_eq!(
942            event_rx.recv(),
943            Ok(StateChange::StatusChanged(Status::Playing))
944        );
945
946        // Play if already playing, this has no effect //
947        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        // Play: If paused, playback resumes from the current position. //
955        // not easily testable
956
957        // Play: If there is no track to play, this has no effect. //
958        // tested above before sending the songs to the audio kernel
959
960        // Play-Pause: Pauses playback. //
961        mpris.play_pause().await.unwrap();
962        assert_eq!(
963            event_rx.recv(),
964            Ok(StateChange::StatusChanged(Status::Paused))
965        );
966
967        // Play-Pause: If playback is already paused, resumes playback. //
968        mpris.play_pause().await.unwrap();
969        assert_eq!(
970            event_rx.recv(),
971            Ok(StateChange::StatusChanged(Status::Playing))
972        );
973
974        // Play-Pause: If playback is stopped, starts playback. //
975        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        // Stop: Stops playback. //
991        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        // Stop: If playback is already stopped, this has no effect. //
1002        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        // Stop: Calling Play after this should cause playback to start again from the beginning of the track. //
1010        // not easily testable
1011
1012        drop(tempdir);
1013    }
1014
1015    /// """
1016    /// Opens the uri given as an argument
1017    /// If the playback is stopped, starts playing
1018    /// If the uri scheme or the mime-type of the uri to open is not supported,
1019    ///  this method does nothing and may raise an error.
1020    ///  In particular, if the list of available uri schemes is empty,
1021    ///  this method may not be implemented.
1022    /// If the media player implements the [TrackList interface], then the opened track should be made part of the tracklist,
1023    ///  the [TrackAdded] or [TrackListReplaced] signal should be fired, as well as the
1024    ///  org.freedesktop.DBus.Properties.PropertiesChanged signal on the [TrackList interface]
1025    /// """
1026    ///
1027    /// Mecomp supports opening file URIs, and returns errors for unsupported or invalid URIs.
1028    ///
1029    /// Mecomp does not currently implement the [TrackList interface], so the last case is irrelevant here.
1030    #[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        // setup
1045        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        // Opens the uri given as an argument //
1061
1062        // open a valid file uri
1063        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        // If the playback is stopped, starts playing //
1072        assert_eq!(
1073            event_rx.recv(),
1074            Ok(StateChange::StatusChanged(Status::Playing))
1075        );
1076
1077        // If the uri scheme or the mime-type of the uri to open is not supported, this method does nothing and may raise an error. //
1078        // In particular, if the list of available uri schemes is empty, this method may not be implemented. //
1079
1080        // open a uri with an unsupported scheme
1081        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        // open a file uri with an invalid path (is not a file path)
1085        let file_uri = "file://".to_string();
1086        let result = mpris.open_uri(file_uri).await;
1087        assert!(result.is_err());
1088        // open a file uri with an invalid path (a directory)
1089        let file_uri = format!("file://{}", tempdir.path().display());
1090        let result = mpris.open_uri(file_uri).await;
1091        assert!(result.is_err());
1092        // open a file uri that doesn't exist
1093        let file_uri = "file:///nonexistent.mp3".to_string();
1094        let result = mpris.open_uri(file_uri).await;
1095        assert!(result.is_err());
1096        // open a file uri with an unsupported mime type
1097        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        // If the media player implements the [TrackList interface], then the opened track should be made part of the tracklist, the [TrackAdded] or [TrackListReplaced] signal should be fired, as well as the org.freedesktop.DBus.Properties.PropertiesChanged signal on the [TrackList interface] //
1107        // TODO: test this when we implement the [TrackList interface]
1108
1109        drop(tempdir);
1110    }
1111
1112    /// """
1113    /// Returns the playback status.
1114    /// """
1115    /// Mecomp supports returning the playback status.
1116    #[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        // setup
1131        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        // Returns the playback status. //
1147        // playback is stopped
1148        assert_eq!(
1149            mpris.playback_status().await.unwrap(),
1150            PlaybackStatus::Stopped
1151        );
1152
1153        // send all the songs to the audio kernel (adding them to the queue and starting playback)
1154        audio_kernel.send(AudioCommand::Queue(
1155            mecomp_core::audio::commands::QueueCommand::AddToQueue(Box::new(
1156                first_song.clone().into(),
1157            )),
1158        ));
1159
1160        // pause playback
1161        mpris.pause().await.unwrap();
1162
1163        // we expect there to be 3 events
1164        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    /// """
1199    /// Returns the loop status.
1200    /// """
1201    ///
1202    /// Mecomp supports returning the loop status.
1203    ///
1204    /// """
1205    /// Sets the loop status.
1206    /// """
1207    ///
1208    /// Mecomp supports setting the loop status.
1209    #[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        // Returns the loop status. //
1224        // loop status is none
1225        assert_eq!(mpris.loop_status().await.unwrap(), LoopStatus::None);
1226
1227        // set loop status to track
1228        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        // set loop status to playlist
1237        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        // set loop status to none
1246        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    /// """
1256    /// Returns the rate.
1257    /// """
1258    /// """
1259    /// The minimum value which the [Rate] property can take. Clients should not attempt to set the [Rate] property below this value.
1260    /// """
1261    /// """
1262    /// """
1263    /// The maximum value which the [Rate] property can take. Clients should not attempt to set the [Rate] property above this value.
1264    /// """
1265    /// """
1266    /// Sets the playback rate.
1267    /// """
1268    ///
1269    /// Mecomp supports returning the playback rate, but does not support changing it.
1270    #[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        // Returns the playback rate. //
1285        let rate = mpris.rate().await.unwrap();
1286        assert!(f64::EPSILON > (rate - 1.0).abs(), "{rate} != 1.0");
1287
1288        // The minimum value which the [Rate] property can take. Clients should not attempt to set the [Rate] property below this value. //
1289        let min_rate = mpris.minimum_rate().await.unwrap();
1290        assert!(f64::EPSILON > (min_rate - 1.0).abs(), "{min_rate} != 1.0");
1291
1292        // The maximum value which the [Rate] property can take. Clients should not attempt to set the [Rate] property above this value. //
1293        let max_rate = mpris.maximum_rate().await.unwrap();
1294        assert!(f64::EPSILON > (max_rate - 1.0).abs(), "{max_rate} != 1.0");
1295
1296        // Sets the playback rate. //
1297        // not supported, but the spec doesn't specify that an error should be reported so we just return Ok
1298        let result = mpris.set_rate(1.0).await;
1299        assert!(result.is_ok());
1300        assert!(event_rx.try_recv().is_err());
1301    }
1302
1303    /// """
1304    /// Returns whether playback is shuffled.
1305    /// """
1306    ///
1307    /// Mecomp supports returning whether playback is shuffled.
1308    ///
1309    /// """
1310    /// Sets whether playback is shuffled.
1311    /// """
1312    ///
1313    /// Mecomp supports setting whether playback is shuffled.
1314    ///
1315    /// NOTE: Mecomp does not actually implement this properly,
1316    /// as setting shuffle to false will not restore the original order of the queue
1317    /// and is instead a no-op.
1318    #[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        // Returns whether playback is shuffled. //
1333        assert_eq!(mpris.shuffle().await.unwrap(), true);
1334
1335        // Sets whether playback is shuffled. //
1336        // set shuffle to true
1337        mpris.set_shuffle(true).await.unwrap();
1338        assert_eq!(mpris.shuffle().await.unwrap(), true);
1339        assert!(event_rx.try_recv().is_err());
1340
1341        // set shuffle to false
1342        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    /// """
1348    /// The metadata of the current element.
1349    /// When this property changes, the org.freedesktop.DBus.Properties.PropertiesChanged
1350    ///     signal via [properties_changed] must be emitted with the new value.
1351    /// If there is a current track, this must have a [mpris:trackid] entry at the very least,
1352    ///     which contains a D-Bus path that uniquely identifies this track.
1353    /// """
1354    #[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        // setup
1369        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        // The metadata of the current element. //
1385        // when there is no current song
1386        assert_eq!(
1387            mpris.metadata().await.unwrap(),
1388            Metadata::builder().trackid(TrackId::NO_TRACK).build()
1389        );
1390
1391        // send all the songs to the audio kernel (adding them to the queue and starting playback)
1392        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        // when there is a current song
1417        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    /// """
1428    /// The volume level.
1429    /// When setting, if a negative value is passed, the volume should be set to 0.0.
1430    /// """
1431    #[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        // The volume level. //
1445        let volume = mpris.volume().await.unwrap();
1446        assert!(f64::EPSILON > (volume - 1.0).abs(), "{volume} != 1.0");
1447
1448        // When setting, if a negative value is passed, the volume should be set to 0.0. //
1449        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        // set the volume back to 1.0
1459        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        // set the volume to the same value
1469        mpris.set_volume(1.0).await.unwrap();
1470        assert!(event_rx.try_recv().is_err());
1471    }
1472
1473    /// """
1474    /// The current track position, between 0 and the [mpris:length] metadata entry.
1475    /// If the media player allows it, the current playback position can be changed either the [SetPosition] method or the [Seek] method on this interface.
1476    /// If this is not the case, the [CanSeek] property is false, and setting this property has no effect and can raise an error.
1477    /// """
1478    ///
1479    /// for set_position:
1480    /// """
1481    /// If the Position argument is less than 0, do nothing.
1482    /// If the Position argument is greater than the track length, do nothing.
1483    /// If the given `track_id` this does not match the id of the currently-playing track, the call is ignored as "stale"
1484    /// """
1485    #[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        // setup
1502        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        // The current track position, between 0 and the [mpris:length] metadata entry. //
1518        // when there is no current song
1519        assert_eq!(mpris.position().await.unwrap(), Time::from_micros(0));
1520
1521        // send all the songs to the audio kernel (adding them to the queue and starting playback)
1522        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        // update internal state
1532        *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        // normal:
1546        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        // If the Position argument is less than 0, do nothing. //
1556        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        // If the Position argument is greater than the track length, do nothing. //
1563        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        // If the media player allows it, the current playback position can be changed either the [SetPosition] method or the [Seek] method on this interface. //
1570        // If this is not the case, the [CanSeek] property is false, and setting this property has no effect and can raise an error. //
1571        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}