use crate::error::SearchError;
use lyrics_core::models::TrackMetadata;
use super::compare_helper::{MatchType, rank_by_match};
use super::search_result::SearchResult;
use super::searcher::Searcher;
fn join_query(parts: [&str; 3]) -> String {
parts
.join(" ")
.replace(" - ", " ")
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
}
pub fn build_search_string(track: &TrackMetadata) -> String {
join_query([
track.title.as_deref().unwrap_or(""),
&track.artist.as_deref().unwrap_or("").replace(", ", " "),
track.album.as_deref().unwrap_or(""),
])
}
pub fn strip_feat(title: &str) -> String {
let mut new_title = title.to_string();
if let Some(idx) = new_title.find("(feat.") {
new_title = new_title[..idx].trim().to_string();
}
if let Some(idx) = new_title.find(" - feat.") {
new_title = new_title[..idx].trim().to_string();
}
new_title
}
pub fn build_refinement_queries(track: &TrackMetadata) -> Vec<String> {
let title = strip_feat(track.title.as_deref().unwrap_or(""));
let artist = track.artist.as_deref().unwrap_or("").replace(", ", " ");
vec![
join_query([&title, &artist, ""]),
join_query([&title, "", ""]),
]
}
fn search_queries(track: &TrackMetadata) -> Vec<String> {
let mut queries = vec![build_search_string(track)];
for query in build_refinement_queries(track) {
if !queries.contains(&query) {
queries.push(query);
}
}
queries
}
pub async fn search_with_refinement(
searcher: &dyn Searcher,
track: &TrackMetadata,
full_search: bool,
) -> Result<Vec<SearchResult>, SearchError> {
let mut last_error: Option<SearchError> = None;
match searcher.search_for_results(track).await {
Ok(mut results) if !results.is_empty() => {
rank_by_match(&mut results, track);
return Ok(results);
}
Ok(_) => {}
Err(error) => last_error = Some(error),
}
let mut all_results: Vec<SearchResult> = Vec::new();
for query in &search_queries(track) {
match searcher.search_for_results_str(query).await {
Ok(results) => all_results.extend(results),
Err(error) => last_error = Some(error),
}
if !full_search && !all_results.is_empty() {
break;
}
}
if all_results.is_empty()
&& let Some(error) = last_error
{
return Err(error);
}
rank_by_match(&mut all_results, track);
Ok(all_results)
}
pub async fn search_for_best_result(
searcher: &dyn Searcher,
track: &TrackMetadata,
) -> Result<Option<SearchResult>, SearchError> {
let first_error = match search_with_refinement(searcher, track, false).await {
Ok(results) => {
if let Some(first) = results.into_iter().next() {
return Ok(Some(first));
}
None
}
Err(error) => Some(error),
};
match search_with_refinement(searcher, track, true).await {
Ok(results) => Ok(results.into_iter().next()),
Err(error) => Err(first_error.unwrap_or(error)),
}
}
pub async fn search_for_best_result_with_match(
searcher: &dyn Searcher,
track: &TrackMetadata,
minimum_match: MatchType,
) -> Result<Option<SearchResult>, SearchError> {
let mut first_error: Option<SearchError> = None;
for full_search in [false, true] {
match search_with_refinement(searcher, track, full_search).await {
Ok(results) => {
let matched = results
.first()
.is_some_and(|first| first.match_type.is_some_and(|m| m >= minimum_match));
if matched {
return Ok(results.into_iter().next());
}
}
Err(error) => {
if first_error.is_none() {
first_error = Some(error);
}
}
}
}
match first_error {
Some(error) => Err(error),
None => Ok(None),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn track(title: &str, artist: &str, album: Option<&str>) -> TrackMetadata {
TrackMetadata {
title: Some(title.to_string()),
artist: Some(artist.to_string()),
album: album.map(str::to_string),
..TrackMetadata::new()
}
}
#[test]
fn falls_back_to_the_title_without_an_album() {
assert_eq!(
search_queries(&track("晴天", "周杰伦", None)),
vec!["晴天 周杰伦".to_string(), "晴天".to_string()]
);
}
#[test]
fn goes_from_exact_to_broad() {
assert_eq!(
search_queries(&track("Idol (feat. X)", "YOASOBI", Some("Idol"))),
vec![
"Idol (feat. X) YOASOBI Idol".to_string(),
"Idol YOASOBI".to_string(),
"Idol".to_string(),
]
);
}
}