selene-daemon 0.5.7

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

use lunar_lib::{database::Database, debug, error, trace};
use selene_core::{database::LibraryDb, library::track::ResolvedTrack};

use crate::{
    SHUTDOWN,
    decoder::{DecoderEvent, DecoderHandle},
    event_handler::EventHandler,
    player::{Player, PlayerError, PlayerRequest, playback::DeviceConfig},
    playlist::{AtomicPlaybackStatus, PlaybackStatus, Playlist},
    wait,
};

// Thread start impls
impl Player {
    fn new() -> Result<(Sender<PlayerRequest>, Self), PlayerError> {
        let (player_tx, rx) = channel();

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

        let volume = Arc::new(AtomicU32::new(1.0_f32.to_bits()));
        let playback_state = Arc::new(AtomicPlaybackStatus::new(PlaybackStatus::Stopped));
        let playlist = Playlist::new(event_tx.clone());
        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(),
        )?;

        Ok((
            player_tx,
            Self {
                rx,
                decoder_tx,

                event_tx,

                playlist,
                device_config,
                playback_state,

                volume,
            },
        ))
    }

    /// Opens a new [`Player`] and runs it on a new thread
    pub fn open() -> Result<Sender<PlayerRequest>, PlayerError> {
        let (player_tx, player) = Self::new()?;

        thread::spawn(move || {
            if let Err(err) = player.run() {
                error!("Engine failed with error: {err}");
            }
            SHUTDOWN.store(true, Ordering::Relaxed);
        });

        Ok(player_tx)
    }

    /// 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");

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

            loop {
                match self.rx.try_recv() {
                    Ok(PlayerRequest::Command(command)) => self.run_command(*command)?,
                    Ok(PlayerRequest::DecoderEvent(DecoderEvent::PreloadConsumed)) => {
                        let db = LibraryDb::open().unwrap();
                        self.playlist.pop_next(&db)?;
                        if let Some(peek_next) = self.playlist.peek_next(&db)? {
                            self.preload(peek_next)?;
                        }
                    }
                    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(());
                    }
                }
            }

            wait();
            continue;
        }
    }
}

fn scrobble(track: Arc<ResolvedTrack>, start_time: u64) {
    trace!(
        "Received 'scrobble' for: {} | time: {}",
        track.track.metadata.safe_title(),
        start_time
    );

    #[cfg(feature = "lastfm")]
    {
        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, start_time, true).await {
                    error!("LastFM scrobble error: {err}")
                }
            });
        }
    }
}

fn now_playing(track: Arc<ResolvedTrack>) {
    trace!(
        "Received 'now_playing' for: {}",
        track.track.metadata.safe_title()
    );

    #[cfg(feature = "lastfm")]
    {
        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).await {
                    error!("LastFM now_playing error: {err}")
                }
            });
        }
    }
}