diff --git a/src/backend/common/Cache.ts b/src/backend/common/Cache.ts index c6faf4ea..4c8d4f1b 100644 --- a/src/backend/common/Cache.ts +++ b/src/backend/common/Cache.ts @@ -1,6 +1,14 @@ import { Cacheable, CacheableMemory, Keyv, KeyvStoreAdapter } from 'cacheable'; import { FlatCache } from 'flat-cache'; +import {parse} from 'flatted'; import KeyvValkey from '@keyv/valkey'; +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration.js'; +import isBetween from 'dayjs/plugin/isBetween.js'; +import relativeTime from 'dayjs/plugin/relativeTime.js'; +import isToday from 'dayjs/plugin/isToday.js'; +import timezone from 'dayjs/plugin/timezone.js'; +import utc from 'dayjs/plugin/utc.js'; import { childLogger, Logger } from '@foxxmd/logging'; import { projectDir } from './index.js'; import path from 'path'; @@ -9,6 +17,13 @@ import { asCacheMetadataProvider, asCacheScrobbleProvider, CacheConfig, CacheCon const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`); +dayjs.extend(utc) +dayjs.extend(isBetween); +dayjs.extend(relativeTime); +dayjs.extend(duration); +dayjs.extend(timezone); +dayjs.extend(isToday); + export class MSCache { config: Required @@ -93,6 +108,10 @@ export class MSCache { cacheId: 'scrobble.cache', persistInterval: 1 * 1000 * 60, expirationInterval: 1 * 1000 * 60, // 1 minute + // deserialize: (str) => { + // const data = parse(str) + // return data; + // } }); let loadError: Error; diff --git a/src/backend/scrobblers/AbstractScrobbleClient.ts b/src/backend/scrobblers/AbstractScrobbleClient.ts index 01383025..0abfea83 100644 --- a/src/backend/scrobblers/AbstractScrobbleClient.ts +++ b/src/backend/scrobblers/AbstractScrobbleClient.ts @@ -54,6 +54,7 @@ import { WebhookPayload } from "../common/infrastructure/config/health/webhooks. import { AsyncTask, SimpleIntervalJob, Task, ToadScheduler } from "toad-scheduler"; import { MSCache } from "../common/Cache.js"; import { getRoot } from "../ioc.js"; +import { rehydratePlay } from "../utils/CacheUtils.js"; type PlatformMappedPlays = Map; type NowPlayingQueue = Map; @@ -306,11 +307,11 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i protected async doParseCache(): Promise { const cachedQueue = (await this.cache.cacheScrobble.get(`${this.getMachineId()}-queue`) as QueuedScrobble[] ?? []); const cachedQLength = cachedQueue.length; - this.queuedScrobbles = cachedQueue; + this.queuedScrobbles = cachedQueue.map(x => ({...x, play: rehydratePlay(x.play)})); const cachedDead = (await this.cache.cacheScrobble.get(`${this.getMachineId()}-dead`) as DeadLetterScrobble[] ?? []); const cachedDLength = cachedDead.length; - this.deadLetterScrobbles = cachedDead; + this.deadLetterScrobbles = cachedDead.map(x => ({...x, play: rehydratePlay(x.play), lastRetry: x.lastRetry !== undefined ? dayjs(x.lastRetry) : undefined})); return `Scrobbles from Cache: ${cachedQLength} Queue | ${cachedDLength} Dead Letter`; } diff --git a/src/backend/utils/CacheUtils.ts b/src/backend/utils/CacheUtils.ts new file mode 100644 index 00000000..60bb4faf --- /dev/null +++ b/src/backend/utils/CacheUtils.ts @@ -0,0 +1,46 @@ +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration.js'; +import isBetween from 'dayjs/plugin/isBetween.js'; +import relativeTime from 'dayjs/plugin/relativeTime.js'; +import isToday from 'dayjs/plugin/isToday.js'; +import timezone from 'dayjs/plugin/timezone.js'; +import utc from 'dayjs/plugin/utc.js'; +import { AmbPlayObject, ListenRangeData, ListenRangeDataAmb, PlayObject, PlayProgress, PlayProgressAmb } from '../../core/Atomic.js'; +import objectHash from 'object-hash'; + +dayjs.extend(utc) +dayjs.extend(isBetween); +dayjs.extend(relativeTime); +dayjs.extend(duration); +dayjs.extend(timezone); +dayjs.extend(isToday); + +export const rehydratePlay = (obj: AmbPlayObject): PlayObject => { + if(obj.data?.playDate !== undefined && typeof obj.data.playDate === 'string') { + obj.data.playDate = dayjs(obj.data.playDate); + 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); + } + } + return obj as PlayObject; +} + +// this may become problematic since we aren't re-instantiating Progress class, just implementing interface +// but that may only be an issue if rehydrating source data which isn't in scope so far +export const rehydrateListenRangeData = (obj: ListenRangeDataAmb): ListenRangeData => { + return { + start: rehydratePlayProgress(obj.start), + end: rehydratePlayProgress(obj.end) + } as ListenRangeData; +} + +export const rehydratePlayProgress = (obj: PlayProgressAmb): PlayProgress => { + if(typeof obj.timestamp === 'string') { + obj.timestamp = dayjs(obj.timestamp); + } + return obj as PlayProgress; +} \ No newline at end of file diff --git a/src/core/Atomic.ts b/src/core/Atomic.ts index e406133a..dd86b12c 100644 --- a/src/core/Atomic.ts +++ b/src/core/Atomic.ts @@ -76,17 +76,26 @@ export interface TrackStringOptions { } } -export interface PlayProgress { - timestamp: Dayjs +export interface PlayProgressAmb { + timestamp: string | Dayjs position?: number positionPercent?: number } +export interface PlayProgress extends PlayProgressAmb { + timestamp: Dayjs +} + export interface PlayProgressPositional extends PlayProgress { position: number } -export interface ListenRangeData { +export interface ListenRangeDataAmb { + start: PlayProgressAmb + end: PlayProgressAmb +} + +export interface ListenRangeData extends ListenRangeDataAmb { start: ListenProgress end: ListenProgress }