selene-daemon 0.8.2

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

use blake3::Hash;
use lunar_lib::database::{DatabaseError, DbHandle};
use rand::seq::SliceRandom;
use selene_core::{database::LibraryDb, library::track::Track};

use crate::{
    LoopMode, ShuffleMode, changed_state,
    event_handler::EventTx,
    player::{PlayerError, PlayerEvent, PlayerState},
    playlist::{Playable, PlayingTrack, Playlist, PlaylistRng},
};

// Core impls
impl Playlist {
    #[must_use]
    pub fn new(
        event_tx: Sender<PlayerEvent>,
        looping: Arc<AtomicBool>,
        state: &PlayerState,
    ) -> Result<Self, PlayerError> {
        let db = DbHandle::<LibraryDb>::open()?;
        let (playlist, tracklist) = state.playlist_tracklist(&db)?;

        Ok(Self {
            event_tx,
            rng: PlaylistRng::new(),

            queue: VecDeque::new(),
            playlist,
            tracklist,
            tracklist_index: state.tracklist_index,

            shuffle_mode: ShuffleMode::None,
            loop_mode: LoopMode::None,
            looping,
        })
    }

    pub fn set_shuffle_mode(&mut self, shuffle_mode: ShuffleMode) {
        self.shuffle_mode = shuffle_mode;
        self.rebuild_tracklist();
        self.event_tx
            .event(PlayerEvent::ShuffleModeChanged { shuffle_mode });
        changed_state();
    }

    pub fn set_loop_mode(&mut self, loop_mode: LoopMode) {
        self.loop_mode = loop_mode;
        self.looping.store(
            matches!(loop_mode, LoopMode::RepeatTrack),
            Ordering::Relaxed,
        );
        self.event_tx
            .event(PlayerEvent::LoopModeChanged { loop_mode });
        changed_state();
    }
}

// Playlist
impl Playlist {
    /// Gets the current state of the playlist
    #[must_use]
    pub fn playlist(&self) -> &[Playable] {
        &self.playlist
    }

    /// Returns the hash of the playlist
    #[must_use]
    pub fn playlist_hash(&self) -> Hash {
        let mut hash_seed = Vec::with_capacity(self.playlist.len() * 32);
        for playable in &self.playlist {
            hash_seed.extend(playable.id_as_bytes());
        }
        blake3::hash(&hash_seed)
    }

    /// Sets the current state of the playlist
    pub fn set_playlist(&mut self, playlist: impl IntoIterator<Item = Playable>) {
        self.playlist = playlist.into_iter().collect();
        self.rebuild_tracklist();
    }

    /// Appends the input items to the end of the playlist and tracklist, shuffling them based off the current shuffle mode
    ///
    /// This function will reset the current RNG
    pub fn extend_playlist(&mut self, items: impl IntoIterator<Item = Playable>) {
        let items: Vec<Playable> = items.into_iter().collect();

        let mut rng = self.rng.current_rng();
        let mut tracklist_items: Vec<Arc<Track>> = items
            .iter()
            .flat_map(|p| p.flatten_shuffle(self.shuffle_mode, &mut rng))
            .collect();

        if matches!(self.shuffle_mode, ShuffleMode::Full) {
            tracklist_items.shuffle(&mut rng);
        }

        self.playlist.extend(items);
        self.tracklist.extend(tracklist_items);
        self.event_tx.event(PlayerEvent::TracklistChanged {
            tracklist: self.tracklist.iter().map(|t| t.id()).collect(),
            position: self.tracklist_index,
        });
        self.rng = PlaylistRng::new();
        changed_state();
    }
}

// Queue
impl Playlist {
    pub fn queue(&self) -> &VecDeque<Arc<Track>> {
        &self.queue
    }

    // Returns the hash of the queue
    #[must_use]
    pub fn queue_hash(&self) -> Hash {
        let mut hash_seed = Vec::with_capacity(self.queue.len() * 32);
        for track in &self.queue {
            hash_seed.extend(*track.id());
        }
        blake3::hash(&hash_seed)
    }

    pub fn set_queue<'a>(&mut self, items: impl IntoIterator<Item = &'a Playable>) {
        self.queue = items.into_iter().flat_map(Playable::flatten).collect();
    }

    pub fn shuffle_queue(&mut self) {
        let mut queue: Vec<Arc<Track>> = std::mem::take(&mut self.queue).into();
        queue.shuffle(&mut rand::rng());
        self.queue = queue.into();
    }

    pub fn clear_queue(&mut self) {
        self.queue.clear();
    }

    pub fn extend_queue<'a>(&mut self, with: impl IntoIterator<Item = &'a Playable>) {
        self.queue
            .extend(with.into_iter().flat_map(Playable::flatten));
    }
}

// Accessors
impl Playlist {
    #[must_use]
    pub fn position(&self) -> Option<usize> {
        self.tracklist_index
    }

    #[must_use]
    pub fn shuffle_mode(&self) -> ShuffleMode {
        self.shuffle_mode
    }

    #[must_use]
    pub fn loop_mode(&self) -> LoopMode {
        self.loop_mode
    }

    #[must_use]
    pub fn tracklist(&self) -> &[Arc<Track>] {
        &self.tracklist
    }

    #[must_use]
    pub fn rng(&self) -> PlaylistRng {
        self.rng
    }
}

