selene-daemon 0.1.0

Official music player daemon for Selene
Documentation
use std::sync::mpsc::{RecvError, SendError, Sender, channel};

use blake3::Hash;
use rubato::ResamplerConstructionError;
use symphonia::core::errors::Error as SymphoniaError;
use thiserror::Error;

use crate::{
    IpcActionError, IpcCommand, IpcHandleError, PlayerEvent, PlayerResponse,
    player::playback::CpalError,
    playlist::{LoopMode, Playable, PlayableTrack, ShuffleMode, TracklistTrack},
};

// Critical Errors
#[derive(Debug, Error)]
pub enum DaemonError {
    #[error("{0}")]
    Io(#[from] std::io::Error),

    #[error("{0}")]
    FailedToSerialize(#[from] ciborium::ser::Error<std::io::Error>),

    #[error("A MPSC sender closed unexpectedly")]
    ReceiverDisconnected,

    #[error("A MPSC receiever closed unexpectedly")]
    SenderDisconnected,

    #[error("{0}")]
    Cpal(#[from] CpalError),

    #[error("{0}")]
    Symphonia(#[from] SymphoniaError),

    #[error("{0}")]
    Resampler(#[from] ResamplerConstructionError),
}

impl From<RecvError> for DaemonError {
    fn from(_value: RecvError) -> Self {
        Self::ReceiverDisconnected
    }
}

impl<T> From<SendError<T>> for DaemonError {
    fn from(_value: SendError<T>) -> Self {
        Self::SenderDisconnected
    }
}

// Interface stuff
pub struct SeleneDaemon {
    handle_tx: Sender<IpcRequest>,
}

// Daemon impls
impl SeleneDaemon {
    /// Create a new connection to the daemon
    ///
    /// # Errors
    ///
    /// This function will error if:
    /// - There is no [`DaemonHandle`] configured for the current system
    /// - The connection could not be established (May be because the daemon is not running)
    pub fn connect<F>(callback: Option<F>) -> Result<SeleneDaemon, IpcHandleError>
    where
        Self: Sized,
        F: FnMut(PlayerEvent) + Send + Sync + 'static,
    {
        #[cfg(unix)]
        {
            Ok(Self {
                handle_tx: unix_socket_handle::UnixSocketHandle::connect(callback)?,
            })
        }

        #[cfg(not(any(unix)))]
        {
            Err(IpcHandleError::UnsupportedPlatform)
        }
    }

    fn send_ipc(&self, command: IpcCommand) -> Result<PlayerResponse, IpcActionError> {
        self.handle_tx.request(command)
    }

    fn send_ipc_action(&self, command: IpcCommand) -> Result<(), IpcActionError> {
        match self.send_ipc(command) {
            Ok(PlayerResponse::Done) => Ok(()),
            Err(err) => Err(err),
            response => panic!("Ipc action received unexpected response: {response:?}"),
        }
    }

    fn send_ipc_action_infallible(&self, command: IpcCommand) -> Result<(), IpcActionError> {
        match self.send_ipc(command) {
            Ok(PlayerResponse::Done) => Ok(()),
            Err(IpcActionError::Disconnected) => Err(IpcActionError::Disconnected),
            response => panic!("Ipc action received unexpected response: {response:?}"),
        }
    }

    pub fn disconnect(&self) {
        let _ = self.handle_tx.request(IpcCommand::Disconnect);
    }
}

// Helper impls
impl SeleneDaemon {
    // Generic
    pub fn ipc_flush(&self) -> Result<(), IpcActionError> {
        self.send_ipc_action_infallible(IpcCommand::Flush)
    }

    pub fn ipc_play(&self, playable: Playable) -> Result<(), IpcActionError> {
        self.send_ipc_action_infallible(IpcCommand::Play { playable })
    }

    pub fn ipc_stop(&self) -> Result<(), IpcActionError> {
        self.send_ipc_action_infallible(IpcCommand::Stop)
    }

    // IsPlaying
    pub fn ipc_set_playing(&self, playing: bool) -> Result<(), IpcActionError> {
        self.send_ipc_action_infallible(IpcCommand::SetPlaying(playing))
    }

    pub fn ipc_toggle_playing(&self) -> Result<bool, IpcActionError> {
        match self.send_ipc(IpcCommand::TogglePlaying) {
            Ok(PlayerResponse::IsPlaying(playing)) => Ok(playing),
            Err(IpcActionError::Disconnected) => Err(IpcActionError::Disconnected),
            response => panic!("Ipc action received unexpected response: {response:?}"),
        }
    }

    pub fn ipc_get_playing(&self) -> Result<bool, IpcActionError> {
        match self.send_ipc(IpcCommand::GetPlaying) {
            Ok(PlayerResponse::IsPlaying(playing)) => Ok(playing),
            Err(IpcActionError::Disconnected) => Err(IpcActionError::Disconnected),
            response => panic!("Ipc action received unexpected response: {response:?}"),
        }
    }

    // Seek
    pub fn ipc_seek(&self, seconds: f64, increment: bool) -> Result<Option<f64>, IpcActionError> {
        match self.send_ipc(IpcCommand::Seek { seconds, increment }) {
            Ok(PlayerResponse::Time(time)) => Ok(time),
            Err(IpcActionError::NothingPlaying) => Err(IpcActionError::NothingPlaying),
            Err(IpcActionError::Disconnected) => Err(IpcActionError::Disconnected),
            response => panic!("Ipc action received unexpected response: {response:?}"),
        }
    }

    // Volume
    pub fn ipc_set_volume(&self, volume: f32, increment: bool) -> Result<f32, IpcActionError> {
        match self.send_ipc(IpcCommand::SetVolume { volume, increment }) {
            Ok(PlayerResponse::Volume(vol)) => Ok(vol),
            Err(IpcActionError::Disconnected) => Err(IpcActionError::Disconnected),
            response => panic!("Ipc action received unexpected response: {response:?}"),
        }
    }

    // Queue
    pub fn ipc_queue_extend(
        &self,
        playables: impl IntoIterator<Item = Playable>,
    ) -> Result<(), IpcActionError> {
        self.send_ipc_action_infallible(IpcCommand::QueueExtend(playables.into_iter().collect()))
    }

    pub fn ipc_queue_set(
        &self,
        expected_state: Hash,
        tracks: impl IntoIterator<Item = PlayableTrack>,
    ) -> Result<(), IpcActionError> {
        self.send_ipc_action(IpcCommand::QueueSet {
            tracks: tracks.into_iter().collect(),
            expected_state,
        })
    }

    pub fn ipc_queue_get(&self) -> Result<Vec<PlayableTrack>, IpcActionError> {
        match self.send_ipc(IpcCommand::QueueGet) {
            Ok(PlayerResponse::Queue(queue)) => Ok(queue),
            Err(IpcActionError::Disconnected) => Err(IpcActionError::Disconnected),
            response => panic!("Ipc action received unexpected response: {response:?}"),
        }
    }

    pub fn ipc_queue_shuffle(&self) -> Result<(), IpcActionError> {
        self.send_ipc_action_infallible(IpcCommand::QueueShuffle)
    }

    pub fn ipc_queue_clear(&self) -> Result<(), IpcActionError> {
        self.send_ipc_action_infallible(IpcCommand::QueueClear)
    }

    // Playlist
    pub fn ipc_playlist_clear(&self) -> Result<(), IpcActionError> {
        self.send_ipc_action_infallible(IpcCommand::PlaylistClear)
    }

    pub fn ipc_playlist_extend(
        &self,
        playables: impl IntoIterator<Item = Playable>,
    ) -> Result<(), IpcActionError> {
        self.send_ipc_action_infallible(IpcCommand::PlaylistExtend(playables.into_iter().collect()))
    }

    pub fn ipc_playlist_set(
        &self,
        expected_state: Hash,
        playables: impl IntoIterator<Item = Playable>,
    ) -> Result<(), IpcActionError> {
        self.send_ipc_action(IpcCommand::PlaylistSet {
            playables: playables.into_iter().collect(),
            expected_state,
        })
    }

    // Tracklist
    pub fn ipc_tracklist_seek(
        &self,
        index: isize,
        increment: bool,
    ) -> Result<(usize, usize), IpcActionError> {
        match self.send_ipc(IpcCommand::TracklistSeek { index, increment }) {
            Ok(PlayerResponse::TracklistPosition { index, max }) => Ok((index, max)),
            Err(IpcActionError::Disconnected) => Err(IpcActionError::Disconnected),
            response => panic!("Ipc action received unexpected response: {response:?}"),
        }
    }

    pub fn ipc_playlist_get(&self) -> Result<Vec<Playable>, IpcActionError> {
        match self.send_ipc(IpcCommand::PlaylistGet) {
            Ok(PlayerResponse::Playlist(playlist)) => Ok(playlist),
            Err(IpcActionError::Disconnected) => Err(IpcActionError::Disconnected),
            response => panic!("Ipc action received unexpected response: {response:?}"),
        }
    }

    pub fn ipc_set_shuffle_mode(&self, shuffle_mode: ShuffleMode) -> Result<(), IpcActionError> {
        self.send_ipc_action_infallible(IpcCommand::PlaylistSetShuffleMode { shuffle_mode })
    }

    pub fn ipc_get_shuffle_mode(&self) -> Result<ShuffleMode, IpcActionError> {
        match self.send_ipc(IpcCommand::PlaylistGetShuffleMode) {
            Ok(PlayerResponse::ShuffleMode(shuffle_mode)) => Ok(shuffle_mode),
            Err(IpcActionError::Disconnected) => Err(IpcActionError::Disconnected),
            response => panic!("Ipc action received unexpected response: {response:?}"),
        }
    }

    pub fn ipc_set_loop_mode(&self, loop_mode: LoopMode) -> Result<(), IpcActionError> {
        self.send_ipc_action_infallible(IpcCommand::PlaylistSetLoopMode { loop_mode })
    }

    pub fn ipc_get_loop_mode(&self) -> Result<LoopMode, IpcActionError> {
        match self.send_ipc(IpcCommand::PlaylistGetLoopMode) {
            Ok(PlayerResponse::LoopMode(loop_mode)) => Ok(loop_mode),
            Err(IpcActionError::Disconnected) => Err(IpcActionError::Disconnected),
            response => panic!("Ipc action received unexpected response: {response:?}"),
        }
    }

    pub fn ipc_tracklist_rebuild(&self) -> Result<(), IpcActionError> {
        self.send_ipc_action_infallible(IpcCommand::TracklistRebuild)
    }

    pub fn ipc_tracklist_get(&self) -> Result<Vec<TracklistTrack>, IpcActionError> {
        match self.send_ipc(IpcCommand::TracklistGet) {
            Ok(PlayerResponse::Tracklist(tracklist)) => Ok(tracklist),
            Err(IpcActionError::Disconnected) => Err(IpcActionError::Disconnected),
            response => panic!("Ipc action received unexpected response: {response:?}"),
        }
    }

    pub fn ipc_queue_get_state(&self) -> Result<Hash, IpcActionError> {
        match self.send_ipc(IpcCommand::QueueGetState) {
            Ok(PlayerResponse::QueueState(state)) => Ok(state),
            Err(IpcActionError::Disconnected) => Err(IpcActionError::Disconnected),
            response => panic!("Ipc action received unexpected response: {response:?}"),
        }
    }

    pub fn ipc_playlist_get_state(&self) -> Result<Hash, IpcActionError> {
        match self.send_ipc(IpcCommand::PlaylistGetState) {
            Ok(PlayerResponse::PlaylistState(state)) => Ok(state),
            Err(IpcActionError::Disconnected) => Err(IpcActionError::Disconnected),
            response => panic!("Ipc action received unexpected response: {response:?}"),
        }
    }
}

impl Drop for SeleneDaemon {
    fn drop(&mut self) {
        self.disconnect()
    }
}

pub enum IpcRequest {
    Ipc {
        command: Box<IpcCommand>,
        callback: Sender<Result<PlayerResponse, IpcActionError>>,
    },
    Reconnect {
        callback: Sender<bool>,
    },
}

pub trait IpcTx {
    fn request(&self, command: IpcCommand) -> Result<PlayerResponse, IpcActionError>;

    fn reconnect(&self) -> Result<bool, IpcHandleError>;
}

impl IpcTx for Sender<IpcRequest> {
    fn request(&self, command: IpcCommand) -> Result<PlayerResponse, IpcActionError> {
        let (tx, rx) = channel();
        self.send(IpcRequest::Ipc {
            command: Box::new(command),
            callback: tx,
        })
        .map_err(|_| IpcActionError::Disconnected)?;
        rx.recv()
            .map_err(|_| IpcActionError::Disconnected)
            .flatten()
    }

    fn reconnect(&self) -> Result<bool, IpcHandleError> {
        let (tx, rx) = channel();
        self.send(IpcRequest::Reconnect { callback: tx })
            .map_err(|_| IpcHandleError::HandleDied)?;
        rx.recv().map_err(|_| IpcHandleError::HandleDied)
    }
}

pub trait DaemonHandle<Command, Response>: Send {
    /// Connects to the daemon and runs the handle in its own thread
    fn connect<F>(callback: Option<F>) -> Result<Sender<IpcRequest>, IpcHandleError>
    where
        Self: Sized,
        F: FnMut(PlayerEvent) + Send + Sync + 'static;

    /// Attempts to reconnect to the daemon
    fn reconnect(&mut self) -> bool;

    /// Main handle logic. This should run forever and in its own thread
    fn run<F>(self, callback: Option<F>) -> Result<(), IpcHandleError>
    where
        F: FnMut(PlayerEvent) + Send + Sync + 'static;
}

#[cfg(unix)]
pub mod unix_socket_handle;