From 961dba996c9de81d43e4331656f251694c041baa Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:45:38 +0200 Subject: [PATCH] fix(discord): resolve cover profile from the store, not getPlaybackServerId MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the dual-address cover fix: a playback/active cover scope was routed through getPlaybackServerId(), which returns a `string` that can be empty or an index-key (not a profile id) for locally-cached tracks. The `?? activeServerId` fallback never fired (empty string isn't nullish), so no profile matched and the URL came back null — which the presence layer caches per cover id, producing intermittent "cover shows, then doesn't". A playback/active scope always means the active server (a cross-server track gets an explicit `server` scope), so resolve the active profile directly. (cherry picked from commit 8f93f30e6fd28e9ac64e958cf80250c32bd5e896) --- src/cover/integrations/discord.test.ts | 21 +++++++++++++++++++ src/cover/integrations/discord.ts | 28 ++++++++++++-------------- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/cover/integrations/discord.test.ts b/src/cover/integrations/discord.test.ts index 0a70e9e3..8771ad4e 100644 --- a/src/cover/integrations/discord.test.ts +++ b/src/cover/integrations/discord.test.ts @@ -45,4 +45,25 @@ describe('coverArtUrlForDiscord', () => { expect(url).toContain('music.example.com'); }); + + it('resolves a playback scope to the active profile (public address)', async () => { + // playback scope is the common case for locally-cached tracks; it must + // resolve to the active server, not yield a null cover. + const server = makeServer({ + url: 'http://192.168.1.50:4533', + alternateUrl: 'https://music.example.com', + }); + useAuthStore.setState({ servers: [server], activeServerId: server.id } as never); + + const ref: CoverArtRef = { + cacheKind: 'album', + cacheEntityId: 'al-1', + fetchCoverArtId: 'al-1', + serverScope: { kind: 'playback' }, + }; + const url = await coverArtUrlForDiscord(ref); + + expect(url).toContain('music.example.com'); + expect(url).not.toContain('192.168.1.50'); + }); }); diff --git a/src/cover/integrations/discord.ts b/src/cover/integrations/discord.ts index 909a86c1..d52bd24c 100644 --- a/src/cover/integrations/discord.ts +++ b/src/cover/integrations/discord.ts @@ -1,17 +1,7 @@ import { buildCoverArtUrlForServer } from '../../api/subsonicStreamUrl'; import { serverShareBaseUrl } from '../../utils/server/serverEndpoint'; -import { getPlaybackServerId } from '../../utils/playback/playbackServer'; import { useAuthStore } from '../../store/authStore'; -import type { CoverArtRef, CoverServerScope } from '../types'; - -/** The saved profile id that a cover scope resolves to (active/playback/server). */ -function serverIdForScope(scope: CoverServerScope): string | null { - if (scope.kind === 'server') return scope.serverId; - if (scope.kind === 'playback') { - return getPlaybackServerId() ?? useAuthStore.getState().activeServerId ?? null; - } - return useAuthStore.getState().activeServerId ?? null; -} +import type { CoverArtRef } from '../types'; /** * Discord large image — an https:// URL Discord's own servers can reach. @@ -21,13 +11,21 @@ function serverIdForScope(scope: CoverServerScope): string | null { * remotely, so a `http://192.168.x.x` address is unreachable and falls back to * the app icon. Discord is an external consumer just like a share link, so use * `serverShareBaseUrl` (public address preferred when both are set). + * + * Resolve the profile straight from the store: a `playback`/`active` scope + * always means the active server (a cross-server track gets an explicit + * `server` scope), so we never route through `getPlaybackServerId()`, whose + * empty-string / index-key returns previously yielded a null cover URL on + * locally-cached tracks. */ export async function coverArtUrlForDiscord(ref: CoverArtRef): Promise { const { serverScope, fetchCoverArtId } = ref; - const serverId = serverIdForScope(serverScope); - const profile = serverId - ? useAuthStore.getState().servers.find(s => s.id === serverId) - : undefined; + const auth = useAuthStore.getState(); + + const profile = + serverScope.kind === 'server' + ? auth.servers.find(s => s.id === serverScope.serverId) + : auth.servers.find(s => s.id === auth.activeServerId); if (profile) { return buildCoverArtUrlForServer(