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
+30 -16
View File
@@ -9,8 +9,6 @@ import (
"strings"
"time"
"github.com/wailsapp/wails/v2/pkg/runtime"
"yellowjacket/backend/database"
"yellowjacket/backend/database/sql/sqlcgen"
"yellowjacket/backend/events"
@@ -104,7 +102,7 @@ func (l *Library) AddLibrary(path string) (*sqlcgen.Library, error) {
"libraryID", lib.ID, "claimed", claimed)
}
runtime.EventsEmit(l.ctx, events.LibraryAdded, lib)
l.emit(events.LibraryAdded, lib)
go func() {
if scanErr := l.ScanLibrary(lib.ID); scanErr != nil {
@@ -149,7 +147,7 @@ func (l *Library) RenameLibrary(id int64, newName string) error {
return fmt.Errorf("could not rename library: %w", err)
}
runtime.EventsEmit(l.ctx, events.LibraryRenamed, map[string]any{
l.emit(events.LibraryRenamed, map[string]any{
"id": id,
"name": newName,
})
@@ -427,7 +425,18 @@ func (l *Library) RemoveLibrary(id int64) (*RemovalSummary, error) {
return nil, fmt.Errorf("could not delete orphaned cover_art: %w", err)
}
// 17. Delete library row.
// 17. Delete the library's tagging queue. tagging_items holds a
// FOREIGN KEY to libraries with no ON DELETE clause, so leaving these
// rows behind makes the DELETE below fail the whole transaction and
// the library becomes unremovable. tagging_candidates is tied to
// tagging_items by ON DELETE CASCADE and goes with it.
// SAFETY: Hand-crafted DELETE — sqlc has no query for this. Parameterized.
if _, err := tx.ExecContext(l.ctx,
`DELETE FROM tagging_items WHERE library_id = ?`, id); err != nil {
return nil, fmt.Errorf("could not delete tagging items: %w", err)
}
// 18. Delete library row.
// SAFETY: Hand-crafted DELETE matching sqlc DeleteLibrary but within
// the same transaction. Parameterized.
if _, err := tx.ExecContext(l.ctx,
@@ -435,36 +444,41 @@ func (l *Library) RemoveLibrary(id int64) (*RemovalSummary, error) {
return nil, fmt.Errorf("could not delete library: %w", err)
}
// 18. Commit transaction.
// 19. Commit transaction.
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("could not commit removal transaction: %w", err)
}
committed = true
// 19. FTS5 rebuild skipped — contentless FTS5 (content='') cannot
// 20. FTS5 rebuild skipped — contentless FTS5 (content='') cannot
// delete individual rows, but stale entries are harmless: search
// queries JOIN against track_metadata which filters out deleted
// rows. The index is rebuilt on the next full rescan. Skipping
// avoids a costly full re-index of all remaining tracks (~10s for
// 25K tracks).
// 20. Post-commit: Delete orphaned cover art files.
// 21. Post-commit: Delete orphaned cover art files and their sized
// variants. Only the original is stored in cover_art.file_path; the
// _sm/_md/_lg thumbnails are derived filenames beside it, so they
// have to be removed by name or they accumulate forever.
for _, coverPath := range orphanedCoverArtPaths {
if err := os.Remove(coverPath); err != nil && !os.IsNotExist(err) {
l.logger.Warn("could not remove orphaned cover art file",
"path", coverPath,
"err", err,
)
for _, path := range CoverArtFileSet(coverPath) {
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
l.logger.Warn("could not remove orphaned cover art file",
"path", path,
"err", err,
)
}
}
}
// 21. Post-commit: Compact queue.
// 22. Post-commit: Compact queue.
if l.removalHooks.CompactQueue != nil {
l.removalHooks.CompactQueue()
}
// 22. Post-commit: invalidate library-sync markers so the gated
// 23. Post-commit: invalidate library-sync markers so the gated
// index/lyric re-sync runs on the next launch.
if l.removalHooks.PostRemove != nil {
l.removalHooks.PostRemove()
@@ -480,7 +494,7 @@ func (l *Library) RemoveLibrary(id int64) (*RemovalSummary, error) {
}
// 22. Emit events.
runtime.EventsEmit(l.ctx, events.LibraryRemoved, map[string]any{
l.emit(events.LibraryRemoved, map[string]any{
"id": id,
"summary": summary,
})