Skip to content

feat(music): add ListenBrainz provider #3123

Open
Crow-Control wants to merge 2 commits into
seerr-team:musicfrom
Crow-Control:feat/listenbrainz-provider
Open

feat(music): add ListenBrainz provider #3123
Crow-Control wants to merge 2 commits into
seerr-team:musicfrom
Crow-Control:feat/listenbrainz-provider

Conversation

@Crow-Control

@Crow-Control Crow-Control commented Jun 5, 2026

Copy link
Copy Markdown

Description

Adds configurable ListenBrainz metadata provider clients along with the admin settings routes, UI and OpenAPI documentation so their API surface, rate limiting and configuration can be validated before any music features consume them.

These are explicitly chosen, because they needed significant work I terms of GUI exposure and featureset, compare to the original "music support" PR. combined with the fact that these metadata providers are fully stand-alone capable, so dont rely on extensive database migrations and intregrations.

Yet Musicbrains builds the core of any future music feature

Added:

  • New ListenBrainz Metadata Provider
  • Extensive configurability for both self-hosting as well as api-keyed access
  • GUI elements for both providers
  • Docs for the Metadata Providers
  • New unit tests for both

Dependencies:

  • Adds dompurify, jsdom and @types/jsdom for HTML sanitization of the Wikipedia extract from MusicBrains

AI used:

  • Claude Opus 4.7 for splitting this of from the original PR
  • Claude Opus 4.7 for unittest pre-generation
  • Claude Opus 4.7 for initial documentation-writing
  • Copilot-auto as review tool
    (design of expanded feature-set and GUI structure done manually and iteratively)

How Has This Been Tested?

  • Linting
  • Unittesting
  • pnpm build
  • pnpm dev
  • prettier
  • Manual UX/GUI testing
  • Testing through the GUI

Screenshots / Logs (if applicable)

Screenshot 2026-06-03 at 22 19 41 Screenshot 2026-06-03 at 22 19 48 Screenshot 2026-06-03 at 22 19 55

Checklist:

  • I have read and followed the contribution guidelines.
  • Disclosed any use of AI (see our policy)
  • I have updated the documentation accordingly.
  • All new and existing tests passed.
  • Successful build pnpm build
  • Translation keys pnpm i18n:extract
  • Database migration (if required) -> Not required

Summary by CodeRabbit

Release Notes

  • New Features

    • Added music metadata provider support with ListenBrainz integration
    • Added Settings section to configure music metadata provider credentials and URLs
    • Added ability to test music metadata provider connectivity
  • Documentation

    • Added comprehensive guidance on metadata provider configuration and usage

Copilot AI review requested due to automatic review settings June 5, 2026 14:59
@Crow-Control Crow-Control requested a review from a team as a code owner June 5, 2026 14:59
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 67f03b27-a239-4799-a57c-cb11026ec2e2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces ListenBrainz music metadata provider support to Seerr. It adds an API client with metadata/statistics queries, backend settings routes with SSRF-safe validation, a React settings component with configuration forms, and complete user documentation and localization.

Changes

Music Metadata Provider Integration

