57 lines
1.7 KiB
Go
57 lines
1.7 KiB
Go
// Package scanner implements the core fuzzy-diff engine of NaviWatcher.
|
|
//
|
|
// It compares a user's local albums (from Navidrome) against an artist's
|
|
// external discography (from MusicBrainz) and reports the releases that are
|
|
// present externally but have no sufficiently similar local album.
|
|
package scanner
|
|
|
|
import (
|
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
|
"naviwatcher/internal/normalize"
|
|
)
|
|
|
|
// Similarity returns a normalized similarity score in the range [0.0, 1.0]
|
|
// between two strings. The strings are normalized first (lowercased,
|
|
// bracketed/parenthesized content and years stripped, special characters
|
|
// removed), then compared with a Levenshtein-distance-based ratio.
|
|
//
|
|
// A score of 1.0 means the normalized strings are identical; 0.0 means they
|
|
// share nothing. Empty strings (after normalization) always score 0.0.
|
|
func Similarity(a, b string) float64 {
|
|
na := normalize.NormalizeString(a)
|
|
nb := normalize.NormalizeString(b)
|
|
|
|
// Two empty inputs are not considered a match.
|
|
if na == "" && nb == "" {
|
|
return 0.0
|
|
}
|
|
// One empty, one non-empty: no similarity.
|
|
if na == "" || nb == "" {
|
|
return 0.0
|
|
}
|
|
|
|
dist := fuzzy.LevenshteinDistance(na, nb)
|
|
maxLen := len(na)
|
|
if len(nb) > maxLen {
|
|
maxLen = len(nb)
|
|
}
|
|
|
|
// Guard against maxLen == 0 (already handled above, but kept for safety).
|
|
if maxLen == 0 {
|
|
return 0.0
|
|
}
|
|
|
|
// 1.0 - normalized distance → higher is more similar.
|
|
score := 1.0 - float64(dist)/float64(maxLen)
|
|
if score < 0.0 {
|
|
return 0.0
|
|
}
|
|
return score
|
|
}
|
|
|
|
// IsMatch reports whether a and b are similar enough to be considered the
|
|
// same release, given the provided threshold in [0.0, 1.0].
|
|
func IsMatch(a, b string, threshold float64) bool {
|
|
return Similarity(a, b) >= threshold
|
|
}
|