use std::{
fmt::Display,
io,
sync::mpsc::{Sender, channel},
};
use blake3::Hash;
use lunar_lib::config::ConfigError;
use selene_core::library::{collection::Collectable, track::TrackId};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use thiserror::Error;
use crate::{
daemon::unix_socket_handle::CallbackFn,
player::PlayerQueryFlags,
playlist::{LoopMode, ResolvedTrack, ShuffleMode},
};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum IpcCommand {
Flush,
Disconnect,
ReloadConfig,
Play {
collectable: Collectable,
},
Stop,
SetIsPlaying {
is_playing: bool,
},
TogglePlaying,
Seek {
seconds: f64,
increment: bool,
},
SetVolume {
volume: f32,
increment: bool,
},
QueueSet {
tracks: Vec<Collectable>,
expected_state: Hash,
},
QueueExtend(Vec<Collectable>),
QueueShuffle,
QueueClear,
PlaylistSet {
collectables: Vec<Collectable>,
expected_state: Hash,
},
PlaylistExtend(Vec<Collectable>),
PlaylistClear,
PlaylistSetShuffleMode {
shuffle_mode: ShuffleMode,
},
TracklistRebuild,
PlaylistSetLoopMode {
loop_mode: LoopMode,
},
TracklistSeek {
index: isize,
increment: bool,
},
Next,
Previous,
GetState {
flags: PlayerQueryFlags,
},
}
impl IpcCommand {
#[must_use]
pub fn responds(&self) -> bool {
matches!(
self,
IpcCommand::Flush
| IpcCommand::TogglePlaying
| IpcCommand::Seek { .. }
| IpcCommand::SetVolume { .. }
| IpcCommand::QueueSet { .. }
| IpcCommand::PlaylistSet { .. }
| IpcCommand::TracklistSeek { .. }
| IpcCommand::GetState { .. }
)
}
}
pub struct IpcRequest {
pub command: IpcCommand,
pub callback: Option<CallbackFn>,
}
pub trait IpcTx {
fn no_response(&self, command: IpcCommand) -> Result<(), PacketError>;
fn request<T: DeserializeOwned + Send + 'static>(
&self,
command: IpcCommand,
) -> Result<T, PacketError>;
fn action(&self, command: IpcCommand);
fn disconnect(&self);
}
impl IpcTx for Sender<IpcRequest> {
fn no_response(&self, command: IpcCommand) -> Result<(), PacketError> {
self.send(IpcRequest {
command,
callback: None,
})
.unwrap();
Ok(())
}
fn request<T: DeserializeOwned + Send + 'static>(
&self,
command: IpcCommand,
) -> Result<T, PacketError> {
assert!(
command.responds(),
"Commands must always respond with request()"
);
let (tx, rx) = channel();
let callback: CallbackFn = Box::new(move |result| {
let _ = tx.send(result.map(|bytes| {
ciborium::from_reader::<T, &[u8]>(bytes).expect("Daemon sent invalid bytes")
}));
});
self.send(IpcRequest {
command,
callback: Some(Box::new(callback)),
})
.unwrap();
rx.recv().unwrap()
}
fn action(&self, command: IpcCommand) {
assert!(
!command.responds(),
"Commands must never respond with action()"
);
self.send(IpcRequest {
command,
callback: None,
})
.unwrap();
}
fn disconnect(&self) {
let _ = self.send(IpcRequest {
command: IpcCommand::Disconnect,
callback: None,
});
}
}
#[repr(u8)]
pub enum PacketType {
Unknown,
Event,
Response,
Error,
Disconnect,
}
#[derive(Debug, Error, Serialize, Deserialize, Clone, Copy)]
pub enum PacketError {
#[error("Packet size '{size}' too large: Max size is {max_size}")]
PacketTooLarge { size: usize, max_size: usize },
#[error("Client was disconnected while waiting for a packet")]
Disconnect,
}
impl From<u8> for PacketType {
fn from(value: u8) -> Self {
match value {
1 => Self::Event,
2 => Self::Response,
3 => Self::Error,
4 => Self::Disconnect,
_ => Self::Unknown,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum PlayerEvent {
CurrentlyPlayingChanged {
currently_playing: Box<ResolvedTrack>,
},
PlaybackIsPlayingChanged {
is_playing: bool,
changed_at: f64,
},
PlaybackStopped,
ShuffleModeChanged {
shuffle_mode: ShuffleMode,
},
LoopModeChanged {
loop_mode: LoopMode,
},
VolumeChanged {
volume: f32,
},
SeekOccured {
time: f64,
},
QueueChanged {
queue: Vec<TrackId>,
},
PlaylistChanged {
playlist: Vec<Collectable>,
},
TracklistChanged {
tracklist: Vec<TrackId>,
},
Shutdown,
}
impl Display for PlayerEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PlayerEvent::CurrentlyPlayingChanged { .. } => f.write_str("CurrentlyPlayingChanged"),
PlayerEvent::PlaybackIsPlayingChanged { .. } => f.write_str("PlaybackIsPlayingChanged"),
PlayerEvent::PlaybackStopped => f.write_str("PlaybackStopped"),
PlayerEvent::ShuffleModeChanged { .. } => f.write_str("ShuffleModeChanged"),
PlayerEvent::LoopModeChanged { .. } => f.write_str("LoopModeChanged"),
PlayerEvent::VolumeChanged { .. } => f.write_str("VolumeChanged"),
PlayerEvent::SeekOccured { .. } => f.write_str("SeekOccured"),
PlayerEvent::QueueChanged { .. } => f.write_str("QueueChanged"),
PlayerEvent::PlaylistChanged { .. } => f.write_str("PlaylistChanged"),
PlayerEvent::TracklistChanged { .. } => f.write_str("TracklistChanged"),
PlayerEvent::Shutdown => f.write_str("Shutdown"),
}
}
}
#[derive(Debug)]
pub enum ConnectErrorKind {
DaemonNotRunning,
ConnectionRefused,
FailedToLoadConfig(String),
Other(io::Error),
}
impl Display for ConnectErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConnectErrorKind::DaemonNotRunning => f.write_str("The daemon is not running"),
ConnectErrorKind::ConnectionRefused => {
f.write_str("The daemon listener thread halted and must be restarted")
}
ConnectErrorKind::FailedToLoadConfig(string) => string.fmt(f),
ConnectErrorKind::Other(error) => error.fmt(f),
}
}
}
#[derive(Debug, Error)]
pub enum IpcHandleError {
#[error("Failed to connect: {0}")]
FailedToConnect(ConnectErrorKind),
#[error("The handling thread cannot be communicated with")]
HandleDied,
#[error("The current platform is not supported")]
UnsupportedPlatform,
}
impl From<ConfigError> for IpcHandleError {
fn from(value: ConfigError) -> Self {
Self::FailedToConnect(ConnectErrorKind::FailedToLoadConfig(value.to_string()))
}
}