termusiclib/
player.rs

1#![allow(clippy::module_name_repetitions)]
2use anyhow::{Context, anyhow, bail};
3
4// using lower mod to restrict clippy
5#[allow(clippy::pedantic)]
6mod protobuf {
7    tonic::include_proto!("player");
8}
9
10pub use protobuf::*;
11
12use crate::config::v2::server::LoopMode;
13
14// implement transform function for easy use
15impl From<protobuf::Duration> for std::time::Duration {
16    fn from(value: protobuf::Duration) -> Self {
17        std::time::Duration::new(value.secs, value.nanos)
18    }
19}
20
21impl From<std::time::Duration> for protobuf::Duration {
22    fn from(value: std::time::Duration) -> Self {
23        Self {
24            secs: value.as_secs(),
25            nanos: value.subsec_nanos(),
26        }
27    }
28}
29
30/// The primitive in which time (current position / total duration) will be stored as
31pub type PlayerTimeUnit = std::time::Duration;
32
33#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
34pub enum RunningStatus {
35    #[default]
36    Stopped,
37    Running,
38    Paused,
39}
40
41impl RunningStatus {
42    #[must_use]
43    pub fn as_u32(&self) -> u32 {
44        match self {
45            RunningStatus::Stopped => 0,
46            RunningStatus::Running => 1,
47            RunningStatus::Paused => 2,
48        }
49    }
50
51    #[must_use]
52    pub fn from_u32(status: u32) -> Self {
53        match status {
54            1 => RunningStatus::Running,
55            2 => RunningStatus::Paused,
56            _ => RunningStatus::Stopped,
57        }
58    }
59}
60
61impl std::fmt::Display for RunningStatus {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        match self {
64            Self::Running => write!(f, "Running"),
65            Self::Stopped => write!(f, "Stopped"),
66            Self::Paused => write!(f, "Paused"),
67        }
68    }
69}
70
71/// Struct to keep both values with a name, as tuples cannot have named fields
72#[derive(Debug, Clone, Copy, PartialEq)]
73pub struct PlayerProgress {
74    pub position: Option<PlayerTimeUnit>,
75    /// Total duration of the currently playing track, if there is a known total duration
76    pub total_duration: Option<PlayerTimeUnit>,
77}
78
79impl From<protobuf::PlayerTime> for PlayerProgress {
80    fn from(value: protobuf::PlayerTime) -> Self {
81        Self {
82            position: value.position.map(Into::into),
83            total_duration: value.total_duration.map(Into::into),
84        }
85    }
86}
87
88impl From<PlayerProgress> for protobuf::PlayerTime {
89    fn from(value: PlayerProgress) -> Self {
90        Self {
91            position: value.position.map(Into::into),
92            total_duration: value.total_duration.map(Into::into),
93        }
94    }
95}
96
97impl TryFrom<protobuf::UpdateProgress> for PlayerProgress {
98    type Error = anyhow::Error;
99
100    fn try_from(value: protobuf::UpdateProgress) -> Result<Self, Self::Error> {
101        let Some(val) = value.progress else {
102            bail!("Expected \"UpdateProgress\" to contain \"Some(progress)\"");
103        };
104
105        Ok(Self::from(val))
106    }
107}
108
109impl From<PlayerProgress> for protobuf::UpdateProgress {
110    fn from(value: PlayerProgress) -> Self {
111        Self {
112            progress: Some(value.into()),
113        }
114    }
115}
116
117#[derive(Debug, Clone, PartialEq)]
118pub struct TrackChangedInfo {
119    /// Current track index in the playlist
120    pub current_track_index: u64,
121    /// Indicate if the track changed to another track
122    pub current_track_updated: bool,
123    /// Title of the current track / radio
124    pub title: Option<String>,
125    /// Current progress of the track
126    pub progress: Option<PlayerProgress>,
127}
128
129#[derive(Debug, Clone, PartialEq)]
130pub enum UpdateEvents {
131    MissedEvents { amount: u64 },
132    VolumeChanged { volume: u16 },
133    SpeedChanged { speed: i32 },
134    PlayStateChanged { playing: u32 },
135    TrackChanged(TrackChangedInfo),
136    GaplessChanged { gapless: bool },
137    PlaylistChanged(UpdatePlaylistEvents),
138    Progress(PlayerProgress),
139}
140
141// might not be fully true, but necessary for Msg
142impl Eq for UpdateEvents {}
143
144type StreamTypes = protobuf::stream_updates::Type;
145
146// mainly for server to grpc
147impl From<UpdateEvents> for protobuf::StreamUpdates {
148    fn from(value: UpdateEvents) -> Self {
149        let val = match value {
150            UpdateEvents::MissedEvents { amount } => {
151                StreamTypes::MissedEvents(UpdateMissedEvents { amount })
152            }
153            UpdateEvents::VolumeChanged { volume } => {
154                StreamTypes::VolumeChanged(UpdateVolumeChanged {
155                    msg: Some(VolumeReply {
156                        volume: u32::from(volume),
157                    }),
158                })
159            }
160            UpdateEvents::SpeedChanged { speed } => StreamTypes::SpeedChanged(UpdateSpeedChanged {
161                msg: Some(SpeedReply { speed }),
162            }),
163            UpdateEvents::PlayStateChanged { playing } => {
164                StreamTypes::PlayStateChanged(UpdatePlayStateChanged {
165                    msg: Some(PlayState { status: playing }),
166                })
167            }
168            UpdateEvents::TrackChanged(info) => StreamTypes::TrackChanged(UpdateTrackChanged {
169                current_track_index: info.current_track_index,
170                current_track_updated: info.current_track_updated,
171                optional_title: info
172                    .title
173                    .map(protobuf::update_track_changed::OptionalTitle::Title),
174                progress: info.progress.map(Into::into),
175            }),
176            UpdateEvents::GaplessChanged { gapless } => {
177                StreamTypes::GaplessChanged(UpdateGaplessChanged {
178                    msg: Some(GaplessState { gapless }),
179                })
180            }
181            UpdateEvents::PlaylistChanged(ev) => StreamTypes::PlaylistChanged(ev.into()),
182            UpdateEvents::Progress(ev) => StreamTypes::ProgressChanged(ev.into()),
183        };
184
185        Self { r#type: Some(val) }
186    }
187}
188
189// mainly for grpc to client(tui)
190impl TryFrom<protobuf::StreamUpdates> for UpdateEvents {
191    type Error = anyhow::Error;
192
193    fn try_from(value: protobuf::StreamUpdates) -> Result<Self, Self::Error> {
194        let value = unwrap_msg(value.r#type, "StreamUpdates.type")?;
195
196        let res = match value {
197            StreamTypes::VolumeChanged(ev) => Self::VolumeChanged {
198                volume: clamp_u16(
199                    unwrap_msg(ev.msg, "StreamUpdates.types.volume_changed.msg")?.volume,
200                ),
201            },
202            StreamTypes::SpeedChanged(ev) => Self::SpeedChanged {
203                speed: unwrap_msg(ev.msg, "StreamUpdates.types.speed_changed.msg")?.speed,
204            },
205            StreamTypes::PlayStateChanged(ev) => Self::PlayStateChanged {
206                playing: unwrap_msg(ev.msg, "StreamUpdates.types.play_state_changed.msg")?.status,
207            },
208            StreamTypes::MissedEvents(ev) => Self::MissedEvents { amount: ev.amount },
209            StreamTypes::TrackChanged(ev) => Self::TrackChanged(TrackChangedInfo {
210                current_track_index: ev.current_track_index,
211                current_track_updated: ev.current_track_updated,
212                title: ev.optional_title.map(|v| {
213                    let protobuf::update_track_changed::OptionalTitle::Title(v) = v;
214                    v
215                }),
216                progress: ev.progress.map(Into::into),
217            }),
218            StreamTypes::GaplessChanged(ev) => Self::GaplessChanged {
219                gapless: unwrap_msg(ev.msg, "StreamUpdates.types.gapless_changed.msg")?.gapless,
220            },
221            StreamTypes::PlaylistChanged(ev) => Self::PlaylistChanged(
222                ev.try_into()
223                    .context("In \"StreamUpdates.types.playlist_changed\"")?,
224            ),
225            StreamTypes::ProgressChanged(ev) => Self::Progress(
226                ev.try_into()
227                    .context("In \"StreamUpdates.types.progress_changed\"")?,
228            ),
229        };
230
231        Ok(res)
232    }
233}
234
235#[derive(Debug, Clone, PartialEq)]
236pub struct PlaylistAddTrackInfo {
237    /// The Index at which a track was added at.
238    /// If this is not at the end, all tracks at this index and beyond should be shifted.
239    pub at_index: u64,
240    pub title: Option<String>,
241    /// Duration of the track
242    pub duration: PlayerTimeUnit,
243    pub trackid: playlist_helpers::PlaylistTrackSource,
244}
245
246#[derive(Debug, Clone, PartialEq)]
247pub struct PlaylistRemoveTrackInfo {
248    /// The Index at which a track was removed at.
249    pub at_index: u64,
250    /// The Id of the removed track.
251    pub trackid: playlist_helpers::PlaylistTrackSource,
252}
253
254#[derive(Debug, Clone, PartialEq)]
255pub struct PlaylistLoopModeInfo {
256    /// The actual mode, mapped to [`LoopMode`]
257    pub mode: u32,
258}
259
260impl From<LoopMode> for PlaylistLoopModeInfo {
261    fn from(value: LoopMode) -> Self {
262        Self {
263            mode: u32::from(value.discriminant()),
264        }
265    }
266}
267
268#[derive(Debug, Clone, PartialEq)]
269pub struct PlaylistSwapInfo {
270    pub index_a: u64,
271    pub index_b: u64,
272}
273
274#[derive(Debug, Clone, PartialEq)]
275pub struct PlaylistShuffledInfo {
276    pub tracks: PlaylistTracks,
277}
278
279/// Separate nested enum to handle all playlist related events
280#[derive(Debug, Clone, PartialEq)]
281pub enum UpdatePlaylistEvents {
282    PlaylistAddTrack(PlaylistAddTrackInfo),
283    PlaylistRemoveTrack(PlaylistRemoveTrackInfo),
284    PlaylistCleared,
285    PlaylistLoopMode(PlaylistLoopModeInfo),
286    PlaylistSwapTracks(PlaylistSwapInfo),
287    PlaylistShuffled(PlaylistShuffledInfo),
288}
289
290type PPlaylistTypes = protobuf::update_playlist::Type;
291
292// mainly for server to grpc
293impl From<UpdatePlaylistEvents> for protobuf::UpdatePlaylist {
294    fn from(value: UpdatePlaylistEvents) -> Self {
295        let val = match value {
296            UpdatePlaylistEvents::PlaylistAddTrack(vals) => {
297                PPlaylistTypes::AddTrack(protobuf::PlaylistAddTrack {
298                    at_index: vals.at_index,
299                    optional_title: vals
300                        .title
301                        .map(protobuf::playlist_add_track::OptionalTitle::Title),
302                    duration: Some(vals.duration.into()),
303                    id: Some(vals.trackid.into()),
304                })
305            }
306            UpdatePlaylistEvents::PlaylistRemoveTrack(vals) => {
307                PPlaylistTypes::RemoveTrack(protobuf::PlaylistRemoveTrack {
308                    at_index: vals.at_index,
309                    id: Some(vals.trackid.into()),
310                })
311            }
312            UpdatePlaylistEvents::PlaylistCleared => PPlaylistTypes::Cleared(PlaylistCleared {}),
313            UpdatePlaylistEvents::PlaylistLoopMode(vals) => {
314                PPlaylistTypes::LoopMode(PlaylistLoopMode { mode: vals.mode })
315            }
316            UpdatePlaylistEvents::PlaylistSwapTracks(vals) => {
317                PPlaylistTypes::SwapTracks(protobuf::PlaylistSwapTracks {
318                    index_a: vals.index_a,
319                    index_b: vals.index_b,
320                })
321            }
322            UpdatePlaylistEvents::PlaylistShuffled(vals) => {
323                PPlaylistTypes::Shuffled(protobuf::PlaylistShuffled {
324                    shuffled: Some(vals.tracks),
325                })
326            }
327        };
328
329        Self { r#type: Some(val) }
330    }
331}
332
333// mainly for grpc to client(tui)
334impl TryFrom<protobuf::UpdatePlaylist> for UpdatePlaylistEvents {
335    type Error = anyhow::Error;
336
337    fn try_from(value: protobuf::UpdatePlaylist) -> Result<Self, Self::Error> {
338        let value = unwrap_msg(value.r#type, "UpdatePlaylist.type")?;
339
340        let res = match value {
341            PPlaylistTypes::AddTrack(ev) => Self::PlaylistAddTrack(PlaylistAddTrackInfo {
342                at_index: ev.at_index,
343                title: ev.optional_title.map(|v| {
344                    let protobuf::playlist_add_track::OptionalTitle::Title(v) = v;
345                    v
346                }),
347                duration: unwrap_msg(ev.duration, "UpdatePlaylist.type.add_track.duration")?.into(),
348                trackid: unwrap_msg(
349                    unwrap_msg(ev.id, "UpdatePlaylist.type.add_track.id")?.source,
350                    "UpdatePlaylist.type.add_track.id.source",
351                )?
352                .try_into()?,
353            }),
354            PPlaylistTypes::RemoveTrack(ev) => Self::PlaylistRemoveTrack(PlaylistRemoveTrackInfo {
355                at_index: ev.at_index,
356                trackid: unwrap_msg(
357                    unwrap_msg(ev.id, "UpdatePlaylist.type.remove_track.id")?.source,
358                    "UpdatePlaylist.type.remove_track.id.source",
359                )?
360                .try_into()?,
361            }),
362            PPlaylistTypes::Cleared(_) => Self::PlaylistCleared,
363            PPlaylistTypes::LoopMode(ev) => {
364                Self::PlaylistLoopMode(PlaylistLoopModeInfo { mode: ev.mode })
365            }
366            PPlaylistTypes::SwapTracks(ev) => Self::PlaylistSwapTracks(PlaylistSwapInfo {
367                index_a: ev.index_a,
368                index_b: ev.index_b,
369            }),
370            PPlaylistTypes::Shuffled(ev) => {
371                let shuffled = unwrap_msg(ev.shuffled, "UpdatePlaylist.type.shuffled.shuffled")?;
372                Self::PlaylistShuffled(PlaylistShuffledInfo { tracks: shuffled })
373            }
374        };
375
376        Ok(res)
377    }
378}
379
380/// Easily unwrap a given grpc option and convert it to a result, with a location on None
381fn unwrap_msg<T>(opt: Option<T>, place: &str) -> Result<T, anyhow::Error> {
382    match opt {
383        Some(val) => Ok(val),
384        None => Err(anyhow!("Got \"None\" in grpc \"{place}\"!")),
385    }
386}
387
388/// Clamp a given `u32` to be `u16`.
389///
390/// This is mainly used for volume clamping as we only use u16 for that, but protobuf minimal number is u32.
391#[allow(clippy::cast_possible_truncation)]
392#[must_use]
393pub fn clamp_u16(val: u32) -> u16 {
394    val.min(u32::from(u16::MAX)) as u16
395}
396
397pub mod playlist_helpers {
398    use anyhow::Context;
399
400    use super::{PlaylistTracksToRemoveClear, protobuf, unwrap_msg};
401
402    /// A Id / Source for a given Track
403    #[derive(Debug, Clone, PartialEq)]
404    pub enum PlaylistTrackSource {
405        Path(String),
406        Url(String),
407        PodcastUrl(String),
408    }
409
410    impl From<PlaylistTrackSource> for protobuf::track_id::Source {
411        fn from(value: PlaylistTrackSource) -> Self {
412            match value {
413                PlaylistTrackSource::Path(v) => Self::Path(v),
414                PlaylistTrackSource::Url(v) => Self::Url(v),
415                PlaylistTrackSource::PodcastUrl(v) => Self::PodcastUrl(v),
416            }
417        }
418    }
419
420    impl From<PlaylistTrackSource> for protobuf::TrackId {
421        fn from(value: PlaylistTrackSource) -> Self {
422            Self {
423                source: Some(value.into()),
424            }
425        }
426    }
427
428    impl TryFrom<protobuf::track_id::Source> for PlaylistTrackSource {
429        type Error = anyhow::Error;
430
431        fn try_from(value: protobuf::track_id::Source) -> Result<Self, Self::Error> {
432            Ok(match value {
433                protobuf::track_id::Source::Path(v) => Self::Path(v),
434                protobuf::track_id::Source::Url(v) => Self::Url(v),
435                protobuf::track_id::Source::PodcastUrl(v) => Self::PodcastUrl(v),
436            })
437        }
438    }
439
440    impl TryFrom<protobuf::TrackId> for PlaylistTrackSource {
441        type Error = anyhow::Error;
442
443        fn try_from(value: protobuf::TrackId) -> Result<Self, Self::Error> {
444            unwrap_msg(value.source, "TrackId.source").and_then(Self::try_from)
445        }
446    }
447
448    /// Data for requesting some tracks to be added in the server
449    #[derive(Debug, Clone, PartialEq)]
450    pub struct PlaylistAddTrack {
451        pub at_index: u64,
452        pub tracks: Vec<PlaylistTrackSource>,
453    }
454
455    impl PlaylistAddTrack {
456        #[must_use]
457        pub fn new_single(at_index: u64, track: PlaylistTrackSource) -> Self {
458            Self {
459                at_index,
460                tracks: vec![track],
461            }
462        }
463
464        #[must_use]
465        pub fn new_vec(at_index: u64, tracks: Vec<PlaylistTrackSource>) -> Self {
466            Self { at_index, tracks }
467        }
468    }
469
470    impl From<PlaylistAddTrack> for protobuf::PlaylistTracksToAdd {
471        fn from(value: PlaylistAddTrack) -> Self {
472            Self {
473                at_index: value.at_index,
474                tracks: value.tracks.into_iter().map(Into::into).collect(),
475            }
476        }
477    }
478
479    impl TryFrom<protobuf::PlaylistTracksToAdd> for PlaylistAddTrack {
480        type Error = anyhow::Error;
481
482        fn try_from(value: protobuf::PlaylistTracksToAdd) -> Result<Self, Self::Error> {
483            let tracks = value
484                .tracks
485                .into_iter()
486                .map(|v| PlaylistTrackSource::try_from(v).context("PlaylistTracksToAdd.tracks"))
487                .collect::<Result<Vec<_>, anyhow::Error>>()?;
488
489            Ok(Self {
490                at_index: value.at_index,
491                tracks,
492            })
493        }
494    }
495
496    /// Data for requesting some tracks to be removed in the server
497    #[derive(Debug, Clone, PartialEq)]
498    pub struct PlaylistRemoveTrackIndexed {
499        pub at_index: u64,
500        pub tracks: Vec<PlaylistTrackSource>,
501    }
502
503    impl PlaylistRemoveTrackIndexed {
504        #[must_use]
505        pub fn new_single(at_index: u64, track: PlaylistTrackSource) -> Self {
506            Self {
507                at_index,
508                tracks: vec![track],
509            }
510        }
511
512        #[must_use]
513        pub fn new_vec(at_index: u64, tracks: Vec<PlaylistTrackSource>) -> Self {
514            Self { at_index, tracks }
515        }
516    }
517
518    impl From<PlaylistRemoveTrackIndexed> for protobuf::PlaylistTracksToRemoveIndexed {
519        fn from(value: PlaylistRemoveTrackIndexed) -> Self {
520            Self {
521                at_index: value.at_index,
522                tracks: value.tracks.into_iter().map(Into::into).collect(),
523            }
524        }
525    }
526
527    impl TryFrom<protobuf::PlaylistTracksToRemoveIndexed> for PlaylistRemoveTrackIndexed {
528        type Error = anyhow::Error;
529
530        fn try_from(value: protobuf::PlaylistTracksToRemoveIndexed) -> Result<Self, Self::Error> {
531            let tracks = value
532                .tracks
533                .into_iter()
534                .map(|v| {
535                    PlaylistTrackSource::try_from(v).context("PlaylistTracksToRemoveIndexed.tracks")
536                })
537                .collect::<Result<Vec<_>, anyhow::Error>>()?;
538
539            Ok(Self {
540                at_index: value.at_index,
541                tracks,
542            })
543        }
544    }
545
546    /// Data for requesting some tracks to be removed in the server
547    #[derive(Debug, Clone, PartialEq)]
548    pub enum PlaylistRemoveTrackType {
549        Indexed(PlaylistRemoveTrackIndexed),
550        Clear,
551    }
552
553    type PToRemoveTypes = protobuf::playlist_tracks_to_remove::Type;
554
555    impl From<PlaylistRemoveTrackType> for protobuf::PlaylistTracksToRemove {
556        fn from(value: PlaylistRemoveTrackType) -> Self {
557            Self {
558                r#type: Some(match value {
559                    PlaylistRemoveTrackType::Indexed(v) => PToRemoveTypes::Indexed(v.into()),
560                    PlaylistRemoveTrackType::Clear => {
561                        PToRemoveTypes::Clear(PlaylistTracksToRemoveClear {})
562                    }
563                }),
564            }
565        }
566    }
567
568    impl TryFrom<protobuf::PlaylistTracksToRemove> for PlaylistRemoveTrackType {
569        type Error = anyhow::Error;
570
571        fn try_from(value: protobuf::PlaylistTracksToRemove) -> Result<Self, Self::Error> {
572            let value = unwrap_msg(value.r#type, "PlaylistTracksToRemove.type")?;
573
574            Ok(match value {
575                PToRemoveTypes::Indexed(v) => Self::Indexed(v.try_into()?),
576                PToRemoveTypes::Clear(_) => Self::Clear,
577            })
578        }
579    }
580
581    /// Data for requesting some tracks to be swapped in the server
582    #[derive(Debug, Clone, PartialEq)]
583    pub struct PlaylistSwapTrack {
584        pub index_a: u64,
585        pub index_b: u64,
586    }
587
588    impl From<PlaylistSwapTrack> for protobuf::PlaylistSwapTracks {
589        fn from(value: PlaylistSwapTrack) -> Self {
590            Self {
591                index_a: value.index_a,
592                index_b: value.index_b,
593            }
594        }
595    }
596
597    impl TryFrom<protobuf::PlaylistSwapTracks> for PlaylistSwapTrack {
598        type Error = anyhow::Error;
599
600        fn try_from(value: protobuf::PlaylistSwapTracks) -> Result<Self, Self::Error> {
601            Ok(Self {
602                index_a: value.index_a,
603                index_b: value.index_b,
604            })
605        }
606    }
607
608    /// Data for requesting to skip / play a specific track
609    #[derive(Debug, Clone, PartialEq)]
610    pub struct PlaylistPlaySpecific {
611        pub track_index: u64,
612        pub id: PlaylistTrackSource,
613    }
614
615    impl From<PlaylistPlaySpecific> for protobuf::PlaylistPlaySpecific {
616        fn from(value: PlaylistPlaySpecific) -> Self {
617            Self {
618                track_index: value.track_index,
619                id: Some(value.id.into()),
620            }
621        }
622    }
623
624    impl TryFrom<protobuf::PlaylistPlaySpecific> for PlaylistPlaySpecific {
625        type Error = anyhow::Error;
626
627        fn try_from(value: protobuf::PlaylistPlaySpecific) -> Result<Self, Self::Error> {
628            Ok(Self {
629                track_index: value.track_index,
630                id: unwrap_msg(value.id, "PlaylistPlaySpecific.id").and_then(|v| {
631                    PlaylistTrackSource::try_from(v).context("PlaylistPlaySpecific.id")
632                })?,
633            })
634        }
635    }
636}