Layer / File(s) Summary
ListenBrainz API Client & Response Types
server/api/listenbrainz/index.ts, server/api/listenbrainz/interfaces.ts, server/api/listenbrainz/listenbrainz.test.ts
Implements ListenBrainzAPI extending ExternalAPI with URL normalization, metadata queries (getAlbum, getArtist), statistics methods (getTopAlbums, getTopArtists, getFreshReleases), and 20 TypeScript interfaces modeling ListenBrainz API responses. Includes comprehensive test coverage for URL handling, query methods, error wrapping, constructor overrides, and web URL builders.
Cache & Settings Infrastructure
server/lib/cache.ts, server/lib/settings/index.ts
Adds listenbrainz cache with 6-hour TTL. Introduces ListenBrainzSettings and MusicMetadataSettings types to MetadataSettings, initializes defaults with ListenBrainz endpoints and empty user token, and exposes getter/setter accessors for persistence.
Backend Routes & API Endpoints
server/routes/settings/index.ts, server/routes/settings/musicMetadata.ts, server/routes/settings/musicMetadata.test.ts
Wires /music-metadata routes with GET (returns settings + defaults), PUT (merge/validate/persist), and POST /test (validate URLs then test via getFreshReleases). Includes SSRF protection for base URLs and full route test suite covering defaults, merging, validation failures, error handling, and fallback logic.
OpenAPI Schema Definitions
seerr-api.yml
Defines ListenBrainzSettings, MusicMetadataSettings, and MusicMetadataTestResult schemas; adds GET/PUT /settings/music-metadata and POST /settings/music-metadata/test endpoints with request/response contracts.
Frontend Settings Component
src/components/Settings/SettingsMetadata.tsx
Extends SettingsMetadata with SWR-backed music metadata loading. Refactors testConnection to test both metadata and music providers in parallel, centralizes toast messaging, and adds a new ListenBrainz configuration form section with apiBaseUrl, webBaseUrl, and userToken (via SensitiveInput) fields, plus integrated "Test" and "Save" buttons. Updates provider status rendering with new listenbrainz badge row and adjusts label widths for existing providers.
Documentation & Internationalization
docs/using-seerr/settings/metadata.md, src/i18n/locale/en.json
Adds "Metadata Providers" user guide covering provider selection, ListenBrainz configuration rules (API/web URL normalization, optional token, self-hosted setup), and Save/Test behavior. Adds English i18n keys for API/web base URL labels, ListenBrainz provider name, music metadata configuration title/description, user token label, and save success/failure messages.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SettingsUI as Settings UI
  participant API as Backend Routes
  participant Settings as Settings Store
  participant LB as ListenBrainzAPI
  
  User->>SettingsUI: Load settings page
  SettingsUI->>API: GET /settings/music-metadata
  API->>Settings: get musicMetadata
  Settings-->>API: {listenbrainz: {...}}
  API-->>SettingsUI: 200 + settings
  SettingsUI-->>User: Show form with current config
  
  User->>SettingsUI: Enter API base URL<br/>and click Test
  SettingsUI->>API: POST /settings/music-metadata/test<br/>{apiBaseUrl: "...", ...}
  API->>API: validateBaseUrl(apiBaseUrl)
  API->>LB: new ListenBrainzAPI(candidateSettings)
  LB->>LB: getFreshReleases()
  alt test success
    LB-->>API: response
    API-->>SettingsUI: 200 + {listenbrainz: success}
  else test failure
    LB-->>API: error
    API-->>SettingsUI: 500 + {listenbrainz: failed}
  end
  SettingsUI-->>User: Update status badge
  
  User->>SettingsUI: Click Save
  SettingsUI->>API: PUT /settings/music-metadata<br/>{apiBaseUrl, webBaseUrl, userToken}
  API->>API: validateBaseUrl + merge with saved
  API->>Settings: set musicMetadata
  Settings-->>API: persisted
  API-->>SettingsUI: 200 + updated settings
  SettingsUI-->>User: Show success toast
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • fallenbagel
  • gauthier-th
  • M0NsTeRRR

🐰 A music client hops into view,
With fresh releases and artists too,
ListenBrainz now sings its part,
Metadata flows, a crafted art! 🎵

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(music): add ListenBrainz provider' directly and accurately summarizes the main change—adding a new ListenBrainz metadata provider with configuration, routing, and UI support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds ListenBrainz as a configurable music metadata provider, including UI configuration, server-side settings storage/routes, and API client support.

Changes:

  • Introduces ListenBrainz API client + cache entry and exposes music-metadata settings in server settings.
  • Adds Settings UI for configuring/testing ListenBrainz (API base URL, web base URL, user token) and expands provider status badges/toasts.
  • Documents and specifies the new endpoints in docs and OpenAPI, plus adds route and API client tests.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/i18n/locale/en.json Adds i18n strings for ListenBrainz/music metadata settings UI.
