From f3531f41ec22842f2264eaf93050eda9815e3f0b Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Tue, 9 Jun 2026 16:39:00 +0000 Subject: [PATCH] feat: Add creation timestamps to lifecycle data --- src/backend/common/AbstractComponent.ts | 3 ++- src/backend/common/vendor/LastfmApiClient.ts | 2 +- src/backend/common/vendor/ListenbrainzApiClient.ts | 2 +- src/backend/common/vendor/RockSkyApiClient.ts | 4 ++-- src/backend/common/vendor/koito/KoitoApiClient.ts | 2 +- src/backend/common/vendor/maloja/MalojaApiClient.ts | 2 +- src/backend/common/vendor/teal/TealApiClient.ts | 4 ++-- src/backend/scrobblers/DiscordScrobbler.ts | 4 ++-- src/backend/tests/scrobbler/TestScrobbler.ts | 3 ++- src/backend/utils/PlayComparisonUtils.ts | 4 +++- src/core/Atomic.ts | 3 +++ src/core/tests/utils/fixtures.ts | 5 +++-- 12 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/backend/common/AbstractComponent.ts b/src/backend/common/AbstractComponent.ts index 3d0da933..b074f758 100644 --- a/src/backend/common/AbstractComponent.ts +++ b/src/backend/common/AbstractComponent.ts @@ -290,7 +290,7 @@ export default abstract class AbstractComponent extends AbstractInitializable { } for(const s of cachedSteps) { - steps.push({...s, cached: true}); + steps.push({...s, cached: true, createdAt: dayjs().toISOString()}); if(shouldTransform && s.patch !== undefined) { transformedPlay.data = patchObject(transformedPlay.data, s.patch); // jdiff.patch(clone(transformedPlay.data),s.patch); } @@ -417,6 +417,7 @@ export default abstract class AbstractComponent extends AbstractInitializable { const step: LifecycleStep = existingStepIndex !== -1 && playTruth.meta.lifecycle.steps[existingStepIndex] !== undefined ? playTruth.meta.lifecycle.steps[existingStepIndex] : { name: stepName, source: this.getIdentifier(), + createdAt: dayjs().toString() } let newTransformedPlay: PlayObject, diff --git a/src/backend/common/vendor/LastfmApiClient.ts b/src/backend/common/vendor/LastfmApiClient.ts index c9a4178f..5637c0a9 100644 --- a/src/backend/common/vendor/LastfmApiClient.ts +++ b/src/backend/common/vendor/LastfmApiClient.ts @@ -484,7 +484,7 @@ export default class LastfmApiClient extends AbstractApiClient implements Pagina modifiedPlay.data.album = albumName; } - return {payload: scrobblePayload, response, mergedScrobble: modifiedPlay}; + return {payload: scrobblePayload, response, mergedScrobble: modifiedPlay, createdAt: dayjs().toISOString()}; // last fm has rate limits but i can't find a specific example of what that limit is. going to default to 1 scrobble/sec to be safe //await sleep(1000); } catch (e) { diff --git a/src/backend/common/vendor/ListenbrainzApiClient.ts b/src/backend/common/vendor/ListenbrainzApiClient.ts index e20aa980..b65d6f6e 100644 --- a/src/backend/common/vendor/ListenbrainzApiClient.ts +++ b/src/backend/common/vendor/ListenbrainzApiClient.ts @@ -228,7 +228,7 @@ export class ListenbrainzApiClient extends AbstractApiClient implements Pageless if(log) { this.logger.debug(`Submit Response: ${resp.text}`) } - return {payload: listenPayload, response: resp.text}; + return {payload: listenPayload, response: resp.text, createdAt: dayjs().toISOString()}; } catch (e) { throw new ScrobbleSubmitError(`Failed to submit to Listenbrainz (listen_type ${listenPayload.listen_type})`, {cause: e, payload: listenPayload, response: e.response, responseBody: e.response?.text}); } diff --git a/src/backend/common/vendor/RockSkyApiClient.ts b/src/backend/common/vendor/RockSkyApiClient.ts index cf2aa437..c5774422 100644 --- a/src/backend/common/vendor/RockSkyApiClient.ts +++ b/src/backend/common/vendor/RockSkyApiClient.ts @@ -225,7 +225,7 @@ export class RockSkyApiClient extends AbstractApiClient { if(log) { this.logger.debug(`Submit Response: ${resp.text}`) } - return {payload: submitPayload, response: resp.body as SubmitResponse}; + return {payload: submitPayload, response: resp.body as SubmitResponse, createdAt: dayjs().toISOString()}; } catch (e) { throw new ScrobbleSubmitError(`Error occurred while making Rocksky API scrobble (${listenType}) request`, {cause: e, payload: submitPayload}); } @@ -235,7 +235,7 @@ export class RockSkyApiClient extends AbstractApiClient { this.logger.debug(`Submit Payload: ${JSON.stringify(payload)}`); } const resp = await this.rsClient.scrobble.createScrobble(playToRockskyRecord(play)); - return {payload, response: resp} + return {payload, response: resp, createdAt: dayjs().toISOString()} } } diff --git a/src/backend/common/vendor/koito/KoitoApiClient.ts b/src/backend/common/vendor/koito/KoitoApiClient.ts index 8dfc2020..99b9bab5 100644 --- a/src/backend/common/vendor/koito/KoitoApiClient.ts +++ b/src/backend/common/vendor/koito/KoitoApiClient.ts @@ -207,7 +207,7 @@ export class KoitoApiClient extends AbstractApiClient implements PaginatedTimeRa if (log) { this.logger.debug(`Submit Response: ${resp.text}`) } - return {payload: listenPayload, response: resp.text}; + return {payload: listenPayload, response: resp.text, createdAt: dayjs().toISOString()}; } catch (e) { throw new ScrobbleSubmitError(`Error occurred while making Koito API submit request (listen_type ${listenPayload.listen_type})`, {cause: e, payload: listenPayload, response: e.response, responseBody: e.response?.text}); } diff --git a/src/backend/common/vendor/maloja/MalojaApiClient.ts b/src/backend/common/vendor/maloja/MalojaApiClient.ts index 19186c74..0499bbe6 100644 --- a/src/backend/common/vendor/maloja/MalojaApiClient.ts +++ b/src/backend/common/vendor/maloja/MalojaApiClient.ts @@ -294,7 +294,7 @@ export class MalojaApiClient extends AbstractApiClient implements PaginatedTimeR throw new ScrobbleSubmitError(buildMalojaErrorString(response.body), { showStopper: false, payload: scrobbleData, response, responseBody }); } - return {payload: scrobbleData, warnings: msWarnings.length > 0 ? msWarnings : undefined, response: responseBody, mergedScrobble: scrobbleResponse !== undefined ? formatPlayObj(scrobbleResponse, {url: this.url.normal}) : undefined}; + return {createdAt: dayjs().toISOString(), payload: scrobbleData, warnings: msWarnings.length > 0 ? msWarnings : undefined, response: responseBody, mergedScrobble: scrobbleResponse !== undefined ? formatPlayObj(scrobbleResponse, {url: this.url.normal}) : undefined}; } catch (e) { let scrobbleError: ScrobbleSubmitError; if(e instanceof ScrobbleSubmitError) { diff --git a/src/backend/common/vendor/teal/TealApiClient.ts b/src/backend/common/vendor/teal/TealApiClient.ts index 3300bfa4..12cb644a 100644 --- a/src/backend/common/vendor/teal/TealApiClient.ts +++ b/src/backend/common/vendor/teal/TealApiClient.ts @@ -53,7 +53,7 @@ export class TealApiClient extends AbstractApiClient implements PagelessTimeRang input, params: {} }); - return {payload: input, response: res.data}; + return {payload: input, response: res.data, createdAt: dayjs().toISOString()}; } catch (e) { throw new ScrobbleSubmitError(`Failed to create record for scrobble`, { cause: e, payload: input, response: 'response' in e ? e.response : undefined }); } @@ -71,7 +71,7 @@ export class TealApiClient extends AbstractApiClient implements PagelessTimeRang input, params: {} }); - return {payload: input, response: res.data}; + return {payload: input, response: res.data, createdAt: dayjs().toISOString()}; } catch (e) { throw new ScrobbleSubmitError(`Failed to update status record for scrobble`, { cause: e, payload: input, response: 'response' in e ? e.response : undefined }); } diff --git a/src/backend/scrobblers/DiscordScrobbler.ts b/src/backend/scrobblers/DiscordScrobbler.ts index b2b287ca..065d6f17 100644 --- a/src/backend/scrobblers/DiscordScrobbler.ts +++ b/src/backend/scrobblers/DiscordScrobbler.ts @@ -124,7 +124,7 @@ export default class DiscordScrobbler extends AbstractScrobbleClient { return []; } - alreadyScrobbled = async (playObj: PlayObject, log = false): Promise<[boolean, PlayMatchResult]> => ([false, {match: false, breakdowns: [], score: 0}]) + alreadyScrobbled = async (playObj: PlayObject, log = false): Promise<[boolean, PlayMatchResult]> => ([false, {match: false, breakdowns: [], score: 0, createdAt: dayjs().toISOString()}]) public playToClientPayload(playObj: PlayObject): any { return playStateToActivityData({ @@ -142,7 +142,7 @@ export default class DiscordScrobbler extends AbstractScrobbleClient { } doScrobble = async (playObj: PlayObject) => { - return { play: playObj, payload: {} }; + return { play: playObj, payload: {}, createdAt: dayjs().toISOString() }; } doPlayingNow = async (data: SourcePlayerObj) => { diff --git a/src/backend/tests/scrobbler/TestScrobbler.ts b/src/backend/tests/scrobbler/TestScrobbler.ts index 60eeaea7..9e0c4d15 100644 --- a/src/backend/tests/scrobbler/TestScrobbler.ts +++ b/src/backend/tests/scrobbler/TestScrobbler.ts @@ -11,6 +11,7 @@ import { DrizzlePlayRepository, RepositoryCreatePlayOpts } from "../../common/da import { DrizzleQueueRepository } from "../../common/database/drizzle/repositories/QueueRepository.js"; import { PlaySelect } from "../../common/database/drizzle/drizzleTypes.js"; import { loggerDebug } from "@foxxmd/logging"; +import dayjs from "dayjs"; export class TestScrobbler extends AbstractScrobbleClient { @@ -34,7 +35,7 @@ export class TestScrobbler extends AbstractScrobbleClient { } doScrobble(playObj: PlayObject) { - return Promise.resolve({payload: {}, mergedScrobble: clone(playObj, true)}); + return Promise.resolve({payload: {}, mergedScrobble: clone(playObj, true), createdAt: dayjs().toISOString()}); } protected async doParseCache() { diff --git a/src/backend/utils/PlayComparisonUtils.ts b/src/backend/utils/PlayComparisonUtils.ts index 784baab6..e54b6cf0 100644 --- a/src/backend/utils/PlayComparisonUtils.ts +++ b/src/backend/utils/PlayComparisonUtils.ts @@ -12,6 +12,7 @@ import { Logger } from "@foxxmd/logging"; import { loggerNoop } from '../common/MaybeLogger.js'; import { lifecyclelessInvariantTransform } from "../../core/PlayUtils.js"; import { findAsyncSequential } from "./AsyncUtils.js"; +import dayjs from "dayjs"; export const metaInvariantTransform = (play: PlayObject): PlayObjectLifecycleless => { @@ -443,7 +444,8 @@ export const existingScrobble = async (playObjPre: PlayObject, existingScrobbles match: false, score: 0, breakdowns: [], - reason: 'No existing scrobble matched with a score higher than 0' + reason: 'No existing scrobble matched with a score higher than 0', + createdAt: dayjs().toISOString() }; const playObj = await transformPlay(playObjPre, TRANSFORM_HOOK.candidate); diff --git a/src/core/Atomic.ts b/src/core/Atomic.ts index 67aef9c3..1e7560e7 100644 --- a/src/core/Atomic.ts +++ b/src/core/Atomic.ts @@ -328,6 +328,7 @@ export interface LifecycleStep { error?: ErrorLike patch?: Changeset inputs?: LifecycleInput[] + createdAt: string } export type ScrobblePayload = object | string; @@ -338,6 +339,7 @@ export interface ScrobbleActionResult { response?: ScrobbleResponse, mergedScrobble?: AmbPlayObject warnings?: string[] + createdAt: string } export interface PlayMatchResult { @@ -348,6 +350,7 @@ export interface PlayMatchResult { closestMatchedPlay?: PlayObjectLifecycleless transformedPlay?: PlayObjectLifecycleless summary?: String + createdAt: string } export type ScrobbleTsSOC = 1 | 2; diff --git a/src/core/tests/utils/fixtures.ts b/src/core/tests/utils/fixtures.ts index 50b2bf57..82e89fcf 100644 --- a/src/core/tests/utils/fixtures.ts +++ b/src/core/tests/utils/fixtures.ts @@ -10,7 +10,7 @@ import { existingScrobble } from '../../../backend/utils/PlayComparisonUtils.js' import { UpstreamError } from '../../../backend/common/errors/UpstreamError.js'; import { playToListenPayload } from '../../../backend/common/vendor/listenbrainz/lzUtils.js'; import { mergeSimpleError, SimpleError, SkipTransformStageError, StagePrerequisiteError } from '../../../backend/common/errors/MSErrors.js'; -import { Dayjs } from 'dayjs'; +import dayjs, { Dayjs } from 'dayjs'; export interface ScrobbleMatchOptions { match?: boolean @@ -175,7 +175,8 @@ export const generateLifecycleStep = (play: PlayObject, opts: GenerateLifecycleO name, source, flowResult: 'continue', - inputs + inputs, + createdAt: dayjs().toISOString() } if (equal) { -- 2.51.2