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:
Mark 2024-01-10 14:05:22 +01:00
parent 8a9ee5c9cf
commit 6f9535a28e
4 changed files with 126 additions and 39 deletions

View File

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

View File

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

View File

@ -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);

View File

@ -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);