198 lines
4.8 KiB
Go
198 lines
4.8 KiB
Go
package music
|
|
|
|
import (
|
|
"crypto/sha1"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
func GenerateStem(fileName, prefix string, used map[string]struct{}) (string, error) {
|
|
return GenerateStemWithMax(fileName, prefix, MaxStemLen, used)
|
|
}
|
|
|
|
func GenerateStemWithMax(fileName, prefix string, maxStemLen int, used map[string]struct{}) (string, error) {
|
|
if maxStemLen <= 0 {
|
|
maxStemLen = MaxStemLen
|
|
}
|
|
maxCore := maxStemLen - len(prefix)
|
|
if maxCore < 3 {
|
|
return "", fmt.Errorf("prefix too long: %q", prefix)
|
|
}
|
|
base := strings.TrimSuffix(fileName, filepath.Ext(fileName))
|
|
words := SlugWords(base)
|
|
core := MakeMusicCore(words, maxCore)
|
|
if core == "" {
|
|
sum := sha1.Sum([]byte(fileName))
|
|
core = fmt.Sprintf("%x", sum[:])[:maxCore]
|
|
}
|
|
stem := strings.TrimRight((prefix + core)[:Min(maxStemLen, len(prefix)+len(core))], "_")
|
|
if stem == "" {
|
|
return "", fmt.Errorf("could not derive music stem for %s", fileName)
|
|
}
|
|
return UniqueNameWithMax(stem, maxStemLen, used), nil
|
|
}
|
|
|
|
func SlugWords(value string) []string {
|
|
value = BracketRE.ReplaceAllString(value, " ")
|
|
value = strings.ReplaceAll(value, "&", " and ")
|
|
value = strings.ReplaceAll(value, "'", "")
|
|
value = strings.ReplaceAll(value, "\u2019", "")
|
|
value = strings.ToLower(value)
|
|
var ascii strings.Builder
|
|
for _, r := range value {
|
|
switch {
|
|
case r >= 'a' && r <= 'z':
|
|
ascii.WriteRune(r)
|
|
case r >= '0' && r <= '9':
|
|
ascii.WriteRune(r)
|
|
default:
|
|
ascii.WriteRune(' ')
|
|
}
|
|
}
|
|
rawWords := SplitRE.Split(ascii.String(), -1)
|
|
words := make([]string, 0, len(rawWords))
|
|
for _, word := range rawWords {
|
|
if word == "" {
|
|
continue
|
|
}
|
|
if _, drop := DropWords[word]; drop {
|
|
continue
|
|
}
|
|
if YearRE.MatchString(word) {
|
|
continue
|
|
}
|
|
words = append(words, word)
|
|
}
|
|
return words
|
|
}
|
|
|
|
func MakeMusicCore(words []string, maxCore int) string {
|
|
core := ""
|
|
for _, word := range words {
|
|
candidate := word
|
|
if core != "" {
|
|
candidate = core + "_" + word
|
|
}
|
|
if len(candidate) <= maxCore {
|
|
core = candidate
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
if core != "" {
|
|
return core
|
|
}
|
|
abbrev := make([]string, 0, Min(4, len(words)))
|
|
for _, word := range words {
|
|
if len(abbrev) == 4 {
|
|
break
|
|
}
|
|
abbrev = append(abbrev, AbbreviateWord(word, 5))
|
|
}
|
|
for _, word := range abbrev {
|
|
candidate := word
|
|
if core != "" {
|
|
candidate = core + "_" + word
|
|
}
|
|
if len(candidate) <= maxCore {
|
|
core = candidate
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
if core != "" {
|
|
return core
|
|
}
|
|
if len(words) > 0 {
|
|
return strings.Trim(words[0][:Min(maxCore, len(words[0]))], "_")
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func AbbreviateWord(word string, maxLen int) string {
|
|
if len(word) <= maxLen {
|
|
return word
|
|
}
|
|
consonants := VowelRE.ReplaceAllString(word, "")
|
|
candidate := word[:1]
|
|
if len(consonants) > 1 {
|
|
candidate += consonants[1:]
|
|
}
|
|
if len(candidate) >= 3 {
|
|
return candidate[:Min(maxLen, len(candidate))]
|
|
}
|
|
return word[:Min(maxLen, len(word))]
|
|
}
|
|
|
|
func UniqueName(stem string, used map[string]struct{}) string {
|
|
return UniqueNameWithMax(stem, MaxStemLen, used)
|
|
}
|
|
|
|
func UniqueNameWithMax(stem string, maxStemLen int, used map[string]struct{}) string {
|
|
if maxStemLen <= 0 {
|
|
maxStemLen = MaxStemLen
|
|
}
|
|
if _, exists := used[stem]; !exists {
|
|
used[stem] = struct{}{}
|
|
return stem
|
|
}
|
|
for index := 1; ; index++ {
|
|
suffix := fmt.Sprintf("_%d", index)
|
|
prefixLen := Min(len(stem), Max(0, maxStemLen-len(suffix)))
|
|
candidate := strings.TrimRight(stem[:prefixLen], "_") + suffix
|
|
if _, exists := used[candidate]; exists {
|
|
continue
|
|
}
|
|
used[candidate] = struct{}{}
|
|
return candidate
|
|
}
|
|
}
|
|
|
|
func ReserveName(stem string, used map[string]struct{}) error {
|
|
stem = strings.ToLower(strings.TrimSpace(stem))
|
|
if stem == "" {
|
|
return fmt.Errorf("output filename is empty")
|
|
}
|
|
if _, exists := used[stem]; exists {
|
|
return fmt.Errorf("output filename %s collides with an existing asset", stem)
|
|
}
|
|
used[stem] = struct{}{}
|
|
return nil
|
|
}
|
|
|
|
func ValidateManualOutputFile(name string) error {
|
|
return ValidateManualOutputFileWithRules(name, ".bmu", MaxStemLen)
|
|
}
|
|
|
|
func ValidateManualOutputFileWithRules(name, extension string, maxStemLen int) error {
|
|
name = strings.ToLower(strings.TrimSpace(name))
|
|
extension = strings.ToLower(strings.TrimSpace(extension))
|
|
if extension == "" {
|
|
extension = ".bmu"
|
|
}
|
|
if maxStemLen <= 0 {
|
|
maxStemLen = MaxStemLen
|
|
}
|
|
if !strings.HasSuffix(name, extension) {
|
|
return fmt.Errorf("output file must end with %s", extension)
|
|
}
|
|
stem := strings.TrimSuffix(name, extension)
|
|
if stem == "" {
|
|
return fmt.Errorf("output file stem is empty")
|
|
}
|
|
if len(stem) > maxStemLen {
|
|
return fmt.Errorf("output file stem %q exceeds %d characters", stem, maxStemLen)
|
|
}
|
|
for _, r := range stem {
|
|
switch {
|
|
case r >= 'a' && r <= 'z':
|
|
case r >= '0' && r <= '9':
|
|
case r == '_':
|
|
default:
|
|
return fmt.Errorf("output file stem %q contains unsupported character %q", stem, string(r))
|
|
}
|
|
}
|
|
return nil
|
|
}
|