293 lines
7.9 KiB
Go
293 lines
7.9 KiB
Go
package music
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
func ParseCreditsOverlay(path string) (CreditsOverlay, error) {
|
|
entries, err := ParseCreditsMarkdown(path)
|
|
if err != nil {
|
|
return CreditsOverlay{}, err
|
|
}
|
|
overlay := CreditsOverlay{
|
|
ByOriginal: make(map[string]CreditsEntry),
|
|
ByOutput: make(map[string]CreditsEntry),
|
|
Entries: entries,
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.OriginalFile != "" {
|
|
key := strings.ToLower(strings.TrimSpace(entry.OriginalFile))
|
|
if _, exists := overlay.ByOriginal[key]; exists {
|
|
return CreditsOverlay{}, fmt.Errorf("duplicate manual credits row for original file %s", entry.OriginalFile)
|
|
}
|
|
overlay.ByOriginal[key] = entry
|
|
}
|
|
if entry.OutputFile != "" {
|
|
key := strings.ToLower(strings.TrimSpace(entry.OutputFile))
|
|
if _, exists := overlay.ByOutput[key]; exists {
|
|
return CreditsOverlay{}, fmt.Errorf("duplicate manual credits row for output file %s", entry.OutputFile)
|
|
}
|
|
overlay.ByOutput[key] = entry
|
|
}
|
|
}
|
|
return overlay, nil
|
|
}
|
|
|
|
func (o CreditsOverlay) Match(originalFile, outputFile string) *CreditsEntry {
|
|
if entry, ok := o.ByOutput[strings.ToLower(strings.TrimSpace(outputFile))]; ok {
|
|
entryCopy := entry
|
|
return &entryCopy
|
|
}
|
|
if entry, ok := o.ByOriginal[strings.ToLower(strings.TrimSpace(originalFile))]; ok {
|
|
entryCopy := entry
|
|
return &entryCopy
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ValidateOverlayEntries(dir string, overlay CreditsOverlay, generated []CreditsEntry) error {
|
|
if len(overlay.Entries) == 0 {
|
|
return nil
|
|
}
|
|
validOriginal := make(map[string]struct{}, len(generated))
|
|
validOutput := make(map[string]struct{}, len(generated))
|
|
for _, entry := range generated {
|
|
validOriginal[strings.ToLower(entry.OriginalFile)] = struct{}{}
|
|
validOutput[strings.ToLower(entry.OutputFile)] = struct{}{}
|
|
}
|
|
for _, entry := range overlay.Entries {
|
|
if entry.OriginalFile != "" {
|
|
if _, ok := validOriginal[strings.ToLower(entry.OriginalFile)]; !ok {
|
|
return fmt.Errorf("manual credits entry in %s references unknown original file %s", dir, entry.OriginalFile)
|
|
}
|
|
}
|
|
if entry.OutputFile != "" {
|
|
if _, ok := validOutput[strings.ToLower(entry.OutputFile)]; !ok {
|
|
return fmt.Errorf("manual credits entry in %s references unknown output file %s", dir, entry.OutputFile)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ParseCreditsMarkdown(path string) ([]CreditsEntry, error) {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
lines := strings.Split(strings.ReplaceAll(string(raw), "\r\n", "\n"), "\n")
|
|
header := []string(nil)
|
|
entries := make([]CreditsEntry, 0)
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if !strings.HasPrefix(line, "|") {
|
|
continue
|
|
}
|
|
cells := ParseMarkdownTableRow(line)
|
|
if len(cells) == 0 {
|
|
continue
|
|
}
|
|
if header == nil {
|
|
header = make([]string, len(cells))
|
|
for i, cell := range cells {
|
|
header[i] = NormalizeCreditsHeader(cell)
|
|
}
|
|
continue
|
|
}
|
|
if IsMarkdownSeparatorRow(cells) {
|
|
continue
|
|
}
|
|
entry := CreditsEntry{}
|
|
for i, key := range header {
|
|
if i >= len(cells) {
|
|
continue
|
|
}
|
|
value := CleanCreditsCell(cells[i])
|
|
switch key {
|
|
case "artist":
|
|
entry.Artist = value
|
|
case "title":
|
|
entry.Title = value
|
|
case "output_file":
|
|
entry.OutputFile = value
|
|
case "original_file":
|
|
entry.OriginalFile = value
|
|
case "album":
|
|
entry.Album = value
|
|
case "date":
|
|
entry.Date = value
|
|
case "rights":
|
|
entry.Rights = value
|
|
case "notes":
|
|
entry.Notes = value
|
|
}
|
|
}
|
|
if entry.Artist == "" && entry.Title == "" && entry.OutputFile == "" && entry.OriginalFile == "" {
|
|
continue
|
|
}
|
|
entries = append(entries, entry)
|
|
}
|
|
return entries, nil
|
|
}
|
|
|
|
func ParseMarkdownTableRow(line string) []string {
|
|
trimmed := strings.TrimSpace(line)
|
|
trimmed = strings.TrimPrefix(trimmed, "|")
|
|
trimmed = strings.TrimSuffix(trimmed, "|")
|
|
parts := strings.Split(trimmed, "|")
|
|
cells := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
cells = append(cells, strings.TrimSpace(strings.ReplaceAll(part, `\|`, "|")))
|
|
}
|
|
return cells
|
|
}
|
|
|
|
func IsMarkdownSeparatorRow(cells []string) bool {
|
|
if len(cells) == 0 {
|
|
return false
|
|
}
|
|
for _, cell := range cells {
|
|
cell = strings.TrimSpace(cell)
|
|
cell = strings.ReplaceAll(cell, "-", "")
|
|
cell = strings.ReplaceAll(cell, ":", "")
|
|
if cell != "" {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func NormalizeCreditsHeader(value string) string {
|
|
switch strings.ToLower(CleanCreditsCell(value)) {
|
|
case "artist":
|
|
return "artist"
|
|
case "title":
|
|
return "title"
|
|
case "output file":
|
|
return "output_file"
|
|
case "original file":
|
|
return "original_file"
|
|
case "album":
|
|
return "album"
|
|
case "date":
|
|
return "date"
|
|
case "license / copyright":
|
|
return "rights"
|
|
case "notes":
|
|
return "notes"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func CleanCreditsCell(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
value = strings.Trim(value, "`")
|
|
value = strings.ReplaceAll(value, "<br>", "\n")
|
|
return strings.TrimSpace(value)
|
|
}
|
|
|
|
func RenderCreditsMarkdown(entries []CreditsEntry) string {
|
|
var builder strings.Builder
|
|
builder.WriteString(CreditsHeader)
|
|
for _, entry := range entries {
|
|
builder.WriteString("| ")
|
|
builder.WriteString(EscapeMarkdownCell(entry.Artist))
|
|
builder.WriteString(" | ")
|
|
builder.WriteString(EscapeMarkdownCell(entry.Title))
|
|
builder.WriteString(" | ")
|
|
builder.WriteString(EscapeMarkdownCell(WrapCodeCell(entry.OutputFile)))
|
|
builder.WriteString(" | ")
|
|
builder.WriteString(EscapeMarkdownCell(WrapCodeCell(entry.OriginalFile)))
|
|
builder.WriteString(" | ")
|
|
builder.WriteString(EscapeMarkdownCell(entry.Album))
|
|
builder.WriteString(" | ")
|
|
builder.WriteString(EscapeMarkdownCell(entry.Date))
|
|
builder.WriteString(" | ")
|
|
builder.WriteString(EscapeMarkdownCell(entry.Rights))
|
|
builder.WriteString(" | ")
|
|
builder.WriteString(EscapeMarkdownCell(entry.Notes))
|
|
builder.WriteString(" |\n")
|
|
}
|
|
return builder.String()
|
|
}
|
|
|
|
func WrapCodeCell(value string) string {
|
|
if strings.TrimSpace(value) == "" {
|
|
return ""
|
|
}
|
|
return "`" + value + "`"
|
|
}
|
|
|
|
func EscapeMarkdownCell(value string) string {
|
|
value = strings.ReplaceAll(value, `\`, `\\`)
|
|
value = strings.ReplaceAll(value, "|", `\|`)
|
|
value = strings.ReplaceAll(value, "\n", "<br>")
|
|
return value
|
|
}
|
|
|
|
func ApplyCreditsOverlay(entry *CreditsEntry, overlay *CreditsEntry) {
|
|
if overlay == nil {
|
|
return
|
|
}
|
|
entry.Artist = FirstNonEmpty(overlay.Artist, entry.Artist)
|
|
entry.Title = FirstNonEmpty(overlay.Title, entry.Title)
|
|
entry.Album = FirstNonEmpty(overlay.Album, entry.Album)
|
|
entry.Date = FirstNonEmpty(overlay.Date, entry.Date)
|
|
entry.Rights = FirstNonEmpty(overlay.Rights, entry.Rights)
|
|
entry.Notes = FirstNonEmpty(overlay.Notes, entry.Notes)
|
|
}
|
|
|
|
func SyncGeneratedCreditsArtifacts(root string, desiredFiles map[string][]byte) (int, error) {
|
|
changedFiles := 0
|
|
existingFiles := make(map[string]struct{})
|
|
if err := filepath.WalkDir(root, func(path string, d os.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
if os.IsNotExist(walkErr) {
|
|
return nil
|
|
}
|
|
return walkErr
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
existingFiles[path] = struct{}{}
|
|
return nil
|
|
}); err != nil {
|
|
return 0, fmt.Errorf("scan credits dir: %w", err)
|
|
}
|
|
|
|
for path, content := range desiredFiles {
|
|
current, err := os.ReadFile(path)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return 0, fmt.Errorf("read credits artifact %s: %w", path, err)
|
|
}
|
|
if err == nil && string(current) == string(content) {
|
|
delete(existingFiles, path)
|
|
continue
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return 0, fmt.Errorf("create generated credits dir: %w", err)
|
|
}
|
|
if err := os.WriteFile(path, content, 0o644); err != nil {
|
|
return 0, fmt.Errorf("write credits artifact %s: %w", path, err)
|
|
}
|
|
changedFiles++
|
|
delete(existingFiles, path)
|
|
}
|
|
|
|
for path := range existingFiles {
|
|
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
|
return 0, fmt.Errorf("remove stale credits artifact %s: %w", path, err)
|
|
}
|
|
changedFiles++
|
|
}
|
|
return changedFiles, nil
|
|
}
|