Files
yellowjacket/.planning/phases/18-batch-edit/18-01-PLAN.md
T
2026-03-18 12:30:36 -04:00

13 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
18-batch-edit 01 execute 1
backend/tagwriter/pipeline.go
backend/events/events.go
frontend/src/events.ts
frontend/wailsjs/go/tagwriter/TagWriter.js
frontend/wailsjs/go/tagwriter/TagWriter.d.ts
true
BATCH-01
BATCH-03
truths artifacts key_links
Backend can write the same tag changes to N tracks sequentially, emitting progress events after each track
Frontend can call BatchWriteTrackTags with an array of file paths and a TagChanges map
Progress events include current index, total count, current file path, and whether the batch was cancelled
Partial failures do not abort the batch — failed tracks are collected and returned as a structured result
The batch can be cancelled mid-flight via a cancel channel, and already-written tracks keep their changes
path provides contains
backend/tagwriter/pipeline.go BatchWriteTrackTags method, BatchResult type, BatchWriteProgress event emission func (tw *TagWriter) BatchWriteTrackTags
path provides contains
backend/events/events.go BatchWriteProgress event constant BatchWriteProgress
path provides contains
frontend/src/events.ts Auto-generated BatchWriteProgress event constant BatchWriteProgress
path provides contains
frontend/wailsjs/go/tagwriter/TagWriter.js Wails binding for BatchWriteTrackTags BatchWriteTrackTags
path provides contains
frontend/wailsjs/go/tagwriter/TagWriter.d.ts TypeScript declaration for BatchWriteTrackTags BatchWriteTrackTags
from to via pattern
backend/tagwriter/pipeline.go backend/events/events.go EventsEmit(tw.ctx, events.BatchWriteProgress, ...) events.BatchWriteProgress
from to via pattern
frontend/wailsjs/go/tagwriter/TagWriter.js backend/tagwriter/pipeline.go Wails binding bridge BatchWriteTrackTags
Add a backend BatchWriteTrackTags method that writes the same TagChanges to multiple tracks sequentially, emitting progress events after each track and collecting partial failures.

Purpose: The batch edit UI needs a backend endpoint that processes N tracks, reports progress per-track, supports cancellation, and returns a structured result with success/failure counts — the existing WriteTrackTagsByPath only handles one track.

Output: BatchWriteTrackTags Go method exposed via Wails, BatchWriteProgress event for live progress, BatchResult return type.

<execution_context> @/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md @/home/caleb/.config/opencode/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/18-batch-edit/18-CONTEXT.md @.planning/phases/17-single-track-edit/17-02-SUMMARY.md @.planning/phases/16-tag-writing-database-sync/16-03-SUMMARY.md

@backend/tagwriter/pipeline.go @backend/tagwriter/tagwriter.go @backend/events/events.go @frontend/src/events.ts @frontend/wailsjs/go/tagwriter/TagWriter.js @frontend/wailsjs/go/tagwriter/TagWriter.d.ts

From backend/tagwriter/tagwriter.go:

// TagChanges is a diff map of field name → new value. Only changed
// fields are present.
type TagChanges map[string]any

// Field name constants.
const (
    FieldTitle       = "title"
    FieldArtist      = "artist"
    FieldAlbum       = "album"
    FieldAlbumArtist = "album_artist"
    FieldGenre       = "genre"
    FieldYear        = "year"
    FieldTrackNumber = "track_number"
    FieldDiscNumber  = "disc_number"
    FieldComposer    = "composer"
    FieldCoverArt    = "cover_art" // []byte for set, nil for clear
)

From backend/tagwriter/pipeline.go:

type TagWriter struct {
    logger  *slog.Logger
    db      *database.DB
    ctx     context.Context // Wails context for event emission
    player  PlayerStopper
    library PipelineLocker
}

func (tw *TagWriter) WriteTrackTags(trackID int64, changes TagChanges) error { ... }
func (tw *TagWriter) WriteTrackTagsByPath(filePath string, changes TagChanges) error { ... }

From backend/events/events.go:

// Tag writing events.
const (
    TrackMetadataChanged = "TrackMetadataChanged"
)

