heos 0.3.1

Rust bindings for HEOS ecosystem API
Documentation
//! Data types representing HEOS players.
//!
//! A HEOS player is a single device that can play music.

use educe::Educe;
use qstring::QString;
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use std::str::FromStr;
use strum::EnumString;

use super::*;
use crate::command::CommandError;
use crate::data::group::GroupId;

id_type! {
    /// ID representing a HEOS player.
    ///
    /// This ID can be any number, and is generated by the HEOS system, making it effectively
    /// opaque.
    pub struct PlayerId(pub i64);
}
impl_try_from_qs!(PlayerId, "pid");

/// The method a player is connected to the local network with.
#[derive(Serialize, Deserialize, EnumString, strum::Display, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(into = "String", try_from = "String")]
#[strum(serialize_all = "lowercase")]
pub enum NetworkType {
    /// The player is physically connected with ethernet.
    Wired,
    /// The player is connected wirelessly to a Wi-Fi network.
    WiFi,
    /// The player is somehow getting connectivity in a way it doesn't recognize.
    Unknown,
}
impl_enum_string_conversions!(NetworkType);

/// How the LineOut level is controlled.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(try_from = "i64", into = "i64")]
pub enum LineOutLevelType {
    /// There is no LineOut connection.
    None,
    /// The LineOut level control is variable, and could be multiple types.
    Variable,
    /// The LineOut level control is a fixed type, as specified by [LineOutLevelControlType].
    Fixed,
}

impl TryFrom<i64> for LineOutLevelType {
    type Error = String;

    #[inline]
    fn try_from(value: i64) -> Result<Self, Self::Error> {
        match value {
            0 => Ok(LineOutLevelType::None),
            1 => Ok(LineOutLevelType::Variable),
            2 => Ok(LineOutLevelType::Fixed),
            other => Err(format!("Unknown line_out type: '{other}'")),
        }
    }
}

impl From<LineOutLevelType> for i64 {
    #[inline]
    fn from(value: LineOutLevelType) -> Self {
        match value {
            LineOutLevelType::None => 0,
            LineOutLevelType::Variable => 1,
            LineOutLevelType::Fixed => 2,
        }
    }
}

/// How the LineOut connection is controlled for [fixed](LineOutLevelType::Fixed) types.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(try_from = "i64", into = "i64")]
pub enum LineOutLevelControlType {
    /// There is no LineOut connection.
    None,
    /// The LineOut level is controlled via IR.
    IR,
    /// The LineOut level is controlled via physical controls.
    Trigger,
    /// The LineOut level is controlled via network communication.
    Network,
}

impl TryFrom<i64> for LineOutLevelControlType {
    type Error = String;

    #[inline]
    fn try_from(value: i64) -> Result<Self, Self::Error> {
        match value {
            0 | 1 => Ok(LineOutLevelControlType::None),
            2 => Ok(LineOutLevelControlType::IR),
            3 => Ok(LineOutLevelControlType::Trigger),
            4 => Ok(LineOutLevelControlType::Network),
            other => Err(format!("Unknown line_out_control type: '{other}'")),
        }
    }
}

impl From<LineOutLevelControlType> for i64 {
    #[inline]
    fn from(value: LineOutLevelControlType) -> Self {
        match value {
            LineOutLevelControlType::None => 1,
            LineOutLevelControlType::IR => 2,
            LineOutLevelControlType::Trigger => 3,
            LineOutLevelControlType::Network => 4,
        }
    }
}

/// Information about a specific player.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PlayerInfo {
    /// The user-friendly name of the player.
    pub name: String,
    /// ID of the player.
    #[serde(rename = "pid")]
    pub player_id: PlayerId,
    /// ID of the group this player belongs to, if it belongs to a group.
    #[serde(rename = "gid")]
    pub group_id: Option<GroupId>,
    /// HEOS device model.
    pub model: String,
    /// HEOS device version.
    pub version: String,
    /// IP address of this player.
    pub ip: IpAddr,
    /// The method this player uses to connect to the network.
    pub network: NetworkType,
    /// The type of LineOut level controls this player has.
    #[serde(rename = "lineout")]
    pub line_out: LineOutLevelType,
    /// If `line_out` is [fixed](LineOutLevelType::Fixed), this is the specific fixed type.
    #[serde(rename = "control")]
    pub line_out_control: Option<LineOutLevelControlType>,
    /// Serial number of this player, if it exists.
    pub serial: Option<String>,
}
impl_try_from_response_payload!(PlayerInfo);
impl_try_from_response_payload!(Vec<PlayerInfo>);

