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 |
|
true |
|
|
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>;
// 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.
- 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"`
}
- 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"`
}
- 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.
- 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)
}
}
}
- 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 boolfield 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 checktw.suppressEvents.
- After each track (success or failure), emit
BatchWriteProgressevent 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
TrackMetadataChangedevent (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.
- Modify WriteTrackTags event emission — Add a check for
tw.suppressEventsbefore the event emission in step 7:
// 7. Emit event (suppressed during batch writes).
if tw.ctx != nil && !tw.suppressEvents {
- 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. slogstructured 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.
<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>