Wails binding pattern (frontend/wailsjs/go/tagwriter/TagWriter.js):

export function WriteTrackTagsByPath(arg1, arg2) {
  return window['go']['tagwriter']['TagWriter']['WriteTrackTagsByPath'](arg1, arg2);
}

Wails TypeScript declaration pattern (frontend/wailsjs/go/tagwriter/TagWriter.d.ts):

export function WriteTrackTagsByPath(arg1:string,arg2:tagwriter.TagChanges):Promise<void>;
Task 1: Add BatchWriteProgress event constant backend/events/events.go, frontend/src/events.ts Add `BatchWriteProgress` to the "Tag writing events" const block in `backend/events/events.go`:
// Tag writing events.
const (
    TrackMetadataChanged = "TrackMetadataChanged"
    BatchWriteProgress   = "BatchWriteProgress"
)

Then regenerate the TypeScript events file:

cd backend/events && go generate ./...

This runs the existing genevents codegen tool that parses Go AST and outputs frontend/src/events.ts. Verify the generated file contains BatchWriteProgress. grep -q "BatchWriteProgress" backend/events/events.go && grep -q "BatchWriteProgress" frontend/src/events.ts && echo "PASS" BatchWriteProgress event constant exists in both Go and TypeScript, auto-generated via existing codegen pipeline.

Task 2: Add BatchWriteTrackTags method with progress, cancellation, and partial failure backend/tagwriter/pipeline.go, frontend/wailsjs/go/tagwriter/TagWriter.js, frontend/wailsjs/go/tagwriter/TagWriter.d.ts In `backend/tagwriter/pipeline.go`, add:
  1. BatchFailure struct — holds per-track failure info:
// BatchFailure records a single track that failed during a batch write.
type BatchFailure struct {
    FilePath string `json:"filePath"`
    Error    string `json:"error"`
}
  1. BatchResult struct — returned from the batch method:
// BatchResult summarises the outcome of a batch tag write.
type BatchResult struct {
    Total     int            `json:"total"`
    Succeeded int            `json:"succeeded"`
    Failed    int            `json:"failed"`
    Cancelled bool           `json:"cancelled"`
    Failures  []BatchFailure `json:"failures"`
}
  1. cancelBatch field on TagWriter — a chan struct{} that signals cancellation:
cancelBatch chan struct{}

Add cancelBatch to the TagWriter struct. Initialize to nil. The field is checked by BatchWriteTrackTags before each track.

  1. CancelBatchWrite method — callable from frontend:
// CancelBatchWrite signals the in-progress batch write to stop after
// the current track completes.
func (tw *TagWriter) CancelBatchWrite() {
    ch := tw.cancelBatch
    if ch != nil {
        select {
        case <-ch:
            // Already closed.
        default:
            close(ch)
        }
    }
}
  1. BatchWriteTrackTags method — the core batch pipeline:
