selene-daemon 0.8.2

Official music player daemon for Selene
Documentation
use std::{
    fs, io,
    sync::{Arc, atomic::Ordering, mpsc::Sender},
};

use lunar_lib::{
    database::{DatabaseError, EntryIdIteratorExt},
    trace, warn,
};
use selene_core::{
    database::LibraryDb,
    library::{
        collectable::Collectable,
        track::{Track, TrackId},
    },
    state_dir,
};
use serde::{Deserialize, Serialize};

use crate::{
    LoopMode, ShuffleMode,
    event_handler::EventTx,
    player::{Player, PlayerEvent},
    playlist::{Playable, PlaylistRng},
};

#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct PlayerState {
    pub volume: f32,

    pub shuffle_mode: ShuffleMode,
    pub loop_mode: LoopMode,

    pub playlist_rng: PlaylistRng,
    pub playlist: Vec<Collectable>,

    pub tracklist: Vec<TrackId>,
    pub tracklist_index: Option<usize>,
}

impl Default for PlayerState {
    fn default() -> Self {
        Self {
            volume: 1.0,
            shuffle_mode: ShuffleMode::None,
            loop_mode: LoopMode::Loop,
            playlist_rng: PlaylistRng::new(),
            playlist: Vec::new(),
            tracklist: Vec::new(),
            tracklist_index: None,
        }
    }
}

impl PlayerState {
    pub fn load() -> PlayerState {
        let state_file = state_dir().join("playlist_state");
        let bytes = match fs::read(&state_file) {
            Ok(bytes) => bytes,
            Err(err) => {
                warn!(
                    "Failed to read state file '{file}': {err}",
                    file = state_file.display()
                );
                return PlayerState::default();
            }
        };

        match postcard::from_bytes(&bytes) {
            Ok(state) => {
                trace!("PlayerState loaded from file");
                state
            }
            Err(err) => {
                warn!(
                    "Failed to read state file '{file}': {err}",
                    file = state_file.display()
                );
                PlayerState::default()
            }
        }
    }

    pub fn save(player: &Player) -> io::Result<()> {
        let state = PlayerState {
            volume: f32::from_bits(player.volume.load(Ordering::Relaxed)),
            shuffle_mode: player.playlist.shuffle_mode(),
            loop_mode: player.playlist.loop_mode(),
            playlist_rng: player.playlist.rng(),
            playlist: player
                .playlist
                .playlist()
                .iter()
                .map(Playable::to_collectable)
                .collect(),
            tracklist: player.playlist.tracklist().iter().map(|t| t.id()).collect(),
            tracklist_index: player.playlist.position(),
        };

        let state_file = state_dir().join("playlist_state.cbor");

        fs::create_dir_all(state_dir())?;
        let writer = fs::OpenOptions::new()
            .write(true)
            .truncate(true)
            .create(true)
            .open(state_file)?;

        let _ = postcard::to_io(&state, writer);

        Ok(())
    }

    pub fn trigger_events(&self, event_tx: &Sender<PlayerEvent>) {
        event_tx.event(PlayerEvent::VolumeChanged {
            volume: self.volume,
        });

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

        event_tx.event(PlayerEvent::LoopModeChanged {
            loop_mode: self.loop_mode,
        });

        event_tx.event(PlayerEvent::PlaylistChanged {
            playlist: self.playlist.clone(),
        });
        event_tx.event(PlayerEvent::TracklistChanged {
            tracklist: self.tracklist.clone(),
            position: self.tracklist_index,
        });
    }

    pub fn playlist_tracklist(
        &self,
        db: &LibraryDb,
    ) -> Result<(Vec<Playable>, Vec<Arc<Track>>), DatabaseError> {
        let playlist = self
            .playlist
            .iter()
            .map(|c| Playable::from_collectable(*c, db))
            .collect::<Result<Vec<_>, _>>()?;

        let tracklist: Vec<Arc<Track>> = self.tracklist.iter().copied().cache_get_batch(db)?;

        Ok((playlist, tracklist))
    }
}