From 8e3d86c25ea494521c17e1f0b2d87fd8ca34bad9 Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Tue, 24 Mar 2026 15:58:14 -0400 Subject: [PATCH] Added the import/cleanup functionality --- src/matcher.rs | 44 +++++++++++++++++++++++++++++++++++++------- src/tagger.rs | 2 +- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/matcher.rs b/src/matcher.rs index 33c6de0..0eb0faf 100644 --- a/src/matcher.rs +++ b/src/matcher.rs @@ -80,17 +80,21 @@ pub fn score_match(track: &track::Model, candidate: &RecordingMatch) -> f64 { let mut score = 0.6 * title_sim + 0.4 * artist_sim; - // Bonus: album name matches a release + // Album match bonus: strongly prefer recordings that appear on the track's album. + // This is critical for imported files that already have correct album tags. if let Some(ref album) = track.album { let track_album = normalize(album); if !track_album.is_empty() { + let mut best_album_sim = 0.0f64; for release in &candidate.releases { let release_title = normalize(&release.title); - let album_sim = strsim::jaro_winkler(&track_album, &release_title); - if album_sim > 0.85 { - score += 0.05; - break; - } + let sim = strsim::jaro_winkler(&track_album, &release_title); + best_album_sim = best_album_sim.max(sim); + } + if best_album_sim > 0.85 { + score += 0.15; // Strong bonus for matching album + } else if best_album_sim < 0.5 { + score -= 0.10; // Penalty for clearly wrong album } } } @@ -125,7 +129,8 @@ pub fn select_best_match( ); if confidence >= threshold { - let best_release = candidate.releases.first().cloned(); + // Pick the release that best matches the track's album name + let best_release = pick_best_release(track, &candidate.releases); let scored = ScoredMatch { recording: candidate, confidence, @@ -141,6 +146,31 @@ pub fn select_best_match( best } +/// Pick the best release from candidates based on the track's album metadata. +/// If the track has an album name, prefer the release with the closest title match. +/// Otherwise, fall back to the first release. +pub fn pick_best_release(track: &track::Model, releases: &[ReleaseRef]) -> Option { + if releases.is_empty() { + return None; + } + + let track_album = track.album.as_deref().map(normalize).unwrap_or_default(); + if track_album.is_empty() { + return releases.first().cloned(); + } + + let mut best: Option<(f64, &ReleaseRef)> = None; + for release in releases { + let sim = strsim::jaro_winkler(&track_album, &normalize(&release.title)); + match best { + Some((best_sim, _)) if sim <= best_sim => {} + _ => best = Some((sim, release)), + } + } + + best.map(|(_, r)| r.clone()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/tagger.rs b/src/tagger.rs index 0f7514b..37f1aa2 100644 --- a/src/tagger.rs +++ b/src/tagger.rs @@ -61,7 +61,7 @@ pub async fn tag_track( } let details = provider.get_recording(mbid).await?; - let best_release = details.releases.first().cloned(); + let best_release = crate::matcher::pick_best_release(track, &details.releases); (details, best_release) } else { // No MBID — search by artist + title