command and help ux pass (#21)
Reviewed-on: #21 Co-authored-by: vickydotbat <vickydotbat@tutamail.com> Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
This commit was merged in pull request #21.
This commit is contained in:
@@ -26,8 +26,6 @@ type BuildResult struct {
|
||||
HAKPaths []string
|
||||
Manifest string
|
||||
AutogenManifestPaths []string
|
||||
CreditsArtifactPaths []string
|
||||
CreditsSummary CreditsRefreshSummary
|
||||
HAKSummary HAKArchiveSummary
|
||||
Resources int
|
||||
HAKAssets int
|
||||
@@ -36,21 +34,6 @@ type BuildResult struct {
|
||||
TopPackageFiles int
|
||||
}
|
||||
|
||||
type CreditsRefreshSummary struct {
|
||||
ScannedDirs []string
|
||||
TrackCount int
|
||||
ArtifactPaths []string
|
||||
GeneratedPaths []string
|
||||
InventoryPath string
|
||||
ChangedFiles int
|
||||
Mappings []MusicFileMapping
|
||||
}
|
||||
|
||||
type MusicFileMapping struct {
|
||||
Source string
|
||||
Output string
|
||||
}
|
||||
|
||||
type HAKArchiveSummary struct {
|
||||
Total int
|
||||
Reused int
|
||||
@@ -110,8 +93,6 @@ type BuildHAKOptions struct {
|
||||
ArchiveNames []string
|
||||
SourceManifestPath string
|
||||
ContentAddressedRoot string
|
||||
SkipMusic bool
|
||||
MusicDatasetIDs []string
|
||||
}
|
||||
|
||||
func Build(p *project.Project) (BuildResult, error) {
|
||||
@@ -241,26 +222,11 @@ func planOrBuildHAKs(p *project.Project, progress ProgressFunc, writeArchives bo
|
||||
if err != nil {
|
||||
return BuildResult{}, err
|
||||
}
|
||||
musicAssets := &preparedMusicAssets{SkipSourceRel: map[string]struct{}{}}
|
||||
if !opts.SkipMusic {
|
||||
musicAssets, err = prepareMusicAssetsWithOptions(p, musicPrepareOptions{
|
||||
datasetIDs: opts.MusicDatasetIDs,
|
||||
write: writeArchives,
|
||||
})
|
||||
if err != nil {
|
||||
return BuildResult{}, err
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
if cleanupErr := cleanupPreparedMusicAssets(musicAssets); cleanupErr != nil && err == nil {
|
||||
err = cleanupErr
|
||||
}
|
||||
}()
|
||||
generated2DAAssets, err := topdata.BuildGenerated2DAAssets(p, progress)
|
||||
if err != nil {
|
||||
return BuildResult{}, err
|
||||
}
|
||||
assetResources, err := collectAssetResources(p, false, allowedAssets, musicAssets, generated2DAAssets)
|
||||
assetResources, err := collectAssetResources(p, false, allowedAssets, generated2DAAssets)
|
||||
if err != nil {
|
||||
return BuildResult{}, err
|
||||
}
|
||||
@@ -277,8 +243,6 @@ func planOrBuildHAKs(p *project.Project, progress ProgressFunc, writeArchives bo
|
||||
}
|
||||
|
||||
result = BuildResult{HAKAssets: len(assetResources)}
|
||||
result.CreditsArtifactPaths = append(result.CreditsArtifactPaths, musicAssets.Artifacts...)
|
||||
result.CreditsSummary = musicAssets.Summary
|
||||
if err := writeAutogenManifestOutputs(progress, autogenManifests); err != nil {
|
||||
return BuildResult{}, err
|
||||
}
|
||||
@@ -410,8 +374,8 @@ func planOrBuildHAKs(p *project.Project, progress ProgressFunc, writeArchives bo
|
||||
}
|
||||
|
||||
// buildDirectHAKs packages HAKs directly from the content-addressed source blob
|
||||
// cache. It skips music preparation, generated 2da discovery, git creation-time
|
||||
// lookups, LFS discovery, and any materialized asset tree scan.
|
||||
// cache. It skips generated 2da discovery, git creation-time lookups, LFS
|
||||
// discovery, and any materialized asset tree scan.
|
||||
func buildDirectHAKs(p *project.Project, progress ProgressFunc, writeArchives bool, manifest *SourceBuildManifest, opts BuildHAKOptions) (BuildResult, error) {
|
||||
progressf(progress, "Collecting asset resources...")
|
||||
assetResources, err := collectDirectSourceResources(manifest, opts.ContentAddressedRoot)
|
||||
@@ -651,7 +615,7 @@ func collectModuleResources(p *project.Project, moduleHakOrder []string) ([]erf.
|
||||
return moduleResources, nil
|
||||
}
|
||||
|
||||
func collectAssetResources(p *project.Project, requireContent bool, allowed map[string]struct{}, musicAssets *preparedMusicAssets, generated2DAAssets []topdata.Generated2DAAsset) ([]assetResource, error) {
|
||||
func collectAssetResources(p *project.Project, requireContent bool, allowed map[string]struct{}, generated2DAAssets []topdata.Generated2DAAsset) ([]assetResource, error) {
|
||||
var hakResources []assetResource
|
||||
assetCreatedAt, err := collectGitAssetCreationTimes(p)
|
||||
if err != nil {
|
||||
@@ -674,11 +638,6 @@ func collectAssetResources(p *project.Project, requireContent bool, allowed map[
|
||||
}
|
||||
|
||||
for _, rel := range p.Inventory.AssetFiles {
|
||||
if musicAssets != nil {
|
||||
if _, skip := musicAssets.SkipSourceRel[filepath.ToSlash(rel)]; skip {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(allowed) > 0 {
|
||||
if _, ok := allowed[filepath.ToSlash(rel)]; !ok {
|
||||
continue
|
||||
@@ -706,16 +665,6 @@ func collectAssetResources(p *project.Project, requireContent bool, allowed map[
|
||||
ContentID: resourceInfo.ContentID,
|
||||
})
|
||||
}
|
||||
if musicAssets != nil {
|
||||
for _, asset := range musicAssets.Generated {
|
||||
if len(allowed) > 0 {
|
||||
if _, ok := allowed[filepath.ToSlash(asset.Rel)]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
hakResources = append(hakResources, asset)
|
||||
}
|
||||
}
|
||||
for _, asset := range generated2DAAssets {
|
||||
rel := filepath.ToSlash(asset.Rel)
|
||||
if len(allowed) > 0 {
|
||||
@@ -1617,20 +1566,11 @@ func plannedModuleHAKOrder(p *project.Project) (order []string, err error) {
|
||||
return append([]string(nil), manifest.ModuleHAKs...), nil
|
||||
}
|
||||
|
||||
musicAssets, err := prepareMusicAssets(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if cleanupErr := cleanupPreparedMusicAssets(musicAssets); cleanupErr != nil && err == nil {
|
||||
err = cleanupErr
|
||||
}
|
||||
}()
|
||||
generated2DAAssets, err := topdata.BuildGenerated2DAAssets(p, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assetResources, err := collectAssetResources(p, false, nil, musicAssets, generated2DAAssets)
|
||||
assetResources, err := collectAssetResources(p, false, nil, generated2DAAssets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ func ensureBuildIsCurrent(p *project.Project, modulePath string, hakPaths []stri
|
||||
}
|
||||
|
||||
if !newestSource.IsZero() && newestSource.After(oldestArchive) {
|
||||
return fmt.Errorf("built archives are older than source file %s; run build-module or build-haks before compare", newestPath)
|
||||
return fmt.Errorf("built archives are older than source file %s; run crucible module build or crucible hak build before compare", newestPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,529 +0,0 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf"
|
||||
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/music"
|
||||
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
|
||||
)
|
||||
|
||||
type preparedMusicAssets struct {
|
||||
Generated []assetResource
|
||||
SkipSourceRel map[string]struct{}
|
||||
Artifacts []string
|
||||
TempRoots []string
|
||||
Summary CreditsRefreshSummary
|
||||
}
|
||||
|
||||
type MusicBuildOptions struct {
|
||||
DatasetIDs []string
|
||||
Write bool
|
||||
}
|
||||
|
||||
type musicPrepareOptions struct {
|
||||
datasetIDs []string
|
||||
write bool
|
||||
}
|
||||
|
||||
type musicSourceGroup struct {
|
||||
DatasetID string
|
||||
Dataset project.EffectiveMusicDataset
|
||||
SourceDir string
|
||||
OutputDir string
|
||||
Sources []string
|
||||
}
|
||||
|
||||
type musicCreditGroup struct {
|
||||
CreditsRoot string
|
||||
SourceDir string
|
||||
Entries []music.CreditsEntry
|
||||
}
|
||||
|
||||
func musicRootPath(p *project.Project, path string) string {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return p.Root
|
||||
}
|
||||
if filepath.IsAbs(path) {
|
||||
return path
|
||||
}
|
||||
return filepath.Join(p.Root, filepath.FromSlash(path))
|
||||
}
|
||||
|
||||
func effectiveMusicDatasets(p *project.Project, onlyIDs []string) (map[string]project.EffectiveMusicDataset, error) {
|
||||
effective := p.EffectiveConfig()
|
||||
wanted := map[string]struct{}{}
|
||||
for _, id := range onlyIDs {
|
||||
id = strings.TrimSpace(id)
|
||||
if id != "" {
|
||||
wanted[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
datasets := map[string]project.EffectiveMusicDataset{}
|
||||
for id, dataset := range effective.Music.Datasets {
|
||||
if len(wanted) > 0 {
|
||||
if _, ok := wanted[id]; !ok {
|
||||
continue
|
||||
}
|
||||
delete(wanted, id)
|
||||
}
|
||||
datasets[id] = dataset
|
||||
}
|
||||
if len(wanted) > 0 {
|
||||
unknown := make([]string, 0, len(wanted))
|
||||
for id := range wanted {
|
||||
unknown = append(unknown, id)
|
||||
}
|
||||
sort.Strings(unknown)
|
||||
return nil, fmt.Errorf("unknown music dataset(s): %s", strings.Join(unknown, ", "))
|
||||
}
|
||||
if len(datasets) == 0 && len(wanted) == 0 {
|
||||
for _, rel := range p.Inventory.AssetFiles {
|
||||
ext := strings.ToLower(filepath.Ext(rel))
|
||||
if !music.PathIsUnder("envi/music", rel) {
|
||||
continue
|
||||
}
|
||||
if _, ok := extensionSet(effective.Music.ConvertExtensions)[ext]; ok {
|
||||
datasets["legacy_envi_music"] = project.EffectiveMusicDataset{
|
||||
Source: "envi/music",
|
||||
Output: "envi/music",
|
||||
StageRoot: effective.Music.StageRoot,
|
||||
CreditsRoot: effective.Music.CreditsRoot,
|
||||
NamingScheme: effective.Music.NamingScheme,
|
||||
OutputExtension: effective.Music.OutputExtension,
|
||||
MaxStemLength: effective.Music.MaxStemLength,
|
||||
PackageMode: "hak_asset",
|
||||
ConvertExtensions: effective.Music.ConvertExtensions,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return datasets, nil
|
||||
}
|
||||
|
||||
func extensionSet(values []string) map[string]struct{} {
|
||||
out := make(map[string]struct{}, len(values))
|
||||
for _, value := range values {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
if value != "" {
|
||||
out[value] = struct{}{}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func datasetForMusicSource(datasets map[string]project.EffectiveMusicDataset, rel, ext string) (string, project.EffectiveMusicDataset, bool) {
|
||||
bestID := ""
|
||||
var best project.EffectiveMusicDataset
|
||||
bestLen := -1
|
||||
for id, dataset := range datasets {
|
||||
if _, ok := extensionSet(dataset.ConvertExtensions)[ext]; !ok {
|
||||
continue
|
||||
}
|
||||
if !music.PathIsUnder(dataset.Source, rel) {
|
||||
continue
|
||||
}
|
||||
if len(dataset.Source) > bestLen {
|
||||
bestID = id
|
||||
best = dataset
|
||||
bestLen = len(dataset.Source)
|
||||
}
|
||||
}
|
||||
return bestID, best, bestID != ""
|
||||
}
|
||||
|
||||
func plannedMusicAsset(outputRel, sourceRel string) (assetResource, error) {
|
||||
extension := strings.TrimPrefix(strings.ToLower(filepath.Ext(outputRel)), ".")
|
||||
resourceType, ok := erf.HAKResourceTypeForExtension(extension)
|
||||
if !ok {
|
||||
return assetResource{}, fmt.Errorf("unsupported generated music resource extension %q", filepath.Ext(outputRel))
|
||||
}
|
||||
name := strings.ToLower(strings.TrimSuffix(filepath.Base(outputRel), filepath.Ext(outputRel)))
|
||||
return assetResource{
|
||||
Rel: outputRel,
|
||||
Resource: erf.Resource{
|
||||
Name: name,
|
||||
Type: resourceType,
|
||||
},
|
||||
ContentID: "music-plan:" + filepath.ToSlash(sourceRel),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func BuildMusic(p *project.Project, opts MusicBuildOptions) (BuildResult, error) {
|
||||
prepared, err := prepareMusicAssetsWithOptions(p, musicPrepareOptions{datasetIDs: opts.DatasetIDs, write: opts.Write})
|
||||
if err != nil {
|
||||
return BuildResult{}, err
|
||||
}
|
||||
if err := cleanupPreparedMusicAssets(prepared); err != nil {
|
||||
return BuildResult{}, err
|
||||
}
|
||||
return BuildResult{
|
||||
CreditsArtifactPaths: prepared.Artifacts,
|
||||
CreditsSummary: prepared.Summary,
|
||||
HAKAssets: len(prepared.Generated),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func prepareMusicAssets(p *project.Project) (*preparedMusicAssets, error) {
|
||||
return prepareMusicAssetsWithOptions(p, musicPrepareOptions{write: true})
|
||||
}
|
||||
|
||||
func prepareMusicAssetsWithOptions(p *project.Project, opts musicPrepareOptions) (*preparedMusicAssets, error) {
|
||||
usedNames := make(map[string]struct{})
|
||||
groupsByKey := make(map[string]*musicSourceGroup)
|
||||
datasets, err := effectiveMusicDatasets(p, opts.datasetIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 datasetID, dataset, ok := datasetForMusicSource(datasets, rel, ext); ok {
|
||||
sourceDir := filepath.ToSlash(filepath.Dir(rel))
|
||||
relDir, err := filepath.Rel(filepath.FromSlash(dataset.Source), filepath.FromSlash(sourceDir))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve music source directory %s: %w", sourceDir, err)
|
||||
}
|
||||
relDir = filepath.ToSlash(relDir)
|
||||
outputDir := dataset.Output
|
||||
if relDir != "." && relDir != "" {
|
||||
outputDir = filepath.ToSlash(filepath.Join(outputDir, filepath.FromSlash(relDir)))
|
||||
}
|
||||
key := datasetID + "\x00" + sourceDir
|
||||
group := groupsByKey[key]
|
||||
if group == nil {
|
||||
group = &musicSourceGroup{
|
||||
DatasetID: datasetID,
|
||||
Dataset: dataset,
|
||||
SourceDir: sourceDir,
|
||||
OutputDir: outputDir,
|
||||
}
|
||||
groupsByKey[key] = group
|
||||
}
|
||||
group.Sources = append(group.Sources, rel)
|
||||
continue
|
||||
}
|
||||
if name != "" {
|
||||
usedNames[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
result := &preparedMusicAssets{
|
||||
SkipSourceRel: make(map[string]struct{}),
|
||||
}
|
||||
if len(groupsByKey) == 0 {
|
||||
if !opts.write {
|
||||
return result, nil
|
||||
}
|
||||
writeResult, err := writeCreditsArtifacts(p, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Artifacts = writeResult.Artifacts
|
||||
result.Summary = writeResult.Summary
|
||||
return result, nil
|
||||
}
|
||||
|
||||
groups := make([]*musicSourceGroup, 0, len(groupsByKey))
|
||||
for _, group := range groupsByKey {
|
||||
sort.Strings(group.Sources)
|
||||
groups = append(groups, group)
|
||||
}
|
||||
sort.Slice(groups, func(i, j int) bool {
|
||||
if groups[i].SourceDir != groups[j].SourceDir {
|
||||
return groups[i].SourceDir < groups[j].SourceDir
|
||||
}
|
||||
return groups[i].DatasetID < groups[j].DatasetID
|
||||
})
|
||||
|
||||
creditGroups := make([]musicCreditGroup, 0, len(groups))
|
||||
for _, group := range groups {
|
||||
result.Summary.ScannedDirs = append(result.Summary.ScannedDirs, group.SourceDir)
|
||||
result.Summary.TrackCount += len(group.Sources)
|
||||
}
|
||||
ffmpegPath := ""
|
||||
ffprobePath := ""
|
||||
if opts.write {
|
||||
var err error
|
||||
ffmpegPath, err = music.ResolveFFmpegWithConfig(p.MusicFFmpegPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ffprobePath, err = music.ResolveFFprobeWithConfig(p.MusicFFprobePath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
stageRoot := musicRootPath(p, group.Dataset.StageRoot)
|
||||
result.TempRoots = append(result.TempRoots, stageRoot)
|
||||
prefix := music.SanitizePrefix(group.Dataset.Prefix)
|
||||
overlayPath := filepath.Join(p.AssetsDir(), filepath.FromSlash(group.SourceDir), 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(group.Sources))
|
||||
for _, rel := range group.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.ValidateManualOutputFileWithRules(outputFile, group.Dataset.OutputExtension, group.Dataset.MaxStemLength); 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.GenerateStemWithMax(base, prefix, group.Dataset.MaxStemLength, usedNames)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate music name for %s: %w", rel, err)
|
||||
}
|
||||
outputFile = stem + group.Dataset.OutputExtension
|
||||
}
|
||||
outputRel := filepath.ToSlash(filepath.Join(group.OutputDir, outputFile))
|
||||
result.Summary.Mappings = append(result.Summary.Mappings, MusicFileMapping{
|
||||
Source: rel,
|
||||
Output: outputRel,
|
||||
})
|
||||
result.SkipSourceRel[rel] = struct{}{}
|
||||
|
||||
if !opts.write {
|
||||
if group.Dataset.PackageMode == "hak_asset" {
|
||||
asset, err := plannedMusicAsset(outputRel, rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Generated = append(result.Generated, asset)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if group.Dataset.PackageMode == "hak_asset" {
|
||||
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,
|
||||
})
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
if opts.write {
|
||||
if err := music.ValidateOverlayEntries(group.SourceDir, overlay, entries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creditGroups = append(creditGroups, musicCreditGroup{
|
||||
CreditsRoot: group.Dataset.CreditsRoot,
|
||||
SourceDir: group.SourceDir,
|
||||
Entries: entries,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !opts.write {
|
||||
sort.Slice(result.Generated, func(i, j int) bool {
|
||||
return compareResourceKeys(result.Generated[i].Resource, result.Generated[j].Resource) < 0
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
writeResult, err := writeCreditsArtifacts(p, creditGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
writeResult.Summary.ScannedDirs = append(writeResult.Summary.ScannedDirs, result.Summary.ScannedDirs...)
|
||||
writeResult.Summary.TrackCount = result.Summary.TrackCount
|
||||
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, generated []musicCreditGroup) (creditsArtifactWriteResult, error) {
|
||||
defaultCreditsRoot := p.CreditsDir()
|
||||
sources := make([]music.CreditsSource, 0)
|
||||
desiredByRoot := map[string]map[string][]byte{}
|
||||
summary := CreditsRefreshSummary{}
|
||||
if generated != nil {
|
||||
sort.Slice(generated, func(i, j int) bool {
|
||||
if generated[i].SourceDir != generated[j].SourceDir {
|
||||
return generated[i].SourceDir < generated[j].SourceDir
|
||||
}
|
||||
return generated[i].CreditsRoot < generated[j].CreditsRoot
|
||||
})
|
||||
for _, group := range generated {
|
||||
creditsRoot := musicRootPath(p, group.CreditsRoot)
|
||||
if desiredByRoot[creditsRoot] == nil {
|
||||
desiredByRoot[creditsRoot] = map[string][]byte{}
|
||||
}
|
||||
entries := append([]music.CreditsEntry(nil), group.Entries...)
|
||||
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(group.SourceDir), p.EffectiveConfig().Music.CreditsOverlay)
|
||||
desiredByRoot[creditsRoot][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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Authored credits.md files live under the assets tree. A project with no
|
||||
// configured assets dir (e.g. a module that consumes prebuilt HAKs) has none
|
||||
// to collect — walking "" would fail with `lstat : no such file or directory`.
|
||||
if assetsDir := p.AssetsDir(); assetsDir != "" {
|
||||
err := filepath.WalkDir(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 })
|
||||
payload, err := json.MarshalIndent(music.CreditsInventory{Sources: sources}, "", " ")
|
||||
if err != nil {
|
||||
return creditsArtifactWriteResult{}, fmt.Errorf("marshal credits inventory: %w", err)
|
||||
}
|
||||
payload = append(payload, '\n')
|
||||
if len(desiredByRoot) == 0 {
|
||||
desiredByRoot[defaultCreditsRoot] = map[string][]byte{}
|
||||
}
|
||||
artifacts := make([]string, 0)
|
||||
changedFiles := 0
|
||||
roots := make([]string, 0, len(desiredByRoot))
|
||||
for root := range desiredByRoot {
|
||||
roots = append(roots, root)
|
||||
}
|
||||
sort.Strings(roots)
|
||||
for _, creditsRoot := range roots {
|
||||
desiredFiles := desiredByRoot[creditsRoot]
|
||||
inventoryPath := filepath.Join(creditsRoot, "credits.json")
|
||||
desiredFiles[inventoryPath] = payload
|
||||
if err := os.MkdirAll(creditsRoot, 0o755); err != nil {
|
||||
return creditsArtifactWriteResult{}, fmt.Errorf("create credits dir: %w", err)
|
||||
}
|
||||
changed, err := music.SyncGeneratedCreditsArtifacts(creditsRoot, desiredFiles)
|
||||
if err != nil {
|
||||
return creditsArtifactWriteResult{}, err
|
||||
}
|
||||
changedFiles += changed
|
||||
for path := range desiredFiles {
|
||||
artifacts = append(artifacts, path)
|
||||
}
|
||||
if summary.InventoryPath == "" {
|
||||
summary.InventoryPath = inventoryPath
|
||||
}
|
||||
}
|
||||
sort.Strings(artifacts)
|
||||
summary.ArtifactPaths = append(summary.ArtifactPaths, artifacts...)
|
||||
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
|
||||
}
|
||||
@@ -2929,7 +2929,7 @@ func TestPlanHAKChunksPutsNewestAssetsInLastChunk(t *testing.T) {
|
||||
t.Fatalf("scan: %v", err)
|
||||
}
|
||||
|
||||
assets, err := collectAssetResources(p, false, nil, nil, nil)
|
||||
assets, err := collectAssetResources(p, false, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("collect asset resources: %v", err)
|
||||
}
|
||||
@@ -3676,418 +3676,9 @@ func TestBuildHAKsFailsForDuplicateResourcesInSameHAK(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHAKsConvertsMusicSourcesAndWritesCreditsArtifacts(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mustMkdir(t, filepath.Join(root, "assets", "envi", "music", "westgate"))
|
||||
mustMkdir(t, filepath.Join(root, "build"))
|
||||
|
||||
mustWriteFile(t, filepath.Join(root, "nwn-tool.json"), `{
|
||||
"module": {
|
||||
"name": "Test Assets",
|
||||
"resref": "testassets"
|
||||
},
|
||||
"paths": {
|
||||
"assets": "assets",
|
||||
"build": "build"
|
||||
},
|
||||
"music": {
|
||||
"prefixes": {
|
||||
"envi/music/westgate": "mus_wg_"
|
||||
}
|
||||
},
|
||||
"haks": [
|
||||
{
|
||||
"name": "envi",
|
||||
"priority": 1,
|
||||
"max_bytes": 1048576,
|
||||
"split": false,
|
||||
"include": ["envi/**"]
|
||||
}
|
||||
]
|
||||
}
|
||||
`)
|
||||
|
||||
mustWriteFile(t, filepath.Join(root, "assets", "envi", "music", "westgate", "AleandAnecdotes.mp3"), "source-mp3")
|
||||
mustWriteFile(t, filepath.Join(root, "assets", "envi", "music", "westgate", "CREDITS.md"), "# NWN Music Pack Credits\n\n| Artist | Title | Output File | Original File | Album | Date | License / Copyright | Notes |\n|---|---|---|---|---|---|---|---|\n| Darren Curtis | Ale and Anecdotes | `mus_wg_andnc.bmu` | `AleandAnecdotes.mp3` | Darren's Commercially Free Music | | Darren Curtis Music | |\n")
|
||||
|
||||
ffprobePath := filepath.Join(root, "ffprobe")
|
||||
mustWriteFile(t, ffprobePath, "#!/bin/sh\nprintf '%s\\n' '{\"format\":{\"tags\":{\"artist\":\"Unknown\",\"title\":\"Broken Metadata\"}}}'\n")
|
||||
if err := os.Chmod(ffprobePath, 0o755); err != nil {
|
||||
t.Fatalf("chmod ffprobe: %v", err)
|
||||
}
|
||||
|
||||
ffmpegPath := filepath.Join(root, "ffmpeg")
|
||||
mustWriteFile(t, ffmpegPath, "#!/bin/sh\nfor last; do :; done\nprintf 'bmu-data' > \"$last\"\n")
|
||||
if err := os.Chmod(ffmpegPath, 0o755); err != nil {
|
||||
t.Fatalf("chmod ffmpeg: %v", err)
|
||||
}
|
||||
|
||||
t.Setenv("SOW_FFMPEG", ffmpegPath)
|
||||
t.Setenv("SOW_FFPROBE", ffprobePath)
|
||||
|
||||
p, err := project.Load(root)
|
||||
if err != nil {
|
||||
t.Fatalf("load project: %v", err)
|
||||
}
|
||||
if err := p.ValidateLayout(); err != nil {
|
||||
t.Fatalf("validate layout: %v", err)
|
||||
}
|
||||
if err := p.Scan(); err != nil {
|
||||
t.Fatalf("scan: %v", err)
|
||||
}
|
||||
|
||||
result, err := BuildHAKs(p)
|
||||
if err != nil {
|
||||
t.Fatalf("build haks: %v", err)
|
||||
}
|
||||
if len(result.CreditsArtifactPaths) == 0 {
|
||||
t.Fatal("expected credits artifacts to be written")
|
||||
}
|
||||
|
||||
manifestRaw, err := os.ReadFile(filepath.Join(root, "build", "haks.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("read manifest: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(manifestRaw), "envi/music/westgate/mus_wg_andnc.bmu") {
|
||||
t.Fatalf("expected converted bmu in manifest, got %s", string(manifestRaw))
|
||||
}
|
||||
if strings.Contains(string(manifestRaw), "AleandAnecdotes.mp3") {
|
||||
t.Fatalf("did not expect source mp3 in manifest, got %s", string(manifestRaw))
|
||||
}
|
||||
|
||||
creditsRaw, err := os.ReadFile(filepath.Join(root, ".cache", "credits", "envi", "music", "westgate", "CREDITS.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("read generated credits: %v", err)
|
||||
}
|
||||
creditsText := string(creditsRaw)
|
||||
if !strings.Contains(creditsText, "Darren Curtis") || !strings.Contains(creditsText, "mus_wg_andnc.bmu") {
|
||||
t.Fatalf("unexpected generated credits contents: %s", creditsText)
|
||||
}
|
||||
if strings.Contains(creditsText, "Broken Metadata") {
|
||||
t.Fatalf("manual credits overlay should override bad metadata: %s", creditsText)
|
||||
}
|
||||
|
||||
inventoryRaw, err := os.ReadFile(filepath.Join(root, ".cache", "credits", "credits.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("read credits inventory: %v", err)
|
||||
}
|
||||
inventoryText := string(inventoryRaw)
|
||||
if !strings.Contains(inventoryText, "assets/envi/music/westgate/CREDITS.md") {
|
||||
t.Fatalf("expected authored credits source in inventory: %s", inventoryText)
|
||||
}
|
||||
if !strings.Contains(inventoryText, ".cache/credits/envi/music/westgate/CREDITS.md") {
|
||||
t.Fatalf("expected generated credits source in inventory: %s", inventoryText)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, ".cache", "music")); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected temporary music staging to be cleaned, got err=%v", err)
|
||||
}
|
||||
|
||||
secondResult, err := BuildHAKs(p)
|
||||
if err != nil {
|
||||
t.Fatalf("build haks second pass: %v", err)
|
||||
}
|
||||
if secondResult.CreditsSummary.ChangedFiles != 0 {
|
||||
t.Fatalf("expected second credits refresh to be unchanged, got %d changed files", secondResult.CreditsSummary.ChangedFiles)
|
||||
}
|
||||
if secondResult.CreditsSummary.TrackCount != 1 {
|
||||
t.Fatalf("expected one tracked music conversion, got %d", secondResult.CreditsSummary.TrackCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHAKsUsesExplicitMusicDatasetOutsideLegacyRoot(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mustMkdir(t, filepath.Join(root, "assets", "audio", "westgate"))
|
||||
mustMkdir(t, filepath.Join(root, "build"))
|
||||
|
||||
ffprobePath := filepath.Join(root, "tools", "ffprobe")
|
||||
ffmpegPath := filepath.Join(root, "tools", "ffmpeg")
|
||||
mustMkdir(t, filepath.Dir(ffprobePath))
|
||||
mustWriteFile(t, ffprobePath, "#!/bin/sh\nprintf '%s\\n' '{\"format\":{\"tags\":{\"artist\":\"Dataset Artist\",\"title\":\"Dataset Title\"}}}'\n")
|
||||
mustWriteFile(t, ffmpegPath, "#!/bin/sh\nfor last; do :; done\nprintf 'dataset-bmu' > \"$last\"\n")
|
||||
if err := os.Chmod(ffprobePath, 0o755); err != nil {
|
||||
t.Fatalf("chmod ffprobe: %v", err)
|
||||
}
|
||||
if err := os.Chmod(ffmpegPath, 0o755); err != nil {
|
||||
t.Fatalf("chmod ffmpeg: %v", err)
|
||||
}
|
||||
|
||||
mustWriteFile(t, filepath.Join(root, "nwn-tool.yaml"), `
|
||||
module:
|
||||
name: Test Assets
|
||||
resref: testassets
|
||||
paths:
|
||||
assets: assets
|
||||
build: build
|
||||
music:
|
||||
tools:
|
||||
ffmpeg: tools/ffmpeg
|
||||
ffprobe: tools/ffprobe
|
||||
defaults:
|
||||
credits_root: custom-credits
|
||||
datasets:
|
||||
westgate_audio:
|
||||
source: audio/westgate
|
||||
output: generated/music
|
||||
prefix: wg_
|
||||
haks:
|
||||
- name: music
|
||||
priority: 1
|
||||
max_bytes: 1048576
|
||||
split: false
|
||||
include:
|
||||
- generated/music/**
|
||||
`)
|
||||
mustWriteFile(t, filepath.Join(root, "assets", "audio", "westgate", "Theme Song.mp3"), "source-mp3")
|
||||
|
||||
p, err := project.Load(root)
|
||||
if err != nil {
|
||||
t.Fatalf("load project: %v", err)
|
||||
}
|
||||
if err := p.ValidateLayout(); err != nil {
|
||||
t.Fatalf("validate layout: %v", err)
|
||||
}
|
||||
if err := p.Scan(); err != nil {
|
||||
t.Fatalf("scan: %v", err)
|
||||
}
|
||||
|
||||
result, err := BuildHAKs(p)
|
||||
if err != nil {
|
||||
t.Fatalf("build haks: %v", err)
|
||||
}
|
||||
if result.CreditsSummary.TrackCount != 1 {
|
||||
t.Fatalf("expected one music track, got %#v", result.CreditsSummary)
|
||||
}
|
||||
manifestRaw, err := os.ReadFile(filepath.Join(root, "build", "haks.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("read manifest: %v", err)
|
||||
}
|
||||
manifestText := string(manifestRaw)
|
||||
if !strings.Contains(manifestText, "generated/music/wg_theme.bmu") {
|
||||
t.Fatalf("expected dataset output resource in manifest, got:\n%s", manifestText)
|
||||
}
|
||||
if strings.Contains(manifestText, "audio/westgate/Theme Song.mp3") {
|
||||
t.Fatalf("did not expect source mp3 in manifest, got:\n%s", manifestText)
|
||||
}
|
||||
creditsRaw, err := os.ReadFile(filepath.Join(root, "custom-credits", "audio", "westgate", "CREDITS.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("read generated dataset credits: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(creditsRaw), "Dataset Artist") || !strings.Contains(string(creditsRaw), "wg_theme.bmu") {
|
||||
t.Fatalf("unexpected generated credits:\n%s", string(creditsRaw))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHAKsConvertsFLACMusicSourcesByDefault(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mustMkdir(t, filepath.Join(root, "assets", "audio", "westgate"))
|
||||
mustMkdir(t, filepath.Join(root, "build"))
|
||||
|
||||
ffprobePath := filepath.Join(root, "tools", "ffprobe")
|
||||
ffmpegPath := filepath.Join(root, "tools", "ffmpeg")
|
||||
mustMkdir(t, filepath.Dir(ffprobePath))
|
||||
mustWriteFile(t, ffprobePath, "#!/bin/sh\nprintf '%s\\n' '{\"format\":{\"tags\":{\"artist\":\"FLAC Artist\",\"title\":\"FLAC Title\"}}}'\n")
|
||||
mustWriteFile(t, ffmpegPath, "#!/bin/sh\nfor last; do :; done\nprintf 'dataset-bmu' > \"$last\"\n")
|
||||
if err := os.Chmod(ffprobePath, 0o755); err != nil {
|
||||
t.Fatalf("chmod ffprobe: %v", err)
|
||||
}
|
||||
if err := os.Chmod(ffmpegPath, 0o755); err != nil {
|
||||
t.Fatalf("chmod ffmpeg: %v", err)
|
||||
}
|
||||
|
||||
mustWriteFile(t, filepath.Join(root, "nwn-tool.yaml"), `
|
||||
module:
|
||||
name: Test Assets
|
||||
resref: testassets
|
||||
paths:
|
||||
assets: assets
|
||||
build: build
|
||||
music:
|
||||
tools:
|
||||
ffmpeg: tools/ffmpeg
|
||||
ffprobe: tools/ffprobe
|
||||
datasets:
|
||||
westgate_audio:
|
||||
source: audio/westgate
|
||||
output: generated/music
|
||||
prefix: wg_
|
||||
haks:
|
||||
- name: music
|
||||
priority: 1
|
||||
max_bytes: 1048576
|
||||
split: false
|
||||
include:
|
||||
- generated/music/**
|
||||
`)
|
||||
mustWriteFile(t, filepath.Join(root, "assets", "audio", "westgate", "Theme Song.flac"), "source-flac")
|
||||
|
||||
p, err := project.Load(root)
|
||||
if err != nil {
|
||||
t.Fatalf("load project: %v", err)
|
||||
}
|
||||
if err := p.ValidateLayout(); err != nil {
|
||||
t.Fatalf("validate layout: %v", err)
|
||||
}
|
||||
if err := p.Scan(); err != nil {
|
||||
t.Fatalf("scan: %v", err)
|
||||
}
|
||||
|
||||
result, err := BuildHAKs(p)
|
||||
if err != nil {
|
||||
t.Fatalf("build haks: %v", err)
|
||||
}
|
||||
if result.CreditsSummary.TrackCount != 1 {
|
||||
t.Fatalf("expected one music track, got %#v", result.CreditsSummary)
|
||||
}
|
||||
manifestRaw, err := os.ReadFile(filepath.Join(root, "build", "haks.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("read manifest: %v", err)
|
||||
}
|
||||
manifestText := string(manifestRaw)
|
||||
if !strings.Contains(manifestText, "generated/music/wg_theme.bmu") {
|
||||
t.Fatalf("expected generated FLAC music in manifest, got:\n%s", manifestText)
|
||||
}
|
||||
if strings.Contains(manifestText, "audio/westgate/Theme Song.flac") {
|
||||
t.Fatalf("did not expect source flac in manifest, got:\n%s", manifestText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHAKsUsesYAMLMusicConvertExtensionsForDiscovery(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mustMkdir(t, filepath.Join(root, "assets", "audio", "westgate"))
|
||||
mustMkdir(t, filepath.Join(root, "build"))
|
||||
|
||||
ffprobePath := filepath.Join(root, "tools", "ffprobe")
|
||||
ffmpegPath := filepath.Join(root, "tools", "ffmpeg")
|
||||
mustMkdir(t, filepath.Dir(ffprobePath))
|
||||
mustWriteFile(t, ffprobePath, "#!/bin/sh\nprintf '%s\\n' '{\"format\":{\"tags\":{\"artist\":\"AAC Artist\",\"title\":\"AAC Title\"}}}'\n")
|
||||
mustWriteFile(t, ffmpegPath, "#!/bin/sh\nfor last; do :; done\nprintf 'dataset-bmu' > \"$last\"\n")
|
||||
if err := os.Chmod(ffprobePath, 0o755); err != nil {
|
||||
t.Fatalf("chmod ffprobe: %v", err)
|
||||
}
|
||||
if err := os.Chmod(ffmpegPath, 0o755); err != nil {
|
||||
t.Fatalf("chmod ffmpeg: %v", err)
|
||||
}
|
||||
|
||||
mustWriteFile(t, filepath.Join(root, "nwn-tool.yaml"), `
|
||||
module:
|
||||
name: Test Assets
|
||||
resref: testassets
|
||||
paths:
|
||||
assets: assets
|
||||
build: build
|
||||
music:
|
||||
defaults:
|
||||
convert_extensions:
|
||||
- .aac
|
||||
tools:
|
||||
ffmpeg: tools/ffmpeg
|
||||
ffprobe: tools/ffprobe
|
||||
datasets:
|
||||
westgate_audio:
|
||||
source: audio/westgate
|
||||
output: generated/music
|
||||
prefix: wg_
|
||||
haks:
|
||||
- name: music
|
||||
priority: 1
|
||||
max_bytes: 1048576
|
||||
split: false
|
||||
include:
|
||||
- generated/music/**
|
||||
`)
|
||||
mustWriteFile(t, filepath.Join(root, "assets", "audio", "westgate", "Theme Song.aac"), "source-aac")
|
||||
|
||||
p, err := project.Load(root)
|
||||
if err != nil {
|
||||
t.Fatalf("load project: %v", err)
|
||||
}
|
||||
if err := p.ValidateLayout(); err != nil {
|
||||
t.Fatalf("validate layout: %v", err)
|
||||
}
|
||||
if err := p.Scan(); err != nil {
|
||||
t.Fatalf("scan: %v", err)
|
||||
}
|
||||
|
||||
result, err := BuildHAKs(p)
|
||||
if err != nil {
|
||||
t.Fatalf("build haks: %v", err)
|
||||
}
|
||||
if result.CreditsSummary.TrackCount != 1 {
|
||||
t.Fatalf("expected one music track discovered through YAML convert_extensions, got %#v", result.CreditsSummary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanHAKsDoesNotTranscodeMusicSources(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mustMkdir(t, filepath.Join(root, "assets", "audio", "westgate"))
|
||||
mustMkdir(t, filepath.Join(root, "build"))
|
||||
|
||||
ffmpegPath := filepath.Join(root, "tools", "ffmpeg")
|
||||
mustMkdir(t, filepath.Dir(ffmpegPath))
|
||||
mustWriteFile(t, ffmpegPath, "#!/bin/sh\necho should-not-run >&2\nexit 42\n")
|
||||
if err := os.Chmod(ffmpegPath, 0o755); err != nil {
|
||||
t.Fatalf("chmod ffmpeg: %v", err)
|
||||
}
|
||||
|
||||
mustWriteFile(t, filepath.Join(root, "nwn-tool.yaml"), `
|
||||
module:
|
||||
name: Test Assets
|
||||
resref: testassets
|
||||
paths:
|
||||
assets: assets
|
||||
build: build
|
||||
music:
|
||||
tools:
|
||||
ffmpeg: tools/ffmpeg
|
||||
ffprobe: tools/ffprobe
|
||||
datasets:
|
||||
westgate_audio:
|
||||
source: audio/westgate
|
||||
output: generated/music
|
||||
prefix: wg_
|
||||
haks:
|
||||
- name: music
|
||||
priority: 1
|
||||
max_bytes: 1048576
|
||||
split: false
|
||||
include:
|
||||
- generated/music/**
|
||||
`)
|
||||
mustWriteFile(t, filepath.Join(root, "assets", "audio", "westgate", "Theme Song.mp3"), "source-mp3")
|
||||
|
||||
p, err := project.Load(root)
|
||||
if err != nil {
|
||||
t.Fatalf("load project: %v", err)
|
||||
}
|
||||
if err := p.ValidateLayout(); err != nil {
|
||||
t.Fatalf("validate layout: %v", err)
|
||||
}
|
||||
if err := p.Scan(); err != nil {
|
||||
t.Fatalf("scan: %v", err)
|
||||
}
|
||||
|
||||
result, err := PlanHAKs(p)
|
||||
if err != nil {
|
||||
t.Fatalf("plan haks: %v", err)
|
||||
}
|
||||
if result.CreditsSummary.TrackCount != 1 {
|
||||
t.Fatalf("expected one planned music track, got %#v", result.CreditsSummary)
|
||||
}
|
||||
manifestRaw, err := os.ReadFile(filepath.Join(root, "build", "haks.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("read manifest: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(manifestRaw), "generated/music/wg_theme.bmu") {
|
||||
t.Fatalf("expected planned generated music in manifest, got:\n%s", string(manifestRaw))
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, ".cache", "music")); !os.IsNotExist(err) {
|
||||
t.Fatalf("plan-only should not stage music, stat err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A module-only project (consumes prebuilt HAKs, no paths.assets) must build via
|
||||
// the full `crucible module build` (pipeline.Build) without an assets tree. The
|
||||
// HAK + music stages used to crash on the unset assets dir
|
||||
// the full `crucible module build` (pipeline.Build) without an assets tree. HAK
|
||||
// collection used to crash on the unset assets dir
|
||||
// (`lstat : no such file or directory` / `Rel: can't make "" relative`).
|
||||
func TestBuildModuleOnlyProjectWithoutAssetsDir(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
|
||||
Reference in New Issue
Block a user