213 lines
5.2 KiB
Go
213 lines
5.2 KiB
Go
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 (
|
|
ConvertExtensions = map[string]struct{}{
|
|
".mp3": {},
|
|
".ogg": {},
|
|
}
|
|
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]`)
|
|
)
|
|
|
|
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)
|
|
}
|