Files
watch/src/matching.rs
Connor Johnstone 0b336789da Formatting
2026-03-18 15:37:02 -04:00

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é"));
}
}