Skip to content

Instantly share code, notes, and snippets.

@radiosilence
Created March 29, 2026 17:31
Show Gist options
  • Select an option

  • Save radiosilence/2b251e93ce49d339131ef0549ac9809b to your computer and use it in GitHub Desktop.

Select an option

Save radiosilence/2b251e93ce49d339131ef0549ac9809b to your computer and use it in GitHub Desktop.
Navidrome: Artist Art & Description Pipeline Report

Navidrome: Artist Art & Description Pipeline

The Agent System

Navidrome uses a pluggable multi-agent system where external providers are tried in priority order (configured via Agents setting). First one to return valid data wins.

Agent Auth Required Provides
Last.fm API key + secret Bio (multi-lang), images (OG scrape), similar artists, top songs
Deezer None (public API) Bio, images (4 sizes), similar artists, top songs
ListenBrainz None Similar artists, top songs, track similarity
Local N/A Top songs from library only (always-on fallback)
WASM Plugins Varies Any of the 11 agent interfaces

Config: Agents = "lastfm,deezer" — comma-separated, order = priority.


Image Resolution Flow

Request: GET /img/{jwt-encoded-artwork-id}
                    │
                    ▼
         ┌─ Image Cache Hit? ─── Yes ──▶ Serve (10-year cache header)
         │         │
         │        No
         │         │
         ▼         ▼
    ArtistArtPriority (configurable order):
    ┌──────────────────────────────────────────┐
    │ 1. uploaded-image  ← User-uploaded file  │
    │ 2. external        ← Agent URLs (DB)     │
    │ 3. image-folder    ← ArtistImageFolder/  │
    │    (matches MBID.jpg or "Artist Name.jpg")│
    │ 4. album/artist.*  ← Album art folders   │
    │ 5. folder.*        ← Artist folder +     │
    │    up to 2 parent dirs                    │
    └──────────────────────────────────────────┘
                    │
                    ▼
              Cache result → Serve

Key config: ArtistArtPriority controls the source order. Default includes uploaded image first.


Biography Flow

UI mount (ArtistShow.jsx)
        │
        ▼
subsonic.getArtistInfo(id)
        │
        ▼
Provider.UpdateArtistInfo()
        │
        ├── Cache valid (< 24h)? → Return cached data immediately
        │
        ├── Cache expired? → Return cached + enqueue background refresh
        │
        └── Never fetched? → Synchronous fetch from agents:
                │
                ▼
            GetArtistMBID() → parallel:
              ├─ GetArtistBiography()  (HTML, sanitized)
              ├─ GetArtistImages()     (small/medium/large URLs)
              ├─ GetArtistURL()
              └─ GetSimilarArtists()
                │
                ▼
            Store in DB → Return to UI

Bio is rendered via SafeHTML (DOMPurify sanitized) in a collapsible section — 4.5em on desktop, 1.5em on mobile.


Custom Images — Yes, It's Supported

There's a full upload/delete system already built:

Upload API

Backend (server/nativeapi/artists.go):

  • POST /api/artist/{id}/image — upload (max 10MB, any image/*)
  • DELETE /api/artist/{id}/image — remove uploaded image

Frontend (ui/src/common/ImageUploadOverlay.jsx):

  • Hover over artist image → camera icon overlay appears
  • Click to upload, delete button shows when hasUploadedImage is true
  • Gated by: config.enableArtworkUpload === true OR user is admin

DB: uploaded_image field on artist table (added in migration 20260315233131).

Priority: When ArtistArtPriority has uploaded-image first (default), your custom image takes precedence over everything — external APIs, folder images, album art.

Folder-Based Custom Images

Also works without the upload UI:

  • Set ArtistImageFolder config to a directory path
  • Drop files named {MusicBrainz-ID}.jpg or {Artist Name}.jpg in that folder
  • They'll be picked up by the image-folder priority source

Key Files

Layer File Purpose
Model model/artist.go Artist struct, ArtistImageUrl() method
Agent interfaces core/agents/interfaces.go 11 interfaces agents can implement
Agent orchestration core/agents/agents.go Priority fallback iteration
Caching provider core/external/provider.go TTL cache, background refresh queue
Image reader core/artwork/reader_artist.go Source selection by ArtistArtPriority
Image sources core/artwork/sources.go Local/external/URL fetch implementations
HTTP serving server/public/handle_images.go JWT-authed image endpoint
Upload API server/nativeapi/artists.go POST/DELETE artist image
UI entry ui/src/artist/ArtistShow.jsx Artist detail page
UI desktop ui/src/artist/DesktopArtistDetails.jsx Desktop bio + image layout
UI mobile ui/src/artist/MobileArtistDetails.jsx Mobile layout
UI upload ui/src/common/ImageUploadOverlay.jsx Upload overlay component

Mermaid Diagram: Full Data Flow

flowchart TD
    subgraph External["External Agents (priority order)"]
        LFM["Last.fm"]
        DEE["Deezer"]
        LB["ListenBrainz"]
        LOCAL["Local Agent"]
        WASM["WASM Plugins"]
    end

    subgraph Backend["Backend"]
        PROV["Provider\n(core/external/provider.go)"]
        AGENTS["Agent Orchestrator\n(core/agents/agents.go)"]
        DB[("SQLite DB\n- biography\n- image URLs\n- similar_artists\n- external_info_updated_at")]
        CACHE[("File Cache\n(image_cache.go)")]
        READER["Artist Image Reader\n(reader_artist.go)"]
    end

    subgraph Sources["Image Sources (ArtistArtPriority)"]
        S1["1. uploaded-image"]
        S2["2. external (agent URLs)"]
        S3["3. image-folder"]
        S4["4. album/artist.*"]
        S5["5. folder.*"]
    end

    subgraph Frontend["React UI"]
        SHOW["ArtistShow.jsx"]
        DESK["DesktopArtistDetails"]
        MOB["MobileArtistDetails"]
        UPLOAD["ImageUploadOverlay"]
        SAFE["SafeHTML (biography)"]
    end

    SHOW -->|"subsonic.getArtistInfo(id)"| PROV
    PROV -->|"cache miss / expired"| AGENTS
    AGENTS --> LFM & DEE & LB & LOCAL & WASM
    LFM & DEE & LB & LOCAL & WASM -->|"bio, images, similar"| AGENTS
    AGENTS --> PROV
    PROV -->|"store"| DB
    PROV -->|"return artistInfo"| SHOW

    SHOW -->|"GET /img/{id}"| CACHE
    CACHE -->|"miss"| READER
    READER --> S1 & S2 & S3 & S4 & S5
    S2 -->|"fetch URL"| DB
    READER -->|"store"| CACHE
    CACHE -->|"image bytes"| SHOW

    SHOW --> DESK & MOB
    DESK & MOB --> SAFE
    DESK & MOB --> UPLOAD
    UPLOAD -->|"POST/DELETE /api/artist/{id}/image"| S1
Loading

TL;DR

Images: Agents (Last.fm/Deezer) → cached URLs in DB → served via file cache with configurable priority. Custom images supported via upload UI or ArtistImageFolder directory — both take priority over external sources by default.

Bios: Agents fetch HTML bios (multi-language fallback on Last.fm) → cached 24h in DB with async background refresh → sanitized and rendered in collapsible UI section.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment