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; + pub index: u16, + pub path: Option, } /// 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, pub format: Option, pub duration: Duration, pub play_time: Duration, @@ -33,25 +35,26 @@ pub struct Song { pub last_played: Option>, #[serde(with = "ts_seconds_option")] pub date_added: Option>, + pub album_art: Vec, pub tags: Vec<(String, String)>, } impl Song { - pub fn get_tag(&self, target_key: String) -> Option { - 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) -> Vec> { + pub fn get_tags(&self, target_keys: &Vec) -> Vec> { 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, } -/// 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, Box> { - let mut library: Vec = 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) -> bool { - unimplemented!() -} - -pub fn find_all_music( - config: &Config, - target_path: &str, -) -> Result<(), Box> { - - 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 = 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, } -/// Query the database, returning a list of items -pub fn query ( - query_string: &String, // The query itself - target_tags: &Vec, // The tags to search - sort_by: &Vec, // Tags to sort the resulting data by -) -> Option> { - 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> { + let mut library: Vec = 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> { + 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 { + 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> { + 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> { + // 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 = 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 = 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> { + 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> { + 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, // The tags to search + sort_by: &Vec, // Tags to sort the resulting data by + ) -> Option> { + 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) };