selene-daemon 0.1.0

Official music player daemon for Selene
Documentation
use std::{
    collections::VecDeque,
    fmt::Display,
    sync::mpsc::{Sender, channel},
};

use blake3::Hash;
use lunar_lib::trace;
use rand::{rng, seq::SliceRandom};
use serde::{Deserialize, Serialize};
use symphonia::core::errors::{Error as SymphoniaError, SeekErrorKind};

use crate::{
    IpcActionError, IpcCommand, PlayerEvent, PlayerResponse,
    daemon::DaemonError,
    player::{OpenedDecoder, Player},
    playlist::{LoopMode, Playable, PlayableTrack, ShuffleMode},
};

#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum PlayerCommand {
    // Playback
    SetPlaying(bool),
    TogglePlaying,
    Stop,
    TracklistSeek {
        index: isize,
        increment: bool,
    },
    Seek {
        seconds: f64,
        increment: bool,
    },
    SetVolume {
        volume: f32,
        increment: bool,
    },

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

    // The playlist
    PlaylistExtend(Vec<Playable>),
    PlaylistSet {
        playables: Vec<Playable>,
        expected_state: Hash,
    },
    PlaylistClear,

    // Queries
    GetState,
    GetPlaying,
    PlaylistGetLoopMode,
    Flush,
    PlaylistSetLoopMode {
        loop_mode: LoopMode,
    },
    TracklistRebuild,
    GetTime,
    GetVolume,
    QueueGet,
    PlaylistGet,
    PlaylistSetShuffleMode {
        shuffle_mode: ShuffleMode,
    },
    PlaylistGetShuffleMode,
    TracklistGet,
    PlaylistGetState,
    QueueGetState,
    GetPlayingConnectorId,
    GetIsPlaying,
    Play {
        playable: Playable,
    },
}

impl Display for PlayerCommand {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PlayerCommand::SetPlaying(_) => f.write_str("SetPlaying"),
            PlayerCommand::TogglePlaying => f.write_str("TogglePlaying"),
            PlayerCommand::Stop => f.write_str("Stop"),
            PlayerCommand::TracklistSeek { .. } => f.write_str("TracklistSeek"),
            PlayerCommand::Seek { .. } => f.write_str("Seek"),
            PlayerCommand::SetVolume { .. } => f.write_str("SetVolume"),
            PlayerCommand::QueueExtend(_) => f.write_str("QueueExtend"),
            PlayerCommand::QueueSet { .. } => f.write_str("QueueSet"),
            PlayerCommand::QueueShuffle => f.write_str("QueueShuffle"),
            PlayerCommand::QueueClear => f.write_str("QueueClear"),
            PlayerCommand::PlaylistExtend(_) => f.write_str("PlaylistExtend"),
            PlayerCommand::PlaylistSet { .. } => f.write_str("PlaylistSet"),
            PlayerCommand::PlaylistClear => f.write_str("PlaylistClear"),
            PlayerCommand::GetState => f.write_str("GetState"),
            PlayerCommand::GetPlaying => f.write_str("GetPlaying"),
            PlayerCommand::PlaylistGetLoopMode => f.write_str("PlaylistGetLoopMode"),
            PlayerCommand::Flush => f.write_str("Flush"),
            PlayerCommand::PlaylistSetLoopMode { .. } => f.write_str("PlaylistSetLoopMode"),
            PlayerCommand::TracklistRebuild => f.write_str("TracklistRebuild"),
            PlayerCommand::GetTime => f.write_str("GetTime"),
            PlayerCommand::GetVolume => f.write_str("GetVolume"),
            PlayerCommand::QueueGet => f.write_str("QueueGet"),
            PlayerCommand::PlaylistGet => f.write_str("PlaylistGet"),
            PlayerCommand::PlaylistSetShuffleMode { .. } => f.write_str("PlaylistSetShuffleMode"),
            PlayerCommand::PlaylistGetShuffleMode => f.write_str("PlaylistGetShuffleMode"),
            PlayerCommand::TracklistGet => f.write_str("TracklistGet"),
            PlayerCommand::PlaylistGetState => f.write_str("PlaylistGetState"),
            PlayerCommand::QueueGetState => f.write_str("QueueGetState"),
            PlayerCommand::GetPlayingConnectorId => f.write_str("GetPlayingConnectorId"),
            PlayerCommand::GetIsPlaying => f.write_str("GetIsPlaying"),
            PlayerCommand::Play { .. } => f.write_str("Play"),
        }
    }
}

