diff --git a/backend/library/rescan.go b/backend/library/rescan.go index 061cdcc..a31ff92 100644 --- a/backend/library/rescan.go +++ b/backend/library/rescan.go @@ -16,14 +16,13 @@ var errNoLibrariesConfigured = errors.New( // FullRescan clears the queue and player, wipes all library data // (database records and cover art files), and performs a fresh -// 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. +// scan of every library in the database. The returned ScanMetrics +// reflects the last library scanned; clear-phase durations are +// folded into its totals. func (l *Library) FullRescan() (*ScanMetrics, error) { l.logger.Info("beginning full library rescan") - // Resolve the first library from the database. + // Resolve all libraries from the database. libs, err := l.db.Queries.GetAllLibraries(l.ctx) if err != nil { return nil, fmt.Errorf( @@ -35,8 +34,6 @@ func (l *Library) FullRescan() (*ScanMetrics, error) { 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. @@ -71,9 +68,17 @@ func (l *Library) FullRescan() (*ScanMetrics, error) { l.logger.Info("library data cleared successfully") - // Scan the first library directly via scanInternal, - // bypassing the queue coordinator. - metrics := l.scanInternal(lib.ID, lib.Name, lib.Path) + // Scan the first library directly (bypassing the queue) so + // we get ScanMetrics back. Queue remaining libraries so + // they run sequentially via the scan coordinator. + metrics := l.scanInternal(libs[0].ID, libs[0].Name, libs[0].Path) + + for _, lib := range libs[1:] { + if scanErr := l.ScanLibrary(lib.ID); scanErr != nil { + l.logger.Error("could not queue library for rescan", + "libraryID", lib.ID, "err", scanErr) + } + } if metrics != nil { metrics.ClearQueue = clearQueueDur diff --git a/frontend/src/components/config-page/config-page.ts b/frontend/src/components/config-page/config-page.ts index 5aa7652..4ad671d 100644 --- a/frontend/src/components/config-page/config-page.ts +++ b/frontend/src/components/config-page/config-page.ts @@ -350,6 +350,7 @@ export class ConfigPage extends LitElement { @state() private toastMessage = ''; @state() private toastVisible = false; @state() private activeMenuId: number | null = null; + @state() private selectedLibraryIds: Set = new Set(); @state() private scanning = false; @state() private statusMessage = ''; @state() private scanProgress: ScanProgress | null = null; @@ -834,6 +835,25 @@ export class ConfigPage extends LitElement { gap: 0; } + .library-header { + display: flex; + align-items: center; + gap: 0.75em; + padding: 0.4em 0.5em; + border-bottom: 1px solid var(--yj-border-subtle, #333); + font-size: var(--yj-text-sm, 0.8em); + color: var(--yj-text-muted, #999); + } + + .library-header-label { + user-select: none; + } + + .library-checkbox { + cursor: pointer; + flex-shrink: 0; + } + .library-row { display: flex; align-items: center; @@ -1152,6 +1172,18 @@ export class ConfigPage extends LitElement { this.libraries = libs ?? []; this.concurrencyMode = mode; + + // Auto-select all libraries on initial load so scan + // buttons default to operating on everything. + if (this.selectedLibraryIds.size === 0 && this.libraries.length > 0) { + this.selectedLibraryIds = new Set(this.libraries.map((l) => l.id)); + } else { + // Prune selections for libraries that no longer exist. + const validIds = new Set(this.libraries.map((l) => l.id)); + const pruned = new Set([...this.selectedLibraryIds].filter((id) => validIds.has(id))); + + this.selectedLibraryIds = pruned; + } } catch (err) { console.error( 'Failed to load libraries:', @@ -1381,6 +1413,26 @@ export class ConfigPage extends LitElement { this.activeMenuId = this.activeMenuId === id ? null : id; }; + private toggleLibrarySelection = (id: number): void => { + const next = new Set(this.selectedLibraryIds); + + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + + this.selectedLibraryIds = next; + }; + + private toggleSelectAllLibraries = (): void => { + if (this.selectedLibraryIds.size === this.libraries.length) { + this.selectedLibraryIds = new Set(); + } else { + this.selectedLibraryIds = new Set(this.libraries.map((l) => l.id)); + } + }; + private handleDocumentClick = (): void => { if (this.activeMenuId !== null) { this.activeMenuId = null; @@ -1419,19 +1471,19 @@ export class ConfigPage extends LitElement { }); }; - private handleScanAll = async (): Promise => { - try { - await ScanAllLibraries(); - } catch (err) { - this.statusMessage = - 'Scan all libraries completed with errors.'; - this.scanErrors = String(err); - } - }; - private handleSoftScan = async (): Promise => { try { - await ScanAllLibraries(); + if (this.selectedLibraryIds.size === 0) return; + + // If all libraries selected, use the batch method. + if (this.selectedLibraryIds.size === this.libraries.length) { + await ScanAllLibraries(); + } else { + // Queue each selected library individually. + for (const id of this.selectedLibraryIds) { + await ScanLibrary(id); + } + } } catch (err) { this.statusMessage = 'Scan completed with errors.'; @@ -2198,77 +2250,107 @@ export class ConfigPage extends LitElement { (l) => l.id === this.removingLibraryId, ); + const allSelected = this.libraries.length > 0 + && this.selectedLibraryIds.size === this.libraries.length; + const someSelected = this.selectedLibraryIds.size > 0 + && !allSelected; + const selectionCount = this.selectedLibraryIds.size; + return html` -
- ${this.libraries.map( - (lib) => html` -
- ${this.editingLibraryId === lib.id - ? html` - e.stopPropagation()} - /> - ` - : html` - this.handleStartRename(lib.id, lib.name)} - > - ${lib.name} - - `} - ${lib.path} - - ${lib.trackCount} tracks + ${this.libraries.length > 0 + ? html` +
+
+ e.stopPropagation()} + /> + + ${allSelected ? 'All' : someSelected ? `${selectionCount}` : 'None'} selected -
- - ${this.activeMenuId === lib.id - ? html` -
e.stopPropagation()} - > -
+ ${this.libraries.map( + (lib) => html` +
+ this.toggleLibrarySelection(lib.id)} + @click=${(e: Event) => e.stopPropagation()} + /> + ${this.editingLibraryId === lib.id + ? html` + e.stopPropagation()} + /> + ` + : html` + this.handleStartRename(lib.id, lib.name)} > - Rename -
-
this.handleRescanLibrary(lib.id)} - > - Rescan -
-
void this.handleRemoveClick(lib.id)} - > - Remove -
-
- ` - : nothing} -
-
- `, - )} -
+ ${lib.name} + + `} + ${lib.path} + + ${lib.trackCount} tracks + +
+ + ${this.activeMenuId === lib.id + ? html` +
e.stopPropagation()} + > +
this.handleStartRename(lib.id, lib.name)} + > + Rename +
+
this.handleRescanLibrary(lib.id)} + > + Rescan +
+
void this.handleRemoveClick(lib.id)} + > + Remove +
+
+ ` + : nothing} +
+
+ `, + )} +
+ ` + : nothing} - `}