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    zbus::{fdo, Error as ZbusError},
10    LoopStatus, Metadata, PlaybackRate, PlaybackStatus, PlayerInterface, Time, TrackId, Volume,
11};
12use tarpc::context::Context;
13
14use crate::{interfaces::root::SUPPORTED_MIME_TYPES, metadata_from_opt_song, Mpris};
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('/').last())
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::{mpsc::Receiver, Arc};
386
387    use mecomp_core::{
388        audio::{commands::AudioCommand, AudioKernelSender},
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::Thing = songs[2].id.clone().into();
645        let fourth_song: mecomp_storage::db::schemas::Thing = 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::Thing = songs[1].id.clone().into();
704        let third_song: mecomp_storage::db::schemas::Thing = songs[2].id.clone().into();
705        let fourth_song: mecomp_storage::db::schemas::Thing = 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        assert_eq!(mpris.rate().await.unwrap(), 1.0);
1286
1287        // The minimum value which the [Rate] property can take. Clients should not attempt to set the [Rate] property below this value. //
1288        assert_eq!(mpris.minimum_rate().await.unwrap(), 1.0);
1289
1290        // The maximum value which the [Rate] property can take. Clients should not attempt to set the [Rate] property above this value. //
1291        assert_eq!(mpris.maximum_rate().await.unwrap(), 1.0);
1292
1293        // Sets the playback rate. //
1294        // not supported, but the spec doesn't specify that an error should be reported so we just return Ok
1295        let result = mpris.set_rate(1.0).await;
1296        assert!(result.is_ok());
1297        assert!(event_rx.try_recv().is_err());
1298    }
1299
1300    /// """
1301    /// Returns whether playback is shuffled.
1302    /// """
1303    ///
1304    /// Mecomp supports returning whether playback is shuffled.
1305    ///
1306    /// """
1307    /// Sets whether playback is shuffled.
1308    /// """
1309    ///
1310    /// Mecomp supports setting whether playback is shuffled.
1311    ///
1312    /// NOTE: Mecomp does not actually implement this properly,
1313    /// as setting shuffle to false will not restore the original order of the queue
1314    /// and is instead a no-op.
1315    #[rstest]
1316    #[timeout(Duration::from_secs(10))]
1317    #[tokio::test]
1318    async fn test_shuffle(
1319        #[future] fixtures: (
1320            Mpris,
1321            Receiver<StateChange>,
1322            TempDir,
1323            Arc<AudioKernelSender>,
1324        ),
1325    ) {
1326        init();
1327        let (mpris, event_rx, _, _) = fixtures.await;
1328
1329        // Returns whether playback is shuffled. //
1330        assert_eq!(mpris.shuffle().await.unwrap(), true);
1331
1332        // Sets whether playback is shuffled. //
1333        // set shuffle to true
1334        mpris.set_shuffle(true).await.unwrap();
1335        assert_eq!(mpris.shuffle().await.unwrap(), true);
1336        assert!(event_rx.try_recv().is_err());
1337
1338        // set shuffle to false
1339        mpris.set_shuffle(false).await.unwrap();
1340        assert_eq!(mpris.shuffle().await.unwrap(), true);
1341        assert!(event_rx.try_recv().is_err());
1342    }
1343
1344    /// """
1345    /// The metadata of the current element.
1346    /// When this property changes, the org.freedesktop.DBus.Properties.PropertiesChanged
1347    ///     signal via [properties_changed] must be emitted with the new value.
1348    /// If there is a current track, this must have a [mpris:trackid] entry at the very least,
1349    ///     which contains a D-Bus path that uniquely identifies this track.
1350    /// """
1351    #[rstest]
1352    #[timeout(Duration::from_secs(10))]
1353    #[tokio::test]
1354    async fn test_metadata(
1355        #[future] fixtures: (
1356            Mpris,
1357            Receiver<StateChange>,
1358            TempDir,
1359            Arc<AudioKernelSender>,
1360        ),
1361    ) {
1362        init();
1363        let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
1364
1365        // setup
1366        let context = Context::current();
1367        let songs: Vec<Song> = mpris
1368            .daemon
1369            .read()
1370            .await
1371            .as_ref()
1372            .unwrap()
1373            .library_songs_full(context)
1374            .await
1375            .unwrap()
1376            .unwrap()
1377            .to_vec();
1378        assert_eq!(songs.len(), 4);
1379        let first_song = songs[0].clone();
1380
1381        // The metadata of the current element. //
1382        // when there is no current song
1383        assert_eq!(
1384            mpris.metadata().await.unwrap(),
1385            Metadata::builder().trackid(TrackId::NO_TRACK).build()
1386        );
1387
1388        // send all the songs to the audio kernel (adding them to the queue and starting playback)
1389        audio_kernel.send(AudioCommand::Queue(
1390            mecomp_core::audio::commands::QueueCommand::AddToQueue(Box::new(
1391                first_song.clone().into(),
1392            )),
1393        ));
1394
1395        assert_eq!(
1396            event_rx.recv(),
1397            Ok(StateChange::TrackChanged(Some(
1398                first_song.id.clone().into()
1399            )))
1400        );
1401
1402        *mpris.state.write().await = mpris
1403            .daemon
1404            .read()
1405            .await
1406            .as_ref()
1407            .unwrap()
1408            .state_audio(Context::current())
1409            .await
1410            .unwrap()
1411            .unwrap();
1412
1413        // when there is a current song
1414        let metadata = mpris.metadata().await.unwrap();
1415        assert_eq!(metadata, metadata_from_opt_song(Some(&first_song)));
1416        assert_ne!(
1417            metadata,
1418            Metadata::builder().trackid(TrackId::NO_TRACK).build()
1419        );
1420
1421        drop(tempdir);
1422    }
1423
1424    /// """
1425    /// The volume level.
1426    /// When setting, if a negative value is passed, the volume should be set to 0.0.
1427    /// """
1428    #[rstest]
1429    #[timeout(Duration::from_secs(10))]
1430    #[tokio::test]
1431    async fn test_volume(
1432        #[future] fixtures: (
1433            Mpris,
1434            Receiver<StateChange>,
1435            TempDir,
1436            Arc<AudioKernelSender>,
1437        ),
1438    ) {
1439        let (mpris, event_rx, _, _) = fixtures.await;
1440
1441        // The volume level. //
1442        assert_eq!(mpris.volume().await.unwrap(), 1.0);
1443
1444        // When setting, if a negative value is passed, the volume should be set to 0.0. //
1445        mpris.set_volume(-1.0).await.unwrap();
1446        if event_rx.recv() == Ok(StateChange::VolumeChanged(0.0)) {
1447            mpris.state.write().await.volume = 0.0;
1448            assert_eq!(mpris.volume().await.unwrap(), 0.0);
1449        } else {
1450            panic!("Expected a VolumeChanged event, but got something else");
1451        }
1452
1453        // set the volume back to 1.0
1454        mpris.set_volume(1.0).await.unwrap();
1455        if event_rx.recv() == Ok(StateChange::VolumeChanged(1.0)) {
1456            mpris.state.write().await.volume = 1.0;
1457            assert_eq!(mpris.volume().await.unwrap(), 1.0);
1458        } else {
1459            panic!("Expected a VolumeChanged event, but got something else");
1460        }
1461
1462        // set the volume to the same value
1463        mpris.set_volume(1.0).await.unwrap();
1464        assert!(event_rx.try_recv().is_err());
1465    }
1466
1467    /// """
1468    /// The current track position, between 0 and the [mpris:length] metadata entry.
1469    /// If the media player allows it, the current playback position can be changed either the [SetPosition] method or the [Seek] method on this interface.
1470    /// If this is not the case, the [CanSeek] property is false, and setting this property has no effect and can raise an error.
1471    /// """
1472    ///
1473    /// for set_position:
1474    /// """
1475    /// If the Position argument is less than 0, do nothing.
1476    /// If the Position argument is greater than the track length, do nothing.
1477    /// If the given `track_id` this does not match the id of the currently-playing track, the call is ignored as "stale"
1478    /// """
1479    #[rstest]
1480    #[timeout(Duration::from_secs(10))]
1481    #[tokio::test]
1482    async fn test_position(
1483        #[future] fixtures: (
1484            Mpris,
1485            Receiver<StateChange>,
1486            TempDir,
1487            Arc<AudioKernelSender>,
1488        ),
1489    ) {
1490        init();
1491        let (mpris, event_rx, tempdir, audio_kernel) = fixtures.await;
1492
1493        assert!(mpris.can_seek().await.unwrap());
1494
1495        // setup
1496        let context = Context::current();
1497        let songs: Vec<Song> = mpris
1498            .daemon
1499            .read()
1500            .await
1501            .as_ref()
1502            .unwrap()
1503            .library_songs_full(context)
1504            .await
1505            .unwrap()
1506            .unwrap()
1507            .to_vec();
1508        assert_eq!(songs.len(), 4);
1509        let first_song = songs[0].clone();
1510
1511        // The current track position, between 0 and the [mpris:length] metadata entry. //
1512        // when there is no current song
1513        assert_eq!(mpris.position().await.unwrap(), Time::from_micros(0));
1514
1515        // send all the songs to the audio kernel (adding them to the queue and starting playback)
1516        audio_kernel.send(AudioCommand::Queue(
1517            mecomp_core::audio::commands::QueueCommand::AddToQueue(Box::new(
1518                first_song.clone().into(),
1519            )),
1520        ));
1521        audio_kernel.send(AudioCommand::Pause);
1522        let _ = event_rx.recv().unwrap();
1523        let _ = event_rx.recv().unwrap();
1524        let _ = event_rx.recv().unwrap();
1525        // update internal state
1526        *mpris.state.write().await = mpris
1527            .daemon
1528            .read()
1529            .await
1530            .as_ref()
1531            .unwrap()
1532            .state_audio(Context::current())
1533            .await
1534            .unwrap()
1535            .unwrap();
1536
1537        let first_song_track_id = mpris.metadata().await.unwrap().trackid().unwrap();
1538
1539        // normal:
1540        mpris
1541            .set_position(first_song_track_id.clone(), Time::from_secs(2))
1542            .await
1543            .unwrap();
1544        assert_eq!(
1545            event_rx.recv(),
1546            Ok(StateChange::Seeked(Duration::from_secs(2)))
1547        );
1548
1549        // If the Position argument is less than 0, do nothing. //
1550        mpris
1551            .set_position(first_song_track_id.clone(), Time::from_secs(-1))
1552            .await
1553            .unwrap();
1554        assert!(event_rx.try_recv().is_err());
1555
1556        // If the Position argument is greater than the track length, do nothing. //
1557        mpris
1558            .set_position(first_song_track_id.clone(), Time::from_secs(100))
1559            .await
1560            .unwrap();
1561        assert!(event_rx.try_recv().is_err());
1562
1563        // If the media player allows it, the current playback position can be changed either the [SetPosition] method or the [Seek] method on this interface. //
1564        // If this is not the case, the [CanSeek] property is false, and setting this property has no effect and can raise an error. //
1565        assert!(mpris.can_seek().await.unwrap());
1566
1567        mpris.seek(Time::from_secs(1)).await.unwrap();
1568        assert_eq!(
1569            event_rx.recv(),
1570            Ok(StateChange::Seeked(Duration::from_secs(3)))
1571        );
1572
1573        mpris.seek(Time::from_secs(-1)).await.unwrap();
1574        assert_eq!(
1575            event_rx.recv(),
1576            Ok(StateChange::Seeked(Duration::from_secs(2)))
1577        );
1578
1579        drop(tempdir);
1580    }
1581}