selene-daemon 0.8.2

Official music player daemon for Selene
Documentation
use std::{
    cell::LazyCell,
    sync::{
        Arc, Mutex,
        atomic::{AtomicBool, AtomicU32, Ordering},
        mpsc::{Sender, TryRecvError, channel},
    },
    time::Instant,
};

use lunar_lib::{database::DbHandle, debug};
use selene_core::database::LibraryDb;

use crate::{
    LoopMode, SHUTDOWN, STATE_CHANGED,
    config::daemon_config,
    decoder::{DecoderEvent, DecoderHandle},
    event_handler::EventHandler,
    player::{
        AtomicPlaybackStatus, PlaybackStatus, Player, PlayerError, PlayerRequest,
        playback::DeviceConfig, state::PlayerState,
    },
    playlist::{PlayingTrack, Playlist},
    wait,
};

// Thread start impls
impl Player {
    fn new() -> Result<(Sender<PlayerRequest>, Self), PlayerError> {
        let state = PlayerState::load();

        let (player_tx, rx) = channel();

        let event_tx = EventHandler::open(player_tx.clone())?;

        state.trigger_events(&event_tx);

        let volume = Arc::new(AtomicU32::new(state.volume.to_bits()));
        let playback_state = Arc::new(AtomicPlaybackStatus::new(PlaybackStatus::Stopped));
        let looping = Arc::new(AtomicBool::new(matches!(
            state.loop_mode,
            LoopMode::RepeatTrack
        )));

        let playlist = Playlist::new(event_tx.clone(), looping.clone(), &state)?;

        let device_config = Arc::new(Mutex::new(DeviceConfig::default_config()?));

        let decoder_tx = DecoderHandle::open(
            player_tx.clone(),
            event_tx.clone(),
            device_config.clone(),
            playback_state.clone(),
            volume.clone(),
            looping,
        )?;

        Ok((
            player_tx,
            Self {
                rx,
                decoder_tx,

                event_tx,

                playlist,
                device_config,
                playback_state,

                volume,
            },
        ))
    }

    /// Opens a new [`Player`] and runs it on the current thread
    pub fn start() -> Result<(), PlayerError> {
        let (_, player) = Self::new()?;
        player.run()?;
        SHUTDOWN.store(true, Ordering::Relaxed);
        Ok(())
    }

    fn run(mut self) -> Result<(), PlayerError> {
        debug!("Engine thread started");

        self.replace_decoders(false)?;

        let mut last_state_save = Instant::now();

        loop {
            if SHUTDOWN.load(Ordering::Relaxed) {
                return Ok(());
            }

            {
                let db = LazyCell::new(|| DbHandle::<LibraryDb>::open().unwrap());
                loop {
                    match self.rx.try_recv() {
                        Ok(PlayerRequest::Command(command)) => self.run_command(*command, &db)?,
                        Ok(PlayerRequest::DecoderEvent(DecoderEvent::PreloadConsumed)) => {
                            self.playlist.pop_next(&db)?;
                            self.try_preload(&db)?;
                        }
                        Ok(PlayerRequest::DecoderEvent(DecoderEvent::Scrobble {
                            track,
                            start_time,
                        })) => {
                            scrobble(track, start_time);
                        }
                        Ok(PlayerRequest::DecoderEvent(DecoderEvent::NowPlaying { track })) => {
                            now_playing(track);
                        }
                        Err(TryRecvError::Empty) => break,
                        Err(TryRecvError::Disconnected) => {
                            return Ok(());
                        }
                    }
                }
            }

            if STATE_CHANGED.load(Ordering::Relaxed)
                && last_state_save.elapsed().as_secs_f64() > 5.0
            {
                last_state_save = Instant::now();
                let _ = PlayerState::save(&self);
                STATE_CHANGED.store(false, Ordering::Release);
            }

            wait();
            continue;
        }
    }
}

#[allow(unused_variables)]
fn scrobble(track: PlayingTrack, start_time: u64) {
    if !daemon_config().main.scrobbling {
        return;
    }

    #[cfg(feature = "lastfm")]
    {
        use lunar_lib::error;
        use selene_core::scrobbling::lastfm::last_fm_client;

        if let Some(last_fm_client) = last_fm_client() {
            tokio::runtime::Handle::current().spawn(async move {
                if let Err(err) = last_fm_client
                    .scrobble(&track.source, start_time, true)
                    .await
                {
                    error!("LastFM scrobble error: {err}");
                }
            });
        }
    }
}

#[allow(unused_variables)]
fn now_playing(track: PlayingTrack) {
    if !daemon_config().main.scrobbling {
        return;
    }

    #[cfg(feature = "lastfm")]
    {
        use lunar_lib::error;
        use selene_core::scrobbling::lastfm::last_fm_client;

        if let Some(last_fm_client) = last_fm_client() {
            tokio::runtime::Handle::current().spawn(async move {
                if let Err(err) = last_fm_client.now_playing(&track.source).await {
                    error!("LastFM now_playing error: {err}");
                }
            });
        }
    }
}