/// The state of a player with regard to playing/pausing music.
#[derive(Serialize, Deserialize, EnumString, strum::Display, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(into = "String", try_from = "String")]
#[strum(serialize_all = "lowercase")]
pub enum PlayState {
    /// The player is currently playing music.
    Play,
    /// The player is currently paused.
    Pause,
    /// The player is currently stopped.
    Stop,
}
impl_enum_string_conversions!(PlayState);
impl_try_from_qs!(PlayState, "state");
impl_try_from_response_qs!(PlayState);

/// The repeat setting of a player.
#[derive(Serialize, Deserialize, EnumString, strum::Display, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(into = "String", try_from = "String")]
#[strum(serialize_all = "lowercase")]
pub enum RepeatMode {
    /// After the player's queue is finished, it will repeat from the beginning of the queue.
    #[strum(serialize = "on_all")]
    All,
    /// The player will repeat the currently playing track.
    #[strum(serialize = "on_one")]
    One,
    /// The player will not repeat anything.
    Off,
}
impl_enum_string_conversions!(RepeatMode);
impl_try_from_qs!(RepeatMode, "repeat");
impl_try_from_response_qs!(RepeatMode);

/// The shuffle setting of a player.
#[derive(Serialize, Deserialize, EnumString, strum::Display, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(into = "String", try_from = "String")]
#[strum(serialize_all = "lowercase")]
pub enum ShuffleMode {
    /// The player will choose a random track from the queue to play next.
    On,
    /// The player will play tracks from the queue in order.
    Off,
}
impl_enum_string_conversions!(ShuffleMode);
impl_try_from_qs!(ShuffleMode, "shuffle");
impl_try_from_response_qs!(ShuffleMode);

/// Combination of repeat/shuffle settings for a player.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PlayMode {
    pub repeat: RepeatMode,
    pub shuffle: ShuffleMode,
}
impl_try_from_response_qs!(PlayMode);

impl TryFrom<QString> for PlayMode {
    type Error = CommandError;

    #[inline]
    fn try_from(qs: QString) -> Result<Self, Self::Error> {
        let repeat = RepeatMode::try_from(&qs)?;
        let shuffle = ShuffleMode::try_from(qs)?;
        Ok(Self {
            repeat,
            shuffle,
        })
    }
}

/// Information on whether a new software update is available for a player.
#[derive(Serialize, Deserialize, EnumString, strum::Display, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(into = "String", try_from = "String")]
pub enum UpdateAvailable {
    /// The player is up-to-date.
    #[strum(serialize = "update_none")]
    None,
    /// A new software update exists for the player.
    #[strum(serialize = "update_exist")]
    Exists,
}
impl_enum_string_conversions!(UpdateAvailable);

/// Payload response when checking for a new software update.
///
/// This exists in order to properly deserialize the response, but the actual update information is
/// stored in `update`, which can be easily unwrapped via `into()` or field access.
#[derive(Serialize, Deserialize, Educe, Debug, Clone, Copy)]
#[educe(Into(UpdateAvailable))]
pub struct UpdatePayload {
    pub update: UpdateAvailable,
}
impl_try_from_response_payload!(UpdatePayload);

/// The method that is used to add new media to the queue.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(into = "i64", try_from = "i64")]
pub enum AddToQueueType {
    /// The new media will replace the currently playing track.
    PlayNow,
    /// The new media will be put at the beginning of the queue, and will be played next.
    PlayNext,
    /// The new media will be put at the end of the queue.
    AddToEnd,
    /// The queue will be cleared, and the new media will replace the queue entirely.
    ReplaceAndPlay,
}

#[derive(thiserror::Error, Debug, Clone)]
pub enum AddToQueueTypeParseError {
    #[error("unknown type id '{0}'")]
    UnknownId(i64),
    #[error(transparent)]
    ParseIntError(#[from] ParseIntError),
}

impl TryFrom<i64> for AddToQueueType {
    type Error = AddToQueueTypeParseError;

    fn try_from(value: i64) -> Result<Self, Self::Error> {
        match value {
            1 => Ok(AddToQueueType::PlayNow),
            2 => Ok(AddToQueueType::PlayNext),
            3 => Ok(AddToQueueType::AddToEnd),
            4 => Ok(AddToQueueType::ReplaceAndPlay),
            value => Err(AddToQueueTypeParseError::UnknownId(value)),
        }
    }
}

impl From<AddToQueueType> for i64 {
    #[inline]
    fn from(value: AddToQueueType) -> Self {
        match value {
            AddToQueueType::PlayNow => 1,
            AddToQueueType::PlayNext => 2,
            AddToQueueType::AddToEnd => 3,
            AddToQueueType::ReplaceAndPlay => 4,
        }
    }
}

impl FromStr for AddToQueueType {
    type Err = AddToQueueTypeParseError;

    #[inline]
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let value: i64 = s.parse()?;
        let aid = Self::try_from(value)?;
        Ok(aid)
    }
}