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

310 lines
13 KiB
Markdown

---
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"
---
<objective>
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.
</objective>
<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>
<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
<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
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<void>;
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add BatchWriteProgress event constant</name>
<files>backend/events/events.go, frontend/src/events.ts</files>
<action>
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`.
</action>
<verify>
<automated>grep -q "BatchWriteProgress" backend/events/events.go && grep -q "BatchWriteProgress" frontend/src/events.ts && echo "PASS"</automated>
</verify>
<done>BatchWriteProgress event constant exists in both Go and TypeScript, auto-generated via existing codegen pipeline.</done>
</task>
<task type="auto">
<name>Task 2: Add BatchWriteTrackTags method with progress, cancellation, and partial failure</name>
<files>backend/tagwriter/pipeline.go, frontend/wailsjs/go/tagwriter/TagWriter.js, frontend/wailsjs/go/tagwriter/TagWriter.d.ts</files>
<action>
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<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.
</action>
<verify>
<automated>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"</automated>
</verify>
<done>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.</done>
</task>
</tasks>
<verification>
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
</verification>
<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>
<output>
After completion, create `.planning/phases/18-batch-edit/18-01-SUMMARY.md`
</output>