Files
music-agregator/internal/matcher/album.go
T
Alexander d52ce9508e Some more
2026-07-01 20:07:49 +02:00

136 lines
2.9 KiB
Go

package matcher
import (
"regexp"
"strings"
)
const MatchThreshold = 0.7
var (
punctuationRe = regexp.MustCompile(`[^\w\s]`)
yearRe = regexp.MustCompile(`\b(19|20)\d{2}\b`)
whitespaceRe = regexp.MustCompile(`\s+`)
noiseWords = map[string]struct{}{
"remastered": {}, "remaster": {}, "deluxe": {}, "edition": {},
"expanded": {}, "anniversary": {}, "special": {}, "limited": {},
"collectors": {}, "collector": {}, "bonus": {}, "tracks": {},
"standard": {}, "super": {},
"flac": {}, "mp3": {}, "aac": {}, "ogg": {}, "wav": {},
"24bit": {}, "16bit": {}, "48khz": {}, "44khz": {}, "96khz": {}, "192khz": {},
"cd": {}, "vinyl": {}, "lp": {}, "web": {}, "sacd": {},
"box": {}, "set": {}, "japan": {}, "shm": {},
"singles": {}, "single": {},
"pmedia": {},
}
)
func normalize(s string) string {
s = strings.ToLower(s)
s = punctuationRe.ReplaceAllString(s, " ")
s = yearRe.ReplaceAllString(s, " ")
s = whitespaceRe.ReplaceAllString(s, " ")
return strings.TrimSpace(s)
}
func tokenize(s string) []string {
var tokens []string
for _, word := range strings.Fields(s) {
if _, noise := noiseWords[word]; !noise && len(word) > 1 {
tokens = append(tokens, word)
}
}
return tokens
}
func AlbumMatchScore(albumTitle, torrentTitle, artistName string) float64 {
normAlbum := normalize(albumTitle)
normTorrent := normalize(torrentTitle)
if artistName != "" {
normArtist := normalize(artistName)
normTorrent = strings.Replace(normTorrent, normArtist, "", 1)
normTorrent = strings.TrimSpace(whitespaceRe.ReplaceAllString(normTorrent, " "))
}
if normAlbum == "" {
return 0
}
if strings.Contains(normTorrent, normAlbum) {
return 1.0
}
albumTokens := tokenize(normAlbum)
if len(albumTokens) == 0 {
return 0
}
torrentTokens := make(map[string]struct{}, len(albumTokens))
for _, t := range tokenize(normTorrent) {
torrentTokens[t] = struct{}{}
}
var matches int
for _, token := range albumTokens {
if _, found := torrentTokens[token]; found {
matches++
continue
}
for tt := range torrentTokens {
if levenshteinClose(token, tt) {
matches++
break
}
}
}
return float64(matches) / float64(len(albumTokens))
}
func levenshteinClose(a, b string) bool {
if len(a) < 3 || len(b) < 3 {
return false
}
maxDist := 1
if len(a) > 6 {
maxDist = 2
}
return levenshtein(a, b) <= maxDist
}
func levenshtein(a, b string) int {
la, lb := len(a), len(b)
if la == 0 {
return lb
}
if lb == 0 {
return la
}
prev := make([]int, lb+1)
curr := make([]int, lb+1)
for j := 0; j <= lb; j++ {
prev[j] = j
}
for i := 1; i <= la; i++ {
curr[0] = i
for j := 1; j <= lb; j++ {
cost := 1
if a[i-1] == b[j-1] {
cost = 0
}
curr[j] = min(curr[j-1]+1, min(prev[j]+1, prev[j-1]+cost))
}
prev, curr = curr, prev
}
return prev[lb]
}
func IsAlbumMatch(albumTitle, torrentTitle, artistName string) bool {
return AlbumMatchScore(albumTitle, torrentTitle, artistName) >= MatchThreshold
}