#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::multiple_crate_versions)]
#![doc = include_str!("../README.md")]
use std::sync::LazyLock;
use moosicbox_audio_zone_models::{ApiPlayer, Player};
use moosicbox_json_utils::{ParseError, ToValueType, database::ToValue as _};
use moosicbox_music_models::{PlaybackQuality, api::ApiTrack};
use serde::{Deserialize, Serialize};
use strum_macros::{AsRefStr, EnumString};
use switchy_database::{AsId, DatabaseValue};
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct SetSessionAudioZone {
pub session_id: u64,
pub audio_zone_id: u64,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct CreateSession {
pub name: String,
pub audio_zone_id: Option<u64>,
pub playlist: CreateSessionPlaylist,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct CreateSessionPlaylist {
pub tracks: Vec<u64>,
}
#[derive(Debug, Serialize, Deserialize, Clone, EnumString, AsRefStr, PartialEq, Eq)]
#[serde(tag = "type")]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub enum PlaybackTarget {
#[serde(rename_all = "camelCase")]
AudioZone {
audio_zone_id: u64,
},
#[serde(rename_all = "camelCase")]
ConnectionOutput {
connection_id: String,
output_id: String,
},
}
const DEFAULT_AUDIO_ZONE: PlaybackTarget = PlaybackTarget::AudioZone { audio_zone_id: 0 };
static DEFAULT_CONNECTION_OUTPUT: LazyLock<PlaybackTarget> =
LazyLock::new(|| PlaybackTarget::ConnectionOutput {
connection_id: String::new(),
output_id: String::new(),
});
impl PlaybackTarget {
pub fn default_from_str(r#type: &str) -> Option<Self> {
if DEFAULT_AUDIO_ZONE.as_ref() == r#type {
Some(DEFAULT_AUDIO_ZONE)
} else if DEFAULT_CONNECTION_OUTPUT.as_ref() == r#type {
Some(DEFAULT_CONNECTION_OUTPUT.clone())
} else {
None
}
}
}
impl Default for PlaybackTarget {
fn default() -> Self {
Self::AudioZone { audio_zone_id: 0 }
}
}
impl From<ApiPlaybackTarget> for PlaybackTarget {
fn from(value: ApiPlaybackTarget) -> Self {
match value {
ApiPlaybackTarget::AudioZone { audio_zone_id } => Self::AudioZone { audio_zone_id },
ApiPlaybackTarget::ConnectionOutput {
connection_id,
output_id,
} => Self::ConnectionOutput {
connection_id,
output_id,
},
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct UpdateSession {
pub session_id: u64,
pub profile: String,
pub playback_target: PlaybackTarget,
#[serde(skip_serializing_if = "Option::is_none")]
pub play: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub playing: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub position: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seek: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub volume: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub playlist: Option<UpdateSessionPlaylist>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<PlaybackQuality>,
}
impl UpdateSession {
#[must_use]
pub const fn playback_updated(&self) -> bool {
self.play.is_some()
|| self.stop.is_some()
|| self.active.is_some()
|| self.playing.is_some()
|| self.position.is_some()
|| self.volume.is_some()
|| self.seek.is_some()
|| self.playlist.is_some()
}
}
impl From<ApiUpdateSession> for UpdateSession {
fn from(value: ApiUpdateSession) -> Self {
Self {
session_id: value.session_id,
profile: value.profile,
playback_target: value.playback_target.into(),
play: value.play,
stop: value.stop,
name: value.name,
active: value.active,
playing: value.playing,
position: value.position,
seek: value.seek,
volume: value.volume,
playlist: value.playlist.map(Into::into),
quality: value.quality,
}
}
}
impl From<UpdateSession> for ApiUpdateSession {
fn from(value: UpdateSession) -> Self {
Self {
session_id: value.session_id,
profile: value.profile,
playback_target: value.playback_target.into(),
play: value.play,
stop: value.stop,
name: value.name,
active: value.active,
playing: value.playing,
position: value.position,
seek: value.seek,
volume: value.volume,
playlist: value.playlist.map(Into::into),
quality: value.quality,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct UpdateSessionPlaylist {
pub session_playlist_id: u64,
pub tracks: Vec<ApiTrack>,
}
impl From<UpdateSessionPlaylist> for ApiUpdateSessionPlaylist {
fn from(value: UpdateSessionPlaylist) -> Self {
Self {
session_playlist_id: value.session_playlist_id,
tracks: value.tracks,
}
}
}
impl From<ApiUpdateSessionPlaylist> for UpdateSessionPlaylist {
fn from(value: ApiUpdateSessionPlaylist) -> Self {
Self {
session_playlist_id: value.session_playlist_id,
tracks: value.tracks,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, EnumString, AsRefStr, PartialEq, Eq)]
#[serde(tag = "type")]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
pub enum ApiPlaybackTarget {
#[serde(rename_all = "camelCase")]
AudioZone {
audio_zone_id: u64,
},
#[serde(rename_all = "camelCase")]
ConnectionOutput {
connection_id: String,
output_id: String,
},
}
impl Default for ApiPlaybackTarget {
fn default() -> Self {
Self::AudioZone { audio_zone_id: 0 }
}
}
impl From<PlaybackTarget> for ApiPlaybackTarget {
fn from(value: PlaybackTarget) -> Self {
match value {
PlaybackTarget::AudioZone { audio_zone_id } => Self::AudioZone { audio_zone_id },
PlaybackTarget::ConnectionOutput {
connection_id,
output_id,
} => Self::ConnectionOutput {
connection_id,
output_id,
},
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct ApiUpdateSession {
pub session_id: u64,
pub profile: String,
pub playback_target: ApiPlaybackTarget,
#[serde(skip_serializing_if = "Option::is_none")]
pub play: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub playing: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub position: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seek: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub volume: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub playlist: Option<ApiUpdateSessionPlaylist>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<PlaybackQuality>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct ApiUpdateSessionPlaylist {
pub session_playlist_id: u64,
pub tracks: Vec<ApiTrack>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct DeleteSession {
pub session_id: u64,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct Session {
pub id: u64,
pub name: String,
pub active: bool,
pub playing: bool,
pub position: Option<u16>,
pub seek: Option<f64>,
pub volume: Option<f64>,
pub playback_target: Option<PlaybackTarget>,
pub playlist: SessionPlaylist,
}
impl ToValueType<Session> for &switchy_database::Row {
fn to_value_type(self) -> Result<Session, ParseError> {
let playback_target_type: Option<String> = self.to_value("playback_target")?;
let playback_target_type =
playback_target_type.and_then(|x| PlaybackTarget::default_from_str(&x));
Ok(Session {
id: self.to_value("id")?,
name: self.to_value("name")?,
active: self.to_value("active")?,
playing: self.to_value("playing")?,
position: self.to_value("position")?,
#[allow(clippy::cast_precision_loss)]
seek: self.to_value::<Option<i64>>("seek")?.map(|x| x as f64),
volume: self.to_value("volume")?,
playback_target: match playback_target_type {
Some(PlaybackTarget::AudioZone { .. }) => Some(PlaybackTarget::AudioZone {
audio_zone_id: self.to_value("audio_zone_id")?,
}),
Some(PlaybackTarget::ConnectionOutput { .. }) => {
Some(PlaybackTarget::ConnectionOutput {
connection_id: self.to_value("connection_id")?,
output_id: self.to_value("output_id")?,
})
}
None => None,
},
..Default::default()
})
}
}
impl AsId for Session {
fn as_id(&self) -> DatabaseValue {
#[allow(clippy::cast_possible_wrap)]
DatabaseValue::Int64(self.id as i64)
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ApiSession {
pub session_id: u64,
pub name: String,
pub active: bool,
pub playing: bool,
pub position: Option<u16>,
pub seek: Option<f64>,
pub volume: Option<f64>,
pub playback_target: Option<PlaybackTarget>,
pub playlist: ApiSessionPlaylist,
}
impl From<Session> for ApiSession {
fn from(value: Session) -> Self {
Self {
session_id: value.id,
name: value.name,
active: value.active,
playing: value.playing,
position: value.position,
seek: value.seek,
volume: value.volume,
playback_target: value.playback_target,
playlist: value.playlist.into(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct SessionPlaylist {
pub id: u64,
pub tracks: Vec<ApiTrack>,
}
impl ToValueType<SessionPlaylist> for &switchy_database::Row {
fn to_value_type(self) -> Result<SessionPlaylist, ParseError> {
Ok(SessionPlaylist {
id: self.to_value("id")?,
..Default::default()
})
}
}
#[derive(Debug)]
pub struct SessionPlaylistTracks(
pub Vec<ApiTrack>,
);
impl AsId for SessionPlaylist {
fn as_id(&self) -> DatabaseValue {
#[allow(clippy::cast_possible_wrap)]
DatabaseValue::Int64(self.id as i64)
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ApiSessionPlaylist {
pub session_playlist_id: u64,
pub tracks: Vec<ApiTrack>,
}
impl From<SessionPlaylist> for ApiSessionPlaylist {
fn from(value: SessionPlaylist) -> Self {
Self {
session_playlist_id: value.id,
tracks: value.tracks,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct RegisterConnection {
pub connection_id: String,
pub name: String,
pub players: Vec<RegisterPlayer>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct Connection {
pub id: String,
pub name: String,
pub created: String,
pub updated: String,
pub players: Vec<Player>,
}
impl ToValueType<Connection> for &switchy_database::Row {
fn to_value_type(self) -> Result<Connection, ParseError> {
Ok(Connection {
id: self.to_value("id")?,
name: self.to_value("name")?,
created: self.to_value("created")?,
updated: self.to_value("updated")?,
..Default::default()
})
}
}
impl AsId for Connection {
fn as_id(&self) -> DatabaseValue {
DatabaseValue::String(self.id.clone())
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ApiConnection {
pub connection_id: String,
pub name: String,
pub alive: bool,
pub players: Vec<ApiPlayer>,
}
impl From<Connection> for ApiConnection {
fn from(value: Connection) -> Self {
Self {
connection_id: value.id,
name: value.name,
alive: false,
players: value.players.into_iter().map(Into::into).collect(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct RegisterPlayer {
pub audio_output_id: String,
pub name: String,
}