From 045890e243bbffcbc9e0198182fc319b97ef8871 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Wed, 5 Aug 2026 19:18:21 +0000 Subject: [PATCH] feat(sourc): Add Ingress SOT type and force-push ingress player updates #659 --- src/backend/sources/EndpointLastfmSource.ts | 4 ++-- src/backend/sources/EndpointListenbrainzSource.ts | 4 ++-- src/backend/sources/MemorySource.ts | 15 ++++++++++++++- src/backend/sources/WebScrobblerSource.ts | 4 ++-- src/backend/utils/PlayComparisonUtils.ts | 4 ++-- src/client/components/player/Player.tsx | 2 +- src/core/Atomic.ts | 11 ++++++----- 7 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/backend/sources/EndpointLastfmSource.ts b/src/backend/sources/EndpointLastfmSource.ts index c363e5c3..cba73725 100644 --- a/src/backend/sources/EndpointLastfmSource.ts +++ b/src/backend/sources/EndpointLastfmSource.ts @@ -29,7 +29,7 @@ export class EndpointLastfmSource extends MemorySource { constructor(name: any, config: LastFMEndpointSourceConfig, internal: InternalConfig, emitter: EventEmitter) { super('endpointlfm', name, config, internal, emitter); this.multiPlatform = false; - this.playerSourceOfTruth = SOURCE_SOT.HISTORY; + this.playerSourceOfTruth = SOURCE_SOT.INGRESS; const { data = {}, @@ -95,7 +95,7 @@ export class EndpointLastfmSource extends MemorySource { export const playStateFromRequest = (obj: Record): PlayerStateData[] => ingressPayloads(obj).map(x => { const play = scrobblePayloadToPlay(x); - play.meta.sourceSOT = SOURCE_SOT.HISTORY; + play.meta.sourceSOT = SOURCE_SOT.INGRESS; return { platformId: [play.meta.deviceId, NO_USER], play, diff --git a/src/backend/sources/EndpointListenbrainzSource.ts b/src/backend/sources/EndpointListenbrainzSource.ts index 44a22c5f..27eebeab 100644 --- a/src/backend/sources/EndpointListenbrainzSource.ts +++ b/src/backend/sources/EndpointListenbrainzSource.ts @@ -32,7 +32,7 @@ export class EndpointListenbrainzSource extends MemorySource { constructor(name: any, config: ListenbrainzEndpointSourceConfig, internal: InternalConfig, emitter: EventEmitter) { super('endpointlz', name, config, internal, emitter); this.multiPlatform = false; - this.playerSourceOfTruth = SOURCE_SOT.HISTORY; + this.playerSourceOfTruth = SOURCE_SOT.INGRESS; const { data = {}, @@ -127,7 +127,7 @@ export const playStateFromRequest = (obj: SubmitPayload): PlayerStateData[] => { const playStates: PlayerStateData[] = payload.map((x) => { const play = listenPayloadToPlay(x, listen_type === 'playing_now'); - play.meta.sourceSOT = SOURCE_SOT.HISTORY; + play.meta.sourceSOT = SOURCE_SOT.INGRESS; return { platformId: [play.meta.deviceId, NO_USER], play, diff --git a/src/backend/sources/MemorySource.ts b/src/backend/sources/MemorySource.ts index d601e086..41b920fb 100644 --- a/src/backend/sources/MemorySource.ts +++ b/src/backend/sources/MemorySource.ts @@ -60,7 +60,7 @@ export default class MemorySource extends AbstractSource { // player cleanup on *schedule* is needed when the Source is non-polling (ingress) // because if the source stops sending updates then processRecentPlays() was never called so we never remove old players - this.scheduler.addSimpleIntervalJob(new SimpleIntervalJob({ seconds: 15 }, new AsyncTask('Player Cleanup', (): Promise => { + this.scheduler.addSimpleIntervalJob(new SimpleIntervalJob({ seconds: 10 }, new AsyncTask('Player Cleanup', (): Promise => { if (this.canPoll) { return Promise.resolve(); } @@ -70,6 +70,19 @@ export default class MemorySource extends AbstractSource { .process(async (key) => { await this.cleanupPlayer(key); + if(this.playerSourceOfTruth === SOURCE_SOT.INGRESS) { + const player = this.players.get(key); + if(![CALCULATED_PLAYER_STATUSES.stale, CALCULATED_PLAYER_STATUSES.orphaned].includes(player.calculatedStatus)) { + // if player isn't stale and this is an ingress source then we want to keep pushing playerUpdates + // so that any downstream Clients can update Now Playing in a timely manner + this.emitEvent('playerUpdate', { + ...player.getApiState(), + options: { + scrobbleTo: this.clients + } + }); + } + } }); }))); } diff --git a/src/backend/sources/WebScrobblerSource.ts b/src/backend/sources/WebScrobblerSource.ts index 2ea3a087..ba1ba464 100644 --- a/src/backend/sources/WebScrobblerSource.ts +++ b/src/backend/sources/WebScrobblerSource.ts @@ -34,7 +34,7 @@ export class WebScrobblerSource extends MemorySource { constructor(name: any, config: WebScrobblerSourceConfig, internal: InternalConfig, emitter: EventEmitter) { super('webscrobbler', name, config, internal, emitter); this.multiPlatform = true; - this.playerSourceOfTruth = SOURCE_SOT.HISTORY; + this.playerSourceOfTruth = SOURCE_SOT.INGRESS; this.logger.info(`Note: The player for this source is an analogue for the 'Now Playing' status exposed by ${this.type} which is NOT used for scrobbling. Instead, the 'recently played' or 'history' information provided by this source is used for scrobbles.`) const { @@ -88,7 +88,7 @@ export class WebScrobblerSource extends MemorySource { } = obj; const play = WebScrobblerSource.formatPlayObj(obj.data.song, {nowPlaying: eventName !== 'scrobble'}); - play.meta.sourceSOT = SOURCE_SOT.HISTORY; + play.meta.sourceSOT = SOURCE_SOT.INGRESS; return { platformId: [play.meta.deviceId, NO_USER], play, diff --git a/src/backend/utils/PlayComparisonUtils.ts b/src/backend/utils/PlayComparisonUtils.ts index 8473e647..5d2c3e5e 100644 --- a/src/backend/utils/PlayComparisonUtils.ts +++ b/src/backend/utils/PlayComparisonUtils.ts @@ -1,5 +1,5 @@ import { getListDiff, type ListDiff } from "@donedeal0/superdiff"; -import { type PlayMatchResult, type PlayObject, type PlayObjectMinimal, SOURCE_SOT, TA_DURING, TA_EXACT, TA_FUZZY, type TemporalAccuracy, type TrackStringOptions } from "../../core/Atomic.ts"; +import { type PlayMatchResult, type PlayObject, type PlayObjectMinimal, SOURCE_SOT, type SOURCE_SOT_TYPES, TA_DURING, TA_EXACT, TA_FUZZY, type TemporalAccuracy, type TrackStringOptions } from "../../core/Atomic.ts"; import { buildTrackString, capitalize, truncateStringToLength } from "../../core/StringUtils.ts"; import { comparingMultipleArtists, playObjDataMatch, setIntersection } from "../utils.ts"; import { comparePlayTemporally, hasAcceptableTemporalAccuracy, temporalAccuracyToString, type TemporalPlayComparisonOptions, temporalPlayComparisonSummary } from "./TimeUtils.ts"; @@ -496,7 +496,7 @@ export const existingScrobble = async (playObjPre: PlayObject, existingScrobbles // // OR if play was generated from a source that uses History (endpoint sources, lfm or lz history sources) // then we can be reasonably sure that our candidate play has an accurate timestamp and wouldn't fuzzy match a previous scrobble - const looseTimeAccuracy = playObj.data.repeat || playObj.meta.sourceSOT === SOURCE_SOT.HISTORY ? [TA_DURING] : [TA_FUZZY, TA_DURING]; + const looseTimeAccuracy = playObj.data.repeat || ([SOURCE_SOT.HISTORY, SOURCE_SOT.INGRESS] as SOURCE_SOT_TYPES[]).includes(playObj.meta.sourceSOT) ? [TA_DURING] : [TA_FUZZY, TA_DURING]; existingScrobble = await findAsyncSequential(existingScrobbles, async (xPre) => { diff --git a/src/client/components/player/Player.tsx b/src/client/components/player/Player.tsx index 44fe5a22..e21502f3 100644 --- a/src/client/components/player/Player.tsx +++ b/src/client/components/player/Player.tsx @@ -83,7 +83,7 @@ art = {}, return (
- {sot === SOURCE_SOT.HISTORY ? diff --git a/src/core/Atomic.ts b/src/core/Atomic.ts index b8c9e3c6..7ef1611e 100644 --- a/src/core/Atomic.ts +++ b/src/core/Atomic.ts @@ -469,12 +469,13 @@ export interface TemporalPlayComparison { } | { type: 'none' } } -export type SOURCE_SOT_TYPES = 'player' | 'history'; +export type SOURCE_SOT_TYPES = 'player' | 'history' | 'ingress'; export const SOURCE_SOT = { - PLAYER : 'player' as SOURCE_SOT_TYPES, - HISTORY: 'history' as SOURCE_SOT_TYPES -} -export const sourceSotTypes: SOURCE_SOT_TYPES[] = ['player','history']; + PLAYER : 'player', + HISTORY: 'history', + INGRESS: 'ingress' +} as const satisfies Record +export const sourceSotTypes: SOURCE_SOT_TYPES[] = ['player','history','ingress']; export interface URLData { url: URL -- 2.51.2