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 }