import { Logger, loggerDebug, LogOptions } from "@foxxmd/logging"; import { EventEmitter } from "events"; import { createContainer } from "iti"; import path from "path"; import { projectDir } from "./common/index.js"; import { WildcardEmitter } from "./common/WildcardEmitter.js"; import { generateBaseURL } from "./utils/NetworkUtils.js"; import { PassThrough } from "stream"; import { CacheConfigOptions, MusicBrainzSingletonMap } from "./common/infrastructure/Atomic.js"; import { MSCache } from "./common/Cache.js"; import TransformerManager from "./common/transforms/TransformerManager.js"; import { TransformerCommonConfig } from "../core/Atomic.js"; import prom, { Counter, Gauge } from 'prom-client'; import { CoverArtApiClient } from "./common/vendor/musicbrainz/CoverArtApiClient.js"; import { version } from "./version.js"; import { StaggerOptions } from "./utils/AsyncUtils.js"; import { DbConcrete } from "./common/database/drizzle/drizzleUtils.js"; let root: ReturnType; export interface RootOptions { baseUrl?: string, port?: number logger: Logger disableWeb?: boolean loggerStream?: PassThrough loggingConfig?: LogOptions cache?: CacheConfigOptions | MSCache | (() => MSCache) mbMap?: MusicBrainzSingletonMap | (() => MusicBrainzSingletonMap) transformers?: TransformerCommonConfig[] db?: DbConcrete | (() => Promise) } const discovered = new prom.Counter({ name: 'multiscrobbler_source_discovered', help: 'Number of discovered 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 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, db, transformers = [] } = options || {}; const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`); 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 dbFunc: () => Promise; if(typeof db === 'function') { dbFunc = db; } else { dbFunc = async () => db; } const cEmitter = new WildcardEmitter(); // do nothing, just catch cEmitter.on('error', (e) => null); const sEmitter = new WildcardEmitter(); sEmitter.on('error', (e) => { const f = e; }); const transformerManager = new TransformerManager(logger, maybeSingletonCache !== undefined ? maybeSingletonCache : cacheFunc()); for(const c of transformers) { try { transformerManager.register(c); } catch (e) { logger.warn(new Error('Could not register a transformer', {cause: e})); } } if(transformers.length === 0) { logger.debug('No user-supplied transformer configs were found.'); } if(!transformerManager.hasTransformerType('user')) { transformerManager.register({type: 'user', name: 'MSDefault'}); } if(!transformerManager.hasTransformerType('native')) { transformerManager.register({type: 'native', name: 'MSDefault'}); } const cacheApi = (maybeSingletonCache !== undefined ? maybeSingletonCache : cacheFunc()).cacheApi; const coverArtApi = new CoverArtApiClient('', {}, {logger, cache: cacheApi}); const portVal: number | string = process.env.PORT ?? port; return createContainer().add({ version, configDir: configDir, 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, //issues: sourceIssues }, clientMetrics: { queued: queuedGauge, scrobbled: scrobbledCounter, //issues: issuesClientGauge, deadLetter: deadLetterGauge }, logger: logger, transformerManager, cache: () => maybeSingletonCache !== undefined ? () => maybeSingletonCache : cacheFunc, mbMap: () => maybeSingletonMb !== undefined ? () => maybeSingletonMb : mbFunc, 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;