From 37df94bd3b5b30405c5c8d018a1bcd7b89eca0a3 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Mon, 25 May 2026 11:56:00 -0700 Subject: [PATCH] add playbackReport handler to Subsonic controller --- .../api/subsonic/subsonic-controller.ts | 55 +++++++++++++++++++ src/shared/types/domain-types.ts | 2 + 2 files changed, 57 insertions(+) diff --git a/src/renderer/api/subsonic/subsonic-controller.ts b/src/renderer/api/subsonic/subsonic-controller.ts index 3aead3db9..a781c98a1 100644 --- a/src/renderer/api/subsonic/subsonic-controller.ts +++ b/src/renderer/api/subsonic/subsonic-controller.ts @@ -1460,6 +1460,10 @@ export const SubsonicController: InternalControllerEndpoint = { features.serverPlayQueue = [1]; } + if (subsonicFeatures[SubsonicExtensions.PLAYBACK_REPORT]) { + features.reportPlayback = [1]; + } + return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion }; }, getSimilarSongs: async (args) => { @@ -2298,6 +2302,57 @@ export const SubsonicController: InternalControllerEndpoint = { scrobble: async (args) => { const { apiClientProps, query } = args; + if (hasFeature(apiClientProps.server, ServerFeature.REPORT_PLAYBACK)) { + if (query.submission) { + const res = await ssApiClient(apiClientProps).scrobble({ + query: { + id: query.id, + submission: query.submission, + }, + }); + + if (res.status !== 200) { + throw new Error('Failed to scrobble'); + } + + return null; + } + + let state: 'paused' | 'playing' | 'starting' | 'stopped' = 'playing'; + + switch (query.event) { + case 'pause': + state = 'paused'; + break; + case 'start': + state = 'starting'; + break; + case 'timeupdate': + case 'unpause': + state = 'playing'; + break; + default: + state = 'playing'; + } + + const res = await ssApiClient(apiClientProps).reportPlayback({ + query: { + ignoreScrobble: true, + mediaId: query.id, + mediaType: query.mediaType, + playbackRate: query.playbackRate, + positionMs: query.position ?? 0, + state, + }, + }); + + if (res.status !== 200) { + throw new Error('Failed to report playback'); + } + + return null; + } + const res = await ssApiClient(apiClientProps).scrobble({ query: { id: query.id, diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts index a8d5f6200..ca4277b31 100644 --- a/src/shared/types/domain-types.ts +++ b/src/shared/types/domain-types.ts @@ -1365,6 +1365,8 @@ export type ScrobbleQuery = { albumId?: string; event?: 'pause' | 'start' | 'timeupdate' | 'unpause'; id: string; + mediaType: 'podcast' | 'song'; + playbackRate: number; position?: number; submission: boolean; };