termusicplayback/
lib.rs

1//! SPDX-License-Identifier: MIT
2
3use std::sync::Arc;
4use std::time::Duration;
5
6use anyhow::{Context, Result};
7use async_trait::async_trait;
8use parking_lot::RwLock;
9pub use playlist::Playlist;
10use termusiclib::config::v2::server::config_extra::ServerConfigVersionedDefaulted;
11use termusiclib::config::SharedServerSettings;
12use termusiclib::library_db::DataBase;
13use termusiclib::player::playlist_helpers::{
14    PlaylistAddTrack, PlaylistPlaySpecific, PlaylistRemoveTrackIndexed, PlaylistSwapTrack,
15};
16use termusiclib::player::{
17    PlayerProgress, PlayerTimeUnit, RunningStatus, TrackChangedInfo, UpdateEvents,
18};
19use termusiclib::podcast::db::Database as DBPod;
20use termusiclib::track::{MediaTypesSimple, Track};
21use termusiclib::utils::get_app_config_path;
22use tokio::runtime::Handle;
23use tokio::sync::mpsc::error::SendError;
24use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
25use tokio::sync::{broadcast, oneshot};
26
27pub use backends::{Backend, BackendSelect};
28
29mod discord;
30mod mpris;
31pub mod playlist;
32
33#[macro_use]
34extern crate log;
35
36mod backends;
37
38/// Private module for benchmarking only, should never be used outside.
39///
40/// This is necessary as benchmarking via criterion can only access public lib(crate) level function, like any other outside binary / crate.
41pub mod __bench {
42    pub use super::backends::rusty::source::async_ring;
43}
44
45pub type PlayerCmdCallback = oneshot::Receiver<()>;
46pub type PlayerCmdReciever = UnboundedReceiver<(PlayerCmd, PlayerCmdCallbackSender)>;
47
48/// Wrapper around the potential oneshot sender to implement convenience functions.
49#[derive(Debug)]
50pub struct PlayerCmdCallbackSender(Option<oneshot::Sender<()>>);
51
52impl PlayerCmdCallbackSender {
53    /// Send on the oneshot, if there is any.
54    pub fn call(self) {
55        let Some(sender) = self.0 else {
56            return;
57        };
58        let _ = sender.send(());
59    }
60}
61
62/// Wrapper for the actual sender, to make it easier to implement new functions.
63#[derive(Debug, Clone)]
64pub struct PlayerCmdSender(UnboundedSender<(PlayerCmd, PlayerCmdCallbackSender)>);
65
66impl PlayerCmdSender {
67    /// Send a given [`PlayerCmd`] without any callback.
68    ///
69    /// # Errors
70    /// Also see [`oneshot::Sender::send`].
71    pub fn send(
72        &self,
73        cmd: PlayerCmd,
74    ) -> Result<(), SendError<(PlayerCmd, PlayerCmdCallbackSender)>> {
75        self.0.send((cmd, PlayerCmdCallbackSender(None)))
76    }
77
78    /// Send a given [`PlayerCmd`] with a callback, returning the receiver.
79    ///
80    /// # Errors
81    /// Also see [`oneshot::Sender::send`].
82    pub fn send_cb(
83        &self,
84        cmd: PlayerCmd,
85    ) -> Result<PlayerCmdCallback, SendError<(PlayerCmd, PlayerCmdCallbackSender)>> {
86        let (tx, rx) = oneshot::channel();
87        self.0.send((cmd, PlayerCmdCallbackSender(Some(tx))))?;
88        Ok(rx)
89    }
90
91    #[must_use]
92    pub fn new(tx: UnboundedSender<(PlayerCmd, PlayerCmdCallbackSender)>) -> Self {
93        Self(tx)
94    }
95}
96
97#[derive(Clone, Debug, Copy, PartialEq)]
98pub enum PlayerErrorType {
99    /// The error happened for the currently playing track.
100    Current,
101    /// The error happened for the track that was tried to be enqueued.
102    Enqueue,
103}
104
105#[derive(Clone, Debug)]
106pub enum PlayerCmd {
107    AboutToFinish,
108    CycleLoop,
109    Eos,
110    GetProgress,
111    SkipPrevious,
112    Pause,
113    Play,
114    Quit,
115    ReloadConfig,
116    ReloadPlaylist,
117    SeekBackward,
118    SeekForward,
119    SkipNext,
120    SpeedDown,
121    SpeedUp,
122    Tick,
123    ToggleGapless,
124    TogglePause,
125    VolumeDown,
126    VolumeUp,
127    /// A Error happened in the backend (for example `NotFound`) that makes it unrecoverable to continue to play the current track.
128    /// This will basically be treated as a [`Eos`](PlayerCmd::Eos), with some extra handling.
129    ///
130    /// This should **not** be used if the whole backend is unrecoverable.
131    Error(PlayerErrorType),
132
133    PlaylistPlaySpecific(PlaylistPlaySpecific),
134    PlaylistAddTrack(PlaylistAddTrack),
135    PlaylistRemoveTrack(PlaylistRemoveTrackIndexed),
136    PlaylistClear,
137    PlaylistSwapTrack(PlaylistSwapTrack),
138    PlaylistShuffle,
139    PlaylistRemoveDeletedTracks,
140}
141
142pub type StreamTX = broadcast::Sender<UpdateEvents>;
143pub type SharedPlaylist = Arc<RwLock<Playlist>>;
144
145#[allow(clippy::module_name_repetitions)]
146pub struct GeneralPlayer {
147    pub backend: Backend,
148    pub playlist: SharedPlaylist,
149    pub config: SharedServerSettings,
150    pub current_track_updated: bool,
151    pub mpris: Option<mpris::Mpris>,
152    pub discord: Option<discord::Rpc>,
153    pub db: DataBase,
154    pub db_podcast: DBPod,
155    pub cmd_tx: PlayerCmdSender,
156    pub stream_tx: StreamTX,
157
158    /// Keep track of continues backend errors (like `NotFound`) to not keep trying infinitely.
159    pub errors_since_last_progress: usize,
160}
161
162impl GeneralPlayer {
163    /// Create a new [`GeneralPlayer`], with the selected `backend`
164    ///
165    /// # Errors
166    ///
167    /// - if connecting to the database fails
168    /// - if config path creation fails
169    pub fn new_backend(
170        backend: BackendSelect,
171        config: SharedServerSettings,
172        cmd_tx: PlayerCmdSender,
173        stream_tx: StreamTX,
174        playlist: SharedPlaylist,
175    ) -> Result<Self> {
176        let backend = Backend::new_select(backend, config.clone(), cmd_tx.clone());
177
178        let db_path = get_app_config_path().with_context(|| "failed to get podcast db path.")?;
179
180        let db_podcast = DBPod::new(&db_path).with_context(|| "error connecting to podcast db.")?;
181        let config_read = config.read();
182        let db = DataBase::new(&config_read)?;
183
184        let mpris = if config.read().settings.player.use_mediacontrols {
185            Some(mpris::Mpris::new(cmd_tx.clone()))
186        } else {
187            None
188        };
189        let discord = if config.read().get_discord_status_enable() {
190            Some(discord::Rpc::default())
191        } else {
192            None
193        };
194
195        drop(config_read);
196
197        Ok(Self {
198            backend,
199            playlist,
200            config,
201            mpris,
202            discord,
203            db,
204            db_podcast,
205            cmd_tx,
206            stream_tx,
207            current_track_updated: false,
208
209            errors_since_last_progress: 0,
210        })
211    }
212
213    /// Increment the errors that happened by one.
214    pub fn increment_errors(&mut self) {
215        self.errors_since_last_progress += 1;
216    }
217
218    /// Reset errors that happened back to 0
219    pub fn reset_errors(&mut self) {
220        self.errors_since_last_progress = 0;
221    }
222
223    /// Create a new [`GeneralPlayer`], with the default Backend ([`BackendSelect::Rusty`])
224    ///
225    /// # Errors
226    ///
227    /// - if connecting to the database fails
228    /// - if config path creation fails
229    pub fn new(
230        config: SharedServerSettings,
231        cmd_tx: PlayerCmdSender,
232        stream_tx: StreamTX,
233        playlist: SharedPlaylist,
234    ) -> Result<Self> {
235        Self::new_backend(
236            BackendSelect::default(),
237            config,
238            cmd_tx,
239            stream_tx,
240            playlist,
241        )
242    }
243
244    /// Reload the config from file, on fail continue to use the old
245    ///
246    /// # Errors
247    ///
248    /// - if Config could not be parsed
249    pub fn reload_config(&mut self) -> Result<()> {
250        info!("Reloading config");
251        let mut config = self.config.write();
252        let parsed = ServerConfigVersionedDefaulted::from_config_path()?.into_settings();
253        config.settings = parsed;
254
255        if config.settings.player.use_mediacontrols && self.mpris.is_none() {
256            // start mpris if new config has it enabled, but is not active yet
257            let mut mpris = mpris::Mpris::new(self.cmd_tx.clone());
258            // actually set the metadata of the currently playing track, otherwise the controls will work but no title or coverart will be set until next track
259            if let Some(track) = self.playlist.read().current_track() {
260                mpris.add_and_play(track);
261            }
262            // the same for volume
263            mpris.update_volume(self.volume());
264            self.mpris.replace(mpris);
265        } else if !config.settings.player.use_mediacontrols && self.mpris.is_some() {
266            // stop mpris if new config does not have it enabled, but is currently active
267            self.mpris.take();
268        }
269
270        if config.get_discord_status_enable() && self.discord.is_none() {
271            // start discord ipc if new config has it enabled, but is not active yet
272            let discord = discord::Rpc::default();
273
274            // actually set the metadata of the currently playing track, otherwise the controls will work but no title or coverart will be set until next track
275            if let Some(track) = self.playlist.read().current_track() {
276                discord.update(track);
277            }
278
279            self.discord.replace(discord);
280        } else if !config.get_discord_status_enable() && self.discord.is_some() {
281            // stop discord ipc if new config does not have it enabled, but is currently active
282            self.discord.take();
283        }
284
285        info!("Config Reloaded");
286
287        Ok(())
288    }
289
290    fn get_player(&self) -> &dyn PlayerTrait {
291        self.backend.as_player()
292    }
293
294    fn get_player_mut(&mut self) -> &mut (dyn PlayerTrait + Send) {
295        self.backend.as_player_mut()
296    }
297
298    pub fn toggle_gapless(&mut self) -> bool {
299        let new_gapless = !<Self as PlayerTrait>::gapless(self);
300        <Self as PlayerTrait>::set_gapless(self, new_gapless);
301        self.config.write().settings.player.gapless = new_gapless;
302        new_gapless
303    }
304
305    /// Requires that the function is called on a thread with a entered tokio runtime
306    ///
307    /// # Panics
308    ///
309    /// if `current_track_index` in playlist is above u32
310    pub fn start_play(&mut self) {
311        let mut playlist = self.playlist.write();
312        if playlist.is_stopped() | playlist.is_paused() {
313            playlist.set_status(RunningStatus::Running);
314        }
315
316        playlist.proceed();
317
318        if let Some(track) = playlist.current_track().cloned() {
319            info!("Starting Track {track:#?}");
320
321            if playlist.has_next_track() {
322                playlist.set_next_track(None);
323                drop(playlist);
324                self.current_track_updated = true;
325                info!("gapless next track played");
326                self.add_and_play_mpris_discord();
327                return;
328            }
329            drop(playlist);
330
331            self.current_track_updated = true;
332            let wait = async {
333                self.add_and_play(&track).await;
334            };
335            Handle::current().block_on(wait);
336
337            self.add_and_play_mpris_discord();
338            self.player_restore_last_position();
339
340            self.send_stream_ev(UpdateEvents::TrackChanged(TrackChangedInfo {
341                current_track_index: u64::try_from(self.playlist.read().get_current_track_index())
342                    .unwrap(),
343                current_track_updated: self.current_track_updated,
344                title: self.media_info().media_title,
345                progress: self.get_progress(),
346            }));
347        }
348    }
349
350    fn add_and_play_mpris_discord(&mut self) {
351        if let Some(track) = self.playlist.read().current_track() {
352            if let Some(ref mut mpris) = self.mpris {
353                mpris.add_and_play(track);
354            }
355
356            if let Some(ref discord) = self.discord {
357                discord.update(track);
358            }
359        }
360    }
361    pub fn enqueue_next_from_playlist(&mut self) {
362        let mut playlist = self.playlist.write();
363        if playlist.has_next_track() {
364            return;
365        }
366
367        let Some(track) = playlist.fetch_next_track().cloned() else {
368            return;
369        };
370        drop(playlist);
371
372        self.enqueue_next(&track);
373
374        info!("Next track enqueued: {track:#?}");
375    }
376
377    /// Skip to the next track, if there is one
378    pub fn next(&mut self) {
379        if self.playlist.read().current_track().is_some() {
380            info!("skip route 1 which is in most cases.");
381            self.playlist.write().set_next_track(None);
382            self.skip_one();
383        } else {
384            info!("skip route 2 cause no current track.");
385            self.stop();
386        }
387    }
388
389    /// Switch & Play the previous track in the playlist
390    pub fn previous(&mut self) {
391        let mut playlist = self.playlist.write();
392        playlist.previous();
393        playlist.proceed_false();
394        drop(playlist);
395        self.next();
396    }
397
398    /// Resume playback if paused, pause playback if running
399    pub fn toggle_pause(&mut self) {
400        // NOTE: if this ".read()" call is in a match's statement, it will not be unlocked until the end of the match
401        // see https://github.com/rust-lang/rust/issues/93883
402        let status = self.playlist.read().status();
403        match status {
404            RunningStatus::Running => {
405                <Self as PlayerTrait>::pause(self);
406            }
407            RunningStatus::Stopped => {}
408            RunningStatus::Paused => {
409                <Self as PlayerTrait>::resume(self);
410            }
411        }
412    }
413
414    /// Pause playback if running
415    pub fn pause(&mut self) {
416        // NOTE: if this ".read()" call is in a match's statement, it will not be unlocked until the end of the match
417        // see https://github.com/rust-lang/rust/issues/93883
418        let status = self.playlist.read().status();
419        match status {
420            RunningStatus::Running => {
421                <Self as PlayerTrait>::pause(self);
422            }
423            RunningStatus::Stopped | RunningStatus::Paused => {}
424        }
425    }
426
427    /// Resume playback if paused
428    pub fn play(&mut self) {
429        // NOTE: if this ".read()" call is in a match's statement, it will not be unlocked until the end of the match
430        // see https://github.com/rust-lang/rust/issues/93883
431        let status = self.playlist.read().status();
432        match status {
433            RunningStatus::Running | RunningStatus::Stopped => {}
434            RunningStatus::Paused => {
435                <Self as PlayerTrait>::resume(self);
436            }
437        }
438    }
439    /// # Panics
440    ///
441    /// if the underlying "seek" returns a error (which current never happens)
442    pub fn seek_relative(&mut self, forward: bool) {
443        // fallback to 5 instead of not seeking at all
444        let track_len = self
445            .playlist
446            .read()
447            .current_track()
448            .and_then(Track::duration)
449            .unwrap_or(Duration::from_secs(5))
450            .as_secs();
451
452        let mut offset = self
453            .config
454            .read()
455            .settings
456            .player
457            .seek_step
458            .get_step(track_len);
459
460        if !forward {
461            offset = -offset;
462        }
463        self.seek(offset).expect("Error in player seek.");
464    }
465
466    #[allow(clippy::cast_sign_loss)]
467    pub fn player_save_last_position(&mut self) {
468        let playlist = self.playlist.read();
469        let Some(track) = playlist.current_track() else {
470            info!("Not saving Last position as there is no current track");
471            return;
472        };
473        let Some(position) = self.position() else {
474            info!("Not saving Last position as there is no position");
475            return;
476        };
477
478        let Some(time_before_save) = self
479            .config
480            .read()
481            .settings
482            .player
483            .remember_position
484            .get_time(track.media_type())
485        else {
486            info!(
487                "Not saving Last position as \"Remember last position\" is not enabled for {:#?}",
488                track.media_type()
489            );
490            return;
491        };
492
493        if time_before_save < position.as_secs() {
494            match track.media_type() {
495                MediaTypesSimple::Music => {
496                    if let Err(err) = self.db.set_last_position(track, position) {
497                        error!("Saving last_position for music failed, Error: {err:#?}");
498                    }
499                }
500                MediaTypesSimple::LiveRadio => (),
501                MediaTypesSimple::Podcast => {
502                    if let Err(err) = self.db_podcast.set_last_position(track, position) {
503                        error!("Saving last_position for podcast failed, Error: {err:#?}");
504                    }
505                }
506            }
507        } else {
508            info!("Not saving Last position as the position is lower than time_before_save");
509        }
510    }
511
512    pub fn player_restore_last_position(&mut self) {
513        let playlist = self.playlist.read();
514        let Some(track) = playlist.current_track().cloned() else {
515            info!("Not restoring Last position as there is no current track");
516            return;
517        };
518        drop(playlist);
519
520        let mut restored = false;
521
522        if self
523            .config
524            .read()
525            .settings
526            .player
527            .remember_position
528            .is_enabled_for(track.media_type())
529        {
530            match track.media_type() {
531                MediaTypesSimple::Music => {
532                    if let Ok(last_pos) = self.db.get_last_position(&track) {
533                        self.seek_to(last_pos);
534                        restored = true;
535                    }
536                }
537                MediaTypesSimple::LiveRadio => (),
538                MediaTypesSimple::Podcast => {
539                    if let Ok(last_pos) = self.db_podcast.get_last_position(&track) {
540                        self.seek_to(last_pos);
541                        restored = true;
542                    }
543                }
544            }
545        } else {
546            info!(
547                "Not restoring Last position as it is not enabled for {:#?}",
548                track.media_type()
549            );
550        }
551
552        if restored {
553            // TODO: dosnt this apply podcast titles into the tracks database?
554            if let Err(err) = self.db.set_last_position(&track, Duration::from_secs(0)) {
555                error!("Resetting last_position failed, Error: {err:#?}");
556            }
557        }
558    }
559
560    /// Send stream events with consistent error handling
561    fn send_stream_ev(&self, ev: UpdateEvents) {
562        // there is only one error case: no receivers
563        if self.stream_tx.send(ev).is_err() {
564            debug!("Stream Event not send: No Receivers");
565        }
566    }
567}
568
569#[async_trait]
570impl PlayerTrait for GeneralPlayer {
571    async fn add_and_play(&mut self, track: &Track) {
572        self.get_player_mut().add_and_play(track).await;
573    }
574    fn volume(&self) -> Volume {
575        self.get_player().volume()
576    }
577    fn add_volume(&mut self, volume: VolumeSigned) -> Volume {
578        let vol = self.get_player_mut().add_volume(volume);
579        self.send_stream_ev(UpdateEvents::VolumeChanged { volume: vol });
580
581        vol
582    }
583    fn set_volume(&mut self, volume: Volume) -> Volume {
584        let vol = self.get_player_mut().set_volume(volume);
585        self.send_stream_ev(UpdateEvents::VolumeChanged { volume: vol });
586
587        vol
588    }
589    /// This function should not be used directly, use GeneralPlayer::pause
590    fn pause(&mut self) {
591        self.playlist.write().set_status(RunningStatus::Paused);
592        self.get_player_mut().pause();
593        if let Some(ref mut mpris) = self.mpris {
594            mpris.pause();
595        }
596        if let Some(ref discord) = self.discord {
597            discord.pause();
598        }
599
600        self.send_stream_ev(UpdateEvents::PlayStateChanged {
601            playing: RunningStatus::Paused.as_u32(),
602        });
603    }
604    /// This function should not be used directly, use GeneralPlayer::play
605    fn resume(&mut self) {
606        self.playlist.write().set_status(RunningStatus::Running);
607        self.get_player_mut().resume();
608        if let Some(ref mut mpris) = self.mpris {
609            mpris.resume();
610        }
611        let time_pos = self.get_player().position();
612        if let Some(ref discord) = self.discord {
613            discord.resume(time_pos);
614        }
615
616        self.send_stream_ev(UpdateEvents::PlayStateChanged {
617            playing: RunningStatus::Running.as_u32(),
618        });
619    }
620    fn is_paused(&self) -> bool {
621        self.get_player().is_paused()
622    }
623    fn seek(&mut self, secs: i64) -> Result<()> {
624        self.get_player_mut().seek(secs)
625    }
626    fn seek_to(&mut self, position: Duration) {
627        self.get_player_mut().seek_to(position);
628    }
629
630    fn set_speed(&mut self, speed: Speed) -> Speed {
631        let speed = self.get_player_mut().set_speed(speed);
632        self.send_stream_ev(UpdateEvents::SpeedChanged { speed });
633
634        speed
635    }
636
637    fn add_speed(&mut self, speed: SpeedSigned) -> Speed {
638        let speed = self.get_player_mut().add_speed(speed);
639        self.send_stream_ev(UpdateEvents::SpeedChanged { speed });
640
641        speed
642    }
643
644    fn speed(&self) -> Speed {
645        self.get_player().speed()
646    }
647
648    fn stop(&mut self) {
649        self.playlist.write().stop();
650        self.get_player_mut().stop();
651    }
652
653    fn get_progress(&self) -> Option<PlayerProgress> {
654        self.get_player().get_progress()
655    }
656
657    fn gapless(&self) -> bool {
658        self.get_player().gapless()
659    }
660
661    fn set_gapless(&mut self, to: bool) {
662        self.get_player_mut().set_gapless(to);
663        self.send_stream_ev(UpdateEvents::GaplessChanged { gapless: to });
664    }
665
666    fn skip_one(&mut self) {
667        self.get_player_mut().skip_one();
668    }
669
670    fn position(&self) -> Option<PlayerTimeUnit> {
671        self.get_player().position()
672    }
673
674    fn enqueue_next(&mut self, track: &Track) {
675        self.get_player_mut().enqueue_next(track);
676    }
677
678    fn media_info(&self) -> MediaInfo {
679        self.get_player().media_info()
680    }
681}
682
683/// Some information that may be available from the backend
684/// This is different from [`Track`] as this is everything parsed from the decoder's metadata
685/// and [`Track`] stores some different extra stuff
686#[derive(Debug, Clone, PartialEq, Default)]
687pub struct MediaInfo {
688    /// The title of the current media playing (if present)
689    pub media_title: Option<String>,
690}
691
692pub type Volume = u16;
693/// The type of [`Volume::saturating_add_signed`]
694pub type VolumeSigned = i16;
695pub type Speed = i32;
696// yes this is currently the same as speed, but for consistentcy with VolumeSigned (and maybe other types)
697pub type SpeedSigned = Speed;
698
699pub const MIN_SPEED: Speed = 1;
700pub const MAX_SPEED: Speed = 30;
701
702#[allow(clippy::module_name_repetitions)]
703#[async_trait]
704pub trait PlayerTrait {
705    /// Add the given track, skip to it (if not already) and start playing
706    async fn add_and_play(&mut self, track: &Track);
707    /// Get the currently set volume
708    fn volume(&self) -> Volume;
709    /// Add a relative amount to the current volume
710    ///
711    /// Returns the new volume
712    fn add_volume(&mut self, volume: VolumeSigned) -> Volume {
713        let volume = self.volume().saturating_add_signed(volume);
714        self.set_volume(volume)
715    }
716    /// Set the volume to a specific amount.
717    ///
718    /// Returns the new volume
719    fn set_volume(&mut self, volume: Volume) -> Volume;
720    fn pause(&mut self);
721    fn resume(&mut self);
722    fn is_paused(&self) -> bool;
723    /// Seek relatively to the current time
724    ///
725    /// # Errors
726    ///
727    /// Depending on different backend, there could be different errors during seek.
728    fn seek(&mut self, secs: i64) -> Result<()>;
729    // TODO: sync return types between "seek" and "seek_to"?
730    /// Seek to a absolute position
731    fn seek_to(&mut self, position: Duration);
732    /// Get current track time position
733    fn get_progress(&self) -> Option<PlayerProgress>;
734    /// Set the speed to a specific amount.
735    ///
736    /// Returns the new speed
737    fn set_speed(&mut self, speed: Speed) -> Speed;
738    /// Add a relative amount to the current speed
739    ///
740    /// Returns the new speed
741    fn add_speed(&mut self, speed: SpeedSigned) -> Speed {
742        // NOTE: the clamping should likely be done in `set_speed` instead of here
743        let speed = (self.speed() + speed).clamp(MIN_SPEED, MAX_SPEED);
744        self.set_speed(speed)
745    }
746    /// Get the currently set speed
747    fn speed(&self) -> Speed;
748    fn stop(&mut self);
749    fn gapless(&self) -> bool;
750    fn set_gapless(&mut self, to: bool);
751    fn skip_one(&mut self);
752    /// Quickly access the position.
753    ///
754    /// This should ALWAYS match up with [`PlayerTrait::get_progress`]'s `.position`!
755    fn position(&self) -> Option<PlayerTimeUnit> {
756        self.get_progress()?.position
757    }
758    /// Add the given URI to be played, but do not skip currently playing track
759    fn enqueue_next(&mut self, track: &Track);
760    /// Get info of the current media
761    fn media_info(&self) -> MediaInfo;
762}