package music import ( "os" "os/exec" "path/filepath" "regexp" "sort" "strings" ) const ( MaxStemLen = 16 CreditsHeader = "# NWN Music Pack Credits\n\nOriginal rights remain with the respective composers and licensors.\n\n## Processed Files\n\n| Artist | Title | Output File | Original File | Album | Date | License / Copyright | Notes |\n|---|---|---|---|---|---|---|---|\n" ) var ( defaultConvertExtensions = []string{ ".flac", ".m4a", ".mp3", ".ogg", ".wav", } ConvertExtensions = map[string]struct{}{ ".flac": {}, ".m4a": {}, ".mp3": {}, ".ogg": {}, ".wav": {}, } PassthroughExtensions = map[string]struct{}{ ".bmu": {}, ".wav": {}, } DropWords = map[string]struct{}{ "mp3": {}, "wav": {}, "flac": {}, "ogg": {}, "m4a": {}, "music": {}, "track": {}, "audio": {}, "song": {}, "soundtrack": {}, "final": {}, "version": {}, "ver": {}, "v": {}, "mix": {}, "master": {}, "remaster": {}, "free": {}, "royalty": {}, "copyright": {}, "background": {}, "bgm": {}, "loop": {}, "loops": {}, "loopable": {}, "seamless": {}, "official": {}, "download": {}, "preview": {}, "the": {}, "a": {}, "an": {}, "and": {}, "of": {}, "to": {}, "in": {}, "for": {}, "with": {}, "by": {}, } BracketRE = regexp.MustCompile(`\[[^\]]*\]|\([^)]*\)|\{[^}]*\}`) SplitRE = regexp.MustCompile(`[^A-Za-z0-9]+`) YearRE = regexp.MustCompile(`^\d{4,}$`) VowelRE = regexp.MustCompile(`[aeiou]`) ) func DefaultConvertExtensions() []string { return append([]string(nil), defaultConvertExtensions...) } type Metadata struct { Title string Artist string Album string Date string Rights string Notes string Comment string Copyright string License string } type CreditsEntry struct { Artist string `json:"artist"` Title string `json:"title"` OutputFile string `json:"output_file"` OriginalFile string `json:"original_file"` Album string `json:"album"` Date string `json:"date"` Rights string `json:"rights"` Notes string `json:"notes"` } type CreditsSource struct { Path string `json:"path"` Kind string `json:"kind"` Entries []CreditsEntry `json:"entries"` } type CreditsInventory struct { Sources []CreditsSource `json:"sources"` } type CreditsOverlay struct { ByOriginal map[string]CreditsEntry ByOutput map[string]CreditsEntry Entries []CreditsEntry } func ResolveFFmpeg() (string, error) { return ResolveFFmpegWithConfig("") } func ResolveFFmpegWithConfig(configuredPath string) (string, error) { if configured := strings.TrimSpace(configuredPath); configured != "" { return configured, nil } if configured := strings.TrimSpace(os.Getenv("SOW_FFMPEG")); configured != "" { return configured, nil } path, err := exec.LookPath("ffmpeg") if err != nil { return "", err } return path, nil } func ResolveFFprobe() (string, error) { return ResolveFFprobeWithConfig("") } func ResolveFFprobeWithConfig(configuredPath string) (string, error) { if configured := strings.TrimSpace(configuredPath); configured != "" { return configured, nil } if configured := strings.TrimSpace(os.Getenv("SOW_FFPROBE")); configured != "" { return configured, nil } path, err := exec.LookPath("ffprobe") if err != nil { return "", err } return path, nil } func IsMusicAssetPath(rel string) bool { rel = filepath.ToSlash(strings.TrimSpace(rel)) return rel == "envi/music" || strings.HasPrefix(rel, "envi/music/") } func PathIsUnder(root, rel string) bool { root = TrimSlashes(filepath.ToSlash(root)) rel = TrimSlashes(filepath.ToSlash(rel)) if root == "" || rel == "" { return false } return rel == root || strings.HasPrefix(rel, root+"/") } func PrefixForDir(prefixes map[string]string, dir string) string { dir = TrimSlashes(filepath.ToSlash(dir)) bestPrefix := "" bestLen := -1 keys := make([]string, 0, len(prefixes)) for key := range prefixes { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { value := prefixes[key] normalizedKey := TrimSlashes(filepath.ToSlash(key)) if normalizedKey == "" { continue } if dir != normalizedKey && !strings.HasPrefix(dir, normalizedKey+"/") { continue } if len(normalizedKey) > bestLen { bestLen = len(normalizedKey) bestPrefix = SanitizePrefix(value) } } return bestPrefix } func SanitizePrefix(prefix string) string { prefix = strings.ToLower(strings.TrimSpace(prefix)) var builder strings.Builder for _, r := range prefix { switch { case r >= 'a' && r <= 'z': builder.WriteRune(r) case r >= '0' && r <= '9': builder.WriteRune(r) case r == '_': builder.WriteRune(r) } } result := builder.String() if strings.TrimSpace(prefix) != "" && strings.HasSuffix(strings.TrimSpace(prefix), "_") && !strings.HasSuffix(result, "_") { result += "_" } return result } func TrimSlashes(value string) string { return strings.Trim(strings.TrimSpace(value), "/") } func Min(a, b int) int { if a < b { return a } return b } func Max(a, b int) int { if a > b { return a } return b } func FirstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" } func JoinNonEmpty(sep string, values ...string) string { parts := make([]string, 0, len(values)) for _, value := range values { value = strings.TrimSpace(value) if value != "" { parts = append(parts, value) } } return strings.Join(parts, sep) }