From 41c8187186629dc6b08d48f5554e2f44ccb34ce6 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Sun, 14 Jun 2026 00:27:30 +0200 Subject: [PATCH] fix(music-network): keep API suffix for self-hosted paste-token providers (#1085) * fix(music-network): keep API suffix for self-hosted paste-token providers The api_key_only connect strategy persisted the raw origin from the baseUrl field instead of the resolved API base, dropping the preset's selfHostedApiSuffix (e.g. /apis/listenbrainz). Scrobbles and now-playing then hit /1/submit-listens (404/405, silently unlogged) instead of /apis/listenbrainz/1/submit-listens, so nothing was recorded. Return the runtime-resolved ctx.baseUrl (origin + suffix) and fall back to the field only when it is absent. Fixes Koito and both Maloja compat surfaces (ListenBrainz and Audioscrobbler). Existing accounts must reconnect to re-persist the corrected base. * docs(changelog): self-hosted scrobble URL fix (#1085) --- CHANGELOG.md | 9 +++++++++ src/music-network/wires/shared/apiKeyOnly.test.ts | 12 +++++++++--- src/music-network/wires/shared/apiKeyOnly.ts | 7 ++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4e62259..bf64e1e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -210,6 +210,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Fixed +### Music Network — self-hosted scrobbling reaches the server + +**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1085](https://github.com/Psychotoxical/psysonic/pull/1085)** + +* Self-hosted scrobble targets that use a pasted token (Koito, Maloja's ListenBrainz and Audioscrobbler compatibility surfaces) silently recorded nothing: the saved server address dropped the API path, so listens were sent to a route that does not exist while the account still showed as connected. +* The correct API path is now kept when connecting. Reconnect an affected account once so it picks up the corrected address. + + + ### Servers — complete border on the active server card **By [@Psychotoxical](https://github.com/Psychotoxical), PR [#998](https://github.com/Psychotoxical/psysonic/pull/998)** diff --git a/src/music-network/wires/shared/apiKeyOnly.test.ts b/src/music-network/wires/shared/apiKeyOnly.test.ts index 6fcf2418..13bcc64b 100644 --- a/src/music-network/wires/shared/apiKeyOnly.test.ts +++ b/src/music-network/wires/shared/apiKeyOnly.test.ts @@ -31,11 +31,17 @@ describe('apiKeyOnlyStrategy', () => { await expect(apiKeyOnlyStrategy.connect(ctx({}))).rejects.toBeInstanceOf(MusicNetworkError); }); - it('prefers a field baseUrl over the context baseUrl', async () => { + it('returns the resolved API base (with suffix), not the raw field origin', async () => { + // The runtime passes ctx.baseUrl already resolved to origin + selfHostedApiSuffix + // (e.g. /apis/listenbrainz for Koito). The strategy must persist that, not the + // bare fields.baseUrl — otherwise scrobbles miss the /apis/listenbrainz path. const res = await apiKeyOnlyStrategy.connect( - ctx({ token: 't', baseUrl: 'https://maloja.example' }, 'https://fallback'), + ctx( + { token: 't', baseUrl: 'https://koito.example' }, + 'https://koito.example/apis/listenbrainz', + ), ); - expect(res.baseUrl).toBe('https://maloja.example'); + expect(res.baseUrl).toBe('https://koito.example/apis/listenbrainz'); }); it('falls back to the context baseUrl when no field given', async () => { diff --git a/src/music-network/wires/shared/apiKeyOnly.ts b/src/music-network/wires/shared/apiKeyOnly.ts index 2cbec801..4a798dc1 100644 --- a/src/music-network/wires/shared/apiKeyOnly.ts +++ b/src/music-network/wires/shared/apiKeyOnly.ts @@ -24,7 +24,12 @@ export const apiKeyOnlyStrategy: AuthStrategy = { providerId: ctx.presetId, }); } - const baseUrl = (ctx.fields.baseUrl ?? '').trim() || ctx.baseUrl; + // ctx.baseUrl is the resolved API base (origin + selfHostedApiSuffix, e.g. + // `/apis/listenbrainz` for Koito) built by the runtime. Prefer it over the + // raw `fields.baseUrl`, which is the bare origin without the suffix — using + // the latter sends scrobbles to `/1/submit-listens` (404/405) instead + // of `/apis/listenbrainz/1/submit-listens`. + const baseUrl = ctx.baseUrl || (ctx.fields.baseUrl ?? '').trim(); return { sessionKey: token, username: (ctx.fields.username ?? '').trim(),