282 lines
9.3 KiB
Go
282 lines
9.3 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf"
|
|
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/music"
|
|
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
|
|
)
|
|
|
|
type preparedMusicAssets struct {
|
|
Generated []assetResource
|
|
SkipSourceRel map[string]struct{}
|
|
Artifacts []string
|
|
TempRoots []string
|
|
Summary CreditsRefreshSummary
|
|
}
|
|
|
|
func prepareMusicAssets(p *project.Project) (*preparedMusicAssets, error) {
|
|
usedNames := make(map[string]struct{})
|
|
dirSources := make(map[string][]string)
|
|
for _, rel := range p.Inventory.AssetFiles {
|
|
rel = filepath.ToSlash(rel)
|
|
ext := strings.ToLower(filepath.Ext(rel))
|
|
name := strings.ToLower(strings.TrimSuffix(filepath.Base(rel), filepath.Ext(rel)))
|
|
if _, convertible := music.ConvertExtensions[ext]; convertible && music.IsMusicAssetPath(rel) {
|
|
dir := filepath.ToSlash(filepath.Dir(rel))
|
|
dirSources[dir] = append(dirSources[dir], rel)
|
|
continue
|
|
}
|
|
if name != "" {
|
|
usedNames[name] = struct{}{}
|
|
}
|
|
}
|
|
|
|
result := &preparedMusicAssets{
|
|
SkipSourceRel: make(map[string]struct{}),
|
|
}
|
|
if len(dirSources) == 0 {
|
|
writeResult, err := writeCreditsArtifacts(p, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.Artifacts = writeResult.Artifacts
|
|
result.Summary = writeResult.Summary
|
|
return result, nil
|
|
}
|
|
|
|
dirs := make([]string, 0, len(dirSources))
|
|
for dir := range dirSources {
|
|
dirs = append(dirs, dir)
|
|
}
|
|
sort.Strings(dirs)
|
|
|
|
generatedByDir := make(map[string][]music.CreditsEntry)
|
|
stageRoot := p.MusicStageDir()
|
|
result.TempRoots = append(result.TempRoots, stageRoot)
|
|
|
|
ffmpegPath, err := music.ResolveFFmpeg()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ffprobePath, err := music.ResolveFFprobe()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, dir := range dirs {
|
|
sources := append([]string(nil), dirSources[dir]...)
|
|
sort.Strings(sources)
|
|
prefix := music.PrefixForDir(p.EffectiveConfig().Music.Prefixes, dir)
|
|
overlayPath := filepath.Join(p.AssetsDir(), filepath.FromSlash(dir), p.EffectiveConfig().Music.CreditsOverlay)
|
|
overlay, err := music.ParseCreditsOverlay(overlayPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse credits overlay %s: %w", overlayPath, err)
|
|
}
|
|
|
|
entries := make([]music.CreditsEntry, 0, len(sources))
|
|
for _, rel := range sources {
|
|
base := filepath.Base(rel)
|
|
manual := overlay.Match(base, "")
|
|
outputFile := ""
|
|
switch {
|
|
case manual != nil && strings.TrimSpace(manual.OutputFile) != "":
|
|
outputFile = strings.ToLower(strings.TrimSpace(manual.OutputFile))
|
|
if err := music.ValidateManualOutputFile(outputFile); err != nil {
|
|
return nil, fmt.Errorf("manual credits output for %s: %w", rel, err)
|
|
}
|
|
if err := music.ReserveName(strings.TrimSuffix(outputFile, filepath.Ext(outputFile)), usedNames); err != nil {
|
|
return nil, fmt.Errorf("manual credits output for %s: %w", rel, err)
|
|
}
|
|
default:
|
|
stem, err := music.GenerateStem(base, prefix, usedNames)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("generate music name for %s: %w", rel, err)
|
|
}
|
|
outputFile = stem + ".bmu"
|
|
}
|
|
outputRel := filepath.ToSlash(filepath.Join(dir, outputFile))
|
|
metadata, err := music.ProbeMetadata(ffprobePath, filepath.Join(p.AssetsDir(), filepath.FromSlash(rel)))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("probe music metadata for %s: %w", rel, err)
|
|
}
|
|
entry := music.CreditsEntry{
|
|
Artist: metadata.Artist,
|
|
Title: music.FirstNonEmpty(metadata.Title, strings.TrimSuffix(base, filepath.Ext(base))),
|
|
OutputFile: outputFile,
|
|
OriginalFile: base,
|
|
Album: metadata.Album,
|
|
Date: metadata.Date,
|
|
Rights: music.FirstNonEmpty(metadata.Rights, music.JoinNonEmpty("; ", metadata.License, metadata.Copyright)),
|
|
Notes: music.FirstNonEmpty(metadata.Notes, metadata.Comment),
|
|
}
|
|
music.ApplyCreditsOverlay(&entry, manual)
|
|
|
|
stagePath := filepath.Join(stageRoot, filepath.FromSlash(outputRel))
|
|
if err := os.MkdirAll(filepath.Dir(stagePath), 0o755); err != nil {
|
|
return nil, fmt.Errorf("create music stage dir: %w", err)
|
|
}
|
|
if err := music.ConvertSource(ffmpegPath, filepath.Join(p.AssetsDir(), filepath.FromSlash(rel)), stagePath); err != nil {
|
|
return nil, fmt.Errorf("convert music %s: %w", rel, err)
|
|
}
|
|
resourceInfo, err := assetResourceFromPath(stagePath, outputRel, lfsAssetInfo{}, false)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load converted music %s: %w", outputRel, err)
|
|
}
|
|
info, err := os.Stat(stagePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stat converted music %s: %w", stagePath, err)
|
|
}
|
|
result.Generated = append(result.Generated, assetResource{
|
|
Rel: outputRel,
|
|
Resource: resourceInfo.Resource,
|
|
Size: erf.ArchiveSize([]erf.Resource{resourceInfo.Resource}),
|
|
CreatedAt: info.ModTime(),
|
|
ContentID: resourceInfo.ContentID,
|
|
})
|
|
result.SkipSourceRel[rel] = struct{}{}
|
|
entries = append(entries, entry)
|
|
}
|
|
if err := music.ValidateOverlayEntries(dir, overlay, entries); err != nil {
|
|
return nil, err
|
|
}
|
|
generatedByDir[dir] = entries
|
|
}
|
|
|
|
writeResult, err := writeCreditsArtifacts(p, generatedByDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
writeResult.Summary.ScannedDirs = append(writeResult.Summary.ScannedDirs, dirs...)
|
|
writeResult.Summary.TrackCount = len(result.Generated)
|
|
result.Artifacts = writeResult.Artifacts
|
|
result.Summary = writeResult.Summary
|
|
sort.Slice(result.Generated, func(i, j int) bool {
|
|
return compareResourceKeys(result.Generated[i].Resource, result.Generated[j].Resource) < 0
|
|
})
|
|
return result, nil
|
|
}
|
|
|
|
type creditsArtifactWriteResult struct {
|
|
Artifacts []string
|
|
Summary CreditsRefreshSummary
|
|
}
|
|
|
|
func writeCreditsArtifacts(p *project.Project, generatedByDir map[string][]music.CreditsEntry) (creditsArtifactWriteResult, error) {
|
|
creditsRoot := p.CreditsDir()
|
|
sources := make([]music.CreditsSource, 0)
|
|
desiredFiles := make(map[string][]byte)
|
|
summary := CreditsRefreshSummary{}
|
|
if generatedByDir != nil {
|
|
dirs := make([]string, 0, len(generatedByDir))
|
|
for dir := range generatedByDir {
|
|
dirs = append(dirs, dir)
|
|
}
|
|
sort.Strings(dirs)
|
|
for _, dir := range dirs {
|
|
entries := append([]music.CreditsEntry(nil), generatedByDir[dir]...)
|
|
sort.Slice(entries, func(i, j int) bool {
|
|
if strings.ToLower(entries[i].Artist) != strings.ToLower(entries[j].Artist) {
|
|
return strings.ToLower(entries[i].Artist) < strings.ToLower(entries[j].Artist)
|
|
}
|
|
if strings.ToLower(entries[i].Title) != strings.ToLower(entries[j].Title) {
|
|
return strings.ToLower(entries[i].Title) < strings.ToLower(entries[j].Title)
|
|
}
|
|
return strings.ToLower(entries[i].OutputFile) < strings.ToLower(entries[j].OutputFile)
|
|
})
|
|
outputPath := filepath.Join(creditsRoot, filepath.FromSlash(dir), p.EffectiveConfig().Music.CreditsOverlay)
|
|
desiredFiles[outputPath] = []byte(music.RenderCreditsMarkdown(entries))
|
|
summary.GeneratedPaths = append(summary.GeneratedPaths, outputPath)
|
|
for _, entry := range entries {
|
|
summary.Mappings = append(summary.Mappings, MusicFileMapping{
|
|
Source: entry.OriginalFile,
|
|
Output: entry.OutputFile,
|
|
})
|
|
}
|
|
sources = append(sources, music.CreditsSource{
|
|
Path: filepath.ToSlash(outputPath),
|
|
Kind: "generated_music",
|
|
Entries: entries,
|
|
})
|
|
}
|
|
}
|
|
|
|
err := filepath.WalkDir(p.AssetsDir(), func(path string, d os.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
return walkErr
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
if strings.ToLower(d.Name()) != "credits.md" {
|
|
return nil
|
|
}
|
|
entries, err := music.ParseCreditsMarkdown(path)
|
|
if err != nil {
|
|
return fmt.Errorf("parse credits %s: %w", path, err)
|
|
}
|
|
rel, err := filepath.Rel(p.Root, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sources = append(sources, music.CreditsSource{
|
|
Path: filepath.ToSlash(rel),
|
|
Kind: "authored",
|
|
Entries: entries,
|
|
})
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return creditsArtifactWriteResult{}, err
|
|
}
|
|
|
|
sort.Slice(sources, func(i, j int) bool { return sources[i].Path < sources[j].Path })
|
|
inventoryPath := filepath.Join(creditsRoot, "credits.json")
|
|
payload, err := json.MarshalIndent(music.CreditsInventory{Sources: sources}, "", " ")
|
|
if err != nil {
|
|
return creditsArtifactWriteResult{}, fmt.Errorf("marshal credits inventory: %w", err)
|
|
}
|
|
payload = append(payload, '\n')
|
|
desiredFiles[inventoryPath] = payload
|
|
if err := os.MkdirAll(creditsRoot, 0o755); err != nil {
|
|
return creditsArtifactWriteResult{}, fmt.Errorf("create credits dir: %w", err)
|
|
}
|
|
changedFiles, err := music.SyncGeneratedCreditsArtifacts(creditsRoot, desiredFiles)
|
|
if err != nil {
|
|
return creditsArtifactWriteResult{}, err
|
|
}
|
|
artifacts := make([]string, 0, len(desiredFiles))
|
|
for path := range desiredFiles {
|
|
artifacts = append(artifacts, path)
|
|
}
|
|
sort.Strings(artifacts)
|
|
summary.ArtifactPaths = append(summary.ArtifactPaths, artifacts...)
|
|
summary.InventoryPath = inventoryPath
|
|
summary.ChangedFiles = changedFiles
|
|
return creditsArtifactWriteResult{
|
|
Artifacts: artifacts,
|
|
Summary: summary,
|
|
}, nil
|
|
}
|
|
|
|
func cleanupPreparedMusicAssets(prepared *preparedMusicAssets) error {
|
|
if prepared == nil {
|
|
return nil
|
|
}
|
|
for _, root := range prepared.TempRoots {
|
|
if strings.TrimSpace(root) == "" {
|
|
continue
|
|
}
|
|
if err := os.RemoveAll(root); err != nil {
|
|
return fmt.Errorf("remove temporary music artifact root %s: %w", root, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|