129 lines
4.7 KiB
Rust
129 lines
4.7 KiB
Rust
use serde::Serialize;
|
|
use std::cmp::Ordering;
|
|
|
|
use crate::route::ToAPI;
|
|
|
|
use super::{album::Album, artist::Artist, playlist::Playlist, track::Track};
|
|
|
|
// Calculate a score for a given field based on its similarity to the search term and recency factor
|
|
fn calculate_score(field: &str, search_term: &str, date_added: Option<i64>) -> f64 {
|
|
// Exact match bonus: assign a high score if the field exactly matches the search term
|
|
let exact_match_bonus = if field.eq_ignore_ascii_case(search_term) {
|
|
10.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
// String similarity score: calculate how similar the field is to the search term using string matching
|
|
let similarity_score = string_similarity(field, search_term);
|
|
|
|
// Recency factor: assign a boost to recent items based on their age in days
|
|
let recency_score = if let Some(date) = date_added {
|
|
recency_factor(date)
|
|
} else {
|
|
0.0 // Assign a low score for no date added
|
|
};
|
|
|
|
// Calculate the total score by summing up the exact match bonus, string similarity score, and recency factor
|
|
exact_match_bonus + similarity_score + recency_score
|
|
}
|
|
|
|
// Calculate the similarity between two strings using character matching and length penalty
|
|
fn string_similarity(field: &str, search_term: &str) -> f64 {
|
|
// Initialize a counter for matching characters
|
|
let mut match_count = 0;
|
|
|
|
// Convert both strings to lowercase for case-insensitive comparison
|
|
let field_lower = field.to_lowercase();
|
|
let search_lower = search_term.to_lowercase();
|
|
|
|
// Iterate over the characters in the search term and count matches with the field
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate a penalty based on the length difference between the two strings
|
|
let length_diff_penalty = (field.len() as f64 - search_term.len() as f64).abs() * 0.1;
|
|
|
|
// Calculate the base similarity score based on the number of matching characters and string lengths
|
|
let base_similarity = (match_count as f64 / search_term.len() as f64) * 8.0;
|
|
|
|
// Return the similarity score, capping it at 0 to prevent negative scores
|
|
(base_similarity - length_diff_penalty).max(0.0)
|
|
}
|
|
|
|
// Calculate a recency factor based on how old an item is in days
|
|
fn recency_factor(date_added: i64) -> f64 {
|
|
// Get the current timestamp
|
|
let current_time = chrono::Utc::now().timestamp();
|
|
|
|
// Calculate the age of the item in seconds and convert it to days
|
|
let age_in_seconds = current_time - date_added;
|
|
let age_in_days = age_in_seconds as f64 / 86400.0; // Convert to days
|
|
|
|
// Assign a boost based on how old the item is, with recent items getting more points
|
|
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,
|
|
}
|
|
|
|
// Perform a search across artists, albums, tracks, and playlists based on the query term
|
|
pub async fn search_for(query: String) -> Option<Vec<SearchResult>> {
|
|
let mut results: Vec<SearchResult> = Vec::new();
|
|
|
|
// Search for artists matching the query term
|
|
for artist in Artist::find_regex_col("name", &query).await {
|
|
results.push(SearchResult {
|
|
kind: "artist".to_string(),
|
|
data: artist.api().await,
|
|
score: calculate_score(&artist.name, &query, None),
|
|
});
|
|
}
|
|
|
|
// Search for albums matching the query term
|
|
for album in Album::find_regex_col("title", &query).await {
|
|
results.push(SearchResult {
|
|
kind: "album".to_string(),
|
|
data: album.api().await,
|
|
score: calculate_score(&album.title, &query, None),
|
|
});
|
|
}
|
|
|
|
// Search for tracks matching the query term
|
|
for track in Track::find_regex_col("title", &query).await {
|
|
results.push(SearchResult {
|
|
kind: "track".to_string(),
|
|
data: track.api().await,
|
|
score: calculate_score(&track.title, &query, Some(track.date_added.timestamp())),
|
|
});
|
|
}
|
|
|
|
// Search for playlists matching the query term
|
|
for playlist in Playlist::find_regex_col("title", &query).await {
|
|
results.push(SearchResult {
|
|
kind: "playlist".to_string(),
|
|
data: playlist.api().await,
|
|
score: calculate_score(&playlist.title, &query, None),
|
|
});
|
|
}
|
|
|
|
// Sort the search results by their score in descending order
|
|
results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(Ordering::Equal));
|
|
|
|
Some(results)
|
|
}
|