Plan 016 B4. The catalog artifact is about 0.6 GB and the app fetched it with no awareness of the connection: on a desktop that is a minute of bandwidth, on a phone it can be a month's allowance. It is now skipped on a cellular connection unless `AllowMeteredCatalogDownload` is on, with the toggle in Settings' Search Index section, where the text explaining what the catalog is already lives. The file layout is dictated by the cgo rule rather than by taste. `explore` is imported by `cmd/indexbuild`, which builds with CGO_ENABLED=0 and must not link Wails, so `netpolicy.go` holds the policy and the JSON parsing -- tested on every platform -- and the single platform call is a closure injected from `app.go`, which already names `application` legitimately. Three rules in it are load-bearing. An unknown answer is not a metered one: only mobile answers at all, and treating silence as metered would have disabled the download for every desktop user in the world. Cellular is the only signal available, because the runtime reports `wifi|cellular|ethernet|none` and no metered flag -- so a metered Wi-Fi cannot be detected and is not refused, which is documented rather than implied. And the gate runs before the first status write, so declining is a no-op instead of a job in the indicator and an error tier to dismiss. Two corrections to the plan while implementing it: the portable API is `application.Mobile.NetworkJSON()`, not `application.Android`'s, which exists only under the `android` build tag; and the permission is read at the moment a download would start, so enabling it takes effect on the next attempt rather than the next launch.
102 lines
2.9 KiB
Go
102 lines
2.9 KiB
Go
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 {
|
|
// Before anything is staged: ~0.6 GB is not a download to start on
|
|
// someone's cellular allowance without being asked (plan 016 B4).
|
|
// This is checked first so no job appears and no status changes --
|
|
// declining is a no-op, not a failure the user has to dismiss.
|
|
if si.netPolicy.refuses() {
|
|
si.logIndexJob(
|
|
jobs.LevelInfo,
|
|
"Skipping the catalog download on a metered connection. "+
|
|
"Enable it in Settings to download anyway.",
|
|
)
|
|
|
|
return ErrMeteredNetwork
|
|
}
|
|
|
|
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)
|
|
}
|