Files
yellowjacket/backend/library/scan_control.go
T
yonluandClaude Opus 5 01bc5f2094 feat(jobs): surface background jobs with progress, logs and controls
Add a central job registry that library scans and search index builds
report into, so background work is visible instead of buried in the
settings page.

- backend/jobs: registry with per-job ring-buffer logs, capability-driven
  controls, and one coalesced JobsChanged snapshot at 4Hz
- pause survives restart via a job_state table; a paused scan is adopted
  back on launch and skipped by the soft scan
- top-bar indicator, popover, details drawer and a Jobs page replacing
  the config page's scan UI; per-library start/stop retained
- scan timing breakdown moves into the job log, Full rescan to the Jobs
  page; delete the orphaned library-manager component

Also add cmd/indexbuild and cmd/indexexport so the explore index can be
built once centrally rather than by every install, which today streams
~205GB from the ListenBrainz spark dump on first run. indexbuild picks
build/refresh/rebuild from index state; the Gitea workflow runs it on
push, weekly, or manually and publishes only when content changed.

fresh-install no longer defaults YJ_HOME under /tmp: it is tmpfs on most
distros, and the import needs ~6GB of real disk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 14:42:22 -04:00

128 lines
2.7 KiB
Go

package library
import (
"context"
"github.com/wailsapp/wails/v2/pkg/runtime"
"yellowjacket/backend/events"
"yellowjacket/backend/jobs"
)
// CancelScan cancels an in-progress scan. Returns immediately;
// scan goroutines stop at their next checkpoint.
//
// Deprecated: Use CancelCurrentScan or CancelAllScans for
// queue-aware cancellation.
func (l *Library) CancelScan() {
l.mu.Lock()
cancel := l.scanCancel
l.mu.Unlock()
if cancel != nil {
cancel()
}
}
// PauseScan pauses an in-progress scan. Workers block at their
// next pause checkpoint until ResumeScan is called.
func (l *Library) PauseScan() {
l.mu.Lock()
if !l.scanActive || l.scanPaused {
l.mu.Unlock()
return
}
l.scanPaused = true
l.scanPauseCh = make(chan struct{})
pausedID := l.currentScanLibraryID
reg := l.jobs
l.mu.Unlock()
runtime.EventsEmit(l.ctx, events.LibraryScanPaused)
// Confirm the pause on the job — the registry moved it to "pausing"
// when the request came in. Writing the durable pause record is a
// side effect of reaching StatePaused, so a scan paused now comes
// back paused after a restart. Done after releasing l.mu: this
// writes to the database, and workers take l.mu on every pause
// checkpoint.
if reg == nil {
return
}
if h := reg.Get(scanJobID(pausedID)); h != nil {
h.SetState(jobs.StatePaused)
h.SetPhase("Paused")
h.Logf(jobs.LevelInfo, "Scan paused")
}
}
// ResumeScan unblocks a paused scan.
func (l *Library) ResumeScan() {
l.mu.Lock()
if !l.scanPaused {
l.mu.Unlock()
return
}
l.scanPaused = false
close(l.scanPauseCh) // unblocks all waiting workers
resumedID := l.currentScanLibraryID
reg := l.jobs
l.mu.Unlock()
runtime.EventsEmit(l.ctx, events.LibraryScanResumed)
// Clears the durable pause record as a side effect of leaving
// StatePaused.
if reg == nil {
return
}
if h := reg.Get(scanJobID(resumedID)); h != nil {
h.SetState(jobs.StateRunning)
h.Logf(jobs.LevelInfo, "Scan resumed")
}
}
// IsScanActive returns whether a scan is currently running.
func (l *Library) IsScanActive() bool {
l.mu.Lock()
defer l.mu.Unlock()
return l.scanActive
}
// IsScanPaused returns whether the scan is currently paused.
func (l *Library) IsScanPaused() bool {
l.mu.Lock()
defer l.mu.Unlock()
return l.scanPaused
}
// waitIfPaused blocks the calling goroutine if the scan is paused.
// Returns ctx.Err() if the context is cancelled while waiting.
func (l *Library) waitIfPaused(ctx context.Context) error {
l.mu.Lock()
ch := l.scanPauseCh
paused := l.scanPaused
l.mu.Unlock()
if !paused || ch == nil {
return nil
}
select {
case <-ch: // closed = unpaused
return nil
case <-ctx.Done():
return ctx.Err()
}
}