Some more
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user