136 lines
4.0 KiB
Rust
136 lines
4.0 KiB
Rust
use sea_orm::DatabaseConnection;
|
|
use unicode_normalization::UnicodeNormalization;
|
|
|
|
use shanty_db::queries;
|
|
|
|
use crate::error::WatchResult;
|
|
|
|
/// Normalize a string for fuzzy comparison: NFC unicode, lowercase, trim.
|
|
pub fn normalize(s: &str) -> String {
|
|
s.nfc()
|
|
.collect::<String>()
|
|
.to_lowercase()
|
|
.trim()
|
|
.to_string()
|
|
}
|
|
|
|
/// Check if an artist is "owned" — i.e., any tracks by this artist exist in the indexed library.
|
|
pub async fn artist_is_owned(conn: &DatabaseConnection, artist_name: &str) -> WatchResult<bool> {
|
|
let normalized = normalize(artist_name);
|
|
|
|
// Check by exact artist record first
|
|
if let Some(artist) = queries::artists::find_by_name(conn, artist_name).await? {
|
|
let tracks = queries::tracks::get_by_artist(conn, artist.id).await?;
|
|
if !tracks.is_empty() {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
|
|
// Fuzzy: search tracks where the artist field matches
|
|
let tracks = queries::tracks::search(conn, artist_name).await?;
|
|
for track in &tracks {
|
|
if let Some(ref track_artist) = track.artist {
|
|
let sim = strsim::jaro_winkler(&normalized, &normalize(track_artist));
|
|
if sim > 0.85 {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
if let Some(ref album_artist) = track.album_artist {
|
|
let sim = strsim::jaro_winkler(&normalized, &normalize(album_artist));
|
|
if sim > 0.85 {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
/// Check if an album is "owned" — tracks from this album by this artist exist.
|
|
pub async fn album_is_owned(
|
|
conn: &DatabaseConnection,
|
|
artist_name: &str,
|
|
album_name: &str,
|
|
) -> WatchResult<bool> {
|
|
let norm_artist = normalize(artist_name);
|
|
let norm_album = normalize(album_name);
|
|
|
|
// Try exact lookup
|
|
if let Some(album) =
|
|
queries::albums::find_by_name_and_artist(conn, album_name, artist_name).await?
|
|
{
|
|
let tracks = queries::tracks::get_by_album(conn, album.id).await?;
|
|
if !tracks.is_empty() {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
|
|
// Fuzzy: search tracks matching album name
|
|
let tracks = queries::tracks::search(conn, album_name).await?;
|
|
for track in &tracks {
|
|
let album_match = track
|
|
.album
|
|
.as_deref()
|
|
.map(|a| strsim::jaro_winkler(&norm_album, &normalize(a)) > 0.85)
|
|
.unwrap_or(false);
|
|
let artist_match = track
|
|
.artist
|
|
.as_deref()
|
|
.or(track.album_artist.as_deref())
|
|
.map(|a| strsim::jaro_winkler(&norm_artist, &normalize(a)) > 0.85)
|
|
.unwrap_or(false);
|
|
if album_match && artist_match {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
/// Check if a specific track is "owned" — a track with matching artist + title exists.
|
|
pub async fn track_is_owned(
|
|
conn: &DatabaseConnection,
|
|
artist_name: &str,
|
|
title: &str,
|
|
) -> WatchResult<bool> {
|
|
let norm_artist = normalize(artist_name);
|
|
let norm_title = normalize(title);
|
|
|
|
// Fuzzy: search tracks matching the title
|
|
let tracks = queries::tracks::search(conn, title).await?;
|
|
for track in &tracks {
|
|
let title_match = track
|
|
.title
|
|
.as_deref()
|
|
.map(|t| strsim::jaro_winkler(&norm_title, &normalize(t)) > 0.85)
|
|
.unwrap_or(false);
|
|
let artist_match = track
|
|
.artist
|
|
.as_deref()
|
|
.or(track.album_artist.as_deref())
|
|
.map(|a| strsim::jaro_winkler(&norm_artist, &normalize(a)) > 0.85)
|
|
.unwrap_or(false);
|
|
if title_match && artist_match {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_normalize() {
|
|
assert_eq!(normalize(" Pink Floyd "), "pink floyd");
|
|
assert_eq!(normalize("RADIOHEAD"), "radiohead");
|
|
}
|
|
|
|
#[test]
|
|
fn test_normalize_unicode() {
|
|
assert_eq!(normalize("café"), normalize("café"));
|
|
}
|
|
}
|