15 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 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 11-per-library-scan-pipeline | 01 | execute | 1 |
|
true |
|
|
Purpose: Enable per-library scanning (LSCAN-01), sequential coordination (LSCAN-02), and per-library cancel/pause scope (LSCAN-04) at the backend level.
Output: ScanLibrary(id) and ScanAllLibraries() Wails-bound methods, scan queue coordinator, updated events with library identification, CreateAudioFile with library_id.
<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/11-per-library-scan-pipeline/11-CONTEXT.md @.planning/phases/09-scan-cancellation-keyboard-shortcuts/09-01-SUMMARY.md @.planning/phases/10-schema-migration/10-02-SUMMARY.mdFrom backend/library/library.go:
type Library struct {
mu sync.Mutex
ctx context.Context
logger *slog.Logger
conf *Config
db *database.DB
rescanHooks RescanHooks
scanActive bool
scanCancel context.CancelFunc
scanPaused bool
scanPauseCh chan struct{}
}
func (l *Library) Scan() (*ScanMetrics, error)
func (l *Library) SetContext(ctx context.Context)
func (l *Library) CancelScan()
func (l *Library) PauseScan()
func (l *Library) ResumeScan()
func (l *Library) IsScanActive() bool
func (l *Library) IsScanPaused() bool
From backend/library/config.go:
type Config struct {
DirectoryPath Directory `toml:"DirectoryPath"`
ScanConcurrency ScanConcurrency `toml:"ScanConcurrency"`
}
From backend/library/metrics.go:
type ScanProgress struct {
Phase string `json:"phase"`
Total int64 `json:"total"`
Processed int64 `json:"processed"`
Added int64 `json:"added"`
Skipped int64 `json:"skipped"`
Updated int64 `json:"updated"`
}
type ScanMetrics struct { ... Cancelled bool ... }
From backend/events/events.go:
const (
LibraryScanStarted = "LibraryScanStarted"
LibraryScanProgress = "LibraryScanProgress"
LibraryScanComplete = "LibraryScanComplete"
LibraryScanCancelled = "LibraryScanCancelled"
LibraryScanPaused = "LibraryScanPaused"
LibraryScanResumed = "LibraryScanResumed"
)
From backend/database/sql/sqlcgen/libraries.sql.go:
func (q *Queries) GetLibrary(ctx context.Context, id int64) (Library, error)
func (q *Queries) GetAllLibraries(ctx context.Context) ([]Library, error)
From backend/database/sql/sqlcgen/audio_files.sql.go:
type CreateAudioFileParams struct {
FilePath string
LengthMilliseconds int64
FileTypeID int64
RecordingID int64
SampleRate int64
BitDepth int64
Channels int64
Bitrate int64
FileSize int64
Basename string
// NOTE: library_id NOT included — uses DEFAULT 0
}
func (q *Queries) GetAudioFilesByLibrary(ctx context.Context, libraryID int64) ([]AudioFile, error)
func (q *Queries) GetAllAudioFiles(ctx context.Context) ([]AudioFile, error)
-
Run
sqlc generateto regenerate Go code:sqlc generateThis will update
CreateAudioFileParamsto includeLibraryID int64. -
Add new event constants to
backend/events/events.go— add a "Scan queue events" group:// Scan queue events. const ( LibraryScanQueued = "LibraryScanQueued" LibraryScanQueueDrained = "LibraryScanQueueDrained" ) -
Regenerate TypeScript events via
go generate ./backend/events/...(uses the genevents tool). -
Add library identification fields to
ScanProgressandScanMetricsinbackend/library/metrics.go:- Add to
ScanProgress:LibraryID int64 \json:"libraryId"`andLibraryName string `json:"libraryName"`` - Add to
ScanProgress:QueuedCount int \json:"queuedCount"`` (number of libraries still queued after this one) - Add to
ScanMetrics:LibraryID int64 \json:"libraryId"`andLibraryName string `json:"libraryName"``
- Add to
-
Fix compilation — update the
CreateAudioFilecall inlibrary.gosaveAudioFile()method to includeLibraryIDfield. The library ID will be threaded through as a parameter toScan/scanInternal(done in Task 2), so for now add the field but use a placeholder0value that Task 2 will replace. Actually — since Task 2 immediately follows and both are in the same plan, addlibraryID int64as a field on theLibrarystruct (or better: pass it through the scan methods). For the compilation fix, addLibraryID: 0to the CreateAudioFileParams in saveAudioFile — Task 2 will thread the real value.
Verify the generated code compiles: go build ./backend/...
cd /mnt/vault/dev/golang/yellowjacket && sqlc generate && go generate ./backend/events/... && go build ./backend/...
CreateAudioFileParams includes LibraryID field. ScanProgress and ScanMetrics include library identification fields. New scan queue events exist in both Go and TypeScript. Code compiles.
Design:
- The
Librarystruct gains scan queue fields (protected bymu):scanQueue []scanQueueEntry— FIFO queue of library IDs to scancurrentScanLibraryID int64— the library currently being scanned (0 if none)currentScanLibraryName string— for event payloads
scanQueueEntrystruct:libraryID int64,libraryName string,libraryPath string
Wails-bound methods (exported, on *Library):
-
ScanLibrary(id int64) error:- Query
l.db.Queries.GetLibrary(l.ctx, id)to get library name and path - If library not found, return error
- Acquire
l.mu:- If this library ID is already
currentScanLibraryIDor already inscanQueue, return nil (silent dedup per CONTEXT.md) - If no scan is active (
!l.scanActive), setcurrentScanLibraryID = idand start scanning in a goroutine - If a scan is active, append to
scanQueueand emitLibraryScanQueuedevent with library name and queue length
- If this library ID is already
- Release
l.mu - Return nil
- Query
-
ScanAllLibraries() error:- Query
l.db.Queries.GetAllLibraries(l.ctx)to get all libraries - For each library, call
ScanLibrary(lib.ID)(reuses dedup logic) - Return nil
- Query
-
CancelCurrentScan()— cancels only the current library's scan (replaces oldCancelScan):- Cancel the scan context (existing
l.scanCancel()call) - The scan completion handler (
drainQueue) will automatically start the next queued library
- Cancel the scan context (existing
-
CancelAllScans()— cancels current and clears queue:- Acquire
l.mu, clearl.scanQueue, releasel.mu - Then cancel the current scan context
- Acquire
-
GetScanQueueLength() int— returns length of scan queue (for UI)
Internal scan orchestration:
-
startScan(entry scanQueueEntry)— goroutine entry point:- Calls
l.scanInternal(entry.libraryID, entry.libraryName, entry.libraryPath) - On completion, calls
l.drainQueue()
- Calls
-
drainQueue()— called after each scan completes:- Acquire
l.mu - If
scanQueueis not empty, pop first entry, set ascurrentScanLibraryID, release lock, callstartScanin new goroutine - If
scanQueueis empty, setcurrentScanLibraryID = 0,scanActive = false, emitLibraryScanQueueDrained, release lock
- Acquire
Refactor Library.Scan() → scanInternal():
- Rename current
Scan()toscanInternal(libraryID int64, libraryName string, libraryPath string)(unexported) - Remove the
l.conf.DirectoryPathdependency — use thelibraryPathparameter instead - Replace
l.db.Queries.GetAllAudioFiles(l.ctx)withl.db.Queries.GetAudioFilesByLibrary(l.ctx, libraryID)in Phase 1 (load existing) - Pass
libraryIDthrough tosaveAudioFilesoCreateAudioFileParams.LibraryIDis set correctly - Update all
ScanProgressemissions to includeLibraryID,LibraryName, andQueuedCount(read queue length under lock) - Update
ScanMetricsto includeLibraryIDandLibraryNamebefore emittingLibraryScanComplete/LibraryScanCancelled - The
workerCountshould useresolveScanWorkerCount(ScanConcurrencyAuto, libraryPath)— no longer from config (each library path may be on different storage)
Keep backward-compatible Scan() method — public method that scans using the legacy l.conf.DirectoryPath for handleConfigUpdate. Mark it deprecated. It should:
- Look up or create a library for
l.conf.DirectoryPathusingGetLibraryByPath - Call
ScanLibrary(lib.ID)
Update scan_control.go:
- Rename
CancelScan()to an internal helpercancelCurrentScan()(unexported) - Keep
PauseScan()andResumeScan()as-is — they operate on the current scan which is correct IsScanActive()unchanged- Add
QueuedLibraryNames() []string— returns names of queued libraries (for UI display)
Update config.go:
- The
Configstruct keepsDirectoryPathandScanConcurrencyfor backward compatibility, butDirectoryPathis now unused for normal scanning (libraries come from DB).ScanConcurrencyis still useful as a global default.
Update rescan.go:
FullRescan()needs updating — it should accept a library ID. For now, keep it working withl.conf.DirectoryPath(it's used from the config page). Phase 12 will add per-library rescan.
Thread libraryID through the scan pipeline:
- Add
libraryID int64field toscanWorkstruct (or pass it via closure) - In
saveAudioFile, useLibraryID: libraryIDinCreateAudioFileParams - In the
commitBatch→saveAudioFilecall chain, thread the library ID through. Simplest: addlibraryID int64as a parameter tocommitBatchandsaveAudioFileandupdateAudioFileMetadata.
Linting notes:
- All exported methods need doc comments ending with period (godot)
- No stuttering (revive) — method names don't repeat "Library"
- Sentinel errors as package vars (err113)
- Blank line after early returns (nlreturn)
- Keep lines under 100 chars (golines) cd /mnt/vault/dev/golang/yellowjacket && go build ./... && go vet ./backend/library/...
ScanLibrary(id)scans a specific library's directory, associating files with that library_idScanAllLibraries()queues all libraries for sequential scanning- Scan queue coordinator ensures only one scan runs at a time, with silent dedup
- Cancel:
CancelCurrentScan()cancels current and starts next;CancelAllScans()cancels current and clears queue - Pause freezes current scan AND queue (existing behavior — drainQueue is only called on scan completion, which doesn't happen while paused)
- All scan events include library name and queue count
go build ./...passes
Vet passes
go vet ./backend/library/...
Generated code is up to date
sqlc generate && go generate ./backend/events/...
Existing tests still pass (scan_test.go uses the old Scan() path)
go test ./backend/library/... -count=1 -timeout 60s
Events synced
diff <(grep -oP '"[A-Z][a-zA-Z]+"' backend/events/events.go | sort) <(grep -oP '"[A-Z][a-zA-Z]+"' frontend/src/events.ts | sort)
</verification>
<success_criteria>
- ScanLibrary(id) resolves library path from DB and scans only that directory
- CreateAudioFile includes library_id — new files are associated with their library
- Only one scan runs at a time — queue coordinates sequential execution
- Duplicate requests are silently ignored
- CancelCurrentScan stops current library, next queued starts automatically
- CancelAllScans stops current and clears queue
- Pause freezes scan AND queue
- All scan events include library name and queue count
- go build ./... passes, go test ./backend/library/... passes
</success_criteria>
<output>
After completion, create `.planning/phases/11-per-library-scan-pipeline/11-01-SUMMARY.md`
</output>