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.
27 kB · 864 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864import type { Dayjs } from "dayjs";import type { AdditionalTrackInfoResponse } from "./vendor/listenbrainz/interfaces.ts";import type { Merge, RequiredKeys, StrictOmit } from "ts-essentials";import type {ErrorObject} from "serialize-error";import type { FlowControlTerm, TransformHook } from "./Transform.ts";import type {Changeset} from "json-diff-ts";import type {IParseBaseOptions} from 'qs';import * as z from "zod";
export const componentTypeClientSchema = z.literal('client');export type ComponentTypeClient = z.infer<typeof componentTypeClientSchema>;export const COMPONENT_TYPE_CLIENT: ComponentTypeClient = 'client';export const componentTypeSourceSchema = z.literal('source');export type ComponentTypeSource = z.infer<typeof componentTypeSourceSchema>;export const COMPONENT_TYPE_SOURCE: ComponentTypeSource = 'source';export const componentTypeSchema = z.enum(['client','source']); // z.union([componentTypeClientSchema, componentTypeSourceSchema]);export type ComponentType = z.infer<typeof componentTypeSchema>;export const COMPONENT_TYPES: ComponentType[] = [COMPONENT_TYPE_SOURCE, COMPONENT_TYPE_CLIENT];export const isComponentTypeSource = (type: string): type is ComponentTypeSource => type === COMPONENT_TYPE_SOURCE;export const isComponentTypeClient = (type: string): type is ComponentTypeClient => type === COMPONENT_TYPE_CLIENT;export const isComponentType = (type: string): type is ComponentType => isComponentTypeClient(type) || isComponentTypeSource(type);export interface SourceStatusData { status: string; type: SourceType; display: string; tracksDiscovered: number; name: string; canPoll: boolean; hasAuth: boolean; hasAuthInteraction: boolean; authed: boolean; players: Record<string, SourcePlayerJson> sot: SOURCE_SOT_TYPES supportsUpstreamRecentlyPlayed: boolean; manualListening?: boolean systemListeningBehavior?: boolean}
export interface ClientStatusData { status: string; type: "maloja" | "lastfm" | "librefm" | "listenbrainz" | "koito" | "tealfm" | "rocksky" | "discord"; display: string; scrobbled: number; deadLetterScrobbles: number deadLetterScrobblesTotal: number queued: number name: string; hasAuth: boolean; hasAuthInteraction: boolean; authed: boolean; initialized: boolean; manualListening?: boolean systemListeningBehavior?: boolean}
export type PlayObjectIncludeTypes = 'album' | 'time' | 'artist' | 'track' | 'timeFromNow' | 'trackId' | 'comment' | 'platform' | 'session';export const recentIncludes: PlayObjectIncludeTypes[] = ['time', 'timeFromNow', 'track', 'album', 'artist', 'comment'];
export interface TrackStringOptions<T = string> { include?: PlayObjectIncludeTypes[] transformers?: { artists?: (a: string[]) => T | string album?: (t: string,data: AmbPlayObject, hasExistingParts?: boolean) => T | string track?: (t: string,data: AmbPlayObject, hasExistingParts?: boolean) => T | string time?: (t: Dayjs, i?: ScrobbleTsSOC) => T | string timeFromNow?: (t: Dayjs) => T | string comment?: (c: string | undefined) => T | string platform?: (d: string | undefined, u: string | undefined, s: string | undefined) => T | string reducer?: (arr: (T | string)[]) => T //(acc: T, curr: T | string) => T }}
export interface PlayProgressAmb<D extends DateLike = Dayjs> { timestamp: D position?: number positionPercent?: number}
export interface PlayProgress extends PlayProgressAmb { timestamp: Dayjs}
export interface PlayProgressPositional extends PlayProgress { position: number}
export interface ListenRangeDataAmb<D extends DateLike = Dayjs> { start: PlayProgressAmb<D> end: PlayProgressAmb<D>}
export interface ListenRangeData extends ListenRangeDataAmb { start: PlayProgress end: PlayProgress}
/** https://musicbrainz.org/doc/MusicBrainz_Database/Schema#Overview */export interface BrainzMeta { /** * artist_mbids * * All artists, including ft guests etc... go here */ artist?: string[] /** * artists_mbid * * If multiple artists for track this is the "original" artist(s) who is releasing the single/album */ albumArtist?: string[] /** * release_mbid * * The unique release like --> 1984 US release of "The Wall" by "Pink Floyd", release on label "Columbia Records" with catalog number "C2K 36183" * */ album?: string /** Unique track id, recording_mbid */ recording?: string /** * * The "consolidated" album like --> "The Wall" by "Pink Floyd" */ releaseGroup?: string additionalInfo?: AdditionalTrackInfoResponse
/** Position of track within Release */ trackNumber?: number
/** Track MBID (tid), not visible to end users and is only relevant in the context of a Release * * Specifies the track on a specific Release. Not the same as the Recording MBID. */ track?: string}
export interface ArtistCredit { name: string mbid?: string}
export interface SpotifyMeta { artist?: string[] albumArtist?: string[] album?: string track?: string}
export interface TrackMeta { brainz?: BrainzMeta spotify?: SpotifyMeta}
export interface TrackData { artists?: ArtistCredit[] albumArtists?: ArtistCredit[] album?: string track?: string /** * The length of the track, in seconds * */ duration?: number
meta?: TrackMeta
/** International Standard Recording Code (ISRC) for this track * * https://musicbrainz.org/doc/ISRC */ isrc?: string}
export interface PlayData<D extends DateLike = Dayjs> extends TrackData { /** * The date the track was played at * */ playDate?: D /** Number of seconds the track was listened to */ listenedFor?: number listenRanges?: ListenRangeDataAmb<D>[] playDateCompleted?: D repeat?: boolean}
export interface ArtMeta { album?: string track?: string artist?: string}
export type PlayMeta<D extends DateLike = Dayjs, T = {}> = Merge<PlayMetaBase<D>, T>;
export interface PlayMetaBase<D extends DateLike = Dayjs> { source?: string sourceSOT?: SOURCE_SOT_TYPES
seenAt?: D
//dbUid?: string //dbId?: number
/* * If applicable, the name of the Service providing the track (Spotify, Tidal, etc...) */ musicService?: string
/** * Specifies from what facet/data from the source this play was parsed from IE player, backlog, now playing, etc... * */ parsedFrom?: PARSED_FROM_TYPE /** * Unique ID for this track, given by the Source * */ trackId?: string
/** * Atomic ID for this instance of played tracked IE a unique ID for "this track played at this time" * */ playId?: string newFromSource?: boolean url?: { /** * The URL where this track can be found for the serice it was created from * * IE * * * Spotify Source <-- url to spotify track * * Maloja Client <--- url to specific scrobble */ web: string /** * The URL where this play was originally played * * IE Frank Sinatra - My way FROM youtube.com <-- URL pointing to specific video */ origin?: string [key: string]: string } /** * Hot-linkable images for use with displaying art for this play */ art?: ArtMeta user?: string mediaType?: string server?: string library?: string /** * The position the "player" is at in the track at the time the play was reported, in seconds * */ trackProgressPosition?: number
/* * Name of the media player (program) */ mediaPlayerName?: string
/* * Version of the media player (program) */ mediaPlayerVersion?: string /** * A unique identifier for the device playing this track * */ deviceId?: string /** The ID/Key for individual sessions on a device/platform */ sessionId?: string
nowPlaying?: boolean
scrobbleTsSOC?: ScrobbleTsSOC
comment?: string
/** Was the component activitely monitoring when this Play was created? */ wasMonitored?: boolean
//lifecycle: PlayLifecycle<D> lifecycleInputs?: LifecycleInput[]
//[key: string]: any}
export interface LifecycleInput { type: string, input: (object | string)}
export type ErrorLike = Error | ErrorObject;
/** scrobble action plus the match result before scrobbling */export interface ScrobbleResult<D extends DateLike = Dayjs> { match?: PlayMatchResult<D> payload?: ScrobblePayload warnings?: string[] error?: Error | ErrorObject response?: ScrobbleResponse mergedScrobble?: AmbPlayObjectMinimal<D> createdAt?: D}
export interface PlayLifecycle<D extends DateLike = Dayjs> { input?: object original?: PlayObjectMinimal<D> steps: LifecycleStep[] scrobble?: ScrobbleResult<D>}
export interface LifecycleStep { stageName: string stageType: string hook: TransformHook source: string cached?: boolean returnPartial?: boolean flowResult?: FlowControlTerm flowReason?: string flowKnownState?: 'skip' | 'prereq' error?: ErrorLike patch?: Changeset inputs?: LifecycleInput[] createdAt: string}
export type ScrobblePayload = object | string;export type ScrobbleResponse = object | string;
export interface ScrobbleActionResult<D extends DateLike = Dayjs> { payload: ScrobblePayload, response?: ScrobbleResponse, mergedScrobble?: AmbPlayObject<D> warnings?: string[] createdAt: string}
export interface PlayMatchResult<D extends DateLike = Dayjs> { match: boolean score: number breakdowns: string[] reason?: string closestMatchedPlay?: AmbPlayObjectMinimal<D> transformedPlay?: PlayObjectMinimal summary?: string createdAt: string}
export type ScrobbleTsSOC = 1 | 2;
export const SCROBBLE_TS_SOC_START: ScrobbleTsSOC = 1;export const SCROBBLE_TS_SOC_END: ScrobbleTsSOC = 2;
export type DateLike = Dayjs | string
export interface PlayOriginal<D extends DateLike = Dayjs> { data?: object play?: PlayObjectMinimal<D>}
export interface AmbPlayObject<D extends DateLike = Dayjs, T = {}> { id?: number uid?: string data: PlayData<D>, meta: PlayMeta<D,T> original?: PlayOriginal<D> scrobble?: ScrobbleResult<D> lifecycle?: LifecycleStep[]}
export type AmbPlayObjectMinimal<D extends DateLike = Dayjs, T = {}> = Pick<AmbPlayObject<D,T>, RequiredKeys<AmbPlayObject<D>>> & Pick<AmbPlayObject<D,T>, 'id' | 'uid'>;
export const isPlayObject = (obj: object): obj is PlayObject => { return obj !== undefined && obj !== null && 'data' in obj && typeof obj.data === 'object' && 'meta' in obj && typeof obj.meta === 'object';}
export type PlayObject<T = {}> = AmbPlayObject<Dayjs,T>;export type PlayObjectMinimal<D extends DateLike = Dayjs, T = {}> = AmbPlayObjectMinimal<D,T>;export interface PlayActivity { play: JsonPlayObject status: string error?: ErrorLike}export type JsonPlayObject = AmbPlayObject<string>;
export interface ObjectPlayData extends PlayData { playDate?: Dayjs playDateCompleted?: Dayjs}
export const logLevelStandaloneSchema = z.enum(['debug','error','verbose','info','silly','silent','log','trace','warn','fatal'])export type LogLevelStandalone = z.infer<typeof logLevelStandaloneSchema>;
export interface LogOutputConfig { level: LogLevelStandalone, sort: string, limit: number}
export type PlayPlatformIdStr = string;
export interface SourcePlayerObj<D extends DateLike = Dayjs> { platformId: PlayPlatformIdStr, play?: AmbPlayObject<D>, playFirstSeenAt?: string, playLastUpdatedAt?: string, playerLastUpdatedAt: string createdAt?: number position?: Second listenedDuration: Second nowPlayingMode?: boolean status: { reported: string calculated: string stale: boolean orphaned: boolean }}
export type SourcePlayerJson = SourcePlayerObj<string>;
export interface SourceScrobble<PlayType> { source: string play: PlayType}
export interface QueuedScrobble<PlayType> extends SourceScrobble<PlayType> { id: string}
export type NowPlayingUpdateThreshold = (play?: PlayObject) => number;
export interface DeadLetterScrobble<PlayType, RetryType = Dayjs> extends QueuedScrobble<PlayType> { id: string retries: number lastRetry?: RetryType error: string status: 'queued' | 'failed'}
export type Second = number;export type Millisecond = number;
export type TemporalAccuracy = 1 | 2 | 3 | 4 | 99;
/** Timestamp diffs are close to exact (less than or equal to 1 second difference) */export const TA_EXACT: TemporalAccuracy = 1;/** Timestamp diffs are within source reporting granularity margin-of-error (see lowGranularitySources): * normal granularity is 10 seconds * low granularity (subsonic usually) is 60 seconds */export const TA_CLOSE: TemporalAccuracy = 2;/** Timestamp diffs are not CLOSE but Scrobble A's timestamp +/- duration is within fuzzyDiffThreshold seconds of Scrobble B's timestamp */export const TA_FUZZY: TemporalAccuracy = 3;/** Timestamp diffs are not FUZZY and Scrobble B's timestamp is within potential full play of Scrobble A (timestamp +/- duration) */export const TA_DURING: TemporalAccuracy = 4;/** No correlation between timestamps */export const TA_NONE: TemporalAccuracy = 99;
export type AcceptableTemporaryAccuracy = TemporalAccuracy[]
export const TA_DEFAULT_ACCURACY: AcceptableTemporaryAccuracy = [TA_EXACT, TA_CLOSE];
export type TemporalDuringReference = 'range' | 'duration' | 'listenedFor';
export type AcceptableTemporalDuringReference = TemporalDuringReference[];
export interface TemporalPlayComparison { match: TemporalAccuracy date?: { threshold: number diff: number fuzzyDurationDiff?: number fuzzyListenedDiff?: number fuzzyDiffThreshold?: number } duringReferences: AcceptableTemporalDuringReference range?: { timestamps: [Dayjs, Dayjs] type: TemporalDuringReference } | { type: 'none' }}
export type SOURCE_SOT_TYPES = 'player' | 'history' | 'ingress';export const SOURCE_SOT = { PLAYER : 'player', HISTORY: 'history', INGRESS: 'ingress'} as const satisfies Record<string, SOURCE_SOT_TYPES>export const sourceSotTypes: SOURCE_SOT_TYPES[] = ['player','history','ingress'];
export type PARSED_FROM_TYPE = 'backlog' | 'now playing' | 'player' | 'history' | 'ingress';export const PARSED_FROM = { backlog : 'backlog', nowPlaying: 'now playing', ingress: 'ingress', player: 'player', history: 'history'} as const satisfies Record<string, PARSED_FROM_TYPE>
export interface URLData { url: URL normal: string port: number input: string}
export type Joiner = ',' | '&' | '/' | '\\' | string;export const JOINERS: Joiner[] = [',','/','\\'];
export type FinalJoiners = '&';export const JOINERS_FINAL: FinalJoiners[] = ['&'];
export type Feat = 'ft' | 'feat' | 'vs' | 'ft.' | 'feat.' | 'vs.' | 'featuring'export const FEAT: Feat[] = ['ft','feat','vs','ft.','feat.','vs.','featuring'];
export interface TransformOptions { failOnFetch?: boolean; throwOnFailure?: boolean | ('artists' | 'title' | 'albumArtists' | 'album' | 'duration' | 'meta' | 'art')[]; ttl?: string}export interface TransformerCommonConfig<T = Record<string, any>, Y = Record<string, any>> { defaults?: T; data?: Y type: string; name?: string; options?: TransformOptions}
export interface TransformerCommon<T = Record<string, any>, Y = Record<string, any>> extends TransformerCommonConfig<T,Y> { name: string}
export const rockskyRequiredFields = z.enum(['track','artists','album']);export type RockskyRequiredFields = z.infer<typeof rockskyRequiredFields>;export const rockskyConfidenceFields = z.enum(['isrc','mbid','spotify']);export type RockskyConfidenceField = z.infer<typeof rockskyConfidenceFields>;// https://stackoverflow.com/a/75478762export const rockskyMissingFields = z.enum([...rockskyRequiredFields.options,...rockskyConfidenceFields.options, 'duration'] as const);export type RockskyMissingField = z.infer<typeof rockskyMissingFields>;export const DEFAULT_ROCKSKY_MISSING_TYPES: RockskyMissingField[] = [...rockskyRequiredFields.options, 'duration', rockskyConfidenceFields.enum.mbid] as const;
export type MissingMbidType = 'artists' | 'title' | 'album' | 'duration';export const DEFAULT_MISSING_TYPES: MissingMbidType[] = ['artists','title','album', 'duration'];export const DEFAULT_MISSING_MBIDS_TYPES: MissingMbidType[] = ['artists','title','album'];
export type MBReleaseStatus = 'official' | 'promotion' | 'bootleg' | 'pseudo-release' | 'withdrawn' | 'expunged' | 'cancelled';export const MB_RELEASE_STATUSES: MBReleaseStatus[] = ['official','promotion','bootleg','pseudo-release','withdrawn','expunged' ,'cancelled'];export const isMBReleaseStatus = (str: string): str is MBReleaseStatus => { return MB_RELEASE_STATUSES.includes(str as MBReleaseStatus);}export const asMBReleaseStatus = (str: string): MBReleaseStatus => { const clean = str.toLocaleLowerCase(); if(isMBReleaseStatus(clean)) { return clean; } else { throw new Error(`Release Status is not valid: ${str}`); }}
export type MBReleaseGroupPrimaryType = 'album' | 'single' | 'ep' | 'broadcast' | 'other';export const MB_RELEASE_GROUP_PRIMARY_TYPES: MBReleaseGroupPrimaryType[] = ['album','single','ep','broadcast','other'];export const isMBReleasePrimaryGroupType = (str: string): str is MBReleaseGroupPrimaryType => { return MB_RELEASE_GROUP_PRIMARY_TYPES.includes(str as MBReleaseGroupPrimaryType);}export const asMBReleasePrimaryGroupType = (str: string): MBReleaseGroupPrimaryType => { const clean = str.toLocaleLowerCase(); if(isMBReleasePrimaryGroupType(clean)) { return clean; } else { throw new Error(`Primary Release Group is not valid: ${str}`); }}
export type MBReleaseGroupSecondaryType = 'compilation' | 'soundtrack' | 'live' | 'remix';export const MB_RELEASE_GROUP_SECONDARY_TYPES: MBReleaseGroupSecondaryType[] = ['compilation','soundtrack','live','remix'];export const isMBReleaseSecondaryGroupType = (str: string): str is MBReleaseGroupSecondaryType => { return MB_RELEASE_GROUP_SECONDARY_TYPES.includes(str as MBReleaseGroupSecondaryType);}export const asMBReleaseSecondaryGroupType = (str: string): MBReleaseGroupSecondaryType => { const clean = str.toLocaleLowerCase(); if(isMBReleaseSecondaryGroupType(clean)) { return clean; } else { throw new Error(`Secondary Release Group is not valid: ${str}`); }}
export interface TransformResult { type: string, name: string, play: PlayData}
export const KNOWN_MEDIA_PROVIDER_URLS = ['spotify.com',// spotify cdn'scdn.co','bandcamp.com','youtube.com','deezer.com','tidal.com','apple.com','archive.org','coverartarchive.org','soundcloud.com','jamendo.com','play.google.com','listenbrainz.org','musicbrainz.org'];
/** Number of SECONDS since 1970 */export type UnixTimestamp = number;
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
export const SHORT_CALENDAR_NOTZ_FORMAT = 'MMM D HH:mm:ss';export const SHORT_TODAY_NOTZ_FORMAT = 'HH:mm:ss';export interface numberFormatOptions { toFixed: number; defaultVal?: any; prefix?: string; suffix?: string; round?: { type?: string; enable: boolean; indicate?: boolean; };}/** Only checks for DateT since we can reasonbly sure if this exists its a date we can parse with dayjs * * It needs to be cheap since we mostly use this when walking play objects to transform strings back to dayjs and there may be many strings to check */export const REGEX_ISO8601_LOOSE = new RegExp(/\d{4}-[01]\d-[0-3]\dT/);/** A string we previously marshalled has a wellknown prefix and only check for DateT since we can reasonbly sure if this exists its a date we can parse with dayjs */export const REGEX_ISO8601_WELLKNOWN = new RegExp(/dayjs-(\d{4}-[01]\d-[0-3]\dT.*)/);
export const INGRESS_QUEUE: QueueName = 'ingress';export const DEAD_QUEUE: QueueName = 'dead';export type QueueName = 'ingress' | 'dead';export const QUEUE_NAMES = [INGRESS_QUEUE, DEAD_QUEUE];
/** * Useful TS type-only utility for testing type equality * * Usage: type EQ = TypesAreEqual<any[], [number][], "same", "different">; // "different" * * @see https://stackoverflow.com/a/53808212/1469797 */export type TypesAreEqual<T, U, Y=unknown, N=never> = (<G>() => G extends T ? 1 : 2) extends (<G>() => G extends U ? 1 : 2) ? Y : N;
export type MBID = `${string}-${string}-${string}-${string}-${string}`
// ['queued','discovered','discarded','scrobbled','failed','duped']export type PlayStateCommon = 'queued' |'discarded' | 'failed' | 'duped';export const PLAY_STATE_COMMON: PlayStateCommon[] = ['queued', 'discarded', 'failed', 'duped'];export type PlaySourceState = PlayStateCommon | 'discovered';export const PLAY_SOURCE_STATE: PlaySourceState[] = [...PLAY_STATE_COMMON, 'discovered'];export type PlayClientState = PlayStateCommon | 'scrobbled';export const PLAY_CLIENT_STATE = [...PLAY_STATE_COMMON, 'scrobbled'];export type PlayState = PlaySourceState | PlayClientState;export const PLAY_STATES = Array.from(new Set([...PLAY_CLIENT_STATE, ...PLAY_SOURCE_STATE]));export const isPlayState = (val: string): val is PlayState => PLAY_STATES.includes(val);
export type QueueStatus = 'queued' | 'completed' | 'failed';export const QUEUE_STATUS_QUEUED: QueueStatus = 'queued';export const QUEUE_STATUS_COMPLETED: QueueStatus = 'completed';export const QUEUE_STATUS_FAILED: QueueStatus = 'failed';export const QUEUE_STATUSES: QueueStatus[] = [QUEUE_STATUS_COMPLETED, QUEUE_STATUS_FAILED, QUEUE_STATUS_QUEUED];
export const DEAD_LETTER_RETRIES_DEFAULT = 3;
export const queueContextSchema = z.object({ transform: z.boolean().optional(), dupeCheck: z.boolean().optional(), useCache: z.boolean().optional(), reason: z.string().optional(), isRetry: z.boolean().optional()});
export type QueueContext = z.infer<typeof queueContextSchema>;
export const actionContextSchema = queueContextSchema.extend({action: z.enum(['all','failures'])});
/** * @see https://github.com/ts-essentials/ts-essentials/issues/339#issuecomment-4681920369 */export type Replace<Type, Keys extends keyof Type, TReplace> = StrictOmit<Type, Keys> & Record<Keys, TReplace>
type Match<Value, ReplaceTuple extends readonly [any, any][], Acc = never> = ReplaceTuple extends readonly [[infer From, infer To], ...infer Rest extends readonly [any, any][]] ? [From] extends [Value] ? Match<Value, Rest, Acc | To> : Match<Value, Rest, Acc> : Acc;
/** * @see https://github.com/ts-essentials/ts-essentials/issues/339#issuecomment-4770849507 * @see https://tsplay.dev/w1rgkN */export type DeepReplaceValue<Type, ReplaceTuple extends readonly [any, any][]> = Type extends {} ? { [Key in keyof Type]: Match<Type[Key], ReplaceTuple> extends infer Value ? [Value] extends [never] ? DeepReplaceValue<Type[Key], ReplaceTuple> : Value : never } : Type;
// example of usage//// type T1 = DeepReplaceValue<{// a: Date;// b: {// c: Date;// d: {// e: Date;// }// }// }, [// [Date, string]// ]>;
export const qsOptions: IParseBaseOptions = { ignoreQueryPrefix: true, depth: 5, parameterLimit: 20, arrayLimit: 20, plainObjects: true, allowPrototypes: false};
export const DELIMITERS = [',', '&', '/', '\\'];export const DELIMETERS_REGEX: RegExp = new RegExp(/[,&\/\\]/);export const DELIMITERS_NO_AMP = [',', '/', '\\'];export type DeviceId = string;export type PlayUserId = string;export type PlayPlatformId = [DeviceId, PlayUserId];export type SourceType = 'spotify' | 'plex' | 'subsonic' | 'jellyfin' | 'lastfm' | 'librefm' | 'deezer' | 'endpointlz' | 'endpointlfm' | 'ytmusic' | 'ymbridge' | 'mpris' | 'mopidy' | 'musiccast' | 'listenbrainz' | 'jriver' | 'kodi' | 'webscrobbler' | 'chromecast' | 'maloja' | 'musikcube' | 'mpd' | 'vlc' | 'icecast' | 'azuracast' | 'koito' | 'tealfm' | 'rocksky' | 'sonos' | 'applemusic';export const sourceTypes: SourceType[] = [ 'spotify', 'plex', 'subsonic', 'jellyfin', 'lastfm', 'librefm', 'deezer', 'endpointlz', 'endpointlfm', 'ytmusic', 'ymbridge', 'mpris', 'mopidy', 'musiccast', 'listenbrainz', 'jriver', 'kodi', 'webscrobbler', 'chromecast', 'maloja', 'musikcube', 'mpd', 'vlc', 'icecast', 'azuracast', 'koito', 'tealfm', 'rocksky', 'sonos', 'applemusic'];export const isSourceType = (data: string): data is SourceType => { return sourceTypes.includes(data as SourceType);};export type ClientType = 'maloja' | 'lastfm' | 'librefm' | 'listenbrainz' | 'koito' | 'tealfm' | 'rocksky' | 'discord';export const clientTypes: ClientType[] = [ 'maloja', 'lastfm', 'librefm', 'listenbrainz', 'koito', 'tealfm', 'rocksky', 'discord'];export type ReportedPlayerStatus = 'playing' | 'stopped' | 'paused' | 'unknown';export const REPORTED_PLAYER_STATUSES = { playing: 'playing' as ReportedPlayerStatus, stopped: 'stopped' as ReportedPlayerStatus, paused: 'paused' as ReportedPlayerStatus, unknown: 'unknown' as ReportedPlayerStatus};export type CalculatedPlayerStatus = ReportedPlayerStatus | 'stale' | 'orphaned';export const CALCULATED_PLAYER_STATUSES = { ...REPORTED_PLAYER_STATUSES, stale: 'stale' as CalculatedPlayerStatus, orphaned: 'orphaned' as CalculatedPlayerStatus,};export const isClientType = (data: string): data is ClientType => { return clientTypes.includes(data as ClientType);};
export type MonitoringOrigin = 'user' | 'system';export const MONITORING_ORIGIN_USER: MonitoringOrigin = 'user';export const MONITORING_ORIGIN_SYSTEM: MonitoringOrigin = 'system';export interface MonitoringStatus { monitoring: boolean origin: MonitoringOrigin}export const NO_DEVICE = 'NoDevice';export const NO_USER = 'SingleUser';export const SINGLE_USER_PLATFORM_ID: PlayPlatformId = [NO_DEVICE, NO_USER];export const SINGLE_USER_PLATFORM_ID_STR = `${NO_DEVICE}-${NO_USER}`;
export type ComponentAuthType = 'none' | 'interactive' | 'unattended';export const COMPONENT_AUTH_TYPE = { none: 'none', interactive: 'interactive', unattended: 'unattended'} as const satisfies Record<string, ComponentAuthType>;
export type EmittedMSEvent<T = Record<string, any>, K = Record<string, any>,Y = ClientType | SourceType> = { type: Y name: string componentId?: number from: ComponentType data: T options?: K}export interface OptionalCacheUsage { useCachedResult?: boolean}