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.
6.9 kB · 207 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207import type {Logger} from "@foxxmd/logging";import type { searchAndReplace as searchAndReplaceFunc} from "@foxxmd/regex-buddy-core";import { testMaybeRegex as testMaybeRegexFunc } from "@foxxmd/regex-buddy-core";import type {PlayObject, PlayObjectMinimal} from "../../core/Atomic.ts";
import { type ConditionalSearchAndReplaceRegExp, type ConditionalSearchAndReplaceTerm, type ExternalMetadataTerm, type PlayTransformStage, type SearchAndReplaceTerm, STAGE_TYPES, type StageTypedConfig, type WhenConditionsConfig, type WhenParts} from "../../core/Transform.ts";import dayjs from "dayjs";
export const isWhenCondition = (val: unknown): val is WhenParts<string> => { if (val !== null && typeof val === 'object') { if ('artists' in val && typeof val.artists !== 'string') { return false; } if ('title' in val && typeof val.title !== 'string') { return false; } if ('album' in val && typeof val.album !== 'string') { return false; } return true; } return false;}export const isWhenConditionConfig = (val: unknown): val is WhenConditionsConfig => { return Array.isArray(val) && val.every(isWhenCondition);}export const configValToSearchReplace = (val: string | undefined | object): ConditionalSearchAndReplaceRegExp | undefined => { if (val === undefined || val === null) { return undefined; } if (typeof val === 'string') { return { search: val, replace: '' } } if (isConditionalSearchAndReplace(val)) { return val as ConditionalSearchAndReplaceRegExp; } throw new Error(`Value must be a string or an object containing 'search: string' and 'replace: 'string'. Given: ${val}`);}export const isConditionalSearchAndReplace = (val: unknown): val is ConditionalSearchAndReplaceRegExp => { return typeof val === 'object' && ('search' in val && typeof val.search === 'string') && ('replace' in val && typeof val.replace === 'string') && (!('when' in val) || isWhenConditionConfig(val.when));}
export const isSearchAndReplaceTerm = (val: unknown | string | ConditionalSearchAndReplaceTerm): val is SearchAndReplaceTerm => { const tf = typeof val; if(tf === 'string') { return true; } if(!(tf == 'object')) { throw new Error(`Must be a string or an object, but found ${tf}`); } if(tf === null) { throw new Error('Cannot be null'); } return isConditionalSearchAndReplace(val);}
export const isExternalMetadataTerm = (val: unknown): val is ExternalMetadataTerm => { if(val === undefined) { return true; } const tf = typeof val; if(tf === 'boolean') { return true; } if(tf === null) { throw new Error(`Value is null but must be one of: true, undefined, or object with 'when'`); } if(tf === 'object') { if(isWhenConditionConfig(val)) { return true; } throw new Error(`Value is not a proper 'when' object`); } throw new Error(`Value is type of ${tf} but must be one of: boolean, undefined, or object with 'when'`);}
export const isStageTyped = (val: unknown): val is StageTypedConfig => { if(typeof val !== 'object' || val === null) { return false; } return 'type' in val;}
export const isPlayTransformStage = (val: object | Partial<PlayTransformStage<SearchAndReplaceTerm[]>>): val is PlayTransformStage<SearchAndReplaceTerm[]> => { if (!('type' in val)) { throw new Error(`Stage is missing 'type'. Must be one of: ${STAGE_TYPES.join(', ')}`); } if (!STAGE_TYPES.includes(val.type)) { throw new Error(`Stage has invalid 'type'. Must be one of: ${STAGE_TYPES.join(', ')}`); }
for (const k of ['artist', 'title', 'album']) { if (!(k in val)) { continue; } if (val.type === 'user') { if (!Array.isArray(val[k])) { throw new Error(`${k} must be an array`); } try { isSearchAndReplaceTerm(val[k]); } catch (e) { throw new Error(`Property '${k}' was not a valid type`, { cause: e }); } } else { try { isExternalMetadataTerm(val[k]);
} catch (e) { throw new Error(`Property '${k}' was not a valid type`, { cause: e }); } } }
return true;}
export const isUserStage = <T>(val: StageTypedConfig): val is StageTypedConfig => { return val.type === 'user';}
export const testWhen = (parts: WhenParts<string>, play: PlayObject, options?: SuppliedRegex): boolean => { const { testMaybeRegex = testMaybeRegexFunc, } = options || {}
if(parts.title !== undefined) { if(!testMaybeRegex(parts.title, play.data.track ?? '')[0]) { return false; } } if(parts.artists !== undefined) { // allows user to test if artists are empty const artists = parts.artists.length === 0 ? [{name: ''}] : play.data.artists; if(artists.every(x => !testMaybeRegex(parts.artists, x.name)[0])) { return false; } } if(parts.album !== undefined) { if(!testMaybeRegex(parts.album, play.data.album ?? '')[0]) { return false; } } if(parts.art !== undefined) { if(parts.art.trim() === '') { // user is testing to see if there is no art // if test is empty string and there is no art then it passes return play.meta.art === undefined || Object.keys(play.meta.art).length === 0 } const {art = {}} = play.meta; for(const [k,v] of Object.entries(art)) { // passes if any art url matches the search if(testMaybeRegex(parts.art, v ?? '')[0]) { return true; } } // otherwise fails return false; } return true;}
export const testWhenConditions = (when: WhenConditionsConfig, play: PlayObject, options?: SuppliedRegex) => when.some(x => testWhen(x, play, options));
export interface SuppliedRegex { searchAndReplace?: typeof searchAndReplaceFunc, testMaybeRegex?: typeof testMaybeRegexFunc,}
export interface TransformPlayPartsOptions { logger?: () => Logger, regex?: SuppliedRegex}
export const baseFormatPlayObj = (data: any, play: PlayObjectMinimal): PlayObject => { const basePlay: PlayObject = { data: { ...play.data }, meta: { seenAt: dayjs(), ...play.meta, }, original: { data, play, } } basePlay.original.play.meta.seenAt = basePlay.meta.seenAt; return basePlay;}