Files
yellowjacket/frontend/bindings/yellowjacket/backend/explore/service.ts
T
yonluandClaude Opus 5 b3737d30af feat(explore): carry multi-artist credits in the catalog
A track credited to more than one artist has exactly one navigable
artist in this app and the rest are punctuation. `primaryArtist()`
string-parses the credit, strips a " feat. " clause and discards the
guest; it deliberately does not split on "&", "with" or "," because
those live inside real artist names.

Measured on a real 26,069-file library plus an 80+80 MusicBrainz
sample: 13% of recordings are multi-artist upstream, while only 0.86%
of files carry any structured multi-artist tag — mp3 carries zero
files with multiple MUSICBRAINZ_ARTISTID across 19,840. Of 1,286 files
saying "feat.", 90% have nothing structured behind it, and a sample of
80 such files was multi-artist in MB 80 times out of 80.

CLAUDE.md justified plan 013's removal of the credit tables with "3
credits of 2,823 listed more than one artist". That measured our own
*writer* — cachedLinkArtist was called once per credit, so a
collaboration could never have been recorded. Dropping the join table
was still right on cost; the evidence for "multi-artist is rare" was
not.

A credit is ordered parts and the credit string is derived from them,
so join phrases are assembly instructions, not disassembly ones.
Nothing here reconstructs a credit by searching a name inside a credit
string: the stored text may come from tags while the parts come from
the catalog, and those disagree for ~1 in 3 multi-artist credits.

Where it comes from, after two dead ends: the canonical dump CI
already streams has no join phrases and no as-credited names, and the
JSON dumps cover 153,691 recordings of ~35M with *zero* overlap
against a real library. So mbdump.tar.bz2 — 7.1 GB, ~13.7 min in
pure-Go bzip2, whose members are alphabetical, which is what lets one
pass resolve an entity's credit without buffering 35M recordings.

- artist_credit_part / artist_credit_ref, multi-artist credits only:
  a single-artist credit is already explore_index's own artist_name.
- Column layouts verified against the real 20260815 export;
  ErrDumpShape makes a wrong guess a failed build, not a wrong catalog.
- The pass runs on every mode, not just a build. The job picks its mode
  from the index's own state, and a complete import means "refresh",
  which never enters the importer — so credits could otherwise only
  arrive via a rebuild that re-downloads ~205 GB. It reports whether it
  populated anything, which is what flips `changed` and republishes.
- The importer asks whether an artifact carries the tables, on the
  writer where `core` is attached, so the artifact already published
  still imports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfVYUVExXsx1nSWrXN8mAh
2026-08-17 08:25:36 -04:00

590 lines
23 KiB
TypeScript

// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* Service is the Wails-bound service for the explore feature.
* It owns the lifecycle of all explore-related components: the
* MusicBrainz client, ListenBrainz client, rate limiter, and
* response cache. Its exported methods form the binding surface
* that the frontend calls via generated TypeScript stubs.
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as time$0 from "../../../time/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as $models from "./models.js";
/**
* AdoptPausedIndexBuild re-registers a build paused in a previous
* session so it appears in the jobs panel, still paused.
*/
export function AdoptPausedIndexBuild(): $CancellablePromise<void> {
return $Call.ByID(3754928399);
}
/**
* BackfillLibraryDiscographies enriches owned artists that have not had
* their discography fetched yet, in the background. Idempotent and
* bounded — the query only returns unenriched artists and each is marked
* discog_fetched on success, so this is cheap (an empty query) once every
* owned artist is covered and safe to call on every scan and launch.
*/
export function BackfillLibraryDiscographies(): $CancellablePromise<void> {
return $Call.ByID(3678857155);
}
/**
* BackfillLibraryLyrics fetches lyrics from LRCLIB for library tracks
* that don't have them, in the background. Idempotent and bounded —
* each recording is tried once (a miss is cached), and a run stops
* after a fixed number of passes, resuming on the next launch.
*/
export function BackfillLibraryLyrics(): $CancellablePromise<void> {
return $Call.ByID(4204427482);
}
/**
* BackfillReleaseGroupMBIDs resolves release groups whose scan only
* found a release-level MBID (MUSICBRAINZ_ALBUMID — many taggers write
* this instead of, or in addition to, MUSICBRAINZ_RELEASEGROUPID) into
* the release-group MBID everything else on the album page is keyed
* by. Bounded and resumable, in the background: a scan can't afford a
* live MusicBrainz call, so `library.updateMBIDs` stashes the release
* MBID in `pending_release_mbid` instead, and this is what resolves it
* — the same "defer the network call out of the scan path" shape as
* BackfillLibraryDiscographies.
*/
export function BackfillReleaseGroupMBIDs(): $CancellablePromise<void> {
return $Call.ByID(2594807082);
}
/**
* BrowseReleaseGroups fetches release groups for a given artist MBID.
* Checks the local index first for instant results, then fetches from
* MusicBrainz for complete data (secondary types, precise dates).
* Also adds results to the search index (Tier 5: organic growth).
*/
export function BrowseReleaseGroups(artistMBID: string): $CancellablePromise<$models.MBReleaseGroup[] | null> {
return $Call.ByID(404562912, artistMBID);
}
/**
* BrowseReleases fetches releases for a given release group MBID.
*
* Local-first, non-blocking: a warm response cache is served instantly;
* on a miss the request does NOT block on a live MusicBrainz browse
* (which pulls every version's full tracklist and can take seconds).
* Instead it kicks off a background fetch and returns empty — the
* AlbumReleasesReady event signals the caller to re-fetch once the cache
* is warm.
*/
export function BrowseReleases(releaseGroupMBID: string): $CancellablePromise<$models.MBRelease[] | null> {
return $Call.ByID(2551207897, releaseGroupMBID);
}
/**
* CheckLibraryMBIDs returns which of the given MBIDs exist in the
* local music library. Returns a map of MBID → entity type
* ("artist", "release_group", "recording").
*
* It has no frontend caller — `downloadcatalog.go` is the one consumer,
* asking about a single MBID at a time.
*/
export function CheckLibraryMBIDs(mbids: string[] | null): $CancellablePromise<{ [_ in string]?: string } | null> {
return $Call.ByID(3168338597, mbids);
}
/**
* CoreCatalogImported reports whether a prebuilt catalog artifact has
* been merged into this index.
*/
export function CoreCatalogImported(): $CancellablePromise<boolean> {
return $Call.ByID(186364153);
}
/**
* CoverArtGroupURL returns the Cover Art Archive URL for a release
* group's front cover at the default 250px size. This is the
* correct endpoint for search results, which return release group
* MBIDs rather than individual release MBIDs.
*/
export function CoverArtGroupURL(releaseGroupMBID: string): $CancellablePromise<string> {
return $Call.ByID(2102228641, releaseGroupMBID);
}
/**
* CoverArtURL returns the Cover Art Archive URL for a release's
* front cover at the default 250px size.
*/
export function CoverArtURL(releaseMBID: string): $CancellablePromise<string> {
return $Call.ByID(3231496554, releaseMBID);
}
/**
* GenerateMix returns the next batch of tracks for a dynamic-mix queue
* fallback, built by expanding the seed's artists to their similar
* artists (weighted by how often each appears in the seed, sharpened
* by shared genre tags) and restricting candidates to what is actually
* in the library — a queue can only play files that exist.
*
* continuing extends the current mix session — regenerating from its
* original seed rather than seedPaths — instead of starting a fresh
* one. Pass false whenever the queue that just exhausted was not
* itself a mix batch (a real selection just ran out); pass true when
* it was (the mix keeps going indefinitely). label names the batch
* after its most-represented seed artist, for the "Playing from" UI.
*/
export function GenerateMix(seedPaths: string[] | null, continuing: boolean): $CancellablePromise<[string[] | null, string]> {
return $Call.ByID(4293623744, seedPaths, continuing);
}
/**
* GetArtistImageCached returns a base64 data URL for the artist's
* photo ONLY if it's already on disk — no MB/Wikidata resolution
* or Wikimedia fetch. Returns "" if not cached.
*/
export function GetArtistImageCached(artistMBID: string): $CancellablePromise<string> {
return $Call.ByID(670428351, artistMBID);
}
/**
* GetArtistImageCachedPath returns the asset-handler URL path for
* the artist's cached medium thumbnail, e.g.
* "/artist-images/b1/b10bbbfc-.../primary_md.jpg". No base64, no
* network calls — just a disk existence check. Returns "" if no
* image is cached.
*/
export function GetArtistImageCachedPath(artistMBID: string): $CancellablePromise<string> {
return $Call.ByID(589683480, artistMBID);
}
/**
* GetArtistImageURL returns a base64 data URL for the artist's
* photo. Cached on disk — first call resolves via MB/Wikidata and
* fetches from Wikimedia Commons, subsequent calls are instant.
* Returns "" if no image is available.
*/
export function GetArtistImageURL(artistMBID: string): $CancellablePromise<string> {
return $Call.ByID(1780431074, artistMBID);
}
/**
* GetArtistImagesCachedPaths resolves artist portraits for many MBIDs
* in one call, returning MBID → asset-handler path for the medium
* thumbnail. Disk existence checks only: no MusicBrainz, no Wikidata,
* no Wikimedia, no network of any kind. MBIDs with no cached portrait
* are omitted rather than returned empty.
*
* It exists because the resolving entry point (GetArtistImageURL) was
* being used where a cache check belonged: a page rendering a dozen
* search results paid a full MB → Wikidata → Wikipedia → Wikimedia
* resolution for every artist whose portrait was already on disk. Its
* predecessor could not serve that — it keyed on artist *name* through
* the library's own MBID map, so it only ever answered for artists the
* user already owned, which on a catalog search is nearly none of them.
*
* Paths rather than base64: a portrait is ~128 kB as a data URL, and
* the asset handler serves the same bytes without crossing the IPC
* boundary or being retained by a JS string.
*/
export function GetArtistImagesCachedPaths(mbids: string[] | null): $CancellablePromise<{ [_ in string]?: string } | null> {
return $Call.ByID(2104788604, mbids);
}
/**
* GetArtistMBID returns the MusicBrainz ID for a local library
* artist by name, or "" if not found or no MBID tagged.
*/
export function GetArtistMBID(artistName: string): $CancellablePromise<string> {
return $Call.ByID(944736774, artistName);
}
/**
* GetArtistPlayCount returns the total LB listen count for an artist.
* Returns 0 if unknown.
*/
export function GetArtistPlayCount(artistMBID: string): $CancellablePromise<number> {
return $Call.ByID(143535277, artistMBID);
}
/**
* GetCandidateThumbnail returns CAA-only cover art for an autotag
* candidate, skipping the library-by-name index so embedded ID3
* art on the user's existing files doesn't pollute the candidate
* preview. Disk cache → network on RG → network on release.
*/
export function GetCandidateThumbnail(releaseMBID: string, releaseGroupMBID: string): $CancellablePromise<string> {
return $Call.ByID(1946932424, releaseMBID, releaseGroupMBID);
}
/**
* GetCredits is the bound form: the frontend asks for a tracklist's
* worth of MBIDs at once rather than one per row.
*
* Batched for the reason every other per-row backend question here is:
* asking on hover or on render turns a list into N IPC round trips, and
* this one is asked about every row of every list in the app.
*/
export function GetCredits(mbids: string[] | null): $CancellablePromise<{ [_ in string]?: $models.CreditPart[] | null } | null> {
return $Call.ByID(225964099, mbids);
}
/**
* GetExploreShelves builds the page Explore shows before a query.
*
* One call rather than one per shelf, for `home`'s reason: the shelves
* share nothing expensive, but the page has nothing useful to render
* until it knows which rows exist, and rows that pop in one at a time
* reflow under the cursor.
*/
export function GetExploreShelves(): $CancellablePromise<$models.ShelfPage> {
return $Call.ByID(144329614);
}
/**
* GetIndexStatus returns the current search index build status.
*/
export function GetIndexStatus(): $CancellablePromise<$models.IndexStatus> {
return $Call.ByID(3481063523);
}
/**
* GetLibrarySimilarArtists returns similar artists to the given
* MBID that are also in the user's local library. Uses the
* pre-computed similar_artist_map table (populated during Tier 4
* index build) joined with the artists table. No API calls.
*
* The artists table allows multiple rows with the same MBID
* (different artist credits like "A feat. B" that resolve to the
* same MB artist), so we use EXISTS instead of JOIN to avoid
* duplicating similar_artist_map rows.
*/
export function GetLibrarySimilarArtists(artistMBID: string): $CancellablePromise<$models.LBSimilarArtist[] | null> {
return $Call.ByID(104313157, artistMBID);
}
/**
* GetThumbnail returns a base64 data URL for the release group's
* cover art. Checks local library art first (by album+artist
* name), then disk cache, then Cover Art Archive.
* Returns "" if no cover art is available.
*/
export function GetThumbnail(releaseGroupMBID: string, albumName: string, artistName: string): $CancellablePromise<string> {
return $Call.ByID(4046192225, releaseGroupMBID, albumName, artistName);
}
/**
* GetThumbnails fetches multiple thumbnails in one call and returns
* a map of MBID → base64 data URL. Entries with no art are omitted.
* GetThumbnails returns ONLY cached/local art instantly — no network
* fetches. For items missing from the cache, the frontend should
* call GetThumbnail() individually so results stream in rather than
* blocking on a batch.
*/
export function GetThumbnails(requests: $models.ThumbnailRequest[] | null): $CancellablePromise<{ [_ in string]?: string } | null> {
return $Call.ByID(3124819542, requests);
}
/**
* GetTrackLyrics returns lyrics for a file. If the library
* already has them (from embedded tags) they're returned as-is;
* otherwise it fetches from LRCLIB, persists them (updating the FTS
* index), and returns them. Never returns an error to the frontend —
* a miss just yields an empty result.
*/
export function GetTrackLyrics(audioFileID: number): $CancellablePromise<$models.TrackLyrics> {
return $Call.ByID(1131284622, audioFileID);
}
/**
* GetTrackThumbnail returns cover art for a track. Accepts both
* the track's CAA release MBID and the resolved parent release
* group MBID (either may be empty). Tries the RG first to reuse
* discography cache; falls back to the release-level CAA endpoint
* when the RG isn't known — useful when the track's preferred CAA
* release doesn't belong to any RG currently in the index.
*/
export function GetTrackThumbnail(releaseMBID: string, releaseGroupMBID: string, albumName: string, artistName: string): $CancellablePromise<string> {
return $Call.ByID(4191599666, releaseMBID, releaseGroupMBID, albumName, artistName);
}
/**
* GetTrackThumbnails returns ONLY cached/local art for track
* requests, keyed by the caller-provided Key so callers can map
* results back to rows in their UI.
*/
export function GetTrackThumbnails(requests: $models.TrackThumbnailRequest[] | null): $CancellablePromise<{ [_ in string]?: string } | null> {
return $Call.ByID(2383043155, requests);
}
/**
* IndexBaselineSeries returns the incremental listens series the index's
* popularity is caught up to. A change across a refresh means new data
* was folded in.
*/
export function IndexBaselineSeries(): $CancellablePromise<number> {
return $Call.ByID(3317566669);
}
/**
* IndexImportComplete reports whether the dump import has finished all
* of its stages. Distinct from IsIndexReady, which only means the index
* holds enough rows to answer queries — a partially imported index is
* ready but not complete. Used by the headless builder to decide
* whether another run is needed.
*/
export function IndexImportComplete(): $CancellablePromise<boolean> {
return $Call.ByID(3872948181);
}
/**
* IndexLastImported returns when the dump import last completed, or the
* zero time if it never has.
*/
export function IndexLastImported(): $CancellablePromise<string> {
return $Call.ByID(1889359471);
}
/**
* InvalidateIndexDiscographies clears the discography build
* timestamp so the next index build re-runs Tiers 2-4. Call
* after a library rescan that may have populated new MBIDs.
*/
export function InvalidateIndexDiscographies(): $CancellablePromise<void> {
return $Call.ByID(2573191521);
}
/**
* InvalidateLibrarySync clears the "ready" markers guarding the gated
* library-sync steps so they re-run on the next launch. Call after a
* mutation that changes owned content outside a scan (e.g. removing a
* library), which would otherwise leave stale in_library flags and
* orphaned lyric-index rows.
*/
export function InvalidateLibrarySync(): $CancellablePromise<void> {
return $Call.ByID(2446309672);
}
/**
* IsIndexReady returns true once the index has been populated.
*/
export function IsIndexReady(): $CancellablePromise<boolean> {
return $Call.ByID(1366461294);
}
/**
* LookupArtist fetches a single MusicBrainz artist by MBID.
* Checks the local index first — has name, type, country,
* disambiguation, sort_name for indexed artists. Falls back to
* MB API for unknown artists and backfills the index for next time.
*/
export function LookupArtist(mbid: string): $CancellablePromise<$models.MBArtist | null> {
return $Call.ByID(3986196952, mbid);
}
/**
* LookupReleaseGroup fetches a single MusicBrainz release group by MBID.
*/
export function LookupReleaseGroup(mbid: string): $CancellablePromise<$models.MBReleaseGroup | null> {
return $Call.ByID(2946174711, mbid);
}
/**
* PopulateLocalCrossReferences updates the local_*_id columns on
* explore_index after a library scan.
*/
export function PopulateLocalCrossReferences(): $CancellablePromise<void> {
return $Call.ByID(1058376024);
}
/**
* PopulateLocalCrossReferencesIfNeeded runs the library→index sync only
* when it has not run since the last library change. Use it on the
* unchanged-library launch path so the write-heavy re-sync is skipped in
* steady state; the scan-completion path calls the unconditional form.
*/
export function PopulateLocalCrossReferencesIfNeeded(): $CancellablePromise<void> {
return $Call.ByID(814782146);
}
/**
* PrefetchReleases warms the local response cache for a set of release
* groups in the background so opening any of them is instant. Called from
* the artist page once its top-releases / discography render — album
* navigation almost always originates there. Already-cached groups are
* skipped; a cap bounds how many live fetches a single artist view can
* trigger so the MusicBrainz rate limiter isn't flooded.
* A release group the user owns *completely* is skipped outright: since
* tag-derived completeness landed, such an album opens with no catalog
* call at all — identity from its MBID, tracklist from its own files —
* so warming the most expensive request in the app on its behalf buys
* nothing. The skip is not merely an optimisation; those slots go to
* albums that will actually need the browse.
*/
export function PrefetchReleases(releaseGroupMBIDs: string[] | null): $CancellablePromise<void> {
return $Call.ByID(306872972, releaseGroupMBIDs);
}
/**
* PrepareIndexRebuild clears the completion marker so the next build
* re-imports from the newest published dump.
*/
export function PrepareIndexRebuild(): $CancellablePromise<void> {
return $Call.ByID(2772457219);
}
/**
* RebuildLyricsIndex rebuilds the FTS lyrics index from the current
* library. Cheap; safe to call after every scan.
*/
export function RebuildLyricsIndex(): $CancellablePromise<void> {
return $Call.ByID(300335776);
}
/**
* RebuildLyricsIndexIfNeeded rebuilds the lyrics FTS only when it has not
* been built since the last library change. The backfill keeps the index
* in sync incrementally thereafter, so on an unchanged library the full
* rebuild is redundant; the scan-completion path calls the unconditional
* form.
*/
export function RebuildLyricsIndexIfNeeded(): $CancellablePromise<void> {
return $Call.ByID(1447291626);
}
/**
* RecordSearchClick records that the user clicked a search result.
* Called from the frontend when any search result is clicked.
*/
export function RecordSearchClick(query: string, mbid: string, entityType: string): $CancellablePromise<void> {
return $Call.ByID(824485756, query, mbid, entityType);
}
/**
* RefreshIndexNow folds newly published incremental listens dumps into
* the index synchronously. Pass 0 to bypass the cadence gate.
*/
export function RefreshIndexNow(minInterval: time$0.Duration): $CancellablePromise<void> {
return $Call.ByID(4165220556, minInterval);
}
/**
* RefreshListenCounts folds any newly-published incremental listen dumps
* into the index's popularity numbers, in the background. No-op when
* offline, when a full build is running, when there is no baseline
* import, or when the last refresh was within the weekly cadence. Fully
* local — downloads the small daily dumps but makes no ListenBrainz API
* calls.
*/
export function RefreshListenCounts(): $CancellablePromise<void> {
return $Call.ByID(3813092323);
}
/**
* ResolveReleaseGroupMBIDs takes a list of CAA release MBIDs (from
* recording metadata) and returns a map of release MBID → release
* group MBID. The frontend uses this to fetch track cover art via
* the parent release group, reusing whatever cache exists for the
* album already.
*/
export function ResolveReleaseGroupMBIDs(caaReleaseMBIDs: string[] | null): $CancellablePromise<{ [_ in string]?: string } | null> {
return $Call.ByID(325093206, caaReleaseMBIDs);
}
/**
* SearchLocal queries only the local FTS5 index and returns fully
* ranked results instantly with no network calls. This is the
* primary interactive search path: now that the index is populated
* from the MetaBrainz dumps it covers essentially every popular
* entity, so the frontend drives search entirely from here. The
* old MusicBrainz network pipeline (Search) is retained for a future
* opt-in "search online" affordance but is no longer called on the
* hot path.
*
* Returns nil if the index has no hits for the query, so the caller
* can fall back to whatever owned-library matches it already has.
*/
export function SearchLocal(query: string): $CancellablePromise<$models.MBSearchResult | null> {
return $Call.ByID(189423736, query);
}
/**
* SearchLyrics finds library tracks whose lyrics contain the given
* fragment, ranked by relevance. Pure local FTS — no network.
*/
export function SearchLyrics(query: string): $CancellablePromise<$models.LyricsResult[] | null> {
return $Call.ByID(4153164053, query);
}
/**
* SetAlbumComplete injects the completeness check. It is injected
* rather than imported because `library` and `explore` do not depend on
* each other in either direction today, and one prefetch heuristic is
* not a reason to introduce that edge — the alternative, re-deriving
* "complete" from SQL here, would be a second definition of it.
*/
export function SetAlbumComplete(fn: $models.AlbumCompleteFunc): $CancellablePromise<void> {
return $Call.ByID(942474493, fn);
}
/**
* SimilarArtists returns artists similar to the given artist MBID.
*/
export function SimilarArtists(artistMBID: string): $CancellablePromise<$models.LBSimilarArtist[] | null> {
return $Call.ByID(2048211426, artistMBID);
}
/**
* StartIndexBuild kicks off the background search index build.
* Call this after the library scan completes so the indexer doesn't
* starve the scan for DB access.
*/
export function StartIndexBuild(): $CancellablePromise<void> {
return $Call.ByID(1444396415);
}
/**
* StopIndexBuild cancels the background search index build.
* Call before a full rescan to free the DB for the scan.
*/
export function StopIndexBuild(): $CancellablePromise<void> {
return $Call.ByID(3056245285);
}
/**
* TopRecordingsForArtist returns the most-listened recordings for an
* artist. Serves instantly from the local index when available; when the
* artist isn't indexed yet it returns empty immediately and fetches the
* discography in the background, emitting ArtistDiscographyReady so the
* caller can re-fetch — the request never blocks on a live fetch.
*/
export function TopRecordingsForArtist(artistMBID: string): $CancellablePromise<$models.LBTopRecording[] | null> {
return $Call.ByID(2077579960, artistMBID);
}
/**
* TopReleaseGroupsForArtist returns the most-listened release groups for
* an artist. Same non-blocking contract as TopRecordingsForArtist: index
* hit is instant, a miss kicks off a background discography fetch and
* returns empty, and ArtistDiscographyReady signals when to re-fetch.
*/
export function TopReleaseGroupsForArtist(artistMBID: string): $CancellablePromise<$models.LBTopReleaseGroup[] | null> {
return $Call.ByID(602197643, artistMBID);
}
/**
* WaitForIndexIdle blocks until no index build or artist indexing
* goroutine is running. Does not cancel a running build.
*/
export function WaitForIndexIdle(): $CancellablePromise<void> {
return $Call.ByID(3200929511);
}