--- phase: 18-batch-edit plan: 01 type: execute wave: 1 depends_on: [] files_modified: - 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 autonomous: true requirements: [BATCH-01, BATCH-03] must_haves: truths: - "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" artifacts: - path: "backend/tagwriter/pipeline.go" provides: "BatchWriteTrackTags method, BatchResult type, BatchWriteProgress event emission" contains: "func (tw *TagWriter) BatchWriteTrackTags" - path: "backend/events/events.go" provides: "BatchWriteProgress event constant" contains: "BatchWriteProgress" - path: "frontend/src/events.ts" provides: "Auto-generated BatchWriteProgress event constant" contains: "BatchWriteProgress" - path: "frontend/wailsjs/go/tagwriter/TagWriter.js" provides: "Wails binding for BatchWriteTrackTags" contains: "BatchWriteTrackTags" - path: "frontend/wailsjs/go/tagwriter/TagWriter.d.ts" provides: "TypeScript declaration for BatchWriteTrackTags" contains: "BatchWriteTrackTags" key_links: - from: "backend/tagwriter/pipeline.go" to: "backend/events/events.go" via: "EventsEmit(tw.ctx, events.BatchWriteProgress, ...)" pattern: "events\\.BatchWriteProgress" - from: "frontend/wailsjs/go/tagwriter/TagWriter.js" to: "backend/tagwriter/pipeline.go" via: "Wails binding bridge" pattern: "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. @/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md @/home/caleb/.config/opencode/get-shit-done/templates/summary.md @.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: ```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: ```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: ```go // Tag writing events. const ( TrackMetadataChanged = "TrackMetadataChanged" ) ``` Wails binding pattern (frontend/wailsjs/go/tagwriter/TagWriter.js): ```javascript export function WriteTrackTagsByPath(arg1, arg2) { return window['go']['tagwriter']['TagWriter']['WriteTrackTagsByPath'](arg1, arg2); } ``` Wails TypeScript declaration pattern (frontend/wailsjs/go/tagwriter/TagWriter.d.ts): ```typescript export function WriteTrackTagsByPath(arg1:string,arg2:tagwriter.TagChanges):Promise; ``` 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`: ```go // Tag writing events. const ( TrackMetadataChanged = "TrackMetadataChanged" BatchWriteProgress = "BatchWriteProgress" ) ``` Then regenerate the TypeScript events file: ```bash 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: ```go // BatchFailure records a single track that failed during a batch write. type BatchFailure struct { FilePath string `json:"filePath"` Error string `json:"error"` } ``` 2. **BatchResult struct** — returned from the batch method: ```go // 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"` } ``` 3. **cancelBatch field** on TagWriter — a `chan struct{}` that signals cancellation: ```go cancelBatch chan struct{} ``` Add `cancelBatch` to the TagWriter struct. Initialize to nil. The field is checked by BatchWriteTrackTags before each track. 4. **CancelBatchWrite method** — callable from frontend: ```go // 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) } } } ``` 5. **BatchWriteTrackTags method** — the core batch pipeline: ```go // 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: ```go 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. 6. **Modify WriteTrackTags event emission** — Add a check for `tw.suppressEvents` before the event emission in step 7: ```go // 7. Emit event (suppressed during batch writes). if tw.ctx != nil && !tw.suppressEvents { ``` 7. **Wails bindings** — Manually add to `frontend/wailsjs/go/tagwriter/TagWriter.js`: ```javascript 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`: ```typescript export function BatchWriteTrackTags(arg1:Array,arg2:tagwriter.TagChanges):Promise; export function CancelBatchWrite():Promise; ``` 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 - 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 After completion, create `.planning/phases/18-batch-edit/18-01-SUMMARY.md`