impl PlayerCommand {
    pub fn from_ipc(value: IpcCommand) -> Option<Self> {
        let command = match value {
            IpcCommand::SetPlaying(playing) => PlayerCommand::SetPlaying(playing),
            IpcCommand::TogglePlaying => PlayerCommand::TogglePlaying,
            IpcCommand::TracklistSeek { index, increment } => {
                PlayerCommand::TracklistSeek { index, increment }
            }
            IpcCommand::Seek { seconds, increment } => PlayerCommand::Seek { seconds, increment },
            IpcCommand::SetVolume { volume, increment } => {
                PlayerCommand::SetVolume { volume, increment }
            }
            IpcCommand::QueueExtend(playables) => PlayerCommand::QueueExtend(playables),
            IpcCommand::QueueSet {
                tracks: new_state,
                expected_state,
            } => PlayerCommand::QueueSet {
                tracks: new_state,
                expected_state,
            },
            IpcCommand::QueueShuffle => PlayerCommand::QueueShuffle,
            IpcCommand::QueueClear => PlayerCommand::QueueClear,
            IpcCommand::PlaylistExtend(playables) => PlayerCommand::PlaylistExtend(playables),
            IpcCommand::PlaylistSet {
                playables: new_state,
                expected_state,
            } => PlayerCommand::PlaylistSet {
                playables: new_state,
                expected_state,
            },
            IpcCommand::PlaylistClear => PlayerCommand::PlaylistClear,
            IpcCommand::GetState => PlayerCommand::GetState,
            IpcCommand::GetPlaying => PlayerCommand::GetPlaying,
            IpcCommand::GetTime => PlayerCommand::GetTime,
            IpcCommand::GetVolume => PlayerCommand::GetVolume,
            IpcCommand::QueueGet => PlayerCommand::QueueGet,
            IpcCommand::PlaylistGet => PlayerCommand::PlaylistGet,
            IpcCommand::PlaylistSetShuffleMode { shuffle_mode } => {
                PlayerCommand::PlaylistSetShuffleMode { shuffle_mode }
            }
            IpcCommand::PlaylistGetShuffleMode => PlayerCommand::PlaylistGetShuffleMode,
            IpcCommand::TracklistRebuild => PlayerCommand::TracklistRebuild,
            IpcCommand::PlaylistSetLoopMode { loop_mode } => {
                PlayerCommand::PlaylistSetLoopMode { loop_mode }
            }
            IpcCommand::PlaylistGetLoopMode => PlayerCommand::PlaylistGetLoopMode,
            IpcCommand::Flush => PlayerCommand::Flush,
            IpcCommand::TracklistGet => PlayerCommand::TracklistGet,
            IpcCommand::QueueGetState => PlayerCommand::QueueGetState,
            IpcCommand::PlaylistGetState => PlayerCommand::PlaylistGetState,
            IpcCommand::Play { playable } => PlayerCommand::Play { playable },
            IpcCommand::Stop => PlayerCommand::Stop,
            IpcCommand::Disconnect => return None,
        };
        Some(command)
    }
}

pub enum PlayerRequest {
    Quit,
    Mpris {
        command: Box<PlayerCommand>,
    },
    Ipc {
        command: Box<PlayerCommand>,
        callback: Sender<Result<PlayerResponse, IpcActionError>>,
    },
}

pub trait PlayerTx {
    fn request_ipc(&self, command: PlayerCommand) -> Result<PlayerResponse, IpcActionError>;

    fn request_mpris(&self, command: PlayerCommand);

    fn quit(&self);
}

impl PlayerTx for Sender<PlayerRequest> {
    fn request_ipc(&self, command: PlayerCommand) -> Result<PlayerResponse, IpcActionError> {
        let command_display = command.to_string();
        trace!("IPC Request: Sending command '{command_display}' to the player");

        let (tx, rx) = channel();
        self.send(PlayerRequest::Ipc {
            command: Box::new(command),
            callback: tx,
        })
        .map_err(|_| IpcActionError::Disconnected)?;
        let response = rx
            .recv()
            .map_err(|_| IpcActionError::Disconnected)
            .flatten();

        trace!(
            "Player responded to '{command_display}': {response}",
            response = response
                .as_ref()
                .map(PlayerResponse::to_string)
                .unwrap_or_else(IpcActionError::to_string)
        );

        response
    }

    fn request_mpris(&self, command: PlayerCommand) {
        trace!("MPRIS Request: Sending command '{command}' to the player");

        let _ = self.send(PlayerRequest::Mpris {
            command: Box::new(command),
        });
    }

    fn quit(&self) {
        let _ = self.send(PlayerRequest::Quit);
    }
}

