fix(discord): resolve cover profile from the store, not getPlaybackServerId

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 8f93f30e6f)
This commit is contained in:
Psychotoxical
2026-06-16 16:45:38 +02:00
committed by cucadmuh
parent 4fd558fa28
commit 961dba996c
2 changed files with 34 additions and 15 deletions
+21
View File
@@ -45,4 +45,25 @@ describe('coverArtUrlForDiscord', () => {
expect(url).toContain('music.example.com'); 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');
});
}); });
+13 -15
View File
@@ -1,17 +1,7 @@
import { buildCoverArtUrlForServer } from '../../api/subsonicStreamUrl'; import { buildCoverArtUrlForServer } from '../../api/subsonicStreamUrl';
import { serverShareBaseUrl } from '../../utils/server/serverEndpoint'; import { serverShareBaseUrl } from '../../utils/server/serverEndpoint';
import { getPlaybackServerId } from '../../utils/playback/playbackServer';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '../../store/authStore';
import type { CoverArtRef, CoverServerScope } from '../types'; import type { CoverArtRef } 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;
}
/** /**
* Discord large image — an https:// URL Discord's own servers can reach. * 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 * 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 * the app icon. Discord is an external consumer just like a share link, so use
* `serverShareBaseUrl` (public address preferred when both are set). * `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<string | null> { export async function coverArtUrlForDiscord(ref: CoverArtRef): Promise<string | null> {
const { serverScope, fetchCoverArtId } = ref; const { serverScope, fetchCoverArtId } = ref;
const serverId = serverIdForScope(serverScope); const auth = useAuthStore.getState();
const profile = serverId
? useAuthStore.getState().servers.find(s => s.id === serverId) const profile =
: undefined; serverScope.kind === 'server'
? auth.servers.find(s => s.id === serverScope.serverId)
: auth.servers.find(s => s.id === auth.activeServerId);
if (profile) { if (profile) {
return buildCoverArtUrlForServer( return buildCoverArtUrlForServer(