termusicplayback/
lib.rs

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