impl Player {
    pub(crate) fn run_command(
        &mut self,
        command: PlayerCommand,
    ) -> Result<Result<PlayerResponse, IpcActionError>, DaemonError> {
        match command {
            PlayerCommand::Flush => (),
            PlayerCommand::GetState => todo!(),
            PlayerCommand::SetPlaying(playing) => self.set_is_playing(playing),
            PlayerCommand::GetIsPlaying => {
                return Ok(Ok(PlayerResponse::IsPlaying(self.is_playing)));
            }
            PlayerCommand::TogglePlaying => {
                self.set_is_playing(!self.is_playing);
                return Ok(Ok(PlayerResponse::IsPlaying(self.is_playing)));
            }
            PlayerCommand::TracklistSeek { index, increment } => {
                let seeked_to = self.playlist.tracklist_seek(index, increment);

                self.replace_decoders()?;

                return Ok(Ok(PlayerResponse::TracklistPosition {
                    index: seeked_to,
                    max: self.playlist.tracklist().len(),
                }));
            }
            PlayerCommand::PlaylistSet {
                expected_state,
                playables: new_state,
            } => {
                if expected_state == self.playlist.playlist_hash() {
                    self.playlist.set_playlist(new_state);
                } else {
                    return Ok(Err(IpcActionError::StateMismatch));
                }
            }
            PlayerCommand::PlaylistExtend(playable_groups) => {
                self.playlist.extend(playable_groups);
            }
            PlayerCommand::PlaylistGet => {
                return Ok(Ok(PlayerResponse::Playlist(
                    self.playlist.playlist().to_vec(),
                )));
            }
            PlayerCommand::PlaylistClear => {
                self.playlist.clear();
            }
            PlayerCommand::PlaylistSetShuffleMode { shuffle_mode } => {
                self.playlist.shuffle_mode = shuffle_mode;
                self.playlist.rebuild_tracklist();

                self.event(PlayerEvent::ShuffleModeChanged { shuffle_mode });

                return Ok(Ok(PlayerResponse::Tracklist(
                    self.playlist.tracklist().to_vec(),
                )));
            }
            PlayerCommand::PlaylistGetShuffleMode => {
                return Ok(Ok(PlayerResponse::ShuffleMode(self.playlist.shuffle_mode)));
            }
            PlayerCommand::PlaylistGetLoopMode => {
                return Ok(Ok(PlayerResponse::LoopMode(self.playlist.loop_mode)));
            }
            PlayerCommand::PlaylistSetLoopMode { loop_mode } => {
                self.playlist.loop_mode = loop_mode;
                self.event(PlayerEvent::LoopModeChanged { loop_mode });
            }
            PlayerCommand::QueueSet {
                expected_state,
                tracks,
            } => {
                if expected_state == self.playlist.queue_hash() {
                    self.playlist.queue = tracks;
                } else {
                    return Ok(Err(IpcActionError::StateMismatch));
                }
            }
            PlayerCommand::QueueGet => {
                return Ok(Ok(PlayerResponse::Queue(
                    self.playlist.queue.clone().into(),
                )));
            }
            PlayerCommand::QueueExtend(playable_groups) => {
                self.playlist.queue_extend(playable_groups);
            }
            PlayerCommand::QueueShuffle => {
                let mut shuffled: Vec<_> = self.playlist.queue.drain(..).collect();
                shuffled.shuffle(&mut rng());
                self.playlist.queue = shuffled.into();
            }
            PlayerCommand::QueueClear => self.playlist.queue.clear(),
            PlayerCommand::TracklistGet => {
                return Ok(Ok(PlayerResponse::Tracklist(
                    self.playlist.tracklist().to_vec(),
                )));
            }
            PlayerCommand::TracklistRebuild => {
                self.playlist.rebuild_tracklist();
                return Ok(Ok(PlayerResponse::Tracklist(
                    self.playlist.tracklist().to_vec(),
                )));
            }
            PlayerCommand::GetPlaying => {
                let playing = self
                    .decoder
                    .as_ref()
                    .map(|d| Box::new(d.decoded_from.clone()));
                return Ok(Ok(PlayerResponse::Playing(playing)));
            }
            PlayerCommand::Play { playable } => {
                self.playlist.set_playlist(std::iter::once(playable));
                self.replace_decoders()?;
                self.set_is_playing(true);
            }
            PlayerCommand::Stop => {
                self.decoder = None;
                self.event(PlayerEvent::CurrentlyPlayingChanged {
                    currently_playing: None,
                });
                self.set_is_playing(false);
                self.preload = None;
            }
            PlayerCommand::SetVolume { volume, increment } => {
                let vol = self.set_volume(volume, increment);
                return Ok(Ok(PlayerResponse::Volume(vol)));
            }
            PlayerCommand::GetVolume => {
                return Ok(Ok(PlayerResponse::Volume(self.volume)));
            }
            PlayerCommand::Seek { seconds, increment } => {
                if let Some(opened_decoder) = &mut self.decoder {
                    match opened_decoder.seek(seconds, increment) {
                        Ok(time) => {
                            self.event(PlayerEvent::SeekOccured { time });
                            return Ok(Ok(PlayerResponse::Time(Some(time))));
                        }
                        Err(SymphoniaError::SeekError(SeekErrorKind::OutOfRange)) => {
                            self.consume_preload()?;
                            return Ok(Ok(PlayerResponse::Time(None)));
                        }
                        Err(other) => return Err(other.into()),
                    };
                };
            }
            PlayerCommand::GetTime => {
                return Ok(Ok(PlayerResponse::Time(
                    self.decoder.as_ref().map(OpenedDecoder::time),
                )));
            }
            PlayerCommand::PlaylistGetState => {
                return Ok(Ok(PlayerResponse::PlaylistState(
                    self.playlist.playlist_hash(),
                )));
            }
            PlayerCommand::QueueGetState => {
                return Ok(Ok(PlayerResponse::QueueState(self.playlist.queue_hash())));
            }
            PlayerCommand::GetPlayingConnectorId => {
                todo!()
            }
        }
        trace!("Player finished running command");
        Ok(Ok(PlayerResponse::Done))
    }
}