selene-daemon 0.4.2

Official music player daemon for Selene
Documentation
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 {
    // Generic
    Flush,
    Disconnect,
    ReloadConfig,

    // Playback
    Play {
        collectable: Collectable,
    },
    Stop,
    SetIsPlaying {
        is_playing: bool,
    },
    TogglePlaying,
    Seek {
        seconds: f64,
        increment: bool,
    },

    SetVolume {
        volume: f32,
        increment: bool,
    },

    // The override queue
    QueueSet {
        tracks: Vec<Collectable>,
        expected_state: Hash,
    },
    QueueExtend(Vec<Collectable>),
    QueueShuffle,
    QueueClear,

    // Playlist
    PlaylistSet {
        collectables: Vec<Collectable>,
        expected_state: Hash,
    },
    PlaylistExtend(Vec<Collectable>),
    PlaylistClear,

    // Playlist // Shuffle mode
    PlaylistSetShuffleMode {
        shuffle_mode: ShuffleMode,
    },
    TracklistRebuild,

    // Playlist // Loop mode
    PlaylistSetLoopMode {
        loop_mode: LoopMode,
    },

    // Tracklist
    TracklistSeek {
        index: isize,
        increment: bool,
    },
    Next,
    Previous,

    // Queries
    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 packet. Either the client is out of date, or the daemon has a logic bug
    Unknown,

    /// An event, can be sent without input
    Event,

    /// A response to a command
    Response,

    /// A [`PacketError`]
    Error,

    /// Client has been disconnected from the daemon. This can happen from a manual disconnect, or from the daemon shutting down
    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()))
    }
}