termusiclib/
player.rs

1#![allow(clippy::module_name_repetitions)]
2use anyhow::{anyhow, Context};
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
97#[derive(Debug, Clone, PartialEq)]
98pub struct TrackChangedInfo {
99    /// Current track index in the playlist
100    pub current_track_index: u64,
101    /// Indicate if the track changed to another track
102    pub current_track_updated: bool,
103    /// Title of the current track / radio
104    pub title: Option<String>,
105    /// Current progress of the track
106    pub progress: Option<PlayerProgress>,
107}
108
109#[derive(Debug, Clone, PartialEq)]
110pub enum UpdateEvents {
111    MissedEvents { amount: u64 },
112    VolumeChanged { volume: u16 },
113    SpeedChanged { speed: i32 },
114    PlayStateChanged { playing: u32 },
115    TrackChanged(TrackChangedInfo),
116    GaplessChanged { gapless: bool },
117    PlaylistChanged(UpdatePlaylistEvents),
118}
119
120type StreamTypes = protobuf::stream_updates::Type;
121
122// mainly for server to grpc
123impl From<UpdateEvents> for protobuf::StreamUpdates {
124    fn from(value: UpdateEvents) -> Self {
125        let val = match value {
126            UpdateEvents::MissedEvents { amount } => {
127                StreamTypes::MissedEvents(UpdateMissedEvents { amount })
128            }
129            UpdateEvents::VolumeChanged { volume } => {
130                StreamTypes::VolumeChanged(UpdateVolumeChanged {
131                    msg: Some(VolumeReply {
132                        volume: u32::from(volume),
133                    }),
134                })
135            }
136            UpdateEvents::SpeedChanged { speed } => StreamTypes::SpeedChanged(UpdateSpeedChanged {
137                msg: Some(SpeedReply { speed }),
138            }),
139            UpdateEvents::PlayStateChanged { playing } => {
140                StreamTypes::PlayStateChanged(UpdatePlayStateChanged {
141                    msg: Some(PlayState { status: playing }),
142                })
143            }
144            UpdateEvents::TrackChanged(info) => StreamTypes::TrackChanged(UpdateTrackChanged {
145                current_track_index: info.current_track_index,
146                current_track_updated: info.current_track_updated,
147                optional_title: info
148                    .title
149                    .map(protobuf::update_track_changed::OptionalTitle::Title),
150                progress: info.progress.map(Into::into),
151            }),
152            UpdateEvents::GaplessChanged { gapless } => {
153                StreamTypes::GaplessChanged(UpdateGaplessChanged {
154                    msg: Some(GaplessState { gapless }),
155                })
156            }
157            UpdateEvents::PlaylistChanged(ev) => StreamTypes::PlaylistChanged(ev.into()),
158        };
159
160        Self { r#type: Some(val) }
161    }
162}
163
164// mainly for grpc to client(tui)
165impl TryFrom<protobuf::StreamUpdates> for UpdateEvents {
166    type Error = anyhow::Error;
167
168    fn try_from(value: protobuf::StreamUpdates) -> Result<Self, Self::Error> {
169        let value = unwrap_msg(value.r#type, "StreamUpdates.type")?;
170
171        let res = match value {
172            StreamTypes::VolumeChanged(ev) => Self::VolumeChanged {
173                volume: clamp_u16(
174                    unwrap_msg(ev.msg, "StreamUpdates.types.volume_changed.msg")?.volume,
175                ),
176            },
177            StreamTypes::SpeedChanged(ev) => Self::SpeedChanged {
178                speed: unwrap_msg(ev.msg, "StreamUpdates.types.speed_changed.msg")?.speed,
179            },
180            StreamTypes::PlayStateChanged(ev) => Self::PlayStateChanged {
181                playing: unwrap_msg(ev.msg, "StreamUpdates.types.play_state_changed.msg")?.status,
182            },
183            StreamTypes::MissedEvents(ev) => Self::MissedEvents { amount: ev.amount },
184            StreamTypes::TrackChanged(ev) => Self::TrackChanged(TrackChangedInfo {
185                current_track_index: ev.current_track_index,
186                current_track_updated: ev.current_track_updated,
187                title: ev.optional_title.map(|v| {
188                    let protobuf::update_track_changed::OptionalTitle::Title(v) = v;
189                    v
190                }),
191                progress: ev.progress.map(Into::into),
192            }),
193            StreamTypes::GaplessChanged(ev) => Self::GaplessChanged {
194                gapless: unwrap_msg(ev.msg, "StreamUpdates.types.gapless_changed.msg")?.gapless,
195            },
196            StreamTypes::PlaylistChanged(ev) => Self::PlaylistChanged(
197                ev.try_into()
198                    .context("In \"StreamUpdates.types.playlist_changed\"")?,
199            ),
200        };
201
202        Ok(res)
203    }
204}
205
206#[derive(Debug, Clone, PartialEq)]
207pub struct PlaylistAddTrackInfo {
208    /// The Index at which a track was added at.
209    /// If this is not at the end, all tracks at this index and beyond should be shifted.
210    pub at_index: u64,
211    pub title: Option<String>,
212    /// Duration of the track
213    pub duration: PlayerTimeUnit,
214    pub trackid: playlist_helpers::PlaylistTrackSource,
215}
216
217#[derive(Debug, Clone, PartialEq)]
218pub struct PlaylistRemoveTrackInfo {
219    /// The Index at which a track was removed at.
220    pub at_index: u64,
221    /// The Id of the removed track.
222    pub trackid: playlist_helpers::PlaylistTrackSource,
223}
224
225#[derive(Debug, Clone, PartialEq)]
226pub struct PlaylistLoopModeInfo {
227    /// The actual mode, mapped to [`LoopMode`]
228    pub mode: u32,
229}
230
231impl From<LoopMode> for PlaylistLoopModeInfo {
232    fn from(value: LoopMode) -> Self {
233        Self {
234            mode: u32::from(value.discriminant()),
235        }
236    }
237}
238
239#[derive(Debug, Clone, PartialEq)]
240pub struct PlaylistSwapInfo {
241    pub index_a: u64,
242    pub index_b: u64,
243}
244
245#[derive(Debug, Clone, PartialEq)]
246pub struct PlaylistShuffledInfo {
247    pub tracks: PlaylistTracks,
248}
249
250/// Separate nested enum to handle all playlist related events
251#[derive(Debug, Clone, PartialEq)]
252pub enum UpdatePlaylistEvents {
253    PlaylistAddTrack(PlaylistAddTrackInfo),
254    PlaylistRemoveTrack(PlaylistRemoveTrackInfo),
255    PlaylistCleared,
256    PlaylistLoopMode(PlaylistLoopModeInfo),
257    PlaylistSwapTracks(PlaylistSwapInfo),
258    PlaylistShuffled(PlaylistShuffledInfo),
259}
260
261type PPlaylistTypes = protobuf::update_playlist::Type;
262
263// mainly for server to grpc
264impl From<UpdatePlaylistEvents> for protobuf::UpdatePlaylist {
265    fn from(value: UpdatePlaylistEvents) -> Self {
266        let val = match value {
267            UpdatePlaylistEvents::PlaylistAddTrack(vals) => {
268                PPlaylistTypes::AddTrack(protobuf::PlaylistAddTrack {
269                    at_index: vals.at_index,
270                    optional_title: vals
271                        .title
272                        .map(protobuf::playlist_add_track::OptionalTitle::Title),
273                    duration: Some(vals.duration.into()),
274                    id: Some(vals.trackid.into()),
275                })
276            }
277            UpdatePlaylistEvents::PlaylistRemoveTrack(vals) => {
278                PPlaylistTypes::RemoveTrack(protobuf::PlaylistRemoveTrack {
279                    at_index: vals.at_index,
280                    id: Some(vals.trackid.into()),
281                })
282            }
283            UpdatePlaylistEvents::PlaylistCleared => PPlaylistTypes::Cleared(PlaylistCleared {}),
284            UpdatePlaylistEvents::PlaylistLoopMode(vals) => {
285                PPlaylistTypes::LoopMode(PlaylistLoopMode { mode: vals.mode })
286            }
287            UpdatePlaylistEvents::PlaylistSwapTracks(vals) => {
288                PPlaylistTypes::SwapTracks(protobuf::PlaylistSwapTracks {
289                    index_a: vals.index_a,
290                    index_b: vals.index_b,
291                })
292            }
293            UpdatePlaylistEvents::PlaylistShuffled(vals) => {
294                PPlaylistTypes::Shuffled(protobuf::PlaylistShuffled {
295                    shuffled: Some(vals.tracks),
296                })
297            }
298        };
299
300        Self { r#type: Some(val) }
301    }
302}
303
304// mainly for grpc to client(tui)
305impl TryFrom<protobuf::UpdatePlaylist> for UpdatePlaylistEvents {
306    type Error = anyhow::Error;
307
308    fn try_from(value: protobuf::UpdatePlaylist) -> Result<Self, Self::Error> {
309        let value = unwrap_msg(value.r#type, "UpdatePlaylist.type")?;
310
311        let res = match value {
312            PPlaylistTypes::AddTrack(ev) => Self::PlaylistAddTrack(PlaylistAddTrackInfo {
313                at_index: ev.at_index,
314                title: ev.optional_title.map(|v| {
315                    let protobuf::playlist_add_track::OptionalTitle::Title(v) = v;
316                    v
317                }),
318                duration: unwrap_msg(ev.duration, "UpdatePlaylist.type.add_track.duration")?.into(),
319                trackid: unwrap_msg(
320                    unwrap_msg(ev.id, "UpdatePlaylist.type.add_track.id")?.source,
321                    "UpdatePlaylist.type.add_track.id.source",
322                )?
323                .try_into()?,
324            }),
325            PPlaylistTypes::RemoveTrack(ev) => Self::PlaylistRemoveTrack(PlaylistRemoveTrackInfo {
326                at_index: ev.at_index,
327                trackid: unwrap_msg(
328                    unwrap_msg(ev.id, "UpdatePlaylist.type.remove_track.id")?.source,
329                    "UpdatePlaylist.type.remove_track.id.source",
330                )?
331                .try_into()?,
332            }),
333            PPlaylistTypes::Cleared(_) => Self::PlaylistCleared,
334            PPlaylistTypes::LoopMode(ev) => {
335                Self::PlaylistLoopMode(PlaylistLoopModeInfo { mode: ev.mode })
336            }
337            PPlaylistTypes::SwapTracks(ev) => Self::PlaylistSwapTracks(PlaylistSwapInfo {
338                index_a: ev.index_a,
339                index_b: ev.index_b,
340            }),
341            PPlaylistTypes::Shuffled(ev) => {
342                let shuffled = unwrap_msg(ev.shuffled, "UpdatePlaylist.type.shuffled.shuffled")?;
343                Self::PlaylistShuffled(PlaylistShuffledInfo { tracks: shuffled })
344            }
345        };
346
347        Ok(res)
348    }
349}
350
351/// Easily unwrap a given grpc option and covert it to a result, with a location on None
352fn unwrap_msg<T>(opt: Option<T>, place: &str) -> Result<T, anyhow::Error> {
353    match opt {
354        Some(val) => Ok(val),
355        None => Err(anyhow!("Got \"None\" in grpc \"{place}\"!")),
356    }
357}
358
359#[allow(clippy::cast_possible_truncation)]
360fn clamp_u16(val: u32) -> u16 {
361    val.min(u32::from(u16::MAX)) as u16
362}
363
364pub mod playlist_helpers {
365    use anyhow::Context;
366
367    use super::{protobuf, unwrap_msg, PlaylistTracksToRemoveClear};
368
369    /// A Id / Source for a given Track
370    #[derive(Debug, Clone, PartialEq)]
371    pub enum PlaylistTrackSource {
372        Path(String),
373        Url(String),
374        PodcastUrl(String),
375    }
376
377    impl From<PlaylistTrackSource> for protobuf::track_id::Source {
378        fn from(value: PlaylistTrackSource) -> Self {
379            match value {
380                PlaylistTrackSource::Path(v) => Self::Path(v),
381                PlaylistTrackSource::Url(v) => Self::Url(v),
382                PlaylistTrackSource::PodcastUrl(v) => Self::PodcastUrl(v),
383            }
384        }
385    }
386
387    impl From<PlaylistTrackSource> for protobuf::TrackId {
388        fn from(value: PlaylistTrackSource) -> Self {
389            Self {
390                source: Some(value.into()),
391            }
392        }
393    }
394
395    impl TryFrom<protobuf::track_id::Source> for PlaylistTrackSource {
396        type Error = anyhow::Error;
397
398        fn try_from(value: protobuf::track_id::Source) -> Result<Self, Self::Error> {
399            Ok(match value {
400                protobuf::track_id::Source::Path(v) => Self::Path(v),
401                protobuf::track_id::Source::Url(v) => Self::Url(v),
402                protobuf::track_id::Source::PodcastUrl(v) => Self::PodcastUrl(v),
403            })
404        }
405    }
406
407    impl TryFrom<protobuf::TrackId> for PlaylistTrackSource {
408        type Error = anyhow::Error;
409
410        fn try_from(value: protobuf::TrackId) -> Result<Self, Self::Error> {
411            unwrap_msg(value.source, "TrackId.source").and_then(Self::try_from)
412        }
413    }
414
415    /// Data for requesting some tracks to be added in the server
416    #[derive(Debug, Clone, PartialEq)]
417    pub struct PlaylistAddTrack {
418        pub at_index: u64,
419        pub tracks: Vec<PlaylistTrackSource>,
420    }
421
422    impl PlaylistAddTrack {
423        #[must_use]
424        pub fn new_single(at_index: u64, track: PlaylistTrackSource) -> Self {
425            Self {
426                at_index,
427                tracks: vec![track],
428            }
429        }
430
431        #[must_use]
432        pub fn new_vec(at_index: u64, tracks: Vec<PlaylistTrackSource>) -> Self {
433            Self { at_index, tracks }
434        }
435    }
436
437    impl From<PlaylistAddTrack> for protobuf::PlaylistTracksToAdd {
438        fn from(value: PlaylistAddTrack) -> Self {
439            Self {
440                at_index: value.at_index,
441                tracks: value.tracks.into_iter().map(Into::into).collect(),
442            }
443        }
444    }
445
446    impl TryFrom<protobuf::PlaylistTracksToAdd> for PlaylistAddTrack {
447        type Error = anyhow::Error;
448
449        fn try_from(value: protobuf::PlaylistTracksToAdd) -> Result<Self, Self::Error> {
450            let tracks = value
451                .tracks
452                .into_iter()
453                .map(|v| PlaylistTrackSource::try_from(v).context("PlaylistTracksToAdd.tracks"))
454                .collect::<Result<Vec<_>, anyhow::Error>>()?;
455
456            Ok(Self {
457                at_index: value.at_index,
458                tracks,
459            })
460        }
461    }
462
463    /// Data for requesting some tracks to be removed in the server
464    #[derive(Debug, Clone, PartialEq)]
465    pub struct PlaylistRemoveTrackIndexed {
466        pub at_index: u64,
467        pub tracks: Vec<PlaylistTrackSource>,
468    }
469
470    impl PlaylistRemoveTrackIndexed {
471        #[must_use]
472        pub fn new_single(at_index: u64, track: PlaylistTrackSource) -> Self {
473            Self {
474                at_index,
475                tracks: vec![track],
476            }
477        }
478
479        #[must_use]
480        pub fn new_vec(at_index: u64, tracks: Vec<PlaylistTrackSource>) -> Self {
481            Self { at_index, tracks }
482        }
483    }
484
485    impl From<PlaylistRemoveTrackIndexed> for protobuf::PlaylistTracksToRemoveIndexed {
486        fn from(value: PlaylistRemoveTrackIndexed) -> Self {
487            Self {
488                at_index: value.at_index,
489                tracks: value.tracks.into_iter().map(Into::into).collect(),
490            }
491        }
492    }
493
494    impl TryFrom<protobuf::PlaylistTracksToRemoveIndexed> for PlaylistRemoveTrackIndexed {
495        type Error = anyhow::Error;
496
497        fn try_from(value: protobuf::PlaylistTracksToRemoveIndexed) -> Result<Self, Self::Error> {
498            let tracks = value
499                .tracks
500                .into_iter()
501                .map(|v| {
502                    PlaylistTrackSource::try_from(v).context("PlaylistTracksToRemoveIndexed.tracks")
503                })
504                .collect::<Result<Vec<_>, anyhow::Error>>()?;
505
506            Ok(Self {
507                at_index: value.at_index,
508                tracks,
509            })
510        }
511    }
512
513    /// Data for requesting some tracks to be removed in the server
514    #[derive(Debug, Clone, PartialEq)]
515    pub enum PlaylistRemoveTrackType {
516        Indexed(PlaylistRemoveTrackIndexed),
517        Clear,
518    }
519
520    type PToRemoveTypes = protobuf::playlist_tracks_to_remove::Type;
521
522    impl From<PlaylistRemoveTrackType> for protobuf::PlaylistTracksToRemove {
523        fn from(value: PlaylistRemoveTrackType) -> Self {
524            Self {
525                r#type: Some(match value {
526                    PlaylistRemoveTrackType::Indexed(v) => PToRemoveTypes::Indexed(v.into()),
527                    PlaylistRemoveTrackType::Clear => {
528                        PToRemoveTypes::Clear(PlaylistTracksToRemoveClear {})
529                    }
530                }),
531            }
532        }
533    }
534
535    impl TryFrom<protobuf::PlaylistTracksToRemove> for PlaylistRemoveTrackType {
536        type Error = anyhow::Error;
537
538        fn try_from(value: protobuf::PlaylistTracksToRemove) -> Result<Self, Self::Error> {
539            let value = unwrap_msg(value.r#type, "PlaylistTracksToRemove.type")?;
540
541            Ok(match value {
542                PToRemoveTypes::Indexed(v) => Self::Indexed(v.try_into()?),
543                PToRemoveTypes::Clear(_) => Self::Clear,
544            })
545        }
546    }
547
548    /// Data for requesting some tracks to be swapped in the server
549    #[derive(Debug, Clone, PartialEq)]
550    pub struct PlaylistSwapTrack {
551        pub index_a: u64,
552        pub index_b: u64,
553    }
554
555    impl From<PlaylistSwapTrack> for protobuf::PlaylistSwapTracks {
556        fn from(value: PlaylistSwapTrack) -> Self {
557            Self {
558                index_a: value.index_a,
559                index_b: value.index_b,
560            }
561        }
562    }
563
564    impl TryFrom<protobuf::PlaylistSwapTracks> for PlaylistSwapTrack {
565        type Error = anyhow::Error;
566
567        fn try_from(value: protobuf::PlaylistSwapTracks) -> Result<Self, Self::Error> {
568            Ok(Self {
569                index_a: value.index_a,
570                index_b: value.index_b,
571            })
572        }
573    }
574
575    /// Data for requesting to skip / play a specific track
576    #[derive(Debug, Clone, PartialEq)]
577    pub struct PlaylistPlaySpecific {
578        pub track_index: u64,
579        pub id: PlaylistTrackSource,
580    }
581
582    impl From<PlaylistPlaySpecific> for protobuf::PlaylistPlaySpecific {
583        fn from(value: PlaylistPlaySpecific) -> Self {
584            Self {
585                track_index: value.track_index,
586                id: Some(value.id.into()),
587            }
588        }
589    }
590
591    impl TryFrom<protobuf::PlaylistPlaySpecific> for PlaylistPlaySpecific {
592        type Error = anyhow::Error;
593
594        fn try_from(value: protobuf::PlaylistPlaySpecific) -> Result<Self, Self::Error> {
595            Ok(Self {
596                track_index: value.track_index,
597                id: unwrap_msg(value.id, "PlaylistPlaySpecific.id").and_then(|v| {
598                    PlaylistTrackSource::try_from(v).context("PlaylistPlaySpecific.id")
599                })?,
600            })
601        }
602    }
603}