docs(11-02): complete frontend scan UI plan
- SUMMARY.md documents per-library progress display and queue-aware cancel dialog - STATE.md updated: plan 2/3, session continuity, new decisions - ROADMAP.md updated: phase 11 progress 2/3 - REQUIREMENTS.md: LSCAN-03 marked complete
This commit is contained in:
@@ -26,8 +26,6 @@ import (
|
||||
"yellowjacket/backend/system"
|
||||
)
|
||||
|
||||
var errLibraryDirNotConfigured = errors.New("library directory not configured")
|
||||
|
||||
// scanBatchSize controls how many files are committed in a single
|
||||
// database transaction during a scan. Larger batches amortize
|
||||
// SQLite's fsync cost but increase the blast radius of a failed commit.
|
||||
@@ -107,8 +105,8 @@ func (l *Library) SetRescanHooks(h RescanHooks) {
|
||||
}
|
||||
|
||||
// NewLibrary creates a new library with the given configuration.
|
||||
// A nil config is permitted; the library will be inert until a valid
|
||||
// configuration is supplied via the LibraryConfigChanged event.
|
||||
// A nil config is permitted; scan paths come from the database
|
||||
// rather than from the config's DirectoryPath.
|
||||
func NewLibrary(
|
||||
ctx context.Context,
|
||||
logger *slog.Logger,
|
||||
@@ -142,63 +140,16 @@ func (l *Library) SetContext(ctx context.Context) {
|
||||
l.registerEventHandlers()
|
||||
}
|
||||
|
||||
// registerEventHandlers sets up Wails runtime event listeners.
|
||||
// The legacy LibraryConfigChanged handler was removed — in the
|
||||
// multi-library model, libraries are managed through the CRUD
|
||||
// API (Phase 12) and scanned via ScanLibrary/ScanAllLibraries.
|
||||
func (l *Library) registerEventHandlers() {
|
||||
if l.ctx == nil {
|
||||
l.logger.Error("Context is nil, cannot register event handlers")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
runtime.EventsOn(l.ctx, events.LibraryConfigChanged, func(data ...any) {
|
||||
l.logger.Info("Received LibraryConfigChanged event")
|
||||
|
||||
if len(data) == 0 {
|
||||
l.logger.Error("LibraryConfigChanged event received with no data")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
configMap, ok := data[0].(map[string]any)
|
||||
if !ok {
|
||||
l.logger.Error("LibraryConfigChanged event data is not a map", "data", data[0])
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
dir, ok := configMap["DirectoryPath"].(string)
|
||||
if !ok {
|
||||
l.logger.Error("DirectoryPath not found or not a string in config event")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
updatedConfig := Config{DirectoryPath: Directory(dir)}
|
||||
if err := l.handleConfigUpdate(updatedConfig); err != nil {
|
||||
l.logger.Error("Failed to handle config update", "err", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Scan syncs the library using the legacy DirectoryPath config.
|
||||
// Retained for backward compatibility with handleConfigUpdate.
|
||||
//
|
||||
// Deprecated: Use ScanLibrary(id) for per-library scanning.
|
||||
func (l *Library) Scan() (*ScanMetrics, error) {
|
||||
if len(l.conf.DirectoryPath) == 0 {
|
||||
return newScanMetrics(), errLibraryDirNotConfigured
|
||||
}
|
||||
|
||||
lib, err := l.db.Queries.GetLibraryByPath(
|
||||
l.ctx, string(l.conf.DirectoryPath),
|
||||
)
|
||||
if err != nil {
|
||||
return newScanMetrics(), fmt.Errorf(
|
||||
"could not resolve library for path %s: %w",
|
||||
l.conf.DirectoryPath, err,
|
||||
l.logger.Error(
|
||||
"Context is nil, cannot register event handlers",
|
||||
)
|
||||
}
|
||||
|
||||
return l.scanInternal(lib.ID, lib.Name, lib.Path), nil
|
||||
}
|
||||
|
||||
// scanInternal performs the full scan pipeline for a single library.
|
||||
@@ -1580,32 +1531,3 @@ func toNullString(v string) sql.NullString {
|
||||
|
||||
return sql.NullString{String: v, Valid: true}
|
||||
}
|
||||
|
||||
func (l *Library) handleConfigUpdate(updatedConfigValues Config) error {
|
||||
l.logger.Info("handling config update", "updated", updatedConfigValues)
|
||||
|
||||
var updateErr error
|
||||
|
||||
if l.conf.DirectoryPath != updatedConfigValues.DirectoryPath {
|
||||
l.logger.Info("new library, scanning")
|
||||
|
||||
l.conf.DirectoryPath = updatedConfigValues.DirectoryPath
|
||||
|
||||
if scanMetrics, err := l.Scan(); err != nil {
|
||||
updateErr = errors.Join(
|
||||
updateErr,
|
||||
fmt.Errorf(
|
||||
"problem scanning library on config update: %w",
|
||||
err,
|
||||
),
|
||||
)
|
||||
} else if len(scanMetrics.Warnings) > 0 {
|
||||
l.logger.Warn(
|
||||
"library scan completed with warnings",
|
||||
"warningCount", len(scanMetrics.Warnings),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return updateErr
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package library
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -9,13 +10,33 @@ import (
|
||||
"yellowjacket/backend/coverart"
|
||||
)
|
||||
|
||||
var errNoLibrariesConfigured = errors.New(
|
||||
"no libraries configured for rescan",
|
||||
)
|
||||
|
||||
// FullRescan clears the queue and player, wipes all library data
|
||||
// (database records and cover art files), and performs a fresh
|
||||
// scan from scratch. The returned ScanMetrics includes timing
|
||||
// scan of the first library from the database. Per-library full
|
||||
// rescan will be added in Phase 12; for now this rescans the
|
||||
// first/only library. The returned ScanMetrics includes timing
|
||||
// for the clear phases in addition to the normal scan metrics.
|
||||
func (l *Library) FullRescan() (*ScanMetrics, error) {
|
||||
l.logger.Info("beginning full library rescan")
|
||||
|
||||
// Resolve the first library from the database.
|
||||
libs, err := l.db.Queries.GetAllLibraries(l.ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"could not get libraries for rescan: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
if len(libs) == 0 {
|
||||
return nil, errNoLibrariesConfigured
|
||||
}
|
||||
|
||||
lib := libs[0]
|
||||
|
||||
// Run the pre-clear hook (e.g. clear queue / stop playback)
|
||||
// before wiping data so the player is not referencing
|
||||
// now-deleted tracks.
|
||||
@@ -50,9 +71,10 @@ func (l *Library) FullRescan() (*ScanMetrics, error) {
|
||||
|
||||
l.logger.Info("library data cleared successfully")
|
||||
|
||||
// Run the full scan and merge clear-phase times into
|
||||
// the metrics it returns.
|
||||
metrics, err := l.Scan()
|
||||
// Scan the first library directly via scanInternal,
|
||||
// bypassing the queue coordinator.
|
||||
metrics := l.scanInternal(lib.ID, lib.Name, lib.Path)
|
||||
|
||||
if metrics != nil {
|
||||
metrics.ClearQueue = clearQueueDur
|
||||
metrics.ClearDatabase = clearDBDur
|
||||
@@ -70,7 +92,7 @@ func (l *Library) FullRescan() (*ScanMetrics, error) {
|
||||
l.rescanHooks.PostScan()
|
||||
}
|
||||
|
||||
return metrics, err
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// clearLibraryTables deletes all library-related rows in FK-safe
|
||||
|
||||
Reference in New Issue
Block a user