diff --git a/src/backend/common/infrastructure/Atomic.ts b/src/backend/common/infrastructure/Atomic.ts index ca81e8dd..d53a1a62 100644 --- a/src/backend/common/infrastructure/Atomic.ts +++ b/src/backend/common/infrastructure/Atomic.ts @@ -94,7 +94,8 @@ export interface PlayerStateDataMaybePlay { play?: PlayObject status?: ReportedPlayerStatus position?: number - timestamp?: Dayjs + stateUpdatedAt?: Dayjs + playUpdatedAt?: Dayjs } export const asPlayerStateData = (obj: object): obj is PlayerStateData => asPlayerStateDataMaybePlay(obj) && 'play' in obj && isPlayObject(obj.play) diff --git a/src/backend/sources/EndpointLastfmSource.ts b/src/backend/sources/EndpointLastfmSource.ts index 7e17ee17..42a20e38 100644 --- a/src/backend/sources/EndpointLastfmSource.ts +++ b/src/backend/sources/EndpointLastfmSource.ts @@ -92,7 +92,7 @@ export const playStateFromRequest = (obj: LastFMScrobbleRequestPayload): PlayerS platformId: [play.meta.deviceId, NO_USER], play, status: obj.method === 'track.updateNowPlaying' ? REPORTED_PLAYER_STATUSES.playing : REPORTED_PLAYER_STATUSES.unknown, - timestamp: dayjs() + stateUpdatedAt: dayjs() } } diff --git a/src/backend/sources/EndpointListenbrainzSource.ts b/src/backend/sources/EndpointListenbrainzSource.ts index a9e7d2bd..147e4090 100644 --- a/src/backend/sources/EndpointListenbrainzSource.ts +++ b/src/backend/sources/EndpointListenbrainzSource.ts @@ -116,7 +116,7 @@ export const playStateFromRequest = (obj: SubmitPayload): PlayerStateData => { platformId: [play.meta.deviceId, NO_USER], play, status: listenTypeAsPlayerStatus(listen_type), - timestamp: dayjs() + stateUpdatedAt: dayjs() } } diff --git a/src/backend/sources/SpotifySource.ts b/src/backend/sources/SpotifySource.ts index ae4db007..64e83766 100644 --- a/src/backend/sources/SpotifySource.ts +++ b/src/backend/sources/SpotifySource.ts @@ -520,7 +520,7 @@ export default class SpotifySource extends MemoryPositionalSource implements Pag platformId: [combinePartsToString([shortDeviceId(device.id), device.name]), NO_USER], status, play: item !== null && item !== undefined ? SpotifySource.formatPlayObj(res.body, {newFromSource: true}) : undefined, - timestamp: dayjs(timestamp), + stateUpdatedAt: dayjs(timestamp), position: progress_ms !== null && progress_ms !== undefined ? progress_ms / 1000 : undefined, } } diff --git a/src/backend/sources/WebScrobblerSource.ts b/src/backend/sources/WebScrobblerSource.ts index 5d8c768f..8100fb44 100644 --- a/src/backend/sources/WebScrobblerSource.ts +++ b/src/backend/sources/WebScrobblerSource.ts @@ -90,7 +90,7 @@ export class WebScrobblerSource extends MemorySource { platformId: [play.meta.deviceId, NO_USER], play, status: WebScrobblerSource.webhookEventAsPlayerStatus(eventName), - timestamp: dayjs.unix(time) + stateUpdatedAt: dayjs.unix(time) } } diff --git a/src/backend/tests/source/source.test.ts b/src/backend/tests/source/source.test.ts index 97c9f0a1..718ae1eb 100644 --- a/src/backend/tests/source/source.test.ts +++ b/src/backend/tests/source/source.test.ts @@ -195,7 +195,7 @@ describe('Player Cleanup', function () { const cleanedUpDuration = async (generateSource: (config: SourceConfig) => MemorySource) => { await using source = generateSource({data: {staleAfter: 21, orphanedAfter: 40}, options: {}}); const initialDate = dayjs(); - const initialState = generatePlayerStateData({position: 0, playData: {duration: 50}, timestamp: initialDate, status: REPORTED_PLAYER_STATUSES.playing}); + const initialState = generatePlayerStateData({position: 0, playData: {duration: 50}, stateUpdatedAt: initialDate, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([initialState])).length).to.be.eq(0); let position = 0; @@ -207,7 +207,7 @@ describe('Player Cleanup', function () { timeSince += 10; MockDate.set(initialDate.add(position, 'seconds').toDate()); await sleep(1); - const advancedState = generatePlayerStateData({play: initialState.play, timestamp: dayjs(), position, status: REPORTED_PLAYER_STATUSES.playing}); + const advancedState = generatePlayerStateData({play: initialState.play, stateUpdatedAt: dayjs(), position, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([advancedState])).length).to.be.eq(0); } @@ -239,7 +239,7 @@ describe('Player Cleanup', function () { await using source = generateSource({data: {staleAfter: 21, orphanedAfter: 40}, options: {}}); const initialDate = dayjs(); - const initialState = generatePlayerStateData({position: 0, playData: {duration: 50}, timestamp: initialDate, status: REPORTED_PLAYER_STATUSES.playing}); + const initialState = generatePlayerStateData({position: 0, playData: {duration: 50}, stateUpdatedAt: initialDate, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([initialState])).length).to.be.eq(0); let position = 0; @@ -251,7 +251,7 @@ describe('Player Cleanup', function () { timeSince += 10; MockDate.set(initialDate.add(position, 'seconds').toDate()); await sleep(1); - const advancedState = generatePlayerStateData({play: initialState.play, timestamp: dayjs(), position, status: REPORTED_PLAYER_STATUSES.playing}); + const advancedState = generatePlayerStateData({play: initialState.play, stateUpdatedAt: dayjs(), position, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([advancedState])).length).to.be.eq(0); } @@ -280,7 +280,7 @@ describe('Player Cleanup', function () { timeSince += 10; MockDate.set(initialDate.add(timeSince, 'seconds').toDate()); await sleep(1); - const advancedState = generatePlayerStateData({play: initialState.play, timestamp: dayjs(), position, status: REPORTED_PLAYER_STATUSES.playing}); + const advancedState = generatePlayerStateData({play: initialState.play, stateUpdatedAt: dayjs(), position, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([advancedState])).length).to.be.eq(0); } @@ -288,7 +288,7 @@ describe('Player Cleanup', function () { MockDate.set(initialDate.add(timeSince, 'seconds').toDate()); await sleep(1); // new Play - const advancedState = generatePlayerStateData({timestamp: dayjs(), position: 0, status: REPORTED_PLAYER_STATUSES.playing}); + const advancedState = generatePlayerStateData({stateUpdatedAt: dayjs(), position: 0, status: REPORTED_PLAYER_STATUSES.playing}); // should not return play because it has only been played for ~20 seconds, less than 50% of duration const plays = await source.processRecentPlays([advancedState]) expect(plays.length).to.be.eq(0); @@ -309,7 +309,7 @@ describe('Player Cleanup', function () { const initialDate = dayjs(); // if player incorrectly counted stale time then 30s of actual play + 20s of stale time > scrobble threshold of 50% of 90s - const initialState = generatePlayerStateData({position: 0, playData: {duration: 90}, timestamp: initialDate, status: REPORTED_PLAYER_STATUSES.playing}); + const initialState = generatePlayerStateData({position: 0, playData: {duration: 90}, stateUpdatedAt: initialDate, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([initialState])).length).to.be.eq(0); let position = 0; @@ -321,7 +321,7 @@ describe('Player Cleanup', function () { timeSince += 10; MockDate.set(initialDate.add(position, 'seconds').toDate()); await sleep(1); - const advancedState = generatePlayerStateData({play: initialState.play, timestamp: initialDate, position, status: REPORTED_PLAYER_STATUSES.playing}); + const advancedState = generatePlayerStateData({play: initialState.play, stateUpdatedAt: initialDate, position, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([advancedState])).length).to.be.eq(0); } @@ -354,7 +354,7 @@ describe('Player Cleanup', function () { const initialDate = dayjs(); // if player incorrectly counted stale time then 30s of actual play + 20s of stale time > scrobble threshold of 50% of 90s - const initialState = generatePlayerStateData({position: 0, playData: {duration: 90}, timestamp: initialDate, status: REPORTED_PLAYER_STATUSES.playing}); + const initialState = generatePlayerStateData({position: 0, playData: {duration: 90}, stateUpdatedAt: initialDate, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([initialState])).length).to.be.eq(0); let position = 0; @@ -366,7 +366,7 @@ describe('Player Cleanup', function () { timeSince += 10; MockDate.set(initialDate.add(position, 'seconds').toDate()); await sleep(1); - const advancedState = generatePlayerStateData({play: initialState.play, timestamp: dayjs(), position, status: REPORTED_PLAYER_STATUSES.playing}); + const advancedState = generatePlayerStateData({play: initialState.play, stateUpdatedAt: dayjs(), position, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([advancedState])).length).to.be.eq(0); } @@ -396,7 +396,7 @@ describe('Player Cleanup', function () { timeSince += 10; MockDate.set(initialDate.add(position, 'seconds').toDate()); await sleep(1); - const advancedState = generatePlayerStateData({play: initialState.play, timestamp: dayjs(), position, status: REPORTED_PLAYER_STATUSES.playing}); + const advancedState = generatePlayerStateData({play: initialState.play, stateUpdatedAt: dayjs(), position, status: REPORTED_PLAYER_STATUSES.playing}); expect((await source.processRecentPlays([advancedState])).length).to.be.eq(0); } @@ -404,7 +404,7 @@ describe('Player Cleanup', function () { MockDate.set(initialDate.add(position, 'seconds').toDate()); await sleep(1); // new Play - const advancedState = generatePlayerStateData({timestamp: dayjs(), position: 0, status: REPORTED_PLAYER_STATUSES.playing}); + const advancedState = generatePlayerStateData({stateUpdatedAt: dayjs(), position: 0, status: REPORTED_PLAYER_STATUSES.playing}); // should return discovered play with ~90 seconds of duration const plays = await source.processRecentPlays([advancedState]) expect(plays.length).to.be.eq(1); diff --git a/src/core/PlayTestUtils.ts b/src/core/PlayTestUtils.ts index 22f18ba6..82e3a344 100644 --- a/src/core/PlayTestUtils.ts +++ b/src/core/PlayTestUtils.ts @@ -157,7 +157,7 @@ export const generatePlayerStateData = (options: Omit