Database works

This commit is contained in:
G2-Games 2023-09-30 23:31:42 -05:00
parent 7f57367aad
commit db53bed111
5 changed files with 260 additions and 181 deletions

View file

@ -16,7 +16,7 @@ pub struct Config {
impl Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
let path = PathBuf::from("./music_database.db3"); let path = PathBuf::from("./music_database");
return Config { return Config {
db_path: Box::new(path), db_path: Box::new(path),

View file

@ -1,8 +1,6 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{RwLock, Arc}; use std::sync::{RwLock, Arc};
use rusqlite::Result;
use crate::music_controller::config::Config; use crate::music_controller::config::Config;
use crate::music_player::music_player::{MusicPlayer, PlayerStatus, DecoderMessage, DSPMessage}; use crate::music_player::music_player::{MusicPlayer, PlayerStatus, DecoderMessage, DSPMessage};
use crate::music_storage::music_db::Song; use crate::music_storage::music_db::Song;

View file

@ -232,7 +232,7 @@ impl MusicPlayer {
// Handles message received from MusicPlayer struct // Handles message received from MusicPlayer struct
match message { match message {
Some(DecoderMessage::OpenSong(song)) => { Some(DecoderMessage::OpenSong(song)) => {
let song_uri = song.path.clone(); let song_uri = song.location.clone();
match SongHandler::new(&song_uri) { match SongHandler::new(&song_uri) {
Ok(new_handler) => { Ok(new_handler) => {
song_handler = Some(new_handler); song_handler = Some(new_handler);

View file

@ -1,31 +1,33 @@
use file_format::{FileFormat, Kind}; use file_format::{FileFormat, Kind};
use lofty::{AudioFile, Probe, TaggedFileExt, ItemKey, ItemValue, TagType}; use lofty::{AudioFile, ItemKey, ItemValue, Probe, TagType, TaggedFileExt};
use std::{error::Error, io::BufReader}; use std::{error::Error, io::BufReader};
use chrono::{serde::ts_seconds_option, DateTime, Utc};
use std::time::Duration; use std::time::Duration;
use chrono::{DateTime, Utc, serde::ts_seconds_option};
use walkdir::WalkDir; use walkdir::WalkDir;
use std::io::BufWriter; use bincode::{deserialize_from, serialize_into};
use std::fs;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs;
use std::io::BufWriter;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use bincode::{serialize_into, deserialize_from};
use crate::music_controller::config::Config; use crate::music_controller::config::Config;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AlbumArt { pub struct AlbumArt {
pub path: Option<URI>; pub index: u16,
pub path: Option<URI>,
} }
/// Stores information about a single song /// Stores information about a single song
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Song { pub struct Song {
pub path: URI, pub location: URI,
pub plays: i32, pub plays: i32,
pub skips: i32, pub skips: i32,
pub favorited: bool, pub favorited: bool,
pub rating: u8, pub rating: Option<u8>,
pub format: Option<FileFormat>, pub format: Option<FileFormat>,
pub duration: Duration, pub duration: Duration,
pub play_time: Duration, pub play_time: Duration,
@ -33,25 +35,26 @@ pub struct Song {
pub last_played: Option<DateTime<Utc>>, pub last_played: Option<DateTime<Utc>>,
#[serde(with = "ts_seconds_option")] #[serde(with = "ts_seconds_option")]
pub date_added: Option<DateTime<Utc>>, pub date_added: Option<DateTime<Utc>>,
pub album_art: Vec<AlbumArt>,
pub tags: Vec<(String, String)>, pub tags: Vec<(String, String)>,
} }
impl Song { impl Song {
pub fn get_tag(&self, target_key: String) -> Option<String> { pub fn get_tag(&self, target_key: &str) -> Option<&String> {
for tag in self.tags { let index = self.tags.iter().position(|r| r.0 == target_key);
if tag.0 == target_key {
return Some(tag.1) match index {
Some(i) => return Some(&self.tags[i].1),
None => None,
} }
} }
None
}
pub fn get_tags(&self, target_keys: Vec<String>) -> Vec<Option<String>> { pub fn get_tags(&self, target_keys: &Vec<String>) -> Vec<Option<String>> {
let mut results = Vec::new(); let mut results = Vec::new();
for tag in self.tags { for tag in &self.tags {
for key in target_keys { for key in target_keys {
if tag.0 == key { if &tag.0 == key {
results.push(Some(tag.1)) results.push(Some(tag.1.to_owned()))
} }
} }
results.push(None); results.push(None);
@ -60,13 +63,14 @@ impl Song {
} }
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum URI{ pub enum URI {
Local(String), Local(String),
//Cue(String, Duration), TODO: Make cue stuff work
Remote(Service, String), Remote(Service, String),
} }
#[derive(Clone, Copy, Debug, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum Service { pub enum Service {
InternetRadio, InternetRadio,
Spotify, Spotify,
@ -79,43 +83,95 @@ pub struct Playlist {
cover_art: Box<Path>, cover_art: Box<Path>,
} }
/// Initialize the database #[derive(Debug)]
/// pub enum MusicObject {
/// If the database file already exists, return the database, otherwise create it first Song(Song),
/// This needs to be run before anything else to retrieve the library vec Album(Playlist),
pub fn init_db(config: &Config) -> Result<Vec<Song>, Box<dyn Error>> { Playlist(Playlist),
}
#[derive(Debug)]
pub struct MusicLibrary {
pub library: Vec<Song>,
}
impl MusicLibrary {
/// Initialize the database
///
/// If the database file already exists, return the Library, otherwise create
/// the database first. This needs to be run before anything else to retrieve
/// the library vec
pub fn init(config: &Config) -> Result<Self, Box<dyn Error>> {
let mut library: Vec<Song> = Vec::new(); let mut library: Vec<Song> = Vec::new();
let mut backup_path = config.db_path.clone();
backup_path.set_extension("bkp");
match config.db_path.try_exists() { match config.db_path.try_exists() {
Ok(_) => { Ok(true) => {
// The database exists, so get it from the file // The database exists, so get it from the file
let database = fs::File::open(config.db_path.into_boxed_path())?; let database = fs::File::open(config.db_path.to_path_buf())?;
let reader = BufReader::new(database); let reader = BufReader::new(database);
library = deserialize_from(reader)?; library = deserialize_from(reader)?;
}, }
Err(_) => { Ok(false) => {
// Create the database if it does not exist // Create the database if it does not exist
let mut writer = BufWriter::new( // possibly from the backup file
fs::File::create(config.db_path.into_boxed_path())? if backup_path.try_exists().is_ok_and(|x| x == true) {
); let database = fs::File::open(config.db_path.to_path_buf())?;
let reader = BufReader::new(database);
library = deserialize_from(reader)?;
} else {
let mut writer = BufWriter::new(fs::File::create(config.db_path.to_path_buf())?);
serialize_into(&mut writer, &library)?; serialize_into(&mut writer, &library)?;
} }
},
Err(error) => return Err(error.into())
}; };
Ok(library) Ok(Self { library })
} }
fn path_in_db(query_path: &Path, library: &Vec<Song>) -> bool { pub fn save(&self, config: &Config) -> Result<(), Box<dyn Error>> {
unimplemented!() match config.db_path.try_exists() {
} Ok(true) => {
// The database exists, rename it to `.bkp` and
// write the new database
let mut backup_name = config.db_path.clone();
backup_name.set_extension("bkp");
fs::rename(config.db_path.as_path(), backup_name.as_path())?;
pub fn find_all_music( // TODO: Make this save properly like in config.rs
config: &Config,
target_path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let mut writer = BufWriter::new(fs::File::create(config.db_path.to_path_buf())?);
serialize_into(&mut writer, &self.library)?;
}
Ok(false) => {
// Create the database if it does not exist
let mut writer = BufWriter::new(fs::File::create(config.db_path.to_path_buf())?);
serialize_into(&mut writer, &self.library)?;
},
Err(error) => return Err(error.into())
}
Ok(())
}
fn find_by_uri(&self, path: &URI) -> Option<Song> {
for track in &self.library {
if path == &track.location {
return Some(track.clone());
}
}
None
}
pub fn find_all_music(&mut self, target_path: &str) -> Result<(), Box<dyn std::error::Error>> {
let mut current_dir = PathBuf::new(); let mut current_dir = PathBuf::new();
for entry in WalkDir::new(target_path).follow_links(true).into_iter().filter_map(|e| e.ok()) { for entry in WalkDir::new(target_path)
.follow_links(true)
.into_iter()
.filter_map(|e| e.ok())
{
let target_file = entry; let target_file = entry;
let is_file = fs::metadata(target_file.path())?.is_file(); let is_file = fs::metadata(target_file.path())?.is_file();
@ -134,27 +190,28 @@ pub fn find_all_music(
// If it's a normal file, add it to the database // If it's a normal file, add it to the database
// if it's a cuesheet, do a bunch of fancy stuff // if it's a cuesheet, do a bunch of fancy stuff
if format.kind() == Kind::Audio { if format.kind() == Kind::Audio {
add_file_to_db(target_file.path()) match self.add_file_to_db(target_file.path()) {
Ok(_) => (),
Err(_error) => () //println!("{}, {:?}: {}", format, target_file.file_name(), error)
};
} else if extension.to_ascii_lowercase() == "cue" { } else if extension.to_ascii_lowercase() == "cue" {
// TODO: implement cuesheet support // TODO: implement cuesheet support
} }
} }
Ok(()) Ok(())
} }
pub fn add_file_to_db(target_file: &Path) { pub fn add_file_to_db(&mut self, target_file: &Path) -> Result<(), Box<dyn std::error::Error>> {
// TODO: Fix error handling here // TODO: Fix error handling here
let tagged_file = match lofty::read_from_path(target_file) { let tagged_file = match lofty::read_from_path(target_file) {
Ok(tagged_file) => tagged_file, Ok(tagged_file) => tagged_file,
Err(_) => match Probe::open(target_file) Err(_) => match Probe::open(target_file)?.read() {
.expect("ERROR: Bad path provided!")
.read() {
Ok(tagged_file) => tagged_file, Ok(tagged_file) => tagged_file,
Err(_) => return Err(error) => return Err(error.into()),
} },
}; };
// Ensure the tags exist, if not, insert blank data // Ensure the tags exist, if not, insert blank data
@ -164,78 +221,100 @@ pub fn add_file_to_db(target_file: &Path) {
None => match tagged_file.first_tag() { None => match tagged_file.first_tag() {
Some(first_tag) => first_tag, Some(first_tag) => first_tag,
None => blank_tag None => blank_tag,
}, },
}; };
let mut custom_insert = String::new(); let mut tags: Vec<(String, String)> = Vec::new();
let mut loops = 0; for item in tag.items() {
for (loops, item) in tag.items().enumerate() { let mut key = String::new();
let mut custom_key = String::new();
match item.key() { match item.key() {
ItemKey::TrackArtist | ItemKey::Unknown(unknown) => key.push_str(&unknown),
ItemKey::TrackTitle | custom => key = format!("{:?}", custom),
ItemKey::AlbumTitle |
ItemKey::Genre |
ItemKey::TrackNumber |
ItemKey::Year |
ItemKey::RecordingDate => continue,
ItemKey::Unknown(unknown) => custom_key.push_str(&unknown),
custom => custom_key.push_str(&format!("{:?}", custom))
// TODO: This is kind of cursed, maybe fix?
}; };
let custom_value = match item.value() { let value = match item.value() {
ItemValue::Text(value) => value, ItemValue::Text(value) => String::from(value),
ItemValue::Locator(value) => value, ItemValue::Locator(value) => String::from(value),
ItemValue::Binary(_) => "" ItemValue::Binary(_) => String::from(""),
}; };
if loops > 0 { tags.push((key, value))
custom_insert.push_str(", ");
} }
// Get all the album artwork information
let mut album_art: Vec<AlbumArt> = Vec::new();
for (i, _art) in tag.pictures().iter().enumerate() {
let new_art = AlbumArt {
index: i as u16,
path: None,
};
album_art.push(new_art)
} }
// Get the format as a string // Get the format as a string
let short_format: Option<String> = match FileFormat::from_file(target_file) { let format: Option<FileFormat> = match FileFormat::from_file(target_file) {
Ok(fmt) => Some(fmt.to_string()), Ok(fmt) => Some(fmt),
Err(_) => None Err(_) => None,
}; };
println!("{}", short_format.as_ref().unwrap()); let duration = tagged_file.properties().duration();
let duration = tagged_file.properties().duration().as_secs().to_string();
// TODO: Fix error handling // TODO: Fix error handling
let binding = fs::canonicalize(target_file).unwrap(); let binding = fs::canonicalize(target_file).unwrap();
let abs_path = binding.to_str().unwrap(); let abs_path = binding.to_str().unwrap();
}
pub fn add_song_to_db(new_song: Song) { let new_song = Song {
location: URI::Local(abs_path.to_string()),
plays: 0,
skips: 0,
favorited: false,
rating: None,
format,
duration,
play_time: Duration::from_secs(0),
last_played: None,
date_added: Some(chrono::offset::Utc::now()),
tags,
album_art,
};
} match self.add_song_to_db(new_song) {
Ok(_) => (),
Err(error) => ()
};
#[derive(Debug)] Ok(())
pub enum MusicObject {
Song(Song),
Album(Playlist),
Playlist(Playlist),
}
impl MusicObject {
pub fn as_song(&self) -> Option<&Song> {
match self {
MusicObject::Song(data) => Some(data),
_ => None
} }
}
}
/// Query the database, returning a list of items pub fn add_song_to_db(&mut self, new_song: Song) -> Result<(), Box<dyn std::error::Error>> {
pub fn query ( match self.find_by_uri(&new_song.location) {
Some(_) => return Err(format!("URI already in database: {:?}", new_song.location).into()),
None => ()
}
self.library.push(new_song);
Ok(())
}
pub fn update_song_tags(&mut self, new_tags: Song) -> Result<(), Box<dyn std::error::Error>> {
match self.find_by_uri(&new_tags.location) {
Some(_) => (),
None => return Err(format!("URI not in database!").into())
}
todo!()
}
/// Query the database, returning a list of items
pub fn query(
&self,
query_string: &String, // The query itself query_string: &String, // The query itself
target_tags: &Vec<String>, // The tags to search target_tags: &Vec<String>, // The tags to search
sort_by: &Vec<String>, // Tags to sort the resulting data by sort_by: &Vec<String>, // Tags to sort the resulting data by
) -> Option<Vec<MusicObject>> { ) -> Option<Vec<MusicObject>> {
unimplemented!() unimplemented!()
}
} }

View file

@ -75,7 +75,7 @@ impl MusicTracker for LastFM {
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("Your time is off.").as_secs() - 30; let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("Your time is off.").as_secs() - 30;
let string_timestamp = timestamp.to_string(); let string_timestamp = timestamp.to_string();
let (artist, track) = match (song.artist, song.title) { let (artist, track) = match (song.get_tag("Artist"), song.get_tag("Title")) {
(Some(artist), Some(track)) => (artist, track), (Some(artist), Some(track)) => (artist, track),
_ => return Err(TrackerError::InvalidSong) _ => return Err(TrackerError::InvalidSong)
}; };
@ -94,7 +94,7 @@ impl MusicTracker for LastFM {
async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> {
let mut params: BTreeMap<&str, &str> = BTreeMap::new(); let mut params: BTreeMap<&str, &str> = BTreeMap::new();
let (artist, track) = match (song.artist, song.title) { let (artist, track) = match (song.get_tag("Artist"), song.get_tag("Title")) {
(Some(artist), Some(track)) => (artist, track), (Some(artist), Some(track)) => (artist, track),
_ => return Err(TrackerError::InvalidSong) _ => return Err(TrackerError::InvalidSong)
}; };
@ -259,11 +259,13 @@ impl DiscordRPC {
#[async_trait] #[async_trait]
impl MusicTracker for DiscordRPC { impl MusicTracker for DiscordRPC {
async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> {
let unknown = String::from("Unknown");
// Sets song title // Sets song title
let song_name = if let Some(song_name) = song.title { let song_name = if let Some(song_name) = song.get_tag("Title") {
song_name song_name
} else { } else {
String::from("Unknown") &unknown
}; };
let _client_thread = self.client.start(); let _client_thread = self.client.start();
@ -316,7 +318,7 @@ pub struct ListenBrainz {
#[async_trait] #[async_trait]
impl MusicTracker for ListenBrainz { impl MusicTracker for ListenBrainz {
async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> {
let (artist, track) = match (song.artist, song.title) { let (artist, track) = match (song.get_tag("Artist"), song.get_tag("Title")) {
(Some(artist), Some(track)) => (artist, track), (Some(artist), Some(track)) => (artist, track),
_ => return Err(TrackerError::InvalidSong) _ => return Err(TrackerError::InvalidSong)
}; };
@ -342,7 +344,7 @@ impl MusicTracker for ListenBrainz {
async fn track_song(&mut self, song: Song) -> Result<(), TrackerError> { async fn track_song(&mut self, song: Song) -> Result<(), TrackerError> {
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("Your time is off.").as_secs() - 30; let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("Your time is off.").as_secs() - 30;
let (artist, track) = match (song.artist, song.title) { let (artist, track) = match (song.get_tag("Artist"), song.get_tag("Title")) {
(Some(artist), Some(track)) => (artist, track), (Some(artist), Some(track)) => (artist, track),
_ => return Err(TrackerError::InvalidSong) _ => return Err(TrackerError::InvalidSong)
}; };