fix(scrobble): Navidrome Now Playing with local playback and mixed-server queue (#1055)

* fix(scrobble): report Now Playing on playback server with local bytes

Navidrome presence and play-count scrobbles no longer skip when audio plays
from hot cache, offline pins, or favorites-auto, and reachability follows the
queue/playback server instead of the browsed active server.

* docs: changelog and credits for PR #1055
This commit is contained in:
cucadmuh
2026-06-10 04:25:22 +03:00
committed by GitHub
parent ae9be74719
commit 707a41f615
8 changed files with 131 additions and 20 deletions
+29 -2
View File
@@ -1,7 +1,8 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useAuthStore } from '../store/authStore';
import { usePlayerStore } from '../store/playerStore';
import { scrobbleSong } from './subsonicScrobble';
import { reportNowPlaying, scrobbleSong } from './subsonicScrobble';
import { shouldAttemptSubsonicForServer } from '../utils/network/subsonicNetworkGuard';
const { apiForServerMock } = vi.hoisted(() => ({
apiForServerMock: vi.fn(async () => ({})),
@@ -12,12 +13,13 @@ vi.mock('./subsonicClient', () => ({
apiForServer: apiForServerMock,
}));
vi.mock('../utils/network/subsonicNetworkGuard', () => ({
shouldAttemptSubsonicForServer: () => true,
shouldAttemptSubsonicForServer: vi.fn(() => true),
}));
describe('subsonicScrobble', () => {
beforeEach(() => {
apiForServerMock.mockClear();
vi.mocked(shouldAttemptSubsonicForServer).mockImplementation(() => true);
useAuthStore.setState({
servers: [
{ id: 'a', name: 'A', url: 'http://a.test', username: 'u', password: 'p' },
@@ -41,4 +43,29 @@ describe('subsonicScrobble', () => {
expect.objectContaining({ id: 't1', submission: true, time: 1_700_000_000_000 }),
);
});
it('reportNowPlaying and scrobbleSong use the presence guard without trackId', async () => {
vi.mocked(shouldAttemptSubsonicForServer).mockImplementation(
(_serverId: string, trackId?: string) => trackId === undefined,
);
await reportNowPlaying('t-local', 'a');
await scrobbleSong('t-local', 1_700_000_000_000, 'a');
expect(shouldAttemptSubsonicForServer).toHaveBeenCalledWith('a');
expect(shouldAttemptSubsonicForServer).not.toHaveBeenCalledWith('a', expect.anything());
expect(apiForServerMock).toHaveBeenCalledTimes(2);
expect(apiForServerMock).toHaveBeenNthCalledWith(
1,
'a',
'scrobble.view',
expect.objectContaining({ id: 't-local', submission: false }),
);
expect(apiForServerMock).toHaveBeenNthCalledWith(
2,
'a',
'scrobble.view',
expect.objectContaining({ id: 't-local', submission: true }),
);
});
});
+3 -1
View File
@@ -9,7 +9,9 @@ async function scrobbleOnServer(
submission: boolean,
time?: number,
): Promise<void> {
if (!shouldAttemptSubsonicForServer(serverId, id)) return;
// Presence / play-count updates are not playback-byte fetches — omit trackId so
// hot cache, offline library, and favorites-auto do not suppress Navidrome calls.
if (!shouldAttemptSubsonicForServer(serverId)) return;
const params: Record<string, unknown> = { id, submission };
if (time !== undefined) params.time = time;
await apiForServer(serverId, 'scrobble.view', params);