preliminary query support, still some bugs to iron out

This commit is contained in:
G2-Games 2023-10-02 01:26:36 -05:00
parent db53bed111
commit d2d92149f8
3 changed files with 114 additions and 20 deletions

View file

@ -33,3 +33,5 @@ arrayvec = "0.7.4"
discord-presence = "0.5.18" discord-presence = "0.5.18"
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
bincode = "1.3.3" bincode = "1.3.3"
wana_kana = "3.0.0"
unidecode = "0.3.0"

View file

@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
use std::io::BufWriter; use std::io::BufWriter;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use unidecode::unidecode;
use crate::music_controller::config::Config; use crate::music_controller::config::Config;
@ -20,6 +21,31 @@ pub struct AlbumArt {
pub path: Option<URI>, pub path: Option<URI>,
} }
#[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 /// Stores information about a single song
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Song { pub struct Song {
@ -35,13 +61,15 @@ 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>>,
#[serde(with = "ts_seconds_option")]
pub date_modified: Option<DateTime<Utc>>,
pub album_art: Vec<AlbumArt>, pub album_art: Vec<AlbumArt>,
pub tags: Vec<(String, String)>, pub tags: Vec<(Tag, String)>,
} }
impl Song { impl Song {
pub fn get_tag(&self, target_key: &str) -> Option<&String> { pub fn get_tag(&self, target_key: &Tag) -> Option<&String> {
let index = self.tags.iter().position(|r| r.0 == target_key); let index = self.tags.iter().position(|r| r.0 == *target_key);
match index { match index {
Some(i) => return Some(&self.tags[i].1), Some(i) => return Some(&self.tags[i].1),
@ -53,7 +81,7 @@ impl Song {
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.to_string() == key {
results.push(Some(tag.1.to_owned())) results.push(Some(tag.1.to_owned()))
} }
} }
@ -95,6 +123,10 @@ pub struct MusicLibrary {
pub library: Vec<Song>, pub library: Vec<Song>,
} }
pub fn normalize(input_string: &String) -> String {
unidecode(input_string).to_ascii_lowercase()
}
impl MusicLibrary { impl MusicLibrary {
/// Initialize the database /// 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() { for item in tag.items() {
let mut key = String::new(); let key = match item.key() {
match item.key() { ItemKey::TrackTitle => Tag::Title,
ItemKey::Unknown(unknown) => key.push_str(&unknown), ItemKey::TrackNumber => Tag::Track,
custom => key = format!("{:?}", custom), 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() { let value = match item.value() {
@ -276,6 +313,7 @@ impl MusicLibrary {
play_time: Duration::from_secs(0), play_time: Duration::from_secs(0),
last_played: None, last_played: None,
date_added: Some(chrono::offset::Utc::now()), date_added: Some(chrono::offset::Utc::now()),
date_modified: Some(chrono::offset::Utc::now()),
tags, tags,
album_art, album_art,
}; };
@ -311,10 +349,63 @@ impl MusicLibrary {
/// Query the database, returning a list of items /// Query the database, returning a list of items
pub fn query( pub fn query(
&self, &self,
query_string: &String, // The query itself query_string: &String, // The query itself
target_tags: &Vec<String>, // The tags to search target_tags: &Vec<Tag>, // The tags to search
sort_by: &Vec<String>, // Tags to sort the resulting data by sort_by: &Vec<Tag>, // Tags to sort the resulting data by
) -> Option<Vec<MusicObject>> { ) -> Option<Vec<Song>> {
unimplemented!() 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::<f64>(), tag_b.parse::<f64>()) {
// 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
}
} }
} }

View file

@ -8,7 +8,7 @@ use md5::{Md5, Digest};
use discord_presence::{Event}; use discord_presence::{Event};
use surf::StatusCode; use surf::StatusCode;
use crate::music_storage::music_db::Song; use crate::music_storage::music_db::{Song, Tag};
#[async_trait] #[async_trait]
pub trait MusicTracker { 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 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.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), (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.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), (Some(artist), Some(track)) => (artist, track),
_ => return Err(TrackerError::InvalidSong) _ => return Err(TrackerError::InvalidSong)
}; };
@ -256,13 +256,14 @@ 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"); let unknown = String::from("Unknown");
// Sets song title // 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 song_name
} else { } else {
&unknown &unknown
@ -318,7 +319,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.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), (Some(artist), Some(track)) => (artist, track),
_ => return Err(TrackerError::InvalidSong) _ => return Err(TrackerError::InvalidSong)
}; };
@ -344,7 +345,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.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), (Some(artist), Some(track)) => (artist, track),
_ => return Err(TrackerError::InvalidSong) _ => return Err(TrackerError::InvalidSong)
}; };