136 lines
2.9 KiB
Go
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
|
|
}
|