From d2d92149f8544cfae45eb9aa1392ea52fbd5f50f Mon Sep 17 00:00:00 2001 From: G2-Games Date: Mon, 2 Oct 2023 01:26:36 -0500 Subject: [PATCH] preliminary query support, still some bugs to iron out --- Cargo.toml | 2 + src/music_storage/music_db.rs | 119 +++++++++++++++++++++++++---- src/music_tracker/music_tracker.rs | 13 ++-- 3 files changed, 114 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a96c12a..0f98190 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,3 +33,5 @@ arrayvec = "0.7.4" discord-presence = "0.5.18" chrono = { version = "0.4.31", features = ["serde"] } bincode = "1.3.3" +wana_kana = "3.0.0" +unidecode = "0.3.0" diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index cb429c6..0292912 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::io::BufWriter; use std::path::{Path, PathBuf}; +use unidecode::unidecode; use crate::music_controller::config::Config; @@ -20,6 +21,31 @@ pub struct AlbumArt { pub path: Option, } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub enum Tag { + Title, + Album, + Artist, + Genre, + Comment, + Track, + Key(String) +} + +impl ToString for Tag { + fn to_string(&self) -> String { + match self { + Self::Title => "TrackTitle".into(), + Self::Album => "AlbumTitle".into(), + Self::Artist => "TrackArtist".into(), + Self::Genre => "Genre".into(), + Self::Comment => "Comment".into(), + Self::Track => "TrackNumber".into(), + Self::Key(key) => key.into() + } + } +} + /// Stores information about a single song #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Song { @@ -35,13 +61,15 @@ pub struct Song { pub last_played: Option>, #[serde(with = "ts_seconds_option")] pub date_added: Option>, + #[serde(with = "ts_seconds_option")] + pub date_modified: Option>, pub album_art: Vec, - pub tags: Vec<(String, String)>, + pub tags: Vec<(Tag, String)>, } impl Song { - pub fn get_tag(&self, target_key: &str) -> Option<&String> { - let index = self.tags.iter().position(|r| r.0 == target_key); + pub fn get_tag(&self, target_key: &Tag) -> Option<&String> { + let index = self.tags.iter().position(|r| r.0 == *target_key); match index { Some(i) => return Some(&self.tags[i].1), @@ -53,7 +81,7 @@ impl Song { let mut results = Vec::new(); for tag in &self.tags { for key in target_keys { - if &tag.0 == key { + if &tag.0.to_string() == key { results.push(Some(tag.1.to_owned())) } } @@ -95,6 +123,10 @@ pub struct MusicLibrary { pub library: Vec, } +pub fn normalize(input_string: &String) -> String { + unidecode(input_string).to_ascii_lowercase() +} + impl MusicLibrary { /// Initialize the database /// @@ -225,12 +257,17 @@ impl MusicLibrary { }, }; - let mut tags: Vec<(String, String)> = Vec::new(); + let mut tags: Vec<(Tag, 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 key = match item.key() { + ItemKey::TrackTitle => Tag::Title, + ItemKey::TrackNumber => Tag::Track, + ItemKey::TrackArtist => Tag::Artist, + ItemKey::Genre => Tag::Genre, + ItemKey::Comment => Tag::Comment, + ItemKey::AlbumTitle => Tag::Album, + ItemKey::Unknown(unknown) => Tag::Key(unknown.to_string()), + custom => Tag::Key(format!("{:?}", custom)), }; let value = match item.value() { @@ -276,6 +313,7 @@ impl MusicLibrary { play_time: Duration::from_secs(0), last_played: None, date_added: Some(chrono::offset::Utc::now()), + date_modified: Some(chrono::offset::Utc::now()), tags, album_art, }; @@ -311,10 +349,63 @@ impl MusicLibrary { /// 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!() + query_string: &String, // The query itself + target_tags: &Vec, // The tags to search + sort_by: &Vec, // Tags to sort the resulting data by + ) -> Option> { + let mut songs = Vec::new(); + + for track in &self.library { + for tag in &track.tags { + if !target_tags.contains(&tag.0) { + continue; + } + + if normalize(&tag.1).contains(&normalize(&query_string)) { + songs.push(track.clone()); + break + } + } + } + + songs.sort_by(|a, b| { + for opt in sort_by { + let tag_a = match a.get_tag(&opt) { + Some(tag) => tag, + None => continue + }; + + let tag_b = match b.get_tag(&opt) { + Some(tag) => tag, + None => continue + }; + + // Try to parse the tags as f64 (floating-point numbers) + if let (Ok(num_a), Ok(num_b)) = (tag_a.parse::(), tag_b.parse::()) { + // If parsing succeeds, compare as numbers + if num_a < num_b { + return std::cmp::Ordering::Less; + } else if num_a > num_b { + return std::cmp::Ordering::Greater; + } + } else { + // If parsing fails, compare as strings + if tag_a < tag_b { + return std::cmp::Ordering::Less; + } else if tag_a > tag_b { + return std::cmp::Ordering::Greater; + } + } + } + + // If all tags are equal, sort by some default criteria (e.g., song title) + a.get_tag(&Tag::Title).cmp(&b.get_tag(&Tag::Title)) + }); + + if songs.len() > 0 { + Some(songs) + } else { + None + } } } diff --git a/src/music_tracker/music_tracker.rs b/src/music_tracker/music_tracker.rs index 5c306cc..8526e89 100644 --- a/src/music_tracker/music_tracker.rs +++ b/src/music_tracker/music_tracker.rs @@ -8,7 +8,7 @@ use md5::{Md5, Digest}; use discord_presence::{Event}; use surf::StatusCode; -use crate::music_storage::music_db::Song; +use crate::music_storage::music_db::{Song, Tag}; #[async_trait] pub trait MusicTracker { @@ -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.get_tag("Artist"), song.get_tag("Title")) { + let (artist, track) = match (song.get_tag(&Tag::Artist), song.get_tag(&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.get_tag("Artist"), song.get_tag("Title")) { + let (artist, track) = match (song.get_tag(&Tag::Artist), song.get_tag(&Tag::Title)) { (Some(artist), Some(track)) => (artist, track), _ => return Err(TrackerError::InvalidSong) }; @@ -256,13 +256,14 @@ 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.get_tag("Title") { + let song_name = if let Some(song_name) = song.get_tag(&Tag::Title) { song_name } else { &unknown @@ -318,7 +319,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.get_tag("Artist"), song.get_tag("Title")) { + let (artist, track) = match (song.get_tag(&Tag::Artist), song.get_tag(&Tag::Title)) { (Some(artist), Some(track)) => (artist, track), _ => return Err(TrackerError::InvalidSong) }; @@ -344,7 +345,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.get_tag("Artist"), song.get_tag("Title")) { + let (artist, track) = match (song.get_tag(&Tag::Artist), song.get_tag(&Tag::Title)) { (Some(artist), Some(track)) => (artist, track), _ => return Err(TrackerError::InvalidSong) };