295d9da361
Parallels EnrichAlbum but with no sidecar layer (artists have no per-artist directory in a music library). Iterates EnabledArtistProviders, writes thumb.jpg + fanart.jpg to <data_dir>/artist-art/<artist_id>/, stamps artist_art_source + artist_art_sources_version atomically. Atomicity: provider returns whichever images it has — partial success (thumb-only or fanart-only) is persisted with whichever paths landed, source still stamped. ErrNotFound from every enabled provider settles the row to 'none' with the current version stamp. Any ErrTransient leaves the row NULL for next-pass retry. CleanupArtistArt removes <data_dir>/artist-art/<artist_id>/ on artist delete; idempotent on missing dir. Hooked into the artist- delete cascade in tracks/service.go after the transaction commits; non-fatal on failure (logs but doesn't fail the delete). EnrichArtistBatch drains rows from ListArtistsMissingArt; the orchestrator's 4th stage (added in T10) calls this. Tests cover the eligibility table, atomicity (thumb-only / fanart-only), all-404-settles, ErrTransient-leaves-NULL, filesystem layout, and CleanupArtistArt idempotency. tracks.Service gains a dataDir field; tracks.NewService takes it as a new parameter. All three call sites updated (server/server.go, api/auth_test.go, api/admin_tracks_test.go). The dataDir flows from cfg.Storage.DataDir → server.New → s.DataDir → tracks.NewService without any change to cmd/minstrel/main.go.
530 lines
15 KiB
Go
530 lines
15 KiB
Go
package coverart
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
|
)
|
|
|
|
// stubArtistProvider is a configurable ArtistArtProvider for artist enricher
|
|
// tests. Embeds fakeProvider for the base Provider methods; overrides
|
|
// FetchArtistArt with controllable byte/error responses.
|
|
type stubArtistProvider struct {
|
|
fakeProvider
|
|
thumb, fanart []byte
|
|
err error
|
|
}
|
|
|
|
func (f *stubArtistProvider) FetchArtistArt(_ context.Context, _ string) ([]byte, []byte, error) {
|
|
return f.thumb, f.fanart, f.err
|
|
}
|
|
|
|
// insertArtist seeds a fresh artist via UpsertArtist and returns its UUID.
|
|
// mbid may be "" to seed a MBID-less artist.
|
|
func insertArtist(t *testing.T, q *dbq.Queries, ctx context.Context, name, mbid string) pgtype.UUID {
|
|
t.Helper()
|
|
var mbidParam *string
|
|
if mbid != "" {
|
|
mbidParam = &mbid
|
|
}
|
|
a, err := q.UpsertArtist(ctx, dbq.UpsertArtistParams{
|
|
Name: name,
|
|
SortName: name,
|
|
Mbid: mbidParam,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("insertArtist %q: %v", name, err)
|
|
}
|
|
return a.ID
|
|
}
|
|
|
|
// --- Eligibility tests ---
|
|
|
|
func TestEnrichArtist_NullSource_Eligible(t *testing.T) {
|
|
pool := newPool(t)
|
|
ctx := context.Background()
|
|
q := dbq.New(pool)
|
|
|
|
resetRegistryForTests()
|
|
t.Cleanup(resetRegistryForTests)
|
|
|
|
stub := &stubArtistProvider{
|
|
fakeProvider: fakeProvider{id: "stub-artist", defaultOn: true},
|
|
thumb: []byte("thumb_bytes"),
|
|
fanart: []byte("fanart_bytes"),
|
|
}
|
|
Register(stub)
|
|
|
|
e := newTestEnricher(t, pool)
|
|
artistID := insertArtist(t, q, ctx, "Null Artist", "11111111-1111-1111-1111-111111111111")
|
|
|
|
dataDir := t.TempDir()
|
|
if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
|
|
t.Fatalf("EnrichArtist: %v", err)
|
|
}
|
|
|
|
row, err := q.GetArtistByID(ctx, artistID)
|
|
if err != nil {
|
|
t.Fatalf("GetArtistByID: %v", err)
|
|
}
|
|
if row.ArtistArtSource == nil || *row.ArtistArtSource != "stub-artist" {
|
|
t.Errorf("source = %v, want 'stub-artist'", row.ArtistArtSource)
|
|
}
|
|
}
|
|
|
|
func TestEnrichArtist_FoundSource_Skip(t *testing.T) {
|
|
pool := newPool(t)
|
|
ctx := context.Background()
|
|
q := dbq.New(pool)
|
|
|
|
resetRegistryForTests()
|
|
t.Cleanup(resetRegistryForTests)
|
|
|
|
stub := &stubArtistProvider{
|
|
fakeProvider: fakeProvider{id: "stub-artist", defaultOn: true},
|
|
thumb: []byte("should_not_write"),
|
|
}
|
|
Register(stub)
|
|
|
|
e := newTestEnricher(t, pool)
|
|
artistID := insertArtist(t, q, ctx, "Found Artist", "22222222-2222-2222-2222-222222222222")
|
|
|
|
// Pre-mark with a "found" source.
|
|
src := "theaudiodb"
|
|
if err := q.SetArtistArtWithVersion(ctx, dbq.SetArtistArtWithVersionParams{
|
|
ID: artistID,
|
|
ArtistArtSource: &src,
|
|
ArtistArtSourcesVersion: e.settings.CurrentVersion(),
|
|
}); err != nil {
|
|
t.Fatalf("set: %v", err)
|
|
}
|
|
|
|
dataDir := t.TempDir()
|
|
if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
|
|
t.Fatalf("EnrichArtist: %v", err)
|
|
}
|
|
|
|
// Source must remain 'theaudiodb'; no files should have been written.
|
|
row, err := q.GetArtistByID(ctx, artistID)
|
|
if err != nil {
|
|
t.Fatalf("GetArtistByID: %v", err)
|
|
}
|
|
if row.ArtistArtSource == nil || *row.ArtistArtSource != "theaudiodb" {
|
|
t.Errorf("source = %v, want 'theaudiodb' (preserved)", row.ArtistArtSource)
|
|
}
|
|
thumbPath := filepath.Join(dataDir, "artist-art", uuidString(artistID), "thumb.jpg")
|
|
if _, err := os.Stat(thumbPath); !os.IsNotExist(err) {
|
|
t.Errorf("expected no thumb.jpg (skip), got stat err = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEnrichArtist_NoneCurrentVersion_Skip(t *testing.T) {
|
|
pool := newPool(t)
|
|
ctx := context.Background()
|
|
q := dbq.New(pool)
|
|
|
|
resetRegistryForTests()
|
|
t.Cleanup(resetRegistryForTests)
|
|
|
|
stub := &stubArtistProvider{
|
|
fakeProvider: fakeProvider{id: "stub-artist", defaultOn: true},
|
|
thumb: []byte("should_not_write"),
|
|
}
|
|
Register(stub)
|
|
|
|
e := newTestEnricher(t, pool)
|
|
artistID := insertArtist(t, q, ctx, "None Current Artist", "33333333-3333-3333-3333-333333333333")
|
|
|
|
src := "none"
|
|
if err := q.SetArtistArtWithVersion(ctx, dbq.SetArtistArtWithVersionParams{
|
|
ID: artistID,
|
|
ArtistArtSource: &src,
|
|
ArtistArtSourcesVersion: e.settings.CurrentVersion(),
|
|
}); err != nil {
|
|
t.Fatalf("set: %v", err)
|
|
}
|
|
|
|
dataDir := t.TempDir()
|
|
if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
|
|
t.Fatalf("EnrichArtist: %v", err)
|
|
}
|
|
|
|
// Source must remain 'none' at current version.
|
|
row, err := q.GetArtistByID(ctx, artistID)
|
|
if err != nil {
|
|
t.Fatalf("GetArtistByID: %v", err)
|
|
}
|
|
if row.ArtistArtSource == nil || *row.ArtistArtSource != "none" {
|
|
t.Errorf("source = %v, want 'none' (skip — current version)", row.ArtistArtSource)
|
|
}
|
|
}
|
|
|
|
func TestEnrichArtist_NoneStaleVersion_FlipAndRetry(t *testing.T) {
|
|
pool := newPool(t)
|
|
ctx := context.Background()
|
|
q := dbq.New(pool)
|
|
|
|
resetRegistryForTests()
|
|
t.Cleanup(resetRegistryForTests)
|
|
|
|
stub := &stubArtistProvider{
|
|
fakeProvider: fakeProvider{id: "stub-artist", defaultOn: true},
|
|
thumb: []byte("retry_thumb"),
|
|
fanart: []byte("retry_fanart"),
|
|
}
|
|
Register(stub)
|
|
|
|
e := newTestEnricher(t, pool)
|
|
artistID := insertArtist(t, q, ctx, "Stale None Artist", "44444444-4444-4444-4444-444444444444")
|
|
|
|
// Set 'none' with a stale version (currentVersion - 1).
|
|
staleVersion := e.settings.CurrentVersion() - 1
|
|
src := "none"
|
|
if err := q.SetArtistArtWithVersion(ctx, dbq.SetArtistArtWithVersionParams{
|
|
ID: artistID,
|
|
ArtistArtSource: &src,
|
|
ArtistArtSourcesVersion: staleVersion,
|
|
}); err != nil {
|
|
t.Fatalf("set stale none: %v", err)
|
|
}
|
|
|
|
dataDir := t.TempDir()
|
|
if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
|
|
t.Fatalf("EnrichArtist: %v", err)
|
|
}
|
|
|
|
row, err := q.GetArtistByID(ctx, artistID)
|
|
if err != nil {
|
|
t.Fatalf("GetArtistByID: %v", err)
|
|
}
|
|
// Stale 'none' should be flipped and re-enriched; stub succeeds so
|
|
// source becomes the provider ID.
|
|
if row.ArtistArtSource == nil || *row.ArtistArtSource != "stub-artist" {
|
|
t.Errorf("source = %v, want 'stub-artist' (stale none flipped and retried)", row.ArtistArtSource)
|
|
}
|
|
}
|
|
|
|
// --- Atomicity: thumb-only / fanart-only ---
|
|
|
|
func TestEnrichArtist_ThumbOnly_ThumbPathSetFanartNull(t *testing.T) {
|
|
pool := newPool(t)
|
|
ctx := context.Background()
|
|
q := dbq.New(pool)
|
|
|
|
resetRegistryForTests()
|
|
t.Cleanup(resetRegistryForTests)
|
|
|
|
stub := &stubArtistProvider{
|
|
fakeProvider: fakeProvider{id: "thumb-only-provider", defaultOn: true},
|
|
thumb: []byte("thumb_only"),
|
|
fanart: nil,
|
|
}
|
|
Register(stub)
|
|
|
|
e := newTestEnricher(t, pool)
|
|
artistID := insertArtist(t, q, ctx, "Thumb Only Artist", "55555555-5555-5555-5555-555555555555")
|
|
|
|
dataDir := t.TempDir()
|
|
if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
|
|
t.Fatalf("EnrichArtist: %v", err)
|
|
}
|
|
|
|
row, err := q.GetArtistByID(ctx, artistID)
|
|
if err != nil {
|
|
t.Fatalf("GetArtistByID: %v", err)
|
|
}
|
|
if row.ArtistArtSource == nil || *row.ArtistArtSource != "thumb-only-provider" {
|
|
t.Errorf("source = %v, want 'thumb-only-provider'", row.ArtistArtSource)
|
|
}
|
|
if row.ArtistThumbPath == nil {
|
|
t.Errorf("artist_thumb_path is nil, want a path")
|
|
}
|
|
if row.ArtistFanartPath != nil {
|
|
t.Errorf("artist_fanart_path = %v, want nil (fanart not provided)", row.ArtistFanartPath)
|
|
}
|
|
if row.ArtistThumbPath != nil {
|
|
body, rerr := os.ReadFile(*row.ArtistThumbPath)
|
|
if rerr != nil {
|
|
t.Fatalf("read thumb: %v", rerr)
|
|
}
|
|
if string(body) != "thumb_only" {
|
|
t.Errorf("thumb content = %q, want 'thumb_only'", string(body))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEnrichArtist_FanartOnly_FanartPathSetThumbNull(t *testing.T) {
|
|
pool := newPool(t)
|
|
ctx := context.Background()
|
|
q := dbq.New(pool)
|
|
|
|
resetRegistryForTests()
|
|
t.Cleanup(resetRegistryForTests)
|
|
|
|
stub := &stubArtistProvider{
|
|
fakeProvider: fakeProvider{id: "fanart-only-provider", defaultOn: true},
|
|
thumb: nil,
|
|
fanart: []byte("fanart_only"),
|
|
}
|
|
Register(stub)
|
|
|
|
e := newTestEnricher(t, pool)
|
|
artistID := insertArtist(t, q, ctx, "Fanart Only Artist", "66666666-6666-6666-6666-666666666666")
|
|
|
|
dataDir := t.TempDir()
|
|
if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
|
|
t.Fatalf("EnrichArtist: %v", err)
|
|
}
|
|
|
|
row, err := q.GetArtistByID(ctx, artistID)
|
|
if err != nil {
|
|
t.Fatalf("GetArtistByID: %v", err)
|
|
}
|
|
if row.ArtistArtSource == nil || *row.ArtistArtSource != "fanart-only-provider" {
|
|
t.Errorf("source = %v, want 'fanart-only-provider'", row.ArtistArtSource)
|
|
}
|
|
if row.ArtistThumbPath != nil {
|
|
t.Errorf("artist_thumb_path = %v, want nil (thumb not provided)", row.ArtistThumbPath)
|
|
}
|
|
if row.ArtistFanartPath == nil {
|
|
t.Errorf("artist_fanart_path is nil, want a path")
|
|
}
|
|
}
|
|
|
|
// --- All-404 settles to 'none' ---
|
|
|
|
func TestEnrichArtist_All404_SettlesNone(t *testing.T) {
|
|
pool := newPool(t)
|
|
ctx := context.Background()
|
|
q := dbq.New(pool)
|
|
|
|
resetRegistryForTests()
|
|
t.Cleanup(resetRegistryForTests)
|
|
|
|
stub := &stubArtistProvider{
|
|
fakeProvider: fakeProvider{id: "404-provider", defaultOn: true},
|
|
err: ErrNotFound,
|
|
}
|
|
Register(stub)
|
|
|
|
e := newTestEnricher(t, pool)
|
|
artistID := insertArtist(t, q, ctx, "All404 Artist", "77777777-7777-7777-7777-777777777777")
|
|
|
|
dataDir := t.TempDir()
|
|
if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
|
|
t.Fatalf("EnrichArtist: %v", err)
|
|
}
|
|
|
|
row, err := q.GetArtistByID(ctx, artistID)
|
|
if err != nil {
|
|
t.Fatalf("GetArtistByID: %v", err)
|
|
}
|
|
if row.ArtistArtSource == nil || *row.ArtistArtSource != "none" {
|
|
t.Errorf("source = %v, want 'none' (all providers 404)", row.ArtistArtSource)
|
|
}
|
|
if row.ArtistArtSourcesVersion != e.settings.CurrentVersion() {
|
|
t.Errorf("version = %d, want %d", row.ArtistArtSourcesVersion, e.settings.CurrentVersion())
|
|
}
|
|
}
|
|
|
|
// --- Empty providers list settles vacuously to 'none' ---
|
|
|
|
func TestEnrichArtist_EmptyProviders_SettlesNone(t *testing.T) {
|
|
pool := newPool(t)
|
|
ctx := context.Background()
|
|
q := dbq.New(pool)
|
|
|
|
// Register a plain provider that does NOT implement ArtistArtProvider —
|
|
// EnabledArtistProviders will return an empty slice.
|
|
resetRegistryForTests()
|
|
t.Cleanup(resetRegistryForTests)
|
|
Register(&fakeProvider{id: "album-only", defaultOn: true})
|
|
|
|
e := newTestEnricher(t, pool)
|
|
artistID := insertArtist(t, q, ctx, "No Provider Artist", "88888888-8888-8888-8888-888888888888")
|
|
|
|
dataDir := t.TempDir()
|
|
if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
|
|
t.Fatalf("EnrichArtist: %v", err)
|
|
}
|
|
|
|
row, err := q.GetArtistByID(ctx, artistID)
|
|
if err != nil {
|
|
t.Fatalf("GetArtistByID: %v", err)
|
|
}
|
|
if row.ArtistArtSource == nil || *row.ArtistArtSource != "none" {
|
|
t.Errorf("source = %v, want 'none' (vacuous all-404 with empty provider list)", row.ArtistArtSource)
|
|
}
|
|
}
|
|
|
|
// --- ErrTransient leaves row NULL ---
|
|
|
|
func TestEnrichArtist_ErrTransient_LeavesNull(t *testing.T) {
|
|
pool := newPool(t)
|
|
ctx := context.Background()
|
|
q := dbq.New(pool)
|
|
|
|
resetRegistryForTests()
|
|
t.Cleanup(resetRegistryForTests)
|
|
|
|
stub := &stubArtistProvider{
|
|
fakeProvider: fakeProvider{id: "transient-provider", defaultOn: true},
|
|
err: ErrTransient,
|
|
}
|
|
Register(stub)
|
|
|
|
e := newTestEnricher(t, pool)
|
|
artistID := insertArtist(t, q, ctx, "Transient Artist", "99999999-9999-9999-9999-999999999999")
|
|
|
|
dataDir := t.TempDir()
|
|
if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
|
|
t.Fatalf("EnrichArtist: %v", err)
|
|
}
|
|
|
|
row, err := q.GetArtistByID(ctx, artistID)
|
|
if err != nil {
|
|
t.Fatalf("GetArtistByID: %v", err)
|
|
}
|
|
// Transient: source must remain NULL (eligible for retry).
|
|
if row.ArtistArtSource != nil {
|
|
t.Errorf("source = %v, want nil (transient should not settle)", row.ArtistArtSource)
|
|
}
|
|
}
|
|
|
|
// --- Filesystem layout ---
|
|
|
|
func TestEnrichArtist_FilesystemLayout(t *testing.T) {
|
|
pool := newPool(t)
|
|
ctx := context.Background()
|
|
q := dbq.New(pool)
|
|
|
|
resetRegistryForTests()
|
|
t.Cleanup(resetRegistryForTests)
|
|
|
|
thumbBytes := []byte("thumb_content")
|
|
fanartBytes := []byte("fanart_content")
|
|
stub := &stubArtistProvider{
|
|
fakeProvider: fakeProvider{id: "layout-provider", defaultOn: true},
|
|
thumb: thumbBytes,
|
|
fanart: fanartBytes,
|
|
}
|
|
Register(stub)
|
|
|
|
e := newTestEnricher(t, pool)
|
|
artistID := insertArtist(t, q, ctx, "Layout Artist", "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
|
|
|
|
dataDir := t.TempDir()
|
|
if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
|
|
t.Fatalf("EnrichArtist: %v", err)
|
|
}
|
|
|
|
artistIDStr := uuidString(artistID)
|
|
thumbPath := filepath.Join(dataDir, "artist-art", artistIDStr, "thumb.jpg")
|
|
fanartPath := filepath.Join(dataDir, "artist-art", artistIDStr, "fanart.jpg")
|
|
|
|
thumbBody, rerr := os.ReadFile(thumbPath)
|
|
if rerr != nil {
|
|
t.Fatalf("read thumb.jpg: %v", rerr)
|
|
}
|
|
if string(thumbBody) != string(thumbBytes) {
|
|
t.Errorf("thumb.jpg = %q, want %q", thumbBody, thumbBytes)
|
|
}
|
|
|
|
fanartBody, rerr := os.ReadFile(fanartPath)
|
|
if rerr != nil {
|
|
t.Fatalf("read fanart.jpg: %v", rerr)
|
|
}
|
|
if string(fanartBody) != string(fanartBytes) {
|
|
t.Errorf("fanart.jpg = %q, want %q", fanartBody, fanartBytes)
|
|
}
|
|
|
|
row, err := q.GetArtistByID(ctx, artistID)
|
|
if err != nil {
|
|
t.Fatalf("GetArtistByID: %v", err)
|
|
}
|
|
if row.ArtistThumbPath == nil || *row.ArtistThumbPath != thumbPath {
|
|
t.Errorf("DB thumb_path = %v, want %s", row.ArtistThumbPath, thumbPath)
|
|
}
|
|
if row.ArtistFanartPath == nil || *row.ArtistFanartPath != fanartPath {
|
|
t.Errorf("DB fanart_path = %v, want %s", row.ArtistFanartPath, fanartPath)
|
|
}
|
|
}
|
|
|
|
// --- CleanupArtistArt ---
|
|
|
|
func TestCleanupArtistArt_RemovesDirectory(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
|
|
artistID := pgtype.UUID{
|
|
Bytes: [16]byte{0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb},
|
|
Valid: true,
|
|
}
|
|
artistDir := filepath.Join(dataDir, "artist-art", uuidString(artistID))
|
|
if err := os.MkdirAll(artistDir, 0o755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(artistDir, "thumb.jpg"), []byte("x"), 0o644); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
|
|
if err := CleanupArtistArt(dataDir, artistID); err != nil {
|
|
t.Fatalf("CleanupArtistArt: %v", err)
|
|
}
|
|
|
|
if _, err := os.Stat(artistDir); !os.IsNotExist(err) {
|
|
t.Errorf("directory still exists after cleanup: stat err = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCleanupArtistArt_IdempotentMissingDir(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
artistID := pgtype.UUID{
|
|
Bytes: [16]byte{0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc},
|
|
Valid: true,
|
|
}
|
|
|
|
// Directory does not exist — should be a no-op.
|
|
if err := CleanupArtistArt(dataDir, artistID); err != nil {
|
|
t.Fatalf("CleanupArtistArt on missing dir: %v", err)
|
|
}
|
|
}
|
|
|
|
// --- MBID guard ---
|
|
|
|
func TestEnrichArtist_NoMBID_LeavesNull(t *testing.T) {
|
|
pool := newPool(t)
|
|
ctx := context.Background()
|
|
q := dbq.New(pool)
|
|
|
|
resetRegistryForTests()
|
|
t.Cleanup(resetRegistryForTests)
|
|
|
|
stub := &stubArtistProvider{
|
|
fakeProvider: fakeProvider{id: "stub-artist", defaultOn: true},
|
|
thumb: []byte("should_not_write"),
|
|
}
|
|
Register(stub)
|
|
|
|
e := newTestEnricher(t, pool)
|
|
// Seed with no MBID.
|
|
artistID := insertArtist(t, q, ctx, "No MBID Artist", "")
|
|
|
|
dataDir := t.TempDir()
|
|
if err := e.EnrichArtist(ctx, artistID, dataDir); err != nil {
|
|
t.Fatalf("EnrichArtist: %v", err)
|
|
}
|
|
|
|
row, err := q.GetArtistByID(ctx, artistID)
|
|
if err != nil {
|
|
t.Fatalf("GetArtistByID: %v", err)
|
|
}
|
|
if row.ArtistArtSource != nil {
|
|
t.Errorf("source = %v, want nil (no MBID — skip)", row.ArtistArtSource)
|
|
}
|
|
}
|