src/components/Settings/SettingsMetadata.tsx Adds ListenBrainz status + a new “music metadata” configuration form and test logic.
server/routes/settings/musicMetadata.ts New Express routes for reading/updating/testing music metadata settings with URL sanitization.
server/routes/settings/musicMetadata.test.ts Tests for the new music metadata settings routes (GET/PUT/test).
server/routes/settings/index.ts Wires the new /settings/music-metadata router.
server/lib/settings/index.ts Adds persisted musicMetadata settings (ListenBrainz config) with defaults and accessors.
server/lib/cache.ts Adds a dedicated cache bucket for ListenBrainz.
server/api/listenbrainz/* Implements ListenBrainz client + interfaces + tests.
seerr-api.yml Adds OpenAPI schemas and endpoints for music-metadata settings and test.
docs/using-seerr/settings/metadata.md Documents the new metadata providers page behavior and ListenBrainz configuration.
package.json Formatting-only change (final brace alignment).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +19 to +36
function sanitizeProviderBaseUrl(rawUrl: string, field: string): string {
let parsed: URL;
try {
parsed = new URL(rawUrl);
} catch {
throw new Error(`Invalid URL for ${field}`);
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
throw new Error(`Unsupported protocol for ${field}: ${parsed.protocol}`);
}
if (!parsed.hostname) {
throw new Error(`Missing hostname for ${field}`);
}
if (parsed.username || parsed.password) {
throw new Error(`Embedded credentials are not allowed for ${field}`);
}
return parsed.toString().replace(/\/+$/, '');
}
Comment on lines +12 to +18
/**
* Validate a user-supplied base URL before it is used to construct an
* outbound HTTP client. This is the boundary check that prevents SSRF via
* exotic protocol handlers, embedded credentials, or malformed inputs.
* Returns the canonical URL string on success and throws an Error with a
* human-readable message on failure.
*/
Comment on lines +119 to +121
const { data: musicData, mutate: mutateMusic } =
useSWR<MusicMetadataSettings>('/api/v1/settings/music-metadata');

