Added Controller State and Playlist loading on startup

This commit is contained in:
MrDulfin 2024-12-25 18:26:29 -05:00
parent 10223b23e2
commit 4f2d5ab64a
8 changed files with 220 additions and 55 deletions

View file

@ -1,9 +1,7 @@
pub mod other_settings;
use std::{
fs::{self, File, OpenOptions},
io::{Error, Read, Write},
path::PathBuf,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
@ -100,8 +98,8 @@ pub struct Config {
pub path: PathBuf,
pub backup_folder: Option<PathBuf>,
pub libraries: ConfigLibraries,
pub volume: f32,
pub connections: ConfigConnections,
pub state_path: PathBuf,
}
impl Config {
@ -160,9 +158,9 @@ impl Config {
pub fn read_file(path: PathBuf) -> Result<Self, Error> {
let mut file: File = File::open(path)?;
let mut bun: String = String::new();
_ = file.read_to_string(&mut bun);
let config: Config = serde_json::from_str::<Config>(&bun)?;
let mut buf: String = String::new();
_ = file.read_to_string(&mut buf);
let config: Config = serde_json::from_str::<Config>(&buf)?;
Ok(config)
}

View file

@ -1,7 +0,0 @@
pub enum Setting {
String { name: String, value: String },
Int { name: String, value: i32 },
Bool { name: String, value: bool },
}
pub struct Form {}

View file

@ -6,15 +6,19 @@
use itertools::Itertools;
use kushi::{Queue, QueueItemType};
use kushi::{QueueError, QueueItem};
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
use serde::{Deserialize, Serialize};
use serde_json::to_string_pretty;
use std::error::Error;
use std::fs::OpenOptions;
use std::io::Write;
use std::marker::PhantomData;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use thiserror::Error;
use uuid::Uuid;
use crate::config::ConfigError;
use crate::config::{self, ConfigError};
use crate::music_player::player::{Player, PlayerError};
use crate::music_storage::library::Song;
use crate::music_storage::playlist::{ExternalPlaylist, Playlist, PlaylistFolderItem};
@ -75,7 +79,7 @@ pub enum PlayerCommand {
Pause,
Play,
Enqueue(usize),
SetVolume(f64),
SetVolume(f32),
PlayNow(Uuid, PlayerLocation),
}
@ -90,8 +94,11 @@ pub enum LibraryCommand {
Song(Uuid),
AllSongs,
GetLibrary,
ExternalPlaylist(Uuid),
Playlist(Uuid),
ImportM3UPlayList(PathBuf)
ImportM3UPlayList(PathBuf),
Save,
Playlists,
}
#[derive(Debug, Clone)]
@ -100,8 +107,10 @@ pub enum LibraryResponse {
Song(Song, usize),
AllSongs(Vec<Song>),
Library(MusicLibrary),
Playlist(ExternalPlaylist),
ImportM3UPlayList(Uuid, String)
ExternalPlaylist(ExternalPlaylist),
Playlist(Playlist),
ImportM3UPlayList(Uuid, String),
Playlists(Vec<(Uuid, String)>),
}
#[derive(Debug, PartialEq, Clone)]
@ -170,6 +179,39 @@ impl ControllerHandle {
}
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct ControllerState {
path: PathBuf,
volume: f32,
now_playing: Uuid,
}
impl ControllerState {
fn new(path: PathBuf) -> Self {
ControllerState {
path,
volume: 0.35,
..Default::default()
}
}
fn write_file(&self) -> Result<(), std::io::Error> {
OpenOptions::new()
.truncate(true)
.create(true)
.write(true)
.open(&self.path)
.unwrap()
.write_all(&to_string_pretty(self)?.into_bytes())?;
Ok(())
}
fn read_file(path: impl AsRef<Path>) -> Result<Self, std::io::Error> {
let state = serde_json::from_str(&std::fs::read_to_string(path)?)?;
Ok(state)
}
}
#[allow(unused_variables)]
impl<'c, P: Player + Send + Sync> Controller<'c, P> {
pub async fn start(
@ -184,23 +226,22 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
where
P: Player,
{
//TODO: make a separate event loop for sccessing library that clones borrowed values from inner library loop?
let mut queue: Queue<QueueSong, QueueAlbum> = Queue {
let queue: Queue<QueueSong, QueueAlbum> = Queue {
items: Vec::new(),
played: Vec::new(),
loop_: false,
shuffle: None,
};
// for testing porpuses
// for song in &library.library {
// queue.add_item(
// QueueSong {
// song: song.clone(),
// location: PlayerLocation::Test,
// },
// true,
// );
// }
let state = {
let path = &config.read().unwrap().state_path;
if let Ok(state) = ControllerState::read_file(path) {
state
} else {
ControllerState::new(path.clone())
}
};
let queue = queue;
std::thread::scope(|scope| {
@ -220,19 +261,27 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
player_mail.1,
queue_mail.0,
_lib_mail,
state,
)
.await
.unwrap();
});
scope
.spawn(async move {
Controller::<P>::player_event_loop(player, player_mail.0)
Controller::<P>::player_event_loop(
player,
player_mail.0
)
.await
.unwrap();
});
scope
.spawn(async {
Controller::<P>::library_loop(lib_mail.1, &mut library)
Controller::<P>::library_loop(
lib_mail.1,
&mut library,
config,
)
.await
.unwrap();
});
@ -258,8 +307,14 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
player_mail: MailMan<PlayerResponse, PlayerCommand>,
queue_mail: MailMan<QueueCommand, QueueResponse>,
lib_mail: MailMan<LibraryCommand, LibraryResponse>,
mut state: ControllerState,
) -> Result<(), ()> {
let mut first = true;
{
let volume = state.volume as f64;
player.write().unwrap().set_volume(volume);
println!("volume set to {volume}");
}
while true {
let _mail = player_mail.recv().await;
if let Ok(mail) = _mail {
@ -282,9 +337,12 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
player_mail.send(PlayerResponse::Empty).await.unwrap();
}
PlayerCommand::SetVolume(volume) => {
player.write().unwrap().set_volume(volume);
player.write().unwrap().set_volume(volume as f64);
println!("volume set to {volume}");
player_mail.send(PlayerResponse::Empty).await.unwrap();
state.volume = volume;
_ = state.write_file()
}
PlayerCommand::NextSong => {
queue_mail.send(QueueCommand::Next).await.unwrap();
@ -384,10 +442,25 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
// ...
// let's just pretend I figured that out already
let (songs, index) = match location {
PlayerLocation::Library => {
lib_mail.send(LibraryCommand::AllSongs).await.unwrap();
let LibraryResponse::AllSongs(songs) = lib_mail.recv().await.unwrap() else {
unreachable!()
};
(songs, index)
}
PlayerLocation::Playlist(uuid) => {
lib_mail.send(LibraryCommand::ExternalPlaylist(uuid)).await.unwrap();
let LibraryResponse::ExternalPlaylist(list) = lib_mail.recv().await.unwrap() else {
unreachable!()
};
let index = list.get_index(song.uuid).unwrap();
(list.tracks, index)
}
_ => todo!("Got Location other than Library or Playlist")
};
for i in index+1..(index+50) {
if let Some(song) = songs.get(i) {
@ -396,7 +469,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
unreachable!()
};
} else {
println!("End of Library");
println!("End of Library / Playlist");
break;
}
}
@ -414,6 +487,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
async fn library_loop(
lib_mail: MailMan<LibraryResponse, LibraryCommand>,
library: &'c mut MusicLibrary,
config: Arc<RwLock<Config>>,
) -> Result<(), ()> {
while true {
match lib_mail.recv().await.unwrap() {
@ -424,9 +498,9 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
LibraryCommand::AllSongs => {
lib_mail.send(LibraryResponse::AllSongs(library.library.clone())).await.unwrap();
},
LibraryCommand::Playlist(uuid) => {
LibraryCommand::ExternalPlaylist(uuid) => {
let playlist = library.query_playlist_uuid(&uuid).unwrap();
lib_mail.send(LibraryResponse::Playlist(ExternalPlaylist::from_playlist(playlist, &library))).await.unwrap();
lib_mail.send(LibraryResponse::ExternalPlaylist(ExternalPlaylist::from_playlist(playlist, &library))).await.unwrap();
}
LibraryCommand::ImportM3UPlayList(path) => {
let playlist = Playlist::from_m3u(path, library).unwrap();
@ -436,6 +510,16 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
lib_mail.send(LibraryResponse::ImportM3UPlayList(uuid, name)).await.unwrap();
}
LibraryCommand::Save => {
library.save({config.read().unwrap().libraries.get_library(&library.uuid).unwrap().path.clone()}).unwrap();
lib_mail.send(LibraryResponse::Ok).await.unwrap();
}
LibraryCommand::Playlists => {
let mut lists = vec![];
library.playlists.lists_recursive().into_par_iter().map(|list| (list.uuid, list.title.clone())).collect_into_vec(&mut lists);
lib_mail.send(LibraryResponse::Playlists(lists)).await.unwrap();
}
_ => { todo!() }
}
}

View file

@ -52,6 +52,17 @@ impl PlaylistFolder {
}
None
}
pub fn lists_recursive(&self) -> Vec<&Playlist> {
let mut vec = vec![];
for item in &self.items {
match item {
PlaylistFolderItem::List(ref playlist) => vec.push(playlist),
PlaylistFolderItem::Folder(folder) => vec.append(&mut folder.lists_recursive()),
}
}
vec
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
@ -107,13 +118,13 @@ impl Playlist {
}
pub fn get_index(&self, uuid: Uuid) -> Option<usize> {
let mut i = 0;
if self.contains(uuid) {
if self.tracks.contains(&uuid) {
for track in &self.tracks {
i += 1;
if &uuid == track {
dbg!("Index gotted! ", i);
return Some(i);
}
i += 1;
}
}
None
@ -351,7 +362,9 @@ pub struct ExternalPlaylist {
impl ExternalPlaylist {
pub(crate) fn from_playlist(playlist: &Playlist, library: &MusicLibrary) -> Self {
let tracks: Vec<Song> = playlist.tracks.iter().filter_map(|uuid| {
library.query_uuid(uuid).map(|res| res.0.clone())
library.query_uuid(uuid).map(|res| {
res.0.clone()
})
}).collect_vec();
Self {
@ -363,6 +376,27 @@ impl ExternalPlaylist {
play_time: playlist.play_time
}
}
pub fn get_index(&self, uuid: Uuid) -> Option<usize> {
let mut i = 0;
if self.contains(uuid) {
for track in &self.tracks {
if &uuid == &track.uuid {
return Some(i);
}
i += 1;
}
}
None
}
pub fn contains(&self, uuid: Uuid) -> bool {
for track in &self.tracks {
if track.uuid == uuid {
return true;
}
}
false
}
}

View file

@ -9,6 +9,7 @@ use crate::wrappers::_Song;
#[tauri::command]
pub async fn add_song_to_queue(app: AppHandle<Wry>, ctrl_handle: State<'_, ControllerHandle>, uuid: Uuid, location: PlayerLocation) -> Result<(), String> {
dbg!(&location);
ctrl_handle.lib_mail.send(dmp_core::music_controller::controller::LibraryCommand::Song(uuid)).await.unwrap();
let LibraryResponse::Song(song, _) = ctrl_handle.lib_mail.recv().await.unwrap() else {
unreachable!()

View file

@ -6,7 +6,7 @@ use dmp_core::{config::{Config, ConfigLibrary}, music_controller::controller::{C
use tauri::{http::Response, Emitter, Manager, State, Url, WebviewWindowBuilder, Wry};
use uuid::Uuid;
use crate::wrappers::{get_library, play, pause, prev, set_volume, get_song, next, get_queue, import_playlist, get_playlist};
use crate::wrappers::{get_library, play, pause, prev, set_volume, get_song, next, get_queue, import_playlist, get_playlist, get_playlists};
pub mod wrappers;
pub mod commands;
@ -76,6 +76,7 @@ pub fn run() {
play_now,
import_playlist,
get_playlist,
get_playlists
]).manage(ConfigRx(rx))
.manage(LibRx(lib_rx))
.manage(HandleTx(handle_tx))
@ -142,11 +143,15 @@ async fn get_config(state: State<'_, ConfigRx>) -> Result<Config, String> {
// dbg!(&dir);
let config = if let Ok(c) = Config::read_file(PathBuf::from(path).join("config")) {
let config = if let Ok(mut c) = Config::read_file(PathBuf::from(path).join("config")) {
if c.state_path == PathBuf::default() {
c.state_path = PathBuf::from(path).join("state");
}
c
} else {
let c = Config {
path: PathBuf::from(path).join("config"),
state_path: PathBuf::from(path).join("state"),
..Default::default()
};
c.write_file().unwrap();

View file

@ -39,7 +39,7 @@ pub async fn pause(app: AppHandle<Wry>, ctrl_handle: State<'_, ControllerHandle>
#[tauri::command]
pub async fn set_volume(ctrl_handle: State<'_, ControllerHandle>, volume: String) -> Result<(), String> {
let volume = volume.parse::<f64>().unwrap() / 1000.0;
let volume = volume.parse::<f32>().unwrap() / 1000.0;
ctrl_handle.player_mail.send(dmp_core::music_controller::controller::PlayerCommand::SetVolume(volume)).await.unwrap();
let PlayerResponse::Empty = ctrl_handle.player_mail.recv().await.unwrap() else {
unreachable!()
@ -142,14 +142,25 @@ pub async fn get_library(ctrl_handle: State<'_, ControllerHandle>) -> Result<Vec
#[tauri::command]
pub async fn get_playlist(ctrl_handle: State<'_, ControllerHandle>, uuid: Uuid) -> Result<Vec<_Song>, String> {
ctrl_handle.lib_mail.send(LibraryCommand::Playlist(uuid)).await.unwrap();
let LibraryResponse::Playlist(playlist) = ctrl_handle.lib_mail.recv().await.unwrap() else { unreachable!("It has been reached") };
ctrl_handle.lib_mail.send(LibraryCommand::ExternalPlaylist(uuid)).await.unwrap();
let LibraryResponse::ExternalPlaylist(playlist) = ctrl_handle.lib_mail.recv().await.unwrap() else { unreachable!("It has been reached") };
let songs = playlist.tracks.iter().map(|song| _Song::from(song)).collect::<Vec<_>>();
println!("Got Playlist {}, len {}", playlist.title, playlist.tracks.len());
Ok(songs)
}
#[tauri::command]
pub async fn get_playlists(app: AppHandle<Wry>, ctrl_handle: State<'_, ControllerHandle>) -> Result<(), String> {
println!("getting Playlists");
ctrl_handle.lib_mail.send(LibraryCommand::Playlists).await.unwrap();
let LibraryResponse::Playlists(lists) = ctrl_handle.lib_mail.recv().await.unwrap() else { unreachable!() };
println!("gotten playlists");
app.emit("playlists_gotten", lists.into_iter().map(|(uuid, name)| PlaylistPayload { uuid, name }).collect_vec()).unwrap();
Ok(())
}
#[tauri::command]
pub async fn import_playlist(ctrl_handle: State<'_, ControllerHandle>) -> Result<PlaylistPayload, String> {
let file = rfd::AsyncFileDialog::new()
@ -161,11 +172,13 @@ pub async fn import_playlist(ctrl_handle: State<'_, ControllerHandle>) -> Result
ctrl_handle.lib_mail.send(LibraryCommand::ImportM3UPlayList(PathBuf::from(file.path()))).await.unwrap();
let LibraryResponse::ImportM3UPlayList(uuid, name) = ctrl_handle.lib_mail.recv().await.unwrap() else { unreachable!("It has been reached") };
ctrl_handle.lib_mail.send(LibraryCommand::Save).await.unwrap();
let LibraryResponse::Ok = ctrl_handle.lib_mail.recv().await.unwrap() else { unreachable!() };
println!("Imported Playlist {name}");
Ok(PlaylistPayload {uuid, name})
}
#[derive(Serialize)]
#[derive(Serialize, Clone)]
pub struct PlaylistPayload {
uuid: Uuid,
name: String

View file

@ -98,6 +98,39 @@ interface PlaylistHeadProps {
function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary }: PlaylistHeadProps) {
useEffect(() => {
const unlisten = appWindow.listen<any[]>("playlists_gotten", (_res) => {
// console.log(event);
let res = _res.payload;
setPlaylists([
...res.map( (item) => {
return (
<button onClick={ () => {
invoke('get_playlist', { uuid: item.uuid }).then((list) => {
setLibrary([...(list as any[]).map((song) => {
// console.log(song);
return (
<Song
key={ song.uuid }
location={ song.location }
playerLocation={ {"Playlist" : item.uuid } }
uuid={ song.uuid }
plays={ song.plays }
duration={ song.duration }
tags={ song.tags }
/>
)
})])
})
setViewName( item.name )
} } key={ 'playlist_' + item.uuid }>{ item.name }</button>
)
})
])
})
return () => { unlisten.then((f) => f()) }
}, []);
let handle_import = () => {
invoke('import_playlist').then((_res) => {
let res = _res as any;
@ -114,6 +147,7 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary }: Play
<Song
key={ song.uuid }
location={ song.location }
playerLocation={ {"Playlist" : res.uuid } }
uuid={ song.uuid }
plays={ song.plays }
duration={ song.duration }
@ -140,6 +174,7 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary }: Play
<Song
key={ song.uuid }
location={ song.location }
playerLocation="Library"
uuid={ song.uuid }
plays={ song.plays }
duration={ song.duration }
@ -150,7 +185,7 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary }: Play
})
} }>Library</button>
{ playlists }
<button onClick={ handle_import }>Import .m3u8 Playlist</button>
<button onClick={ handle_import }>Import .m3u Playlist</button>
</section>
)
}
@ -166,15 +201,16 @@ function MainView({ lib_ref, viewName }: MainViewProps) {
useEffect(() => {
const unlisten = appWindow.listen<any>("library_loaded", (_) => {
console.log("library_loaded");
invoke('get_playlists').then(() => {})
invoke('get_library').then((lib) => {
setLibrary([...(lib as any[]).map((song) => {
console.log(song);
return (
<Song
key={ song.uuid }
location={ song.location }
playerLocation="Library"
uuid={ song.uuid }
plays={ song.plays }
duration={ song.duration }
@ -198,6 +234,7 @@ function MainView({ lib_ref, viewName }: MainViewProps) {
interface SongProps {
location: any,
playerLocation: string | {"Playlist" : any},
uuid: string,
plays: number,
format?: string,
@ -218,11 +255,11 @@ function Song(props: SongProps) {
<p className="artist">{ props.tags.AlbumArtist }</p>
<p className="duration">{ props.duration }</p>
<button onClick={(_) => {
invoke('add_song_to_queue', { uuid: props.uuid, location: 'Library' }).then(() => {} )
invoke('add_song_to_queue', { uuid: props.uuid, location: props.playerLocation }).then(() => {} )
}}
>Add to Queue</button>
<button onClick={() => {
invoke("play_now", { uuid: props.uuid, location: 'Library' }).then(() => {})
invoke("play_now", { uuid: props.uuid, location: props.playerLocation }).then(() => {})
}}>Play Now</button>
</div>
)