// BatchWriteTrackTags applies the same TagChanges to every file in
// filePaths.  It processes tracks sequentially, emits a
// BatchWriteProgress event after each track, and continues past
// individual failures.  Returns a BatchResult summarising outcomes.
func (tw *TagWriter) BatchWriteTrackTags(filePaths []string, changes TagChanges) BatchResult {

Implementation details:

  • Create tw.cancelBatch = make(chan struct{}) at the start, defer setting it to nil.
  • Loop over filePaths with index. Before each iteration, check if cancelBatch is closed via non-blocking select; if so, set result.Cancelled = true and break.
  • Call tw.WriteTrackTagsByPath(filePath, changes) for each track. The existing method already handles pipeline lock, player safety, file write, DB sync, and TrackMetadataChanged event per track.
    • IMPORTANT: The existing WriteTrackTags acquires and releases the pipeline lock per track. This is correct for batch — we do NOT want to hold the lock for the entire batch, because that would block scan for the entire duration. Per-track locking is fine.
    • IMPORTANT: The existing WriteTrackTags emits TrackMetadataChanged per track. For batch, we want ONE invalidation at the end, not N. We need to suppress per-track events. Add a suppressEvents bool field to TagWriter that BatchWriteTrackTags sets to true during the loop, then emits a single TrackMetadataChanged after the loop completes. Modify the event emission in WriteTrackTags (step 7) to check tw.suppressEvents.
  • After each track (success or failure), emit BatchWriteProgress event with payload:
    map[string]any{
        "current":  i + 1,
        "total":    len(filePaths),
        "filePath": filePath,
        "succeeded": result.Succeeded,
        "failed":   result.Failed,
    }
    
  • On error, append to result.Failures and increment result.Failed; on success, increment result.Succeeded.
  • After the loop (or after cancel break), emit a single TrackMetadataChanged event (since per-track events were suppressed). This triggers one full library store invalidation.
  • Return the BatchResult (not an error). The return type is the struct itself, so partial success is always communicated. Wails will serialize it as JSON.
  • Log a summary at Info level: total, succeeded, failed, cancelled, duration.
  1. Modify WriteTrackTags event emission — Add a check for tw.suppressEvents before the event emission in step 7:
// 7. Emit event (suppressed during batch writes).
if tw.ctx != nil && !tw.suppressEvents {
  1. Wails bindings — Manually add to frontend/wailsjs/go/tagwriter/TagWriter.js:
export function BatchWriteTrackTags(arg1, arg2) {
  return window['go']['tagwriter']['TagWriter']['BatchWriteTrackTags'](arg1, arg2);
}

export function CancelBatchWrite() {
  return window['go']['tagwriter']['TagWriter']['CancelBatchWrite']();
}

And to frontend/wailsjs/go/tagwriter/TagWriter.d.ts:

export function BatchWriteTrackTags(arg1:Array<string>,arg2:tagwriter.TagChanges):Promise<tagwriter.BatchResult>;

export function CancelBatchWrite():Promise<void>;

Also add BatchResult and BatchFailure to the Wails models file frontend/wailsjs/go/models.ts in the tagwriter namespace (check if a tagwriter namespace already exists; if not, add it following the existing pattern).

Codebase conventions to follow:

  • godot: all doc comments end with a period.
  • nlreturn: blank line before return statements.
  • gci: imports grouped as stdlib, external, internal.
  • 100-char line limit.
  • Error wrapping with %w.
  • slog structured logging with key-value pairs. cd backend && go build ./tagwriter/... && echo "BUILD OK" && grep -q "BatchWriteTrackTags" ../frontend/wailsjs/go/tagwriter/TagWriter.js && grep -q "CancelBatchWrite" ../frontend/wailsjs/go/tagwriter/TagWriter.js && echo "BINDINGS OK" BatchWriteTrackTags method compiles, processes tracks sequentially with progress events and cancellation support, collects partial failures into BatchResult, suppresses per-track TrackMetadataChanged and emits one at the end. Wails bindings exist for BatchWriteTrackTags and CancelBatchWrite.
1. `cd backend && go build ./...` — entire backend compiles 2. `cd backend && go vet ./tagwriter/...` — no vet warnings 3. `grep -c "BatchWriteProgress\|BatchWriteTrackTags\|CancelBatchWrite\|BatchResult\|BatchFailure" backend/tagwriter/pipeline.go` — confirms all new types/methods exist 4. `grep "BatchWriteProgress" frontend/src/events.ts` — event constant auto-generated 5. `grep "BatchWriteTrackTags\|CancelBatchWrite" frontend/wailsjs/go/tagwriter/TagWriter.d.ts` — TypeScript declarations exist

<success_criteria>

  • BatchWriteTrackTags Go method exists and compiles, accepting []string filePaths and TagChanges, returning BatchResult
  • CancelBatchWrite Go method exists for mid-batch cancellation
  • BatchWriteProgress event emitted per-track with current/total/filePath/succeeded/failed
  • Per-track TrackMetadataChanged suppressed during batch; single event emitted after batch completes
  • BatchResult struct contains total, succeeded, failed, cancelled, and failures array
  • Wails bindings (JS + d.ts) manually created for both new methods
  • BatchResult type added to Wails models </success_criteria>
After completion, create `.planning/phases/18-batch-edit/18-01-SUMMARY.md`