Some more

This commit is contained in:
Alexander
2026-07-01 20:07:49 +02:00
parent b448e076bc
commit d52ce9508e
4 changed files with 4279 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
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
}
+95
View File
@@ -0,0 +1,95 @@
package matcher
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAlbumMatchScore(t *testing.T) {
tests := []struct {
album string
torrent string
artist string
wantMin float64
wantMax float64
match bool
}{
{
album: "Ain't My Bitch",
torrent: "Metallica - Ain't My Bitch (2025) [FLAC]",
artist: "Metallica",
wantMin: 0.9, wantMax: 1.0, match: true,
},
{
album: "Ain't My Bitch",
torrent: "Metallica - Aint My Bitch Remastered 2025 FLAC",
artist: "Metallica",
wantMin: 0.9, wantMax: 1.0, match: true,
},
{
album: "72 Seasons",
torrent: "Metallica - 72 Seasons (2023) [24Bit-48kHz] FLAC [PMEDIA] ⭐️",
artist: "Metallica",
wantMin: 0.9, wantMax: 1.0, match: true,
},
{
album: "Kill 'Em All",
torrent: "Metallica - Kill Em All [1983] FLAC",
artist: "Metallica",
wantMin: 0.9, wantMax: 1.0, match: true,
},
{
album: "...And Justice for All",
torrent: "Metallica - And Justice For All (Remastered) FLAC",
artist: "Metallica",
wantMin: 0.7, wantMax: 1.0, match: true,
},
{
album: "Master of Puppets",
torrent: "Metallica - Master of Puppets (Remastered Deluxe Box Set) FLAC",
artist: "Metallica",
wantMin: 0.9, wantMax: 1.0, match: true,
},
{
album: "Ain't My Bitch",
torrent: "Metallica - Load 1996 (Japan SHM-CD)",
artist: "Metallica",
wantMin: 0.0, wantMax: 0.3, match: false,
},
{
album: "Ain't My Bitch",
torrent: "Metallica - The Memory Remains (Singles, 2026 remastered)",
artist: "Metallica",
wantMin: 0.0, wantMax: 0.3, match: false,
},
{
album: "Ain't My Bitch",
torrent: "Metallica - 72 Seasons (2023) [24Bit-48kHz] FLAC",
artist: "Metallica",
wantMin: 0.0, wantMax: 0.3, match: false,
},
{
album: "Hardwired...to Self-Destruct",
torrent: "Metallica - Hardwired To Self-Destruct (Deluxe 2016) [FLAC]",
artist: "Metallica",
wantMin: 0.7, wantMax: 1.0, match: true,
},
{
album: "S&M",
torrent: "Metallica - S&M (1999) [FLAC]",
artist: "Metallica",
wantMin: 0.9, wantMax: 1.0, match: true,
},
}
for _, tt := range tests {
t.Run(tt.album+" vs "+tt.torrent, func(t *testing.T) {
score := AlbumMatchScore(tt.album, tt.torrent, tt.artist)
assert.GreaterOrEqual(t, score, tt.wantMin, "score too low: %.2f", score)
assert.LessOrEqual(t, score, tt.wantMax, "score too high: %.2f", score)
assert.Equal(t, tt.match, IsAlbumMatch(tt.album, tt.torrent, tt.artist),
"match mismatch for score %.2f", score)
})
}
}