fix: a duplicate fold never deletes a lossless copy for a lossy one, nor folds two versions (M498)
release / govulncheck (push) Successful in 14s
release / web (push) Successful in 1m21s
release / go (push) Successful in 1m31s
release / android (push) Successful in 4m58s
release / integration (push) Successful in 5m0s
release / Build signed APK (releases and dev) (push) Successful in 5m7s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 15s
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 14s
release / web (push) Successful in 1m21s
release / go (push) Successful in 1m31s
release / android (push) Successful in 4m58s
release / integration (push) Successful in 5m0s
release / Build signed APK (releases and dev) (push) Successful in 5m7s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 15s
release / Verify release artifacts (tag releases only) (push) Skipped
The first album-copy fold pass on 2026-10-20 removed five FLACs (Måneskin Rush!, Weezer 2016) in favour of mp3s whose only edge was a recording MBID, and an explicit "Suite-Pee" for its clean version. ProposeSurvivor now ranks lossless before tag fit; tag fit still outranks file size, which is what the Humanz rips needed. The fold keys copies by their version words (title and file name), so a clean, live or instrumental copy holding the same title tag is linked, not folded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
package library
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
@@ -90,6 +92,35 @@ func ClassifyDuplicateGroup(tier string, members []ClassifyMember) DuplicateClas
|
||||
var versionSuffix = regexp.MustCompile(
|
||||
`(?i)\s-\s[^-]*\b(version|instrumental|remix|mix|live|acoustic|demo|edit|reprise|rework|vip)\b[^-]*$`)
|
||||
|
||||
// versionWord matches a word that names a version of a song.
|
||||
var versionWord = regexp.MustCompile(
|
||||
`(?i)\b(clean|explicit|instrumentals?|live|acoustic|unplugged|demo|remix|mix|edit|radio|karaoke|a ?cappella|acapella|reprise)\b`)
|
||||
|
||||
// versionWords lists, sorted and lowercased, the version words in a copy's
|
||||
// title and file name together. Two copies holding one title tag can still be
|
||||
// two versions, told apart only by the file name: "Suite-Pee" and
|
||||
// "Suite-Pee (Clean Version)", "Luv (sic) Grand Finale" and its instrumental.
|
||||
func versionWords(title, path string) string {
|
||||
seen := map[string]bool{}
|
||||
name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
||||
for _, w := range versionWord.FindAllString(title+" "+name, -1) {
|
||||
w = strings.ToLower(w)
|
||||
switch {
|
||||
case w == "instrumentals":
|
||||
w = "instrumental"
|
||||
case strings.Contains(w, "cappella") || w == "acapella":
|
||||
w = "acappella"
|
||||
}
|
||||
seen[w] = true
|
||||
}
|
||||
words := make([]string, 0, len(seen))
|
||||
for w := range seen {
|
||||
words = append(words, w)
|
||||
}
|
||||
sort.Strings(words)
|
||||
return strings.Join(words, ",")
|
||||
}
|
||||
|
||||
// songKey is MatchTitleKey with every bracketed segment and a dashed version
|
||||
// suffix dropped: the song a title names, whatever version of it.
|
||||
// "Lovesick (Instrumental)", "Lovesick - Instrumental Version" and "Lovesick"
|
||||
|
||||
Reference in New Issue
Block a user