Phase 5: scaffold Crucible (sow-tools) — dispatcher + builder shims, container, PR-first CI, fail-closed (D11, D19).
This commit is contained in:
@@ -1,226 +0,0 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user