Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
Something went wrong. Try again.
4.3 kB · 140 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140import clone from 'clone';import dayjs from 'dayjs';import { Traverse, type TraverseContext } from 'neotraverse/modern';import { type AmbPlayObject, type DateLike, type JsonPlayObject, type PlayObject, REGEX_ISO8601_LOOSE, type Replace } from './Atomic.ts';import type { ElementOf, MarkOptional } from 'ts-essentials';import { type ErrorObject, serializeError } from 'serialize-error';import type { PlayApiCommonDetailed } from './Api.ts';
interface BlockPath { key: string, parent: string };type BlockPaths = BlockPath[];
/** We know some nodes will never have data that needs to be transformed * and these nodes can have lots of data so we can optimize them away by not (recursively) traversing them */const blockedKeys: PropertyKey[] = ['patch', 'inputs', 'payload', 'response', 'error'];/** We know some paths/nodes will never have data that needs to be transformed * and these nodes can have lots of data so we can optimize them away by not (recursively) traversing them */const blockedPaths: BlockPaths = [ { parent: 'data', key: 'meta' }, { parent: 'lifecycle', key: 'input' }];
export const shouldBlock = (ctx: TraverseContext): boolean => { if (blockedKeys.includes(ctx.key)) { return true; } return blockedPaths.some((x) => { let blocked = x.key === ctx.key; if (blocked && x.parent !== undefined) { blocked = ctx.parent !== undefined && ctx.parent.key === x.parent; } return blocked; });};
export const asJsonPlayObject = (play: AmbPlayObject<DateLike>): JsonPlayObject => { const cloned = clone(play); new Traverse(cloned).forEach((ctx, x) => { if (shouldBlock(ctx)) { ctx.block(); return; }
if (dayjs.isDayjs(x)) { ctx.update(x.toISOString()); } }); return cloned as unknown as JsonPlayObject;};
export type SerializablePlaySelect = Replace<MarkOptional<PlayApiCommonDetailed, 'queueStates'>, 'error', ErrorObject> & {queueStates?: Replace<ElementOf<PlayApiCommonDetailed['queueStates']>, 'error', ErrorObject>[]};export const asSerializablePlaySelect = (data: MarkOptional<PlayApiCommonDetailed, 'queueStates'>): SerializablePlaySelect => { const { error, queueStates = [], ...rest } = data;
const qMapped = queueStates.map((x) => { const { error: e, ...restQ } = x; return { ...restQ, error: e instanceof Error ? serializeError(e) : e } });
return { ...rest, // @ts-expect-error error: error instanceof Error ? serializeError(error) : error, queueStates: qMapped }}
export const asPlay = (data: JsonPlayObject | PlayObject): PlayObject => { const cloned = clone(data); new Traverse(cloned).forEach((ctx, x) => { if (shouldBlock(ctx)) { ctx.block(); return; }
if (typeof x === 'string' && REGEX_ISO8601_LOOSE.test(x)) { ctx.update(dayjs(x), true); } /*else if (ctx.key === 'listenRanges') { const ranges = x[0].map((y: PlayProgressAmb<string>) => { if (y.positionPercent === undefined) { return new ListenProgressPositional({ timestamp: dayjs(y.timestamp), position: y.position }); } else { return new ListenProgressTS({ timestamp: dayjs(y.timestamp), positionPercent: y.positionPercent }); } }); ctx.update(ranges, true); }*/ }); return cloned as unknown as PlayObject;};
/** * Cheaply convert playobject-like json to playobject with full timestamps etc. * * If we know the source is a throw-away object (like from drizzle fromDriver) then we don't need to clone it * @param data * @returns */export const asPlayCheap = (data: JsonPlayObject | PlayObject): PlayObject => { new Traverse(data).forEach((ctx, x) => { if (shouldBlock(ctx)) { ctx.block(); return; }
if (typeof x === 'string' && REGEX_ISO8601_LOOSE.test(x)) { ctx.update(dayjs(x), true); } }); return data as unknown as PlayObject;};
export const marshalDayjsToWellKnownString = (ctx: TraverseContext, x: any): void => { if (dayjs.isDayjs(x)) { ctx.update(`dayjs-x.toISOString()`); }}
export const marshalWellKnownIsoStringToDayjs = (ctx: TraverseContext, x: any): void => { if (typeof x === 'string' && REGEX_ISO8601_LOOSE.test(x)) { ctx.update(dayjs(x.substring(6)), true); }}