Skip to content

Commit

Permalink
feat(listenbrainz): Add player support #74
Browse files Browse the repository at this point in the history
* Use listenbrainz 'playing-now' endpoint for MS player implementation
* Do not use MS player as source of truth -- continue to use LZ listen history
  • Loading branch information
FoxxMD committed Sep 18, 2023
1 parent 7dece44 commit 735264c
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions src/backend/common/schema/aio-source.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
18 changes: 18 additions & 0 deletions src/backend/common/schema/aio.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
18 changes: 18 additions & 0 deletions src/backend/common/schema/source.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 28 additions & 1 deletion src/backend/common/vendor/ListenbrainzApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -185,9 +186,30 @@ export class ListenbrainzApiClient extends AbstractApiClient {
}
}

getPlayingNow = async (user?: string): Promise<ListensResponse> => {
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<PlayObject[]> => {
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}`);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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])
}
}
}
Expand Down
15 changes: 13 additions & 2 deletions src/backend/sources/ListenbrainzSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
}
1 change: 0 additions & 1 deletion src/backend/sources/SubsonicSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 735264c

Please sign in to comment.