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.8 kB · 219 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220import { type Logger, loggerDebug, type LogOptions } from "@foxxmd/logging";import { EventEmitter } from "events";import { createContainer } from "iti";import { getConfigDir } from "./common/index.ts";import { WildcardEmitter } from "./common/WildcardEmitter.ts";
import { generateBaseURL } from "./utils/NetworkUtils.ts";import type { PassThrough } from "stream";import type {CacheConfigOptions} from "./common/infrastructure/Atomic.ts";import type { MusicBrainzSingletonMap } from './common/vendor/musicbrainz/MusicbrainzTypes.ts';import { MSCache } from "./common/Cache.ts";import TransformerManager from "./common/transforms/TransformerManager.ts";import type {TransformerCommonConfig} from "../core/Atomic.ts";import prom from 'prom-client';import { CoverArtApiClient } from "./common/vendor/musicbrainz/CoverArtApiClient.ts";import { version, stable } from "./version.ts";import type {DbConcrete} from "./common/database/drizzle/drizzleUtils.ts";import type { MSBackendEventMap } from "./common/infrastructure/MSBackendEventMap.ts";import type { RockskySingletonMap } from "./common/vendor/rocksky/RockskyClientWrapped.ts";import type { CovertArtSingletonMap } from "./common/vendor/musicbrainz/CovertArtApiPool.ts";
let root: ReturnType<typeof createRoot>;export interface RootOptions { baseUrl?: string, port?: number logger: Logger disableWeb?: boolean loggerStream?: PassThrough loggingConfig?: LogOptions cache?: CacheConfigOptions | MSCache | (() => MSCache) mbMap?: MusicBrainzSingletonMap | (() => MusicBrainzSingletonMap) rsMap?: RockskySingletonMap | (() => RockskySingletonMap) caMap?: CovertArtSingletonMap | (() => CovertArtSingletonMap) transformers?: TransformerCommonConfig[] db?: DbConcrete | (() => Promise<DbConcrete>)}
const discovered = new prom.Counter({ name: 'multiscrobbler_source_discovered', help: 'Number of discovered plays for a Source', labelNames: ['name', 'type']});const sourceDead = new prom.Gauge({ name: 'multiscrobbler_source_dead', help: 'Number of dead letter plays for a Source', labelNames: ['name', 'type']});const queuedGauge = new prom.Gauge({ name: 'multiscrobbler_client_queued', help: 'Number of queued plays for a Client', labelNames: ['name', 'type'] });const queuedSourceGauge = new prom.Gauge({ name: 'multiscrobbler_source_queued', help: 'Number of queued plays for a Source', labelNames: ['name', 'type']});const deadLetterGauge = new prom.Gauge({ name: 'multiscrobbler_client_deadletter', help: 'Number of deadletter plays for a Client', labelNames: ['name', 'type']});const scrobbledCounter = new prom.Counter({ name: 'multiscrobbler_client_scrobbled', help: 'Number of discovered plays for a Source', labelNames: ['name', 'type']});
const createRoot = (options: RootOptions = {logger: loggerDebug}) => { const { port = 9078, baseUrl = process.env.BASE_URL, disableWeb: dw, loggerStream, loggingConfig, logger, cache, mbMap, rsMap, caMap, db, transformers = [] } = options || {}; let disableWeb = dw; if(disableWeb === undefined) { disableWeb = process.env.DISABLE_WEB === 'true'; }
let cacheFunc: () => MSCache; let maybeSingletonCache: MSCache;
if(cache instanceof MSCache) { maybeSingletonCache = cache; } else if(typeof cache === 'function') { cacheFunc = cache; } else { maybeSingletonCache = new MSCache(logger, cache); }
let mbFunc: () => MusicBrainzSingletonMap; let maybeSingletonMb: MusicBrainzSingletonMap; if(typeof mbMap === 'function') { mbFunc = mbMap; } else if(maybeSingletonMb !== undefined) { maybeSingletonMb = mbMap; } else { maybeSingletonMb = new Map(); }
let rsFunc: () => RockskySingletonMap; let maybeSingletonRs: RockskySingletonMap; if(typeof rsMap === 'function') { rsFunc = rsMap; } else if(maybeSingletonRs !== undefined) { maybeSingletonRs = rsMap; } else { maybeSingletonRs = new Map(); }
let caFunc: () => CovertArtSingletonMap; let maybeSingletonCa: CovertArtSingletonMap; if(typeof caMap === 'function') { caFunc = caMap; } else if(maybeSingletonCa !== undefined) { maybeSingletonCa = caMap; } else { maybeSingletonCa = new Map(); }
let dbFunc: () => Promise<DbConcrete>; if(typeof db === 'function') { dbFunc = db; } else { dbFunc = async () => db; }
const cEmitter = new WildcardEmitter<MSBackendEventMap>(); // do nothing, just catch cEmitter.on('error', (e) => { logger.warn(new Error('Client emitter threw an error', {cause: e})); }); const sEmitter = new WildcardEmitter<MSBackendEventMap>(); sEmitter.on('error', (e) => { logger.warn(new Error('Source emitter threw an error', {cause: e})); });
const transformerManager = new TransformerManager(logger, maybeSingletonCache !== undefined ? maybeSingletonCache : cacheFunc()); for(const c of transformers) { transformerManager.addTransformerConfig(c); } if(transformers.length === 0) { logger.debug('No user-supplied transformer configs were found.'); }
const cacheApi = (maybeSingletonCache !== undefined ? maybeSingletonCache : cacheFunc()).cacheApi; const coverArtApi = new CoverArtApiClient('', {}, {logger, cache: cacheApi});
const portVal: number | string = process.env.PORT ?? port;
const versionParts: string[] = []; if(stable === version) { versionParts.push(stable); } else { if(stable !== 'unknown') { versionParts.push(stable); } versionParts.push(version); }
return createContainer().add({ version: versionParts.join('-'), configDir: getConfigDir(), isProd: process.env.NODE_ENV !== undefined && (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'prod'), // @ts-ignore port: (Number.isInteger(portVal) ? portVal : Number.parseInt(portVal)) as number, disableWeb, clientEmitter: () => cEmitter, sourceEmitter: () => sEmitter, notifierEmitter: () => new EventEmitter(), loggerStream, loggingConfig, sourceMetics: { discovered: discovered, queued: queuedSourceGauge, deadLetter: sourceDead //issues: sourceIssues }, clientMetrics: { queued: queuedGauge, scrobbled: scrobbledCounter, //issues: issuesClientGauge, deadLetter: deadLetterGauge }, logger: logger, transformerManager, cache: () => maybeSingletonCache !== undefined ? () => maybeSingletonCache : cacheFunc, mbMap: () => maybeSingletonMb !== undefined ? () => maybeSingletonMb : mbFunc, rsMap: () => maybeSingletonRs !== undefined ? () => maybeSingletonRs : rsFunc, caMap: () => maybeSingletonCa !== undefined ? () => maybeSingletonCa : caFunc, coverArtApi, db: () => dbFunc }).add((items) => { const localUrl = generateBaseURL(baseUrl, items.port) return { localUrl, hasDefinedBaseUrl: baseUrl !== undefined, isSubPath: localUrl.pathname !== '/' && localUrl.pathname.length > 0 } });}
export const getRoot = (options?: RootOptions) => { if(root === undefined) { root = createRoot(options); } return root;}
export default createRoot;