Comment on lines +521 to +522
{!musicInitialValues && <LoadingSpinner />}
{musicInitialValues && (
Comment thread seerr-api.yml
Comment on lines +2706 to +2716
put:
summary: Update music metadata provider settings
description: Updates ListenBrainz settings. Partial updates are merged with the current values.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MusicMetadataSettings'

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
server/routes/settings/musicMetadata.test.ts (1)

108-171: ⚡ Quick win

Add negative tests for the URL validation boundary.

The suite validates happy paths and upstream failures, but it doesn’t lock the SSRF guard behavior (malformed URL, unsupported protocol, embedded credentials) for PUT / and POST /test. Adding these cases will prevent silent regressions in the boundary checks.

Suggested test additions
+it('rejects unsupported protocols on PUT / with 400', async () => {
+  const res = await request(app).put('/').send({
+    listenbrainz: {
+      apiBaseUrl: 'file:///etc/passwd',
+      webBaseUrl: 'https://listenbrainz.org',
+      userToken: '',
+    },
+  });
+  assert.equal(res.status, 400);
+});
+
+it('rejects embedded credentials on POST /test with 400 and not tested', async () => {
+  const res = await request(app).post('/test').send({
+    listenbrainz: {
+      apiBaseUrl: 'https://user:pass@example.com',
+      webBaseUrl: 'https://listenbrainz.org',
+      userToken: '',
+    },
+  });
+  assert.equal(res.status, 400);
+  assert.equal(res.body.tests.listenbrainz, 'not tested');
+});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/routes/settings/musicMetadata.test.ts` around lines 108 - 171, Add
negative tests that assert the SSRF/URL validation for both POST /test and PUT /
rejects malformed URLs, unsupported protocols (e.g. ftp://), and URLs with
embedded credentials; for each case send candidate config bodies with the bad
base URLs and verify the server returns a validation error status (4xx) and
marks the provider as failed, and also assert no external requests were made by
checking captureExternalAPI's getCalls is empty for those requests. Locate the
test suite in musicMetadata.test.ts (the describe 'POST /test' block) and add
new it() cases that reuse captureExternalAPI/getCalls/getImpl patterns and the
same endpoints ('/test' and the PUT '/') to ensure the SSRF guard behavior is
enforced and prevents outbound calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@seerr-api.yml`:
- Around line 544-549: The OpenAPI schemas MusicMetadataSettings and
MusicMetadataTestResult currently only reference ListenBrainzSettings; add
support for MusicBrainz by adding a musicbrainz property that $ref's the
existing (or new) MusicBrainzSettings schema and ensure both schemas reflect the
multi-provider design (e.g., include both listenbrainz and musicbrainz
properties in MusicMetadataSettings, and include corresponding result fields or
a provider-specific object in MusicMetadataTestResult). Update occurrences
mentioned (around lines 555-561 and 2695-2709) to match this pattern and
reference the unique schema names MusicMetadataSettings,
MusicMetadataTestResult, ListenBrainzSettings, and MusicBrainzSettings so
generated clients see both providers.

In `@server/api/listenbrainz/index.ts`:
- Around line 19-23: The resolver functions (e.g., resolveListenBrainzApiUrl)
currently check apiBaseUrl truthiness without trimming, so strings like "   "
bypass the default; update resolveListenBrainzApiUrl (and the similar resolver
around lines 35-36) to trim whitespace from apiBaseUrl first (e.g., const input
= (apiBaseUrl || '').trim()), then use the trimmed value when deciding to fall
back to 'https://api.listenbrainz.org' and when stripping trailing slashes;
ensure all subsequent normalization operates on the trimmed variable.

In `@server/routes/settings/musicMetadata.ts`:
- Around line 38-47: validateMusicMetadataUrls currently calls
sanitizeProviderBaseUrl but throws away the returned canonicalized strings;
change it to capture and assign the sanitized results back into
settings.listenbrainz.apiBaseUrl and settings.listenbrainz.webBaseUrl so the
normalized values are the ones persisted. Specifically, replace the two calls
inside validateMusicMetadataUrls to set settings.listenbrainz.apiBaseUrl =
sanitizeProviderBaseUrl(... ) and settings.listenbrainz.webBaseUrl =
sanitizeProviderBaseUrl(...), ensuring the saved configuration uses these
canonicalized values.

In `@src/components/Settings/SettingsMetadata.tsx`:
- Around line 119-121: The component is only reading musicData from useSWR and
shows <LoadingSpinner /> while missing, causing an endless spinner on fetch
failure; update the useSWR call in SettingsMetadata.tsx to also destructure the
error (e.g., const { data: musicData, error: musicError, mutate: mutateMusic } =
useSWR(...)) and then change the render logic around where <LoadingSpinner /> is
returned (symbols: musicData, musicError, mutateMusic, LoadingSpinner) to show a
user-visible error state (message + retry action that calls mutateMusic) when
musicError exists instead of the spinner; keep showing LoadingSpinner only when
no error and no data.

---

Nitpick comments:
In `@server/routes/settings/musicMetadata.test.ts`:
- Around line 108-171: Add negative tests that assert the SSRF/URL validation
for both POST /test and PUT / rejects malformed URLs, unsupported protocols
(e.g. ftp://), and URLs with embedded credentials; for each case send candidate
config bodies with the bad base URLs and verify the server returns a validation
error status (4xx) and marks the provider as failed, and also assert no external
requests were made by checking captureExternalAPI's getCalls is empty for those
requests. Locate the test suite in musicMetadata.test.ts (the describe 'POST
/test' block) and add new it() cases that reuse
captureExternalAPI/getCalls/getImpl patterns and the same endpoints ('/test' and
the PUT '/') to ensure the SSRF guard behavior is enforced and prevents outbound
calls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4f7355ab-8f30-42f5-9f19-4fc5cbbf054d

📥 Commits

Reviewing files that changed from the base of the PR and between 0a305f6 and 4612c00.

📒 Files selected for processing (13)
  • docs/using-seerr/settings/metadata.md
  • package.json
  • seerr-api.yml
  • server/api/listenbrainz/index.ts
  • server/api/listenbrainz/interfaces.ts
  • server/api/listenbrainz/listenbrainz.test.ts
  • server/lib/cache.ts
  • server/lib/settings/index.ts
  • server/routes/settings/index.ts
  • server/routes/settings/musicMetadata.test.ts
  • server/routes/settings/musicMetadata.ts
  • src/components/Settings/SettingsMetadata.tsx
  • src/i18n/locale/en.json

Comment thread seerr-api.yml
Comment on lines +544 to +549
MusicMetadataSettings:
type: object
properties:
listenbrainz:
$ref: '#/components/schemas/ListenBrainzSettings'
MusicMetadataTestResult:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

OpenAPI contract is missing MusicBrainz in music metadata schemas.

MusicMetadataSettings and MusicMetadataTestResult currently expose only listenbrainz, but this feature set is documented and implemented as multi-provider (MusicBrainz + ListenBrainz). This creates a contract break for API consumers and generated clients.

Proposed schema adjustment
+    MusicBrainzSettings:
+      type: object
+      properties:
+        baseUrl:
+          type: string
+          example: 'https://musicbrainz.org'
+        authToken:
+          type: string
+          example: ''
+        maxRPS:
+          type: number
+          example: 1
     MusicMetadataSettings:
       type: object
       properties:
+        musicbrainz:
+          $ref: '`#/components/schemas/MusicBrainzSettings`'
         listenbrainz:
           $ref: '`#/components/schemas/ListenBrainzSettings`'
     MusicMetadataTestResult:
       type: object
       properties:
         success:
           type: boolean
           example: true
         tests:
           type: object
           properties:
+            musicbrainz:
+              type: string
+              enum: [ok, failed, 'not tested']
+              example: 'ok'
             listenbrainz:
               type: string
               enum: [ok, failed, 'not tested']
               example: 'ok'

Also applies to: 555-561, 2695-2709

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@seerr-api.yml` around lines 544 - 549, The OpenAPI schemas
MusicMetadataSettings and MusicMetadataTestResult currently only reference
ListenBrainzSettings; add support for MusicBrainz by adding a musicbrainz
property that $ref's the existing (or new) MusicBrainzSettings schema and ensure
both schemas reflect the multi-provider design (e.g., include both listenbrainz
and musicbrainz properties in MusicMetadataSettings, and include corresponding
result fields or a provider-specific object in MusicMetadataTestResult). Update
occurrences mentioned (around lines 555-561 and 2695-2709) to match this pattern
and reference the unique schema names MusicMetadataSettings,
MusicMetadataTestResult, ListenBrainzSettings, and MusicBrainzSettings so
generated clients see both providers.

Comment on lines +19 to +23
export const resolveListenBrainzApiUrl = (apiBaseUrl: string): string => {
const trimmed = (apiBaseUrl || 'https://api.listenbrainz.org').replace(
/\/+$/,
''
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Trim whitespace before URL fallback/normalization.

' ' is truthy, so these resolvers currently produce invalid URLs (e.g. ' /1') instead of falling back to defaults.

Proposed fix
 export const resolveListenBrainzApiUrl = (apiBaseUrl: string): string => {
-  const trimmed = (apiBaseUrl || 'https://api.listenbrainz.org').replace(
+  const normalized = (apiBaseUrl ?? '').trim();
+  const trimmed = (normalized || 'https://api.listenbrainz.org').replace(
     /\/+$/,
     ''
   );
@@
 export const resolveListenBrainzWebUrl = (webBaseUrl: string): string => {
-  return (webBaseUrl || 'https://listenbrainz.org').replace(/\/+$/, '');
+  const normalized = (webBaseUrl ?? '').trim();
+  return (normalized || 'https://listenbrainz.org').replace(/\/+$/, '');
 };

Also applies to: 35-36

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api/listenbrainz/index.ts` around lines 19 - 23, The resolver
functions (e.g., resolveListenBrainzApiUrl) currently check apiBaseUrl
truthiness without trimming, so strings like "   " bypass the default; update
resolveListenBrainzApiUrl (and the similar resolver around lines 35-36) to trim
whitespace from apiBaseUrl first (e.g., const input = (apiBaseUrl ||
'').trim()), then use the trimmed value when deciding to fall back to
'https://api.listenbrainz.org' and when stripping trailing slashes; ensure all
subsequent normalization operates on the trimmed variable.

Comment on lines +38 to +47
function validateMusicMetadataUrls(settings: MusicMetadataSettings): void {
sanitizeProviderBaseUrl(
settings.listenbrainz.apiBaseUrl,
'listenbrainz.apiBaseUrl'
);
sanitizeProviderBaseUrl(
settings.listenbrainz.webBaseUrl,
'listenbrainz.webBaseUrl'
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Persist canonicalized URL values after validation.

Line 90 validates URLs, but the canonicalized values returned by sanitizeProviderBaseUrl() are discarded, and Line 98 saves unsanitized input. That defeats the normalization step (e.g., trailing-slash trimming) and can leave stored config non-canonical.

Suggested fix
-function validateMusicMetadataUrls(settings: MusicMetadataSettings): void {
-  sanitizeProviderBaseUrl(
-    settings.listenbrainz.apiBaseUrl,
-    'listenbrainz.apiBaseUrl'
-  );
-  sanitizeProviderBaseUrl(
-    settings.listenbrainz.webBaseUrl,
-    'listenbrainz.webBaseUrl'
-  );
+function sanitizeMusicMetadataUrls(
+  settings: MusicMetadataSettings
+): MusicMetadataSettings {
+  return {
+    listenbrainz: {
+      ...settings.listenbrainz,
+      apiBaseUrl: sanitizeProviderBaseUrl(
+        settings.listenbrainz.apiBaseUrl,
+        'listenbrainz.apiBaseUrl'
+      ),
+      webBaseUrl: sanitizeProviderBaseUrl(
+        settings.listenbrainz.webBaseUrl,
+        'listenbrainz.webBaseUrl'
+      ),
+    },
+  };
 }
@@
-  const updated = mergeCandidateSettings(settings.musicMetadata, body);
+  const candidate = mergeCandidateSettings(settings.musicMetadata, body);
+  let updated: MusicMetadataSettings;
 
   try {
-    validateMusicMetadataUrls(updated);
+    updated = sanitizeMusicMetadataUrls(candidate);
   } catch (e) {
     return res.status(400).json({
       success: false,
       message: e instanceof Error ? e.message : 'Invalid music metadata URL',
     });
   }

Also applies to: 87-101

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/routes/settings/musicMetadata.ts` around lines 38 - 47,
validateMusicMetadataUrls currently calls sanitizeProviderBaseUrl but throws
away the returned canonicalized strings; change it to capture and assign the
sanitized results back into settings.listenbrainz.apiBaseUrl and
settings.listenbrainz.webBaseUrl so the normalized values are the ones
persisted. Specifically, replace the two calls inside validateMusicMetadataUrls
to set settings.listenbrainz.apiBaseUrl = sanitizeProviderBaseUrl(... ) and
settings.listenbrainz.webBaseUrl = sanitizeProviderBaseUrl(...), ensuring the
saved configuration uses these canonicalized values.

Comment on lines +119 to +121
const { data: musicData, mutate: mutateMusic } =
useSWR<MusicMetadataSettings>('/api/v1/settings/music-metadata');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle music settings fetch errors instead of showing an endless spinner.

Line 119 only reads musicData, and Line 521 shows <LoadingSpinner /> whenever it’s missing. If /api/v1/settings/music-metadata fails, this section can stay in a perpetual loading state with no user-visible error.

Suggested fix
-  const { data: musicData, mutate: mutateMusic } =
+  const { data: musicData, error: musicError, mutate: mutateMusic } =
     useSWR<MusicMetadataSettings>('/api/v1/settings/music-metadata');
@@
-        {!musicInitialValues && <LoadingSpinner />}
-        {musicInitialValues && (
+        {!musicError && !musicInitialValues && <LoadingSpinner />}
+        {musicError && (
+          <p className="text-sm text-red-400">
+            {intl.formatMessage(messages.connectionTestFailed)}
+          </p>
+        )}
+        {!musicError && musicInitialValues && (
           <Formik

Also applies to: 521-523

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Settings/SettingsMetadata.tsx` around lines 119 - 121, The
component is only reading musicData from useSWR and shows <LoadingSpinner />
while missing, causing an endless spinner on fetch failure; update the useSWR
call in SettingsMetadata.tsx to also destructure the error (e.g., const { data:
musicData, error: musicError, mutate: mutateMusic } = useSWR(...)) and then
change the render logic around where <LoadingSpinner /> is returned (symbols:
musicData, musicError, mutateMusic, LoadingSpinner) to show a user-visible error
state (message + retry action that calls mutateMusic) when musicError exists
instead of the spinner; keep showing LoadingSpinner only when no error and no
data.

@Crow-Control Crow-Control changed the base branch from develop to music June 5, 2026 15:15
days: days.toString(),
sort,
offset: offset.toString(),
count: count.toString(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like getFreshReleases() exposes parameters that the upstream endpoint does not support. I tested the API directly: count=1&offset=999999 still returned the full result set, while sort=confidence returned a 400 because that sort value is not valid for this endpoint. Could we restrict these options to the documented parameters and handle any result limiting locally if needed?

Comment on lines +20 to +23
const trimmed = (apiBaseUrl || 'https://api.listenbrainz.org').replace(
/\/+$/,
''
);
* the public site when no value is configured).
*/
export const resolveListenBrainzWebUrl = (webBaseUrl: string): string => {
return (webBaseUrl || 'https://listenbrainz.org').replace(/\/+$/, '');
`expected a request against the candidate ListenBrainz baseURL, got ${JSON.stringify(baseURLs)}`
);
assert.ok(
!baseURLs.includes('https://saved-api.example/1'),

assert.equal(res.status, 200);
const baseURLs = getCalls.map((c) => c.baseURL);
assert.ok(baseURLs.includes('https://saved-api.example/1'));
@M0NsTeRRR M0NsTeRRR linked an issue Jun 8, 2026 that may be closed by this pull request
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.

@github-actions github-actions Bot added merge conflict Cannot merge due to merge conflicts and removed merge conflict Cannot merge due to merge conflicts labels Jun 9, 2026
Adds first-class support for music metadata providers:

- New MusicBrainz and ListenBrainz API clients with caching and
  per-provider URL sanitization (http(s) only, hostname required,
  no embedded credentials).
- Persisted musicMetadata settings (baseUrl, authToken, maxRPS for
  MusicBrainz; apiBaseUrl, webBaseUrl, userToken for ListenBrainz).
  userAgent is stripped from GET/PUT responses.
- New /settings/music-metadata GET/PUT routes and POST /test endpoint
  that merges candidate body with persisted settings + defaults before
  testing connectivity.
- Settings UI with combined connectivity test and shared
  toastProviderFailures helper. maxRPS uses codebase numeric input
  idiom (text + inputMode + .short class) and is coerced to an integer
  before submit/test.
- Unit tests for both clients (including override-settings behavior
  verified against axios baseURL) and route tests for sanitization,
  candidate-config merging, and per-provider failure reporting.
- OpenAPI spec, end-user docs (Metadata Providers), and i18n strings.
@Crow-Control Crow-Control force-pushed the feat/listenbrainz-provider branch from 019bfc2 to d243845 Compare June 9, 2026 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Music / Audio support

4 participants