From db53bed1116358e9963983b219e0cd0f7f0f1385 Mon Sep 17 00:00:00 2001
From: G2-Games <ke0bhogsg@gmail.com>
Date: Sat, 30 Sep 2023 23:31:42 -0500
Subject: [PATCH] Database works

---
 src/music_controller/config.rs           |   2 +-
 src/music_controller/music_controller.rs |   2 -
 src/music_player/music_player.rs         |   2 +-
 src/music_storage/music_db.rs            | 421 ++++++++++++++---------
 src/music_tracker/music_tracker.rs       |  14 +-
 5 files changed, 260 insertions(+), 181 deletions(-)

diff --git a/src/music_controller/config.rs b/src/music_controller/config.rs
index 034f12d..f6909f5 100644
--- a/src/music_controller/config.rs
+++ b/src/music_controller/config.rs
@@ -16,7 +16,7 @@ pub struct Config {
 
 impl Default for Config {
     fn default() -> Self {
-        let path = PathBuf::from("./music_database.db3");
+        let path = PathBuf::from("./music_database");
 
         return Config {
             db_path: Box::new(path),
diff --git a/src/music_controller/music_controller.rs b/src/music_controller/music_controller.rs
index da4f676..545d5aa 100644
--- a/src/music_controller/music_controller.rs
+++ b/src/music_controller/music_controller.rs
@@ -1,8 +1,6 @@
 use std::path::PathBuf;
 use std::sync::{RwLock, Arc};
 
-use rusqlite::Result;
-
 use crate::music_controller::config::Config;
 use crate::music_player::music_player::{MusicPlayer, PlayerStatus, DecoderMessage, DSPMessage};
 use crate::music_storage::music_db::Song;
diff --git a/src/music_player/music_player.rs b/src/music_player/music_player.rs
index 5ab0ad9..478fca1 100644
--- a/src/music_player/music_player.rs
+++ b/src/music_player/music_player.rs
@@ -232,7 +232,7 @@ impl MusicPlayer {
                     // Handles message received from MusicPlayer struct
                     match message {
                         Some(DecoderMessage::OpenSong(song)) => {
-                            let song_uri = song.path.clone();
+                            let song_uri = song.location.clone();
                             match SongHandler::new(&song_uri) {
                                 Ok(new_handler) => {
                                     song_handler = Some(new_handler);
diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs
index 77aaf0b..cb429c6 100644
--- a/src/music_storage/music_db.rs
+++ b/src/music_storage/music_db.rs
@@ -1,31 +1,33 @@
 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 chrono::{serde::ts_seconds_option, DateTime, Utc};
 use std::time::Duration;
-use chrono::{DateTime, Utc, serde::ts_seconds_option};
 use walkdir::WalkDir;
 
-use std::io::BufWriter;
-use std::fs;
+use bincode::{deserialize_from, serialize_into};
 use serde::{Deserialize, Serialize};
+use std::fs;
+use std::io::BufWriter;
 use std::path::{Path, PathBuf};
-use bincode::{serialize_into, deserialize_from};
 
 use crate::music_controller::config::Config;
 
+#[derive(Debug, Clone, Deserialize, Serialize)]
 pub struct AlbumArt {
-    pub path: Option<URI>;
+    pub index: u16,
+    pub path: Option<URI>,
 }
 
 /// Stores information about a single song
 #[derive(Debug, Clone, Deserialize, Serialize)]
 pub struct Song {
-    pub path: URI,
+    pub location: URI,
     pub plays: i32,
     pub skips: i32,
     pub favorited: bool,
-    pub rating: u8,
+    pub rating: Option<u8>,
     pub format: Option<FileFormat>,
     pub duration: Duration,
     pub play_time: Duration,
@@ -33,25 +35,26 @@ pub struct Song {
     pub last_played: Option<DateTime<Utc>>,
     #[serde(with = "ts_seconds_option")]
     pub date_added: Option<DateTime<Utc>>,
+    pub album_art: Vec<AlbumArt>,
     pub tags: Vec<(String, String)>,
 }
 
 impl Song {
-    pub fn get_tag(&self, target_key: String) -> Option<String> {
-        for tag in self.tags {
-            if tag.0 == target_key {
-                return Some(tag.1)
-            }
+    pub fn get_tag(&self, target_key: &str) -> Option<&String> {
+        let index = self.tags.iter().position(|r| r.0 == target_key);
+
+        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();
-        for tag in self.tags {
+        for tag in &self.tags {
             for key in target_keys {
-                if tag.0 == key {
-                    results.push(Some(tag.1))
+                if &tag.0 == key {
+                    results.push(Some(tag.1.to_owned()))
                 }
             }
             results.push(None);
@@ -60,13 +63,14 @@ impl Song {
     }
 }
 
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub enum URI{
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
+pub enum URI {
     Local(String),
+    //Cue(String, Duration), TODO: Make cue stuff work
     Remote(Service, String),
 }
 
-#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
 pub enum Service {
     InternetRadio,
     Spotify,
@@ -79,142 +83,6 @@ pub struct Playlist {
     cover_art: Box<Path>,
 }
 
-/// Initialize the database
-///
-/// If the database file already exists, return the database, otherwise create it first
-/// This needs to be run before anything else to retrieve the library vec
-pub fn init_db(config: &Config) -> Result<Vec<Song>, Box<dyn Error>> {
-    let mut library: Vec<Song> = Vec::new();
-
-    match config.db_path.try_exists() {
-        Ok(_) => {
-            // The database exists, so get it from the file
-            let database = fs::File::open(config.db_path.into_boxed_path())?;
-            let reader = BufReader::new(database);
-            library = deserialize_from(reader)?;
-        },
-        Err(_) => {
-            // Create the database if it does not exist
-            let mut writer = BufWriter::new(
-                fs::File::create(config.db_path.into_boxed_path())?
-            );
-            serialize_into(&mut writer, &library)?;
-        }
-    };
-
-    Ok(library)
-}
-
-fn path_in_db(query_path: &Path, library: &Vec<Song>) -> bool {
-    unimplemented!()
-}
-
-pub fn find_all_music(
-    config: &Config,
-    target_path: &str,
-) -> Result<(), Box<dyn std::error::Error>> {
-
-    let mut current_dir = PathBuf::new();
-    for entry in WalkDir::new(target_path).follow_links(true).into_iter().filter_map(|e| e.ok()) {
-        let target_file = entry;
-        let is_file = fs::metadata(target_file.path())?.is_file();
-
-        // Ensure the target is a file and not a directory, if it isn't, skip this loop
-        if !is_file {
-            current_dir = target_file.into_path();
-            continue;
-        }
-
-        let format = FileFormat::from_file(target_file.path())?;
-        let extension = target_file
-            .path()
-            .extension()
-            .expect("Could not find file extension");
-
-        // If it's a normal file, add it to the database
-        // if it's a cuesheet, do a bunch of fancy stuff
-        if format.kind() == Kind::Audio {
-            add_file_to_db(target_file.path())
-        } else if extension.to_ascii_lowercase() == "cue" {
-            // TODO: implement cuesheet support
-        }
-    }
-
-    Ok(())
-}
-
-pub fn add_file_to_db(target_file: &Path) {
-    // TODO: Fix error handling here
-    let tagged_file = match lofty::read_from_path(target_file) {
-        Ok(tagged_file) => tagged_file,
-
-        Err(_) => match Probe::open(target_file)
-            .expect("ERROR: Bad path provided!")
-            .read() {
-                Ok(tagged_file) => tagged_file,
-
-                Err(_) => return
-            }
-    };
-
-    // Ensure the tags exist, if not, insert blank data
-    let blank_tag = &lofty::Tag::new(TagType::Id3v2);
-    let tag = match tagged_file.primary_tag() {
-        Some(primary_tag) => primary_tag,
-
-        None => match tagged_file.first_tag() {
-            Some(first_tag) => first_tag,
-            None => blank_tag
-        },
-    };
-
-    let mut custom_insert = String::new();
-    let mut loops = 0;
-    for (loops, item) in tag.items().enumerate() {
-        let mut custom_key = String::new();
-        match item.key() {
-            ItemKey::TrackArtist |
-            ItemKey::TrackTitle  |
-            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() {
-            ItemValue::Text(value) => value,
-            ItemValue::Locator(value) => value,
-            ItemValue::Binary(_) => ""
-        };
-
-        if loops > 0 {
-            custom_insert.push_str(", ");
-        }
-    }
-    
-    // Get the format as a string
-    let short_format: Option<String> = match FileFormat::from_file(target_file) {
-        Ok(fmt) => Some(fmt.to_string()),
-        Err(_) => None
-    };
-
-    println!("{}", short_format.as_ref().unwrap());
-    
-    let duration = tagged_file.properties().duration().as_secs().to_string();
-    
-    // TODO: Fix error handling
-    let binding = fs::canonicalize(target_file).unwrap();
-    let abs_path = binding.to_str().unwrap();
-}
-
-pub fn add_song_to_db(new_song: Song) {
-
-}
-
 #[derive(Debug)]
 pub enum MusicObject {
     Song(Song),
@@ -222,20 +90,231 @@ pub enum MusicObject {
     Playlist(Playlist),
 }
 
-impl MusicObject {
-    pub fn as_song(&self) -> Option<&Song> {
-        match self {
-            MusicObject::Song(data) => Some(data),
-            _ => None
-        }
-    }
+#[derive(Debug)]
+pub struct MusicLibrary {
+    pub library: Vec<Song>,
 }
 
-/// Query the database, returning a list of items
-pub fn query (
-    query_string: &String,      // The query itself
-    target_tags: &Vec<String>,  // The tags to search
-    sort_by: &Vec<String>,      // Tags to sort the resulting data by
-) -> Option<Vec<MusicObject>> {
-    unimplemented!()
+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 backup_path = config.db_path.clone();
+        backup_path.set_extension("bkp");
+
+        match config.db_path.try_exists() {
+            Ok(true) => {
+                // The database exists, so get it from the file
+                let database = fs::File::open(config.db_path.to_path_buf())?;
+                let reader = BufReader::new(database);
+                library = deserialize_from(reader)?;
+            }
+            Ok(false) => {
+                // Create the database if it does not exist
+                // possibly from the backup file
+                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)?;
+                }
+            },
+            Err(error) => return Err(error.into())
+        };
+
+        Ok(Self { library })
+    }
+
+    pub fn save(&self, config: &Config) -> Result<(), Box<dyn Error>> {
+        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())?;
+
+                // TODO: Make this save properly like in config.rs
+
+                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();
+        for entry in WalkDir::new(target_path)
+            .follow_links(true)
+            .into_iter()
+            .filter_map(|e| e.ok())
+        {
+            let target_file = entry;
+            let is_file = fs::metadata(target_file.path())?.is_file();
+
+            // Ensure the target is a file and not a directory, if it isn't, skip this loop
+            if !is_file {
+                current_dir = target_file.into_path();
+                continue;
+            }
+
+            let format = FileFormat::from_file(target_file.path())?;
+            let extension = target_file
+                .path()
+                .extension()
+                .expect("Could not find file extension");
+
+            // If it's a normal file, add it to the database
+            // if it's a cuesheet, do a bunch of fancy stuff
+            if format.kind() == Kind::Audio {
+                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" {
+                // TODO: implement cuesheet support
+            }
+        }
+
+        Ok(())
+    }
+
+    pub fn add_file_to_db(&mut self, target_file: &Path) -> Result<(), Box<dyn std::error::Error>> {
+        // TODO: Fix error handling here
+        let tagged_file = match lofty::read_from_path(target_file) {
+            Ok(tagged_file) => tagged_file,
+
+            Err(_) => match Probe::open(target_file)?.read() {
+                Ok(tagged_file) => tagged_file,
+
+                Err(error) => return Err(error.into()),
+            },
+        };
+
+        // Ensure the tags exist, if not, insert blank data
+        let blank_tag = &lofty::Tag::new(TagType::Id3v2);
+        let tag = match tagged_file.primary_tag() {
+            Some(primary_tag) => primary_tag,
+
+            None => match tagged_file.first_tag() {
+                Some(first_tag) => first_tag,
+                None => blank_tag,
+            },
+        };
+
+        let mut tags: Vec<(String, String)> = Vec::new();
+        for item in tag.items() {
+            let mut key = String::new();
+            match item.key() {
+                ItemKey::Unknown(unknown) => key.push_str(&unknown),
+                custom => key = format!("{:?}", custom),
+            };
+
+            let value = match item.value() {
+                ItemValue::Text(value) => String::from(value),
+                ItemValue::Locator(value) => String::from(value),
+                ItemValue::Binary(_) => String::from(""),
+            };
+
+            tags.push((key, value))
+        }
+
+        // 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
+        let format: Option<FileFormat> = match FileFormat::from_file(target_file) {
+            Ok(fmt) => Some(fmt),
+            Err(_) => None,
+        };
+
+        let duration = tagged_file.properties().duration();
+
+        // TODO: Fix error handling
+        let binding = fs::canonicalize(target_file).unwrap();
+        let abs_path = binding.to_str().unwrap();
+
+        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) => ()
+        };
+
+        Ok(())
+    }
+
+    pub fn add_song_to_db(&mut self, new_song: Song) -> Result<(), Box<dyn std::error::Error>> {
+        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
+        target_tags: &Vec<String>, // The tags to search
+        sort_by: &Vec<String>,     // Tags to sort the resulting data by
+    ) -> Option<Vec<MusicObject>> {
+        unimplemented!()
+    }
 }
diff --git a/src/music_tracker/music_tracker.rs b/src/music_tracker/music_tracker.rs
index 213a5f1..5c306cc 100644
--- a/src/music_tracker/music_tracker.rs
+++ b/src/music_tracker/music_tracker.rs
@@ -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 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),
             _ => return Err(TrackerError::InvalidSong)
         };
@@ -94,7 +94,7 @@ impl MusicTracker for LastFM {
     async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> {
         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),
             _ => return Err(TrackerError::InvalidSong)
         };
@@ -259,11 +259,13 @@ impl DiscordRPC {
 #[async_trait]
 impl MusicTracker for DiscordRPC {
     async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> {
+        let unknown = String::from("Unknown");
+
         // 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
         } else {
-            String::from("Unknown")
+            &unknown
         };
         
         let _client_thread = self.client.start();
@@ -316,7 +318,7 @@ pub struct ListenBrainz {
 #[async_trait]
 impl MusicTracker for ListenBrainz {
     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),
             _ => return Err(TrackerError::InvalidSong)
         };
@@ -342,7 +344,7 @@ impl MusicTracker for ListenBrainz {
     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 (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),
             _ => return Err(TrackerError::InvalidSong)
         };