feat: a release catalogue from MusicBrainz; request search asks it directly (M502 #5519)
release / govulncheck (push) Successful in 22s
release / web (push) Failing after 24s
release / go (push) Successful in 1m57s
release / integration (push) Successful in 4m1s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / android (push) Canceled after 4m38s
release / Build signed APK (releases and dev) (push) Canceled after 4m37s
release / govulncheck (push) Successful in 22s
release / web (push) Failing after 24s
release / go (push) Successful in 1m57s
release / integration (push) Successful in 4m1s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / android (push) Canceled after 4m38s
release / Build signed APK (releases and dev) (push) Canceled after 4m37s
Minstrel keeps its own record of what artists have released, read from MusicBrainz, so the wanted list, search and import no longer need Lidarr. - internal/musicbrainz: searches (artists, release groups, recordings) and browses (an artist's release groups; a group's releases with track lists). One process-wide pacer at 1.1s, shared with tag enrichment, because MusicBrainz's limit is per address. A 503 throttle is retried. - Migration 0080: mb_artists, mb_release_groups, mb_artist_release_groups (a collaboration belongs to each credited artist), mb_releases, mb_release_tracks. - internal/catalogue: Discography and Releases serve the held copy and re-read it when older than 7 or 30 days. An unreachable MusicBrainz serves the stale copy, marked. An hourly pass re-reads due artists. - Art for what isn't held comes through /api/catalogue/art from the enabled cover-art sources (Cover Art Archive by release group, then the name-based sources) and is kept on disk, so the browser never fetches a third-party host. A miss is a 404 and the card shows its icon. - Request search moves from /api/lidarr/search to /api/catalogue/search, with the same response shape. Albums are release groups, and "in library" matches albums.release_group_mbid. A 10-minute cache absorbs search-as-you-type. Discover suggestions point at the art endpoint instead of looking up Lidarr. - Android calls the new path and loads Discover art through ServerImage. Installed builds older than this one lose Discover search until they update (no compatibility path, rule 22). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+13
-49
@@ -1,21 +1,18 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/lidarr"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/recommendation"
|
||||
)
|
||||
|
||||
@@ -31,10 +28,10 @@ type suggestionView struct {
|
||||
// out-of-library artists is permanently partial (#2376), and the card
|
||||
// falls back to the seed attribution it has always shown.
|
||||
MatchedTags []string `json:"matched_tags,omitempty"`
|
||||
// ImageURL is resolved on-demand from Lidarr (out-of-library
|
||||
// artists have no local art row). Omitted when Lidarr is disabled
|
||||
// or has no match — the client falls back to a placeholder. Not
|
||||
// cached: a remote URL Lidarr surfaced, fetched by the browser.
|
||||
// ImageURL is Minstrel's catalogue art endpoint for the candidate
|
||||
// (out-of-library artists have no local art row, M502 #5519). It
|
||||
// answers 404 when no enabled source has an image, and the client
|
||||
// falls back to a placeholder.
|
||||
ImageURL string `json:"image_url,omitempty"`
|
||||
}
|
||||
|
||||
@@ -105,7 +102,7 @@ func (h *handlers) handleListSuggestions(w http.ResponseWriter, r *http.Request)
|
||||
MatchedTags: s.MatchedTags,
|
||||
})
|
||||
}
|
||||
h.resolveSuggestionArt(r.Context(), out)
|
||||
resolveSuggestionArt(out)
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
@@ -250,48 +247,15 @@ func (h *handlers) handleListSuggestionSnoozes(w http.ResponseWriter, r *http.Re
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
// resolveSuggestionArt fills ImageURL on-demand from Lidarr's artist
|
||||
// lookup, matched by MBID (foreignArtistId). Best-effort and cache-free:
|
||||
// Lidarr is the only source — when it's disabled, unreachable, or has
|
||||
// no match for a candidate, that entry keeps an empty ImageURL and the
|
||||
// client renders its placeholder. Lookups run with bounded concurrency
|
||||
// so a full Discover page doesn't serialize ~12 round-trips. Never
|
||||
// fails the request; the suggestions list is the contract, art is a
|
||||
// nicety.
|
||||
func (h *handlers) resolveSuggestionArt(ctx context.Context, views []suggestionView) {
|
||||
if len(views) == 0 {
|
||||
return
|
||||
}
|
||||
cfg, err := h.lidarrCfg.Get(ctx)
|
||||
if err != nil || !cfg.Enabled || cfg.BaseURL == "" || cfg.APIKey == "" {
|
||||
return // Lidarr off / unconfigured → placeholders only
|
||||
}
|
||||
client := lidarr.NewClient(cfg.BaseURL, cfg.APIKey)
|
||||
|
||||
const maxConcurrent = 6
|
||||
sem := make(chan struct{}, maxConcurrent)
|
||||
var wg sync.WaitGroup
|
||||
// resolveSuggestionArt points each suggestion's ImageURL at Minstrel's
|
||||
// catalogue art endpoint (M502 #5519). Nothing is fetched here: the image is
|
||||
// looked up from the enabled cover-art sources when the browser asks for it,
|
||||
// and kept, so listing suggestions makes no outbound call. A candidate with
|
||||
// no MBID keeps an empty ImageURL and the client renders its placeholder.
|
||||
func resolveSuggestionArt(views []suggestionView) {
|
||||
for i := range views {
|
||||
if views[i].MBID == "" || views[i].Name == "" {
|
||||
continue
|
||||
if views[i].MBID != "" {
|
||||
views[i].ImageURL = artistArtURL(views[i].MBID, views[i].Name)
|
||||
}
|
||||
wg.Add(1)
|
||||
sem <- struct{}{}
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
results, lerr := client.LookupArtist(ctx, views[idx].Name)
|
||||
if lerr != nil {
|
||||
return // best-effort: no art on lookup failure
|
||||
}
|
||||
for _, res := range results {
|
||||
if res.MBID == views[idx].MBID && res.ImageURL != "" {
|
||||
// Distinct slice index per goroutine → race-free.
|
||||
views[idx].ImageURL = res.ImageURL
|
||||
return
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user