// Navigation
impl Playlist {
    /// Returns the item at the current index
    pub fn current(&mut self, db: &LibraryDb) -> Result<Option<PlayingTrack>, DatabaseError> {
        let Some(index) = self.position() else {
            return Ok(None);
        };

        let track = PlayingTrack::from_tracklist(self.tracklist[index].clone(), index, db)?;

        Ok(Some(track))
    }

    /// Peeks the next item in the track, returning its index, and if its part of a new cycle
    fn peek_next_raw(&self) -> Option<(usize, bool)> {
        if self.tracklist.is_empty() {
            return None;
        }
        let len = self.tracklist.len();

        let result = match self.loop_mode {
            LoopMode::None | LoopMode::RepeatTrack => {
                let next_i = self.tracklist_index.map_or(0, |i| i + 1);
                if next_i >= len {
                    return None;
                }
                (next_i, false)
            }
            LoopMode::Loop => (self.tracklist_index.map_or(0, |i| (i + 1) % len), false),
            LoopMode::LoopAndReshuffle => {
                let next_i = self.tracklist_index.map_or(0, |i| i + 1);
                (next_i % len, next_i >= len)
            }
        };

        Some(result)
    }

    /// Peeks at the next item
    ///
    /// If this function returns none, there is nothing to play
    pub fn peek_next(&self, db: &LibraryDb) -> Result<Option<PlayingTrack>, DatabaseError> {
        if let Some(front) = self.queue.front().cloned() {
            return Ok(Some(PlayingTrack::from_queue(front, db)?));
        }

        let Some((index, new_cycle)) = self.peek_next_raw() else {
            return Ok(None);
        };

        let track = if new_cycle {
            let tracklist = self.build_tracklist(
                &mut self
                    .rng
                    .find_cycle_rng(self.rng.current_cycle().wrapping_add(1)),
            );
            PlayingTrack::from_tracklist(tracklist[index].clone(), index, db)?
        } else {
            PlayingTrack::from_tracklist(self.tracklist[index].clone(), index, db)?
        };

        Ok(Some(track))
    }

    /// Moves to the next item and returns it
    ///
    /// If this function returns none, it means there is nothing left to play or nothing to play
    ///
    /// This function will skip tracks without a container.
    pub fn pop_next(&mut self, db: &LibraryDb) -> Result<Option<PlayingTrack>, DatabaseError> {
        if let Some(front) = self.queue.pop_front() {
            return Ok(Some(PlayingTrack::from_queue(front, db)?));
        }

        let Some((index, new_cycle)) = self.peek_next_raw() else {
            return Ok(None);
        };

        let track = if new_cycle {
            let mut rng = self.rng.increment();
            let tracklist = self.build_tracklist(&mut rng);
            let track = PlayingTrack::from_tracklist(tracklist[index].clone(), index, db)?;
            self.tracklist = tracklist;
            self.event_tx.event(PlayerEvent::TracklistChanged {
                tracklist: self.tracklist.iter().map(|t| t.id()).collect(),
                position: self.tracklist_index,
            });
            track
        } else {
            PlayingTrack::from_tracklist(self.tracklist[index].clone(), index, db)?
        };
        self.tracklist_index = Some(self.tracklist_index.map_or(0, |i| i + 1));
        changed_state();

        Ok(Some(track))
    }

    /// Seeks within the tracklist
    ///
    /// If [`LoopMode::None`] is set, overflows will end the current cycle
    /// If [`LoopMode::Loop`] or [`LoopMode::LoopAndReshuffle`] is set, overflow will modulo a max of one cycle forward regardless of how large the input is
    /// If [`LoopMode::RepeatTrack`] is set, this will move to that track and repeat it. Overflows will end the current cycle
    ///
    /// A cycle is one loop of the tracklist.
    /// If your tracklist is 10 tracks long, and you seek-increment in the tracklist by +30, it will only move up 1 cycle, not 3. This is only important for [`LoopMode::LoopAndReshuffle`]
    pub fn tracklist_seek(&mut self, to: isize, increment: bool) -> Option<usize> {
        let len = self.tracklist.len();
        if len == 0 {
            return None;
        }

        let i = if increment {
            self.tracklist_index
                .map_or(0, |i| (i as isize).saturating_add(to))
        } else {
            to.max(0)
        };

        self.tracklist_index = match self.loop_mode {
            LoopMode::None | LoopMode::RepeatTrack => {
                let i = i.clamp(0, len as isize) as usize;
                if i >= len {
                    return None;
                }
                Some(i)
            }
            LoopMode::Loop => Some(i.rem_euclid(len as isize) as usize),
            LoopMode::LoopAndReshuffle => {
                let current_cycle = self.rng.current_cycle();
                let cycles_delta = i.div_euclid(len as isize);

                let new_cycle = if cycles_delta > 0 {
                    current_cycle + 1
                } else {
                    current_cycle.wrapping_add_signed(cycles_delta as i64)
                };

                if new_cycle > current_cycle {
                    let mut rng = self.rng.increment();
                    self.shuffle_tracklist(&mut rng);
                } else if new_cycle < current_cycle {
                    self.rng.set_cycle(new_cycle);
                    let mut rng = self.rng.find_cycle_rng(new_cycle);
                    self.shuffle_tracklist(&mut rng);
                }

                Some(i.rem_euclid(len as isize) as usize)
            }
        };
        changed_state();

        self.tracklist_index
    }
}