feat: data lifecycle rewrite, download clients, wanted list, and central catalog index
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:
@@ -0,0 +1,87 @@
|
||||
package explore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"yellowjacket/backend/jobs"
|
||||
)
|
||||
|
||||
// Fetching and merging the prebuilt catalog artifact.
|
||||
//
|
||||
// This is the whole catalog build on a user's machine. The import that
|
||||
// derives the catalog from the MetaBrainz dumps lives behind the
|
||||
// `indexbuild` tag and runs only in CI (see dumpbuild_stub.go).
|
||||
|
||||
// Artifact build stages, shown in the jobs panel.
|
||||
const (
|
||||
artifactStageDownload = iota
|
||||
artifactStageMerge
|
||||
)
|
||||
|
||||
var artifactStageNames = [...]string{
|
||||
"Downloading catalog",
|
||||
"Merging catalog",
|
||||
}
|
||||
|
||||
// tryCoreArtifact fetches and merges the prebuilt catalog. Every
|
||||
// failure path is non-fatal by design: the caller falls back, and a
|
||||
// fresh install with no network still gets its own library in Explore.
|
||||
func (si *SearchIndex) tryCoreArtifact(ctx context.Context) error {
|
||||
si.mu.Lock()
|
||||
si.buildStatus = IndexStatus{
|
||||
Building: true,
|
||||
Tiers: []TierStatus{
|
||||
{Name: artifactStageNames[artifactStageDownload], State: "pending"},
|
||||
{Name: artifactStageNames[artifactStageMerge], State: "pending"},
|
||||
},
|
||||
}
|
||||
si.mu.Unlock()
|
||||
si.refreshStatusCounts()
|
||||
|
||||
fetcher, err := newArtifactFetcher(si)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
si.setTierStatus(artifactStageNames[artifactStageDownload], "running", 0, 0)
|
||||
si.logIndexJob(jobs.LevelInfo, "Fetching prebuilt catalog")
|
||||
|
||||
path, err := fetcher.fetch(ctx)
|
||||
if err != nil {
|
||||
si.setTierStatus(artifactStageNames[artifactStageDownload], "error", 0, 0)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
si.setTierStatus(artifactStageNames[artifactStageDownload], "complete", 0, 0)
|
||||
si.setTierStatus(artifactStageNames[artifactStageMerge], "running", 0, 0)
|
||||
|
||||
if err := si.importCoreArtifact(ctx, path); err != nil {
|
||||
si.setTierStatus(artifactStageNames[artifactStageMerge], "error", 0, 0)
|
||||
si.removeArtifactFile(path)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
si.removeArtifactFile(path)
|
||||
si.setTierStatus(artifactStageNames[artifactStageMerge], "complete", 0, 0)
|
||||
|
||||
si.mu.Lock()
|
||||
si.buildStatus.Building = false
|
||||
si.mu.Unlock()
|
||||
|
||||
// Fold the user's own library into the freshly-merged catalog:
|
||||
// owned entities the artifact does not cover are inserted, and
|
||||
// covered ones are flagged in_library.
|
||||
si.PopulateLocalCrossReferences()
|
||||
si.refreshStatusCounts()
|
||||
si.scheduleChampionRebuild()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// artifactAlreadyMerged reports whether this index already carries a
|
||||
// merged artifact, so a restart does not re-download one.
|
||||
func (si *SearchIndex) artifactAlreadyMerged() bool {
|
||||
return si.hasMeta(coreArtifactVersionKey)
|
||||
}
|
||||
Reference in New Issue
Block a user