Add optional MusicBrainz/Cover Art Archive metadata enrichment

getArtistInfo/2 and getAlbumInfo/2 now return a real MusicBrainz
disambiguation/cover-art lookup for entities that carry a locally
tagged MusicBrainz ID, gated behind the new
VIBESONIC_ENABLE_EXTERNAL_METADATA flag (default off). Responses are
cached in the existing external_metadata_cache table for 30 days to
avoid hammering the free MusicBrainz/CAA services.

VibeSonic still works completely offline: with the flag off, or when
an entity has no tagged MusicBrainz ID, or on any network/lookup
failure, the handlers fall back to local-only fields exactly as
before — nothing is ever fabricated, and a failed external lookup
never fails the request.

internal/metadataenrich is a small, independently-testable HTTP client
for the two real APIs (base URLs overridable for tests, exercised
against httptest servers rather than the live network).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014r98AtcBbevpscWLsfwVvv
This commit is contained in:
2026-08-23 08:35:40 -05:00
co-authored by Claude Sonnet 5
parent 2a69f6dc33
commit a921f9e2bc
7 changed files with 649 additions and 12 deletions
+18 -12
View File
@@ -88,44 +88,50 @@ func handleGetVideoInfo(rc *Context) *apiError {
return nil
}
// --- Artist/album info: VibeSonic has no external metadata provider
// (last.fm etc.) in Phase 2, so these honestly return only what's known
// locally — which for biography/images/similar-artists is nothing —
// rather than fabricating placeholder content. ---
// --- Artist/album info: local data only (title/name, and a
// MusicBrainz ID if one was tagged), optionally enriched from
// MusicBrainz/Cover Art Archive when VIBESONIC_ENABLE_EXTERNAL_METADATA
// is set and the entity has a tagged MusicBrainz ID — see enrich.go.
// VibeSonic must work completely offline; enrichment is always
// best-effort and never blocks or fails the response. ---
func handleGetArtistInfo(rc *Context) *apiError {
if _, aerr := rc.Params.Required("id"); aerr != nil {
id, aerr := rc.Params.Required("id")
if aerr != nil {
return aerr
}
rc.Response = newResponse()
rc.Response.ArtistInfo = &ArtistInfo{}
rc.Response.ArtistInfo = &ArtistInfo{ArtistInfoBase: artistInfoBase(rc, id)}
return nil
}
func handleGetArtistInfo2(rc *Context) *apiError {
if _, aerr := rc.Params.Required("id"); aerr != nil {
id, aerr := rc.Params.Required("id")
if aerr != nil {
return aerr
}
rc.Response = newResponse()
rc.Response.ArtistInfo2 = &ArtistInfo2{}
rc.Response.ArtistInfo2 = &ArtistInfo2{ArtistInfoBase: artistInfoBase(rc, id)}
return nil
}
func handleGetAlbumInfo(rc *Context) *apiError {
if _, aerr := rc.Params.Required("id"); aerr != nil {
id, aerr := rc.Params.Required("id")
if aerr != nil {
return aerr
}
rc.Response = newResponse()
rc.Response.AlbumInfo = &AlbumInfo{}
rc.Response.AlbumInfo = albumInfo(rc, id)
return nil
}
func handleGetAlbumInfo2(rc *Context) *apiError {
if _, aerr := rc.Params.Required("id"); aerr != nil {
id, aerr := rc.Params.Required("id")
if aerr != nil {
return aerr
}
rc.Response = newResponse()
rc.Response.AlbumInfo = &AlbumInfo{}
rc.Response.AlbumInfo = albumInfo(rc, id)
return nil
}