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.
7.3 kB · 227 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227import JSON5 from "json5";import { constants, promises } from "fs";import { MaybeLogger } from '../common/MaybeLogger.ts';import { deepEqual } from 'fast-equals';import type {CommonConfigPrimitives} from "../common/infrastructure/config/common.ts";import { parseBoolStrict } from "../utils.ts";import { removeUndefinedKeys } from '../../core/DataUtils.ts';import { nonEmptyStringOrDefault } from "../../core/StringUtils.ts";
export const asArray = <T>(data: T | T[]): T[] => { if (Array.isArray(data)) { return data; } return [data];}
const handler: ProxyHandler<object> ={ ownKeys(target) { return Reflect.ownKeys(target).map(x => typeof x === 'string' ? x.toLocaleLowerCase() : x); }, getOwnPropertyDescriptor(target, key) { return { ...Reflect.getOwnPropertyDescriptor(target, key), enumerable: true, configurable: true, }; }, get: function (target, key) { //console.log("key: " + key.toString()); if (typeof key == "string") { const uKey = key.toUpperCase();
if ((key != uKey) && (key in target)) return target[key]; return target[uKey]; } return target[key]; }, set: function (target, key, value) { if (typeof key == "string") { const uKey = key.toUpperCase();
if ((key != uKey) && (key in target)) target[key] = value; target[uKey] = value; return true; } else { target[key] = value; return true; }
}, deleteProperty: function (target, key) { if (typeof key == "string") { const uKey = key.toUpperCase();
if ((key != uKey) && (key in target)) delete target[key]; if (uKey in target) delete target[uKey]; return true; } else { delete target[key]; return true; }
},};
const checkAtomic = (value) => { if (typeof value == "object") return noCasePropObj(value); // recursive call only for Objects return value;}
export type toLowerCase<T> = { [K in keyof T]: T[K]}
export type LowercaseString<S extends string> = S extends `${infer Str}` ? `${Lowercase<Str>}` : S;
/** https://medium.com/@vincent.dibon_78881/type-level-uppercase-and-lowercase-in-typescript-c18f574f4572 */export type LowercaseKeys<T> = { [K in keyof T as LowercaseString<Extract<K, string>>]: T[K]};
/** Return a Proxy of an object where keys can be accessed case-insensitive * * Note: objects from rest operator in spread are *always* lowercase * * @see https://stackoverflow.com/a/50102779/1469797 */export const noCasePropObj = <T extends object>(obj: T): LowercaseKeys<T> => {
const newObj = new Proxy<LowercaseKeys<T>>({} as LowercaseKeys<T>, handler); // traverse the Original object converting string keys to upper case for (const key in obj) { if (typeof key == "string") { const objKey = key.toUpperCase(); if (!(key in newObj)) newObj[objKey] = checkAtomic(obj[key]); } } return newObj; // object with upper cased keys}export async function readJson(this: any, path: any, options: ReadJsonOptions = {}) { const { throwOnNotFound = true, interpolateEnvs = true, logger } = options;
try { await promises.access(path, constants.R_OK); const data = (await promises.readFile(path)).toString(); if(interpolateEnvs) { const replaced = replaceInterpolatedValues(data, process.env, logger); return JSON5.parse(replaced); } return JSON5.parse(data); } catch (e) { const { code } = e; if (code === 'ENOENT') { if (throwOnNotFound) { throw new Error(`No file found at given path: ${path}`, { cause: e }); } else { return; } } throw new Error(`Encountered error while parsing file: ${path}`, { cause: e }); }}export interface ReadJsonOptions { throwOnNotFound?: boolean; interpolateEnvs?: boolean; logger?: MaybeLogger}export const replaceInterpolatedValues = (str: string, fromVals: Record<string, any>, logger: MaybeLogger = new MaybeLogger()): string => { const cleanFromValKeys = noCasePropObj(fromVals);
const matched = new Set(), unmatched = new Set(); const replaced = str.replaceAll(INTERPOLATION_WRAPPED_REGEX, (match, p1) => { //const fv = cleanFromValKeys[p1.toLocaleLowerCase().trim()]; const fv = cleanFromValKeys[p1.trim()]; if (fv !== undefined) { matched.add(p1); return fv; } unmatched.add(p1); return match; }); if (matched.size !== 0 || unmatched.size !== 0) { const logMsg = `Matched: ${matched.size === 0 ? 'None' : Array.from(matched.values()).join(', ')} | Unmatched: ${unmatched.size === 0 ? 'None' : Array.from(unmatched.values()).join(', ')}`; if(unmatched.size > 0) { logger.warn(logMsg); } else { logger.debug(logMsg); }
} return replaced;};export const INTERPOLATION_WRAPPED_REGEX: RegExp = new RegExp(/\[\[([^\r\n\[\]]+?)\]\]/g);
export const objectIsEmpty = (obj: object, valueIsEmpty?: undefined | ((val: any) => boolean)): boolean => { if(Object.keys(obj).length === 0) { return true; }
if(valueIsEmpty === undefined) { return false; }
return Object.values(obj).every(x => valueIsEmpty(x));}
/** Shuffle array in-place * * https://stackoverflow.com/a/12646864/1469797 */export const shuffleArray = (array: any[]): void => { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; }}
export const objectsEqual = (a: object, b: object) => { try { return deepEqual(a, b); } catch (e) { throw new Error('Could not compare objects', {cause: e}); }}
export const getCommonComponentEnvConfig = (prefix: string): Partial<CommonConfigPrimitives> => { const e = nonEmptyStringOrDefault(process.env[`${prefix}_ENABLE`], undefined); return removeUndefinedKeys<CommonConfigPrimitives>({ id: nonEmptyStringOrDefault(process.env[`${prefix}_ID`], undefined), name: nonEmptyStringOrDefault(process.env[`${prefix}_NAME`], undefined), enable: e !== undefined ? parseBoolStrict(e) : undefined }, false);}
const byteSizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
/** * Convert bytes into human readable size * * @see https://stackoverflow.com/a/18650828/1469797 */export const formatBytes = (bytes: number, decimals: number = 2): [string, number, string] => { if (!+bytes) return ['0 Bytes', 0, 'Byes'];
const k = 1024 const dm = decimals < 0 ? 0 : decimals
const i = Math.floor(Math.log(bytes) / Math.log(k))
const friendlySize = parseFloat((bytes / Math.pow(k, i)).toFixed(dm)); const friendlyUnit = byteSizes[i]; return [`${friendlySize} ${friendlyUnit}`, friendlySize, friendlyUnit];}