From 66b2a959d7b3207b465a9ec08dcac369c20b2c2f Mon Sep 17 00:00:00 2001 From: JMARyA Date: Sat, 17 Aug 2024 10:45:10 +0200 Subject: [PATCH] search --- src/library/mod.rs | 3 +- src/library/search.rs | 141 ++++++++++++++++++++++++++++++++++++++++++ src/route/mod.rs | 1 + src/route/playlist.rs | 2 +- src/route/search.rs | 12 ++++ 5 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 src/library/search.rs create mode 100644 src/route/search.rs diff --git a/src/library/mod.rs b/src/library/mod.rs index 5de78ed..e39351c 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -15,6 +15,7 @@ pub mod artist; pub mod event; pub mod metadata; pub mod playlist; +pub mod search; pub mod track; pub mod user; @@ -22,7 +23,7 @@ pub mod user; fn is_music_file(path: &Path) -> bool { if let Some(extension) = path.extension() { match extension.to_str().unwrap_or("").to_lowercase().as_str() { - "mp3" | "flac" | "wav" | "aac" | "ogg" | "m4a" | "opus" => return true, + "flac" | "wav" | "aac" | "ogg" | "opus" => return true, _ => return false, } } diff --git a/src/library/search.rs b/src/library/search.rs new file mode 100644 index 0000000..f92573a --- /dev/null +++ b/src/library/search.rs @@ -0,0 +1,141 @@ +use std::cmp::Ordering; + +use mongod::Model; +use mongodb::bson::doc; +use serde::Serialize; + +use crate::route::ToAPI; + +use super::{album::Album, artist::Artist, playlist::Playlist, track::Track}; + +fn calculate_score(field: &str, search_term: &str, date_added: Option) -> f64 { + // Exact match bonus + let exact_match_bonus = if field.eq_ignore_ascii_case(search_term) { + 10.0 + } else { + 0.0 + }; + + // String similarity score + let similarity_score = string_similarity(field, search_term); + + // Recency factor + let recency_score = if let Some(date) = date_added { + recency_factor(date) + } else { + 0.0 + }; + + // Calculate total score + exact_match_bonus + similarity_score + recency_score +} + +fn string_similarity(field: &str, search_term: &str) -> f64 { + let mut match_count = 0; + let field_lower = field.to_lowercase(); + let search_lower = search_term.to_lowercase(); + + for (i, c) in search_lower.chars().enumerate() { + if let Some(field_char) = field_lower.chars().nth(i) { + if field_char == c { + match_count += 1; + } + } + } + + let length_diff_penalty = (field.len() as f64 - search_term.len() as f64).abs() * 0.1; + let base_similarity = (match_count as f64 / search_term.len() as f64) * 8.0; + + (base_similarity - length_diff_penalty).max(0.0) +} + +fn recency_factor(date_added: i64) -> f64 { + let current_time = chrono::Utc::now().timestamp(); + let age_in_seconds = current_time - date_added; + let age_in_days = age_in_seconds as f64 / 86400.0; // Convert to days + + if age_in_days < 30.0 { + 5.0 // Boost recent items + } else if age_in_days < 365.0 { + 3.0 // Less boost for older items + } else { + 1.0 // Minimal boost for very old items + } +} + +#[derive(Serialize)] +pub struct SearchResult { + kind: String, + data: serde_json::Value, + score: f64, +} + +pub async fn search_for(query: String) -> Option> { + let mut results: Vec = Vec::new(); + + // Add artist results + for artist in Artist::find( + doc! { "name": { "$regex": &query, "$options": "i" } }, + None, + None, + ) + .await? + { + results.push(SearchResult { + kind: "artist".to_string(), + data: artist.api().await, + score: calculate_score(&artist.name, &query, None), + }); + } + + // Add album results + for album in Album::find( + doc! { "title": { "$regex": &query, "$options": "i" } }, + None, + None, + ) + .await? + { + results.push(SearchResult { + kind: "album".to_string(), + data: album.api().await, + score: calculate_score(&album.title, &query, None), + }); + } + + // Add track results + for track in Track::find( + doc! { "title": { "$regex": &query, "$options": "i" } }, + None, + None, + ) + .await? + { + results.push(SearchResult { + kind: "track".to_string(), + data: track.api().await, + score: calculate_score(&track.title, &query, Some(track.date_added)), + }); + } + + // Add playlist results + for playlist in Playlist::find( + doc! { "title": { "$regex": &query, "$options": "i" } }, + None, + None, + ) + .await? + { + results.push(SearchResult { + kind: "playlist".to_string(), + data: playlist.api().await, + score: calculate_score(&playlist.title, &query, None), + }); + } + + // Sort results by score (descending) + results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(Ordering::Equal)); + + // Return results + Some(results) +} diff --git a/src/route/mod.rs b/src/route/mod.rs index 74de508..22ac0b3 100644 --- a/src/route/mod.rs +++ b/src/route/mod.rs @@ -10,6 +10,7 @@ pub mod album; pub mod artist; pub mod event; pub mod playlist; +pub mod search; pub mod track; pub mod user; diff --git a/src/route/playlist.rs b/src/route/playlist.rs index c78eadd..3fd959b 100644 --- a/src/route/playlist.rs +++ b/src/route/playlist.rs @@ -20,7 +20,7 @@ use super::ToAPI; #[get("/playlists")] pub async fn playlists_route(u: User) -> FallibleApiResponse { let mut playlists = vec![ - json!({"id": "recent", "name": "Recently Played"}), + json!({"id": "recents", "name": "Recently Played"}), json!({"id": "recentlyAdded", "name": "Recently Added"}), ]; diff --git a/src/route/search.rs b/src/route/search.rs new file mode 100644 index 0000000..c830ed6 --- /dev/null +++ b/src/route/search.rs @@ -0,0 +1,12 @@ +use super::api_error; +use super::FallibleApiResponse; +use mongodb::bson::doc; +use rocket::get; +use serde_json::json; + +#[get("/search?")] +pub async fn search_route(query: String) -> FallibleApiResponse { + Ok(json!(crate::library::search::search_for(query) + .await + .ok_or_else(|| api_error("Search failed"))?)) +}