Fork channel

Create a new channel as a copy of main.

Rename channel

Rename main to:

Delete channel

Delete main? This cannot be undone.

prepend.go
package tools

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

// PrependInput contains the parameters for the prepend operation.
type PrependInput struct {
	Folder    string
	Prefix    string
	Recursive bool
	DryRun    bool
}

// PrependResult contains the result of a single file rename operation.
type PrependResult struct {
	Old string `json:"old"`
	New string `json:"new"`
}

// PrependSkipped contains info about a skipped file.
type PrependSkipped struct {
	File   string `json:"file"`
	Reason string `json:"reason"`
}

// PrependError contains info about a failed rename.
type PrependError struct {
	File  string `json:"file"`
	Error string `json:"error"`
}

// PrependOutput contains the complete result of the prepend operation.
type PrependOutput struct {
	Folder    string           `json:"folder"`
	Prefix    string           `json:"prefix"`
	Recursive bool             `json:"recursive"`
	DryRun    bool             `json:"dry_run"`
	Renamed   []PrependResult  `json:"renamed"`
	Skipped   []PrependSkipped `json:"skipped"`
	Errors    []PrependError   `json:"errors"`
}

// datestringRegex matches filenames starting with YYYYMMDD_HHMMSS.
var datestringRegex = regexp.MustCompile(`^\d{8}_\d{6}\.`)

// prependFolders collects the folders to process (root + immediate subdirs if recursive).
func prependFolders(input PrependInput) ([]string, error) {
	folders := []string{input.Folder}
	if !input.Recursive {
		return folders, nil
	}
	entries, err := os.ReadDir(input.Folder)
	if err != nil {
		return nil, fmt.Errorf("failed to read folder: %w", err)
	}
	for _, entry := range entries {
		if entry.IsDir() {
			folders = append(folders, filepath.Join(input.Folder, entry.Name()))
		}
	}
	return folders, nil
}

// processPrependFile handles a single file within a folder rename pass.
func processPrependFile(folder, filename, prefix string, dryRun bool, output *PrependOutput) {
	oldPath := filepath.Join(folder, filename)
	shouldRename, skipReason := shouldPrependFile(filename, prefix)

	if !shouldRename {
		if skipReason != "" {
			output.Skipped = append(output.Skipped, PrependSkipped{File: oldPath, Reason: skipReason})
		}
		return
	}

	newPath := filepath.Join(folder, prefix+"_"+filename)
	if dryRun {
		output.Renamed = append(output.Renamed, PrependResult{Old: oldPath, New: newPath})
		return
	}

	if err := os.Rename(oldPath, newPath); err != nil {
		output.Errors = append(output.Errors, PrependError{File: oldPath, Error: err.Error()})
		return
	}
	output.Renamed = append(output.Renamed, PrependResult{Old: oldPath, New: newPath})
}

// Prepend renames files in a folder by prepending a prefix.
// WAV files (.wav, .WAV) and their .data files are only renamed if they start with a datestring.
// log.txt is always renamed if present.
func Prepend(input PrependInput) (*PrependOutput, error) {
	output := &PrependOutput{
		Folder:    input.Folder,
		Prefix:    input.Prefix,
		Recursive: input.Recursive,
		DryRun:    input.DryRun,
		Renamed:   []PrependResult{},
		Skipped:   []PrependSkipped{},
		Errors:    []PrependError{},
	}

	folders, err := prependFolders(input)
	if err != nil {
		return nil, err
	}

	for _, folder := range folders {
		entries, err := os.ReadDir(folder)
		if err != nil {
			return nil, fmt.Errorf("failed to read folder %s: %w", folder, err)
		}
		for _, entry := range entries {
			if entry.IsDir() {
				continue
			}
			processPrependFile(folder, entry.Name(), input.Prefix, input.DryRun, output)
		}
	}

	return output, nil
}

// shouldPrependFile determines if a file should be prepended.
// Returns (shouldRename, skipReason). If shouldRename is false and skipReason is empty,
// the file is not a target type (silently ignored).
func shouldPrependFile(filename, prefix string) (bool, string) {
	lowerName := strings.ToLower(filename)

	// Check if already prefixed (applies to all target files)
	if strings.HasPrefix(filename, prefix+"_") {
		// Only report as "already prefixed" if it's a target file type
		if filename == prefix+"_log.txt" || isWavOrData(lowerName) {
			return false, "already prefixed"
		}
		return false, ""
	}

	// Check for log.txt (exact match, case-sensitive as per spec)
	if filename == "log.txt" {
		return true, ""
	}

	// Check for WAV files and their .data files
	if !isWavOrData(lowerName) {
		return false, "" // Not a target file type, silently ignore
	}

	// Check for datestring prefix (YYYYMMDD_HHMMSS.)
	if !datestringRegex.MatchString(filename) {
		return false, "no datestring prefix"
	}

	return true, ""
}

// isWavOrData checks if the lowercase filename is a .wav or .wav.data file
func isWavOrData(lowerName string) bool {
	return strings.HasSuffix(lowerName, ".wav") || strings.HasSuffix(lowerName, ".wav.data")
}