From 981805aa96c8ede36017d763784711034c823f9d Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Wed, 22 Apr 2026 13:40:00 +0000 Subject: [PATCH 1/4] fix: Simplify listenrange usage * Use and return only plain PlayProgress on player state finish * Removes the need to serialize/deserialize ListenProgress class and compare/persist toad scheduler or timeout functions * Potentially fixes timeout comparison stack error #569 --- .../PlayerState/AbstractPlayerState.ts | 2 +- src/backend/tests/cache/cache.test.ts | 2 +- src/backend/utils/CacheUtils.ts | 7 ++-- src/core/Atomic.ts | 4 +- src/core/PlayTestUtils.ts | 40 +++++++++++++++++-- 5 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/backend/sources/PlayerState/AbstractPlayerState.ts b/src/backend/sources/PlayerState/AbstractPlayerState.ts index a5dc045f..5a065587 100644 --- a/src/backend/sources/PlayerState/AbstractPlayerState.ts +++ b/src/backend/sources/PlayerState/AbstractPlayerState.ts @@ -298,7 +298,7 @@ export abstract class AbstractPlayerState { ...this.currentPlay.data, playDate: this.playFirstSeenAt, listenedFor: this.getListenDuration(), - listenRanges: ranges, + listenRanges: ranges.map(x => ({start: x.start, end: x.end})), playDateCompleted: completed ? dayjs() : undefined, repeat: this.isRepeatPlay }, diff --git a/src/backend/tests/cache/cache.test.ts b/src/backend/tests/cache/cache.test.ts index f481c4ec..4eb0b8d2 100644 --- a/src/backend/tests/cache/cache.test.ts +++ b/src/backend/tests/cache/cache.test.ts @@ -166,7 +166,7 @@ describe('#Caching', function () { await using test = new TestScrobbler(); await test.initialize(); - const plays = generatePlays(100); + const plays = generatePlays(100, {}, {}, {listenRanges: true}); await test.queueScrobble(plays, 'testSource'); const queued = test.queuedScrobbles.map(x => x.play); await sleep(101); diff --git a/src/backend/utils/CacheUtils.ts b/src/backend/utils/CacheUtils.ts index be0d4c64..26afc541 100644 --- a/src/backend/utils/CacheUtils.ts +++ b/src/backend/utils/CacheUtils.ts @@ -20,10 +20,9 @@ export const rehydratePlay = (obj: AmbPlayObject): PlayObject => { if(obj.data.playDateCompleted !== undefined) { obj.data.playDateCompleted = dayjs(obj.data.playDateCompleted); } - - if(obj.data.listenRanges !== undefined) { - obj.data.listenRanges = obj.data.listenRanges.map(rehydrateListenRangeData); - } + } + if(obj.data.listenRanges !== undefined) { + obj.data.listenRanges = obj.data.listenRanges.map(rehydrateListenRangeData); } return obj as PlayObject; } diff --git a/src/core/Atomic.ts b/src/core/Atomic.ts index af4a7b6c..41da04ef 100644 --- a/src/core/Atomic.ts +++ b/src/core/Atomic.ts @@ -107,8 +107,8 @@ export interface ListenRangeDataAmb { } export interface ListenRangeData extends ListenRangeDataAmb { - start: ListenProgress - end: ListenProgress + start: PlayProgress + end: PlayProgress } /** https://musicbrainz.org/doc/MusicBrainz_Database/Schema#Overview */ diff --git a/src/core/PlayTestUtils.ts b/src/core/PlayTestUtils.ts index 97fef11c..22f18ba6 100644 --- a/src/core/PlayTestUtils.ts +++ b/src/core/PlayTestUtils.ts @@ -16,6 +16,7 @@ import { nanoid } from 'nanoid'; import { LastFMTrackObject } from '../backend/common/vendor/LastfmApiClient.js'; import { MarkOptional } from 'ts-essentials'; import { defaultLifecycle } from '../backend/utils/PlayTransformUtils.js'; +import clone from 'clone'; dayjs.extend(utc) dayjs.extend(isBetween); @@ -161,11 +162,13 @@ export const generatePlayerStateData = (options: Omit = {}, opts: GeneratePlayOpts = {}): PlayObject => { const { - playDateCompleted = false + playDateCompleted = false, + listenRanges = false, } = opts; const duration = faker.number.int({min: 30, max: 300}); @@ -201,6 +204,35 @@ export const generatePlay = (data: ObjectPlayData = {}, meta: MarkOptional = {}): PlayObject[] => { - return Array.from(Array(numberOfPlays), () => generatePlay(data, meta)); +export const generatePlays = (numberOfPlays: number, data: ObjectPlayData = {}, meta: MarkOptional = {}, opts: GeneratePlayOpts = {}): PlayObject[] => { + return Array.from(Array(numberOfPlays), () => generatePlay(data, meta, opts)); } export const generateArtist = () => faker.music.artist; From 4caf1c8cdb0c2fd4f3fe9930c595951f252ab3b7 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Wed, 22 Apr 2026 13:40:29 +0000 Subject: [PATCH 2/4] fix: Fix missing await on stagger mapper initial sleep --- src/backend/index.ts | 2 +- src/backend/utils/AsyncUtils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/index.ts b/src/backend/index.ts index b8b48fa6..37a4dae7 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -18,7 +18,7 @@ import { parseVersion } from "./version.js"; import { initServer } from "./server/index.js"; import { createHeartbeatClientsTask } from "./tasks/heartbeatClients.js"; import { createHeartbeatSourcesTask } from "./tasks/heartbeatSources.js"; -import { isDebugMode, parseBool, retry, sleep } from "./utils.js"; +import { isDebugMode, parseBool, retry } from "./utils.js"; import { readJson } from './utils/DataUtils.js'; //import { createVegaGenerator } from './utils/SchemaUtils.js'; import ScrobbleClients from './scrobblers/ScrobbleClients.js'; diff --git a/src/backend/utils/AsyncUtils.ts b/src/backend/utils/AsyncUtils.ts index 949ad901..02bc77ad 100644 --- a/src/backend/utils/AsyncUtils.ts +++ b/src/backend/utils/AsyncUtils.ts @@ -62,7 +62,7 @@ export function staggerMapper(options: StaggerOptions) { return (mapper: Mapper) => async (x: Element, index: number) => { if (index < concurrency) { - sleep(initialStagger); + await sleep(initialStagger); initialStagger += initialInterval; } else { const s = Math.min((Math.random() * 1000), maxRandomStagger) From 77387c292bf3e4fff27f3d9531b6a6cb93196d0b Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Wed, 22 Apr 2026 13:43:14 +0000 Subject: [PATCH 3/4] refactor: Replace fast-deep-equal with fast-equals fast-deep-equal is abandoned and has bugs that are fixed in fast-equals Potentially fixes #569 --- package-lock.json | 11 ++++++++++- package.json | 2 +- src/backend/common/AbstractComponent.ts | 4 ++-- src/backend/utils/DataUtils.ts | 9 +++++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 33c0cab2..0ac33fd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,7 @@ "dotenv": "^10.0.0", "express": "^5.2.1", "express-session": "^1.19.0", - "fast-deep-equal": "^3.1.3", + "fast-equals": "^6.0.0", "fixed-size-list": "^0.3.0", "flat-cache": "^6.1.13", "formidable": "^3.5", @@ -8253,6 +8253,15 @@ "version": "3.1.3", "license": "MIT" }, + "node_modules/fast-equals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-6.0.0.tgz", + "integrity": "sha512-PFhhIGgdM79r5Uztdj9Zb6Tt1zKafqVfdMGwVca1z5z6fbX7DmsySSuJd8HiP6I1j505DCS83cLxo5rmSNeVEA==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.2", "dev": true, diff --git a/package.json b/package.json index 327fd45d..747885d2 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "dotenv": "^10.0.0", "express": "^5.2.1", "express-session": "^1.19.0", - "fast-deep-equal": "^3.1.3", + "fast-equals": "^6.0.0", "fixed-size-list": "^0.3.0", "flat-cache": "^6.1.13", "formidable": "^3.5", diff --git a/src/backend/common/AbstractComponent.ts b/src/backend/common/AbstractComponent.ts index eef9b8e1..167497d3 100644 --- a/src/backend/common/AbstractComponent.ts +++ b/src/backend/common/AbstractComponent.ts @@ -2,7 +2,6 @@ import { childLogger, Logger } from "@foxxmd/logging"; import { cacheFunctions, } from "@foxxmd/regex-buddy-core"; -import deepEqual from 'fast-deep-equal'; import { LifecycleStep, PlayData, PlayObject, TransformResult } from "../../core/Atomic.js"; import { buildPlayHumanDiffable, buildTrackString } from "../../core/StringUtils.js"; import { CommonClientConfig } from "./infrastructure/config/client/index.js"; @@ -27,6 +26,7 @@ import { MSCache } from "./Cache.js"; import { diffObjects, diffObjectsConsoleOutput, patchObject } from "../../core/DataUtils.js"; import clone from "clone"; import { loggerNoop } from "./MaybeLogger.js"; +import { objectsEqual } from "../utils/DataUtils.js"; export type AbstractComponentConfig = (CommonClientConfig | CommonSourceConfig) & { transformManager?: TransformerManager }; @@ -406,7 +406,7 @@ export default abstract class AbstractComponent extends AbstractInitializable { } else { step.flowResult = onSuccess; - if (!deepEqual(playTruth.data, newTransformedPlay.data)) { + if (!objectsEqual(playTruth.data, newTransformedPlay.data)) { const o = JSON.parse(JSON.stringify(playTruth.data)); const t = JSON.parse(JSON.stringify(newTransformedPlay.data)); const patch = diffObjects(o, t); // jdiff.diff(o, t); diff --git a/src/backend/utils/DataUtils.ts b/src/backend/utils/DataUtils.ts index 57f2a9eb..176f1adb 100644 --- a/src/backend/utils/DataUtils.ts +++ b/src/backend/utils/DataUtils.ts @@ -1,6 +1,7 @@ import JSON5 from "json5"; import { constants, promises } from "fs"; import { MaybeLogger } from '../common/MaybeLogger.js'; +import { deepEqual } from 'fast-equals'; export const asArray = (data: T | T[]): T[] => { if (Array.isArray(data)) { @@ -166,4 +167,12 @@ export const shuffleArray = (array: any[]): void => { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } +} + +export const objectsEqual = (a: object, b: object) => { + try { + return deepEqual(a, b); + } catch (e) { + throw new Error('Could not compare objects', {cause: e}); + } } \ No newline at end of file From 5a4bcb2949b2416276fe1cb3a81fc6b1c9d7f199 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Wed, 22 Apr 2026 13:46:38 +0000 Subject: [PATCH 4/4] fix(discord): Include known spotify cdn for media providers #569 --- src/core/Atomic.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/Atomic.ts b/src/core/Atomic.ts index 41da04ef..8c88925b 100644 --- a/src/core/Atomic.ts +++ b/src/core/Atomic.ts @@ -554,6 +554,8 @@ export interface TransformResult { export const KNOWN_MEDIA_PROVIDER_URLS = [ 'spotify.com', +// spotify cdn +'scdn.co', 'bandcamp.com', 'youtube.com', 'deezer.com',