Skip to main content

heos/data/
source.rs

1//! Data types representing music sources.
2//!
3//! A music source is anything that can provide music to HEOS players.
4//!
5//! Built-in music sources include:
6//!  * Local USB Media / Local DLNA servers
7//!  * HEOS Playlists
8//!  * HEOS History
9//!  * HEOS aux inputs
10//!  * HEOS Favorites
11//!
12//! As of CLI spec v1.17, these are the 3rd-party music services that HEOS supports through the CLI:
13//!  * Pandora
14//!  * Rhapsody
15//!  * TuneIn
16//!  * Deezer
17//!  * Napster
18//!  * iHeartRadio
19//!  * Sirius XM
20//!  * Soundcloud
21//!  * Tidal
22//!  * Amazon Music
23
24use educe::Educe;
25use serde::{Deserialize, Serialize};
26use std::fmt::{Debug, Formatter};
27use std::str::FromStr;
28use strum::EnumString;
29use url::Url;
30
31use super::*;
32
33#[derive(Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)]
34#[serde(from = "i64", into = "i64")]
35pub enum SourceId {
36    Pandora,
37    Rhapsody,
38    TuneIn,
39    Spotify,
40    Deezer,
41    Napster,
42    IHeartRadio,
43    SiriusXm,
44    Soundcloud,
45    Tidal,
46    AmazonMusic,
47    Moodmix,
48    QQMusic,
49    Qobuz,
50    LocalUsbOrDlna,
51    HeosPlaylists,
52    HeosHistory,
53    HeosAuxInputs,
54    HeosFavorites,
55    Unknown(i64),
56}
57
58impl Debug for SourceId {
59    #[inline]
60    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
61        write!(f, "SourceId({:?})", i64::from(*self))
62    }
63}
64
65impl Display for SourceId {
66    #[inline]
67    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
68        Display::fmt(&i64::from(*self), f)
69    }
70}
71
72impl From<i64> for SourceId {
73    #[inline]
74    fn from(value: i64) -> Self {
75        match value {
76            1 => Self::Pandora,
77            2 => Self::Rhapsody,
78            3 => Self::TuneIn,
79            4 => Self::Spotify,
80            5 => Self::Deezer,
81            6 => Self::Napster,
82            7 => Self::IHeartRadio,
83            8 => Self::SiriusXm,
84            9 => Self::Soundcloud,
85            10 => Self::Tidal,
86            13 => Self::AmazonMusic,
87            15 => Self::Moodmix,
88            18 => Self::QQMusic,
89            30 => Self::Qobuz,
90            1024 => Self::LocalUsbOrDlna,
91            1025 => Self::HeosPlaylists,
92            1026 => Self::HeosHistory,
93            1027 => Self::HeosAuxInputs,
94            1028 => Self::HeosFavorites,
95            value => Self::Unknown(value),
96        }
97    }
98}
99
100impl From<SourceId> for i64 {
101    #[inline]
102    fn from(value: SourceId) -> Self {
103        match value {
104            SourceId::Pandora => 1,
105            SourceId::Rhapsody => 2,
106            SourceId::TuneIn => 3,
107            SourceId::Spotify => 4,
108            SourceId::Deezer => 5,
109            SourceId::Napster => 6,
110            SourceId::IHeartRadio => 7,
111            SourceId::SiriusXm => 8,
112            SourceId::Soundcloud => 9,
113            SourceId::Tidal => 10,
114            SourceId::AmazonMusic => 13,
115            SourceId::Moodmix => 15,
116            SourceId::QQMusic => 18,
117            SourceId::Qobuz => 30,
118            SourceId::LocalUsbOrDlna => 1024,
119            SourceId::HeosPlaylists => 1025,
120            SourceId::HeosHistory => 1026,
121            SourceId::HeosAuxInputs => 1027,
122            SourceId::HeosFavorites => 1028,
123            SourceId::Unknown(value) => value,
124        }
125    }
126}
127
128impl FromStr for SourceId {
129    type Err = ParseIntError;
130
131    #[inline]
132    fn from_str(s: &str) -> Result<Self, Self::Err> {
133        let value: i64 = s.parse()?;
134        Ok(value.into())
135    }
136}
137
138impl SourceId {
139    #[inline]
140    pub fn cli_browse(&self) -> bool {
141        match self {
142            Self::Pandora |
143            Self::Rhapsody |
144            Self::TuneIn |
145            Self::Deezer |
146            Self::Napster |
147            Self::IHeartRadio |
148            Self::SiriusXm |
149            Self::Soundcloud |
150            Self::Tidal |
151            Self::AmazonMusic |
152            Self::LocalUsbOrDlna |
153            Self::HeosPlaylists |
154            Self::HeosHistory |
155            Self::HeosAuxInputs |
156            Self::HeosFavorites => true,
157            _ => false,
158        }
159    }
160
161    #[inline]
162    pub fn cli_search(&self) -> bool {
163        match self {
164            Self::Rhapsody |
165            Self::TuneIn |
166            Self::Deezer |
167            Self::Napster |
168            Self::Soundcloud |
169            Self::Tidal |
170            Self::Qobuz |
171            Self::LocalUsbOrDlna |
172            Self::HeosFavorites => true,
173            _ => false,
174        }
175    }
176
177    #[inline]
178    pub fn cli_new_station(&self) -> bool {
179        match self {
180            Self::Pandora |
181            Self::Rhapsody |
182            Self::TuneIn |
183            Self::Deezer |
184            Self::Napster |
185            Self::IHeartRadio |
186            Self::Soundcloud |
187            Self::Tidal => true,
188            _ => false,
189        }
190    }
191}
192
193/// Broad category that a source belongs to.
194#[derive(Serialize, Deserialize, EnumString, strum::Display, Debug, Clone, Copy, PartialEq, Eq)]
195#[serde(into = "String", try_from = "String")]
196#[strum(serialize_all = "snake_case")]
197pub enum SourceType {
198    /// Third-party music services.
199    MusicService,
200    /// Built-in HEOS services.
201    HeosService,
202    /// Built-in HEOS servers.
203    HeosServer,
204    /// DLNA servers.
205    DlnaServer,
206}
207impl_enum_string_conversions!(SourceType);
208
209/// Status of source availability.
210#[derive(Serialize, Deserialize, EnumString, strum::Display, Debug, Clone, Copy, PartialEq, Eq)]
211#[serde(into = "String", try_from = "String")]
212#[strum(serialize_all = "lowercase")]
213pub enum SourceAvailable {
214    /// The source is available to play from.
215    ///
216    /// For music services, this means that an account is logged-in and associated with the music
217    /// service for it to be usable.
218    True,
219    /// The source is NOT available to play from.
220    False,
221}
222impl_enum_string_conversions!(SourceAvailable);
223
224/// Information about a specific music source.
225#[derive(Serialize, Deserialize, Educe, Clone)]
226#[educe(Debug)]
227pub struct SourceInfo {
228    /// Name of the source.
229    pub name: String,
230    /// URL of an image that can be used to represent the source.
231    #[educe(Debug(method(std::fmt::Display::fmt)))]
232    pub image_url: Url,
233    /// Broad category the source belongs to.
234    #[serde(rename = "type")]
235    pub source_type: SourceType,
236    /// ID of the source.
237    #[serde(rename = "sid")]
238    pub source_id: SourceId,
239    /// Whether the source is available to play music from.
240    pub available: SourceAvailable,
241    /// Username associated with the source, if the source is a music service and there is an
242    /// account logged-in with the music service.
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub service_username: Option<String>,
245}
246impl_try_from_response_payload!(SourceInfo);
247impl_try_from_response_payload!(Vec<SourceInfo>);
248
249#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
250#[serde(from = "i64", into = "i64")]
251pub enum CriteriaId {
252    Artist,
253    Album,
254    Track,
255    Station,
256    Shows,
257    Playlist,
258    Accounts,
259    Unknown(i64),
260}
261
262impl Debug for CriteriaId {
263    #[inline]
264    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
265        write!(f, "CriteriaId({:?})", i64::from(*self))
266    }
267}
268
269impl Display for CriteriaId {
270    #[inline]
271    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
272        Display::fmt(&i64::from(*self), f)
273    }
274}
275
276impl From<i64> for CriteriaId {
277    #[inline]
278    fn from(value: i64) -> Self {
279        match value {
280            1 => Self::Artist,
281            2 => Self::Album,
282            3 => Self::Track,
283            4 => Self::Station,
284            5 => Self::Shows,
285            6 => Self::Playlist,
286            7 => Self::Accounts,
287            value => Self::Unknown(value),
288        }
289    }
290}
291
292impl From<CriteriaId> for i64 {
293    #[inline]
294    fn from(value: CriteriaId) -> Self {
295        match value {
296            CriteriaId::Artist => 1,
297            CriteriaId::Album => 2,
298            CriteriaId::Track => 3,
299            CriteriaId::Station => 4,
300            CriteriaId::Shows => 5,
301            CriteriaId::Playlist => 6,
302            CriteriaId::Accounts => 7,
303            CriteriaId::Unknown(value) => value,
304        }
305    }
306}
307
308impl FromStr for CriteriaId {
309    type Err = ParseIntError;
310
311    #[inline]
312    fn from_str(s: &str) -> Result<Self, Self::Err> {
313        let value: i64 = s.parse()?;
314        Ok(value.into())
315    }
316}
317
318/// Criteria to use to search by.
319///
320/// This is used by some services when searching in order to determine what media types are being
321/// searched. The `name` corresponds to the type of media, and the `criteria` is the media type ID.
322/// For example, if a search criteria result has the name "Artist", then the `criteria` can be used
323/// with [Search](crate::command::browse::Search) commands to search for artists.
324#[derive(Serialize, Deserialize, Debug, Clone)]
325pub struct SearchCriteria {
326    /// Name of the criteria.
327    ///
328    /// This can be e.g. Artist/Album/Track/Station.
329    pub name: String,
330    /// ID to be used with [Search](crate::command::browse::Search) commands to search for this
331    /// media type.
332    #[serde(rename = "scid")]
333    pub criteria: CriteriaId,
334    /// Do searches of this type support wildcards ('*').
335    #[serde(default)]
336    #[serde(with = "super::yes_no")]
337    pub wildcard: bool,
338    /// Are searches of this type directly playable by adding the search results to the queue.
339    #[serde(default)]
340    #[serde(with = "super::yes_no")]
341    pub playable: bool,
342    /// If present, this should be prefixed to search strings when searching for media of this type.
343    #[serde(rename = "cid")]
344    #[serde(default)]
345    pub search_prefix: Option<String>,
346}
347impl_try_from_response_payload!(Vec<SearchCriteria>);