feat: data lifecycle rewrite, download clients, wanted list, and central catalog index
Build & publish Arch package / arch-package (push) Successful in 2m12s
Search index maintenance / maintain-index (push) Successful in 2h22m28s

Ships the fresh-start schema cleanup: rebuilt explore catalog index
pipeline (dump import, artifact fetch/build, incremental listen-count
refresh), a new download subsystem (Lidarr/Prowlarr/qBittorrent/SABnzbd/
slskd/yt-dlp providers, staging, reconciliation, wanted list), and the
supporting schema/query/store changes across backend and frontend.

Also includes two smaller follow-ups: bump the central index's
rebuild-after cadence from 90 to 180 days, and remove the Explore
"library only" online/offline toggle entirely (frontend-only, no
backend counterpart) rather than carry unused UI/state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2Agd9af5hE7qzti2ackiS
This commit is contained in:
2026-08-06 17:12:01 -04:00
co-authored by Claude Sonnet 5
parent d0d86f85d5
commit e190fd75b9
165 changed files with 31088 additions and 5192 deletions
+42 -7
View File
@@ -3,8 +3,6 @@ package library
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/runtime"
"yellowjacket/backend/events"
)
@@ -65,7 +63,7 @@ func (l *Library) ScanLibrary(id int64) error {
queueLength := len(l.scanQueue)
l.mu.Unlock()
runtime.EventsEmit(l.ctx, events.LibraryScanQueued, map[string]any{
l.emit(events.LibraryScanQueued, map[string]any{
"libraryId": lib.ID,
"libraryName": lib.Name,
"queueLength": queueLength,
@@ -167,9 +165,31 @@ func (l *Library) SoftScanAllLibraries() error {
continue
}
diskCount := countAudioFiles(lib.Path)
dbModTime, modErr := l.db.Queries.GetLibraryMaxModifiedAt(
l.ctx, lib.ID,
)
if modErr != nil {
l.logger.Warn(
"soft scan: could not read newest mtime, queueing full scan",
"libraryID", lib.ID,
"libraryName", lib.Name,
"err", modErr,
)
if diskCount == dbCount {
_ = l.ScanLibrary(lib.ID)
continue
}
diskCount, diskModTime := surveyAudioFiles(lib.Path)
// A newer file on disk than anything on record means something
// was edited in place since the last scan. An older newest-mtime
// is not evidence of a change: deleting the newest file lowers it
// while the count check already covers that case.
staleTags := diskModTime > dbModTime
if diskCount == dbCount && !staleTags {
l.logger.Info(
"soft scan: library unchanged, skipping",
"libraryID", lib.ID,
@@ -181,11 +201,14 @@ func (l *Library) SoftScanAllLibraries() error {
}
l.logger.Info(
"soft scan: file count mismatch, queueing scan",
"soft scan: library changed, queueing scan",
"libraryID", lib.ID,
"libraryName", lib.Name,
"diskFiles", diskCount,
"dbTracks", dbCount,
"diskModTime", diskModTime,
"dbModTime", dbModTime,
"reason", softScanReason(diskCount != dbCount, staleTags),
)
if err := l.ScanLibrary(lib.ID); err != nil {
@@ -201,6 +224,18 @@ func (l *Library) SoftScanAllLibraries() error {
return nil
}
// softScanReason labels why the soft scan queued a library, for the log.
func softScanReason(countChanged, staleTags bool) string {
switch {
case countChanged && staleTags:
return "file count mismatch and modified files"
case countChanged:
return "file count mismatch"
default:
return "modified files"
}
}
// CancelCurrentScan cancels only the currently scanning library.
// The next queued library (if any) starts automatically when the
// current scan's goroutine completes.
@@ -281,7 +316,7 @@ func (l *Library) drainQueue() {
hooks := l.scanHooks
l.mu.Unlock()
runtime.EventsEmit(l.ctx, events.LibraryScanQueueDrained)
l.emit(events.LibraryScanQueueDrained)
if hooks.OnAllScansComplete != nil {
hooks.OnAllScansComplete()