mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-03-10 14:13:53 +01:00
Change from dbfile to dbdir, dbfile is now dbdir/dbfile
- add autosave 1 minute after change is made to db 1 minute - improved gapless playback (used to run some code 10 times a second for gapless playback, is now event driven) - saving now moves the previous file to dbfile-[UNIX-TIMESTAMP] in case something goes wrong when saving the new dbfile.
This commit is contained in:
parent
8a9ee5c9cf
commit
6f9535a28e
@ -1,5 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
|
fmt::format,
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::{BufReader, Read, Write},
|
io::{BufReader, Read, Write},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
@ -20,6 +21,8 @@ use super::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
|
/// the directory that contains the dbfile, backups, statistics, ...
|
||||||
|
pub db_dir: PathBuf,
|
||||||
/// the path to the file used to save/load the data. empty if database is in client mode.
|
/// the path to the file used to save/load the data. empty if database is in client mode.
|
||||||
pub db_file: PathBuf,
|
pub db_file: PathBuf,
|
||||||
/// the path to the directory containing the actual music and cover image files
|
/// the path to the directory containing the actual music and cover image files
|
||||||
@ -34,9 +37,6 @@ pub struct Database {
|
|||||||
/// Some(None) -> access to lib_directory
|
/// Some(None) -> access to lib_directory
|
||||||
/// Some(Some(path)) -> access to path
|
/// Some(Some(path)) -> access to path
|
||||||
pub custom_files: Option<Option<PathBuf>>,
|
pub custom_files: Option<Option<PathBuf>>,
|
||||||
// These will be used for autosave once that gets implemented
|
|
||||||
db_data_file_change_first: Option<Instant>,
|
|
||||||
db_data_file_change_last: Option<Instant>,
|
|
||||||
pub queue: Queue,
|
pub queue: Queue,
|
||||||
/// if the database receives an update, it will inform all of its clients so they can stay in sync.
|
/// if the database receives an update, it will inform all of its clients so they can stay in sync.
|
||||||
/// this is a list containing all the clients.
|
/// this is a list containing all the clients.
|
||||||
@ -48,6 +48,10 @@ pub struct Database {
|
|||||||
Option<Arc<Mutex<crate::server::get::Client<Box<dyn ClientIo>>>>>,
|
Option<Arc<Mutex<crate::server::get::Client<Box<dyn ClientIo>>>>>,
|
||||||
/// only relevant for clients. true if init is done
|
/// only relevant for clients. true if init is done
|
||||||
client_is_init: bool,
|
client_is_init: bool,
|
||||||
|
|
||||||
|
/// If `Some`, contains the first time and the last time data was modified.
|
||||||
|
/// When the DB is saved, this is reset to `None` to represent that nothing was modified.
|
||||||
|
pub times_data_modified: Option<(Instant, Instant)>,
|
||||||
}
|
}
|
||||||
pub trait ClientIo: Read + Write + Send {}
|
pub trait ClientIo: Read + Write + Send {}
|
||||||
impl<T: Read + Write + Send> ClientIo for T {}
|
impl<T: Read + Write + Send> ClientIo for T {}
|
||||||
@ -60,13 +64,6 @@ pub enum UpdateEndpoint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
/// TODO!
|
|
||||||
fn panic(&self, msg: &str) -> ! {
|
|
||||||
// custom panic handler
|
|
||||||
// make a backup
|
|
||||||
// exit
|
|
||||||
panic!("DatabasePanic: {msg}");
|
|
||||||
}
|
|
||||||
pub fn is_client(&self) -> bool {
|
pub fn is_client(&self) -> bool {
|
||||||
self.db_file.as_os_str().is_empty()
|
self.db_file.as_os_str().is_empty()
|
||||||
}
|
}
|
||||||
@ -76,11 +73,20 @@ impl Database {
|
|||||||
pub fn get_path(&self, location: &DatabaseLocation) -> PathBuf {
|
pub fn get_path(&self, location: &DatabaseLocation) -> PathBuf {
|
||||||
self.lib_directory.join(&location.rel_path)
|
self.lib_directory.join(&location.rel_path)
|
||||||
}
|
}
|
||||||
|
fn modified_data(&mut self) {
|
||||||
|
let now = Instant::now();
|
||||||
|
if let Some((_first, last)) = &mut self.times_data_modified {
|
||||||
|
*last = now;
|
||||||
|
} else {
|
||||||
|
self.times_data_modified = Some((now, now));
|
||||||
|
}
|
||||||
|
}
|
||||||
// NOTE: just use `songs` directly? not sure yet...
|
// NOTE: just use `songs` directly? not sure yet...
|
||||||
pub fn get_song(&self, song: &SongId) -> Option<&Song> {
|
pub fn get_song(&self, song: &SongId) -> Option<&Song> {
|
||||||
self.songs.get(song)
|
self.songs.get(song)
|
||||||
}
|
}
|
||||||
pub fn get_song_mut(&mut self, song: &SongId) -> Option<&mut Song> {
|
pub fn get_song_mut(&mut self, song: &SongId) -> Option<&mut Song> {
|
||||||
|
self.modified_data();
|
||||||
self.songs.get_mut(song)
|
self.songs.get_mut(song)
|
||||||
}
|
}
|
||||||
/// adds a song to the database.
|
/// adds a song to the database.
|
||||||
@ -101,6 +107,7 @@ impl Database {
|
|||||||
}
|
}
|
||||||
/// used internally
|
/// used internally
|
||||||
pub fn add_song_new_nomagic(&mut self, mut song: Song) -> SongId {
|
pub fn add_song_new_nomagic(&mut self, mut song: Song) -> SongId {
|
||||||
|
self.modified_data();
|
||||||
for key in 0.. {
|
for key in 0.. {
|
||||||
if !self.songs.contains_key(&key) {
|
if !self.songs.contains_key(&key) {
|
||||||
song.id = key;
|
song.id = key;
|
||||||
@ -119,6 +126,7 @@ impl Database {
|
|||||||
}
|
}
|
||||||
/// used internally
|
/// used internally
|
||||||
fn add_artist_new_nomagic(&mut self, mut artist: Artist) -> ArtistId {
|
fn add_artist_new_nomagic(&mut self, mut artist: Artist) -> ArtistId {
|
||||||
|
self.modified_data();
|
||||||
for key in 0.. {
|
for key in 0.. {
|
||||||
if !self.artists.contains_key(&key) {
|
if !self.artists.contains_key(&key) {
|
||||||
artist.id = key;
|
artist.id = key;
|
||||||
@ -141,6 +149,7 @@ impl Database {
|
|||||||
}
|
}
|
||||||
/// used internally
|
/// used internally
|
||||||
fn add_album_new_nomagic(&mut self, mut album: Album) -> AlbumId {
|
fn add_album_new_nomagic(&mut self, mut album: Album) -> AlbumId {
|
||||||
|
self.modified_data();
|
||||||
for key in 0.. {
|
for key in 0.. {
|
||||||
if !self.albums.contains_key(&key) {
|
if !self.albums.contains_key(&key) {
|
||||||
album.id = key;
|
album.id = key;
|
||||||
@ -157,6 +166,7 @@ impl Database {
|
|||||||
}
|
}
|
||||||
/// used internally
|
/// used internally
|
||||||
fn add_cover_new_nomagic(&mut self, cover: Cover) -> AlbumId {
|
fn add_cover_new_nomagic(&mut self, cover: Cover) -> AlbumId {
|
||||||
|
self.modified_data();
|
||||||
for key in 0.. {
|
for key in 0.. {
|
||||||
if !self.covers.contains_key(&key) {
|
if !self.covers.contains_key(&key) {
|
||||||
self.covers.insert(key, cover);
|
self.covers.insert(key, cover);
|
||||||
@ -171,21 +181,27 @@ impl Database {
|
|||||||
/// Otherwise Some(old_data) is returned.
|
/// Otherwise Some(old_data) is returned.
|
||||||
pub fn update_song(&mut self, song: Song) -> Result<Song, ()> {
|
pub fn update_song(&mut self, song: Song) -> Result<Song, ()> {
|
||||||
if let Some(prev_song) = self.songs.get_mut(&song.id) {
|
if let Some(prev_song) = self.songs.get_mut(&song.id) {
|
||||||
Ok(std::mem::replace(prev_song, song))
|
let old = std::mem::replace(prev_song, song);
|
||||||
|
self.modified_data();
|
||||||
|
Ok(old)
|
||||||
} else {
|
} else {
|
||||||
Err(())
|
Err(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn update_album(&mut self, album: Album) -> Result<Album, ()> {
|
pub fn update_album(&mut self, album: Album) -> Result<Album, ()> {
|
||||||
if let Some(prev_album) = self.albums.get_mut(&album.id) {
|
if let Some(prev_album) = self.albums.get_mut(&album.id) {
|
||||||
Ok(std::mem::replace(prev_album, album))
|
let old = std::mem::replace(prev_album, album);
|
||||||
|
self.modified_data();
|
||||||
|
Ok(old)
|
||||||
} else {
|
} else {
|
||||||
Err(())
|
Err(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn update_artist(&mut self, artist: Artist) -> Result<Artist, ()> {
|
pub fn update_artist(&mut self, artist: Artist) -> Result<Artist, ()> {
|
||||||
if let Some(prev_artist) = self.artists.get_mut(&artist.id) {
|
if let Some(prev_artist) = self.artists.get_mut(&artist.id) {
|
||||||
Ok(std::mem::replace(prev_artist, artist))
|
let old = std::mem::replace(prev_artist, artist);
|
||||||
|
self.modified_data();
|
||||||
|
Ok(old)
|
||||||
} else {
|
} else {
|
||||||
Err(())
|
Err(())
|
||||||
}
|
}
|
||||||
@ -194,11 +210,13 @@ impl Database {
|
|||||||
/// uses song.id. If another song with that ID exists, it is replaced and Some(other_song) is returned.
|
/// uses song.id. If another song with that ID exists, it is replaced and Some(other_song) is returned.
|
||||||
/// If no other song exists, the song will be added to the database with the given ID and None is returned.
|
/// If no other song exists, the song will be added to the database with the given ID and None is returned.
|
||||||
pub fn update_or_add_song(&mut self, song: Song) -> Option<Song> {
|
pub fn update_or_add_song(&mut self, song: Song) -> Option<Song> {
|
||||||
|
self.modified_data();
|
||||||
self.songs.insert(song.id, song)
|
self.songs.insert(song.id, song)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_song(&mut self, song: SongId) -> Option<Song> {
|
pub fn remove_song(&mut self, song: SongId) -> Option<Song> {
|
||||||
if let Some(removed) = self.songs.remove(&song) {
|
if let Some(removed) = self.songs.remove(&song) {
|
||||||
|
self.modified_data();
|
||||||
Some(removed)
|
Some(removed)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -206,6 +224,7 @@ impl Database {
|
|||||||
}
|
}
|
||||||
pub fn remove_album(&mut self, song: SongId) -> Option<Song> {
|
pub fn remove_album(&mut self, song: SongId) -> Option<Song> {
|
||||||
if let Some(removed) = self.songs.remove(&song) {
|
if let Some(removed) = self.songs.remove(&song) {
|
||||||
|
self.modified_data();
|
||||||
Some(removed)
|
Some(removed)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -213,6 +232,7 @@ impl Database {
|
|||||||
}
|
}
|
||||||
pub fn remove_artist(&mut self, song: SongId) -> Option<Song> {
|
pub fn remove_artist(&mut self, song: SongId) -> Option<Song> {
|
||||||
if let Some(removed) = self.songs.remove(&song) {
|
if let Some(removed) = self.songs.remove(&song) {
|
||||||
|
self.modified_data();
|
||||||
Some(removed)
|
Some(removed)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -231,7 +251,6 @@ impl Database {
|
|||||||
if self.playing {
|
if self.playing {
|
||||||
Command::Resume.to_bytes(con)?;
|
Command::Resume.to_bytes(con)?;
|
||||||
}
|
}
|
||||||
// since this is so easy to check for, it comes last.
|
|
||||||
// this allows clients to find out when init_connection is done.
|
// this allows clients to find out when init_connection is done.
|
||||||
Command::InitComplete.to_bytes(con)?;
|
Command::InitComplete.to_bytes(con)?;
|
||||||
// is initialized now - client can receive updates after this point.
|
// is initialized now - client can receive updates after this point.
|
||||||
@ -459,10 +478,18 @@ impl Database {
|
|||||||
// file saving/loading
|
// file saving/loading
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
|
/// TODO!
|
||||||
|
fn panic(&self, msg: &str) -> ! {
|
||||||
|
// custom panic handler
|
||||||
|
// make a backup
|
||||||
|
// exit
|
||||||
|
panic!("DatabasePanic: {msg}");
|
||||||
|
}
|
||||||
/// Database is also used for clients, to keep things consistent.
|
/// Database is also used for clients, to keep things consistent.
|
||||||
/// A client database doesn't need any storage paths and won't perform autosaves.
|
/// A client database doesn't need any storage paths and won't perform autosaves.
|
||||||
pub fn new_clientside() -> Self {
|
pub fn new_clientside() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
db_dir: PathBuf::new(),
|
||||||
db_file: PathBuf::new(),
|
db_file: PathBuf::new(),
|
||||||
lib_directory: PathBuf::new(),
|
lib_directory: PathBuf::new(),
|
||||||
artists: HashMap::new(),
|
artists: HashMap::new(),
|
||||||
@ -470,18 +497,19 @@ impl Database {
|
|||||||
songs: HashMap::new(),
|
songs: HashMap::new(),
|
||||||
covers: HashMap::new(),
|
covers: HashMap::new(),
|
||||||
custom_files: None,
|
custom_files: None,
|
||||||
db_data_file_change_first: None,
|
|
||||||
db_data_file_change_last: None,
|
|
||||||
queue: QueueContent::Folder(0, vec![], String::new()).into(),
|
queue: QueueContent::Folder(0, vec![], String::new()).into(),
|
||||||
update_endpoints: vec![],
|
update_endpoints: vec![],
|
||||||
playing: false,
|
playing: false,
|
||||||
command_sender: None,
|
command_sender: None,
|
||||||
remote_server_as_song_file_source: None,
|
remote_server_as_song_file_source: None,
|
||||||
client_is_init: false,
|
client_is_init: false,
|
||||||
|
times_data_modified: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn new_empty(path: PathBuf, lib_dir: PathBuf) -> Self {
|
pub fn new_empty_in_dir(dir: PathBuf, lib_dir: PathBuf) -> Self {
|
||||||
|
let path = dir.join("dbfile");
|
||||||
Self {
|
Self {
|
||||||
|
db_dir: dir,
|
||||||
db_file: path,
|
db_file: path,
|
||||||
lib_directory: lib_dir,
|
lib_directory: lib_dir,
|
||||||
artists: HashMap::new(),
|
artists: HashMap::new(),
|
||||||
@ -489,20 +517,24 @@ impl Database {
|
|||||||
songs: HashMap::new(),
|
songs: HashMap::new(),
|
||||||
covers: HashMap::new(),
|
covers: HashMap::new(),
|
||||||
custom_files: None,
|
custom_files: None,
|
||||||
db_data_file_change_first: None,
|
|
||||||
db_data_file_change_last: None,
|
|
||||||
queue: QueueContent::Folder(0, vec![], String::new()).into(),
|
queue: QueueContent::Folder(0, vec![], String::new()).into(),
|
||||||
update_endpoints: vec![],
|
update_endpoints: vec![],
|
||||||
playing: false,
|
playing: false,
|
||||||
command_sender: None,
|
command_sender: None,
|
||||||
remote_server_as_song_file_source: None,
|
remote_server_as_song_file_source: None,
|
||||||
client_is_init: false,
|
client_is_init: false,
|
||||||
|
times_data_modified: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn load_database(path: PathBuf, lib_directory: PathBuf) -> Result<Self, std::io::Error> {
|
pub fn load_database_from_dir(
|
||||||
|
dir: PathBuf,
|
||||||
|
lib_directory: PathBuf,
|
||||||
|
) -> Result<Self, std::io::Error> {
|
||||||
|
let path = dir.join("dbfile");
|
||||||
let mut file = BufReader::new(File::open(&path)?);
|
let mut file = BufReader::new(File::open(&path)?);
|
||||||
eprintln!("[{}] loading library from {file:?}", "INFO".cyan());
|
eprintln!("[{}] loading library from {file:?}", "INFO".cyan());
|
||||||
let s = Self {
|
let s = Self {
|
||||||
|
db_dir: dir,
|
||||||
db_file: path,
|
db_file: path,
|
||||||
lib_directory,
|
lib_directory,
|
||||||
artists: ToFromBytes::from_bytes(&mut file)?,
|
artists: ToFromBytes::from_bytes(&mut file)?,
|
||||||
@ -510,20 +542,19 @@ impl Database {
|
|||||||
songs: ToFromBytes::from_bytes(&mut file)?,
|
songs: ToFromBytes::from_bytes(&mut file)?,
|
||||||
covers: ToFromBytes::from_bytes(&mut file)?,
|
covers: ToFromBytes::from_bytes(&mut file)?,
|
||||||
custom_files: None,
|
custom_files: None,
|
||||||
db_data_file_change_first: None,
|
|
||||||
db_data_file_change_last: None,
|
|
||||||
queue: QueueContent::Folder(0, vec![], String::new()).into(),
|
queue: QueueContent::Folder(0, vec![], String::new()).into(),
|
||||||
update_endpoints: vec![],
|
update_endpoints: vec![],
|
||||||
playing: false,
|
playing: false,
|
||||||
command_sender: None,
|
command_sender: None,
|
||||||
remote_server_as_song_file_source: None,
|
remote_server_as_song_file_source: None,
|
||||||
client_is_init: false,
|
client_is_init: false,
|
||||||
|
times_data_modified: None,
|
||||||
};
|
};
|
||||||
eprintln!("[{}] loaded library", "INFO".green());
|
eprintln!("[{}] loaded library", "INFO".green());
|
||||||
Ok(s)
|
Ok(s)
|
||||||
}
|
}
|
||||||
/// saves the database's contents. save path can be overridden
|
/// saves the database's contents. save path can be overridden
|
||||||
pub fn save_database(&self, path: Option<PathBuf>) -> Result<PathBuf, std::io::Error> {
|
pub fn save_database(&mut self, path: Option<PathBuf>) -> Result<PathBuf, std::io::Error> {
|
||||||
let path = if let Some(p) = path {
|
let path = if let Some(p) = path {
|
||||||
p
|
p
|
||||||
} else {
|
} else {
|
||||||
@ -534,6 +565,22 @@ impl Database {
|
|||||||
return Ok(path);
|
return Ok(path);
|
||||||
}
|
}
|
||||||
eprintln!("[{}] saving db to {path:?}", "INFO".cyan());
|
eprintln!("[{}] saving db to {path:?}", "INFO".cyan());
|
||||||
|
if path.try_exists()? {
|
||||||
|
let backup_name = format!(
|
||||||
|
"dbfile-{}",
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::SystemTime::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0),
|
||||||
|
);
|
||||||
|
if let Err(e) = fs::rename(&path, self.db_dir.join(&backup_name)) {
|
||||||
|
eprintln!(
|
||||||
|
"[{}] Couldn't move previous dbfile to {backup_name}!",
|
||||||
|
"ERR!".red()
|
||||||
|
);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut file = fs::OpenOptions::new()
|
let mut file = fs::OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
@ -544,6 +591,8 @@ impl Database {
|
|||||||
self.songs.to_bytes(&mut file)?;
|
self.songs.to_bytes(&mut file)?;
|
||||||
self.covers.to_bytes(&mut file)?;
|
self.covers.to_bytes(&mut file)?;
|
||||||
eprintln!("[{}] saved db", "INFO".green());
|
eprintln!("[{}] saved db", "INFO".green());
|
||||||
|
// all changes saved, data no longer modified
|
||||||
|
self.times_data_modified = None;
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
pub fn broadcast_update(&mut self, update: &Command) {
|
pub fn broadcast_update(&mut self, update: &Command) {
|
||||||
@ -595,6 +644,7 @@ impl Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn sync(&mut self, artists: Vec<Artist>, albums: Vec<Album>, songs: Vec<Song>) {
|
pub fn sync(&mut self, artists: Vec<Artist>, albums: Vec<Album>, songs: Vec<Song>) {
|
||||||
|
self.modified_data();
|
||||||
self.artists = artists.iter().map(|v| (v.id, v.clone())).collect();
|
self.artists = artists.iter().map(|v| (v.id, v.clone())).collect();
|
||||||
self.albums = albums.iter().map(|v| (v.id, v.clone())).collect();
|
self.albums = albums.iter().map(|v| (v.id, v.clone())).collect();
|
||||||
self.songs = songs.iter().map(|v| (v.id, v.clone())).collect();
|
self.songs = songs.iter().map(|v| (v.id, v.clone())).collect();
|
||||||
@ -616,18 +666,22 @@ impl Database {
|
|||||||
}
|
}
|
||||||
/// you should probably use a Command to do this...
|
/// you should probably use a Command to do this...
|
||||||
pub fn songs_mut(&mut self) -> &mut HashMap<SongId, Song> {
|
pub fn songs_mut(&mut self) -> &mut HashMap<SongId, Song> {
|
||||||
|
self.modified_data();
|
||||||
&mut self.songs
|
&mut self.songs
|
||||||
}
|
}
|
||||||
/// you should probably use a Command to do this...
|
/// you should probably use a Command to do this...
|
||||||
pub fn albums_mut(&mut self) -> &mut HashMap<AlbumId, Album> {
|
pub fn albums_mut(&mut self) -> &mut HashMap<AlbumId, Album> {
|
||||||
|
self.modified_data();
|
||||||
&mut self.albums
|
&mut self.albums
|
||||||
}
|
}
|
||||||
/// you should probably use a Command to do this...
|
/// you should probably use a Command to do this...
|
||||||
pub fn artists_mut(&mut self) -> &mut HashMap<ArtistId, Artist> {
|
pub fn artists_mut(&mut self) -> &mut HashMap<ArtistId, Artist> {
|
||||||
|
self.modified_data();
|
||||||
&mut self.artists
|
&mut self.artists
|
||||||
}
|
}
|
||||||
/// you should probably use a Command to do this...
|
/// you should probably use a Command to do this...
|
||||||
pub fn covers_mut(&mut self) -> &mut HashMap<CoverId, Cover> {
|
pub fn covers_mut(&mut self) -> &mut HashMap<CoverId, Cover> {
|
||||||
|
self.modified_data();
|
||||||
&mut self.covers
|
&mut self.covers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
use std::{collections::HashSet, sync::Arc};
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
sync::{atomic::AtomicBool, Arc},
|
||||||
|
};
|
||||||
|
|
||||||
use awedio::{
|
use awedio::{
|
||||||
backends::CpalBackend,
|
backends::CpalBackend,
|
||||||
@ -20,7 +23,7 @@ pub struct Player {
|
|||||||
backend: CpalBackend,
|
backend: CpalBackend,
|
||||||
source: Option<(
|
source: Option<(
|
||||||
Controller<AsyncCompletionNotifier<Pausable<Box<dyn Sound>>>>,
|
Controller<AsyncCompletionNotifier<Pausable<Box<dyn Sound>>>>,
|
||||||
tokio::sync::oneshot::Receiver<()>,
|
Arc<AtomicBool>,
|
||||||
)>,
|
)>,
|
||||||
manager: Manager,
|
manager: Manager,
|
||||||
current_song_id: SongOpt,
|
current_song_id: SongOpt,
|
||||||
@ -82,7 +85,11 @@ impl Player {
|
|||||||
self.current_song_id = SongOpt::New(None);
|
self.current_song_id = SongOpt::New(None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn update(&mut self, db: &mut Database) {
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
db: &mut Database,
|
||||||
|
command_sender: &Arc<impl Fn(Command) + Send + Sync + 'static>,
|
||||||
|
) {
|
||||||
macro_rules! apply_command {
|
macro_rules! apply_command {
|
||||||
($cmd:expr) => {
|
($cmd:expr) => {
|
||||||
if self.allow_sending_commands {
|
if self.allow_sending_commands {
|
||||||
@ -99,9 +106,8 @@ impl Player {
|
|||||||
apply_command!(Command::Stop);
|
apply_command!(Command::Stop);
|
||||||
}
|
}
|
||||||
} else if let Some((_source, notif)) = &mut self.source {
|
} else if let Some((_source, notif)) = &mut self.source {
|
||||||
if let Ok(()) = notif.try_recv() {
|
if notif.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
// song has finished playing
|
// song has finished playing
|
||||||
apply_command!(Command::NextSong);
|
|
||||||
self.current_song_id = SongOpt::New(db.queue.get_current_song().cloned());
|
self.current_song_id = SongOpt::New(db.queue.get_current_song().cloned());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,7 +150,17 @@ impl Player {
|
|||||||
v.pausable().with_async_completion_notifier();
|
v.pausable().with_async_completion_notifier();
|
||||||
// add it
|
// add it
|
||||||
let (sound, controller) = sound.controllable();
|
let (sound, controller) = sound.controllable();
|
||||||
self.source = Some((controller, notif));
|
let finished = Arc::new(AtomicBool::new(false));
|
||||||
|
let fin = Arc::clone(&finished);
|
||||||
|
let command_sender = Arc::clone(command_sender);
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
// `blocking_recv` returns `Err(_)` when the sound is dropped, so the thread won't linger forever
|
||||||
|
if let Ok(_v) = notif.blocking_recv() {
|
||||||
|
fin.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
command_sender(Command::NextSong);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.source = Some((controller, finished));
|
||||||
// and play it
|
// and play it
|
||||||
self.manager.play(Box::new(sound));
|
self.manager.play(Box::new(sound));
|
||||||
}
|
}
|
||||||
|
@ -115,6 +115,8 @@ pub fn run_server(
|
|||||||
addr_tcp: Option<SocketAddr>,
|
addr_tcp: Option<SocketAddr>,
|
||||||
sender_sender: Option<tokio::sync::mpsc::Sender<mpsc::Sender<Command>>>,
|
sender_sender: Option<tokio::sync::mpsc::Sender<mpsc::Sender<Command>>>,
|
||||||
) {
|
) {
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
let mut player = Player::new().unwrap();
|
let mut player = Player::new().unwrap();
|
||||||
// commands sent to this will be handeled later in this function in an infinite loop.
|
// commands sent to this will be handeled later in this function in an infinite loop.
|
||||||
// these commands are sent to the database asap.
|
// these commands are sent to the database asap.
|
||||||
@ -171,11 +173,26 @@ pub fn run_server(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// for now, update the player 10 times a second so it can detect when a song has finished and start a new one.
|
let dur = Duration::from_secs(10);
|
||||||
// TODO: player should send a NextSong update to the mpsc::Sender to wake up this thread
|
let command_sender = Arc::new(move |cmd| {
|
||||||
let dur = Duration::from_secs_f32(0.1);
|
_ = command_sender.send(cmd);
|
||||||
|
});
|
||||||
loop {
|
loop {
|
||||||
player.update(&mut database.lock().unwrap());
|
{
|
||||||
|
// at the start and once after every command sent to the server,
|
||||||
|
let mut db = database.lock().unwrap();
|
||||||
|
// update the player
|
||||||
|
player.update(&mut db, &command_sender);
|
||||||
|
// autosave if necessary
|
||||||
|
if let Some((first, last)) = db.times_data_modified {
|
||||||
|
let now = Instant::now();
|
||||||
|
if (now - first).as_secs_f32() > 60.0 && (now - last).as_secs_f32() > 5.0 {
|
||||||
|
if let Err(e) = db.save_database(None) {
|
||||||
|
eprintln!("[{}] Autosave failed: {e}", "ERR!".red());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Ok(command) = command_receiver.recv_timeout(dur) {
|
if let Ok(command) = command_receiver.recv_timeout(dur) {
|
||||||
player.handle_command(&command);
|
player.handle_command(&command);
|
||||||
database.lock().unwrap().apply_command(command);
|
database.lock().unwrap().apply_command(command);
|
||||||
|
@ -15,13 +15,13 @@ use musicdb_lib::data::database::Database;
|
|||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
struct Args {
|
struct Args {
|
||||||
/// The file which contains information about the songs in your library
|
/// The directory which contains information about the songs in your library
|
||||||
#[arg()]
|
#[arg()]
|
||||||
dbfile: PathBuf,
|
db_dir: PathBuf,
|
||||||
/// The path containing your actual library.
|
/// The path containing your actual library.
|
||||||
#[arg()]
|
#[arg()]
|
||||||
lib_dir: PathBuf,
|
lib_dir: PathBuf,
|
||||||
/// skip reading the `dbfile` (because it doesn't exist yet)
|
/// skip reading the dbfile (because it doesn't exist yet)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
init: bool,
|
init: bool,
|
||||||
/// optional address for tcp connections to the server
|
/// optional address for tcp connections to the server
|
||||||
@ -42,13 +42,13 @@ async fn main() {
|
|||||||
// parse args
|
// parse args
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
let mut database = if args.init {
|
let mut database = if args.init {
|
||||||
Database::new_empty(args.dbfile, args.lib_dir)
|
Database::new_empty_in_dir(args.db_dir, args.lib_dir)
|
||||||
} else {
|
} else {
|
||||||
match Database::load_database(args.dbfile.clone(), args.lib_dir.clone()) {
|
match Database::load_database_from_dir(args.db_dir.clone(), args.lib_dir.clone()) {
|
||||||
Ok(db) => db,
|
Ok(db) => db,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Couldn't load database!");
|
eprintln!("Couldn't load database!");
|
||||||
eprintln!(" dbfile: {:?}", args.dbfile);
|
eprintln!(" dbfile: {:?}", args.db_dir);
|
||||||
eprintln!(" libdir: {:?}", args.lib_dir);
|
eprintln!(" libdir: {:?}", args.lib_dir);
|
||||||
eprintln!(" err: {}", e);
|
eprintln!(" err: {}", e);
|
||||||
exit(1);
|
exit(1);
|
||||||
|
Loading…
Reference in New Issue
Block a user