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.
5.7 kB · 172 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172import { create as diffCreate } from "jsondiffpatch";import { type numberFormatOptions, REGEX_ISO8601_LOOSE } from './Atomic.ts';import { diff, applyChangeset, type Changeset, type Options } from 'json-diff-ts';// may want to return to this one day// but currently the jsondiffpatch formatter is the best console/ansi diff output for humans :(//import {DiffOptions, DiffOptionsColor, diff as jestDiff} from 'jest-diff';import clone from "clone";import ConsoleFormatter from "jsondiffpatch/formatters/console";import assert from "node:assert";import dayjs from "dayjs";import { Traverse } from "neotraverse/modern";import { serializeError } from "serialize-error";
const console = new ConsoleFormatter();
export const jdiff = diffCreate({ propertyFilter(name, context) { return name !== 'lifecycle'; }, cloneDiffValues: true //omitRemovedValues: true});
const diffOptions: Options = { /*arrayIdentityKeys: {artists: '$value'},*/ keysToSkip: ['playDate','playDateCompleted','listenRanges']};
export const diffObjects = (a: object, b: object) => { return diff(a, b, diffOptions);}
export const patchObject = <T>(a: T, b: Changeset): T => { return applyChangeset(clone(a), b);}
// const jestDiffOptions: DiffOptions = {// aAnnotation: 'Old', // bAnnotation: 'New', // aColor: chalk.red,// bColor: chalk.green// }
export const diffObjectsConsoleOutput = (a: object, b: object, showUnchanged: boolean = false) => { //return jestDiff(a, b, jestDiffOptions);
const left = JSON.parse(JSON.stringify(a)); return console.format(jdiff.diff(left, JSON.parse(JSON.stringify(b))), showUnchanged ? left : undefined);}
export const formatNumber = (val: number | string, options?: numberFormatOptions) => { const { toFixed = 2, defaultVal = null, prefix = '', suffix = '', round, } = options || {}; let parsedVal = typeof val === 'number' ? val : Number.parseFloat(val); if (Number.isNaN(parsedVal)) { return defaultVal; } if (!Number.isFinite(val)) { return 'Infinite'; } let prefixStr = prefix; const { enable = false, indicate = true, type = 'round' } = round || {}; if (enable && !Number.isInteger(parsedVal)) { switch (type) { case 'round': parsedVal = Math.round(parsedVal); break; case 'ceil': parsedVal = Math.ceil(parsedVal); break; case 'floor': parsedVal = Math.floor(parsedVal); } if (indicate) { prefixStr = `~${prefix}`; } } const localeString = parsedVal.toLocaleString(undefined, { minimumFractionDigits: toFixed, maximumFractionDigits: toFixed, }); return `${prefixStr}${localeString}${suffix}`;};
export const generateArray = <T = any>(size: number, gen: (index: number) => T): T[] => { return Array.from(Array(size), (v,k) => gen(k));}
/** Return an array in chunks * * https://stackoverflow.com/a/8495740/1469797 */export const chunkArray = <T>(chunkSize: number, arr: T[]): T[][] => { assert(chunkSize !== 0, 'chunkSize cannot be 0'); const chunks: T[][] = []; for (let i = 0; i < arr.length; i += chunkSize) { const chunk = arr.slice(i, i + chunkSize); chunks.push(chunk); } return chunks;}
export const asDayjsHydratedObject = <T, U>(obj: T): U => { const cloned = clone(obj); new Traverse(cloned).forEach((ctx, x) => {
if (typeof x === 'string' && REGEX_ISO8601_LOOSE.test(x)) { ctx.update(dayjs(x), true); } }); return cloned as unknown as U;};
export const asErrorSerializedObject = <T, U>(obj: T): U => { const cloned = clone(obj); new Traverse(cloned).forEach((ctx, x) => {
if (x !== null && typeof x === 'object' && x instanceof Error) { ctx.update(serializeError(x), true); } }); return cloned as unknown as U;};
/** * Get indexes of all elements in array that make the function return true * * @see https://stackoverflow.com/a/20798567/1469797 */export const getAllIndexes = <T>(arr: T[], truthyFunc: (val: T) => boolean) => { const indexes = []; for(let i = 0; i < arr.length; i++) if (truthyFunc(arr[i])) indexes.push(i); return indexes;};export const removeUndefinedKeys = <T extends Record<string, any>>(obj: T, returnUndefined: boolean = true): T | undefined => { const newObj: any = {}; Object.keys(obj).forEach((key) => { if (Array.isArray(obj[key])) { newObj[key] = obj[key]; } else if (obj[key] === Object(obj[key])) { // dumb assign nested objects // bc they may be third party library-objects that use prototyping and we don't want to mess with newObj[key] = obj[key]; } else if (obj[key] !== undefined) { newObj[key] = obj[key]; } }); if (Object.keys(newObj).length === 0) { if (returnUndefined) { return undefined; } return newObj; } Object.keys(newObj).forEach(key => { if (newObj[key] === undefined || (null !== newObj[key] && typeof newObj[key] === 'object' && Object.keys(newObj[key]).length === 0)) { delete newObj[key]; } }); //Object.keys(newObj).forEach(key => newObj[key] === undefined || newObj[key] && delete newObj[key]) return newObj;};
export const pick = <T extends {}, K extends keyof T>(obj: T, ...keys: K[]) => ( Object.fromEntries( keys .filter(key => key in obj) .map(key => [key, obj[key]]) ) as Pick<T, K>);