diff --git a/src/backend/common/infrastructure/config/source/listenbrainz.ts b/src/backend/common/infrastructure/config/source/listenbrainz.ts index b8bdd3a7..1e7ffff5 100644 --- a/src/backend/common/infrastructure/config/source/listenbrainz.ts +++ b/src/backend/common/infrastructure/config/source/listenbrainz.ts @@ -1,7 +1,8 @@ import { CommonSourceConfig, CommonSourceData } from "./index"; import { ListenBrainzData } from "../client/listenbrainz"; +import {PollingOptions} from "../common.js"; -export interface ListenBrainzSourceData extends ListenBrainzData, CommonSourceData { +export interface ListenBrainzSourceData extends ListenBrainzData, CommonSourceData, PollingOptions { } export interface ListenBrainzSourceConfig extends CommonSourceConfig { diff --git a/src/backend/common/schema/aio-source.json b/src/backend/common/schema/aio-source.json index 0fc5aec4..3bc52375 100644 --- a/src/backend/common/schema/aio-source.json +++ b/src/backend/common/schema/aio-source.json @@ -759,6 +759,24 @@ }, "ListenBrainzSourceData": { "properties": { + "interval": { + "default": 10, + "description": "How long to wait before polling the source API for new tracks (in seconds)", + "examples": [ + 10 + ], + "title": "interval", + "type": "number" + }, + "maxInterval": { + "default": 30, + "description": "When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)", + "examples": [ + 30 + ], + "title": "maxInterval", + "type": "number" + }, "maxPollRetries": { "default": 5, "description": "default # of automatic polling restarts on error", diff --git a/src/backend/common/schema/aio.json b/src/backend/common/schema/aio.json index 94ec3e58..29ccb45a 100644 --- a/src/backend/common/schema/aio.json +++ b/src/backend/common/schema/aio.json @@ -1119,6 +1119,24 @@ }, "ListenBrainzSourceData": { "properties": { + "interval": { + "default": 10, + "description": "How long to wait before polling the source API for new tracks (in seconds)", + "examples": [ + 10 + ], + "title": "interval", + "type": "number" + }, + "maxInterval": { + "default": 30, + "description": "When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)", + "examples": [ + 30 + ], + "title": "maxInterval", + "type": "number" + }, "maxPollRetries": { "default": 5, "description": "default # of automatic polling restarts on error", diff --git a/src/backend/common/schema/source.json b/src/backend/common/schema/source.json index ba054e3d..25b45d72 100644 --- a/src/backend/common/schema/source.json +++ b/src/backend/common/schema/source.json @@ -752,6 +752,24 @@ }, "ListenBrainzSourceData": { "properties": { + "interval": { + "default": 10, + "description": "How long to wait before polling the source API for new tracks (in seconds)", + "examples": [ + 10 + ], + "title": "interval", + "type": "number" + }, + "maxInterval": { + "default": 30, + "description": "When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)", + "examples": [ + 30 + ], + "title": "maxInterval", + "type": "number" + }, "maxPollRetries": { "default": 5, "description": "default # of automatic polling restarts on error", diff --git a/src/backend/common/vendor/ListenbrainzApiClient.ts b/src/backend/common/vendor/ListenbrainzApiClient.ts index b593f42e..173621ca 100644 --- a/src/backend/common/vendor/ListenbrainzApiClient.ts +++ b/src/backend/common/vendor/ListenbrainzApiClient.ts @@ -5,6 +5,7 @@ import {DEFAULT_RETRY_MULTIPLIER, DELIMITERS, FormatPlayObjectOptions} from "../ import dayjs from "dayjs"; import { stringSameness } from '@foxxmd/string-sameness'; import { + combinePartsToString, containsDelimiters, findDelimiters, normalizeStr, @@ -185,9 +186,30 @@ export class ListenbrainzApiClient extends AbstractApiClient { } } + getPlayingNow = async (user?: string): Promise => { + try { + + const resp = await this.callApi(request + .get(`${this.url}1/user/${user ?? this.config.username}/playing-now`) + // this endpoint can take forever, sometimes, and we want to make sure we timeout in a reasonable amount of time for polling sources to continue trying to scrobble + .timeout({ + response: 15000, // wait 15 seconds before timeout if server doesn't response at all + deadline: 30000 // wait 30 seconds overall for request to complete + })); + const {body: {payload}} = resp as any; + // const data = payload as ListensResponse; + // if(data.listens.length > 0) {} + // return data.listens[0]; + return payload as ListensResponse; + } catch (e) { + throw e; + } + } + getRecentlyPlayed = async (maxTracks: number, user?: string): Promise => { try { const resp = await this.getUserListens(maxTracks, user); + const now = await this.getPlayingNow(user); return resp.listens.map(x => ListenbrainzApiClient.listenResponseToPlay(x)); } catch (e) { this.logger.error(`Error encountered while getting User listens | Error => ${e.message}`); @@ -464,6 +486,10 @@ export class ListenbrainzApiClient extends AbstractApiClient { recording_mbid: aRecordingMbid, duration: aDuration, duration_ms: aDurationMs, + music_service_name, + music_service, + submission_client, + submission_client_version } = {}, mbid_mapping: { recording_mbid: mRecordingMbid @@ -509,7 +535,8 @@ export class ListenbrainzApiClient extends AbstractApiClient { meta: { source: 'listenbrainz', trackId, - playId + playId, + deviceId: combinePartsToString([music_service_name ?? music_service, submission_client, submission_client_version]) } } } diff --git a/src/backend/sources/ListenbrainzSource.ts b/src/backend/sources/ListenbrainzSource.ts index fd928936..e2c6e586 100644 --- a/src/backend/sources/ListenbrainzSource.ts +++ b/src/backend/sources/ListenbrainzSource.ts @@ -3,8 +3,9 @@ import { FormatPlayObjectOptions, INITIALIZING, InternalConfig } from "../common import EventEmitter from "events"; import { ListenBrainzSourceConfig } from "../common/infrastructure/config/source/listenbrainz"; import { ListenbrainzApiClient } from "../common/vendor/ListenbrainzApiClient"; +import MemorySource from "./MemorySource.js"; -export default class ListenbrainzSource extends AbstractSource { +export default class ListenbrainzSource extends MemorySource { api: ListenbrainzApiClient; requiresAuth = true; @@ -13,9 +14,17 @@ export default class ListenbrainzSource extends AbstractSource { declare config: ListenBrainzSourceConfig; constructor(name: any, config: ListenBrainzSourceConfig, internal: InternalConfig, emitter: EventEmitter) { - super('listenbrainz', name, config, internal, emitter); + const { + data: { + interval = 15, + maxInterval = 60, + ...restData + } = {} + } = config; + super('listenbrainz', name, {...config, data: {interval, maxInterval, ...restData}}, internal, emitter); this.canPoll = true; this.api = new ListenbrainzApiClient(name, config.data); + this.playerSourceOfTruth = false; } static formatPlayObj = (obj: any, options: FormatPlayObjectOptions = {}) => ListenbrainzApiClient.formatPlayObj(obj, options); @@ -52,6 +61,8 @@ export default class ListenbrainzSource extends AbstractSource { getRecentlyPlayed = async(options: RecentlyPlayedOptions = {}) => { const {limit = 20} = options; + const now = await this.api.getPlayingNow(); + this.processRecentPlays(now.listens.map(x => ListenbrainzSource.formatPlayObj(x))); return await this.api.getRecentlyPlayed(limit); } } diff --git a/src/backend/sources/SubsonicSource.ts b/src/backend/sources/SubsonicSource.ts index ccc81dea..1eb16c1e 100644 --- a/src/backend/sources/SubsonicSource.ts +++ b/src/backend/sources/SubsonicSource.ts @@ -21,7 +21,6 @@ export class SubsonicSource extends MemorySource { declare config: SubSonicSourceConfig; constructor(name: any, config: SubSonicSourceConfig, internal: InternalConfig, emitter: EventEmitter) { - // default to quick interval so we can get a decently accurate nowPlaying const { data: { ...restData