termusiclib/
player.rs

1#![allow(clippy::module_name_repetitions)]
2use anyhow::anyhow;
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
12// implement transform function for easy use
13impl From<protobuf::Duration> for std::time::Duration {
14    fn from(value: protobuf::Duration) -> Self {
15        std::time::Duration::new(value.secs, value.nanos)
16    }
17}
18
19impl From<std::time::Duration> for protobuf::Duration {
20    fn from(value: std::time::Duration) -> Self {
21        Self {
22            secs: value.as_secs(),
23            nanos: value.subsec_nanos(),
24        }
25    }
26}
27
28/// The primitive in which time (current position / total duration) will be stored as
29pub type PlayerTimeUnit = std::time::Duration;
30
31/// Struct to keep both values with a name, as tuples cannot have named fields
32#[derive(Debug, Clone, Copy, PartialEq)]
33pub struct PlayerProgress {
34    pub position: Option<PlayerTimeUnit>,
35    /// Total duration of the currently playing track, if there is a known total duration
36    pub total_duration: Option<PlayerTimeUnit>,
37}
38
39impl From<protobuf::PlayerTime> for PlayerProgress {
40    fn from(value: protobuf::PlayerTime) -> Self {
41        Self {
42            position: value.position.map(Into::into),
43            total_duration: value.total_duration.map(Into::into),
44        }
45    }
46}
47
48impl From<PlayerProgress> for protobuf::PlayerTime {
49    fn from(value: PlayerProgress) -> Self {
50        Self {
51            position: value.position.map(Into::into),
52            total_duration: value.total_duration.map(Into::into),
53        }
54    }
55}
56
57#[derive(Debug, Clone, PartialEq)]
58pub struct TrackChangedInfo {
59    /// Current track index in the playlist
60    pub current_track_index: u64,
61    /// Indicate if the track changed to another track
62    pub current_track_updated: bool,
63    /// Title of the current track / radio
64    pub title: Option<String>,
65    /// Current progress of the track
66    pub progress: Option<PlayerProgress>,
67}
68
69#[derive(Debug, Clone, PartialEq)]
70pub enum UpdateEvents {
71    MissedEvents { amount: u64 },
72    VolumeChanged { volume: u16 },
73    SpeedChanged { speed: i32 },
74    PlayStateChanged { playing: u32 },
75    TrackChanged(TrackChangedInfo),
76    GaplessChanged { gapless: bool },
77}
78
79type StreamTypes = protobuf::stream_updates::Type;
80
81// mainly for server to grpc
82impl From<UpdateEvents> for protobuf::StreamUpdates {
83    fn from(value: UpdateEvents) -> Self {
84        let val = match value {
85            UpdateEvents::MissedEvents { amount } => {
86                StreamTypes::MissedEvents(UpdateMissedEvents { amount })
87            }
88            UpdateEvents::VolumeChanged { volume } => {
89                StreamTypes::VolumeChanged(UpdateVolumeChanged {
90                    msg: Some(VolumeReply {
91                        volume: u32::from(volume),
92                    }),
93                })
94            }
95            UpdateEvents::SpeedChanged { speed } => StreamTypes::SpeedChanged(UpdateSpeedChanged {
96                msg: Some(SpeedReply { speed }),
97            }),
98            UpdateEvents::PlayStateChanged { playing } => {
99                StreamTypes::PlayStateChanged(UpdatePlayStateChanged {
100                    msg: Some(PlayState { status: playing }),
101                })
102            }
103            UpdateEvents::TrackChanged(info) => StreamTypes::TrackChanged(UpdateTrackChanged {
104                current_track_index: info.current_track_index,
105                current_track_updated: info.current_track_updated,
106                optional_title: info
107                    .title
108                    .map(protobuf::update_track_changed::OptionalTitle::Title),
109                progress: info.progress.map(Into::into),
110            }),
111            UpdateEvents::GaplessChanged { gapless } => {
112                StreamTypes::GaplessChanged(UpdateGaplessChanged {
113                    msg: Some(GaplessState { gapless }),
114                })
115            }
116        };
117
118        Self { r#type: Some(val) }
119    }
120}
121
122// mainly for grpc to client(tui)
123impl TryFrom<protobuf::StreamUpdates> for UpdateEvents {
124    type Error = anyhow::Error;
125
126    fn try_from(value: protobuf::StreamUpdates) -> Result<Self, Self::Error> {
127        let value = unwrap_msg(value.r#type, "StreamUpdates.type")?;
128
129        let res = match value {
130            StreamTypes::VolumeChanged(ev) => Self::VolumeChanged {
131                volume: clamp_u16(
132                    unwrap_msg(ev.msg, "StreamUpdates.types.volume_changed.msg")?.volume,
133                ),
134            },
135            StreamTypes::SpeedChanged(ev) => Self::SpeedChanged {
136                speed: unwrap_msg(ev.msg, "StreamUpdates.types.speed_changed.msg")?.speed,
137            },
138            StreamTypes::PlayStateChanged(ev) => Self::PlayStateChanged {
139                playing: unwrap_msg(ev.msg, "StreamUpdates.types.play_state_changed.msg")?.status,
140            },
141            StreamTypes::MissedEvents(ev) => Self::MissedEvents { amount: ev.amount },
142            StreamTypes::TrackChanged(ev) => Self::TrackChanged(TrackChangedInfo {
143                current_track_index: ev.current_track_index,
144                current_track_updated: ev.current_track_updated,
145                title: ev.optional_title.map(|v| {
146                    let protobuf::update_track_changed::OptionalTitle::Title(v) = v;
147                    v
148                }),
149                progress: ev.progress.map(Into::into),
150            }),
151            StreamTypes::GaplessChanged(ev) => Self::GaplessChanged {
152                gapless: unwrap_msg(ev.msg, "StreamUpdates.types.gapless_changed.msg")?.gapless,
153            },
154        };
155
156        Ok(res)
157    }
158}
159
160/// Easily unwrap a given grpc option and covert it to a result, with a location on None
161fn unwrap_msg<T>(opt: Option<T>, place: &str) -> Result<T, anyhow::Error> {
162    match opt {
163        Some(val) => Ok(val),
164        None => Err(anyhow!("Got \"None\" in grpc \"{place}\"!")),
165    }
166}
167
168#[allow(clippy::cast_possible_truncation)]
169fn clamp_u16(val: u32) -> u16 {
170    val.min(u32::from(u16::MAX)) as u16
171}