Merge pull request #570 from FoxxMD/fixListenRange
Some checks failed
Publish Docker image to Dockerhub / test (push) Has been cancelled
Publish Docker image to Dockerhub / Build OCI Images (push) Has been cancelled
Publish Docker image to Dockerhub / Build OCI Images-1 (push) Has been cancelled
Publish Docker image to Dockerhub / Merge OCI Images and Push (push) Has been cancelled

Fix listen range
This commit is contained in:
Matt Foxx 2026-04-22 13:27:32 -04:00 committed by GitHub
commit ec4a51074e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 69 additions and 18 deletions

11
package-lock.json generated
View file

@ -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,

View file

@ -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",

View file

@ -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);

View file

@ -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';

View file

@ -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
},

View file

@ -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);

View file

@ -62,7 +62,7 @@ export function staggerMapper<Element, NewElement>(options: StaggerOptions) {
return (mapper: Mapper<Element, NewElement>) => async (x: Element, index: number) => {
if (index < concurrency) {
sleep(initialStagger);
await sleep(initialStagger);
initialStagger += initialInterval;
} else {
const s = Math.min((Math.random() * 1000), maxRandomStagger)

View file

@ -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;
}

View file

@ -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 = <T>(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});
}
}

View file

@ -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 */
@ -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',

View file

@ -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<PlayerStateDataMaybePlay,
}
export interface GeneratePlayOpts {
playDateCompleted?: boolean
playDateCompleted?: boolean,
listenRanges?: boolean
}
export const generatePlay = (data: ObjectPlayData = {}, meta: MarkOptional<PlayMeta, 'lifecycle'> = {}, 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<PlayM
play.data.playDateCompleted = play.data.playDate.add(play.data.duration)
}
if(listenRanges) {
play.data.listenRanges = [];
const lf = play.data.listenedFor;
const sessions = faker.number.int({min: 1, max: 3});
const sessionTime = lf / sessions;
let nextTime = play.data.playDate;
switch(faker.number.int({min: 1, max: 2})) {
case 1:
// timestamps only
for(let i = 0; i < sessions; i++) {
const newTime = nextTime.add(sessionTime, 's');
play.data.listenRanges.push({start: {timestamp: clone(nextTime)}, end: {timestamp: newTime}});
nextTime = newTime;
}
break;
case 2:
// timestamps + position
let position = 0;
for(let i = 0; i < sessions; i++) {
const newTime = nextTime.add(sessionTime, 's');
const nextPosition = position + sessionTime;
play.data.listenRanges.push({start: {timestamp: clone(nextTime), position}, end: {timestamp: newTime, position: nextPosition}});
nextTime = newTime;
position = nextPosition;
}
break;
}
}
return play;
}
@ -262,8 +294,8 @@ export const generatePlayPlatformId = (deviceId?: string, userId?: string): Play
return [did, uid];
}
export const generatePlays = (numberOfPlays: number, data: ObjectPlayData = {}, meta: MarkOptional<PlayMeta, 'lifecycle'> = {}): PlayObject[] => {
return Array.from(Array(numberOfPlays), () => generatePlay(data, meta));
export const generatePlays = (numberOfPlays: number, data: ObjectPlayData = {}, meta: MarkOptional<PlayMeta, 'lifecycle'> = {}, opts: GeneratePlayOpts = {}): PlayObject[] => {
return Array.from(Array(numberOfPlays), () => generatePlay(data, meta, opts));
}
export const generateArtist = () => faker.music.artist;