310 lines
13 KiB
Markdown
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>
|