mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-04-28 10:06:05 +02:00
1127 lines
47 KiB
Rust
Executable File
1127 lines
47 KiB
Rust
Executable File
use rand::prelude::SliceRandom;
|
|
use std::{
|
|
collections::{BTreeSet, HashMap},
|
|
fs::{self, File},
|
|
io::{BufReader, Read, Write},
|
|
path::{Path, PathBuf},
|
|
sync::{mpsc, Arc, Mutex},
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use colorize::AnsiColor;
|
|
use rand::thread_rng;
|
|
|
|
use crate::{
|
|
load::ToFromBytes,
|
|
server::{Action, Command, Commander},
|
|
};
|
|
|
|
use super::{
|
|
album::Album,
|
|
artist::Artist,
|
|
queue::{Queue, QueueContent, QueueFolder},
|
|
song::Song,
|
|
AlbumId, ArtistId, CoverId, DatabaseLocation, SongId,
|
|
};
|
|
|
|
pub struct Database {
|
|
pub seq: Commander,
|
|
/// 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.
|
|
pub db_file: PathBuf,
|
|
/// the path to the directory containing the actual music and cover image files
|
|
pub lib_directory: PathBuf,
|
|
artists: HashMap<ArtistId, Artist>,
|
|
albums: HashMap<AlbumId, Album>,
|
|
songs: HashMap<SongId, Song>,
|
|
covers: HashMap<CoverId, Cover>,
|
|
/// clients can access files in this directory if they know the relative path.
|
|
/// can be used to embed custom images in tags of songs/albums/artists.
|
|
/// None -> no access
|
|
/// Some(None) -> access to lib_directory
|
|
/// Some(Some(path)) -> access to path
|
|
pub custom_files: Option<Option<PathBuf>>,
|
|
pub queue: Queue,
|
|
/// 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.
|
|
pub update_endpoints: Vec<UpdateEndpoint>,
|
|
/// true if a song is/should be playing
|
|
pub playing: bool,
|
|
pub command_sender: Option<mpsc::Sender<Command>>,
|
|
pub remote_server_as_song_file_source:
|
|
Option<Arc<Mutex<crate::server::get::Client<Box<dyn ClientIo>>>>>,
|
|
/// only relevant for clients. true if init is done
|
|
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 {}
|
|
impl<T: Read + Write + Send> ClientIo for T {}
|
|
// for custom server implementations, this enum should allow you to deal with updates from any context (writers such as tcp streams, sync/async mpsc senders, or via closure as a fallback)
|
|
pub enum UpdateEndpoint {
|
|
Bytes(Box<dyn Write + Sync + Send>),
|
|
CmdChannel(mpsc::Sender<Arc<Command>>),
|
|
Custom(Box<dyn FnMut(&Command) + Send>),
|
|
CustomArc(Box<dyn FnMut(&Arc<Command>) + Send>),
|
|
CustomBytes(Box<dyn FnMut(&[u8]) + Send>),
|
|
}
|
|
|
|
impl Database {
|
|
pub fn is_client(&self) -> bool {
|
|
self.db_file.as_os_str().is_empty()
|
|
}
|
|
pub fn is_client_init(&self) -> bool {
|
|
self.client_is_init
|
|
}
|
|
pub fn get_path(&self, location: &DatabaseLocation) -> PathBuf {
|
|
Self::get_path_nodb(&self.lib_directory, location)
|
|
}
|
|
pub fn get_path_nodb(lib_directory: &impl AsRef<Path>, location: &DatabaseLocation) -> PathBuf {
|
|
lib_directory.as_ref().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...
|
|
pub fn get_song(&self, song: &SongId) -> Option<&Song> {
|
|
self.songs.get(song)
|
|
}
|
|
pub fn get_song_mut(&mut self, song: &SongId) -> Option<&mut Song> {
|
|
self.modified_data();
|
|
self.songs.get_mut(song)
|
|
}
|
|
/// adds a song to the database.
|
|
/// ignores song.id and just assigns a new id, which it then returns.
|
|
/// this function also adds a reference to the new song to the album (or artist.singles, if no album)
|
|
pub fn add_song_new(&mut self, song: Song) -> SongId {
|
|
let album = song.album.clone();
|
|
let artist = song.artist.clone();
|
|
let id = self.add_song_new_nomagic(song);
|
|
if let Some(Some(album)) = album.map(|v| self.albums.get_mut(&v)) {
|
|
album.songs.push(id);
|
|
} else {
|
|
if let Some(artist) = self.artists.get_mut(&artist) {
|
|
artist.singles.push(id);
|
|
}
|
|
}
|
|
id
|
|
}
|
|
/// used internally
|
|
pub fn add_song_new_nomagic(&mut self, mut song: Song) -> SongId {
|
|
self.modified_data();
|
|
for key in 0.. {
|
|
if !self.songs.contains_key(&key) {
|
|
song.id = key;
|
|
self.songs.insert(key, song);
|
|
return key;
|
|
}
|
|
}
|
|
self.panic("database.songs all keys used - no more capacity for new songs!");
|
|
}
|
|
/// adds an artist to the database.
|
|
/// ignores artist.id and just assigns a new id, which it then returns.
|
|
/// this function does nothing special.
|
|
pub fn add_artist_new(&mut self, artist: Artist) -> ArtistId {
|
|
let id = self.add_artist_new_nomagic(artist);
|
|
id
|
|
}
|
|
/// used internally
|
|
fn add_artist_new_nomagic(&mut self, mut artist: Artist) -> ArtistId {
|
|
self.modified_data();
|
|
for key in 0.. {
|
|
if !self.artists.contains_key(&key) {
|
|
artist.id = key;
|
|
self.artists.insert(key, artist);
|
|
return key;
|
|
}
|
|
}
|
|
self.panic("database.artists all keys used - no more capacity for new artists!");
|
|
}
|
|
/// adds an album to the database.
|
|
/// ignores album.id and just assigns a new id, which it then returns.
|
|
/// this function also adds a reference to the new album to the artist
|
|
pub fn add_album_new(&mut self, album: Album) -> AlbumId {
|
|
let artist = album.artist.clone();
|
|
let id = self.add_album_new_nomagic(album);
|
|
if let Some(artist) = self.artists.get_mut(&artist) {
|
|
artist.albums.push(id);
|
|
}
|
|
id
|
|
}
|
|
/// used internally
|
|
fn add_album_new_nomagic(&mut self, mut album: Album) -> AlbumId {
|
|
self.modified_data();
|
|
for key in 0.. {
|
|
if !self.albums.contains_key(&key) {
|
|
album.id = key;
|
|
self.albums.insert(key, album);
|
|
return key;
|
|
}
|
|
}
|
|
self.panic("database.artists all keys used - no more capacity for new artists!");
|
|
}
|
|
/// adds a cover to the database.
|
|
/// assigns a new id, which it then returns.
|
|
pub fn add_cover_new(&mut self, cover: Cover) -> AlbumId {
|
|
self.add_cover_new_nomagic(cover)
|
|
}
|
|
/// used internally
|
|
fn add_cover_new_nomagic(&mut self, cover: Cover) -> AlbumId {
|
|
self.modified_data();
|
|
for key in 0.. {
|
|
if !self.covers.contains_key(&key) {
|
|
self.covers.insert(key, cover);
|
|
return key;
|
|
}
|
|
}
|
|
self.panic("database.artists all keys used - no more capacity for new artists!");
|
|
}
|
|
/// updates an existing song in the database with the new value.
|
|
/// uses song.id to find the correct song.
|
|
/// if the id doesn't exist in the db, Err(()) is returned.
|
|
/// Otherwise Some(old_data) is returned.
|
|
pub fn update_song(&mut self, mut song: Song) -> Result<Song, ()> {
|
|
if let Some(prev_song) = self.songs.remove(&song.id) {
|
|
self.modified_data();
|
|
if song.album != prev_song.album || song.artist != prev_song.artist {
|
|
// remove previous song from album/artist
|
|
if let Some(a) = prev_song.album {
|
|
if let Some(a) = self.albums.get_mut(&a) {
|
|
if let Some(i) = a.songs.iter().position(|s| *s == song.id) {
|
|
a.songs.remove(i);
|
|
} else {
|
|
eprintln!(
|
|
"[{}] Couldn't remove Song {} from previous album, because the album with the ID {} didn't contain that song.",
|
|
"WARN".yellow(),
|
|
song.id,
|
|
a.id
|
|
);
|
|
}
|
|
} else {
|
|
eprintln!(
|
|
"[{}] Couldn't remove Song {} from previous album, because no album with the ID {} was found.",
|
|
"ERR!".red(),
|
|
song.id,
|
|
a
|
|
);
|
|
}
|
|
} else {
|
|
if let Some(a) = self.artists.get_mut(&prev_song.artist) {
|
|
if let Some(i) = a.singles.iter().position(|s| *s == song.id) {
|
|
a.singles.remove(i);
|
|
} else {
|
|
eprintln!("[{}] Couldn't remove Song {} from Artist {} singles, because that song wasn't found in that artist.", "WARN".yellow(), song.id, prev_song.artist);
|
|
}
|
|
} else {
|
|
eprintln!("[{}] Couldn't remove Song {} from Artist {} singles, because that artist wasn't found.", "ERR!".red(), song.id, prev_song.artist);
|
|
}
|
|
}
|
|
// add new song to album/artist
|
|
if let Some(a) = song.album {
|
|
if let Some(a) = self.albums.get_mut(&a) {
|
|
if song.artist != a.artist {
|
|
eprintln!("[{}] Changing song's artist because it doesn't match the specified album's artist.", "WARN".yellow());
|
|
song.artist = a.artist;
|
|
}
|
|
if !a.songs.contains(&song.id) {
|
|
a.songs.push(song.id);
|
|
}
|
|
} else {
|
|
eprintln!(
|
|
"[{}] Couldn't add Song {} to new album, because no album with the ID {} was found.",
|
|
"ERR!".red(),
|
|
song.id, a
|
|
);
|
|
}
|
|
} else {
|
|
if let Some(a) = self.artists.get_mut(&song.artist) {
|
|
if !a.singles.contains(&song.id) {
|
|
a.singles.push(song.id);
|
|
}
|
|
} else {
|
|
eprintln!("[{}] Couldn't add Song {} to Artist {} singles, because that artist wasn't found.", "ERR!".red(), song.id, song.artist);
|
|
}
|
|
}
|
|
}
|
|
|
|
self.songs.insert(song.id, song);
|
|
Ok(prev_song)
|
|
} else {
|
|
eprintln!(
|
|
"[{}] Couldn't update Song {}, because no song with that ID exists.",
|
|
"WARN".yellow(),
|
|
song.id,
|
|
);
|
|
Err(())
|
|
}
|
|
}
|
|
pub fn update_album(&mut self, album: Album) -> Result<Album, ()> {
|
|
if let Some(prev_album) = self.albums.remove(&album.id) {
|
|
// some checks
|
|
let new_songs = album.songs.iter().copied().collect::<BTreeSet<_>>();
|
|
let prev_songs = album.songs.iter().copied().collect::<BTreeSet<_>>();
|
|
// check if we would end up with songs that aren't referenced anywhere, and, if yes, don't do anything.
|
|
if prev_songs.difference(&new_songs).next().is_some() {
|
|
eprintln!("[{}] Can't update Album {} because some songs that used to be in this album are not included in the new data.", "ERR!".red(), album.id);
|
|
return Err(());
|
|
}
|
|
|
|
// change artist
|
|
if prev_album.artist != album.artist {
|
|
// remove album from previous artist
|
|
if let Some(prev_artist) = self.artists.get_mut(&prev_album.artist) {
|
|
if let Some(i) = prev_artist.albums.iter().position(|a| *a != prev_album.id) {
|
|
prev_artist.albums.remove(i);
|
|
} else {
|
|
eprintln!(
|
|
"[{}] Couldn't remove Album {} from Artist {}, because it was not listed as an album in that artist.",
|
|
"ERR!".red(),
|
|
prev_album.id,
|
|
prev_album.artist
|
|
);
|
|
}
|
|
} else {
|
|
eprintln!(
|
|
"[{}] Couldn't remove Album {} from Artist {}, because no artist with that ID exists.",
|
|
"ERR!".red(),
|
|
prev_album.id,
|
|
prev_album.artist
|
|
);
|
|
}
|
|
// add album to new artist
|
|
if let Some(artist) = self.artists.get_mut(&album.artist) {
|
|
if !artist.albums.contains(&album.id) {
|
|
artist.albums.push(album.id);
|
|
} else {
|
|
eprintln!(
|
|
"[{}] Couldn't add Album {} to Artist {}, because the album was already added (this should never happen...).",
|
|
"WARN".yellow(),
|
|
album.id,
|
|
album.artist
|
|
);
|
|
}
|
|
} else {
|
|
eprintln!(
|
|
"[{}] Couldn't add Album {} to Artist {}, because no artist with that ID exists.",
|
|
"ERR!".red(),
|
|
album.id,
|
|
album.artist
|
|
);
|
|
}
|
|
// change artist of songs in album (if album artist is changed AND album has gotten more songs, this will be done twice for some songs, but that is okay)
|
|
for song in &album.songs {
|
|
if let Some(song) = self.songs.get_mut(song) {
|
|
song.artist = album.artist;
|
|
} else {
|
|
eprintln!(
|
|
"[{}] Couldn't change Song {} artist to Artist {}, because no song with that ID exists (changing because album artist was changed).",
|
|
"ERR!".red(),
|
|
song,
|
|
album.artist
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// change artist & album of songs that were previously not in this album
|
|
for song in new_songs.difference(&prev_songs) {
|
|
if let Some(song) = self.songs.get_mut(song) {
|
|
// change song's artist to that of this album
|
|
song.artist = album.artist;
|
|
// if song was previously in another album, remove it from that album
|
|
// it will be added to this new album because its id is already in `album`, so we don't need to do anything to achieve that.
|
|
if let Some(prev_album) = song.album {
|
|
if prev_album != album.id {
|
|
// remove song from its previous album
|
|
if let Some(prev_album) = self.albums.get_mut(&prev_album) {
|
|
if let Some(i) = prev_album.songs.iter().position(|s| *s == song.id)
|
|
{
|
|
prev_album.songs.remove(i);
|
|
} else {
|
|
eprintln!(
|
|
"[{}] Couldn't remove Song {} from its previous album, Album {}, because no song with that ID exists in that album.",
|
|
"WARN".yellow(),
|
|
song.id,
|
|
prev_album.id
|
|
);
|
|
}
|
|
} else {
|
|
eprintln!(
|
|
"[{}] Couldn't remove Song {} from its previous album, Album {}, because no album with that ID exists.",
|
|
"WARN".yellow(),
|
|
song.id,
|
|
prev_album
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
eprintln!(
|
|
"[{}] Couldn't remove Song {} from its previous album because no song with that ID exists.",
|
|
"ERR!".red(),
|
|
*song,
|
|
);
|
|
}
|
|
}
|
|
|
|
self.albums.insert(album.id, album);
|
|
self.modified_data();
|
|
Ok(prev_album)
|
|
} else {
|
|
eprintln!(
|
|
"[{}] Couldn't update Album {}, because no album with that ID exists.",
|
|
"WARN".yellow(),
|
|
album.id,
|
|
);
|
|
Err(())
|
|
}
|
|
}
|
|
pub fn update_artist(&mut self, artist: Artist) -> Result<Artist, ()> {
|
|
if let Some(prev_artist) = self.artists.remove(&artist.id) {
|
|
self.modified_data();
|
|
|
|
let prev_albums = prev_artist.albums.iter().copied().collect::<BTreeSet<_>>();
|
|
let new_albums = artist.albums.iter().copied().collect::<BTreeSet<_>>();
|
|
if prev_albums.difference(&new_albums).next().is_some() {
|
|
eprintln!("[{}] Can't update Artist {} because some albums that used to be in this artist are not included in the new data.", "ERR!".red(), artist.id);
|
|
return Err(());
|
|
}
|
|
|
|
let prev_singles = prev_artist.singles.iter().copied().collect::<BTreeSet<_>>();
|
|
let new_singles = artist.singles.iter().copied().collect::<BTreeSet<_>>();
|
|
if prev_singles.difference(&new_singles).next().is_some() {
|
|
eprintln!("[{}] Can't update Artist {} because some singles that used to be in this artist are not included in the new data.", "ERR!".red(), artist.id);
|
|
return Err(());
|
|
}
|
|
|
|
// change artist of newly added albums and their songs
|
|
for album in new_albums.difference(&prev_albums) {
|
|
if let Some(album) = self.albums.get_mut(album) {
|
|
if let Some(a) = self.artists.get_mut(&album.artist) {
|
|
if let Some(i) = a.albums.iter().position(|a| *a == album.id) {
|
|
a.albums.remove(i);
|
|
} else {
|
|
eprintln!("[{}] Couldn't remove Album {} from Artist {} because that artist doesn't contain that album.", "ERR!".red(), album.id, album.artist);
|
|
}
|
|
} else {
|
|
eprintln!("[{}] Couldn't remove Album {} from Artist {} because that artist doesn't exist.", "ERR!".red(), album.id, album.artist);
|
|
}
|
|
album.artist = artist.id;
|
|
for song in &album.songs {
|
|
if let Some(song) = self.songs.get_mut(song) {
|
|
song.artist = artist.id;
|
|
} else {
|
|
eprintln!("[{}] Couldn't change Song {} artist to Artist {} because no song with that ID exists (should change because song is newly added to Album {}).", "ERR!".red(), song, artist.id, album.id);
|
|
}
|
|
}
|
|
} else {
|
|
eprintln!("[{}] Couldn't move Album {} to Artist {} because no album with that ID exists.", "ERR!".red(), album, artist.id);
|
|
}
|
|
}
|
|
|
|
// change artist of new singles
|
|
for song in new_singles.difference(&prev_singles) {
|
|
if let Some(song) = self.songs.get_mut(song) {
|
|
// remove song from previous album or artist
|
|
if let Some(a) = &song.album {
|
|
if let Some(a) = self.albums.get_mut(a) {
|
|
if let Some(i) = a.songs.iter().position(|s| *s == song.id) {
|
|
a.songs.remove(i);
|
|
} else {
|
|
eprintln!("[{}] Couldn't remove Song {} from Album {} because the album doesn't contain that song.", "ERR!".red(), song.id, a.id);
|
|
}
|
|
} else {
|
|
eprintln!("[{}] Couldn't remove Song {} from Album {} because no album with that ID exists.", "ERR!".red(), song.id, a);
|
|
}
|
|
} else {
|
|
if let Some(a) = self.artists.get_mut(&song.artist) {
|
|
if let Some(i) = a.singles.iter().position(|s| *s == song.id) {
|
|
a.singles.remove(i);
|
|
} else {
|
|
eprintln!("[{}] Couldn't remove Song {} from Artist {} because the artist doesn't contain that song.", "ERR!".red(), song.id, a.id);
|
|
}
|
|
} else {
|
|
eprintln!("[{}] Couldn't remove Song {} from Artist {} because no artist with that ID exists.", "ERR!".red(), song.id, song.artist);
|
|
}
|
|
}
|
|
song.artist = artist.id;
|
|
} else {
|
|
eprintln!("[{}] Couldn't move Song {} to Artist {} singles because no song with that ID exists.", "ERR!".red(), song, artist.id);
|
|
}
|
|
}
|
|
|
|
self.artists.insert(artist.id, artist);
|
|
Ok(prev_artist)
|
|
} else {
|
|
Err(())
|
|
}
|
|
}
|
|
/// [NOT RECOMMENDED - use add_song_new or update_song instead!] inserts the song into the database.
|
|
/// 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.
|
|
pub fn update_or_add_song(&mut self, song: Song) -> Option<Song> {
|
|
self.modified_data();
|
|
self.songs.insert(song.id, song)
|
|
}
|
|
|
|
pub fn remove_song(&mut self, song: SongId) -> Option<Song> {
|
|
if let Some(removed) = self.songs.remove(&song) {
|
|
self.modified_data();
|
|
Some(removed)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
pub fn remove_album(&mut self, song: SongId) -> Option<Song> {
|
|
if let Some(removed) = self.songs.remove(&song) {
|
|
self.modified_data();
|
|
Some(removed)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
pub fn remove_artist(&mut self, song: SongId) -> Option<Song> {
|
|
if let Some(removed) = self.songs.remove(&song) {
|
|
self.modified_data();
|
|
Some(removed)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub fn init_connection<T: Write>(&self, con: &mut T) -> Result<(), std::io::Error> {
|
|
// TODO! this is slow because it clones everything - there has to be a better way...
|
|
self.seq
|
|
.pack(Action::SyncDatabase(
|
|
self.artists().iter().map(|v| v.1.clone()).collect(),
|
|
self.albums().iter().map(|v| v.1.clone()).collect(),
|
|
self.songs().iter().map(|v| v.1.clone()).collect(),
|
|
))
|
|
.to_bytes(con)?;
|
|
self.seq
|
|
.pack(Action::QueueUpdate(vec![], self.queue.clone()))
|
|
.to_bytes(con)?;
|
|
if self.playing {
|
|
self.seq.pack(Action::Resume).to_bytes(con)?;
|
|
}
|
|
// this allows clients to find out when init_connection is done.
|
|
self.seq.pack(Action::InitComplete).to_bytes(con)?;
|
|
// is initialized now - client can receive updates after this point.
|
|
// NOTE: Don't write to connection anymore - the db will dispatch updates on its own.
|
|
// we just need to handle commands (receive from the connection).
|
|
Ok(())
|
|
}
|
|
|
|
/// `apply_action_unchecked_seq(command.action)` if `command.seq` is correct or `0xFF`
|
|
pub fn apply_command(&mut self, command: Command) {
|
|
if command.seq != self.seq.seq() && command.seq != 0xFF {
|
|
eprintln!(
|
|
"Invalid sequence number: got {} but expected {}.",
|
|
command.seq,
|
|
self.seq.seq()
|
|
);
|
|
return;
|
|
}
|
|
self.apply_action_unchecked_seq(command.action)
|
|
}
|
|
pub fn apply_action_unchecked_seq(&mut self, mut action: Action) {
|
|
if !self.is_client() {
|
|
if let Action::ErrorInfo(t, _) = &mut action {
|
|
// clients can send ErrorInfo to the server and it will show up on other clients,
|
|
// BUT only the server can set the Title of the ErrorInfo.
|
|
t.clear();
|
|
}
|
|
}
|
|
// some commands shouldn't be broadcast. these will broadcast a different command in their specific implementation.
|
|
match &action {
|
|
// Will broadcast `QueueSetShuffle`
|
|
Action::QueueShuffle(_) => (),
|
|
Action::NextSong if self.queue.is_almost_empty() => (),
|
|
Action::Pause if !self.playing => (),
|
|
Action::Resume if self.playing => (),
|
|
// since db.update_endpoints is empty for clients, this won't cause unwanted back and forth
|
|
_ => action = self.broadcast_update(action),
|
|
}
|
|
match action {
|
|
Action::Resume => self.playing = true,
|
|
Action::Pause => self.playing = false,
|
|
Action::Stop => self.playing = false,
|
|
Action::NextSong => {
|
|
if !Queue::advance_index_db(self) {
|
|
// end of queue
|
|
self.apply_action_unchecked_seq(Action::Pause);
|
|
self.queue.init();
|
|
}
|
|
}
|
|
Action::Save => {
|
|
if let Err(e) = self.save_database(None) {
|
|
eprintln!("[{}] Couldn't save: {e}", "ERR!".red());
|
|
}
|
|
}
|
|
Action::SyncDatabase(a, b, c) => self.sync(a, b, c),
|
|
Action::QueueUpdate(index, new_data) => {
|
|
if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) {
|
|
*v = new_data;
|
|
}
|
|
}
|
|
Action::QueueAdd(index, new_data) => {
|
|
if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) {
|
|
v.add_to_end(new_data, false);
|
|
}
|
|
}
|
|
Action::QueueInsert(index, pos, new_data) => {
|
|
if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) {
|
|
v.insert(new_data, pos, false);
|
|
}
|
|
}
|
|
Action::QueueRemove(index) => {
|
|
self.queue.remove_by_index(&index, 0);
|
|
}
|
|
Action::QueueMove(index_from, mut index_to) => 'queue_move: {
|
|
if index_to.len() == 0 || index_to.starts_with(&index_from) {
|
|
break 'queue_move;
|
|
}
|
|
// if same parent path, perform folder move operation instead
|
|
if index_from[0..index_from.len() - 1] == index_to[0..index_to.len() - 1] {
|
|
if let Some(parent) = self
|
|
.queue
|
|
.get_item_at_index_mut(&index_from[0..index_from.len() - 1], 0)
|
|
{
|
|
if let QueueContent::Folder(folder) = parent.content_mut() {
|
|
let i1 = index_from[index_from.len() - 1];
|
|
let mut i2 = index_to[index_to.len() - 1];
|
|
if i2 > i1 {
|
|
i2 -= 1;
|
|
}
|
|
// this preserves "is currently active queue element" status
|
|
folder.move_elem(i1, i2);
|
|
break 'queue_move;
|
|
}
|
|
}
|
|
}
|
|
// otherwise, remove then insert
|
|
let was_current = self.queue.is_current(&index_from);
|
|
if let Some(elem) = self.queue.remove_by_index(&index_from, 0) {
|
|
if index_to.len() >= index_from.len()
|
|
&& index_to.starts_with(&index_from[0..index_from.len() - 1])
|
|
&& index_to[index_from.len() - 1] > index_from[index_from.len() - 1]
|
|
{
|
|
index_to[index_from.len() - 1] -= 1;
|
|
}
|
|
if let Some(parent) = self
|
|
.queue
|
|
.get_item_at_index_mut(&index_to[0..index_to.len() - 1], 0)
|
|
{
|
|
parent.insert(vec![elem], index_to[index_to.len() - 1], true);
|
|
if was_current {
|
|
self.queue.set_index_inner(&index_to, 0, vec![], true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Action::QueueMoveInto(index_from, mut parent_to) => 'queue_move_into: {
|
|
if parent_to.starts_with(&index_from) {
|
|
break 'queue_move_into;
|
|
}
|
|
// remove then insert
|
|
let was_current = self.queue.is_current(&index_from);
|
|
if let Some(elem) = self.queue.remove_by_index(&index_from, 0) {
|
|
if parent_to.len() >= index_from.len()
|
|
&& parent_to.starts_with(&index_from[0..index_from.len() - 1])
|
|
&& parent_to[index_from.len() - 1] > index_from[index_from.len() - 1]
|
|
{
|
|
parent_to[index_from.len() - 1] -= 1;
|
|
}
|
|
if let Some(parent) = self.queue.get_item_at_index_mut(&parent_to, 0) {
|
|
if let Some(i) = parent.add_to_end(vec![elem], true) {
|
|
if was_current {
|
|
parent_to.push(i);
|
|
self.queue.set_index_inner(&parent_to, 0, vec![], true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Action::QueueGoto(index) => Queue::set_index_db(self, &index),
|
|
Action::QueueShuffle(path) => {
|
|
if let Some(elem) = self.queue.get_item_at_index_mut(&path, 0) {
|
|
if let QueueContent::Folder(QueueFolder {
|
|
index: _,
|
|
content,
|
|
name: _,
|
|
order: _,
|
|
}) = elem.content_mut()
|
|
{
|
|
let mut ord: Vec<usize> = (0..content.len()).collect();
|
|
ord.shuffle(&mut thread_rng());
|
|
self.apply_action_unchecked_seq(Action::QueueSetShuffle(path, ord));
|
|
} else {
|
|
eprintln!("(QueueShuffle) QueueElement at {path:?} not a folder!");
|
|
}
|
|
} else {
|
|
eprintln!("(QueueShuffle) No QueueElement at {path:?}");
|
|
}
|
|
}
|
|
Action::QueueSetShuffle(path, ord) => {
|
|
if let Some(elem) = self.queue.get_item_at_index_mut(&path, 0) {
|
|
if let QueueContent::Folder(QueueFolder {
|
|
index,
|
|
content,
|
|
name: _,
|
|
order,
|
|
}) = elem.content_mut()
|
|
{
|
|
if ord.len() == content.len() {
|
|
if let Some(ni) = ord.iter().position(|v| *v == *index) {
|
|
*index = ni;
|
|
}
|
|
*order = Some(ord);
|
|
} else {
|
|
eprintln!(
|
|
"[warn] can't QueueSetShuffle - length of new ord ({}) is not the same as length of content ({})!",
|
|
ord.len(),
|
|
content.len()
|
|
);
|
|
}
|
|
} else {
|
|
eprintln!(
|
|
"[warn] can't QueueSetShuffle - element at path {path:?} isn't a folder"
|
|
);
|
|
}
|
|
} else {
|
|
eprintln!(
|
|
"[{}] can't QueueSetShuffle - no element at path {path:?}",
|
|
"WARN".yellow()
|
|
);
|
|
}
|
|
}
|
|
Action::QueueUnshuffle(path) => {
|
|
if let Some(elem) = self.queue.get_item_at_index_mut(&path, 0) {
|
|
if let QueueContent::Folder(QueueFolder {
|
|
index,
|
|
content: _,
|
|
name: _,
|
|
order,
|
|
}) = elem.content_mut()
|
|
{
|
|
if let Some(ni) = order.as_ref().and_then(|v| v.get(*index).copied()) {
|
|
*index = ni;
|
|
}
|
|
*order = None;
|
|
}
|
|
}
|
|
}
|
|
Action::AddSong(song) => {
|
|
self.add_song_new(song);
|
|
}
|
|
Action::AddAlbum(album) => {
|
|
self.add_album_new(album);
|
|
}
|
|
Action::AddArtist(artist) => {
|
|
self.add_artist_new(artist);
|
|
}
|
|
Action::AddCover(cover) => _ = self.add_cover_new(cover),
|
|
Action::ModifySong(song) => {
|
|
_ = self.update_song(song);
|
|
}
|
|
Action::ModifyAlbum(album) => {
|
|
_ = self.update_album(album);
|
|
}
|
|
Action::ModifyArtist(artist) => {
|
|
_ = self.update_artist(artist);
|
|
}
|
|
Action::RemoveSong(song) => {
|
|
_ = self.remove_song(song);
|
|
}
|
|
Action::RemoveAlbum(album) => {
|
|
_ = self.remove_album(album);
|
|
}
|
|
Action::RemoveArtist(artist) => {
|
|
_ = self.remove_artist(artist);
|
|
}
|
|
Action::TagSongFlagSet(id, tag) => {
|
|
if let Some(v) = self.get_song_mut(&id) {
|
|
if !v.general.tags.contains(&tag) {
|
|
v.general.tags.push(tag);
|
|
}
|
|
}
|
|
}
|
|
Action::TagSongFlagUnset(id, tag) => {
|
|
if let Some(v) = self.get_song_mut(&id) {
|
|
if let Some(i) = v.general.tags.iter().position(|v| v == &tag) {
|
|
v.general.tags.remove(i);
|
|
}
|
|
}
|
|
}
|
|
Action::TagAlbumFlagSet(id, tag) => {
|
|
if let Some(v) = self.albums.get_mut(&id) {
|
|
if !v.general.tags.contains(&tag) {
|
|
v.general.tags.push(tag);
|
|
}
|
|
}
|
|
}
|
|
Action::TagAlbumFlagUnset(id, tag) => {
|
|
if let Some(v) = self.albums.get_mut(&id) {
|
|
if let Some(i) = v.general.tags.iter().position(|v| v == &tag) {
|
|
v.general.tags.remove(i);
|
|
}
|
|
}
|
|
}
|
|
Action::TagArtistFlagSet(id, tag) => {
|
|
if let Some(v) = self.artists.get_mut(&id) {
|
|
if !v.general.tags.contains(&tag) {
|
|
v.general.tags.push(tag);
|
|
}
|
|
}
|
|
}
|
|
Action::TagArtistFlagUnset(id, tag) => {
|
|
if let Some(v) = self.artists.get_mut(&id) {
|
|
if let Some(i) = v.general.tags.iter().position(|v| v == &tag) {
|
|
v.general.tags.remove(i);
|
|
}
|
|
}
|
|
}
|
|
Action::TagSongPropertySet(id, key, val) => {
|
|
if let Some(v) = self.get_song_mut(&id) {
|
|
let new = format!("{key}{val}");
|
|
if let Some(v) = v.general.tags.iter_mut().find(|v| v.starts_with(&key)) {
|
|
*v = new;
|
|
} else {
|
|
v.general.tags.push(new);
|
|
}
|
|
}
|
|
}
|
|
Action::TagSongPropertyUnset(id, key) => {
|
|
if let Some(v) = self.get_song_mut(&id) {
|
|
let tags = std::mem::replace(&mut v.general.tags, vec![]);
|
|
v.general.tags = tags.into_iter().filter(|v| !v.starts_with(&key)).collect();
|
|
}
|
|
}
|
|
Action::TagAlbumPropertySet(id, key, val) => {
|
|
if let Some(v) = self.albums.get_mut(&id) {
|
|
let new = format!("{key}{val}");
|
|
if let Some(v) = v.general.tags.iter_mut().find(|v| v.starts_with(&key)) {
|
|
*v = new;
|
|
} else {
|
|
v.general.tags.push(new);
|
|
}
|
|
}
|
|
}
|
|
Action::TagAlbumPropertyUnset(id, key) => {
|
|
if let Some(v) = self.albums.get_mut(&id) {
|
|
let tags = std::mem::replace(&mut v.general.tags, vec![]);
|
|
v.general.tags = tags.into_iter().filter(|v| !v.starts_with(&key)).collect();
|
|
}
|
|
}
|
|
Action::TagArtistPropertySet(id, key, val) => {
|
|
if let Some(v) = self.artists.get_mut(&id) {
|
|
let new = format!("{key}{val}");
|
|
if let Some(v) = v.general.tags.iter_mut().find(|v| v.starts_with(&key)) {
|
|
*v = new;
|
|
} else {
|
|
v.general.tags.push(new);
|
|
}
|
|
}
|
|
}
|
|
Action::TagArtistPropertyUnset(id, key) => {
|
|
if let Some(v) = self.artists.get_mut(&id) {
|
|
let tags = std::mem::replace(&mut v.general.tags, vec![]);
|
|
v.general.tags = tags.into_iter().filter(|v| !v.starts_with(&key)).collect();
|
|
}
|
|
}
|
|
Action::SetSongDuration(id, duration) => {
|
|
if let Some(song) = self.get_song_mut(&id) {
|
|
song.duration_millis = duration;
|
|
}
|
|
}
|
|
Action::InitComplete => {
|
|
self.client_is_init = true;
|
|
}
|
|
Action::ErrorInfo(..) => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
// file saving/loading
|
|
|
|
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.
|
|
/// A client database doesn't need any storage paths and won't perform autosaves.
|
|
pub fn new_clientside() -> Self {
|
|
Self {
|
|
seq: Commander::new(true),
|
|
db_dir: PathBuf::new(),
|
|
db_file: PathBuf::new(),
|
|
lib_directory: PathBuf::new(),
|
|
artists: HashMap::new(),
|
|
albums: HashMap::new(),
|
|
songs: HashMap::new(),
|
|
covers: HashMap::new(),
|
|
custom_files: None,
|
|
queue: QueueContent::Folder(QueueFolder::default()).into(),
|
|
update_endpoints: vec![],
|
|
playing: false,
|
|
command_sender: None,
|
|
remote_server_as_song_file_source: None,
|
|
client_is_init: false,
|
|
times_data_modified: None,
|
|
}
|
|
}
|
|
pub fn new_empty_in_dir(dir: PathBuf, lib_dir: PathBuf) -> Self {
|
|
let path = dir.join("dbfile");
|
|
Self {
|
|
seq: Commander::new(false),
|
|
db_dir: dir,
|
|
db_file: path,
|
|
lib_directory: lib_dir,
|
|
artists: HashMap::new(),
|
|
albums: HashMap::new(),
|
|
songs: HashMap::new(),
|
|
covers: HashMap::new(),
|
|
custom_files: None,
|
|
queue: QueueContent::Folder(QueueFolder::default()).into(),
|
|
update_endpoints: vec![],
|
|
playing: false,
|
|
command_sender: None,
|
|
remote_server_as_song_file_source: None,
|
|
client_is_init: false,
|
|
times_data_modified: None,
|
|
}
|
|
}
|
|
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)?);
|
|
eprintln!("[{}] loading library from {file:?}", "INFO".cyan());
|
|
let s = Self {
|
|
seq: Commander::new(false),
|
|
db_dir: dir,
|
|
db_file: path,
|
|
lib_directory,
|
|
artists: ToFromBytes::from_bytes(&mut file)?,
|
|
albums: ToFromBytes::from_bytes(&mut file)?,
|
|
songs: ToFromBytes::from_bytes(&mut file)?,
|
|
covers: ToFromBytes::from_bytes(&mut file)?,
|
|
custom_files: None,
|
|
queue: QueueContent::Folder(QueueFolder::default()).into(),
|
|
update_endpoints: vec![],
|
|
playing: false,
|
|
command_sender: None,
|
|
remote_server_as_song_file_source: None,
|
|
client_is_init: false,
|
|
times_data_modified: None,
|
|
};
|
|
eprintln!("[{}] loaded library", "INFO".green());
|
|
Ok(s)
|
|
}
|
|
/// saves the database's contents. save path can be overridden
|
|
pub fn save_database(&mut self, path: Option<PathBuf>) -> Result<PathBuf, std::io::Error> {
|
|
let path = if let Some(p) = path {
|
|
p
|
|
} else {
|
|
self.db_file.clone()
|
|
};
|
|
// if no path is set (client mode), do nothing
|
|
if path.as_os_str().is_empty() {
|
|
return Ok(path);
|
|
}
|
|
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()
|
|
.write(true)
|
|
.truncate(true)
|
|
.create(true)
|
|
.open(&path)?;
|
|
self.artists.to_bytes(&mut file)?;
|
|
self.albums.to_bytes(&mut file)?;
|
|
self.songs.to_bytes(&mut file)?;
|
|
self.covers.to_bytes(&mut file)?;
|
|
eprintln!("[{}] saved db", "INFO".green());
|
|
// all changes saved, data no longer modified
|
|
self.times_data_modified = None;
|
|
Ok(path)
|
|
}
|
|
pub fn broadcast_update(&mut self, update: Action) -> Action {
|
|
match update {
|
|
Action::InitComplete => return update,
|
|
_ => {}
|
|
}
|
|
if !self.is_client() {
|
|
self.seq.inc();
|
|
}
|
|
let update = self.seq.pack(update);
|
|
let mut remove = vec![];
|
|
let mut bytes = None;
|
|
let mut arc = None;
|
|
for (i, udep) in self.update_endpoints.iter_mut().enumerate() {
|
|
match udep {
|
|
UpdateEndpoint::Bytes(writer) => {
|
|
if bytes.is_none() {
|
|
bytes = Some(update.to_bytes_vec());
|
|
}
|
|
if writer.write_all(bytes.as_ref().unwrap()).is_err() {
|
|
remove.push(i);
|
|
}
|
|
}
|
|
UpdateEndpoint::CmdChannel(sender) => {
|
|
if arc.is_none() {
|
|
arc = Some(Arc::new(update.clone()));
|
|
}
|
|
if sender.send(arc.clone().unwrap()).is_err() {
|
|
remove.push(i);
|
|
}
|
|
}
|
|
UpdateEndpoint::Custom(func) => func(&update),
|
|
UpdateEndpoint::CustomArc(func) => {
|
|
if arc.is_none() {
|
|
arc = Some(Arc::new(update.clone()));
|
|
}
|
|
func(arc.as_ref().unwrap())
|
|
}
|
|
UpdateEndpoint::CustomBytes(func) => {
|
|
if bytes.is_none() {
|
|
bytes = Some(update.to_bytes_vec());
|
|
}
|
|
func(bytes.as_ref().unwrap())
|
|
}
|
|
}
|
|
}
|
|
if !remove.is_empty() {
|
|
eprintln!(
|
|
"[info] closing {} connections, {} are still active",
|
|
remove.len(),
|
|
self.update_endpoints.len() - remove.len()
|
|
);
|
|
for i in remove.into_iter().rev() {
|
|
self.update_endpoints.remove(i);
|
|
}
|
|
}
|
|
update.action
|
|
}
|
|
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.albums = albums.iter().map(|v| (v.id, v.clone())).collect();
|
|
self.songs = songs.iter().map(|v| (v.id, v.clone())).collect();
|
|
}
|
|
}
|
|
|
|
impl Database {
|
|
pub fn songs(&self) -> &HashMap<SongId, Song> {
|
|
&self.songs
|
|
}
|
|
pub fn albums(&self) -> &HashMap<AlbumId, Album> {
|
|
&self.albums
|
|
}
|
|
pub fn artists(&self) -> &HashMap<ArtistId, Artist> {
|
|
&self.artists
|
|
}
|
|
pub fn covers(&self) -> &HashMap<CoverId, Cover> {
|
|
&self.covers
|
|
}
|
|
/// you should probably use a Command to do this...
|
|
pub fn songs_mut(&mut self) -> &mut HashMap<SongId, Song> {
|
|
self.modified_data();
|
|
&mut self.songs
|
|
}
|
|
/// you should probably use a Command to do this...
|
|
pub fn albums_mut(&mut self) -> &mut HashMap<AlbumId, Album> {
|
|
self.modified_data();
|
|
&mut self.albums
|
|
}
|
|
/// you should probably use a Command to do this...
|
|
pub fn artists_mut(&mut self) -> &mut HashMap<ArtistId, Artist> {
|
|
self.modified_data();
|
|
&mut self.artists
|
|
}
|
|
/// you should probably use a Command to do this...
|
|
pub fn covers_mut(&mut self) -> &mut HashMap<CoverId, Cover> {
|
|
self.modified_data();
|
|
&mut self.covers
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct Cover {
|
|
pub location: DatabaseLocation,
|
|
pub data: Arc<Mutex<(bool, Option<(Instant, Vec<u8>)>)>>,
|
|
}
|
|
impl Cover {
|
|
pub fn get_bytes_from_file<O>(
|
|
&self,
|
|
path: impl FnOnce(&DatabaseLocation) -> PathBuf,
|
|
conv: impl FnOnce(&Vec<u8>) -> O,
|
|
) -> Option<O> {
|
|
let mut data = loop {
|
|
let data = self.data.lock().unwrap();
|
|
if data.0 {
|
|
drop(data);
|
|
std::thread::sleep(Duration::from_secs(1));
|
|
} else {
|
|
break data;
|
|
}
|
|
};
|
|
if let Some((accessed, data)) = &mut data.1 {
|
|
*accessed = Instant::now();
|
|
Some(conv(&data))
|
|
} else {
|
|
match std::fs::read(path(&self.location)) {
|
|
Ok(bytes) => {
|
|
data.1 = Some((Instant::now(), bytes));
|
|
Some(conv(&data.1.as_ref().unwrap().1))
|
|
}
|
|
Err(_) => None,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
impl ToFromBytes for Cover {
|
|
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
|
where
|
|
T: Write,
|
|
{
|
|
self.location.to_bytes(s)
|
|
}
|
|
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
|
where
|
|
T: std::io::Read,
|
|
{
|
|
Ok(Self {
|
|
location: ToFromBytes::from_bytes(s)?,
|
|
data: Arc::new(Mutex::new((false, None))),
|
|
})
|
|
}
|
|
}
|