search
This commit is contained in:
parent
7e6b65392e
commit
66b2a959d7
5 changed files with 157 additions and 2 deletions
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
141
src/library/search.rs
Normal file
141
src/library/search.rs
Normal file
|
@ -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<i64>) -> 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<Vec<SearchResult>> {
|
||||
let mut results: Vec<SearchResult> = 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)
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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"}),
|
||||
];
|
||||
|
||||
|
|
12
src/route/search.rs
Normal file
12
src/route/search.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use super::api_error;
|
||||
use super::FallibleApiResponse;
|
||||
use mongodb::bson::doc;
|
||||
use rocket::get;
|
||||
use serde_json::json;
|
||||
|
||||
#[get("/search?<query>")]
|
||||
pub async fn search_route(query: String) -> FallibleApiResponse {
|
||||
Ok(json!(crate::library::search::search_for(query)
|
||||
.await
|
||||
.ok_or_else(|| api_error("Search failed"))?))
|
||||
}
|
Loading…
Reference in a new issue