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))
}
}