diff --git a/docsite/docs/configuration/clients/_env_configs/_maloja.md b/docsite/docs/configuration/clients/_env_configs/_maloja.md new file mode 100644 index 00000000..33d0628a --- /dev/null +++ b/docsite/docs/configuration/clients/_env_configs/_maloja.md @@ -0,0 +1,7 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | -------------------- | ----------------------------- | +| _**`MALOJA_ID`**_ | string | | A globally unique ID | +| `MALOJA_NAME` | string | Value of `MALOJA_ID` | A vanity name | +| `MALOJA_ENABLE` | boolean | | | +| _**`MALOJA_URL`**_ | string | | Base URL of your installation | +| `MALOJA_API_KEY` | string | fsdfsd | | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/maloja.mdx b/docsite/docs/configuration/clients/maloja.mdx index 1d5edc11..9c11f3b4 100644 --- a/docsite/docs/configuration/clients/maloja.mdx +++ b/docsite/docs/configuration/clients/maloja.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import JsonConfig from '!!raw-loader!@site/../config/maloja.json.example'; +import ENVConfig from "@site/docs/configuration/clients/_env_configs/_maloja.md" Setup a [Maloja server](https://github.com/krateng/maloja?tab=readme-ov-file#how-to-install) if you have not already done this. @@ -38,10 +39,5 @@ Finally, add the Maloja server URL and API Key to the configuration type you cho ## Configuration - | Environmental Variable | Required? | Default | Description | - | ---------------------- | --------- | ------- | ------------------------------- | - | `MALOJA_ID` | Yes | | A unique ID | - | `MALOJA_URL` | Yes | | Base URL of your installation | - | `MALOJA_API_KEY` | Yes | | Api Key | - | `MALOJA_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/src/components/GenericConfiguration.mdx b/docsite/src/components/GenericConfiguration.mdx index 15342f80..863853c6 100644 --- a/docsite/src/components/GenericConfiguration.mdx +++ b/docsite/src/components/GenericConfiguration.mdx @@ -11,6 +11,8 @@ import {useWindowSize} from '@docusaurus/theme-common'; {props.children} + + _**Bold/Italic**_ = Required diff --git a/package-lock.json b/package-lock.json index ee166195..75d9ff20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -182,6 +182,7 @@ "git-cliff": "^2.12.0", "globals": "^17.7.0", "ky": "^2.0.2", + "markdown-table": "^3.0.4", "mocha": "^10.3.0", "mockdate": "^3.0.5", "msw": "^2.14.6", @@ -15190,6 +15191,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index 2877ad83..bb3ec336 100644 --- a/package.json +++ b/package.json @@ -222,6 +222,7 @@ "git-cliff": "^2.12.0", "globals": "^17.7.0", "ky": "^2.0.2", + "markdown-table": "^3.0.4", "mocha": "^10.3.0", "mockdate": "^3.0.5", "msw": "^2.14.6", diff --git a/src/backend/common/infrastructure/config/client/maloja.ts b/src/backend/common/infrastructure/config/client/maloja.ts index 138cb67a..31fe2828 100644 --- a/src/backend/common/infrastructure/config/client/maloja.ts +++ b/src/backend/common/infrastructure/config/client/maloja.ts @@ -31,6 +31,15 @@ export const malojaClientDataSchema = malojaDataSchema.extend(commonClientDataSc export type MalojaClientData = z.infer; +const envDataSchema = z.object({ + MALOJA_URL: z.string().meta({description: 'Base URL of your installation'}), + MALOJA_API_KEY: z.string().default('fsdfsd') +}); + +export const envSchemas = { + data: envDataSchema +}; + export const malojaClientConfigSchema = z.object({ ...commonClientConfigSchema.shape, /** @@ -54,4 +63,4 @@ export const malojaClientAIOConfigSchema = z.object({ type: z.literal('maloja'), }).meta({title: 'Maloja'}); -export type MalojaClientAIOConfig = z.infer; +export type MalojaClientAIOConfig = z.infer; \ No newline at end of file diff --git a/src/backend/common/infrastructure/config/common.ts b/src/backend/common/infrastructure/config/common.ts index 3f3ed479..ab940df2 100644 --- a/src/backend/common/infrastructure/config/common.ts +++ b/src/backend/common/infrastructure/config/common.ts @@ -1,5 +1,7 @@ import { stripIndents } from "common-tags"; import * as z from "zod"; +import type { PlayTransformHooks, ExternalMetadataTerm } from "../../../../core/Transform.ts"; +import type { CommonClientOptions } from "./client/index.ts"; export const commonConfigPrimitivesSchema = z.object({ name: z.string().optional(), @@ -140,4 +142,65 @@ export const monitorOptionsSchema = z.object({ description: 'Set the default behavior for wether this component should automatically monitor any activity, or scrobble, it encounters' }) }) -export type MonitorOptions = z.infer; \ No newline at end of file +export type MonitorOptions = z.infer; + +export const transformPresetEnv = (prefix: string, existing: T = undefined): undefined | T => { + + const env = process.env[`${prefix}_TRANSFORMS`]; + if (env === undefined || env.trim() === '') { + return existing; + } + + const popts: PlayTransformHooks = { + preCompare: [] + }; + for (const p of env.split(',').map(x => x.trim().toLocaleLowerCase())) { + switch (p) { + case 'native': + popts.preCompare.push({ type: 'native' }); + break; + case 'musicbrainz': + popts.preCompare.push({ type: 'musicbrainz' }); + break; + } + } + + // @ts-expect-error T is fine + return { + ...(existing || {}), + playTransform: popts + }; +}; + + +type CommonComponentEnvShape = { + [K in `${T}_ID`]: z.ZodString +} & { + [K in `${T}_NAME`]: z.ZodOptional +} & { + [K in `${T}_ENABLE`]: z.ZodOptional> +}; + +export const generateCommonComponentEnvConfigSchema = (prefix: T) => + z.object({ + [`${prefix}_ID`]: z.string().meta({description: 'A globally unique ID'}), + [`${prefix}_NAME`]: z.string().optional().meta({description: 'A vanity name', default: `Value of \`${prefix}_ID\``}), + [`${prefix}_ENABLE`]: z.stringbool().optional() + } as CommonComponentEnvShape); + +type CommonComponentEnvConfigParsed = { + [K in `${T}_ID`]: string +} & { + [K in `${T}_NAME`]?: string +} & { + [K in `${T}_ENABLE`]?: boolean +}; + +export const commonComponentEnvConfigToConfigPrimitives = (prefix: T, envConfig: CommonComponentEnvConfigParsed): CommonConfigPrimitives => { + const raw = envConfig as Record; + return { + id: raw[`${prefix}_ID`] as string, + name: raw[`${prefix}_NAME`] as string | undefined, + enable: raw[`${prefix}_ENABLE`] as boolean | undefined + }; +}; diff --git a/src/backend/scrobblers/ScrobbleClients.ts b/src/backend/scrobblers/ScrobbleClients.ts index 85b89c84..f8a373e1 100644 --- a/src/backend/scrobblers/ScrobbleClients.ts +++ b/src/backend/scrobblers/ScrobbleClients.ts @@ -20,14 +20,13 @@ import type AbstractScrobbleClient from "./AbstractScrobbleClient.ts"; import type {KoitoClientConfig, KoitoData} from '../common/infrastructure/config/client/koito.ts'; import type {TealClientConfig, TealData} from '../common/infrastructure/config/client/tealfm.ts'; import type {RockSkyClientConfig, RockSkyData} from '../common/infrastructure/config/client/rocksky.ts'; -import type {CommonClientOptions} from '../common/infrastructure/config/client/index.ts'; -import type {ExternalMetadataTerm, PlayTransformHooks} from '../../core/Transform.ts'; import type {LibrefmClientConfig, LibrefmData} from '../common/infrastructure/config/client/librefm.ts'; import clone from 'clone'; import type {DiscordClientConfig, DiscordData} from '../common/infrastructure/config/client/discord.ts'; import { stripIndents } from 'common-tags'; import { normalizeStr, type StringNormalizationOptions } from '../utils/StringUtils.ts'; import { prettifyError, ZodError } from 'zod'; +import { transformPresetEnv } from '../common/infrastructure/config/common.ts'; type groupedNamedConfigs = {[key: string]: ParsedConfig[]}; @@ -604,33 +603,4 @@ ${sources.join('\n')}`); this.logger.trace(`These clients were filtered from scrobbling from Source '${scrobbleFrom}' => ${excluded.join(' | ')}`); } } -} - -const transformPresetEnv = (prefix: string, existing: T = undefined): undefined | T => { - - const env = process.env[`${prefix}_TRANSFORMS`]; - if(env === undefined || env.trim() === '') { - return existing; - } - - const popts: PlayTransformHooks = { - preCompare: [ - ] - } - for(const p of env.split(',').map(x => x.trim().toLocaleLowerCase())) { - switch(p) { - case 'native': - popts.preCompare.push({type: 'native'}); - break; - case 'musicbrainz': - popts.preCompare.push({type: 'musicbrainz'}); - break; - } - } - - // @ts-expect-error T is fine - return { - ...(existing || {}), - playTransform: popts - }; -} +} \ No newline at end of file diff --git a/src/backend/utils/SchemaDocsStaticUtils.ts b/src/backend/utils/SchemaDocsStaticUtils.ts index c2b237e9..30f61110 100644 --- a/src/backend/utils/SchemaDocsStaticUtils.ts +++ b/src/backend/utils/SchemaDocsStaticUtils.ts @@ -5,9 +5,36 @@ import { clientConfigSchemaMap } from '../common/infrastructure/config/client/cl import { sourceConfigSchemaMap } from '../common/infrastructure/config/source/sources.ts'; import { projectRootDir } from "../common/infrastructure/Atomic.ts"; import { aioConfigSchema } from "../common/infrastructure/config/aioConfig.ts"; +import { envSchemas } from "../common/infrastructure/config/client/maloja.ts"; +import { generateCommonComponentEnvConfigSchema } from "../common/infrastructure/config/common.ts"; +import { zodObjectToTableColumns } from "./ZodUtils.ts"; +import {markdownTable} from 'markdown-table' mkdirSync(resolve(projectRootDir, 'docsite/static/schemas'), {recursive: true}); +const common = generateCommonComponentEnvConfigSchema('MALOJA'); +const schema = z.object({ + ...common.shape, + ...envSchemas.data.shape +}); +const col = zodObjectToTableColumns(schema, 'out'); + +const mdCols: string[][] = col.map((x) => { + const name = `\`${x.title}\``; + return [ + x.required ? `**${name}**` : name, + x.type, + (x.default ?? '').toString(), + x.description ?? '' + ] +}); +const tableContent = markdownTable([ + ['Environmental Variable', 'Type', 'Default', 'Description'], + ...mdCols +]); + +writeFileSync(resolve(projectRootDir, `docsite/static/schemas/maloja-env-test.md`), tableContent); + const generateSchema = (schema: z.ZodType, reused: z.core.ToJSONSchemaParams['reused'] = 'inline') => z.toJSONSchema(schema, { io: "input", reused, diff --git a/src/backend/utils/ZodUtils.ts b/src/backend/utils/ZodUtils.ts new file mode 100644 index 00000000..33e4a410 --- /dev/null +++ b/src/backend/utils/ZodUtils.ts @@ -0,0 +1,52 @@ +import z from 'zod'; + +export interface TableColumn { + title: string + required: boolean + type: string + default: unknown + description: string | undefined +} + +export type PipeUnwrapDirection = 'in' | 'out'; + +const unwrapZodType = (schema: z.ZodTypeAny, pipeDirection: PipeUnwrapDirection = 'in'): z.ZodTypeAny => { + let current = schema; + while (current instanceof z.ZodOptional || current instanceof z.ZodNullable || current instanceof z.ZodDefault || current instanceof z.ZodPipe) { + current = (current instanceof z.ZodPipe ? current[pipeDirection] : current.unwrap()) as z.ZodTypeAny; + } + return current; +}; + +const getExplicitDefault = (schema: z.ZodTypeAny): unknown => { + let current = schema; + while (true) { + if (current instanceof z.ZodDefault) { + return current.def.defaultValue; + } + if (current instanceof z.ZodOptional || current instanceof z.ZodNullable) { + current = current.unwrap() as z.ZodTypeAny; + continue; + } + return undefined; + } +}; + +/** + * Takes a z.object and returns objects that can be used to build table rows for docs + * + * Based loosely on matejchalk/zod2md + * */ +export const zodObjectToTableColumns = (schema: z.ZodObject, pipeDirection: PipeUnwrapDirection = 'in'): TableColumn[] => + Object.entries(schema.shape).map(([key, value]) => { + const fieldSchema = value as z.ZodTypeAny; + const meta = fieldSchema.meta() as Record | undefined; + + return { + title: (meta?.title as string | undefined) ?? key, + required: !fieldSchema.isOptional(), + type: unwrapZodType(fieldSchema, pipeDirection).def.type, + default: getExplicitDefault(fieldSchema) ?? (meta?.default as unknown), + description: meta?.description as string | undefined + }; + }); \ No newline at end of file -- 2.51.2 From 74605caf157e86655e58a3887b50b7b4e7bb42d2 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 13:10:13 +0000 Subject: [PATCH 02/35] add interface for env schemas --- src/backend/common/infrastructure/config/client/maloja.ts | 4 ++-- src/backend/common/infrastructure/config/common.ts | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/backend/common/infrastructure/config/client/maloja.ts b/src/backend/common/infrastructure/config/client/maloja.ts index 31fe2828..9091e196 100644 --- a/src/backend/common/infrastructure/config/client/maloja.ts +++ b/src/backend/common/infrastructure/config/client/maloja.ts @@ -1,6 +1,6 @@ import * as z from "zod"; import {componentTypeSchema} from "../../../../../core/Atomic.ts"; -import {requestRetryOptionsSchema} from "../common.ts"; +import {requestRetryOptionsSchema, type EnvSchemas} from "../common.ts"; import {commonClientConfigSchema, commonClientDataSchema} from "./index.ts"; export const malojaDataSchema = z.object({ @@ -36,7 +36,7 @@ const envDataSchema = z.object({ MALOJA_API_KEY: z.string().default('fsdfsd') }); -export const envSchemas = { +export const envSchemas: EnvSchemas = { data: envDataSchema }; diff --git a/src/backend/common/infrastructure/config/common.ts b/src/backend/common/infrastructure/config/common.ts index ab940df2..21d96443 100644 --- a/src/backend/common/infrastructure/config/common.ts +++ b/src/backend/common/infrastructure/config/common.ts @@ -204,3 +204,8 @@ export const commonComponentEnvConfigToConfigPrimitives = (pre enable: raw[`${prefix}_ENABLE`] as boolean | undefined }; }; + +export interface EnvSchemas { + data?: z.ZodObject + options?: z.ZodObject +} \ No newline at end of file -- 2.51.2 From 1bd10326eb53d4fe2913eec12edd9317cc53568f Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 13:31:52 +0000 Subject: [PATCH 03/35] add generic client env data validation interface --- .../common/infrastructure/config/client/index.ts | 5 +++++ .../common/infrastructure/config/client/maloja.ts | 14 ++++++++++---- src/backend/common/infrastructure/config/common.ts | 7 +------ src/backend/utils/SchemaDocsStaticUtils.ts | 2 +- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/backend/common/infrastructure/config/client/index.ts b/src/backend/common/infrastructure/config/client/index.ts index dc277222..74e3a9fb 100644 --- a/src/backend/common/infrastructure/config/client/index.ts +++ b/src/backend/common/infrastructure/config/client/index.ts @@ -184,3 +184,8 @@ export const commonClientConfigSchema = z.object({ }); export type CommonClientConfig = Omit, 'data'> & { data?: CommonClientData }; + +export interface EnvClientSchema { + env: Y + toConfig: (parsed: z.output) => Partial> +} \ No newline at end of file diff --git a/src/backend/common/infrastructure/config/client/maloja.ts b/src/backend/common/infrastructure/config/client/maloja.ts index 9091e196..eb58a0d1 100644 --- a/src/backend/common/infrastructure/config/client/maloja.ts +++ b/src/backend/common/infrastructure/config/client/maloja.ts @@ -1,7 +1,7 @@ import * as z from "zod"; import {componentTypeSchema} from "../../../../../core/Atomic.ts"; import {requestRetryOptionsSchema, type EnvSchemas} from "../common.ts"; -import {commonClientConfigSchema, commonClientDataSchema} from "./index.ts"; +import {commonClientConfigSchema, commonClientDataSchema, type EnvClientSchema} from "./index.ts"; export const malojaDataSchema = z.object({ ...requestRetryOptionsSchema.shape, @@ -33,11 +33,17 @@ export type MalojaClientData = z.infer; const envDataSchema = z.object({ MALOJA_URL: z.string().meta({description: 'Base URL of your installation'}), - MALOJA_API_KEY: z.string().default('fsdfsd') + MALOJA_API_KEY: z.string() }); -export const envSchemas: EnvSchemas = { - data: envDataSchema +export const envSchemas: EnvClientSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + url: partial.MALOJA_URL, + apiKey: partial.MALOJA_API_KEY + } + }) }; export const malojaClientConfigSchema = z.object({ diff --git a/src/backend/common/infrastructure/config/common.ts b/src/backend/common/infrastructure/config/common.ts index 21d96443..56d429c2 100644 --- a/src/backend/common/infrastructure/config/common.ts +++ b/src/backend/common/infrastructure/config/common.ts @@ -203,9 +203,4 @@ export const commonComponentEnvConfigToConfigPrimitives = (pre name: raw[`${prefix}_NAME`] as string | undefined, enable: raw[`${prefix}_ENABLE`] as boolean | undefined }; -}; - -export interface EnvSchemas { - data?: z.ZodObject - options?: z.ZodObject -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/backend/utils/SchemaDocsStaticUtils.ts b/src/backend/utils/SchemaDocsStaticUtils.ts index 30f61110..bc4c490d 100644 --- a/src/backend/utils/SchemaDocsStaticUtils.ts +++ b/src/backend/utils/SchemaDocsStaticUtils.ts @@ -15,7 +15,7 @@ mkdirSync(resolve(projectRootDir, 'docsite/static/schemas'), {recursive: true}); const common = generateCommonComponentEnvConfigSchema('MALOJA'); const schema = z.object({ ...common.shape, - ...envSchemas.data.shape + ...envSchemas.env.shape }); const col = zodObjectToTableColumns(schema, 'out'); -- 2.51.2 From 7cf105935ebabca317df26e462141765c007bcc5 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 09:59:57 -0400 Subject: [PATCH 04/35] feat: Implement remaining env schemas for clients --- .../infrastructure/config/client/discord.ts | 27 ++++++++++++++++++- .../infrastructure/config/client/koito.ts | 19 ++++++++++++- .../infrastructure/config/client/lastfm.ts | 21 ++++++++++++++- .../infrastructure/config/client/librefm.ts | 23 +++++++++++++++- .../config/client/listenbrainz.ts | 19 ++++++++++++- .../infrastructure/config/client/maloja.ts | 2 +- .../infrastructure/config/client/rocksky.ts | 19 ++++++++++++- .../infrastructure/config/client/tealfm.ts | 17 +++++++++++- 8 files changed, 139 insertions(+), 8 deletions(-) diff --git a/src/backend/common/infrastructure/config/client/discord.ts b/src/backend/common/infrastructure/config/client/discord.ts index d0e3461b..90c47118 100644 --- a/src/backend/common/infrastructure/config/client/discord.ts +++ b/src/backend/common/infrastructure/config/client/discord.ts @@ -1,5 +1,5 @@ import * as z from "zod"; -import {commonClientConfigSchema, commonClientDataSchema} from "./index.ts"; +import {commonClientConfigSchema, commonClientDataSchema, type EnvClientSchema} from "./index.ts"; import {componentTypeSchema} from "../../../../../core/Atomic.ts"; export const statusTypeSchema = z.union([z.literal("online"), z.literal("idle"), z.literal("dnd"), z.literal("invisible")]); @@ -20,6 +20,31 @@ export const discordDataSchema = z.object({ export type DiscordData = z.infer; +const envDataSchema = z.object({ + DISCORD_TOKEN: z.string().optional(), + DISCORD_ARTWORK: z.string().optional(), + DISCORD_APPLICATION_ID: z.string().optional(), + DISCORD_IPC_LOCATIONS: z.string().optional(), + DISCORD_ARTWORK_DEFAULT_URL: z.string().optional(), + DISCORD_STATUS_OVERRIDE_ALLOW: z.string().optional(), + DISCORD_LISTENING_ACTIVITY_ALLOW: z.string().optional(), +}); + +export const envSchemas: EnvClientSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + token: partial.DISCORD_TOKEN, + artwork: partial.DISCORD_ARTWORK, + applicationId: partial.DISCORD_APPLICATION_ID, + ipcLocations: partial.DISCORD_IPC_LOCATIONS, + artworkDefaultUrl: partial.DISCORD_ARTWORK_DEFAULT_URL, + statusOverrideAllow: partial.DISCORD_STATUS_OVERRIDE_ALLOW, + listeningActivityAllow: partial.DISCORD_LISTENING_ACTIVITY_ALLOW + } + }) +}; + export const discordClientDataSchema = discordDataSchema.extend(commonClientDataSchema.shape); export type DiscordClientData = z.infer; diff --git a/src/backend/common/infrastructure/config/client/koito.ts b/src/backend/common/infrastructure/config/client/koito.ts index 41d22aad..d7decf44 100644 --- a/src/backend/common/infrastructure/config/client/koito.ts +++ b/src/backend/common/infrastructure/config/client/koito.ts @@ -2,7 +2,7 @@ import * as z from "zod"; import type {UnixTimestamp} from "../../../../../core/Atomic.ts"; import {componentTypeSchema} from "../../../../../core/Atomic.ts"; import {requestRetryOptionsSchema} from "../common.ts"; -import {commonClientConfigSchema, commonClientDataSchema} from "./index.ts"; +import {commonClientConfigSchema, commonClientDataSchema, type EnvClientSchema} from "./index.ts"; export interface ListensResponse { items: ListenObjectResponse[] @@ -78,6 +78,23 @@ export const koitoDataSchema = z.object({ export type KoitoData = z.infer; +const envDataSchema = z.object({ + KOITO_URL: z.string().meta({description: 'URL for the Koito server'}), + KOITO_TOKEN: z.string().meta({description: 'User token for the user to scrobble for'}), + KOITO_USER: z.string().meta({description: 'Username of the user to scrobble for'}), +}); + +export const envSchemas: EnvClientSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + url: partial.KOITO_URL, + token: partial.KOITO_TOKEN, + username: partial.KOITO_USER + } + }) +}; + export const koitoClientDataSchema = koitoDataSchema.extend(commonClientDataSchema.shape).meta({title: 'KoitoData'}); export type KoitoClientData = z.infer; diff --git a/src/backend/common/infrastructure/config/client/lastfm.ts b/src/backend/common/infrastructure/config/client/lastfm.ts index a69b910e..81ac43d8 100644 --- a/src/backend/common/infrastructure/config/client/lastfm.ts +++ b/src/backend/common/infrastructure/config/client/lastfm.ts @@ -1,6 +1,6 @@ import * as z from "zod"; import {requestRetryOptionsSchema} from "../common.ts"; -import {commonClientConfigSchema, commonClientDataSchema, commonClientOptionsSchema, nowPlayingOptionsSchema} from "./index.ts"; +import {commonClientConfigSchema, commonClientDataSchema, commonClientOptionsSchema, nowPlayingOptionsSchema, type EnvClientSchema} from "./index.ts"; export const lastfmDataSchema = z.object({ ...commonClientDataSchema.shape, @@ -44,6 +44,25 @@ export const lastfmDataSchema = z.object({ export type LastfmData = z.infer; +const envDataSchema = z.object({ + LASTFM_API_KEY: z.string().meta({description: 'API Key generated from Last.fm/Libre.fm account'}), + LASTFM_SECRET: z.string().meta({description: 'Secret generated from Last.fm/Libre.fm account'}), + LASTFM_REDIRECT_URI: z.string().optional().meta({description: 'Optional URI to use for callback.'}), + LASTFM_SESSION: z.string().optional().meta({description: 'Optional session id returned from a completed auth flow'}), +}); + +export const envSchemas: EnvClientSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + apiKey: partial.LASTFM_API_KEY, + secret: partial.LASTFM_SECRET, + redirectUri: partial.LASTFM_REDIRECT_URI, + session: partial.LASTFM_SESSION + } + }) +}; + export const lastfmClientOptionsSchema = z.object({ ...commonClientOptionsSchema.shape, ...nowPlayingOptionsSchema.shape, diff --git a/src/backend/common/infrastructure/config/client/librefm.ts b/src/backend/common/infrastructure/config/client/librefm.ts index 7ee3a628..73ba46ab 100644 --- a/src/backend/common/infrastructure/config/client/librefm.ts +++ b/src/backend/common/infrastructure/config/client/librefm.ts @@ -1,6 +1,6 @@ import * as z from "zod"; import {requestRetryOptionsSchema} from "../common.ts"; -import {commonClientConfigSchema, commonClientDataSchema} from "./index.ts"; +import {commonClientConfigSchema, commonClientDataSchema, type EnvClientSchema} from "./index.ts"; import {lastfmClientOptionsSchema} from "./lastfm.ts"; export const librefmDataSchema = z.object({ @@ -62,6 +62,27 @@ export const librefmDataSchema = z.object({ export type LibrefmData = z.infer; +const envDataSchema = z.object({ + LIBREFM_API_KEY: z.string().optional().meta({description: 'Optional Secret for Libre.fm account'}), + LIBREFM_SECRET: z.string().optional().meta({description: 'Optional Secret for Libre.fm account'}), + LIBREFM_REDIRECT_URI: z.string().optional().meta({description: 'Optional URI to use for callback.'}), + LIBREFM_SESSION: z.string().optional().meta({description: 'Optional session id returned from a completed auth flow'}), + LIBREFM_URLBASE: z.string().optional().meta({description: '(Optional) The host and path prefix for your Libre.fm instance'}), +}); + +export const envSchemas: EnvClientSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + apiKey: partial.LIBREFM_API_KEY, + secret: partial.LIBREFM_SECRET, + redirectUri: partial.LIBREFM_REDIRECT_URI, + session: partial.LIBREFM_SESSION, + urlBase: partial.LIBREFM_URLBASE + } + }) +}; + export const librefmClientOptionsSchema = z.object({ ...lastfmClientOptionsSchema.shape, }); diff --git a/src/backend/common/infrastructure/config/client/listenbrainz.ts b/src/backend/common/infrastructure/config/client/listenbrainz.ts index ede8c76f..bed0a33f 100644 --- a/src/backend/common/infrastructure/config/client/listenbrainz.ts +++ b/src/backend/common/infrastructure/config/client/listenbrainz.ts @@ -1,7 +1,7 @@ import * as z from "zod"; import {componentTypeSchema} from "../../../../../core/Atomic.ts"; import {requestRetryOptionsSchema} from "../common.ts"; -import {commonClientConfigSchema, commonClientDataSchema} from "./index.ts"; +import {commonClientConfigSchema, commonClientDataSchema, type EnvClientSchema} from "./index.ts"; export const listenBrainzDataSchema = z.object({ ...requestRetryOptionsSchema.shape, @@ -36,6 +36,23 @@ export const listenBrainzDataSchema = z.object({ export type ListenBrainzData = z.infer; +const envDataSchema = z.object({ + LZ_URL: z.string().optional().meta({description: 'URL for the ListenBrainz server, if not using the default'}), + LZ_TOKEN: z.string().meta({description: 'User token for the user to scrobble for'}), + LZ_USER: z.string().meta({description: 'Username of the user to scrobble for'}), +}); + +export const envSchemas: EnvClientSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + url: partial.LZ_URL, + token: partial.LZ_TOKEN, + username: partial.LZ_USER + } + }) +}; + export const listenBrainzClientDataSchema = listenBrainzDataSchema.extend(commonClientDataSchema.shape); export type ListenBrainzClientData = z.infer; diff --git a/src/backend/common/infrastructure/config/client/maloja.ts b/src/backend/common/infrastructure/config/client/maloja.ts index eb58a0d1..6d18d689 100644 --- a/src/backend/common/infrastructure/config/client/maloja.ts +++ b/src/backend/common/infrastructure/config/client/maloja.ts @@ -1,6 +1,6 @@ import * as z from "zod"; import {componentTypeSchema} from "../../../../../core/Atomic.ts"; -import {requestRetryOptionsSchema, type EnvSchemas} from "../common.ts"; +import {requestRetryOptionsSchema} from "../common.ts"; import {commonClientConfigSchema, commonClientDataSchema, type EnvClientSchema} from "./index.ts"; export const malojaDataSchema = z.object({ diff --git a/src/backend/common/infrastructure/config/client/rocksky.ts b/src/backend/common/infrastructure/config/client/rocksky.ts index b716a0b7..b010c900 100644 --- a/src/backend/common/infrastructure/config/client/rocksky.ts +++ b/src/backend/common/infrastructure/config/client/rocksky.ts @@ -1,6 +1,6 @@ import * as z from "zod"; import {requestRetryOptionsSchema} from "../common.ts"; -import {commonClientConfigSchema, commonClientDataSchema, commonClientOptionsSchema, nowPlayingOptionsSchema} from "./index.ts"; +import {commonClientConfigSchema, commonClientDataSchema, commonClientOptionsSchema, nowPlayingOptionsSchema, type EnvClientSchema} from "./index.ts"; export const rockSkyDataSchema = z.object({ ...requestRetryOptionsSchema.shape, @@ -40,6 +40,23 @@ export const rockSkyDataSchema = z.object({ export type RockSkyData = z.infer; +const envDataSchema = z.object({ + ROCKSKY_KEY: z.string().optional().meta({description: 'API Key generated from API Applications in Rocksky for your account'}), + ROCKSKY_TOKEN: z.string().optional().meta({description: 'Access Token generated from https://rocksky.app/access-tokens in Rocksky for your account'}), + ROCKSKY_HANDLE: z.string().meta({description: 'The fully-qualified handle for your ATPRoto/Bluesky account'}), +}); + +export const envSchemas: EnvClientSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + key: partial.ROCKSKY_KEY, + token: partial.ROCKSKY_TOKEN, + handle: partial.ROCKSKY_HANDLE + } + }) +}; + export const rockSkyClientDataSchema = rockSkyDataSchema.extend(commonClientDataSchema.shape); export type RockSkyClientData = z.infer; diff --git a/src/backend/common/infrastructure/config/client/tealfm.ts b/src/backend/common/infrastructure/config/client/tealfm.ts index da27dcb7..8f2684a5 100644 --- a/src/backend/common/infrastructure/config/client/tealfm.ts +++ b/src/backend/common/infrastructure/config/client/tealfm.ts @@ -2,7 +2,7 @@ import * as z from "zod"; import {componentTypeSchema} from "../../../../../core/Atomic.ts"; import {requestRetryOptionsSchema} from "../common.ts"; import {atProtoAppDataSchema, atProtoUserIdentifierDataSchema} from "./atproto.ts"; -import {commonClientConfigSchema, commonClientDataSchema, commonClientOptionsSchema} from "./index.ts"; +import {commonClientConfigSchema, commonClientDataSchema, commonClientOptionsSchema, type EnvClientSchema} from "./index.ts"; export const tealDataSchema = z.object({ ...requestRetryOptionsSchema.shape, @@ -20,6 +20,21 @@ export const tealDataSchema = z.object({ export type TealData = z.infer; +const envDataSchema = z.object({ + TEALFM_IDENTIFIER: z.string().meta({description: 'Identify the account to login as'}), + TEALFM_APP_PW: z.string().optional().meta({description: 'The App Password you created for your account'}), +}); + +export const envSchemas: EnvClientSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + identifier: partial.TEALFM_IDENTIFIER, + appPassword: partial.TEALFM_APP_PW + } + }) +}; + export const tealClientDataSchema = tealDataSchema.extend(commonClientDataSchema.shape); export type TealClientData = z.infer; -- 2.51.2 From 4fbdd71588f6a77cb28f0b959a84b8c3f1937938 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 14:03:04 +0000 Subject: [PATCH 05/35] add EnvSourceSchema interface --- src/backend/common/infrastructure/config/source/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/backend/common/infrastructure/config/source/index.ts b/src/backend/common/infrastructure/config/source/index.ts index 604246bf..483721d5 100644 --- a/src/backend/common/infrastructure/config/source/index.ts +++ b/src/backend/common/infrastructure/config/source/index.ts @@ -205,3 +205,8 @@ export const commonSourceConfigSchema = z.object({ }); export type CommonSourceConfig = Omit, 'data'> & { data?: CommonSourceData }; + +export interface EnvSourceSchema { + env: Y + toConfig: (parsed: z.output) => Partial> +} \ No newline at end of file -- 2.51.2 From 09fc0ce15cae61369a83b9e9862c6b1d0246cb69 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 10:32:21 -0400 Subject: [PATCH 06/35] Implement majority of source env schemas --- .../config/source/chromecast.ts | 21 ++++++++++- .../config/source/endpointlfm.ts | 15 +++++++- .../config/source/endpointlz.ts | 19 +++++++++- .../infrastructure/config/source/icecast.ts | 15 +++++++- .../infrastructure/config/source/jellyfin.ts | 37 ++++++++++++++++++- .../infrastructure/config/source/jriver.ts | 19 +++++++++- .../infrastructure/config/source/kodi.ts | 19 +++++++++- .../infrastructure/config/source/koito.ts | 19 +++++++++- .../infrastructure/config/source/lastfm.ts | 21 ++++++++++- .../infrastructure/config/source/librefm.ts | 23 +++++++++++- .../config/source/listenbrainz.ts | 19 +++++++++- .../infrastructure/config/source/maloja.ts | 17 ++++++++- .../infrastructure/config/source/mpd.ts | 17 ++++++++- .../infrastructure/config/source/mpris.ts | 17 ++++++++- .../infrastructure/config/source/musiccast.ts | 15 +++++++- .../infrastructure/config/source/musikcube.ts | 17 ++++++++- .../infrastructure/config/source/plex.ts | 29 ++++++++++++++- .../infrastructure/config/source/rocksky.ts | 17 ++++++++- .../infrastructure/config/source/sonos.ts | 23 +++++++++++- .../infrastructure/config/source/spotify.ts | 19 +++++++++- .../infrastructure/config/source/subsonic.ts | 19 +++++++++- .../infrastructure/config/source/tealfm.ts | 17 ++++++++- .../infrastructure/config/source/vlc.ts | 17 ++++++++- .../config/source/webscrobbler.ts | 17 ++++++++- .../infrastructure/config/source/ymbridge.ts | 17 ++++++++- .../infrastructure/config/source/ytmusic.ts | 21 ++++++++++- 26 files changed, 480 insertions(+), 26 deletions(-) diff --git a/src/backend/common/infrastructure/config/source/chromecast.ts b/src/backend/common/infrastructure/config/source/chromecast.ts index 415c352b..54595c4b 100644 --- a/src/backend/common/infrastructure/config/source/chromecast.ts +++ b/src/backend/common/infrastructure/config/source/chromecast.ts @@ -1,5 +1,5 @@ import * as z from "zod"; -import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; export const chromecastDeviceInfoSchema = z.object({ /** @@ -132,6 +132,25 @@ export const chromecastDataSchema = z.object({ export type ChromecastData = z.infer; +const envDataSchema = z.object({ + CC_BLACKLIST_DEVICES: chromecastDataSchema.shape.blacklistDevices, + CC_WHITELIST_DEVICES: chromecastDataSchema.shape.whitelistDevices, + CC_BLACKLIST_APPS: chromecastDataSchema.shape.blacklistApps, + CC_WHITELIST_APPS: chromecastDataSchema.shape.whitelistApps, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + blacklistDevices: partial.CC_BLACKLIST_DEVICES, + whitelistDevices: partial.CC_WHITELIST_DEVICES, + blacklistApps: partial.CC_BLACKLIST_APPS, + whitelistApps: partial.CC_WHITELIST_APPS + } + }) +}; + export const chromecastSourceConfigSchema = z.object({ ...commonSourceConfigSchema.shape, data: chromecastDataSchema, diff --git a/src/backend/common/infrastructure/config/source/endpointlfm.ts b/src/backend/common/infrastructure/config/source/endpointlfm.ts index bfe6bf47..f7cf2598 100644 --- a/src/backend/common/infrastructure/config/source/endpointlfm.ts +++ b/src/backend/common/infrastructure/config/source/endpointlfm.ts @@ -1,5 +1,5 @@ import * as z from "zod"; -import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; export const lastFmEndpointDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -22,6 +22,19 @@ export const lastFmEndpointDataSchema = z.object({ export type LastFMEndpointData = z.infer; +const envDataSchema = z.object({ + LFM_SLUG: lastFmEndpointDataSchema.shape.slug, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + slug: partial.LFM_SLUG + } + }) +}; + export const lastFmEndpointSourceConfigSchema = z.object({ ...commonSourceConfigSchema.shape, data: lastFmEndpointDataSchema.optional(), diff --git a/src/backend/common/infrastructure/config/source/endpointlz.ts b/src/backend/common/infrastructure/config/source/endpointlz.ts index bff08abb..0cacc7fb 100644 --- a/src/backend/common/infrastructure/config/source/endpointlz.ts +++ b/src/backend/common/infrastructure/config/source/endpointlz.ts @@ -1,5 +1,5 @@ import * as z from "zod"; -import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; export const listenbrainzEndpointDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -42,6 +42,23 @@ export const listenbrainzEndpointDataSchema = z.object({ export type ListenbrainzEndpointData = z.infer; +const envDataSchema = z.object({ + LZE_SLUG: listenbrainzEndpointDataSchema.shape.slug, + LZE_TOKEN: listenbrainzEndpointDataSchema.shape.token, + LZE_USERNAME: listenbrainzEndpointDataSchema.shape.username, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + slug: partial.LZE_SLUG, + token: partial.LZE_TOKEN, + username: partial.LZE_USERNAME + } + }) +}; + export const listenbrainzEndpointSourceConfigSchema = z.object({ ...commonSourceConfigSchema.shape, data: listenbrainzEndpointDataSchema.optional(), diff --git a/src/backend/common/infrastructure/config/source/icecast.ts b/src/backend/common/infrastructure/config/source/icecast.ts index 39714fd7..0010d534 100644 --- a/src/backend/common/infrastructure/config/source/icecast.ts +++ b/src/backend/common/infrastructure/config/source/icecast.ts @@ -1,5 +1,5 @@ import * as z from "zod"; -import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, manualListeningOptionsSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, manualListeningOptionsSchema, type EnvSourceSchema} from "./index.ts"; export interface IcecastMetadata { icy?: { @@ -44,6 +44,19 @@ export const icecastDataSchema = z.object({ export type IcecastData = z.infer; +const envDataSchema = z.object({ + ICECAST_URL: icecastDataSchema.shape.url, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + url: partial.ICECAST_URL + } + }) +}; + export const icecastSourceOptionsSchema = z.object({ ...commonSourceOptionsSchema.shape, ...manualListeningOptionsSchema.shape, diff --git a/src/backend/common/infrastructure/config/source/jellyfin.ts b/src/backend/common/infrastructure/config/source/jellyfin.ts index 1de7f970..d465eb06 100644 --- a/src/backend/common/infrastructure/config/source/jellyfin.ts +++ b/src/backend/common/infrastructure/config/source/jellyfin.ts @@ -1,5 +1,5 @@ import * as z from "zod"; -import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, type EnvSourceSchema} from "./index.ts"; export const jellyApiDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -121,6 +121,41 @@ export const jellyApiDataSchema = z.object({ export type JellyApiData = z.infer; +const envDataSchema = z.object({ + JELLYFIN_USER: jellyApiDataSchema.shape.user, + JELLYFIN_PASSWORD: jellyApiDataSchema.shape.password, + JELLYFIN_APIKEY: jellyApiDataSchema.shape.apiKey, + JELLYFIN_URL: jellyApiDataSchema.shape.url, + JELLYFIN_USERS_ALLOW: jellyApiDataSchema.shape.usersAllow, + JELLYFIN_USERS_BLOCK: jellyApiDataSchema.shape.usersBlock, + JELLYFIN_DEVICES_ALLOW: jellyApiDataSchema.shape.devicesAllow, + JELLYFIN_DEVICES_BLOCK: jellyApiDataSchema.shape.devicesBlock, + JELLYFIN_LIBRARIES_ALLOW: jellyApiDataSchema.shape.librariesAllow, + JELLYFIN_LIBRARIES_BLOCK: jellyApiDataSchema.shape.librariesBlock, + JELLYFIN_FRONTEND_URL_OVERRIDE: jellyApiDataSchema.shape.frontendUrlOverride, + JELLYFIN_MEDIATYPES_ALLOW: jellyApiDataSchema.shape.allowMediaTypes, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + user: partial.JELLYFIN_USER, + password: partial.JELLYFIN_PASSWORD, + apiKey: partial.JELLYFIN_APIKEY, + url: partial.JELLYFIN_URL, + usersAllow: partial.JELLYFIN_USERS_ALLOW, + usersBlock: partial.JELLYFIN_USERS_BLOCK, + devicesAllow: partial.JELLYFIN_DEVICES_ALLOW, + devicesBlock: partial.JELLYFIN_DEVICES_BLOCK, + librariesAllow: partial.JELLYFIN_LIBRARIES_ALLOW, + librariesBlock: partial.JELLYFIN_LIBRARIES_BLOCK, + frontendUrlOverride: partial.JELLYFIN_FRONTEND_URL_OVERRIDE, + allowMediaTypes: partial.JELLYFIN_MEDIATYPES_ALLOW + } + }) +}; + export const jellyApiOptionsSchema = z.object({ ...commonSourceOptionsSchema.shape, }); diff --git a/src/backend/common/infrastructure/config/source/jriver.ts b/src/backend/common/infrastructure/config/source/jriver.ts index 481f0d7d..f0e28e21 100644 --- a/src/backend/common/infrastructure/config/source/jriver.ts +++ b/src/backend/common/infrastructure/config/source/jriver.ts @@ -1,6 +1,6 @@ import * as z from "zod"; import {pollingOptionsSchema, requestRetryOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; export const jRiverDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -47,6 +47,23 @@ export const jRiverDataSchema = z.object({ export type JRiverData = z.infer; +const envDataSchema = z.object({ + JRIVER_URL: jRiverDataSchema.shape.url, + JRIVER_USER: jRiverDataSchema.shape.username, + JRIVER_PASSWORD: jRiverDataSchema.shape.password, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + url: partial.JRIVER_URL, + username: partial.JRIVER_USER, + password: partial.JRIVER_PASSWORD + } + }) +}; + export const jRiverSourceConfigSchema = z.object({ ...commonSourceConfigSchema.shape, data: jRiverDataSchema, diff --git a/src/backend/common/infrastructure/config/source/kodi.ts b/src/backend/common/infrastructure/config/source/kodi.ts index d501f427..4b2b12c8 100644 --- a/src/backend/common/infrastructure/config/source/kodi.ts +++ b/src/backend/common/infrastructure/config/source/kodi.ts @@ -1,6 +1,6 @@ import * as z from "zod"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; export const kodiDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -46,6 +46,23 @@ export const kodiDataSchema = z.object({ export type KodiData = z.infer; +const envDataSchema = z.object({ + KODI_URL: kodiDataSchema.shape.url, + KODI_USER: kodiDataSchema.shape.username, + KODI_PASSWORD: kodiDataSchema.shape.password, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + url: partial.KODI_URL, + username: partial.KODI_USER, + password: partial.KODI_PASSWORD + } + }) +}; + export const kodiSourceConfigSchema = z.object({ ...commonSourceConfigSchema.shape, data: kodiDataSchema, diff --git a/src/backend/common/infrastructure/config/source/koito.ts b/src/backend/common/infrastructure/config/source/koito.ts index 17ab6d1b..b651a38c 100644 --- a/src/backend/common/infrastructure/config/source/koito.ts +++ b/src/backend/common/infrastructure/config/source/koito.ts @@ -1,7 +1,7 @@ import * as z from "zod"; import {koitoDataSchema} from "../client/koito.ts"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; export const koitoSourceDataSchema = z.object({ ...koitoDataSchema.shape, @@ -11,6 +11,23 @@ export const koitoSourceDataSchema = z.object({ export type KoitoSourceData = z.infer; +const envDataSchema = z.object({ + SOURCE_KOITO_URL: koitoSourceDataSchema.shape.url, + SOURCE_KOITO_TOKEN: koitoSourceDataSchema.shape.token, + SOURCE_KOITO_USER: koitoSourceDataSchema.shape.username, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + url: partial.SOURCE_KOITO_URL, + token: partial.SOURCE_KOITO_TOKEN, + username: partial.SOURCE_KOITO_USER + } + }) +}; + export const koitoSourceConfigSchema = z.object({ ...commonSourceConfigSchema.shape, /** diff --git a/src/backend/common/infrastructure/config/source/lastfm.ts b/src/backend/common/infrastructure/config/source/lastfm.ts index 1782728d..bfb23087 100644 --- a/src/backend/common/infrastructure/config/source/lastfm.ts +++ b/src/backend/common/infrastructure/config/source/lastfm.ts @@ -1,7 +1,7 @@ import * as z from "zod"; import {lastfmDataSchema} from "../client/lastfm.ts"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; export const lastFmSourceDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -11,6 +11,25 @@ export const lastFmSourceDataSchema = z.object({ export type LastFmSourceData = z.infer; +const envDataSchema = z.object({ + SOURCE_LASTFM_API_KEY: lastFmSourceDataSchema.shape.apiKey, + SOURCE_LASTFM_SECRET: lastFmSourceDataSchema.shape.secret, + SOURCE_LASTFM_REDIRECT_URI: lastFmSourceDataSchema.shape.redirectUri, + SOURCE_LASTFM_SESSION: lastFmSourceDataSchema.shape.session, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + apiKey: partial.SOURCE_LASTFM_API_KEY, + secret: partial.SOURCE_LASTFM_SECRET, + redirectUri: partial.SOURCE_LASTFM_REDIRECT_URI, + session: partial.SOURCE_LASTFM_SESSION + } + }) +}; + export const lastfmSourceConfigSchema = z.object({ ...commonSourceConfigSchema.shape, /** diff --git a/src/backend/common/infrastructure/config/source/librefm.ts b/src/backend/common/infrastructure/config/source/librefm.ts index bb7f5682..e2ea0d4b 100644 --- a/src/backend/common/infrastructure/config/source/librefm.ts +++ b/src/backend/common/infrastructure/config/source/librefm.ts @@ -1,7 +1,7 @@ import * as z from "zod"; import {librefmDataSchema} from "../client/librefm.ts"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; export const librefmSourceDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -11,6 +11,27 @@ export const librefmSourceDataSchema = z.object({ export type librefmSourceData = z.infer; +const envDataSchema = z.object({ + SOURCE_LIBREFM_API_KEY: librefmSourceDataSchema.shape.apiKey, + SOURCE_LIBREFM_SECRET: librefmSourceDataSchema.shape.secret, + SOURCE_LIBREFM_REDIRECT_URI: librefmSourceDataSchema.shape.redirectUri, + SOURCE_LIBREFM_SESSION: librefmSourceDataSchema.shape.session, + SOURCE_LIBREFM_URLBASE: librefmSourceDataSchema.shape.urlBase, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + apiKey: partial.SOURCE_LIBREFM_API_KEY, + secret: partial.SOURCE_LIBREFM_SECRET, + redirectUri: partial.SOURCE_LIBREFM_REDIRECT_URI, + session: partial.SOURCE_LIBREFM_SESSION, + urlBase: partial.SOURCE_LIBREFM_URLBASE + } + }) +}; + export const librefmSourceConfigSchema = z.object({ ...commonSourceConfigSchema.shape, /** diff --git a/src/backend/common/infrastructure/config/source/listenbrainz.ts b/src/backend/common/infrastructure/config/source/listenbrainz.ts index 45931b45..3d373908 100644 --- a/src/backend/common/infrastructure/config/source/listenbrainz.ts +++ b/src/backend/common/infrastructure/config/source/listenbrainz.ts @@ -1,7 +1,7 @@ import * as z from "zod"; import {listenBrainzDataSchema} from "../client/listenbrainz.ts"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; export const listenBrainzSourceDataSchema = z.object({ ...listenBrainzDataSchema.shape, @@ -11,6 +11,23 @@ export const listenBrainzSourceDataSchema = z.object({ export type ListenBrainzSourceData = z.infer; +const envDataSchema = z.object({ + SOURCE_LZ_URL: listenBrainzSourceDataSchema.shape.url, + SOURCE_LZ_TOKEN: listenBrainzSourceDataSchema.shape.token, + SOURCE_LZ_USER: listenBrainzSourceDataSchema.shape.username, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + url: partial.SOURCE_LZ_URL, + token: partial.SOURCE_LZ_TOKEN, + username: partial.SOURCE_LZ_USER + } + }) +}; + export const listenBrainzSourceConfigSchema = z.object({ ...commonSourceConfigSchema.shape, /** diff --git a/src/backend/common/infrastructure/config/source/maloja.ts b/src/backend/common/infrastructure/config/source/maloja.ts index dac951fa..89754720 100644 --- a/src/backend/common/infrastructure/config/source/maloja.ts +++ b/src/backend/common/infrastructure/config/source/maloja.ts @@ -1,7 +1,7 @@ import * as z from "zod"; import {malojaDataSchema} from "../client/maloja.ts"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; export const malojaSourceDataSchema = z.object({ ...malojaDataSchema.shape, @@ -11,6 +11,21 @@ export const malojaSourceDataSchema = z.object({ export type MalojaSourceData = z.infer; +const envDataSchema = z.object({ + SOURCE_MALOJA_URL: malojaSourceDataSchema.shape.url, + SOURCE_MALOJA_API_KEY: malojaSourceDataSchema.shape.apiKey, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + url: partial.SOURCE_MALOJA_URL, + apiKey: partial.SOURCE_MALOJA_API_KEY + } + }) +}; + export const malojaSourceConfigSchema = z.object({ ...commonSourceConfigSchema.shape, /** diff --git a/src/backend/common/infrastructure/config/source/mpd.ts b/src/backend/common/infrastructure/config/source/mpd.ts index 345a6f5a..9680e928 100644 --- a/src/backend/common/infrastructure/config/source/mpd.ts +++ b/src/backend/common/infrastructure/config/source/mpd.ts @@ -1,6 +1,6 @@ import * as z from "zod"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, type EnvSourceSchema} from "./index.ts"; export const mpdDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -39,6 +39,21 @@ export const mpdDataSchema = z.object({ export type MPDData = z.infer; +const envDataSchema = z.object({ + MPD_URL: mpdDataSchema.shape.url, + MPD_PASSWORD: mpdDataSchema.shape.password, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + url: partial.MPD_URL, + password: partial.MPD_PASSWORD + } + }) +}; + export const mpdSourceOptionsSchema = z.object({ ...commonSourceOptionsSchema.shape, }); diff --git a/src/backend/common/infrastructure/config/source/mpris.ts b/src/backend/common/infrastructure/config/source/mpris.ts index 761ff327..dadf8b80 100644 --- a/src/backend/common/infrastructure/config/source/mpris.ts +++ b/src/backend/common/infrastructure/config/source/mpris.ts @@ -1,5 +1,5 @@ import * as z from "zod"; -import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; export const PLAYBACK_STATUS_PLAYING = 'Playing'; export const PLAYBACK_STATUS_PAUSED = 'Paused'; @@ -56,6 +56,21 @@ export const mprisDataSchema = z.object({ export type MPRISData = z.infer; +const envDataSchema = z.object({ + MPRIS_BLACKLIST: mprisDataSchema.shape.blacklist, + MPRIS_WHITELIST: mprisDataSchema.shape.whitelist, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + blacklist: partial.MPRIS_BLACKLIST, + whitelist: partial.MPRIS_WHITELIST + } + }) +}; + export const mprisSourceConfigSchema = z.object({ ...commonSourceConfigSchema.shape, data: mprisDataSchema, diff --git a/src/backend/common/infrastructure/config/source/musiccast.ts b/src/backend/common/infrastructure/config/source/musiccast.ts index c94b4863..94cb6782 100644 --- a/src/backend/common/infrastructure/config/source/musiccast.ts +++ b/src/backend/common/infrastructure/config/source/musiccast.ts @@ -1,7 +1,7 @@ import * as z from "zod"; import { REPORTED_PLAYER_STATUSES } from '../../../../../core/Atomic.ts'; import type {ReportedPlayerStatus} from '../../../../../core/Atomic.ts'; -import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; export type PlaybackStatus = 'play' | 'stop' | 'pause' | 'fast_reverse' | 'fast_forward' @@ -86,6 +86,19 @@ export const musicCastDataSchema = z.object({ export type MusicCastData = z.infer; +const envDataSchema = z.object({ + MCAST_URL: musicCastDataSchema.shape.url, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + url: partial.MCAST_URL + } + }) +}; + export const musicCastSourceConfigSchema = z.object({ ...commonSourceConfigSchema.shape, data: musicCastDataSchema, diff --git a/src/backend/common/infrastructure/config/source/musikcube.ts b/src/backend/common/infrastructure/config/source/musikcube.ts index baa807e8..afc00c4c 100644 --- a/src/backend/common/infrastructure/config/source/musikcube.ts +++ b/src/backend/common/infrastructure/config/source/musikcube.ts @@ -1,5 +1,5 @@ import * as z from "zod"; -import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; export const PLAYBACK_STATUS_PLAYING_MC = 'playing'; export const PLAYBACK_STATUS_PAUSED_MC = 'paused'; @@ -117,6 +117,21 @@ export const musikcubeDataSchema = z.object({ export type MusikcubeData = z.infer; +const envDataSchema = z.object({ + MC_URL: musikcubeDataSchema.shape.url, + MC_PASSWORD: musikcubeDataSchema.shape.password, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + url: partial.MC_URL, + password: partial.MC_PASSWORD + } + }) +}; + export const musikcubeSourceConfigSchema = z.object({ ...commonSourceConfigSchema.shape, data: musikcubeDataSchema, diff --git a/src/backend/common/infrastructure/config/source/plex.ts b/src/backend/common/infrastructure/config/source/plex.ts index 9ca97857..364eb572 100644 --- a/src/backend/common/infrastructure/config/source/plex.ts +++ b/src/backend/common/infrastructure/config/source/plex.ts @@ -1,6 +1,6 @@ import * as z from "zod"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, type EnvSourceSchema} from "./index.ts"; export const plexApiDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -57,6 +57,33 @@ export const plexApiDataSchema = z.object({ export type PlexApiData = z.infer; +const envDataSchema = z.object({ + PLEX_URL: plexApiDataSchema.shape.url, + PLEX_TOKEN: plexApiDataSchema.shape.token, + PLEX_USERS_ALLOW: plexApiDataSchema.shape.usersAllow, + PLEX_USERS_BLOCK: plexApiDataSchema.shape.usersBlock, + PLEX_DEVICES_ALLOW: plexApiDataSchema.shape.devicesAllow, + PLEX_DEVICES_BLOCK: plexApiDataSchema.shape.devicesBlock, + PLEX_LIBRARIES_ALLOW: plexApiDataSchema.shape.librariesAllow, + PLEX_LIBRARIES_BLOCK: plexApiDataSchema.shape.librariesBlock, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + url: partial.PLEX_URL, + token: partial.PLEX_TOKEN, + usersAllow: partial.PLEX_USERS_ALLOW, + usersBlock: partial.PLEX_USERS_BLOCK, + devicesAllow: partial.PLEX_DEVICES_ALLOW, + devicesBlock: partial.PLEX_DEVICES_BLOCK, + librariesAllow: partial.PLEX_LIBRARIES_ALLOW, + librariesBlock: partial.PLEX_LIBRARIES_BLOCK + } + }) +}; + export const plexApiOptionsSchema = z.object({ ...commonSourceOptionsSchema.shape, /** diff --git a/src/backend/common/infrastructure/config/source/rocksky.ts b/src/backend/common/infrastructure/config/source/rocksky.ts index 8468b110..f05b2b2f 100644 --- a/src/backend/common/infrastructure/config/source/rocksky.ts +++ b/src/backend/common/infrastructure/config/source/rocksky.ts @@ -1,7 +1,7 @@ import * as z from "zod"; import {rockSkyDataSchema, rockSkyOptionsSchema} from "../client/rocksky.ts"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, type EnvSourceSchema} from "./index.ts"; export const rockskySourceDataSchema = z.object({ ...rockSkyDataSchema.shape, @@ -11,6 +11,21 @@ export const rockskySourceDataSchema = z.object({ export type RockskySourceData = z.infer; +const envDataSchema = z.object({ + SOURCE_ROCKSKY_KEY: rockskySourceDataSchema.shape.key, + SOURCE_ROCKSKY_HANDLE: rockskySourceDataSchema.shape.handle, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + key: partial.SOURCE_ROCKSKY_KEY, + handle: partial.SOURCE_ROCKSKY_HANDLE + } + }) +}; + export const rockskySourceOptionsSchema = z.object({ ...rockSkyOptionsSchema.shape, ...commonSourceOptionsSchema.shape, diff --git a/src/backend/common/infrastructure/config/source/sonos.ts b/src/backend/common/infrastructure/config/source/sonos.ts index 555b48e0..817b9407 100644 --- a/src/backend/common/infrastructure/config/source/sonos.ts +++ b/src/backend/common/infrastructure/config/source/sonos.ts @@ -1,6 +1,6 @@ import * as z from "zod"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, type EnvSourceSchema} from "./index.ts"; export const sonosDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -44,6 +44,27 @@ export const sonosDataSchema = z.object({ export type SonosData = z.infer; +const envDataSchema = z.object({ + SONOS_HOST: sonosDataSchema.shape.host, + SONOS_DEVICES_ALLOW: sonosDataSchema.shape.devicesAllow, + SONOS_DEVICES_BLOCK: sonosDataSchema.shape.devicesBlock, + SONOS_GROUPS_ALLOW: sonosDataSchema.shape.groupsAllow, + SONOS_GROUPS_BLOCK: sonosDataSchema.shape.groupsBlock, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + host: partial.SONOS_HOST, + devicesAllow: partial.SONOS_DEVICES_ALLOW, + devicesBlock: partial.SONOS_DEVICES_BLOCK, + groupsAllow: partial.SONOS_GROUPS_ALLOW, + groupsBlock: partial.SONOS_GROUPS_BLOCK + } + }) +}; + export const sonosSourceOptionsSchema = z.object({ ...commonSourceOptionsSchema.shape, logEmptyPlayer: z.boolean().optional(), diff --git a/src/backend/common/infrastructure/config/source/spotify.ts b/src/backend/common/infrastructure/config/source/spotify.ts index f8c729ec..8a3431a5 100644 --- a/src/backend/common/infrastructure/config/source/spotify.ts +++ b/src/backend/common/infrastructure/config/source/spotify.ts @@ -1,6 +1,6 @@ import * as z from "zod"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; export const spotifySourceDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -58,6 +58,23 @@ export const spotifySourceDataSchema = z.object({ export type SpotifySourceData = z.infer; +const envDataSchema = z.object({ + SPOTIFY_CLIENT_ID: spotifySourceDataSchema.shape.clientId, + SPOTIFY_CLIENT_SECRET: spotifySourceDataSchema.shape.clientSecret, + SPOTIFY_REDIRECT_URI: spotifySourceDataSchema.shape.redirectUri, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + clientId: partial.SPOTIFY_CLIENT_ID, + clientSecret: partial.SPOTIFY_CLIENT_SECRET, + redirectUri: partial.SPOTIFY_REDIRECT_URI + } + }) +}; + export const spotifySourceConfigSchema = z.object({ ...commonSourceConfigSchema.shape, data: spotifySourceDataSchema, diff --git a/src/backend/common/infrastructure/config/source/subsonic.ts b/src/backend/common/infrastructure/config/source/subsonic.ts index 4a8ccc1d..55fd59f3 100644 --- a/src/backend/common/infrastructure/config/source/subsonic.ts +++ b/src/backend/common/infrastructure/config/source/subsonic.ts @@ -1,6 +1,6 @@ import * as z from "zod"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; export const subsonicDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -102,6 +102,23 @@ export const subsonicDataSchema = z.object({ export type SubsonicData = z.infer; +const envDataSchema = z.object({ + SUBSONIC_USER: subsonicDataSchema.shape.user, + SUBSONIC_PASSWORD: subsonicDataSchema.shape.password, + SUBSONIC_URL: subsonicDataSchema.shape.url, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + user: partial.SUBSONIC_USER, + password: partial.SUBSONIC_PASSWORD, + url: partial.SUBSONIC_URL + } + }) +}; + export const subSonicSourceConfigSchema = z.object({ ...commonSourceConfigSchema.shape, data: subsonicDataSchema, diff --git a/src/backend/common/infrastructure/config/source/tealfm.ts b/src/backend/common/infrastructure/config/source/tealfm.ts index 6e888da3..45bfc385 100644 --- a/src/backend/common/infrastructure/config/source/tealfm.ts +++ b/src/backend/common/infrastructure/config/source/tealfm.ts @@ -1,7 +1,7 @@ import * as z from "zod"; import {tealClientOptionsSchema, tealDataSchema} from "../client/tealfm.ts"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, type EnvSourceSchema} from "./index.ts"; export const tealSourceDataSchema = z.object({ ...tealDataSchema.shape, @@ -13,6 +13,21 @@ export const tealSourceDataSchema = z.object({ export type TealSourceData = z.infer; +const envDataSchema = z.object({ + SOURCE_TEALFM_IDENTIFIER: tealSourceDataSchema.shape.identifier, + SOURCE_TEALFM_APP_PW: tealSourceDataSchema.shape.appPassword, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + identifier: partial.SOURCE_TEALFM_IDENTIFIER, + appPassword: partial.SOURCE_TEALFM_APP_PW + } + }) +}; + export const tealSourceOptionsSchema = z.object({ ...commonSourceOptionsSchema.shape, ...tealClientOptionsSchema.shape, diff --git a/src/backend/common/infrastructure/config/source/vlc.ts b/src/backend/common/infrastructure/config/source/vlc.ts index 42ab419d..dc8aa3be 100644 --- a/src/backend/common/infrastructure/config/source/vlc.ts +++ b/src/backend/common/infrastructure/config/source/vlc.ts @@ -1,7 +1,7 @@ import * as z from "zod"; import type {VlcMeta} from "vlc-client/dist/Types.js"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, type EnvSourceSchema} from "./index.ts"; export const vlcDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -31,6 +31,21 @@ export const vlcDataSchema = z.object({ export type VLCData = z.infer; +const envDataSchema = z.object({ + VLC_URL: vlcDataSchema.shape.url, + VLC_PASSWORD: vlcDataSchema.shape.password, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + url: partial.VLC_URL, + password: partial.VLC_PASSWORD + } + }) +}; + export const vlcSourceOptionsSchema = z.object({ ...commonSourceOptionsSchema.shape, /** A list of regular expressions to use to extract metadata (title, album, artist) from a filename diff --git a/src/backend/common/infrastructure/config/source/webscrobbler.ts b/src/backend/common/infrastructure/config/source/webscrobbler.ts index d37eca78..41f990a3 100644 --- a/src/backend/common/infrastructure/config/source/webscrobbler.ts +++ b/src/backend/common/infrastructure/config/source/webscrobbler.ts @@ -1,5 +1,5 @@ import * as z from "zod"; -import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; export const webScrobblerDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -46,6 +46,21 @@ export const webScrobblerDataSchema = z.object({ export type WebScrobblerData = z.infer; +const envDataSchema = z.object({ + WS_BLACKLIST: webScrobblerDataSchema.shape.blacklist, + WS_WHITELIST: webScrobblerDataSchema.shape.whitelist, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + blacklist: partial.WS_BLACKLIST, + whitelist: partial.WS_WHITELIST + } + }) +}; + export const webScrobblerSourceConfigSchema = z.object({ ...commonSourceConfigSchema.shape, data: webScrobblerDataSchema.optional(), diff --git a/src/backend/common/infrastructure/config/source/ymbridge.ts b/src/backend/common/infrastructure/config/source/ymbridge.ts index 26cb4d22..23e5cf71 100644 --- a/src/backend/common/infrastructure/config/source/ymbridge.ts +++ b/src/backend/common/infrastructure/config/source/ymbridge.ts @@ -1,6 +1,6 @@ import * as z from "zod"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; export const yandexMusicBridgeDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -17,6 +17,21 @@ export const yandexMusicBridgeDataSchema = z.object({ export type YandexMusicBridgeData = z.infer; +const envDataSchema = z.object({ + YMBRIDGE_URL: yandexMusicBridgeDataSchema.shape.url, + YMBRIDGE_API_KEY: yandexMusicBridgeDataSchema.shape.apiKey, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + url: partial.YMBRIDGE_URL, + apiKey: partial.YMBRIDGE_API_KEY + } + }) +}; + export const yandexMusicBridgeSourceConfigSchema = z.object({ ...commonSourceConfigSchema.shape, data: yandexMusicBridgeDataSchema.optional(), diff --git a/src/backend/common/infrastructure/config/source/ytmusic.ts b/src/backend/common/infrastructure/config/source/ytmusic.ts index 45638532..8bc7febd 100644 --- a/src/backend/common/infrastructure/config/source/ytmusic.ts +++ b/src/backend/common/infrastructure/config/source/ytmusic.ts @@ -1,6 +1,6 @@ import * as z from "zod"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, type EnvSourceSchema} from "./index.ts"; export const innertubeOptionsSchema = z.object({ /** @@ -98,6 +98,25 @@ export const ytMusicDataSchema = z.object({ export type YTMusicData = z.infer; +const envDataSchema = z.object({ + YTM_REDIRECT_URI: ytMusicDataSchema.shape.redirectUri, + YTM_CLIENT_ID: ytMusicDataSchema.shape.clientId, + YTM_CLIENT_SECRET: ytMusicDataSchema.shape.clientSecret, + YTM_COOKIE: ytMusicDataSchema.shape.cookie, +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + redirectUri: partial.YTM_REDIRECT_URI, + clientId: partial.YTM_CLIENT_ID, + clientSecret: partial.YTM_CLIENT_SECRET, + cookie: partial.YTM_COOKIE + } + }) +}; + export const ytMusicSourceConfigSchema = z.object({ ...commonSourceConfigSchema.shape, data: ytMusicDataSchema.optional(), -- 2.51.2 From 13acc564246807e0757d36fb50f1241fa78cf378 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 10:39:14 -0400 Subject: [PATCH 07/35] refactor client env schemas to reuse zod shape where possible --- .../common/infrastructure/config/client/discord.ts | 14 +++++++------- .../common/infrastructure/config/client/koito.ts | 6 +++--- .../common/infrastructure/config/client/lastfm.ts | 8 ++++---- .../common/infrastructure/config/client/librefm.ts | 10 +++++----- .../infrastructure/config/client/listenbrainz.ts | 6 +++--- .../common/infrastructure/config/client/maloja.ts | 4 ++-- .../common/infrastructure/config/client/rocksky.ts | 6 +++--- .../common/infrastructure/config/client/tealfm.ts | 4 ++-- 8 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/backend/common/infrastructure/config/client/discord.ts b/src/backend/common/infrastructure/config/client/discord.ts index 90c47118..4b3fc649 100644 --- a/src/backend/common/infrastructure/config/client/discord.ts +++ b/src/backend/common/infrastructure/config/client/discord.ts @@ -21,13 +21,13 @@ export const discordDataSchema = z.object({ export type DiscordData = z.infer; const envDataSchema = z.object({ - DISCORD_TOKEN: z.string().optional(), - DISCORD_ARTWORK: z.string().optional(), - DISCORD_APPLICATION_ID: z.string().optional(), - DISCORD_IPC_LOCATIONS: z.string().optional(), - DISCORD_ARTWORK_DEFAULT_URL: z.string().optional(), - DISCORD_STATUS_OVERRIDE_ALLOW: z.string().optional(), - DISCORD_LISTENING_ACTIVITY_ALLOW: z.string().optional(), + DISCORD_TOKEN: discordDataSchema.shape.token, + DISCORD_ARTWORK: discordDataSchema.shape.artwork, + DISCORD_APPLICATION_ID: discordDataSchema.shape.applicationId, + DISCORD_IPC_LOCATIONS: discordDataSchema.shape.ipcLocations, + DISCORD_ARTWORK_DEFAULT_URL: discordDataSchema.shape.artworkDefaultUrl, + DISCORD_STATUS_OVERRIDE_ALLOW: discordDataSchema.shape.statusOverrideAllow, + DISCORD_LISTENING_ACTIVITY_ALLOW: discordDataSchema.shape.listeningActivityAllow, }); export const envSchemas: EnvClientSchema = { diff --git a/src/backend/common/infrastructure/config/client/koito.ts b/src/backend/common/infrastructure/config/client/koito.ts index d7decf44..2132f391 100644 --- a/src/backend/common/infrastructure/config/client/koito.ts +++ b/src/backend/common/infrastructure/config/client/koito.ts @@ -79,9 +79,9 @@ export const koitoDataSchema = z.object({ export type KoitoData = z.infer; const envDataSchema = z.object({ - KOITO_URL: z.string().meta({description: 'URL for the Koito server'}), - KOITO_TOKEN: z.string().meta({description: 'User token for the user to scrobble for'}), - KOITO_USER: z.string().meta({description: 'Username of the user to scrobble for'}), + KOITO_URL: koitoDataSchema.shape.url, + KOITO_TOKEN: koitoDataSchema.shape.token, + KOITO_USER: koitoDataSchema.shape.username, }); export const envSchemas: EnvClientSchema = { diff --git a/src/backend/common/infrastructure/config/client/lastfm.ts b/src/backend/common/infrastructure/config/client/lastfm.ts index 81ac43d8..4f0ac6f3 100644 --- a/src/backend/common/infrastructure/config/client/lastfm.ts +++ b/src/backend/common/infrastructure/config/client/lastfm.ts @@ -45,10 +45,10 @@ export const lastfmDataSchema = z.object({ export type LastfmData = z.infer; const envDataSchema = z.object({ - LASTFM_API_KEY: z.string().meta({description: 'API Key generated from Last.fm/Libre.fm account'}), - LASTFM_SECRET: z.string().meta({description: 'Secret generated from Last.fm/Libre.fm account'}), - LASTFM_REDIRECT_URI: z.string().optional().meta({description: 'Optional URI to use for callback.'}), - LASTFM_SESSION: z.string().optional().meta({description: 'Optional session id returned from a completed auth flow'}), + LASTFM_API_KEY: lastfmDataSchema.shape.apiKey, + LASTFM_SECRET: lastfmDataSchema.shape.secret, + LASTFM_REDIRECT_URI: lastfmDataSchema.shape.redirectUri, + LASTFM_SESSION: lastfmDataSchema.shape.session, }); export const envSchemas: EnvClientSchema = { diff --git a/src/backend/common/infrastructure/config/client/librefm.ts b/src/backend/common/infrastructure/config/client/librefm.ts index 73ba46ab..b795e642 100644 --- a/src/backend/common/infrastructure/config/client/librefm.ts +++ b/src/backend/common/infrastructure/config/client/librefm.ts @@ -63,11 +63,11 @@ export const librefmDataSchema = z.object({ export type LibrefmData = z.infer; const envDataSchema = z.object({ - LIBREFM_API_KEY: z.string().optional().meta({description: 'Optional Secret for Libre.fm account'}), - LIBREFM_SECRET: z.string().optional().meta({description: 'Optional Secret for Libre.fm account'}), - LIBREFM_REDIRECT_URI: z.string().optional().meta({description: 'Optional URI to use for callback.'}), - LIBREFM_SESSION: z.string().optional().meta({description: 'Optional session id returned from a completed auth flow'}), - LIBREFM_URLBASE: z.string().optional().meta({description: '(Optional) The host and path prefix for your Libre.fm instance'}), + LIBREFM_API_KEY: librefmDataSchema.shape.apiKey, + LIBREFM_SECRET: librefmDataSchema.shape.secret, + LIBREFM_REDIRECT_URI: librefmDataSchema.shape.redirectUri, + LIBREFM_SESSION: librefmDataSchema.shape.session, + LIBREFM_URLBASE: librefmDataSchema.shape.urlBase, }); export const envSchemas: EnvClientSchema = { diff --git a/src/backend/common/infrastructure/config/client/listenbrainz.ts b/src/backend/common/infrastructure/config/client/listenbrainz.ts index bed0a33f..d383219a 100644 --- a/src/backend/common/infrastructure/config/client/listenbrainz.ts +++ b/src/backend/common/infrastructure/config/client/listenbrainz.ts @@ -37,9 +37,9 @@ export const listenBrainzDataSchema = z.object({ export type ListenBrainzData = z.infer; const envDataSchema = z.object({ - LZ_URL: z.string().optional().meta({description: 'URL for the ListenBrainz server, if not using the default'}), - LZ_TOKEN: z.string().meta({description: 'User token for the user to scrobble for'}), - LZ_USER: z.string().meta({description: 'Username of the user to scrobble for'}), + LZ_URL: listenBrainzDataSchema.shape.url, + LZ_TOKEN: listenBrainzDataSchema.shape.token, + LZ_USER: listenBrainzDataSchema.shape.username, }); export const envSchemas: EnvClientSchema = { diff --git a/src/backend/common/infrastructure/config/client/maloja.ts b/src/backend/common/infrastructure/config/client/maloja.ts index 6d18d689..9bc1c2ba 100644 --- a/src/backend/common/infrastructure/config/client/maloja.ts +++ b/src/backend/common/infrastructure/config/client/maloja.ts @@ -32,8 +32,8 @@ export const malojaClientDataSchema = malojaDataSchema.extend(commonClientDataSc export type MalojaClientData = z.infer; const envDataSchema = z.object({ - MALOJA_URL: z.string().meta({description: 'Base URL of your installation'}), - MALOJA_API_KEY: z.string() + MALOJA_URL: malojaDataSchema.shape.url, + MALOJA_API_KEY: malojaDataSchema.shape.apiKey }); export const envSchemas: EnvClientSchema = { diff --git a/src/backend/common/infrastructure/config/client/rocksky.ts b/src/backend/common/infrastructure/config/client/rocksky.ts index b010c900..d91bb56d 100644 --- a/src/backend/common/infrastructure/config/client/rocksky.ts +++ b/src/backend/common/infrastructure/config/client/rocksky.ts @@ -41,9 +41,9 @@ export const rockSkyDataSchema = z.object({ export type RockSkyData = z.infer; const envDataSchema = z.object({ - ROCKSKY_KEY: z.string().optional().meta({description: 'API Key generated from API Applications in Rocksky for your account'}), - ROCKSKY_TOKEN: z.string().optional().meta({description: 'Access Token generated from https://rocksky.app/access-tokens in Rocksky for your account'}), - ROCKSKY_HANDLE: z.string().meta({description: 'The fully-qualified handle for your ATPRoto/Bluesky account'}), + ROCKSKY_KEY: rockSkyDataSchema.shape.key, + ROCKSKY_TOKEN: rockSkyDataSchema.shape.token, + ROCKSKY_HANDLE: rockSkyDataSchema.shape.handle, }); export const envSchemas: EnvClientSchema = { diff --git a/src/backend/common/infrastructure/config/client/tealfm.ts b/src/backend/common/infrastructure/config/client/tealfm.ts index 8f2684a5..72ccce82 100644 --- a/src/backend/common/infrastructure/config/client/tealfm.ts +++ b/src/backend/common/infrastructure/config/client/tealfm.ts @@ -21,8 +21,8 @@ export const tealDataSchema = z.object({ export type TealData = z.infer; const envDataSchema = z.object({ - TEALFM_IDENTIFIER: z.string().meta({description: 'Identify the account to login as'}), - TEALFM_APP_PW: z.string().optional().meta({description: 'The App Password you created for your account'}), + TEALFM_IDENTIFIER: tealDataSchema.shape.identifier, + TEALFM_APP_PW: tealDataSchema.shape.appPassword, }); export const envSchemas: EnvClientSchema = { -- 2.51.2 From 40eff0b901c6ac2329eaf675602ec3fb76b4da43 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 17:27:44 +0000 Subject: [PATCH 08/35] refactor: Simplify ScrobbleClients config builder --- .../infrastructure/config/client/clients.ts | 54 +- .../common/infrastructure/config/common.ts | 3 +- src/backend/scrobblers/ScrobbleClients.ts | 578 +++++++----------- src/backend/tests/config/config.test.ts | 9 +- src/core/DataUtils.ts | 8 + 5 files changed, 262 insertions(+), 390 deletions(-) diff --git a/src/backend/common/infrastructure/config/client/clients.ts b/src/backend/common/infrastructure/config/client/clients.ts index 31ea9862..9c34eded 100644 --- a/src/backend/common/infrastructure/config/client/clients.ts +++ b/src/backend/common/infrastructure/config/client/clients.ts @@ -1,13 +1,13 @@ import * as z from "zod"; -import {koitoClientAIOConfigSchema, koitoClientConfigSchema, type KoitoClientAIOConfig, type KoitoClientConfig} from "./koito.ts"; -import {lastfmClientAIOConfigSchema, lastfmClientConfigSchema, type LastfmClientAIOConfig, type LastfmClientConfig} from "./lastfm.ts"; -import {listenBrainzClientAIOConfigSchema, listenBrainzClientConfigSchema, type ListenBrainzClientAIOConfig, type ListenBrainzClientConfig} from "./listenbrainz.ts"; -import {malojaClientAIOConfigSchema, malojaClientConfigSchema, type MalojaClientAIOConfig, type MalojaClientConfig} from "./maloja.ts"; -import {tealClientAIOConfigSchema, tealClientConfigSchema, type TealClientAIOConfig, type TealClientConfig} from "./tealfm.ts"; -import {rockSkyClientAIOConfigSchema, rockSkyClientConfigSchema, type RockSkyClientAIOConfig, type RockSkyClientConfig} from "./rocksky.ts"; -import {librefmClientAIOConfigSchema, librefmClientConfigSchema, type LibrefmClientAIOConfig, type LibrefmClientConfig} from "./librefm.ts"; -import {discordClientAIOConfigSchema, discordClientConfigSchema, type DiscordClientAIOConfig, type DiscordClientConfig} from "./discord.ts"; -import type { CommonClientConfig } from "./index.ts"; +import {koitoClientAIOConfigSchema, koitoClientConfigSchema, envSchemas as koitoEnvSchemas, type KoitoClientAIOConfig, type KoitoClientConfig} from "./koito.ts"; +import {lastfmClientAIOConfigSchema, lastfmClientConfigSchema, envSchemas as lastfmEnvSchemas, type LastfmClientAIOConfig, type LastfmClientConfig} from "./lastfm.ts"; +import {listenBrainzClientAIOConfigSchema, listenBrainzClientConfigSchema, envSchemas as listenBrainzEnvSchemas, type ListenBrainzClientAIOConfig, type ListenBrainzClientConfig} from "./listenbrainz.ts"; +import {malojaClientAIOConfigSchema, malojaClientConfigSchema, envSchemas as malojaEnvSchemas, type MalojaClientAIOConfig, type MalojaClientConfig} from "./maloja.ts"; +import {tealClientAIOConfigSchema, tealClientConfigSchema, envSchemas as tealEnvSchemas, type TealClientAIOConfig, type TealClientConfig} from "./tealfm.ts"; +import {rockSkyClientAIOConfigSchema, rockSkyClientConfigSchema, envSchemas as rockSkyEnvSchemas, type RockSkyClientAIOConfig, type RockSkyClientConfig} from "./rocksky.ts"; +import {librefmClientAIOConfigSchema, librefmClientConfigSchema, envSchemas as librefmEnvSchemas, type LibrefmClientAIOConfig, type LibrefmClientConfig} from "./librefm.ts"; +import {discordClientAIOConfigSchema, discordClientConfigSchema, envSchemas as discordEnvSchemas, type DiscordClientAIOConfig, type DiscordClientConfig} from "./discord.ts"; +import type { CommonClientConfig, EnvClientSchema } from "./index.ts"; import type { ClientType } from "../../../../../core/Atomic.ts"; import { SimpleError } from "../../../errors/MSErrors.ts"; @@ -37,26 +37,26 @@ export const clientAIOConfigSchema = z.union([ export type ClientAIOConfig = z.infer; -export interface ClientTypeConfigMap extends Record { - maloja: [MalojaClientConfig,MalojaClientAIOConfig], - lastfm: [LastfmClientConfig,LastfmClientAIOConfig], - librefm: [LibrefmClientConfig,LibrefmClientAIOConfig], - listenbrainz: [ListenBrainzClientConfig,ListenBrainzClientAIOConfig], - koito: [KoitoClientConfig,KoitoClientAIOConfig], - tealfm: [TealClientConfig,TealClientAIOConfig], - rocksky: [RockSkyClientConfig,RockSkyClientAIOConfig], - discord: [DiscordClientConfig,DiscordClientAIOConfig] +export interface ClientTypeConfigMap extends Record>]> { + maloja: [MalojaClientConfig,MalojaClientAIOConfig, Partial>], + lastfm: [LastfmClientConfig,LastfmClientAIOConfig, Partial>], + librefm: [LibrefmClientConfig,LibrefmClientAIOConfig, Partial>], + listenbrainz: [ListenBrainzClientConfig,ListenBrainzClientAIOConfig, Partial>], + koito: [KoitoClientConfig,KoitoClientAIOConfig, Partial>], + tealfm: [TealClientConfig,TealClientAIOConfig, Partial>], + rocksky: [RockSkyClientConfig,RockSkyClientAIOConfig, Partial>], + discord: [DiscordClientConfig,DiscordClientAIOConfig, Partial>] } -export const clientConfigSchemaMap: { [K in keyof ClientTypeConfigMap]: [z.ZodType,z.ZodType] } = { - maloja: [malojaClientConfigSchema,malojaClientAIOConfigSchema], - lastfm: [lastfmClientConfigSchema,lastfmClientAIOConfigSchema], - librefm: [librefmClientConfigSchema,librefmClientAIOConfigSchema], - listenbrainz: [listenBrainzClientConfigSchema,listenBrainzClientAIOConfigSchema], - koito: [koitoClientConfigSchema,koitoClientAIOConfigSchema], - tealfm: [tealClientConfigSchema,tealClientAIOConfigSchema], - rocksky: [rockSkyClientConfigSchema,rockSkyClientAIOConfigSchema], - discord: [discordClientConfigSchema,discordClientAIOConfigSchema] +export const clientConfigSchemaMap: { [K in keyof ClientTypeConfigMap]: [z.ZodType,z.ZodType,EnvClientSchema] } = { + maloja: [malojaClientConfigSchema,malojaClientAIOConfigSchema,malojaEnvSchemas], + lastfm: [lastfmClientConfigSchema,lastfmClientAIOConfigSchema,lastfmEnvSchemas], + librefm: [librefmClientConfigSchema,librefmClientAIOConfigSchema,librefmEnvSchemas], + listenbrainz: [listenBrainzClientConfigSchema,listenBrainzClientAIOConfigSchema,listenBrainzEnvSchemas], + koito: [koitoClientConfigSchema,koitoClientAIOConfigSchema,koitoEnvSchemas], + tealfm: [tealClientConfigSchema,tealClientAIOConfigSchema,tealEnvSchemas], + rocksky: [rockSkyClientConfigSchema,rockSkyClientAIOConfigSchema,rockSkyEnvSchemas], + discord: [discordClientConfigSchema,discordClientAIOConfigSchema,discordEnvSchemas] } export const validateClientJson = (clientType: T, json: object): ClientTypeConfigMap[T][0] => { diff --git a/src/backend/common/infrastructure/config/common.ts b/src/backend/common/infrastructure/config/common.ts index 56d429c2..2a7ca19a 100644 --- a/src/backend/common/infrastructure/config/common.ts +++ b/src/backend/common/infrastructure/config/common.ts @@ -2,6 +2,7 @@ import { stripIndents } from "common-tags"; import * as z from "zod"; import type { PlayTransformHooks, ExternalMetadataTerm } from "../../../../core/Transform.ts"; import type { CommonClientOptions } from "./client/index.ts"; +import type { MarkRequired } from "ts-essentials"; export const commonConfigPrimitivesSchema = z.object({ name: z.string().optional(), @@ -9,7 +10,7 @@ export const commonConfigPrimitivesSchema = z.object({ enable: z.boolean().optional() }); -export type CommonConfigPrimitives = z.infer; +export type CommonConfigPrimitives = MarkRequired, 'id'>; export const commonDataSchema = z.record(z.string(), z.any()); // keyOmit<{ [key: string]: any }, "options"> diff --git a/src/backend/scrobblers/ScrobbleClients.ts b/src/backend/scrobblers/ScrobbleClients.ts index f8a373e1..86c749ab 100644 --- a/src/backend/scrobblers/ScrobbleClients.ts +++ b/src/backend/scrobblers/ScrobbleClients.ts @@ -2,35 +2,26 @@ import { childLogger, type Logger } from '@foxxmd/logging'; import dayjs, { type Dayjs } from "dayjs"; import type {PlayObject, SourcePlayerObj} from "../../core/Atomic.ts"; -import type {ConfigMeta, InternalConfig, InternalConfigOptional, SourceIdentifier} from "../common/infrastructure/Atomic.ts"; +import type {InternalConfig, InternalConfigOptional, SourceIdentifier} from "../common/infrastructure/Atomic.ts"; import { isClientType } from '../../core/Atomic.ts'; import { clientTypes } from "../../core/Atomic.ts"; import type {ClientType} from "../../core/Atomic.ts"; -import {aioClientRelaxedConfigSchema, type AIOClientRelaxedConfig} from "../common/infrastructure/config/aioConfig.ts"; -import {validateClientAIOJson, validateClientJson, type ClientAIOConfig} from "../common/infrastructure/config/client/clients.ts"; -import type {LastfmClientConfig, LastfmData} from "../common/infrastructure/config/client/lastfm.ts"; -import type {ListenBrainzClientConfig, ListenBrainzData} from "../common/infrastructure/config/client/listenbrainz.ts"; -import type {MalojaClientConfig, MalojaData} from "../common/infrastructure/config/client/maloja.ts"; +import {aioClientRelaxedConfigSchema, type AIOClientRelaxedConfig, type ClientDefaults} from "../common/infrastructure/config/aioConfig.ts"; +import {clientConfigSchemaMap, validateClientAIOJson, validateClientJson} from "../common/infrastructure/config/client/clients.ts"; import type { WildcardEmitter } from "../common/WildcardEmitter.ts"; -import type { Notifiers } from "../notifier/Notifiers.ts"; -import { nonEmptyObj } from "../utils.ts"; -import { removeUndefinedKeys } from '../../core/DataUtils.ts'; -import { getCommonComponentEnvConfig, readJson } from '../utils/DataUtils.ts'; +import { pick } from '../../core/DataUtils.ts'; +import { readJson } from '../utils/DataUtils.ts'; import type AbstractScrobbleClient from "./AbstractScrobbleClient.ts"; -import type {KoitoClientConfig, KoitoData} from '../common/infrastructure/config/client/koito.ts'; -import type {TealClientConfig, TealData} from '../common/infrastructure/config/client/tealfm.ts'; -import type {RockSkyClientConfig, RockSkyData} from '../common/infrastructure/config/client/rocksky.ts'; -import type {LibrefmClientConfig, LibrefmData} from '../common/infrastructure/config/client/librefm.ts'; import clone from 'clone'; -import type {DiscordClientConfig, DiscordData} from '../common/infrastructure/config/client/discord.ts'; import { stripIndents } from 'common-tags'; import { normalizeStr, type StringNormalizationOptions } from '../utils/StringUtils.ts'; import { prettifyError, ZodError } from 'zod'; -import { transformPresetEnv } from '../common/infrastructure/config/common.ts'; +import { commonComponentEnvConfigToConfigPrimitives, generateCommonComponentEnvConfigSchema, transformPresetEnv, type CommonConfigPrimitives } from '../common/infrastructure/config/common.ts'; +import type { CommonClientConfig } from '../common/infrastructure/config/client/index.ts'; -type groupedNamedConfigs = {[key: string]: ParsedConfig[]}; +type UnparsedConfig = {config: object, type: ClientType, source?: 'file' | 'aio' | 'env', pos: string}; -type ParsedConfig = ClientAIOConfig & ConfigMeta; +type CommonParsedConfig = (CommonClientConfig & {source: string}); const clientScrobbleToNormalization: StringNormalizationOptions = { removeWhitespace: true, @@ -105,28 +96,28 @@ export default class ScrobbleClients { return [clientsReady, messages]; } - buildClientsFromConfig = async (notifier: Notifiers) => { - const configs: ParsedConfig[] = []; + buildClientsFromConfig = async () => { + const unparsedConfigs: UnparsedConfig[] = []; let configFile; try { - configFile = await readJson(`${this.internalConfig.configDir}/config.json`, {throwOnNotFound: false, logger: childLogger(this.logger, `Secrets`)}); + configFile = await readJson(`${this.internalConfig.configDir}/config.json`, { throwOnNotFound: false, logger: childLogger(this.logger, `Secrets`) }); } catch (e) { // think this should stay as show-stopper since config could include important defaults (delay, retries) we don't want to ignore - throw new Error('config.json could not be parsed', {cause: e}); + throw new Error('config.json could not be parsed', { cause: e }); } - let clientDefaults = {}; + let clientDefaults: ClientDefaults = {}; if (configFile !== undefined) { let aioConfig: AIOClientRelaxedConfig; try { aioConfig = aioClientRelaxedConfigSchema.parse(configFile); } catch (e) { const msg = `Validation error occurred while trying to parse 'config.json' for Client data/options`; - if(e instanceof ZodError) { + if (e instanceof ZodError) { this.logger.error(`${msg}:\n${prettifyError(e)}`); } else { - this.logger.error(new Error(msg, {cause: e})); + this.logger.error(new Error(msg, { cause: e })); } return; } @@ -137,372 +128,245 @@ export default class ScrobbleClients { retention } = {}, } = aioConfig; - clientDefaults = {retention, ...cd}; + clientDefaults = { retention, ...cd }; for (const [index, c] of mainConfigClientConfigs.entries()) { - const {name = 'unnamed'} = c; - if(c.type === undefined) { + const { name = 'unnamed' } = c; + if (c.type === undefined) { const invalidMsgType = `Client config ${index + 1} (${name}) in config.json does not have a "type" property! "type": "[clientType]" must be one of ${clientTypes.join(' | ')}`; this.logger.error(invalidMsgType); continue; } - if(c.configureAs === 'source') { - this.logger.debug(`Skipping config ${index + 1} (${name}) in config.json because it is configured as a source.`); - continue; - } - let validatedConfig: ClientAIOConfig; - try { - validatedConfig = await validateClientAIOJson(c.type.toLocaleLowerCase() as ClientType, c); - } catch (e) { - const msg = `Client config ${index + 1} (${c.type} - ${name}) in config.json is invalid and will not be used.`; - const err = new Error(msg, {cause: e}); - this.emitter.emit('error', err); - // pretty print error if its a zod error - if(e instanceof ZodError) { - this.logger.error(`${msg}:\n${prettifyError(e)}`); - } else { - this.logger.error(err); - } + if (isClientType(c.type)) { + unparsedConfigs.push({ + config: c, + source: 'aio', + type: c.type, + pos: `${index + 1} (${name})` + }); + } else { + const invalidMsgType = `Client config ${index + 1} (${name}) in config.json has an invalid "type" property. Must be one of must be one of ${clientTypes.join(' | ')}`; + this.logger.error(invalidMsgType); continue; } - configs.push({...validatedConfig, - name: validatedConfig.name ?? 'unnamed', - source: 'config.json', - configureAs: 'client', //override user value - }); } } + const envKeys = Object.keys(process.env).map(x => x.toUpperCase()); + for (const clientType of clientTypes) { - const defaultConfigureAs = 'client'; - switch (clientType) { - case 'maloja': { - // env builder for single user mode - const data = removeUndefinedKeys({ - url: process.env.MALOJA_URL, - apiKey: process.env.MALOJA_API_KEY - }, false); - const p = getCommonComponentEnvConfig('MALOJA'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'maloja', - name: 'unnamed-mlj', - source: 'ENV', - mode: 'single', - configureAs: 'client', - data: data, - ...p, - options: transformPresetEnv('MALOJA') - }) - } - } break; - case 'lastfm': { - const data = removeUndefinedKeys({ - apiKey: process.env.LASTFM_API_KEY, - secret: process.env.LASTFM_SECRET, - redirectUri: process.env.LASTFM_REDIRECT_URI, - session: process.env.LASTFM_SESSION - }, false); - const p = getCommonComponentEnvConfig('LASTFM'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'lastfm', - name: 'unnamed-lfm', - source: 'ENV', - mode: 'single', - configureAs: 'client', - data: data, - ...p, - options: transformPresetEnv('LASTFM') - }) - } - } break; - case 'librefm': { - const data = removeUndefinedKeys({ - apiKey: process.env.LIBREFM_API_KEY, - secret: process.env.LIBREFM_SECRET, - redirectUri: process.env.LIBREFM_REDIRECT_URI, - session: process.env.LIBREFM_SESSION, - urlBase: process.env.LIBREFM_URLBASE, - }, false); - const p = getCommonComponentEnvConfig('LIBREFM'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'librefm', - name: 'unnamed-librefm', - source: 'ENV', - mode: 'single', - configureAs: 'client', - data: data, - ...p, - options: transformPresetEnv('LIBREFM') - }) - } - } break; - case 'listenbrainz': { - const data = removeUndefinedKeys({ - url: process.env.LZ_URL, - token: process.env.LZ_TOKEN, - username: process.env.LZ_USER - }, false); - const p = getCommonComponentEnvConfig('LZ'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'listenbrainz', - name: 'unnamed-lz', - source: 'ENV', - mode: 'single', - configureAs: 'client', - data: data, - ...p, - options: transformPresetEnv('LZ') - }) - } - } break; - case 'koito': { - const data = removeUndefinedKeys({ - url: process.env.KOITO_URL, - token: process.env.KOITO_TOKEN, - username: process.env.KOITO_USER - }, false); - const p = getCommonComponentEnvConfig('KOITO'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'koito', - name: 'unnamed-koito', - source: 'ENV', - mode: 'single', - configureAs: 'client', - data: data, - ...p, - options: transformPresetEnv('KOITO') - }) - } - } break; - case 'tealfm': { - const data: TealData = removeUndefinedKeys({ - identifier: process.env.TEALFM_IDENTIFIER, - appPassword: process.env.TEALFM_APP_PW, - }, false); - const p = getCommonComponentEnvConfig('TEALFM'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'tealfm', - name: 'unnamed-tealfm', - source: 'ENV', - mode: 'single', - configureAs: 'client', - data: data, - ...p, - options: transformPresetEnv('TEALFM') - }) - } - } break; - case 'rocksky': { - const data: RockSkyData = removeUndefinedKeys({ - key: process.env.ROCKSKY_KEY, - token: process.env.ROCKSKY_TOKEN, - handle: process.env.ROCKSKY_HANDLE - }, false); - const p = getCommonComponentEnvConfig('ROCKSKY'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'rocksky', - name: 'unnamed-rocksky', - source: 'ENV', - mode: 'single', - configureAs: 'client', - data: data, - ...p, - options: transformPresetEnv('ROCKSKY') - }) - } - } break; - case 'discord': { - const data: DiscordData = removeUndefinedKeys({ - token: process.env.DISCORD_TOKEN, - artwork: process.env.DISCORD_ARTWORK, - applicationId: process.env.DISCORD_APPLICATION_ID, - ipcLocations: process.env.DISCORD_IPC_LOCATIONS, - artworkDefaultUrl: process.env.DISCORD_ARTWORK_DEFAULT_URL, - statusOverrideAllow: process.env.DISCORD_STATUS_OVERRIDE_ALLOW, - listeningActivityAllow: process.env.DISCORD_LISTENING_ACTIVITY_ALLOW - }, false); - const p = getCommonComponentEnvConfig('DISCORD'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'discord', - name: 'unnamed-discord', - source: 'ENV', - mode: 'single', - configureAs: 'client', - data: data, - ...p, - options: transformPresetEnv('DISCORD') - }) - } - } break; - default: - break; - } + + let clientUnparsedConfigs = unparsedConfigs.filter(x => x.type === clientType); + + const clientUpper = clientType.toUpperCase(); + let rawClientConfigs; try { - rawClientConfigs = await readJson(`${this.internalConfig.configDir}/${clientType}.json`, {throwOnNotFound: false, logger: childLogger(this.logger, `${clientType} Secrets`)}); + rawClientConfigs = await readJson(`${this.internalConfig.configDir}/${clientType}.json`, { throwOnNotFound: false, logger: childLogger(this.logger, `${clientType} Secrets`) }); } catch (e) { const errMsg = `${clientType}.json config file could not be parsed`; this.emitter.emit('error', errMsg); this.logger.error(errMsg); - continue; } + if (rawClientConfigs !== undefined) { - let clientConfigs: ParsedConfig[] = []; + this.logger.debug(`Found config file ${clientType}.json`); if (Array.isArray(rawClientConfigs)) { - clientConfigs = rawClientConfigs; - } else if(rawClientConfigs === null) { - this.logger.error(`${clientType}.json contained no data`); - continue; - } else if(typeof rawClientConfigs === 'object') { - clientConfigs = [rawClientConfigs]; + clientUnparsedConfigs = clientUnparsedConfigs.concat(rawClientConfigs.map((x, i) => ({ config: x, type: clientType, source: 'file', pos: `${i + 1}` }))); + } else if (rawClientConfigs === null) { + this.logger.warn(`${clientType}.json contained no data`); + } else if (typeof rawClientConfigs === 'object') { + clientUnparsedConfigs.push({ config: rawClientConfigs, type: clientType, source: 'file', pos: `object` }) } else { this.logger.error(`All top level data from ${clientType}.json must be an object or an array of objects, will not parse configs from file`); - continue; } - for(const [i,rawConf] of rawClientConfigs.entries()) { - if(rawConf.configureAs === 'source') - { - this.logger.debug(`Skipping config ${i + 1} from ${clientType}.json because it is configured as a source.`); - continue; - } - try { - const validConfig = await validateClientJson(clientType, rawConf); // await validateJson('client', rawConf, this.getSchemaByType(clientType), this.logger); - const {configureAs = defaultConfigureAs} = validConfig; - if (configureAs === 'client') { - const parsedConfig: ParsedConfig = { - ...rawConf, - source: `${clientType}.json`, - type: clientType + } + + const clientKeys = envKeys.filter(x => x.includes(clientUpper)); + if (clientKeys.length > 0) { + clientUnparsedConfigs.push({ + config: pick(process.env, ...clientKeys), + type: clientType, + source: 'env', + pos: '' + }) + } + + const strongConfigs: CommonParsedConfig[] = []; + for (const entry of clientUnparsedConfigs) { + let parsedConfig: CommonParsedConfig; + try { + const sourceStr = `${entry.source} ${entry.pos}`; + switch (entry.source) { + case 'env': { + const primitiveSchema = generateCommonComponentEnvConfigSchema(clientUpper); + const parsed = primitiveSchema.parse(entry.config); + const primitives: CommonConfigPrimitives = commonComponentEnvConfigToConfigPrimitives(clientUpper, parsed); + const envSchema = clientConfigSchemaMap[clientType][2]; + const parsedEnvConfigValues = envSchema.env.parse(entry.config); + const { data = {}, options = {} } = envSchema.toConfig(parsedEnvConfigValues); + const transformOptions = transformPresetEnv(clientUpper); + parsedConfig = { + name: `${clientType} - ${entry.source}${entry.pos !== '' ? ` - ${entry.pos}` : ''} `, + ...primitives, + data, + source: sourceStr, + options: { + ...options, + ...(transformOptions ?? {}) + } + }; + } break; + case 'file': + case 'aio': { + if ('configureAs' in entry.config && entry.config.configureAs === 'source') { + this.logger.debug(`Skipping ${clientType} Config ${entry.source} ${entry.pos} because it is configured as a Source`); + continue; } - configs.push(parsedConfig); - } - } catch (e: any) { - const msg = `The config entry at index ${i} from ${clientType}.json was not valid`; - const configErr = new Error(msg, {cause: e}); - this.emitter.emit('error', configErr); - // pretty print error if its a zod error - if(e instanceof ZodError) { - this.logger.error(`${msg}:\n${prettifyError(e)}`); - } else { - this.logger.error(configErr); - } + const parsed = entry.source === 'file' ? validateClientJson(entry.type, entry.config) : validateClientAIOJson(entry.type, entry.config); + parsedConfig = { + ...parsed, + source: sourceStr + } + } break; + } + } catch (e) { + const msg = `Failed to validate ${clientType} Config ${entry.source} ${entry.pos}`; + if (e instanceof ZodError) { + this.logger.error(`${msg}:\n${prettifyError(e)}`); + } else { + this.logger.error(new Error(msg, { cause: e })); } + continue; } - } - } - // all client configs are minimally valid - // now check that names are unique - const nameGroupedConfigs = configs.reduce((acc: groupedNamedConfigs, curr: ParsedConfig) => { - const {name = 'unnamed'} = curr; - const {[name]: n = []} = acc; - return {...acc, [name]: [...n, curr]}; - }, {}); - let noConflictConfigs: ParsedConfig[] = []; - for (const [name, configs] of Object.entries(nameGroupedConfigs)) { - if (configs.length > 1) { - const sources = configs.map((c: any) => `Config object from ${c.source} of type [${c.type}]`); - this.logger.error(`The following clients will not be built because of config naming conflicts (they have the same name of "${name}"): -${sources.join('\n')}`); - if (name === 'unnamed') { - this.logger.info('HINT: "unnamed" configs occur when using ENVs, if a multi-user mode config does not have a "name" property, or if a config is built in single-user mode'); + if (parsedConfig.enable === false) { + this.logger.debug(`Not using Config ${parsedConfig.id} (${parsedConfig.name}) because it was marked as not enabled.`); + } else { + strongConfigs.push(parsedConfig); } - } else { - noConflictConfigs = [...noConflictConfigs, ...configs]; } - } - // finally! all configs are valid, structurally, and can now be passed to addClient - // just need to re-map unnnamed to default - const finalConfigs: ParsedConfig[] = noConflictConfigs.map(({name = 'unnamed', ...x}) => ({ - ...x, - name - })); - for (const c of finalConfigs) { - try { - await this.addClient(c, clientDefaults, notifier); - } catch(e) { - const addError = new Error(`Client ${c.name} from ${c.source} was not added because it had unrecoverable errors`, {cause: e}); - this.emitter.emit('error', addError); - this.logger.error(addError); - } + await this.addClient(clientType, strongConfigs, clientDefaults); } } - addClient = async (clientConfig: ParsedConfig, defaults = {}, notifier: Notifiers) => { -/* const isValidConfig = isValidConfigStructure(clientConfig, {name: true, data: true, type: true}); - if (isValidConfig !== true) { - throw new Error(`Config object from ${clientConfig.source || 'unknown'} with name [${clientConfig.name || 'unnamed'}] of type [${clientConfig.type || 'unknown'}] has errors: ${isValidConfig.join(' | ')}`) - }*/ - const {type, name, enable = true, source, data: d = {}, options = {}} = clientConfig; - - if(enable === false) { - this.logger.warn({labels: [`${type} - ${name}`]}, `Client from ${source} was disabled by config`); - return; - } - - // add defaults - const compositeOptions = {...defaults, ...options}; - let newClient; - this.logger.debug({labels: [`${type} - ${name}`]}, `Constructing Client from ${source}`); - switch (type) { - case 'maloja': { - const MalojaScrobbler = (await import('./MalojaScrobbler.ts')).default; - newClient = new MalojaScrobbler(name, ({...clientConfig, data: d, options: compositeOptions} as unknown as MalojaClientConfig), this.emitter, this.logger); - break; - } - case 'lastfm': { - const LastfmScrobbler = (await import('./LastfmScrobbler.ts')).default; - newClient = new LastfmScrobbler(name, {...clientConfig, data: d, options: compositeOptions } as unknown as LastfmClientConfig, this.internalConfig, this.emitter, this.logger); - break; - } - case 'librefm': { - const LibrefmScrobbler = (await import('./LibrefmScrobbler.ts')).default; - newClient = new LibrefmScrobbler(name, {...clientConfig, data: d, options: compositeOptions } as unknown as LibrefmClientConfig, this.internalConfig, this.emitter, this.logger); - break; - } - case 'listenbrainz': { - const ListenbrainzScrobbler = (await import('./ListenbrainzScrobbler.ts')).default; - newClient = new ListenbrainzScrobbler(name, {...clientConfig, data: {configDir: this.internalConfig.configDir, ...d}, options: compositeOptions } as unknown as ListenBrainzClientConfig, {}, this.emitter, this.logger); - break; - } - case 'koito': { - const KoitoScrobbler = (await import('./KoitoScrobbler.ts')).default; - newClient = new KoitoScrobbler(name, {...clientConfig, data: {configDir: this.internalConfig.configDir, ...d}, options: compositeOptions } as unknown as KoitoClientConfig, {}, this.emitter, this.logger); - break; - } - case 'tealfm': { - const TealScrobbler = (await import('./TealfmScrobbler.ts')).default; - newClient = new TealScrobbler(name, {...clientConfig, data: d, options: compositeOptions} as unknown as TealClientConfig, this.internalConfig, this.emitter, this.logger); - break; - } - case 'rocksky': { - const RockskyScrobbler = (await import('./RockskyScrobbler.ts')).default; - newClient = new RockskyScrobbler(name, {...clientConfig, data: {configDir: this.internalConfig.configDir, ...d}, options: compositeOptions } as unknown as RockSkyClientConfig, this.internalConfig, this.emitter, this.logger); - break; - } - case 'discord': { - const DiscordScrobbler = (await import('./DiscordScrobbler.ts')).default; - newClient = new DiscordScrobbler(name, {...clientConfig, data: {configDir: this.internalConfig.configDir, ...d}, options: compositeOptions } as unknown as DiscordClientConfig, {}, this.emitter, this.logger); - break; + addClient = async (clientType: ClientType, strongConfigs: CommonParsedConfig[], clientDefaults: ClientDefaults = {}) => { + switch (clientType) { + case 'discord': { + const DiscordScrobbler = (await import('./DiscordScrobbler.ts')).default; + for (const s of strongConfigs) { + try { + const config = clientConfigSchemaMap[clientType][0].parse(s); + const compositeOptions = { ...clientDefaults, ...config.options }; + const newClient = new DiscordScrobbler(config.name, { ...config, options: compositeOptions }, {}, this.emitter, this.logger); + newClient.logger.info(`Client added from ${s.source}`); + this.clients.push(newClient); + } catch (e) { + this.logger.error(new Error(`Client from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; + case 'koito': { + const KoitoScrobbler = (await import('./KoitoScrobbler.ts')).default; + for (const s of strongConfigs) { + try { + const config = clientConfigSchemaMap[clientType][0].parse(s); + const compositeOptions = { ...clientDefaults, ...config.options, configDir: this.internalConfig.configDir }; + + const newClient = new KoitoScrobbler(config.name, { ...config, options: compositeOptions }, {}, this.emitter, this.logger); + newClient.logger.info(`Client added from ${s.source}`); + this.clients.push(newClient); + } catch (e) { + this.logger.error(new Error(`Client from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; + case 'lastfm': { + const LastfmScrobbler = (await import('./LastfmScrobbler.ts')).default; + for (const s of strongConfigs) { + try { + const config = clientConfigSchemaMap[clientType][0].parse(s); + const compositeOptions = { ...clientDefaults, ...config.options }; + + const newClient = new LastfmScrobbler(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter, this.logger); + newClient.logger.info(`Client added from ${s.source}`); + this.clients.push(newClient); + } catch (e) { + this.logger.error(new Error(`Client from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; + case 'librefm': { + const LibrefmScrobbler = (await import('./LibrefmScrobbler.ts')).default; + for (const s of strongConfigs) { + try { + const config = clientConfigSchemaMap[clientType][0].parse(s); + const compositeOptions = { ...clientDefaults, ...config.options }; + const newClient = new LibrefmScrobbler(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter, this.logger); + newClient.logger.info(`Client added from ${s.source}`); + this.clients.push(newClient); + } catch (e) { + this.logger.error(new Error(`Client from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; + case 'listenbrainz': { + const ListenbrainzScrobbler = (await import('./ListenbrainzScrobbler.ts')).default; + for (const s of strongConfigs) { + try { + const config = clientConfigSchemaMap[clientType][0].parse(s); + const compositeOptions = { ...clientDefaults, ...config.options }; + const newClient = new ListenbrainzScrobbler(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter, this.logger); + newClient.logger.info(`Client added from ${s.source}`); + this.clients.push(newClient); + } catch (e) { + this.logger.error(new Error(`Client from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; + case 'maloja': { + const MalojaScrobbler = (await import('./MalojaScrobbler.ts')).default; + for (const s of strongConfigs) { + try { + const config = clientConfigSchemaMap[clientType][0].parse(s); + const compositeOptions = { ...clientDefaults, ...config.options }; + const newClient = new MalojaScrobbler(config.name, { ...config, options: compositeOptions }, this.emitter, this.logger); + newClient.logger.info(`Client added from ${s.source}`); + this.clients.push(newClient); + } catch (e) { + this.logger.error(new Error(`Client from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; + case 'rocksky': { + const RockskyScrobbler = (await import('./RockskyScrobbler.ts')).default; + for (const s of strongConfigs) { + try { + const config = clientConfigSchemaMap[clientType][0].parse(s); + const compositeOptions = { ...clientDefaults, ...config.options }; + const newClient = new RockskyScrobbler(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter, this.logger); + newClient.logger.info(`Client added from ${s.source}`); + this.clients.push(newClient); + } catch (e) { + this.logger.error(new Error(`Client from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; + case 'tealfm': { + const TealScrobbler = (await import('./TealfmScrobbler.ts')).default; + for (const s of strongConfigs) { + try { + const config = clientConfigSchemaMap[clientType][0].parse(s); + const compositeOptions = { ...clientDefaults, ...config.options }; + const newClient = new TealScrobbler(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter, this.logger); + newClient.logger.info(`Client added from ${s.source}`); + this.clients.push(newClient); + } catch (e) { + this.logger.error(new Error(`Client from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } } - default: - break; - } - - if(newClient === undefined) { - // really shouldn't get here! - throw new Error(`Client of type ${type} from ${source} was not recognized??`); - } - newClient.logger.info(`Client Added from ${source}`); - this.clients.push(newClient); } playingNow = async (data: SourcePlayerObj, options: {scrobbleTo: string[], scrobbleFrom: SourceIdentifier}) => { diff --git a/src/backend/tests/config/config.test.ts b/src/backend/tests/config/config.test.ts index 61ef5efc..a23cbdcf 100644 --- a/src/backend/tests/config/config.test.ts +++ b/src/backend/tests/config/config.test.ts @@ -7,11 +7,10 @@ import path from "path"; import ScrobbleClients from '../../scrobblers/ScrobbleClients.ts'; import ScrobbleSources from '../../sources/ScrobbleSources.ts'; import EventEmitter from "events"; -import {loggerTest} from '@foxxmd/logging'; +import {loggerDebug, loggerTest} from '@foxxmd/logging'; import { clientTypes } from "../../../core/Atomic.ts"; import { projectRootDir } from "../../common/infrastructure/Atomic.ts"; import { sourceTypes } from "../../../core/Atomic.ts"; -import { Notifiers } from '../../notifier/Notifiers.ts'; import { difference } from '../../utils.ts'; import { validateSourceJson } from '../../common/infrastructure/config/source/sources.ts'; import { readJson } from '../../utils/DataUtils.ts'; @@ -135,7 +134,7 @@ describe('Sample Configs', function () { }); it(`Sample ${componentType}.json parses and validates in ScrobbleClients`, async function () { - this.timeout(5000); + this.timeout(500000); const emitter = new EventEmitter(); await copyFile(samplePath(componentType), `${componentType}.json`); @@ -144,8 +143,8 @@ describe('Sample Configs', function () { configDir: process.cwd(), version: 'test' }, - loggerTest); - await clients.buildClientsFromConfig(new Notifiers(new EventEmitter, new EventEmitter, new EventEmitter, loggerTest)); + loggerDebug); + await clients.buildClientsFromConfig(); expect(clients.clients).length(1); }); } diff --git a/src/core/DataUtils.ts b/src/core/DataUtils.ts index 68103dd1..d557e015 100644 --- a/src/core/DataUtils.ts +++ b/src/core/DataUtils.ts @@ -162,3 +162,11 @@ export const removeUndefinedKeys = >(obj: T, retur //Object.keys(newObj).forEach(key => newObj[key] === undefined || newObj[key] && delete newObj[key]) return newObj; }; + +export const pick = (obj: T, ...keys: K[]) => ( + Object.fromEntries( + keys + .filter(key => key in obj) + .map(key => [key, obj[key]]) + ) as Pick +); \ No newline at end of file -- 2.51.2 From 8a71663c9f9d45838c68ea085c24daf2ebe94eb9 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 18:27:01 +0000 Subject: [PATCH 09/35] Implement remaining source env schemas --- .../config/source/applemusic.ts | 76 +++++++++++++++++-- .../infrastructure/config/source/azuracast.ts | 24 +++++- .../config/source/chromecast.ts | 9 ++- .../infrastructure/config/source/deezer.ts | 19 ++++- .../infrastructure/config/source/icecast.ts | 4 + .../infrastructure/config/source/mopidy.ts | 22 +++++- .../config/source/webscrobbler.ts | 5 +- src/backend/utils/ZodUtils.ts | 6 +- 8 files changed, 148 insertions(+), 17 deletions(-) diff --git a/src/backend/common/infrastructure/config/source/applemusic.ts b/src/backend/common/infrastructure/config/source/applemusic.ts index b00ddf6d..d79ce2fa 100644 --- a/src/backend/common/infrastructure/config/source/applemusic.ts +++ b/src/backend/common/infrastructure/config/source/applemusic.ts @@ -1,12 +1,14 @@ import * as z from "zod"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, type EnvSourceSchema} from "./index.ts"; +import { SimpleError } from "../../../errors/MSErrors.ts"; export const appleMusicKeySchema = z.object({ id: z.string(), teamId: z.string(), p8: z.string() }); +const envKeyKeys = ['APPLEMUSIC_KEY_ID','APPLEMUSIC_KEY_P8','APPLEMUSIC_TEAM_ID']; export type AppleMusicKey = z.infer; @@ -15,7 +17,7 @@ export const appleMusicDataSchema = z.object({ ...pollingOptionsSchema.shape, key: appleMusicKeySchema.optional(), token: z.string().optional(), - mediaUserToken: z.string().optional(), + mediaUserToken: z.string(), /** * Origin header to include in every Apple Music API request. * Required when using a browser token (not a MusicKit key). @@ -30,11 +32,7 @@ export const appleMusicDataSchema = z.object({ export type AppleMusicData = z.infer; -export const appleMusicSourceConfigSchema = z.object({ - ...commonSourceConfigSchema.shape, - data: appleMusicDataSchema.optional(), - options: z.object({ - ...commonSourceOptionsSchema.shape, +export const appleMusicOptions = z.object({ logAuth: z.boolean().optional(), logDiff: z.boolean().optional(), /** @@ -70,11 +68,75 @@ export const appleMusicSourceConfigSchema = z.object({ default: true, examples: [true, false] }), +}); + +export type AppleMusicOptions = z.infer; + +export const appleMusicSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: appleMusicDataSchema.optional(), + options: z.object({ + ...commonSourceOptionsSchema.shape, + ...appleMusicOptions.shape }).optional(), }); export type AppleMusicSourceConfig = z.infer; +const envDataSchema = z.object({ + APPLEMUSIC_KEY_ID: appleMusicKeySchema.shape.id.optional(), + APPLEMUSIC_TEAM_ID: appleMusicKeySchema.shape.teamId.optional(), + APPLEMUSIC_KEY_P8: appleMusicKeySchema.shape.p8.optional(), + APPLEMUSIC_MEDIA_USER_TOKEN: appleMusicDataSchema.shape.mediaUserToken, + APPLEMUSIC_TOKEN: appleMusicDataSchema.shape.token, + APPLEMUSIC_ORIGIN_HEADER: appleMusicDataSchema.shape.origin, + APPLEMUSIC_RECOVER_UNCHANGED_TOP_HISTORY: z.stringbool().optional().meta(appleMusicOptions.shape.recoverUnchangedTopHistory.meta()), + APPLEMUSIC_NORMALIZE_ALBUM: z.stringbool().optional().meta(appleMusicOptions.shape.normalizeAlbum.meta()) +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => { + let appleMusicKey: AppleMusicKey | undefined; + let token: string | undefined; + let origin: string | undefined; + if(envKeyKeys.some(x => partial[x] !== undefined)) { + for(const k of envKeyKeys) { + if(partial[k] === undefined) { + throw new SimpleError(`ENV ${partial[k]} is not defined but when providing auth via MusicKit Key you must provide all of these: ${envKeyKeys.join(', ')}`); + } + appleMusicKey = { + id: partial.APPLEMUSIC_KEY_ID, + teamId: partial.APPLEMUSIC_TEAM_ID, + p8: partial.APPLEMUSIC_KEY_P8 + }; + } + } else { + token = partial.APPLEMUSIC_TOKEN; + if(token === undefined) { + throw new SimpleError('If not providing auth via MusicKit then you must provide a browser token with ENV APPLEMUSIC_TOKEN, but none was defined.'); + } + origin = partial.APPLEMUSIC_ORIGIN_HEADER; + if(token === undefined) { + throw new SimpleError('If not providing auth via MusicKit then you must provide an origin header with ENV APPLEMUSIC_ORIGIN_HEADER, but none was defined.'); + } + } + + return { + data: { + key: appleMusicKey, + token, + origin, + mediaUserToken: partial.APPLEMUSIC_MEDIA_USER_TOKEN + }, + options: { + recoverUnchangedTopHistory: partial.APPLEMUSIC_RECOVER_UNCHANGED_TOP_HISTORY, + normalizeAlbum: partial.APPLEMUSIC_NORMALIZE_ALBUM + } + } + } +}; + export const appleMusicSourceAIOConfigSchema = z.object({ ...appleMusicSourceConfigSchema.shape, type: z.literal('applemusic'), diff --git a/src/backend/common/infrastructure/config/source/azuracast.ts b/src/backend/common/infrastructure/config/source/azuracast.ts index a0f49baf..02f85410 100644 --- a/src/backend/common/infrastructure/config/source/azuracast.ts +++ b/src/backend/common/infrastructure/config/source/azuracast.ts @@ -1,5 +1,6 @@ import * as z from "zod"; -import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, manualListeningOptionsSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, manualListeningOptionsSchema, type EnvSourceSchema} from "./index.ts"; +import { parseBoolStrict } from "../../../../utils.ts"; export const azuraStationInfoResponseSchema = z.object({ id: z.string(), @@ -136,6 +137,27 @@ export const azuracastSourceConfigSchema = z.object({ export type AzuracastSourceConfig = z.infer; +const envDataSchema = z.object({ + AZURA_STATION: azuracastDataSchema.shape.station, + AZURA_URL: azuracastDataSchema.shape.url, + AZURA_KEY: azuracastDataSchema.shape.apiKey, + AZURA_LISTENERS_NUM: z.string().optional().transform((val) => !isNaN(Number.parseInt(val)) ? Number.parseInt(val) : parseBoolStrict(val)), + AZURA_LIVE: z.stringbool().optional() +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + url: partial.AZURA_URL, + station: partial.AZURA_STATION, + apiKey: partial.AZURA_KEY, + monitorWhenListeners: partial.AZURA_LISTENERS_NUM, + monitorWhenLive: partial.AZURA_LIVE + } + }) +}; + export const azuracastSourceAIOConfigSchema = z.object({ ...azuracastSourceConfigSchema.shape, type: z.literal('azuracast') diff --git a/src/backend/common/infrastructure/config/source/chromecast.ts b/src/backend/common/infrastructure/config/source/chromecast.ts index 54595c4b..2feaf711 100644 --- a/src/backend/common/infrastructure/config/source/chromecast.ts +++ b/src/backend/common/infrastructure/config/source/chromecast.ts @@ -1,5 +1,6 @@ import * as z from "zod"; import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; +import { transformSplitMaybeString } from "../../../../utils/ZodUtils.ts"; export const chromecastDeviceInfoSchema = z.object({ /** @@ -133,10 +134,10 @@ export const chromecastDataSchema = z.object({ export type ChromecastData = z.infer; const envDataSchema = z.object({ - CC_BLACKLIST_DEVICES: chromecastDataSchema.shape.blacklistDevices, - CC_WHITELIST_DEVICES: chromecastDataSchema.shape.whitelistDevices, - CC_BLACKLIST_APPS: chromecastDataSchema.shape.blacklistApps, - CC_WHITELIST_APPS: chromecastDataSchema.shape.whitelistApps, + CC_BLACKLIST_DEVICES: z.string().optional().pipe(transformSplitMaybeString).meta(chromecastDataSchema.shape.blacklistDevices.meta()), + CC_WHITELIST_DEVICES: z.string().optional().pipe(transformSplitMaybeString).meta(chromecastDataSchema.shape.whitelistDevices.meta()), + CC_BLACKLIST_APPS: z.string().optional().pipe(transformSplitMaybeString).meta(chromecastDataSchema.shape.blacklistApps.meta()), + CC_WHITELIST_APPS: z.string().optional().pipe(transformSplitMaybeString).meta(chromecastDataSchema.shape.whitelistApps.meta()), }); export const envSchemas: EnvSourceSchema = { diff --git a/src/backend/common/infrastructure/config/source/deezer.ts b/src/backend/common/infrastructure/config/source/deezer.ts index 6a509319..267d66f5 100644 --- a/src/backend/common/infrastructure/config/source/deezer.ts +++ b/src/backend/common/infrastructure/config/source/deezer.ts @@ -1,7 +1,7 @@ import * as z from "zod"; import type {Second} from "../../../../../core/Atomic.ts"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, type EnvSourceSchema} from "./index.ts"; export const deezerDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -105,6 +105,23 @@ export const deezerCompatConfigSchema = z.union([deezerSourceConfigSchema, deeze export type DeezerCompatConfig = z.infer; +const envDataSchema = z.object({ + DEEZER_ARL: deezerInternalDataSchema.shape.arl, + DEEZER_ACCOUNT_ID: deezerInternalDataSchema.shape.accountId, + DEEZER_USER_AGENT: deezerInternalDataSchema.shape.userAgent +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + arl: partial.DEEZER_ARL, + accountId: partial.DEEZER_ACCOUNT_ID, + userAgent: partial.DEEZER_USER_AGENT + } + }) +} + export const deezerAIOCompatConfigSchema = z.union([deezerSourceAIOConfigSchema, deezerInternalAIOConfigSchema]); export type DeezerAIOCompatConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/icecast.ts b/src/backend/common/infrastructure/config/source/icecast.ts index 0010d534..50ae8546 100644 --- a/src/backend/common/infrastructure/config/source/icecast.ts +++ b/src/backend/common/infrastructure/config/source/icecast.ts @@ -46,6 +46,7 @@ export type IcecastData = z.infer; const envDataSchema = z.object({ ICECAST_URL: icecastDataSchema.shape.url, + ICECAST_AUTO_MONITOR: z.stringbool().optional() }); export const envSchemas: EnvSourceSchema = { @@ -53,6 +54,9 @@ export const envSchemas: EnvSourceSchema ({ data: { url: partial.ICECAST_URL + }, + options: { + autoMonitor: partial.ICECAST_AUTO_MONITOR } }) }; diff --git a/src/backend/common/infrastructure/config/source/mopidy.ts b/src/backend/common/infrastructure/config/source/mopidy.ts index e7def4fb..9d76ae41 100644 --- a/src/backend/common/infrastructure/config/source/mopidy.ts +++ b/src/backend/common/infrastructure/config/source/mopidy.ts @@ -1,6 +1,7 @@ import * as z from "zod"; import {pollingOptionsSchema} from "../common.ts"; -import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; +import { transformSplitMaybeString } from "../../../../utils/ZodUtils.ts"; export const mopidyDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -100,6 +101,25 @@ export const mopidySourceConfigSchema = z.object({ export type MopidySourceConfig = z.infer; +const envDataSchema = z.object({ + MOPIDY_URL: mopidyDataSchema.shape.url, + MOPIDY_URI_DENYLIST: z.string().optional().pipe(transformSplitMaybeString).meta(mopidyDataSchema.shape.uriBlacklist.meta()), + MOPIDY_URI_ALLOWLIST: z.string().optional().pipe(transformSplitMaybeString).meta(mopidyDataSchema.shape.uriWhitelist.meta()), + MOPIDY_ALBUM_DENYLIST: z.string().optional().pipe(transformSplitMaybeString).meta(mopidyDataSchema.shape.albumBlacklist.meta()), +}); + +export const envSchemas: EnvSourceSchema = { + env: envDataSchema, + toConfig: (partial) => ({ + data: { + url: partial.MOPIDY_URL, + uriBlacklist: partial.MOPIDY_URI_DENYLIST, + uriWhitelist: partial.MOPIDY_URI_ALLOWLIST, + albumBlacklist: partial.MOPIDY_ALBUM_DENYLIST + } + }) +}; + export const mopidySourceAIOConfigSchema = z.object({ ...mopidySourceConfigSchema.shape, type: z.literal('mopidy'), diff --git a/src/backend/common/infrastructure/config/source/webscrobbler.ts b/src/backend/common/infrastructure/config/source/webscrobbler.ts index 41f990a3..730fca71 100644 --- a/src/backend/common/infrastructure/config/source/webscrobbler.ts +++ b/src/backend/common/infrastructure/config/source/webscrobbler.ts @@ -1,5 +1,6 @@ import * as z from "zod"; import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; +import { transformSplitMaybeString } from "../../../../utils/ZodUtils.ts"; export const webScrobblerDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -47,8 +48,8 @@ export const webScrobblerDataSchema = z.object({ export type WebScrobblerData = z.infer; const envDataSchema = z.object({ - WS_BLACKLIST: webScrobblerDataSchema.shape.blacklist, - WS_WHITELIST: webScrobblerDataSchema.shape.whitelist, + WS_BLACKLIST: z.string().optional().pipe(transformSplitMaybeString).default([]).meta(webScrobblerDataSchema.shape.blacklist.meta()), + WS_WHITELIST: z.string().optional().pipe(transformSplitMaybeString).default([]).meta(webScrobblerDataSchema.shape.whitelist.meta()) }); export const envSchemas: EnvSourceSchema = { diff --git a/src/backend/utils/ZodUtils.ts b/src/backend/utils/ZodUtils.ts index 33e4a410..9e61d3be 100644 --- a/src/backend/utils/ZodUtils.ts +++ b/src/backend/utils/ZodUtils.ts @@ -1,4 +1,5 @@ import z from 'zod'; +import { parseArrayFromMaybeString, parseBoolOrArrayFromMaybeString } from './StringUtils.ts'; export interface TableColumn { title: string @@ -49,4 +50,7 @@ export const zodObjectToTableColumns = (schema: z.Z default: getExplicitDefault(fieldSchema) ?? (meta?.default as unknown), description: meta?.description as string | undefined }; - }); \ No newline at end of file +}); + +export const transformSplitMaybeString = z.transform((val: string) => parseArrayFromMaybeString(val)); +export const transformSplitMaybeStringOrBoolean = z.transform((val: string | string[] | boolean) => parseBoolOrArrayFromMaybeString(val)); \ No newline at end of file -- 2.51.2 From 0115ccb862b5d84d74a6d23515ef6e5e11a9df0b Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 19:01:10 +0000 Subject: [PATCH 10/35] refactor: Simplify ScrobbleSources config builder --- .../infrastructure/config/source/deezer.ts | 2 +- .../infrastructure/config/source/sources.ts | 186 +-- src/backend/sources/ScrobbleSources.ts | 1410 ++++++----------- 3 files changed, 573 insertions(+), 1025 deletions(-) diff --git a/src/backend/common/infrastructure/config/source/deezer.ts b/src/backend/common/infrastructure/config/source/deezer.ts index 267d66f5..37e5a86b 100644 --- a/src/backend/common/infrastructure/config/source/deezer.ts +++ b/src/backend/common/infrastructure/config/source/deezer.ts @@ -111,7 +111,7 @@ const envDataSchema = z.object({ DEEZER_USER_AGENT: deezerInternalDataSchema.shape.userAgent }); -export const envSchemas: EnvSourceSchema = { +export const envSchemas: EnvSourceSchema = { env: envDataSchema, toConfig: (partial) => ({ data: { diff --git a/src/backend/common/infrastructure/config/source/sources.ts b/src/backend/common/infrastructure/config/source/sources.ts index 3350d3f2..43081a23 100644 --- a/src/backend/common/infrastructure/config/source/sources.ts +++ b/src/backend/common/infrastructure/config/source/sources.ts @@ -1,36 +1,36 @@ import * as z from "zod"; -import {azuracastSourceAIOConfigSchema, azuracastSourceConfigSchema, type AzuracastSourceAIOConfig, type AzuracastSourceConfig} from "./azuracast.ts"; -import {chromecastSourceAIOConfigSchema, chromecastSourceConfigSchema, type ChromecastSourceAIOConfig, type ChromecastSourceConfig} from "./chromecast.ts"; -import {listenbrainzEndpointSourceAIOConfigSchema, listenbrainzEndpointSourceConfigSchema, type ListenbrainzEndpointSourceAIOConfig, type ListenbrainzEndpointSourceConfig} from "./endpointlz.ts"; -import {lastFmEndpointSourceAIOConfigSchema, lastFmEndpointSourceConfigSchema, type LastFMEndpointSourceAIOConfig, type LastFMEndpointSourceConfig} from "./endpointlfm.ts"; -import {deezerInternalSourceConfigSchema, deezerInternalAIOConfigSchema, type DeezerInternalSourceConfig, type DeezerInternalAIOConfig} from "./deezer.ts"; -import {jellyApiSourceAIOConfigSchema, jellyApiSourceConfigSchema, type JellyApiSourceAIOConfig, type JellyApiSourceConfig} from "./jellyfin.ts"; -import {jRiverSourceAIOConfigSchema, jRiverSourceConfigSchema, type JRiverSourceAIOConfig, type JRiverSourceConfig} from "./jriver.ts"; -import {kodiSourceAIOConfigSchema, kodiSourceConfigSchema, type KodiSourceAIOConfig, type KodiSourceConfig} from "./kodi.ts"; -import {lastFmSouceAIOConfigSchema, lastfmSourceConfigSchema, type LastFmSouceAIOConfig, type LastfmSourceConfig} from "./lastfm.ts"; -import {listenBrainzSourceAIOConfigSchema, listenBrainzSourceConfigSchema, type ListenBrainzSourceAIOConfig, type ListenBrainzSourceConfig} from "./listenbrainz.ts"; -import {mopidySourceAIOConfigSchema, mopidySourceConfigSchema, type MopidySourceAIOConfig, type MopidySourceConfig} from "./mopidy.ts"; -import {mpdSourceAIOConfigSchema, mpdSourceConfigSchema, type MPDSourceAIOConfig, type MPDSourceConfig} from "./mpd.ts"; -import {mprisSourceAIOConfigSchema, mprisSourceConfigSchema, type MPRISSourceAIOConfig, type MPRISSourceConfig} from "./mpris.ts"; -import {musikcubeSourceAIOConfigSchema, musikcubeSourceConfigSchema, type MusikcubeSourceAIOConfig, type MusikcubeSourceConfig} from "./musikcube.ts"; -import {musicCastSourceConfigSchema, musicCastSourceAIOConfigSchema, type MusicCastSourceConfig, type MusicCastSourceAIOConfig} from "./musiccast.ts"; -import {plexApiSourceConfigSchema, plexApiSourceAIOConfigSchema, type PlexApiSourceConfig, type PlexApiSourceAIOConfig} from "./plex.ts"; -import {spotifySourceAIOConfigSchema, spotifySourceConfigSchema, type SpotifySourceAIOConfig, type SpotifySourceConfig} from "./spotify.ts"; -import {subsonicSourceAIOConfigSchema, subSonicSourceConfigSchema, type SubsonicSourceAIOConfig, type SubSonicSourceConfig} from "./subsonic.ts"; -import {vlcSourceAIOConfigSchema, vlcSourceConfigSchema, type VLCSourceAIOConfig, type VLCSourceConfig} from "./vlc.ts"; -import {webScrobblerSourceAIOConfigSchema, webScrobblerSourceConfigSchema, type WebScrobblerSourceAIOConfig, type WebScrobblerSourceConfig} from "./webscrobbler.ts"; -import {ytMusicSourceAIOConfigSchema, ytMusicSourceConfigSchema, type YTMusicSourceAIOConfig, type YTMusicSourceConfig} from "./ytmusic.ts"; -import {yandexMusicBridgeSourceAIOConfigSchema, yandexMusicBridgeSourceConfigSchema, type YandexMusicBridgeSourceAIOConfig, type YandexMusicBridgeSourceConfig} from "./ymbridge.ts"; -import {icecastSourceAIOConfigSchema, icecastSourceConfigSchema, type IcecastSourceAIOConfig, type IcecastSourceConfig} from "./icecast.ts"; -import {koitoSourceAIOConfigSchema, koitoSourceConfigSchema, type KoitoSourceAIOConfig, type KoitoSourceConfig} from "./koito.ts"; -import {malojaSourceAIOConfigSchema, malojaSourceConfigSchema, type MalojaSourceAIOConfig, type MalojaSourceConfig} from "./maloja.ts"; -import {tealSourceAIOConfigSchema, tealSourceConfigSchema, type TealSourceAIOConfig, type TealSourceConfig} from "./tealfm.ts"; -import {rockskySourceAIOConfigSchema, rockskySourceConfigSchema, type RockskySourceAIOConfig, type RockskySourceConfig} from "./rocksky.ts"; -import {librefmSouceAIOConfigSchema, librefmSourceConfigSchema, type LibrefmSouceAIOConfig, type LibrefmSourceConfig} from "./librefm.ts"; -import {sonosSourceAIOConfigSchema, sonosSourceConfigSchema, type SonosSourceAIOConfig, type SonosSourceConfig} from "./sonos.ts"; -import {appleMusicSourceAIOConfigSchema, appleMusicSourceConfigSchema, type AppleMusicSourceAIOConfig, type AppleMusicSourceConfig} from "./applemusic.ts"; +import {azuracastSourceAIOConfigSchema, azuracastSourceConfigSchema, envSchemas as azuracastEnvSchemas, type AzuracastSourceAIOConfig, type AzuracastSourceConfig} from "./azuracast.ts"; +import {chromecastSourceAIOConfigSchema, chromecastSourceConfigSchema, envSchemas as chromecastEnvSchemas, type ChromecastSourceAIOConfig, type ChromecastSourceConfig} from "./chromecast.ts"; +import {listenbrainzEndpointSourceAIOConfigSchema, listenbrainzEndpointSourceConfigSchema, envSchemas as endpointlzEnvSchemas, type ListenbrainzEndpointSourceAIOConfig, type ListenbrainzEndpointSourceConfig} from "./endpointlz.ts"; +import {lastFmEndpointSourceAIOConfigSchema, lastFmEndpointSourceConfigSchema, envSchemas as endpointlfmEnvSchemas, type LastFMEndpointSourceAIOConfig, type LastFMEndpointSourceConfig} from "./endpointlfm.ts"; +import {deezerInternalSourceConfigSchema, deezerInternalAIOConfigSchema, envSchemas as deezerEnvSchemas, type DeezerInternalSourceConfig, type DeezerInternalAIOConfig} from "./deezer.ts"; +import {jellyApiSourceAIOConfigSchema, jellyApiSourceConfigSchema, envSchemas as jellyfinEnvSchemas, type JellyApiSourceAIOConfig, type JellyApiSourceConfig} from "./jellyfin.ts"; +import {jRiverSourceAIOConfigSchema, jRiverSourceConfigSchema, envSchemas as jriverEnvSchemas, type JRiverSourceAIOConfig, type JRiverSourceConfig} from "./jriver.ts"; +import {kodiSourceAIOConfigSchema, kodiSourceConfigSchema, envSchemas as kodiEnvSchemas, type KodiSourceAIOConfig, type KodiSourceConfig} from "./kodi.ts"; +import {lastFmSouceAIOConfigSchema, lastfmSourceConfigSchema, envSchemas as lastfmEnvSchemas, type LastFmSouceAIOConfig, type LastfmSourceConfig} from "./lastfm.ts"; +import {listenBrainzSourceAIOConfigSchema, listenBrainzSourceConfigSchema, envSchemas as listenBrainzEnvSchemas, type ListenBrainzSourceAIOConfig, type ListenBrainzSourceConfig} from "./listenbrainz.ts"; +import {mopidySourceAIOConfigSchema, mopidySourceConfigSchema, envSchemas as mopidyEnvSchemas, type MopidySourceAIOConfig, type MopidySourceConfig} from "./mopidy.ts"; +import {mpdSourceAIOConfigSchema, mpdSourceConfigSchema, envSchemas as mpdEnvSchemas, type MPDSourceAIOConfig, type MPDSourceConfig} from "./mpd.ts"; +import {mprisSourceAIOConfigSchema, mprisSourceConfigSchema, envSchemas as mprisEnvSchemas, type MPRISSourceAIOConfig, type MPRISSourceConfig} from "./mpris.ts"; +import {musikcubeSourceAIOConfigSchema, musikcubeSourceConfigSchema, envSchemas as musikcubeEnvSchemas, type MusikcubeSourceAIOConfig, type MusikcubeSourceConfig} from "./musikcube.ts"; +import {musicCastSourceConfigSchema, musicCastSourceAIOConfigSchema, envSchemas as musicCastEnvSchemas, type MusicCastSourceConfig, type MusicCastSourceAIOConfig} from "./musiccast.ts"; +import {plexApiSourceConfigSchema, plexApiSourceAIOConfigSchema, envSchemas as plexEnvSchemas, type PlexApiSourceConfig, type PlexApiSourceAIOConfig} from "./plex.ts"; +import {spotifySourceAIOConfigSchema, spotifySourceConfigSchema, envSchemas as spotifyEnvSchemas, type SpotifySourceAIOConfig, type SpotifySourceConfig} from "./spotify.ts"; +import {subsonicSourceAIOConfigSchema, subSonicSourceConfigSchema, envSchemas as subsonicEnvSchemas, type SubsonicSourceAIOConfig, type SubSonicSourceConfig} from "./subsonic.ts"; +import {vlcSourceAIOConfigSchema, vlcSourceConfigSchema, envSchemas as vlcEnvSchemas, type VLCSourceAIOConfig, type VLCSourceConfig} from "./vlc.ts"; +import {webScrobblerSourceAIOConfigSchema, webScrobblerSourceConfigSchema, envSchemas as webScrobblerEnvSchemas, type WebScrobblerSourceAIOConfig, type WebScrobblerSourceConfig} from "./webscrobbler.ts"; +import {ytMusicSourceAIOConfigSchema, ytMusicSourceConfigSchema, envSchemas as ytMusicEnvSchemas, type YTMusicSourceAIOConfig, type YTMusicSourceConfig} from "./ytmusic.ts"; +import {yandexMusicBridgeSourceAIOConfigSchema, yandexMusicBridgeSourceConfigSchema, envSchemas as ymbridgeEnvSchemas, type YandexMusicBridgeSourceAIOConfig, type YandexMusicBridgeSourceConfig} from "./ymbridge.ts"; +import {icecastSourceAIOConfigSchema, icecastSourceConfigSchema, envSchemas as icecastEnvSchemas, type IcecastSourceAIOConfig, type IcecastSourceConfig} from "./icecast.ts"; +import {koitoSourceAIOConfigSchema, koitoSourceConfigSchema, envSchemas as koitoEnvSchemas, type KoitoSourceAIOConfig, type KoitoSourceConfig} from "./koito.ts"; +import {malojaSourceAIOConfigSchema, malojaSourceConfigSchema, envSchemas as malojaEnvSchemas, type MalojaSourceAIOConfig, type MalojaSourceConfig} from "./maloja.ts"; +import {tealSourceAIOConfigSchema, tealSourceConfigSchema, envSchemas as tealEnvSchemas, type TealSourceAIOConfig, type TealSourceConfig} from "./tealfm.ts"; +import {rockskySourceAIOConfigSchema, rockskySourceConfigSchema, envSchemas as rockskyEnvSchemas, type RockskySourceAIOConfig, type RockskySourceConfig} from "./rocksky.ts"; +import {librefmSouceAIOConfigSchema, librefmSourceConfigSchema, envSchemas as librefmEnvSchemas, type LibrefmSouceAIOConfig, type LibrefmSourceConfig} from "./librefm.ts"; +import {sonosSourceAIOConfigSchema, sonosSourceConfigSchema, envSchemas as sonosEnvSchemas, type SonosSourceAIOConfig, type SonosSourceConfig} from "./sonos.ts"; +import {appleMusicSourceAIOConfigSchema, appleMusicSourceConfigSchema, envSchemas as appleMusicEnvSchemas, type AppleMusicSourceAIOConfig, type AppleMusicSourceConfig} from "./applemusic.ts"; import type { SourceType } from "../../../../../core/Atomic.ts"; -import type { CommonSourceConfig } from "./index.ts"; +import type { CommonSourceConfig, EnvSourceSchema } from "./index.ts"; import { SimpleError } from "../../../errors/MSErrors.ts"; export const sourceConfigSchema = z.union([ @@ -103,70 +103,70 @@ export const sourceAIOConfigSchema = z.union([ export type SourceAIOConfig = z.infer; -export interface SourceTypeConfigMap extends Record { - spotify: [SpotifySourceConfig, SpotifySourceAIOConfig], - plex: [PlexApiSourceConfig, PlexApiSourceAIOConfig], - deezer: [DeezerInternalSourceConfig, DeezerInternalAIOConfig], - endpointlz: [ListenbrainzEndpointSourceConfig, ListenbrainzEndpointSourceAIOConfig], - endpointlfm: [LastFMEndpointSourceConfig, LastFMEndpointSourceAIOConfig], - icecast: [IcecastSourceConfig, IcecastSourceAIOConfig], - subsonic: [SubSonicSourceConfig, SubsonicSourceAIOConfig], - jellyfin: [JellyApiSourceConfig, JellyApiSourceAIOConfig], - lastfm: [LastfmSourceConfig, LastFmSouceAIOConfig], - librefm: [LibrefmSourceConfig, LibrefmSouceAIOConfig], - ytmusic: [YTMusicSourceConfig, YTMusicSourceAIOConfig], - ymbridge: [YandexMusicBridgeSourceConfig, YandexMusicBridgeSourceAIOConfig], - maloja: [MalojaSourceConfig, MalojaSourceAIOConfig], - mpris: [MPRISSourceConfig, MPRISSourceAIOConfig], - mopidy: [MopidySourceConfig, MopidySourceAIOConfig], - listenbrainz: [ListenBrainzSourceConfig, ListenBrainzSourceAIOConfig], - jriver: [JRiverSourceConfig, JRiverSourceAIOConfig], - kodi: [KodiSourceConfig, KodiSourceAIOConfig], - chromecast: [ChromecastSourceConfig, ChromecastSourceAIOConfig], - webscrobbler: [WebScrobblerSourceConfig, WebScrobblerSourceAIOConfig], - musikcube: [MusikcubeSourceConfig, MusikcubeSourceAIOConfig], - musiccast: [MusicCastSourceConfig, MusicCastSourceAIOConfig], - mpd: [MPDSourceConfig, MPDSourceAIOConfig], - vlc: [VLCSourceConfig, VLCSourceAIOConfig], - azuracast: [AzuracastSourceConfig, AzuracastSourceAIOConfig], - koito: [KoitoSourceConfig, KoitoSourceAIOConfig], - tealfm: [TealSourceConfig, TealSourceAIOConfig], - rocksky: [RockskySourceConfig, RockskySourceAIOConfig], - sonos: [SonosSourceConfig, SonosSourceAIOConfig], - applemusic: [AppleMusicSourceConfig, AppleMusicSourceAIOConfig] +export interface SourceTypeConfigMap extends Record>]> { + spotify: [SpotifySourceConfig, SpotifySourceAIOConfig, Partial>], + plex: [PlexApiSourceConfig, PlexApiSourceAIOConfig, Partial>], + deezer: [DeezerInternalSourceConfig, DeezerInternalAIOConfig, Partial>], + endpointlz: [ListenbrainzEndpointSourceConfig, ListenbrainzEndpointSourceAIOConfig, Partial>], + endpointlfm: [LastFMEndpointSourceConfig, LastFMEndpointSourceAIOConfig, Partial>], + icecast: [IcecastSourceConfig, IcecastSourceAIOConfig, Partial>], + subsonic: [SubSonicSourceConfig, SubsonicSourceAIOConfig, Partial>], + jellyfin: [JellyApiSourceConfig, JellyApiSourceAIOConfig, Partial>], + lastfm: [LastfmSourceConfig, LastFmSouceAIOConfig, Partial>], + librefm: [LibrefmSourceConfig, LibrefmSouceAIOConfig, Partial>], + ytmusic: [YTMusicSourceConfig, YTMusicSourceAIOConfig, Partial>], + ymbridge: [YandexMusicBridgeSourceConfig, YandexMusicBridgeSourceAIOConfig, Partial>], + maloja: [MalojaSourceConfig, MalojaSourceAIOConfig, Partial>], + mpris: [MPRISSourceConfig, MPRISSourceAIOConfig, Partial>], + mopidy: [MopidySourceConfig, MopidySourceAIOConfig, Partial>], + listenbrainz: [ListenBrainzSourceConfig, ListenBrainzSourceAIOConfig, Partial>], + jriver: [JRiverSourceConfig, JRiverSourceAIOConfig, Partial>], + kodi: [KodiSourceConfig, KodiSourceAIOConfig, Partial>], + chromecast: [ChromecastSourceConfig, ChromecastSourceAIOConfig, Partial>], + webscrobbler: [WebScrobblerSourceConfig, WebScrobblerSourceAIOConfig, Partial>], + musikcube: [MusikcubeSourceConfig, MusikcubeSourceAIOConfig, Partial>], + musiccast: [MusicCastSourceConfig, MusicCastSourceAIOConfig, Partial>], + mpd: [MPDSourceConfig, MPDSourceAIOConfig, Partial>], + vlc: [VLCSourceConfig, VLCSourceAIOConfig, Partial>], + azuracast: [AzuracastSourceConfig, AzuracastSourceAIOConfig, Partial>], + koito: [KoitoSourceConfig, KoitoSourceAIOConfig, Partial>], + tealfm: [TealSourceConfig, TealSourceAIOConfig, Partial>], + rocksky: [RockskySourceConfig, RockskySourceAIOConfig, Partial>], + sonos: [SonosSourceConfig, SonosSourceAIOConfig, Partial>], + applemusic: [AppleMusicSourceConfig, AppleMusicSourceAIOConfig, Partial>] } -export const sourceConfigSchemaMap: { [K in keyof SourceTypeConfigMap]: [z.ZodType, z.ZodType] } = { - spotify: [spotifySourceConfigSchema, spotifySourceAIOConfigSchema], - plex: [plexApiSourceConfigSchema, plexApiSourceAIOConfigSchema], - deezer: [deezerInternalSourceConfigSchema, deezerInternalAIOConfigSchema], - endpointlz: [listenbrainzEndpointSourceConfigSchema, listenbrainzEndpointSourceAIOConfigSchema], - endpointlfm: [lastFmEndpointSourceConfigSchema, lastFmEndpointSourceAIOConfigSchema], - icecast: [icecastSourceConfigSchema, icecastSourceAIOConfigSchema], - subsonic: [subSonicSourceConfigSchema, subsonicSourceAIOConfigSchema], - jellyfin: [jellyApiSourceConfigSchema, jellyApiSourceAIOConfigSchema], - lastfm: [lastfmSourceConfigSchema, lastFmSouceAIOConfigSchema], - librefm: [librefmSourceConfigSchema, librefmSouceAIOConfigSchema], - ytmusic: [ytMusicSourceConfigSchema, ytMusicSourceAIOConfigSchema], - ymbridge: [yandexMusicBridgeSourceConfigSchema, yandexMusicBridgeSourceAIOConfigSchema], - maloja: [malojaSourceConfigSchema, malojaSourceAIOConfigSchema], - mpris: [mprisSourceConfigSchema, mprisSourceAIOConfigSchema], - mopidy: [mopidySourceConfigSchema, mopidySourceAIOConfigSchema], - listenbrainz: [listenBrainzSourceConfigSchema, listenBrainzSourceAIOConfigSchema], - jriver: [jRiverSourceConfigSchema, jRiverSourceAIOConfigSchema], - kodi: [kodiSourceConfigSchema, kodiSourceAIOConfigSchema], - chromecast: [chromecastSourceConfigSchema, chromecastSourceAIOConfigSchema], - webscrobbler: [webScrobblerSourceConfigSchema, webScrobblerSourceAIOConfigSchema], - musikcube: [musikcubeSourceConfigSchema, musikcubeSourceAIOConfigSchema], - musiccast: [musicCastSourceConfigSchema, musicCastSourceAIOConfigSchema], - mpd: [mpdSourceConfigSchema, mpdSourceAIOConfigSchema], - vlc: [vlcSourceConfigSchema, vlcSourceAIOConfigSchema], - azuracast: [azuracastSourceConfigSchema, azuracastSourceAIOConfigSchema], - koito: [koitoSourceConfigSchema, koitoSourceAIOConfigSchema], - tealfm: [tealSourceConfigSchema, tealSourceAIOConfigSchema], - rocksky: [rockskySourceConfigSchema, rockskySourceAIOConfigSchema], - sonos: [sonosSourceConfigSchema, sonosSourceAIOConfigSchema], - applemusic: [appleMusicSourceConfigSchema, appleMusicSourceAIOConfigSchema] +export const sourceConfigSchemaMap: { [K in keyof SourceTypeConfigMap]: [z.ZodType, z.ZodType, EnvSourceSchema] } = { + spotify: [spotifySourceConfigSchema, spotifySourceAIOConfigSchema, spotifyEnvSchemas], + plex: [plexApiSourceConfigSchema, plexApiSourceAIOConfigSchema, plexEnvSchemas], + deezer: [deezerInternalSourceConfigSchema, deezerInternalAIOConfigSchema, deezerEnvSchemas], + endpointlz: [listenbrainzEndpointSourceConfigSchema, listenbrainzEndpointSourceAIOConfigSchema, endpointlzEnvSchemas], + endpointlfm: [lastFmEndpointSourceConfigSchema, lastFmEndpointSourceAIOConfigSchema, endpointlfmEnvSchemas], + icecast: [icecastSourceConfigSchema, icecastSourceAIOConfigSchema, icecastEnvSchemas], + subsonic: [subSonicSourceConfigSchema, subsonicSourceAIOConfigSchema, subsonicEnvSchemas], + jellyfin: [jellyApiSourceConfigSchema, jellyApiSourceAIOConfigSchema, jellyfinEnvSchemas], + lastfm: [lastfmSourceConfigSchema, lastFmSouceAIOConfigSchema, lastfmEnvSchemas], + librefm: [librefmSourceConfigSchema, librefmSouceAIOConfigSchema, librefmEnvSchemas], + ytmusic: [ytMusicSourceConfigSchema, ytMusicSourceAIOConfigSchema, ytMusicEnvSchemas], + ymbridge: [yandexMusicBridgeSourceConfigSchema, yandexMusicBridgeSourceAIOConfigSchema, ymbridgeEnvSchemas], + maloja: [malojaSourceConfigSchema, malojaSourceAIOConfigSchema, malojaEnvSchemas], + mpris: [mprisSourceConfigSchema, mprisSourceAIOConfigSchema, mprisEnvSchemas], + mopidy: [mopidySourceConfigSchema, mopidySourceAIOConfigSchema, mopidyEnvSchemas], + listenbrainz: [listenBrainzSourceConfigSchema, listenBrainzSourceAIOConfigSchema, listenBrainzEnvSchemas], + jriver: [jRiverSourceConfigSchema, jRiverSourceAIOConfigSchema, jriverEnvSchemas], + kodi: [kodiSourceConfigSchema, kodiSourceAIOConfigSchema, kodiEnvSchemas], + chromecast: [chromecastSourceConfigSchema, chromecastSourceAIOConfigSchema, chromecastEnvSchemas], + webscrobbler: [webScrobblerSourceConfigSchema, webScrobblerSourceAIOConfigSchema, webScrobblerEnvSchemas], + musikcube: [musikcubeSourceConfigSchema, musikcubeSourceAIOConfigSchema, musikcubeEnvSchemas], + musiccast: [musicCastSourceConfigSchema, musicCastSourceAIOConfigSchema, musicCastEnvSchemas], + mpd: [mpdSourceConfigSchema, mpdSourceAIOConfigSchema, mpdEnvSchemas], + vlc: [vlcSourceConfigSchema, vlcSourceAIOConfigSchema, vlcEnvSchemas], + azuracast: [azuracastSourceConfigSchema, azuracastSourceAIOConfigSchema, azuracastEnvSchemas], + koito: [koitoSourceConfigSchema, koitoSourceAIOConfigSchema, koitoEnvSchemas], + tealfm: [tealSourceConfigSchema, tealSourceAIOConfigSchema, tealEnvSchemas], + rocksky: [rockskySourceConfigSchema, rockskySourceAIOConfigSchema, rockskyEnvSchemas], + sonos: [sonosSourceConfigSchema, sonosSourceAIOConfigSchema, sonosEnvSchemas], + applemusic: [appleMusicSourceConfigSchema, appleMusicSourceAIOConfigSchema, appleMusicEnvSchemas] }; export const validateSourceJson = (sourceType: T, json: object): SourceTypeConfigMap[T][0] => { diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts index de9c10d4..61edfe1f 100644 --- a/src/backend/sources/ScrobbleSources.ts +++ b/src/backend/sources/ScrobbleSources.ts @@ -1,62 +1,25 @@ import { childLogger, type Logger } from '@foxxmd/logging'; import type EventEmitter from "events"; -import type {ConfigMeta, ConfigureAsSource, InternalConfig, InternalConfigOptional} from "../common/infrastructure/Atomic.ts"; -import { clientTypes, isSourceType } from "../../core/Atomic.ts"; +import type {InternalConfig, InternalConfigOptional} from "../common/infrastructure/Atomic.ts"; +import { isSourceType } from "../../core/Atomic.ts"; import { sourceTypes } from "../../core/Atomic.ts"; -import type {ClientType, SourceType} from "../../core/Atomic.ts"; +import type {SourceType} from "../../core/Atomic.ts"; import {aioSourceRelaxedConfigSchema, type AIOSourceRelaxedConfig, type SourceDefaults} from "../common/infrastructure/config/aioConfig.ts"; -import type {AzuracastData, AzuracastSourceConfig} from "../common/infrastructure/config/source/azuracast.ts"; -import type {ChromecastData, ChromecastSourceConfig} from "../common/infrastructure/config/source/chromecast.ts"; -import type {DeezerSourceConfig, DeezerInternalSourceConfig, DeezerCompatConfig} from "../common/infrastructure/config/source/deezer.ts"; -import type {ListenbrainzEndpointSourceConfig, ListenbrainzEndpointData} from "../common/infrastructure/config/source/endpointlz.ts"; -import type {LastFMEndpointSourceConfig, LastFMEndpointData} from "../common/infrastructure/config/source/endpointlfm.ts"; -import type {JellyApiData, JellyApiSourceConfig} from "../common/infrastructure/config/source/jellyfin.ts"; -import type {JRiverData, JRiverSourceConfig} from "../common/infrastructure/config/source/jriver.ts"; -import type {KodiData, KodiSourceConfig} from "../common/infrastructure/config/source/kodi.ts"; -import type {LastfmSourceConfig} from "../common/infrastructure/config/source/lastfm.ts"; -import type {ListenBrainzSourceConfig} from "../common/infrastructure/config/source/listenbrainz.ts"; -import type {MopidySourceConfig} from "../common/infrastructure/config/source/mopidy.ts"; -import type {MusicCastData, MusicCastSourceConfig} from "../common/infrastructure/config/source/musiccast.ts"; -import type {IcecastData, IcecastSourceConfig, IcecastSourceOptions} from "../common/infrastructure/config/source/icecast.ts"; -import type {MPDData, MPDSourceConfig} from "../common/infrastructure/config/source/mpd.ts"; -import type {MPRISData, MPRISSourceConfig} from "../common/infrastructure/config/source/mpris.ts"; -import type {MusikcubeData, MusikcubeSourceConfig} from "../common/infrastructure/config/source/musikcube.ts"; -import type {PlexApiData, PlexApiSourceConfig} from "../common/infrastructure/config/source/plex.ts"; -import type {MalojaSourceConfig} from "../common/infrastructure/config/source/maloja.ts"; -import {validateSourceAIOJson, validateSourceJson, type SourceAIOConfig, type SourceConfig} from "../common/infrastructure/config/source/sources.ts"; -import type {SpotifySourceConfig, SpotifySourceData} from "../common/infrastructure/config/source/spotify.ts"; -import type {SubsonicData, SubSonicSourceConfig} from "../common/infrastructure/config/source/subsonic.ts"; -import type {VLCData, VLCSourceConfig} from "../common/infrastructure/config/source/vlc.ts"; -import type {WebScrobblerData, WebScrobblerSourceConfig} from "../common/infrastructure/config/source/webscrobbler.ts"; -import type {YTMusicData, YTMusicSourceConfig} from "../common/infrastructure/config/source/ytmusic.ts"; -import type {YandexMusicBridgeData, YandexMusicBridgeSourceConfig} from "../common/infrastructure/config/source/ymbridge.ts"; -import type {SonosData, SonosSourceConfig} from "../common/infrastructure/config/source/sonos.ts"; -import type {AppleMusicSourceConfig} from "../common/infrastructure/config/source/applemusic.ts"; +import {sourceConfigSchemaMap, validateSourceAIOJson, validateSourceJson} from "../common/infrastructure/config/source/sources.ts"; import type { WildcardEmitter } from "../common/WildcardEmitter.ts"; -import { nonEmptyObj, parseBool, parseBoolStrict } from "../utils.ts"; -import { removeUndefinedKeys } from '../../core/DataUtils.ts'; -import { getCommonComponentEnvConfig, readJson } from '../utils/DataUtils.ts'; +import { pick } from '../../core/DataUtils.ts'; +import { readJson } from '../utils/DataUtils.ts'; import type AbstractSource from "./AbstractSource.ts"; import { nonEmptyStringOrDefault } from '../../core/StringUtils.ts'; -import type {KoitoSourceConfig} from '../common/infrastructure/config/source/koito.ts'; -import type {TealSourceConfig} from '../common/infrastructure/config/source/tealfm.ts'; -import type {RockskySourceConfig} from '../common/infrastructure/config/source/rocksky.ts'; -import type {CommonSourceOptions} from '../common/infrastructure/config/source/index.ts'; +import type {CommonSourceConfig, CommonSourceOptions} from '../common/infrastructure/config/source/index.ts'; import type {ExternalMetadataTerm, PlayTransformHooks} from '../../core/Transform.ts'; -import type {LibrefmSourceConfig} from '../common/infrastructure/config/source/librefm.ts'; -import type {LastfmData} from '../common/infrastructure/config/client/lastfm.ts'; -import type {MalojaData} from '../common/infrastructure/config/client/maloja.ts'; -import type {LibrefmData} from '../common/infrastructure/config/client/librefm.ts'; -import type {ListenBrainzData} from '../common/infrastructure/config/client/listenbrainz.ts'; -import type {KoitoData} from '../common/infrastructure/config/client/koito.ts'; -import type {TealData} from '../common/infrastructure/config/client/tealfm.ts'; -import type {RockSkyData} from '../common/infrastructure/config/client/rocksky.ts'; import { prettifyError, ZodError } from 'zod'; +import { commonComponentEnvConfigToConfigPrimitives, generateCommonComponentEnvConfigSchema, type CommonConfigPrimitives } from '../common/infrastructure/config/common.ts'; -type groupedNamedConfigs = {[key: string]: ParsedConfig[]}; +type UnparsedConfig = {config: object, type: SourceType, source?: 'file' | 'aio' | 'env', pos: string}; -type ParsedConfig = SourceAIOConfig & ConfigMeta; +type CommonParsedConfig = (CommonSourceConfig & {source: string}); export default class ScrobbleSources { @@ -150,1000 +113,585 @@ export default class ScrobbleSources { return buildDefaults; } - buildSourcesFromConfig = async (additionalConfigs: ParsedConfig[] = []) => { - const configs: ParsedConfig[] = additionalConfigs; + buildSourcesFromConfig = async () => { + const unparsedConfigs: UnparsedConfig[] = []; let configFile; try { - configFile = await readJson(`${this.internalConfig.configDir}/config.json`, {throwOnNotFound: false, logger: childLogger(this.logger, `Secrets`)}); + configFile = await readJson(`${this.internalConfig.configDir}/config.json`, { throwOnNotFound: false, logger: childLogger(this.logger, `Secrets`) }); } catch (e) { - throw new Error('config.json could not be parsed', {cause: e}); + // think this should stay as show-stopper since config could include important defaults (delay, retries) we don't want to ignore + throw new Error('config.json could not be parsed', { cause: e }); } - let sourceDefaults: SourceDefaults; + let sourceDefaults: SourceDefaults = {}; if (configFile !== undefined) { let aioConfig: AIOSourceRelaxedConfig; try { aioConfig = aioSourceRelaxedConfigSchema.parse(configFile); } catch (e) { const msg = `Validation error occurred while trying to parse 'config.json' for Source data/options`; - if(e instanceof ZodError) { + if (e instanceof ZodError) { this.logger.error(`${msg}:\n${prettifyError(e)}`); } else { - this.logger.error(new Error(msg, {cause: e})); + this.logger.error(new Error(msg, { cause: e })); } return; } const { - sources: mainConfigSourcesConfigs = [], - sourceDefaults: sd = {}, + sources: mainConfigs = [], + sourceDefaults: cd = {}, database: { retention } = {}, } = aioConfig; - sourceDefaults = this.buildSourceDefaults({retention, ...sd}); - for (const [index, c] of mainConfigSourcesConfigs.entries()) { - const {name = 'unnamed'} = c; - if(c.type === undefined) { + sourceDefaults = this.buildSourceDefaults({ retention, ...cd }); + for (const [index, c] of mainConfigs.entries()) { + const { name = 'unnamed' } = c; + if (c.type === undefined) { const invalidMsgType = `Source config ${index + 1} (${name}) in config.json does not have a "type" property! "type": "[sourceType]" must be one of ${sourceTypes.join(' | ')}`; - this.emitter.emit('error', new Error(invalidMsgType)); this.logger.error(invalidMsgType); continue; } - if(clientTypes.includes(c.type.toLocaleLowerCase() as ClientType) && (c.configureAs !== 'source')) - { - this.logger.debug(`Skipping config ${index + 1} (${name}) in config.json because it is configured as a client.`); - continue; - } - let validatedSourceConfig: SourceAIOConfig; - try { - validatedSourceConfig = validateSourceAIOJson(c.type.toLocaleLowerCase() as SourceType, c); - } catch (e) { - const msg = `Source config ${index + 1} (${c.type} - ${name}) in config.json is invalid and will not be used.`; - const err = new Error(msg, {cause: e}); - this.emitter.emit('error', err); - // pretty print error if its a zod error - if(e instanceof ZodError) { - this.logger.error(`${msg}:\n${prettifyError(e)}`); - } else { - this.logger.error(err); - } + if (isSourceType(c.type)) { + unparsedConfigs.push({ + config: c, + source: 'aio', + type: c.type, + pos: `${index + 1} (${name})` + }); + } else { + const invalidMsgType = `Source config ${index + 1} (${name}) in config.json has an invalid "type" property. Must be one of must be one of ${sourceTypes.join(' | ')}`; + this.logger.error(invalidMsgType); continue; } - configs.push({...validatedSourceConfig, - name: validatedSourceConfig.name ?? 'unnamed', - source: 'config.json', - configureAs: 'source' // override user value - }); } - } else { - sourceDefaults = this.buildSourceDefaults(); } - for (const sourceType of sourceTypes) { - const defaultConfigureAs: ConfigureAsSource = 'source'; - // env builder for single user mode - switch (sourceType) { - case 'spotify': { - const data: SpotifySourceData = removeUndefinedKeys({ - clientId: process.env.SPOTIFY_CLIENT_ID as string, - clientSecret: process.env.SPOTIFY_CLIENT_SECRET as string, - redirectUri: process.env.SPOTIFY_REDIRECT_URI, - }, false); - const p = getCommonComponentEnvConfig('SPOTIFY'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'spotify', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('SPOTIFY') - }) - } - } break; - case 'plex': { - const data: PlexApiData = removeUndefinedKeys({ - url: process.env.PLEX_URL, - token: process.env.PLEX_TOKEN, - usersAllow: process.env.PLEX_USERS_ALLOW, - usersBlock: process.env.PLEX_USERS_BLOCK, - devicesAllow: process.env.PLEX_DEVICES_ALLOW, - devicesBlock: process.env.PLEX_DEVICES_BLOCK, - librariesAllow: process.env.PLEX_LIBRARIES_ALLOW, - librariesBlock: process.env.PLEX_LIBRARIES_BLOCK - }, false); - const p = getCommonComponentEnvConfig('PLEX'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'plex', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('PLEX') - }) - } - } break; - case 'subsonic': { - const data: SubsonicData = removeUndefinedKeys({ - user: process.env.SUBSONIC_USER, - password: process.env.SUBSONIC_PASSWORD, - url: process.env.SUBSONIC_URL, - }, false); - const p = getCommonComponentEnvConfig('SUBSONIC'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'subsonic', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('SUBSONIC') - }) - } - } break; - case 'jellyfin': { - const data: JellyApiData = removeUndefinedKeys({ - user: process.env.JELLYFIN_USER, - password: process.env.JELLYFIN_PASSWORD, - apiKey: process.env.JELLYFIN_APIKEY, - url: process.env.JELLYFIN_URL, - usersAllow: process.env.JELLYFIN_USERS_ALLOW, - usersBlock: process.env.JELLYFIN_USERS_BLOCK, - devicesAllow: process.env.JELLYFIN_DEVICES_ALLOW, - devicesBlock: process.env.JELLYFIN_DEVICES_BLOCK, - librariesAllow: process.env.JELLYFIN_LIBRARIES_ALLOW, - librariesBlock: process.env.JELLYFIN_LIBRARIES_BLOCK, - frontendUrlOverride: process.env.JELLYFIN_FRONTEND_URL_OVERRIDE, - allowMediaTypes: process.env.JELLYFIN_MEDIATYPES_ALLOW - }, false); - const p = getCommonComponentEnvConfig('JELLYFIN'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'jellyfin', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('JELLYFIN') - }) - } - } break; - case 'lastfm': - { - const data: LastfmData = removeUndefinedKeys({ - apiKey: process.env.SOURCE_LASTFM_API_KEY, - secret: process.env.SOURCE_LASTFM_SECRET, - redirectUri: process.env.SOURCE_LASTFM_REDIRECT_URI, - session: process.env.SOURCE_LASTFM_SESSION, - }, false); - const p = getCommonComponentEnvConfig('SOURCE_LASTFM'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'lastfm', - name: 'unnamed-lfm-source', - source: 'ENV', - mode: 'single', - configureAs: 'source', - data: data, - ...p, - options: transformPresetEnv('SOURCE_LASTFM') - }) - } - } - break; - case 'deezer': { - const data = removeUndefinedKeys({ - clientId: process.env.DEEZER_CLIENT_ID, - clientSecret: process.env.DEEZER_CLIENT_SECRET, - redirectUri: process.env.DEEZER_REDIRECT_URI, - accessToken: process.env.DEEZER_ACCESS_TOKEN, - arl: process.env.DEEZER_ARL, - accountId: process.env.DEEZER_ACCOUNT_ID - }, false); - const p = getCommonComponentEnvConfig('DEEZER'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'deezer', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('DEEZER') - }); - } - } break; - case 'mpris': { - const data: MPRISData = removeUndefinedKeys({ - blacklist: process.env.MPRIS_BLACKLIST, - whitelist: process.env.MPRIS_WHITELIST - }, false); - const p = getCommonComponentEnvConfig('MPRIS'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'mpris', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('MPRIS') - }); - } - } break; - case 'maloja': { - const data = removeUndefinedKeys({ - url: process.env.SOURCE_MALOJA_URL, - apiKey: process.env.SOURCE_MALOJA_API_KEY - }, false); - const p = getCommonComponentEnvConfig('SOURCE_MALOJA'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'maloja', - name: 'unnamed-mlj-source', - source: 'ENV', - mode: 'single', - configureAs: 'source', - data: data, - ...p, - options: transformPresetEnv('SOURCE_MALOJA') - }) - } - } - break; - case 'librefm':{ - const data: LibrefmData = removeUndefinedKeys({ - apiKey: process.env.SOURCE_LIBREFM_API_KEY, - secret: process.env.SOURCE_LIBREFM_SECRET, - redirectUri: process.env.SOURCE_LIBREFM_REDIRECT_URI, - session: process.env.SOURCE_LIBREFM_SESSION, - urlBase: process.env.SOURCE_LIBREFM_URLBASE, - }, false); - const p = getCommonComponentEnvConfig('SOURCE_LIBREFM'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'librefm', - name: 'unnamed-librefm-source', - source: 'ENV', - mode: 'single', - configureAs: 'source', - data: data, - ...p, - options: transformPresetEnv('SOURCE_LIBREFM') - }) - } - } - break; - case 'listenbrainz': { - const data: ListenBrainzData = removeUndefinedKeys({ - url: process.env.SOURCE_LZ_URL, - token: process.env.SOURCE_LZ_TOKEN, - username: process.env.SOURCE_LZ_USER - }, false); - const p = getCommonComponentEnvConfig('SOURCE_LZ'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'listenbrainz', - name: 'unnamed-lz-source', - source: 'ENV', - mode: 'single', - configureAs: 'source', - data: data, - ...p, - options: transformPresetEnv('SOURCE_LZ') - }) - } - } - break; - case 'koito': { - const data: KoitoData = removeUndefinedKeys({ - url: process.env.SOURCE_KOITO_URL, - token: process.env.SOURCE_KOITO_TOKEN, - username: process.env.SOURCE_KOITO_USER - }, false); - const p = getCommonComponentEnvConfig('SOURCE_KOITO'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'koito', - name: 'unnamed-koito-source', - source: 'ENV', - mode: 'single', - configureAs: 'source', - data: data, - ...p, - options: transformPresetEnv('SOURCE_KOITO') - }) - } - } - break; - case 'tealfm': { - const data: TealData = removeUndefinedKeys({ - identifier: process.env.SOURCE_TEALFM_IDENTIFIER, - appPassword: process.env.SOURCE_TEALFM_APP_PW, - }, false); - const p = getCommonComponentEnvConfig('SOURCE_TEALFM'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'tealfm', - name: 'unnamed-tealfm-source', - source: 'ENV', - mode: 'single', - configureAs: 'source', - data: data, - ...p, - options: transformPresetEnv('SOURCE_TEALFM') - }) - } - } - break; - case 'rocksky': { - const data: RockSkyData = removeUndefinedKeys({ - key: process.env.SOURCE_ROCKSKY_KEY, - handle: process.env.SOURCE_ROCKSKY_HANDLE - }, false); - const p = getCommonComponentEnvConfig('SOURCE_ROCKSKY'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'rocksky', - name: 'unnamed-rocksky-source', - source: 'ENV', - mode: 'single', - configureAs: 'source', - data: data, - ...p, - options: transformPresetEnv('SOURCE_ROCKSKY') - }) - } - } - break; - case 'endpointlz': { - const data: ListenbrainzEndpointData = removeUndefinedKeys({ - slug: process.env.LZE_SLUG, - token: process.env.LZE_TOKEN, - username: process.env.LZE_USERNAME - }, false); - const p = getCommonComponentEnvConfig('LZE'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'endpointlz', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('LZE') - }); - } - } break; - case 'endpointlfm': { - const data: LastFMEndpointData = removeUndefinedKeys({ - slug: process.env.LFM_SLUG, - }, false); - const p = getCommonComponentEnvConfig('LFM'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'endpointlfm', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('LFM') - }); - } - } break; - case 'icecast': { - const scrobbleStart = parseBool(process.env.ICECAST_AUTO_MONITOR); - const data: IcecastData = removeUndefinedKeys({ - url: process.env.ICECAST_URL, - }, false); - const p = getCommonComponentEnvConfig('ICECAST'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'icecast', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('ICECAST', { - autoMonitor: scrobbleStart - }) - }); - } - } break; - case 'jriver': { - const data: JRiverData = removeUndefinedKeys({ - url: process.env.JRIVER_URL, - username: process.env.JRIVER_USER, - password: process.env.JRIVER_PASSWORD - }, false); - const p = getCommonComponentEnvConfig('JRIVER'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'jriver', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('JRIVER') - }); - } - } break; - case 'kodi': { - const data: KodiData = removeUndefinedKeys({ - url: process.env.KODI_URL, - username: process.env.KODI_USER, - password: process.env.KODI_PASSWORD - }, false); - const p = getCommonComponentEnvConfig('KODI'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'kodi', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('KODI') - }); - } - } break; - case 'webscrobbler': { - const data: WebScrobblerData = removeUndefinedKeys({ - blacklist: process.env.WS_BLACKLIST, - whitelist: process.env.WS_WHITELIST - }, false); - const p = getCommonComponentEnvConfig('WS'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'webscrobbler', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: { - blacklist: data.blacklist !== undefined ? (data.blacklist as string).split(',') : [], - whitelist: data.whitelist !== undefined ? (data.whitelist as string).split(',') : [], - }, - ...p, - options: transformPresetEnv('WS') - }); - } - } break; - case 'chromecast': { - const data: ChromecastData = removeUndefinedKeys({ - blacklistDevices: process.env.CC_BLACKLIST_DEVICES, - whitelistDevices: process.env.CC_WHITELIST_DEVICES, - blacklistApps: process.env.CC_BLACKLIST_APPS, - whitelistApps: process.env.CC_WHITELIST_APPS - }, false); - const p = getCommonComponentEnvConfig('CC'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'chromecast', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: { - blacklistDevices: data.blacklistDevices !== undefined ? (data.blacklistDevices as string).split(',') : [], - whitelistDevices: data.whitelistDevices !== undefined ? (data.whitelistDevices as string).split(',') : [], - blacklistApps: data.blacklistApps !== undefined ? (data.blacklistApps as string).split(',') : [], - whitelistApps: data.whitelistApps !== undefined ? (data.whitelistApps as string).split(',') : [], - }, - ...p, - options: transformPresetEnv('CC') - }); - } - } break; - case 'musiccast': { - const data: MusicCastData = removeUndefinedKeys({ - url: process.env.MCAST_URL, - }, false); - const p = getCommonComponentEnvConfig('MCAST'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'musiccast', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('MCAST') - }); - } - } break; - case 'musikcube': { - const data: MusikcubeData = removeUndefinedKeys({ - url: process.env.MC_URL, - password: process.env.MC_PASSWORD - }, false); - const p = getCommonComponentEnvConfig('MC'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'musikcube', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data as MusikcubeData, - ...p, - options: transformPresetEnv('MC') - }); - } - } break; - case 'mpd': { - const data: MPDData = removeUndefinedKeys({ - url: process.env.MPD_URL, - password: process.env.MPD_PASSWORD - }, false); - const p = getCommonComponentEnvConfig('MPD'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'mpd', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('MPD') - }); - } - } break; - case 'vlc': { - const data: VLCData = removeUndefinedKeys({ - url: process.env.VLC_URL, - password: process.env.VLC_PASSWORD - }, false); - const p = getCommonComponentEnvConfig('VLC'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'vlc', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('VLC') - }); - } - } break; - case 'ytmusic': { - const data: YTMusicData = removeUndefinedKeys({ - redirectUri: process.env.YTM_REDIRECT_URI, - clientId: process.env.YTM_CLIENT_ID, - clientSecret: process.env.YTM_CLIENT_SECRET, - cookie: process.env.YTM_COOKIE - }, false); - const p = getCommonComponentEnvConfig('YTM'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'ytmusic', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('YTM') - }); - } - } break; - case 'azuracast': { - const data: AzuracastData = removeUndefinedKeys({ - station: process.env.AZURA_STATION, - url: process.env.AZURA_URL, - apiKey: process.env.AZURA_KEY - }, false); - const listenerNum = process.env.AZURA_LISTENERS_NUM ?? ''; - if(listenerNum.trim() !== '') { - data.monitorWhenListeners = !isNaN(Number.parseInt(listenerNum)) ? Number.parseInt(listenerNum) : parseBool(listenerNum); - } - const live = process.env.AZURA_LIVE ?? ''; - if(live.trim() !== '') { - data.monitorWhenLive = parseBool(live); - } - const p = getCommonComponentEnvConfig('AZURA'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'azuracast', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('AZURA') - }); - } - } break; - case 'applemusic': { - const key = (() => { - const id = process.env.APPLEMUSIC_KEY_ID; - const teamId = process.env.APPLEMUSIC_TEAM_ID; - const p8 = process.env.APPLEMUSIC_KEY_P8; - if (id !== undefined && teamId !== undefined && p8 !== undefined) { - return { id, teamId, p8 }; - } - return undefined; - })(); - const data = removeUndefinedKeys({ - mediaUserToken: process.env.APPLEMUSIC_MEDIA_USER_TOKEN, - token: process.env.APPLEMUSIC_TOKEN, - key, - origin: nonEmptyStringOrDefault(process.env.APPLEMUSIC_ORIGIN_HEADER, undefined), - }, false); - const recoverEnv = nonEmptyStringOrDefault(process.env.APPLEMUSIC_RECOVER_UNCHANGED_TOP_HISTORY); - const recoverUnchangedTopHistory = recoverEnv !== undefined ? parseBoolStrict(recoverEnv) : undefined; - const albumNormalizeEnv = nonEmptyStringOrDefault(process.env.APPLEMUSIC_NORMALIZE_ALBUM); - const normalizeAlbumName = albumNormalizeEnv !== undefined ? parseBoolStrict(albumNormalizeEnv) : undefined; - const p = getCommonComponentEnvConfig('APPLEMUSIC'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'applemusic', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('APPLEMUSIC', { - recoverUnchangedTopHistory, - normalizeAlbumName - } as AppleMusicSourceConfig['options']) - }); - } - } break; - case 'sonos': { - const data: SonosData = removeUndefinedKeys({ - host: process.env.SONOS_HOST, - devicesAllow: process.env.SONOS_DEVICES_ALLOW, - devicesBlock: process.env.SONOS_DEVICES_BLOCK, - groupsAllow: process.env.SONOS_GROUPS_ALLOW, - groupsBlock: process.env.SONOS_GROUPS_BLOCK - }, false); - const p = getCommonComponentEnvConfig('SONOS'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'sonos', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('SONOS') - }); - } - } break; - case 'ymbridge': { - const data: YandexMusicBridgeData = removeUndefinedKeys({ - url: process.env.YMBRIDGE_URL, - apiKey: process.env.YMBRIDGE_API_KEY, - }, false); - const p = getCommonComponentEnvConfig('YMBRIDGE'); - if (nonEmptyObj(data) || nonEmptyObj(p)) { - configs.push({ - type: 'ymbridge', - name: 'unnamed', - source: 'ENV', - mode: 'single', - configureAs: defaultConfigureAs, - data: data, - ...p, - options: transformPresetEnv('YMBRIDGE') - }); - } - } break; - default: - break; - } - let rawSourceConfigs; + const envKeys = Object.keys(process.env).map(x => x.toUpperCase()); + + for (const configType of sourceTypes) { + + let sourceUnparsedConfigs = unparsedConfigs.filter(x => x.type === configType); + + const configTypeUpper = configType.toUpperCase(); + + let rawConfigs; try { - rawSourceConfigs = await readJson(`${this.internalConfig.configDir}/${sourceType}.json`, {throwOnNotFound: false, logger: childLogger(this.logger, `${sourceType} Secrets`)}); + rawConfigs = await readJson(`${this.internalConfig.configDir}/${configType}.json`, { throwOnNotFound: false, logger: childLogger(this.logger, `${configType} Secrets`) }); } catch (e) { - const errMsg = `${sourceType}.json config file could not be parsed`; + const errMsg = `${configType}.json config file could not be parsed`; this.emitter.emit('error', errMsg); this.logger.error(errMsg); - continue; } - if (rawSourceConfigs !== undefined) { - let sourceConfigs: ParsedConfig[]; - if (Array.isArray(rawSourceConfigs)) { - sourceConfigs = rawSourceConfigs; - } else if (rawSourceConfigs === null) { - this.logger.error(`${sourceType}.json contained no data`); - continue; - } else if (typeof rawSourceConfigs === 'object') { - sourceConfigs = [rawSourceConfigs]; + + if (rawConfigs !== undefined) { + this.logger.debug(`Found config file ${configType}.json`); + if (Array.isArray(rawConfigs)) { + sourceUnparsedConfigs = sourceUnparsedConfigs.concat(rawConfigs.map((x, i) => ({ config: x, type: configType, source: 'file', pos: `${i + 1}` }))); + } else if (rawConfigs === null) { + this.logger.warn(`${configType}.json contained no data`); + } else if (typeof rawConfigs === 'object') { + sourceUnparsedConfigs.push({ config: rawConfigs, type: configType, source: 'file', pos: `object` }) } else { - this.logger.error(`All top level data from ${sourceType}.json must be an object or an array of objects, will not parse configs from file`); - continue; - } - for (const [i,rawConf] of sourceConfigs.entries()) { - if(clientTypes.includes(sourceType as ClientType) && - (rawConf.configureAs !== 'source')) - { - this.logger.debug(`Skipping config ${i + 1} from ${sourceType}.json because it is configured as a client.`); - continue; - } - try { - await validateSourceJson(sourceType, rawConf); - // @ts-expect-error will eventually have all info (lazy) - const parsedConfig: ParsedConfig = { - ...rawConf, - source: `${sourceType}.json`, - type: sourceType - } - configs.push(parsedConfig); - } catch (e: any) { - const msg = `The config entry at index ${i} from ${sourceType}.json was not valid`; - const configErr = new Error(msg, {cause: e}); - this.emitter.emit('error', configErr); - if(e instanceof ZodError) { - this.logger.error(`${msg}:\n${prettifyError(e)}`); - } else { - this.logger.error(configErr); - } - } + this.logger.error(`All top level data from ${configType}.json must be an object or an array of objects, will not parse configs from file`); } } - } - // finally! all configs are valid, structurally, and can now be passed to addClient - // do a last check that names (within each type) are unique and warn if not, but add anyways - const typeGroupedConfigs = configs.reduce((acc: groupedNamedConfigs, curr: ParsedConfig) => { - const {type} = curr; - const {[type]: t = []} = acc; - return {...acc, [type]: [...t, curr]}; - }, {}); - // only need to warn if dup names PER TYPE - for (const [type, typedConfigs] of Object.entries(typeGroupedConfigs)) { - const nameGroupedConfigs = typedConfigs.reduce((acc: any, curr: any) => { - const {name = 'unnamed'} = curr; - const {[name]: n = []} = acc; - return {...acc, [name]: [...n, curr]}; - }, {}); - for (const [name, namedConfigs] of Object.entries(nameGroupedConfigs)) { - let tempNamedConfigs = namedConfigs; - // @ts-expect-error TS(2571): Object is of type 'unknown'. - const hasDups = namedConfigs.length > 1; - if (hasDups) { - // @ts-expect-error TS(2571): Object is of type 'unknown'. - const sources = namedConfigs.map((c: any) => `Config object from ${c.source} of type [${c.type}]`); - this.logger.warn(`Source configs have naming conflicts -- the following configs have the same name "${name}":\n\n${sources.join('\n')}\n`); - if (name === 'unnamed') { - this.logger.info('HINT: "unnamed" configs occur when using ENVs, if a multi-user mode config does not have a "name" property, or if a config is built in single-user mode'); + const configKeys = envKeys.filter(x => x.includes(configTypeUpper)); + if (configKeys.length > 0) { + sourceUnparsedConfigs.push({ + config: pick(process.env, ...configKeys), + type: configType, + source: 'env', + pos: '' + }) + } + + const strongConfigs: CommonParsedConfig[] = []; + for (const entry of sourceUnparsedConfigs) { + let parsedConfig: CommonParsedConfig; + try { + const sourceStr = `${entry.source} ${entry.pos}`; + switch (entry.source) { + case 'env': { + const primitiveSchema = generateCommonComponentEnvConfigSchema(configTypeUpper); + const parsed = primitiveSchema.parse(entry.config); + const primitives: CommonConfigPrimitives = commonComponentEnvConfigToConfigPrimitives(configTypeUpper, parsed); + const envSchema = sourceConfigSchemaMap[configType][2]; + const parsedEnvConfigValues = envSchema.env.parse(entry.config); + const { data = {}, options = {} } = envSchema.toConfig(parsedEnvConfigValues); + const transformOptions = transformPresetEnv(configTypeUpper); + parsedConfig = { + name: `${configType} - ${entry.source}${entry.pos !== '' ? ` - ${entry.pos}` : ''} `, + ...primitives, + data, + source: sourceStr, + options: { + ...options, + ...(transformOptions ?? {}) + } + }; + } break; + case 'file': + case 'aio': { + if ('configureAs' in entry.config && entry.config.configureAs === 'client') { + this.logger.debug(`Skipping ${configType} Config ${entry.source} ${entry.pos} because it is configured as a Source`); + continue; + } + const parsed = entry.source === 'file' ? validateSourceJson(entry.type, entry.config) : validateSourceAIOJson(entry.type, entry.config); + parsedConfig = { + ...parsed, + source: sourceStr + } + } break; } - } - // @ts-expect-error TS(2571): Object is of type 'unknown'. - tempNamedConfigs = tempNamedConfigs.map(({name = 'unnamed', ...x}, i) => ({ - ...x, - name: hasDups ? `${name}${i + 1}` : name - })); - // @ts-expect-error TS(2571): Object is of type 'unknown'. - for (const c of tempNamedConfigs) { - try { - await this.addSource(c, sourceDefaults); - } catch(e) { - const addError = new Error(`Source ${c.name} of type ${c.type} from source ${c.source} was not added because of unrecoverable errors`, {cause: e}); - this.emitter.emit('error', addError); - this.logger.error(addError); + } catch (e) { + const msg = `Failed to validate ${configType} Config ${entry.source} ${entry.pos}`; + if (e instanceof ZodError) { + this.logger.error(`${msg}:\n${prettifyError(e)}`); + } else { + this.logger.error(new Error(msg, { cause: e })); } + continue; } - } - } - } - - addSource = async (clientConfig: ParsedConfig, defaults: SourceDefaults = {}) => { - // const isValidConfig = isValidConfigStructure(clientConfig, {name: true, data: true, type: true}); - // if (isValidConfig !== true) { - // throw new Error(`Config object from ${clientConfig.source || 'unknown'} with name [${clientConfig.name || 'unnamed'}] of type [${clientConfig.type || 'unknown'}] has errors: ${isValidConfig.join(' | ')}`) - // } - const {type, name, data: d = {}, enable = true, source, options: clientOptions = {}} = clientConfig; + if (parsedConfig.enable === false) { + this.logger.debug(`Not using Config ${parsedConfig.id} (${parsedConfig.name}) because it was marked as not enabled.`); + } else { + strongConfigs.push(parsedConfig); + } + } - if(enable === false) { - this.logger.warn({labels: [`${type} - ${name}`]},`Source from ${source} was disabled by config`); - return; + await this.addSource(configType, strongConfigs, sourceDefaults); } - - // add defaults - // @ts-expect-error idk why this has so many issues - const compositeConfig: SourceConfig = {...clientConfig, data: d, options: {...defaults, ...clientOptions}}; + } - this.logger.debug({labels: [`${type} - ${name}`]},`Constructing Source from ${source}...`); - let newSource: AbstractSource; - switch (type) { + addSource = async (sourceType: SourceType, strongConfigs: CommonParsedConfig[], defaults: SourceDefaults = {}) => { + switch (sourceType) { case 'spotify': { const SpotifySource = (await import('./SpotifySource.ts')).default; - newSource = new SpotifySource(name, compositeConfig as SpotifySourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new SpotifySource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'plex': { const PlexApiSource = (await import('./PlexApiSource.ts')).default; - newSource = await new PlexApiSource(name, compositeConfig as PlexApiSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new PlexApiSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'subsonic': { const {SubsonicSource} = (await import('./SubsonicSource.ts')); - newSource = new SubsonicSource(name, compositeConfig as SubSonicSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new SubsonicSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'jellyfin': { const JellyfinApiSource = (await import('./JellyfinApiSource.ts')).default; - newSource = await new JellyfinApiSource(name, compositeConfig as JellyApiSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new JellyfinApiSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'lastfm': { const LastfmSource = (await import('./LastfmSource.ts')).default; - newSource = await new LastfmSource(name, compositeConfig as LastfmSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new LastfmSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'librefm': { const LibrefmSource = (await import('./LibrefmSource.ts')).default; - newSource = await new LibrefmSource(name, compositeConfig as LibrefmSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new LibrefmSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'deezer': { - const deezerConfig = compositeConfig as DeezerCompatConfig; - if('arl' in deezerConfig.data && deezerConfig.data.arl !== undefined) { - const DeezerInternalSource = (await import('./DeezerInternalSource.ts')).default; - newSource = await new DeezerInternalSource(name, compositeConfig as DeezerInternalSourceConfig, this.internalConfig, this.emitter); - } else { - const DeezerSource = (await import('./DeezerSource.ts')).default; - newSource = await new DeezerSource(name, compositeConfig as DeezerSourceConfig, this.internalConfig, this.emitter); + const DeezerInternalSource = (await import('./DeezerInternalSource.ts')).default; + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new DeezerInternalSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } } - break; - } + } break; case 'ytmusic': { const YTMusicSource = (await import('./YTMusicSource.ts')).default; - newSource = await new YTMusicSource(name, compositeConfig as YTMusicSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new YTMusicSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'ymbridge': { const YandexMusicBridgeSource = (await import('./YandexMusicBridgeSource.ts')).default; - newSource = await new YandexMusicBridgeSource(name, compositeConfig as YandexMusicBridgeSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new YandexMusicBridgeSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'mpris': { const {MPRISSource} = (await import('./MPRISSource.ts')); - newSource = await new MPRISSource(name, compositeConfig as MPRISSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new MPRISSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'mopidy': { const {MopidySource} = (await import('./MopidySource.ts')); - newSource = await new MopidySource(name, compositeConfig as MopidySourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new MopidySource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'listenbrainz': { const ListenbrainzSource = (await import('./ListenbrainzSource.ts')).default; - newSource = await new ListenbrainzSource(name, compositeConfig as ListenBrainzSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new ListenbrainzSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'endpointlz': { const {EndpointListenbrainzSource} = (await import('./EndpointListenbrainzSource.ts')); - newSource = await new EndpointListenbrainzSource(name, compositeConfig as ListenbrainzEndpointSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new EndpointListenbrainzSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'endpointlfm': { const {EndpointLastfmSource} = (await import('./EndpointLastfmSource.ts')); - newSource = await new EndpointLastfmSource(name, compositeConfig as LastFMEndpointSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new EndpointLastfmSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'icecast': { const {IcecastSource} = (await import('./IcecastSource.ts')); - newSource = await new IcecastSource(name, compositeConfig as IcecastSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new IcecastSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'jriver': { const {JRiverSource} = (await import('./JRiverSource.ts')); - newSource = await new JRiverSource(name, compositeConfig as JRiverSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new JRiverSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'kodi': { const {KodiSource} = (await import('./KodiSource.ts')); - newSource = await new KodiSource(name, compositeConfig as KodiSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new KodiSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'webscrobbler': { const {WebScrobblerSource} = (await import('./WebScrobblerSource.ts')); - newSource = await new WebScrobblerSource(name, compositeConfig as WebScrobblerSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new WebScrobblerSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'chromecast': { const {ChromecastSource} = (await import('./ChromecastSource.ts')); - newSource = await new ChromecastSource(name, compositeConfig as ChromecastSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new ChromecastSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'musikcube': { const {MusikcubeSource} = (await import('./MusikcubeSource.ts')); - newSource = await new MusikcubeSource(name, compositeConfig as MusikcubeSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new MusikcubeSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'musiccast': { const {MusicCastSource} = (await import('./MusicCastSource.ts')); - newSource = await new MusicCastSource(name, compositeConfig as MusicCastSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new MusicCastSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'mpd': { const {MPDSource} = (await import('./MPDSource.ts')); - newSource = await new MPDSource(name, compositeConfig as MPDSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new MPDSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'vlc': { const {VLCSource} = (await import('./VLCSource.ts')); - newSource = await new VLCSource(name, compositeConfig as VLCSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new VLCSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'azuracast': { const {AzuracastSource} = (await import('./AzuracastSource.ts')); - newSource = await new AzuracastSource(name, compositeConfig as AzuracastSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new AzuracastSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'koito': { const KoitoSource = (await import('./KoitoSource.ts')).default; - newSource = await new KoitoSource(name, compositeConfig as KoitoSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new KoitoSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'maloja': { const MalojaSource = (await import('./MalojaSource.ts')).default; - newSource = await new MalojaSource(name, compositeConfig as MalojaSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new MalojaSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'tealfm': { const TealfmSource = (await import('./TealfmSource.ts')).default; - newSource = await new TealfmSource(name, compositeConfig as TealSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new TealfmSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'rocksky': { const RockskySource = (await import('./RockskySource.ts')).default; - newSource = await new RockskySource(name, compositeConfig as RockskySourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new RockskySource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'sonos': { const {SonosSource} = (await import('./SonosSource.ts')); - newSource = await new SonosSource(name, compositeConfig as SonosSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new SonosSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; case 'applemusic': { const AppleMusicSource = (await import('./AppleMusicSource.ts')).default; - newSource = await new AppleMusicSource(name, compositeConfig as AppleMusicSourceConfig, this.internalConfig, this.emitter); - break; - } + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new AppleMusicSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } break; default: break; } - - if(newSource === undefined) { - // really shouldn't get here! - this.logger.error(new Error(`Source of type ${type} from ${source} was not recognized??`)); - return; - } - this.sources.push(newSource); - newSource.logger.info(`Source Added from ${source}`); } } -- 2.51.2 From 78aff32334ae4413a30627939b566bdafea85674 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 15:33:34 -0400 Subject: [PATCH 11/35] refactor: Extract source/client building into a function to reduce verbosity --- src/backend/scrobblers/ScrobbleClients.ts | 130 +++----- src/backend/sources/ScrobbleSources.ts | 381 +++------------------- 2 files changed, 88 insertions(+), 423 deletions(-) diff --git a/src/backend/scrobblers/ScrobbleClients.ts b/src/backend/scrobblers/ScrobbleClients.ts index 86c749ab..3c80a7fe 100644 --- a/src/backend/scrobblers/ScrobbleClients.ts +++ b/src/backend/scrobblers/ScrobbleClients.ts @@ -7,7 +7,7 @@ import { isClientType } from '../../core/Atomic.ts'; import { clientTypes } from "../../core/Atomic.ts"; import type {ClientType} from "../../core/Atomic.ts"; import {aioClientRelaxedConfigSchema, type AIOClientRelaxedConfig, type ClientDefaults} from "../common/infrastructure/config/aioConfig.ts"; -import {clientConfigSchemaMap, validateClientAIOJson, validateClientJson} from "../common/infrastructure/config/client/clients.ts"; +import {clientConfigSchemaMap, validateClientAIOJson, validateClientJson, type ClientTypeConfigMap} from "../common/infrastructure/config/client/clients.ts"; import type { WildcardEmitter } from "../common/WildcardEmitter.ts"; import { pick } from '../../core/DataUtils.ts'; import { readJson } from '../utils/DataUtils.ts'; @@ -250,122 +250,68 @@ export default class ScrobbleClients { } } + private instantiateClients = async ( + clientType: T, + strongConfigs: CommonParsedConfig[], + clientDefaults: ClientDefaults, + Ctor: new (...args: any[]) => AbstractScrobbleClient, + buildArgs: (config: ClientTypeConfigMap[T][0], compositeOptions: Record) => ConstructorParameters, + ) => { + for (const s of strongConfigs) { + try { + const config = clientConfigSchemaMap[clientType][0].parse(s); + const compositeOptions = { ...clientDefaults, ...config.options }; + const newClient = new Ctor(...buildArgs(config, compositeOptions)); + newClient.logger.info(`Client added from ${s.source}`); + this.clients.push(newClient); + } catch (e) { + this.logger.error(new Error(`Client from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } + addClient = async (clientType: ClientType, strongConfigs: CommonParsedConfig[], clientDefaults: ClientDefaults = {}) => { switch (clientType) { case 'discord': { const DiscordScrobbler = (await import('./DiscordScrobbler.ts')).default; - for (const s of strongConfigs) { - try { - const config = clientConfigSchemaMap[clientType][0].parse(s); - const compositeOptions = { ...clientDefaults, ...config.options }; - const newClient = new DiscordScrobbler(config.name, { ...config, options: compositeOptions }, {}, this.emitter, this.logger); - newClient.logger.info(`Client added from ${s.source}`); - this.clients.push(newClient); - } catch (e) { - this.logger.error(new Error(`Client from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateClients('discord', strongConfigs, clientDefaults, DiscordScrobbler, + (config, options) => [config.name, { ...config, options }, {}, this.emitter, this.logger]); } break; case 'koito': { const KoitoScrobbler = (await import('./KoitoScrobbler.ts')).default; - for (const s of strongConfigs) { - try { - const config = clientConfigSchemaMap[clientType][0].parse(s); - const compositeOptions = { ...clientDefaults, ...config.options, configDir: this.internalConfig.configDir }; - - const newClient = new KoitoScrobbler(config.name, { ...config, options: compositeOptions }, {}, this.emitter, this.logger); - newClient.logger.info(`Client added from ${s.source}`); - this.clients.push(newClient); - } catch (e) { - this.logger.error(new Error(`Client from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateClients('koito', strongConfigs, clientDefaults, KoitoScrobbler, + (config, options) => [config.name, { ...config, options: { ...options, configDir: this.internalConfig.configDir } }, {}, this.emitter, this.logger]); } break; case 'lastfm': { const LastfmScrobbler = (await import('./LastfmScrobbler.ts')).default; - for (const s of strongConfigs) { - try { - const config = clientConfigSchemaMap[clientType][0].parse(s); - const compositeOptions = { ...clientDefaults, ...config.options }; - - const newClient = new LastfmScrobbler(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter, this.logger); - newClient.logger.info(`Client added from ${s.source}`); - this.clients.push(newClient); - } catch (e) { - this.logger.error(new Error(`Client from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateClients('lastfm', strongConfigs, clientDefaults, LastfmScrobbler, + (config, options) => [config.name, { ...config, options }, this.internalConfig, this.emitter, this.logger]); } break; case 'librefm': { const LibrefmScrobbler = (await import('./LibrefmScrobbler.ts')).default; - for (const s of strongConfigs) { - try { - const config = clientConfigSchemaMap[clientType][0].parse(s); - const compositeOptions = { ...clientDefaults, ...config.options }; - const newClient = new LibrefmScrobbler(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter, this.logger); - newClient.logger.info(`Client added from ${s.source}`); - this.clients.push(newClient); - } catch (e) { - this.logger.error(new Error(`Client from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateClients('librefm', strongConfigs, clientDefaults, LibrefmScrobbler, + (config, options) => [config.name, { ...config, options }, this.internalConfig, this.emitter, this.logger]); } break; case 'listenbrainz': { const ListenbrainzScrobbler = (await import('./ListenbrainzScrobbler.ts')).default; - for (const s of strongConfigs) { - try { - const config = clientConfigSchemaMap[clientType][0].parse(s); - const compositeOptions = { ...clientDefaults, ...config.options }; - const newClient = new ListenbrainzScrobbler(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter, this.logger); - newClient.logger.info(`Client added from ${s.source}`); - this.clients.push(newClient); - } catch (e) { - this.logger.error(new Error(`Client from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateClients('listenbrainz', strongConfigs, clientDefaults, ListenbrainzScrobbler, + (config, options) => [config.name, { ...config, options }, this.internalConfig, this.emitter, this.logger]); } break; case 'maloja': { const MalojaScrobbler = (await import('./MalojaScrobbler.ts')).default; - for (const s of strongConfigs) { - try { - const config = clientConfigSchemaMap[clientType][0].parse(s); - const compositeOptions = { ...clientDefaults, ...config.options }; - const newClient = new MalojaScrobbler(config.name, { ...config, options: compositeOptions }, this.emitter, this.logger); - newClient.logger.info(`Client added from ${s.source}`); - this.clients.push(newClient); - } catch (e) { - this.logger.error(new Error(`Client from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateClients('maloja', strongConfigs, clientDefaults, MalojaScrobbler, + (config, options) => [config.name, { ...config, options }, this.emitter, this.logger]); } break; case 'rocksky': { const RockskyScrobbler = (await import('./RockskyScrobbler.ts')).default; - for (const s of strongConfigs) { - try { - const config = clientConfigSchemaMap[clientType][0].parse(s); - const compositeOptions = { ...clientDefaults, ...config.options }; - const newClient = new RockskyScrobbler(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter, this.logger); - newClient.logger.info(`Client added from ${s.source}`); - this.clients.push(newClient); - } catch (e) { - this.logger.error(new Error(`Client from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateClients('rocksky', strongConfigs, clientDefaults, RockskyScrobbler, + (config, options) => [config.name, { ...config, options }, this.internalConfig, this.emitter, this.logger]); } break; case 'tealfm': { const TealScrobbler = (await import('./TealfmScrobbler.ts')).default; - for (const s of strongConfigs) { - try { - const config = clientConfigSchemaMap[clientType][0].parse(s); - const compositeOptions = { ...clientDefaults, ...config.options }; - const newClient = new TealScrobbler(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter, this.logger); - newClient.logger.info(`Client added from ${s.source}`); - this.clients.push(newClient); - } catch (e) { - this.logger.error(new Error(`Client from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } - } + await this.instantiateClients('tealfm', strongConfigs, clientDefaults, TealScrobbler, + (config, options) => [config.name, { ...config, options }, this.internalConfig, this.emitter, this.logger]); + } break; } } diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts index 61edfe1f..d5ebf85f 100644 --- a/src/backend/sources/ScrobbleSources.ts +++ b/src/backend/sources/ScrobbleSources.ts @@ -6,7 +6,7 @@ import { isSourceType } from "../../core/Atomic.ts"; import { sourceTypes } from "../../core/Atomic.ts"; import type {SourceType} from "../../core/Atomic.ts"; import {aioSourceRelaxedConfigSchema, type AIOSourceRelaxedConfig, type SourceDefaults} from "../common/infrastructure/config/aioConfig.ts"; -import {sourceConfigSchemaMap, validateSourceAIOJson, validateSourceJson} from "../common/infrastructure/config/source/sources.ts"; +import {sourceConfigSchemaMap, validateSourceAIOJson, validateSourceJson, type SourceTypeConfigMap} from "../common/infrastructure/config/source/sources.ts"; import type { WildcardEmitter } from "../common/WildcardEmitter.ts"; import { pick } from '../../core/DataUtils.ts'; import { readJson } from '../utils/DataUtils.ts'; @@ -267,427 +267,146 @@ export default class ScrobbleSources { } } + private instantiateSources = async ( + sourceType: T, + strongConfigs: CommonParsedConfig[], + defaults: SourceDefaults, + Ctor: new (name: string, config: SourceTypeConfigMap[T][0], internalConfig: InternalConfig, emitter: WildcardEmitter) => AbstractSource, + ) => { + for (const s of strongConfigs) { + try { + const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const compositeOptions = { ...defaults, ...config.options }; + const newComponent = new Ctor(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + newComponent.logger.info(`Source added from ${s.source}`); + this.sources.push(newComponent); + } catch (e) { + this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + } + } + } + addSource = async (sourceType: SourceType, strongConfigs: CommonParsedConfig[], defaults: SourceDefaults = {}) => { switch (sourceType) { case 'spotify': { const SpotifySource = (await import('./SpotifySource.ts')).default; - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new SpotifySource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('spotify', strongConfigs, defaults, SpotifySource); } break; case 'plex': { const PlexApiSource = (await import('./PlexApiSource.ts')).default; - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new PlexApiSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('plex', strongConfigs, defaults, PlexApiSource); } break; case 'subsonic': { const {SubsonicSource} = (await import('./SubsonicSource.ts')); - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new SubsonicSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('subsonic', strongConfigs, defaults, SubsonicSource); } break; case 'jellyfin': { const JellyfinApiSource = (await import('./JellyfinApiSource.ts')).default; - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new JellyfinApiSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('jellyfin', strongConfigs, defaults, JellyfinApiSource); } break; case 'lastfm': { const LastfmSource = (await import('./LastfmSource.ts')).default; - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new LastfmSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('lastfm', strongConfigs, defaults, LastfmSource); } break; case 'librefm': { const LibrefmSource = (await import('./LibrefmSource.ts')).default; - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new LibrefmSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('librefm', strongConfigs, defaults, LibrefmSource); } break; case 'deezer': { const DeezerInternalSource = (await import('./DeezerInternalSource.ts')).default; - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new DeezerInternalSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('deezer', strongConfigs, defaults, DeezerInternalSource); } break; case 'ytmusic': { const YTMusicSource = (await import('./YTMusicSource.ts')).default; - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new YTMusicSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('ytmusic', strongConfigs, defaults, YTMusicSource); } break; case 'ymbridge': { const YandexMusicBridgeSource = (await import('./YandexMusicBridgeSource.ts')).default; - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new YandexMusicBridgeSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('ymbridge', strongConfigs, defaults, YandexMusicBridgeSource); } break; case 'mpris': { const {MPRISSource} = (await import('./MPRISSource.ts')); - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new MPRISSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('mpris', strongConfigs, defaults, MPRISSource); } break; case 'mopidy': { const {MopidySource} = (await import('./MopidySource.ts')); - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new MopidySource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('mopidy', strongConfigs, defaults, MopidySource); } break; case 'listenbrainz': { const ListenbrainzSource = (await import('./ListenbrainzSource.ts')).default; - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new ListenbrainzSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('listenbrainz', strongConfigs, defaults, ListenbrainzSource); } break; case 'endpointlz': { const {EndpointListenbrainzSource} = (await import('./EndpointListenbrainzSource.ts')); - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new EndpointListenbrainzSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('endpointlz', strongConfigs, defaults, EndpointListenbrainzSource); } break; case 'endpointlfm': { const {EndpointLastfmSource} = (await import('./EndpointLastfmSource.ts')); - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new EndpointLastfmSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('endpointlfm', strongConfigs, defaults, EndpointLastfmSource); } break; case 'icecast': { const {IcecastSource} = (await import('./IcecastSource.ts')); - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new IcecastSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('icecast', strongConfigs, defaults, IcecastSource); } break; case 'jriver': { const {JRiverSource} = (await import('./JRiverSource.ts')); - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new JRiverSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('jriver', strongConfigs, defaults, JRiverSource); } break; case 'kodi': { const {KodiSource} = (await import('./KodiSource.ts')); - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new KodiSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('kodi', strongConfigs, defaults, KodiSource); } break; case 'webscrobbler': { const {WebScrobblerSource} = (await import('./WebScrobblerSource.ts')); - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new WebScrobblerSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('webscrobbler', strongConfigs, defaults, WebScrobblerSource); } break; case 'chromecast': { const {ChromecastSource} = (await import('./ChromecastSource.ts')); - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new ChromecastSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('chromecast', strongConfigs, defaults, ChromecastSource); } break; case 'musikcube': { const {MusikcubeSource} = (await import('./MusikcubeSource.ts')); - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new MusikcubeSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('musikcube', strongConfigs, defaults, MusikcubeSource); } break; case 'musiccast': { const {MusicCastSource} = (await import('./MusicCastSource.ts')); - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new MusicCastSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('musiccast', strongConfigs, defaults, MusicCastSource); } break; case 'mpd': { const {MPDSource} = (await import('./MPDSource.ts')); - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new MPDSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('mpd', strongConfigs, defaults, MPDSource); } break; case 'vlc': { const {VLCSource} = (await import('./VLCSource.ts')); - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new VLCSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('vlc', strongConfigs, defaults, VLCSource); } break; case 'azuracast': { const {AzuracastSource} = (await import('./AzuracastSource.ts')); - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new AzuracastSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('azuracast', strongConfigs, defaults, AzuracastSource); } break; case 'koito': { const KoitoSource = (await import('./KoitoSource.ts')).default; - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new KoitoSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('koito', strongConfigs, defaults, KoitoSource); } break; case 'maloja': { const MalojaSource = (await import('./MalojaSource.ts')).default; - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new MalojaSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('maloja', strongConfigs, defaults, MalojaSource); } break; case 'tealfm': { const TealfmSource = (await import('./TealfmSource.ts')).default; - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new TealfmSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('tealfm', strongConfigs, defaults, TealfmSource); } break; case 'rocksky': { const RockskySource = (await import('./RockskySource.ts')).default; - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new RockskySource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('rocksky', strongConfigs, defaults, RockskySource); } break; case 'sonos': { const {SonosSource} = (await import('./SonosSource.ts')); - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new SonosSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('sonos', strongConfigs, defaults, SonosSource); } break; case 'applemusic': { const AppleMusicSource = (await import('./AppleMusicSource.ts')).default; - for (const s of strongConfigs) { - try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); - const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new AppleMusicSource(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); - newComponent.logger.info(`Source added from ${s.source}`); - this.sources.push(newComponent); - } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); - } - } + await this.instantiateSources('applemusic', strongConfigs, defaults, AppleMusicSource); } break; default: break; -- 2.51.2 From 9f54d4557631e64e057033c9e59208cbfa6740e0 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 19:43:00 +0000 Subject: [PATCH 12/35] fix(source): Fix missing defaults --- src/backend/sources/ScrobbleSources.ts | 4 +++- src/backend/tests/config/config.test.ts | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts index d5ebf85f..d0e4021e 100644 --- a/src/backend/sources/ScrobbleSources.ts +++ b/src/backend/sources/ScrobbleSources.ts @@ -124,7 +124,7 @@ export default class ScrobbleSources { throw new Error('config.json could not be parsed', { cause: e }); } - let sourceDefaults: SourceDefaults = {}; + let sourceDefaults: SourceDefaults; if (configFile !== undefined) { let aioConfig: AIOSourceRelaxedConfig; try { @@ -166,6 +166,8 @@ export default class ScrobbleSources { continue; } } + } else { + sourceDefaults = this.buildSourceDefaults(); } const envKeys = Object.keys(process.env).map(x => x.toUpperCase()); diff --git a/src/backend/tests/config/config.test.ts b/src/backend/tests/config/config.test.ts index a23cbdcf..dabc7163 100644 --- a/src/backend/tests/config/config.test.ts +++ b/src/backend/tests/config/config.test.ts @@ -7,7 +7,7 @@ import path from "path"; import ScrobbleClients from '../../scrobblers/ScrobbleClients.ts'; import ScrobbleSources from '../../sources/ScrobbleSources.ts'; import EventEmitter from "events"; -import {loggerDebug, loggerTest} from '@foxxmd/logging'; +import {loggerTest} from '@foxxmd/logging'; import { clientTypes } from "../../../core/Atomic.ts"; import { projectRootDir } from "../../common/infrastructure/Atomic.ts"; import { sourceTypes } from "../../../core/Atomic.ts"; @@ -143,7 +143,7 @@ describe('Sample Configs', function () { configDir: process.cwd(), version: 'test' }, - loggerDebug); + loggerTest); await clients.buildClientsFromConfig(); expect(clients.clients).length(1); }); @@ -169,6 +169,7 @@ describe('Global ENVs with Config', function () { it('Parses default scrobble duration', async function () { process.env.SOURCE_SCROBBLE_DURATION = '20'; + process.env.MPRIS_ID = 'test'; process.env.MPRIS_ENABLE = 'true'; const emitter = new EventEmitter(); @@ -185,6 +186,7 @@ describe('Global ENVs with Config', function () { it('Parses default scrobble precentage', async function () { process.env.SOURCE_SCROBBLE_PERCENT = '20'; + process.env.MPRIS_ID = 'test'; process.env.MPRIS_ENABLE = 'true'; const emitter = new EventEmitter(); -- 2.51.2 From fd2a1788087bdf1d83c46232a5465b46c5aacbd9 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 20:08:00 +0000 Subject: [PATCH 13/35] feat: Add env prefix to schema interface --- .../common/infrastructure/config/client/discord.ts | 1 + src/backend/common/infrastructure/config/client/index.ts | 3 ++- src/backend/common/infrastructure/config/client/koito.ts | 1 + src/backend/common/infrastructure/config/client/lastfm.ts | 1 + .../common/infrastructure/config/client/librefm.ts | 1 + .../common/infrastructure/config/client/listenbrainz.ts | 1 + src/backend/common/infrastructure/config/client/maloja.ts | 1 + .../common/infrastructure/config/client/rocksky.ts | 1 + src/backend/common/infrastructure/config/client/tealfm.ts | 1 + src/backend/common/infrastructure/config/common.ts | 4 ++-- .../common/infrastructure/config/source/applemusic.ts | 1 + .../common/infrastructure/config/source/azuracast.ts | 1 + .../common/infrastructure/config/source/chromecast.ts | 1 + src/backend/common/infrastructure/config/source/deezer.ts | 1 + .../common/infrastructure/config/source/endpointlfm.ts | 1 + .../common/infrastructure/config/source/endpointlz.ts | 1 + .../common/infrastructure/config/source/icecast.ts | 1 + src/backend/common/infrastructure/config/source/index.ts | 1 + .../common/infrastructure/config/source/jellyfin.ts | 1 + src/backend/common/infrastructure/config/source/jriver.ts | 1 + src/backend/common/infrastructure/config/source/kodi.ts | 1 + src/backend/common/infrastructure/config/source/koito.ts | 1 + src/backend/common/infrastructure/config/source/lastfm.ts | 1 + .../common/infrastructure/config/source/librefm.ts | 1 + .../common/infrastructure/config/source/listenbrainz.ts | 1 + src/backend/common/infrastructure/config/source/maloja.ts | 1 + src/backend/common/infrastructure/config/source/mopidy.ts | 1 + src/backend/common/infrastructure/config/source/mpd.ts | 1 + src/backend/common/infrastructure/config/source/mpris.ts | 1 + .../common/infrastructure/config/source/musiccast.ts | 1 + .../common/infrastructure/config/source/musikcube.ts | 1 + src/backend/common/infrastructure/config/source/plex.ts | 1 + .../common/infrastructure/config/source/rocksky.ts | 1 + src/backend/common/infrastructure/config/source/sonos.ts | 1 + .../common/infrastructure/config/source/spotify.ts | 1 + .../common/infrastructure/config/source/subsonic.ts | 1 + src/backend/common/infrastructure/config/source/tealfm.ts | 1 + src/backend/common/infrastructure/config/source/vlc.ts | 1 + .../common/infrastructure/config/source/webscrobbler.ts | 1 + .../common/infrastructure/config/source/ymbridge.ts | 1 + .../common/infrastructure/config/source/ytmusic.ts | 1 + src/backend/scrobblers/ScrobbleClients.ts | 8 ++++---- src/backend/sources/ScrobbleSources.ts | 8 ++++---- 43 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/backend/common/infrastructure/config/client/discord.ts b/src/backend/common/infrastructure/config/client/discord.ts index 4b3fc649..ed70f994 100644 --- a/src/backend/common/infrastructure/config/client/discord.ts +++ b/src/backend/common/infrastructure/config/client/discord.ts @@ -32,6 +32,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvClientSchema = { env: envDataSchema, + prefix: 'DISCORD', toConfig: (partial) => ({ data: { token: partial.DISCORD_TOKEN, diff --git a/src/backend/common/infrastructure/config/client/index.ts b/src/backend/common/infrastructure/config/client/index.ts index 74e3a9fb..9bb0d173 100644 --- a/src/backend/common/infrastructure/config/client/index.ts +++ b/src/backend/common/infrastructure/config/client/index.ts @@ -1,6 +1,6 @@ import * as z from "zod"; import {playTransformOptionsSchema} from "../../../../../core/Transform.ts"; -import {commonConfigSchema, requestRetryOptionsSchema, monitorOptionsSchema} from "../common.ts"; +import {commonConfigSchema, requestRetryOptionsSchema, monitorOptionsSchema, type CommonComponentEnvShape} from "../common.ts"; import {retentionConfigDurationValueSchema} from "../database.ts"; /** @@ -187,5 +187,6 @@ export type CommonClientConfig = Omit, export interface EnvClientSchema { env: Y + prefix: string toConfig: (parsed: z.output) => Partial> } \ No newline at end of file diff --git a/src/backend/common/infrastructure/config/client/koito.ts b/src/backend/common/infrastructure/config/client/koito.ts index 2132f391..b79a0c53 100644 --- a/src/backend/common/infrastructure/config/client/koito.ts +++ b/src/backend/common/infrastructure/config/client/koito.ts @@ -86,6 +86,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvClientSchema = { env: envDataSchema, + prefix: 'KOITO', toConfig: (partial) => ({ data: { url: partial.KOITO_URL, diff --git a/src/backend/common/infrastructure/config/client/lastfm.ts b/src/backend/common/infrastructure/config/client/lastfm.ts index 4f0ac6f3..9a9af602 100644 --- a/src/backend/common/infrastructure/config/client/lastfm.ts +++ b/src/backend/common/infrastructure/config/client/lastfm.ts @@ -53,6 +53,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvClientSchema = { env: envDataSchema, + prefix: 'LASTFM', toConfig: (partial) => ({ data: { apiKey: partial.LASTFM_API_KEY, diff --git a/src/backend/common/infrastructure/config/client/librefm.ts b/src/backend/common/infrastructure/config/client/librefm.ts index b795e642..5f7b9071 100644 --- a/src/backend/common/infrastructure/config/client/librefm.ts +++ b/src/backend/common/infrastructure/config/client/librefm.ts @@ -72,6 +72,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvClientSchema = { env: envDataSchema, + prefix: 'LIBREFM', toConfig: (partial) => ({ data: { apiKey: partial.LIBREFM_API_KEY, diff --git a/src/backend/common/infrastructure/config/client/listenbrainz.ts b/src/backend/common/infrastructure/config/client/listenbrainz.ts index d383219a..7145ed56 100644 --- a/src/backend/common/infrastructure/config/client/listenbrainz.ts +++ b/src/backend/common/infrastructure/config/client/listenbrainz.ts @@ -44,6 +44,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvClientSchema = { env: envDataSchema, + prefix: 'LZ', toConfig: (partial) => ({ data: { url: partial.LZ_URL, diff --git a/src/backend/common/infrastructure/config/client/maloja.ts b/src/backend/common/infrastructure/config/client/maloja.ts index 9bc1c2ba..f1f56147 100644 --- a/src/backend/common/infrastructure/config/client/maloja.ts +++ b/src/backend/common/infrastructure/config/client/maloja.ts @@ -38,6 +38,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvClientSchema = { env: envDataSchema, + prefix: 'MALOJA', toConfig: (partial) => ({ data: { url: partial.MALOJA_URL, diff --git a/src/backend/common/infrastructure/config/client/rocksky.ts b/src/backend/common/infrastructure/config/client/rocksky.ts index d91bb56d..7f46819c 100644 --- a/src/backend/common/infrastructure/config/client/rocksky.ts +++ b/src/backend/common/infrastructure/config/client/rocksky.ts @@ -48,6 +48,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvClientSchema = { env: envDataSchema, + prefix: 'ROCKSKY', toConfig: (partial) => ({ data: { key: partial.ROCKSKY_KEY, diff --git a/src/backend/common/infrastructure/config/client/tealfm.ts b/src/backend/common/infrastructure/config/client/tealfm.ts index 72ccce82..b9caef33 100644 --- a/src/backend/common/infrastructure/config/client/tealfm.ts +++ b/src/backend/common/infrastructure/config/client/tealfm.ts @@ -27,6 +27,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvClientSchema = { env: envDataSchema, + prefix: 'TEALFM', toConfig: (partial) => ({ data: { identifier: partial.TEALFM_IDENTIFIER, diff --git a/src/backend/common/infrastructure/config/common.ts b/src/backend/common/infrastructure/config/common.ts index 2a7ca19a..2365be15 100644 --- a/src/backend/common/infrastructure/config/common.ts +++ b/src/backend/common/infrastructure/config/common.ts @@ -174,7 +174,7 @@ export const transformPresetEnv = = { +export type CommonComponentEnvShape = { [K in `${T}_ID`]: z.ZodString } & { [K in `${T}_NAME`]: z.ZodOptional @@ -189,7 +189,7 @@ export const generateCommonComponentEnvConfigSchema = (prefix: [`${prefix}_ENABLE`]: z.stringbool().optional() } as CommonComponentEnvShape); -type CommonComponentEnvConfigParsed = { +export type CommonComponentEnvConfigParsed = { [K in `${T}_ID`]: string } & { [K in `${T}_NAME`]?: string diff --git a/src/backend/common/infrastructure/config/source/applemusic.ts b/src/backend/common/infrastructure/config/source/applemusic.ts index d79ce2fa..e50f7806 100644 --- a/src/backend/common/infrastructure/config/source/applemusic.ts +++ b/src/backend/common/infrastructure/config/source/applemusic.ts @@ -96,6 +96,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'APPLEMUSIC', toConfig: (partial) => { let appleMusicKey: AppleMusicKey | undefined; let token: string | undefined; diff --git a/src/backend/common/infrastructure/config/source/azuracast.ts b/src/backend/common/infrastructure/config/source/azuracast.ts index 02f85410..0bff578f 100644 --- a/src/backend/common/infrastructure/config/source/azuracast.ts +++ b/src/backend/common/infrastructure/config/source/azuracast.ts @@ -147,6 +147,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'AZURA', toConfig: (partial) => ({ data: { url: partial.AZURA_URL, diff --git a/src/backend/common/infrastructure/config/source/chromecast.ts b/src/backend/common/infrastructure/config/source/chromecast.ts index 2feaf711..dd70c271 100644 --- a/src/backend/common/infrastructure/config/source/chromecast.ts +++ b/src/backend/common/infrastructure/config/source/chromecast.ts @@ -142,6 +142,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'CC', toConfig: (partial) => ({ data: { blacklistDevices: partial.CC_BLACKLIST_DEVICES, diff --git a/src/backend/common/infrastructure/config/source/deezer.ts b/src/backend/common/infrastructure/config/source/deezer.ts index 37e5a86b..a8708389 100644 --- a/src/backend/common/infrastructure/config/source/deezer.ts +++ b/src/backend/common/infrastructure/config/source/deezer.ts @@ -113,6 +113,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'DEEZER', toConfig: (partial) => ({ data: { arl: partial.DEEZER_ARL, diff --git a/src/backend/common/infrastructure/config/source/endpointlfm.ts b/src/backend/common/infrastructure/config/source/endpointlfm.ts index f7cf2598..e4d60591 100644 --- a/src/backend/common/infrastructure/config/source/endpointlfm.ts +++ b/src/backend/common/infrastructure/config/source/endpointlfm.ts @@ -28,6 +28,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'LFM', toConfig: (partial) => ({ data: { slug: partial.LFM_SLUG diff --git a/src/backend/common/infrastructure/config/source/endpointlz.ts b/src/backend/common/infrastructure/config/source/endpointlz.ts index 0cacc7fb..58d1dfab 100644 --- a/src/backend/common/infrastructure/config/source/endpointlz.ts +++ b/src/backend/common/infrastructure/config/source/endpointlz.ts @@ -50,6 +50,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'LZE', toConfig: (partial) => ({ data: { slug: partial.LZE_SLUG, diff --git a/src/backend/common/infrastructure/config/source/icecast.ts b/src/backend/common/infrastructure/config/source/icecast.ts index 50ae8546..55431220 100644 --- a/src/backend/common/infrastructure/config/source/icecast.ts +++ b/src/backend/common/infrastructure/config/source/icecast.ts @@ -51,6 +51,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'ICECAST', toConfig: (partial) => ({ data: { url: partial.ICECAST_URL diff --git a/src/backend/common/infrastructure/config/source/index.ts b/src/backend/common/infrastructure/config/source/index.ts index 483721d5..7e6bd447 100644 --- a/src/backend/common/infrastructure/config/source/index.ts +++ b/src/backend/common/infrastructure/config/source/index.ts @@ -208,5 +208,6 @@ export type CommonSourceConfig = Omit, export interface EnvSourceSchema { env: Y + prefix: string, toConfig: (parsed: z.output) => Partial> } \ No newline at end of file diff --git a/src/backend/common/infrastructure/config/source/jellyfin.ts b/src/backend/common/infrastructure/config/source/jellyfin.ts index d465eb06..b006c135 100644 --- a/src/backend/common/infrastructure/config/source/jellyfin.ts +++ b/src/backend/common/infrastructure/config/source/jellyfin.ts @@ -138,6 +138,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'JELLYFIN', toConfig: (partial) => ({ data: { user: partial.JELLYFIN_USER, diff --git a/src/backend/common/infrastructure/config/source/jriver.ts b/src/backend/common/infrastructure/config/source/jriver.ts index f0e28e21..5c801ec3 100644 --- a/src/backend/common/infrastructure/config/source/jriver.ts +++ b/src/backend/common/infrastructure/config/source/jriver.ts @@ -55,6 +55,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'JRIVER', toConfig: (partial) => ({ data: { url: partial.JRIVER_URL, diff --git a/src/backend/common/infrastructure/config/source/kodi.ts b/src/backend/common/infrastructure/config/source/kodi.ts index 4b2b12c8..979a7f5f 100644 --- a/src/backend/common/infrastructure/config/source/kodi.ts +++ b/src/backend/common/infrastructure/config/source/kodi.ts @@ -54,6 +54,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'KODI', toConfig: (partial) => ({ data: { url: partial.KODI_URL, diff --git a/src/backend/common/infrastructure/config/source/koito.ts b/src/backend/common/infrastructure/config/source/koito.ts index b651a38c..67b29ceb 100644 --- a/src/backend/common/infrastructure/config/source/koito.ts +++ b/src/backend/common/infrastructure/config/source/koito.ts @@ -19,6 +19,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'SOURCE_KOITO', toConfig: (partial) => ({ data: { url: partial.SOURCE_KOITO_URL, diff --git a/src/backend/common/infrastructure/config/source/lastfm.ts b/src/backend/common/infrastructure/config/source/lastfm.ts index bfb23087..137b2015 100644 --- a/src/backend/common/infrastructure/config/source/lastfm.ts +++ b/src/backend/common/infrastructure/config/source/lastfm.ts @@ -20,6 +20,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'SOURCE_LASTFM', toConfig: (partial) => ({ data: { apiKey: partial.SOURCE_LASTFM_API_KEY, diff --git a/src/backend/common/infrastructure/config/source/librefm.ts b/src/backend/common/infrastructure/config/source/librefm.ts index e2ea0d4b..556bd80b 100644 --- a/src/backend/common/infrastructure/config/source/librefm.ts +++ b/src/backend/common/infrastructure/config/source/librefm.ts @@ -21,6 +21,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'SOURCE_LIBREFM', toConfig: (partial) => ({ data: { apiKey: partial.SOURCE_LIBREFM_API_KEY, diff --git a/src/backend/common/infrastructure/config/source/listenbrainz.ts b/src/backend/common/infrastructure/config/source/listenbrainz.ts index 3d373908..8bca4859 100644 --- a/src/backend/common/infrastructure/config/source/listenbrainz.ts +++ b/src/backend/common/infrastructure/config/source/listenbrainz.ts @@ -19,6 +19,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'SOURCE_LZ', toConfig: (partial) => ({ data: { url: partial.SOURCE_LZ_URL, diff --git a/src/backend/common/infrastructure/config/source/maloja.ts b/src/backend/common/infrastructure/config/source/maloja.ts index 89754720..abc62324 100644 --- a/src/backend/common/infrastructure/config/source/maloja.ts +++ b/src/backend/common/infrastructure/config/source/maloja.ts @@ -18,6 +18,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'SOURCE_MALOJA', toConfig: (partial) => ({ data: { url: partial.SOURCE_MALOJA_URL, diff --git a/src/backend/common/infrastructure/config/source/mopidy.ts b/src/backend/common/infrastructure/config/source/mopidy.ts index 9d76ae41..b06d0d1a 100644 --- a/src/backend/common/infrastructure/config/source/mopidy.ts +++ b/src/backend/common/infrastructure/config/source/mopidy.ts @@ -110,6 +110,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'MOPIDY', toConfig: (partial) => ({ data: { url: partial.MOPIDY_URL, diff --git a/src/backend/common/infrastructure/config/source/mpd.ts b/src/backend/common/infrastructure/config/source/mpd.ts index 9680e928..084191ce 100644 --- a/src/backend/common/infrastructure/config/source/mpd.ts +++ b/src/backend/common/infrastructure/config/source/mpd.ts @@ -46,6 +46,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'MPD', toConfig: (partial) => ({ data: { url: partial.MPD_URL, diff --git a/src/backend/common/infrastructure/config/source/mpris.ts b/src/backend/common/infrastructure/config/source/mpris.ts index dadf8b80..a3bfb83f 100644 --- a/src/backend/common/infrastructure/config/source/mpris.ts +++ b/src/backend/common/infrastructure/config/source/mpris.ts @@ -63,6 +63,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'MPRIS', toConfig: (partial) => ({ data: { blacklist: partial.MPRIS_BLACKLIST, diff --git a/src/backend/common/infrastructure/config/source/musiccast.ts b/src/backend/common/infrastructure/config/source/musiccast.ts index 94cb6782..37627e57 100644 --- a/src/backend/common/infrastructure/config/source/musiccast.ts +++ b/src/backend/common/infrastructure/config/source/musiccast.ts @@ -92,6 +92,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'MCAST', toConfig: (partial) => ({ data: { url: partial.MCAST_URL diff --git a/src/backend/common/infrastructure/config/source/musikcube.ts b/src/backend/common/infrastructure/config/source/musikcube.ts index afc00c4c..4de3a59a 100644 --- a/src/backend/common/infrastructure/config/source/musikcube.ts +++ b/src/backend/common/infrastructure/config/source/musikcube.ts @@ -124,6 +124,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'MC', toConfig: (partial) => ({ data: { url: partial.MC_URL, diff --git a/src/backend/common/infrastructure/config/source/plex.ts b/src/backend/common/infrastructure/config/source/plex.ts index 364eb572..3660fd4b 100644 --- a/src/backend/common/infrastructure/config/source/plex.ts +++ b/src/backend/common/infrastructure/config/source/plex.ts @@ -70,6 +70,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'PLEX', toConfig: (partial) => ({ data: { url: partial.PLEX_URL, diff --git a/src/backend/common/infrastructure/config/source/rocksky.ts b/src/backend/common/infrastructure/config/source/rocksky.ts index f05b2b2f..55220e26 100644 --- a/src/backend/common/infrastructure/config/source/rocksky.ts +++ b/src/backend/common/infrastructure/config/source/rocksky.ts @@ -18,6 +18,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'SOURCE_ROCKSKY', toConfig: (partial) => ({ data: { key: partial.SOURCE_ROCKSKY_KEY, diff --git a/src/backend/common/infrastructure/config/source/sonos.ts b/src/backend/common/infrastructure/config/source/sonos.ts index 817b9407..b2548b43 100644 --- a/src/backend/common/infrastructure/config/source/sonos.ts +++ b/src/backend/common/infrastructure/config/source/sonos.ts @@ -54,6 +54,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'SONOS', toConfig: (partial) => ({ data: { host: partial.SONOS_HOST, diff --git a/src/backend/common/infrastructure/config/source/spotify.ts b/src/backend/common/infrastructure/config/source/spotify.ts index 8a3431a5..a4302878 100644 --- a/src/backend/common/infrastructure/config/source/spotify.ts +++ b/src/backend/common/infrastructure/config/source/spotify.ts @@ -66,6 +66,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'SPOTIFY', toConfig: (partial) => ({ data: { clientId: partial.SPOTIFY_CLIENT_ID, diff --git a/src/backend/common/infrastructure/config/source/subsonic.ts b/src/backend/common/infrastructure/config/source/subsonic.ts index 55fd59f3..395cd950 100644 --- a/src/backend/common/infrastructure/config/source/subsonic.ts +++ b/src/backend/common/infrastructure/config/source/subsonic.ts @@ -110,6 +110,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'SUBSONIC', toConfig: (partial) => ({ data: { user: partial.SUBSONIC_USER, diff --git a/src/backend/common/infrastructure/config/source/tealfm.ts b/src/backend/common/infrastructure/config/source/tealfm.ts index 45bfc385..39adb78b 100644 --- a/src/backend/common/infrastructure/config/source/tealfm.ts +++ b/src/backend/common/infrastructure/config/source/tealfm.ts @@ -20,6 +20,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'SOURCE_TEALFM', toConfig: (partial) => ({ data: { identifier: partial.SOURCE_TEALFM_IDENTIFIER, diff --git a/src/backend/common/infrastructure/config/source/vlc.ts b/src/backend/common/infrastructure/config/source/vlc.ts index dc8aa3be..d960bcc0 100644 --- a/src/backend/common/infrastructure/config/source/vlc.ts +++ b/src/backend/common/infrastructure/config/source/vlc.ts @@ -38,6 +38,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'VLC', toConfig: (partial) => ({ data: { url: partial.VLC_URL, diff --git a/src/backend/common/infrastructure/config/source/webscrobbler.ts b/src/backend/common/infrastructure/config/source/webscrobbler.ts index 730fca71..910adea9 100644 --- a/src/backend/common/infrastructure/config/source/webscrobbler.ts +++ b/src/backend/common/infrastructure/config/source/webscrobbler.ts @@ -54,6 +54,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'WS', toConfig: (partial) => ({ data: { blacklist: partial.WS_BLACKLIST, diff --git a/src/backend/common/infrastructure/config/source/ymbridge.ts b/src/backend/common/infrastructure/config/source/ymbridge.ts index 23e5cf71..71821f19 100644 --- a/src/backend/common/infrastructure/config/source/ymbridge.ts +++ b/src/backend/common/infrastructure/config/source/ymbridge.ts @@ -24,6 +24,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'YMBRIDGE', toConfig: (partial) => ({ data: { url: partial.YMBRIDGE_URL, diff --git a/src/backend/common/infrastructure/config/source/ytmusic.ts b/src/backend/common/infrastructure/config/source/ytmusic.ts index 8bc7febd..bfd29ab8 100644 --- a/src/backend/common/infrastructure/config/source/ytmusic.ts +++ b/src/backend/common/infrastructure/config/source/ytmusic.ts @@ -107,6 +107,7 @@ const envDataSchema = z.object({ export const envSchemas: EnvSourceSchema = { env: envDataSchema, + prefix: 'YTM', toConfig: (partial) => ({ data: { redirectUri: partial.YTM_REDIRECT_URI, diff --git a/src/backend/scrobblers/ScrobbleClients.ts b/src/backend/scrobblers/ScrobbleClients.ts index 3c80a7fe..c4763cc6 100644 --- a/src/backend/scrobblers/ScrobbleClients.ts +++ b/src/backend/scrobblers/ScrobbleClients.ts @@ -198,13 +198,13 @@ export default class ScrobbleClients { const sourceStr = `${entry.source} ${entry.pos}`; switch (entry.source) { case 'env': { - const primitiveSchema = generateCommonComponentEnvConfigSchema(clientUpper); - const parsed = primitiveSchema.parse(entry.config); - const primitives: CommonConfigPrimitives = commonComponentEnvConfigToConfigPrimitives(clientUpper, parsed); const envSchema = clientConfigSchemaMap[clientType][2]; + const primitiveSchema = generateCommonComponentEnvConfigSchema(envSchema.prefix.toUpperCase()); + const parsed = primitiveSchema.parse(entry.config); + const primitives: CommonConfigPrimitives = commonComponentEnvConfigToConfigPrimitives(envSchema.prefix.toUpperCase(), parsed); const parsedEnvConfigValues = envSchema.env.parse(entry.config); const { data = {}, options = {} } = envSchema.toConfig(parsedEnvConfigValues); - const transformOptions = transformPresetEnv(clientUpper); + const transformOptions = transformPresetEnv(envSchema.prefix.toUpperCase()); parsedConfig = { name: `${clientType} - ${entry.source}${entry.pos !== '' ? ` - ${entry.pos}` : ''} `, ...primitives, diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts index d0e4021e..a81d53cd 100644 --- a/src/backend/sources/ScrobbleSources.ts +++ b/src/backend/sources/ScrobbleSources.ts @@ -217,13 +217,13 @@ export default class ScrobbleSources { const sourceStr = `${entry.source} ${entry.pos}`; switch (entry.source) { case 'env': { - const primitiveSchema = generateCommonComponentEnvConfigSchema(configTypeUpper); - const parsed = primitiveSchema.parse(entry.config); - const primitives: CommonConfigPrimitives = commonComponentEnvConfigToConfigPrimitives(configTypeUpper, parsed); const envSchema = sourceConfigSchemaMap[configType][2]; + const primitiveSchema = generateCommonComponentEnvConfigSchema(envSchema.prefix.toUpperCase()); + const parsed = primitiveSchema.parse(entry.config); + const primitives: CommonConfigPrimitives = commonComponentEnvConfigToConfigPrimitives(envSchema.prefix.toUpperCase(), parsed); const parsedEnvConfigValues = envSchema.env.parse(entry.config); const { data = {}, options = {} } = envSchema.toConfig(parsedEnvConfigValues); - const transformOptions = transformPresetEnv(configTypeUpper); + const transformOptions = transformPresetEnv(envSchema.prefix.toUpperCase()); parsedConfig = { name: `${configType} - ${entry.source}${entry.pos !== '' ? ` - ${entry.pos}` : ''} `, ...primitives, -- 2.51.2 From 2fc90dcfdaac649da81f980207b85cbdb3db4922 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 20:15:11 +0000 Subject: [PATCH 14/35] feat: Generate env markdown for components --- .../clients/_env_configs/_discord.md | 12 ++++++ .../clients/_env_configs/_koito.md | 8 ++++ .../clients/_env_configs/_lastfm.md | 9 +++++ .../clients/_env_configs/_librefm.md | 10 +++++ .../clients/_env_configs/_listenbrainz.md | 8 ++++ .../clients/_env_configs/_maloja.md | 14 +++---- .../clients/_env_configs/_rocksky.md | 8 ++++ .../clients/_env_configs/_tealfm.md | 7 ++++ .../docs/configuration/clients/rocksky.mdx | 8 +--- .../sources/_env_configs/_applemusic.md | 13 +++++++ .../sources/_env_configs/_azuracast.md | 10 +++++ .../sources/_env_configs/_chromecast.md | 9 +++++ .../sources/_env_configs/_deezer.md | 8 ++++ .../sources/_env_configs/_endpointlfm.md | 6 +++ .../sources/_env_configs/_endpointlz.md | 8 ++++ .../sources/_env_configs/_icecast.md | 7 ++++ .../sources/_env_configs/_jellyfin.md | 17 +++++++++ .../sources/_env_configs/_jriver.md | 8 ++++ .../sources/_env_configs/_kodi.md | 8 ++++ .../sources/_env_configs/_koito.md | 8 ++++ .../sources/_env_configs/_lastfm.md | 9 +++++ .../sources/_env_configs/_librefm.md | 10 +++++ .../sources/_env_configs/_listenbrainz.md | 8 ++++ .../sources/_env_configs/_maloja.md | 7 ++++ .../sources/_env_configs/_mopidy.md | 9 +++++ .../sources/_env_configs/_mpd.md | 7 ++++ .../sources/_env_configs/_mpris.md | 7 ++++ .../sources/_env_configs/_musiccast.md | 6 +++ .../sources/_env_configs/_musikcube.md | 7 ++++ .../sources/_env_configs/_plex.md | 13 +++++++ .../sources/_env_configs/_rocksky.md | 7 ++++ .../sources/_env_configs/_sonos.md | 10 +++++ .../sources/_env_configs/_spotify.md | 8 ++++ .../sources/_env_configs/_subsonic.md | 8 ++++ .../sources/_env_configs/_tealfm.md | 7 ++++ .../sources/_env_configs/_vlc.md | 7 ++++ .../sources/_env_configs/_webscrobbler.md | 7 ++++ .../sources/_env_configs/_ymbridge.md | 7 ++++ .../sources/_env_configs/_ytmusic.md | 9 +++++ .../infrastructure/config/source/azuracast.ts | 2 +- src/backend/utils/SchemaDocsStaticUtils.ts | 38 ++++++++++--------- 41 files changed, 348 insertions(+), 31 deletions(-) create mode 100644 docsite/docs/configuration/clients/_env_configs/_discord.md create mode 100644 docsite/docs/configuration/clients/_env_configs/_koito.md create mode 100644 docsite/docs/configuration/clients/_env_configs/_lastfm.md create mode 100644 docsite/docs/configuration/clients/_env_configs/_librefm.md create mode 100644 docsite/docs/configuration/clients/_env_configs/_listenbrainz.md create mode 100644 docsite/docs/configuration/clients/_env_configs/_rocksky.md create mode 100644 docsite/docs/configuration/clients/_env_configs/_tealfm.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_applemusic.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_azuracast.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_chromecast.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_deezer.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_endpointlfm.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_endpointlz.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_icecast.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_jellyfin.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_jriver.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_kodi.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_koito.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_lastfm.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_librefm.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_listenbrainz.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_maloja.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_mopidy.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_mpd.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_mpris.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_musiccast.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_musikcube.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_plex.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_rocksky.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_sonos.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_spotify.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_subsonic.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_tealfm.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_vlc.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_webscrobbler.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_ymbridge.md create mode 100644 docsite/docs/configuration/sources/_env_configs/_ytmusic.md diff --git a/docsite/docs/configuration/clients/_env_configs/_discord.md b/docsite/docs/configuration/clients/_env_configs/_discord.md new file mode 100644 index 00000000..c9d9aec5 --- /dev/null +++ b/docsite/docs/configuration/clients/_env_configs/_discord.md @@ -0,0 +1,12 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------------------- | ------- | --------------------- | -------------------- | +| **`DISCORD_ID`** | string | | A globally unique ID | +| `DISCORD_NAME` | string | Value of `DISCORD_ID` | A vanity name | +| `DISCORD_ENABLE` | boolean | | | +| `DISCORD_TOKEN` | string | | | +| `DISCORD_ARTWORK` | union | | | +| `DISCORD_APPLICATION_ID` | string | | | +| `DISCORD_IPC_LOCATIONS` | union | | | +| `DISCORD_ARTWORK_DEFAULT_URL` | string | | | +| `DISCORD_STATUS_OVERRIDE_ALLOW` | union | | | +| `DISCORD_LISTENING_ACTIVITY_ALLOW` | union | | | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/_env_configs/_koito.md b/docsite/docs/configuration/clients/_env_configs/_koito.md new file mode 100644 index 00000000..d88a5fed --- /dev/null +++ b/docsite/docs/configuration/clients/_env_configs/_koito.md @@ -0,0 +1,8 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ------------------- | --------------------------------------- | +| **`KOITO_ID`** | string | | A globally unique ID | +| `KOITO_NAME` | string | Value of `KOITO_ID` | A vanity name | +| `KOITO_ENABLE` | boolean | | | +| **`KOITO_URL`** | string | | URL for the Koito server | +| **`KOITO_TOKEN`** | string | | User token for the user to scrobble for | +| **`KOITO_USER`** | string | | Username of the user to scrobble for | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/_env_configs/_lastfm.md b/docsite/docs/configuration/clients/_env_configs/_lastfm.md new file mode 100644 index 00000000..d85806c8 --- /dev/null +++ b/docsite/docs/configuration/clients/_env_configs/_lastfm.md @@ -0,0 +1,9 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ------------------------------------- | ------------------------------------------------------- | +| **`LASTFM_ID`** | string | | A globally unique ID | +| `LASTFM_NAME` | string | Value of `LASTFM_ID` | A vanity name | +| `LASTFM_ENABLE` | boolean | | | +| **`LASTFM_API_KEY`** | string | | API Key generated from Last.fm/Libre.fm account | +| **`LASTFM_SECRET`** | string | | Secret generated from Last.fm/Libre.fm account | +| `LASTFM_REDIRECT_URI` | string | http://localhost:9078/lastfm/callback | Optional URI to use for callback. | +| `LASTFM_SESSION` | string | | Optional session id returned from a completed auth flow | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/_env_configs/_librefm.md b/docsite/docs/configuration/clients/_env_configs/_librefm.md new file mode 100644 index 00000000..7a3f0e9a --- /dev/null +++ b/docsite/docs/configuration/clients/_env_configs/_librefm.md @@ -0,0 +1,10 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | -------------------------------------- | -------------------------------------------------------------- | +| **`LIBREFM_ID`** | string | | A globally unique ID | +| `LIBREFM_NAME` | string | Value of `LIBREFM_ID` | A vanity name | +| `LIBREFM_ENABLE` | boolean | | | +| `LIBREFM_API_KEY` | string | | Optional Secret for Libre.fm account | +| `LIBREFM_SECRET` | string | | Optional Secret for Libre.fm account | +| `LIBREFM_REDIRECT_URI` | string | http://localhost:9078/librefm/callback | Optional URI to use for callback. | +| `LIBREFM_SESSION` | string | | Optional session id returned from a completed auth flow | +| `LIBREFM_URLBASE` | string | https://libre.fm/2.0/ | (Optional) The host and path prefix for your Libre.fm instance | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/_env_configs/_listenbrainz.md b/docsite/docs/configuration/clients/_env_configs/_listenbrainz.md new file mode 100644 index 00000000..7b61d4a5 --- /dev/null +++ b/docsite/docs/configuration/clients/_env_configs/_listenbrainz.md @@ -0,0 +1,8 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ----------------------------- | --------------------------------------------------------- | +| **`LZ_ID`** | string | | A globally unique ID | +| `LZ_NAME` | string | Value of `LZ_ID` | A vanity name | +| `LZ_ENABLE` | boolean | | | +| `LZ_URL` | string | https://api.listenbrainz.org/ | URL for the ListenBrainz server, if not using the default | +| **`LZ_TOKEN`** | string | | User token for the user to scrobble for | +| **`LZ_USER`** | string | | Username of the user to scrobble for | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/_env_configs/_maloja.md b/docsite/docs/configuration/clients/_env_configs/_maloja.md index 33d0628a..85d358d8 100644 --- a/docsite/docs/configuration/clients/_env_configs/_maloja.md +++ b/docsite/docs/configuration/clients/_env_configs/_maloja.md @@ -1,7 +1,7 @@ -| Environmental Variable | Type | Default | Description | -| ---------------------- | ------- | -------------------- | ----------------------------- | -| _**`MALOJA_ID`**_ | string | | A globally unique ID | -| `MALOJA_NAME` | string | Value of `MALOJA_ID` | A vanity name | -| `MALOJA_ENABLE` | boolean | | | -| _**`MALOJA_URL`**_ | string | | Base URL of your installation | -| `MALOJA_API_KEY` | string | fsdfsd | | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | -------------------- | ------------------------- | +| **`MALOJA_ID`** | string | | A globally unique ID | +| `MALOJA_NAME` | string | Value of `MALOJA_ID` | A vanity name | +| `MALOJA_ENABLE` | boolean | | | +| **`MALOJA_URL`** | string | | URL for maloja server | +| **`MALOJA_API_KEY`** | string | | API Key for Maloja server | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/_env_configs/_rocksky.md b/docsite/docs/configuration/clients/_env_configs/_rocksky.md new file mode 100644 index 00000000..1b46b56b --- /dev/null +++ b/docsite/docs/configuration/clients/_env_configs/_rocksky.md @@ -0,0 +1,8 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| **`ROCKSKY_ID`** | string | | A globally unique ID | +| `ROCKSKY_NAME` | string | Value of `ROCKSKY_ID` | A vanity name | +| `ROCKSKY_ENABLE` | boolean | | | +| `ROCKSKY_KEY` | string | | API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account | +| `ROCKSKY_TOKEN` | string | | Access Token generated from https://rocksky.app/access-tokens in Rocksky for your account | +| **`ROCKSKY_HANDLE`** | string | | The **fully-qualified** handle for your ATPRoto/Bluesky account, like: | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/_env_configs/_tealfm.md b/docsite/docs/configuration/clients/_env_configs/_tealfm.md new file mode 100644 index 00000000..51ea4df5 --- /dev/null +++ b/docsite/docs/configuration/clients/_env_configs/_tealfm.md @@ -0,0 +1,7 @@ +| Environmental Variable | Type | Default | Description | +| ----------------------- | ------- | -------------------- | -------------------------------- | +| **`TEALFM_ID`** | string | | A globally unique ID | +| `TEALFM_NAME` | string | Value of `TEALFM_ID` | A vanity name | +| `TEALFM_ENABLE` | boolean | | | +| **`TEALFM_IDENTIFIER`** | string | | Identify the account to login as | +| `TEALFM_APP_PW` | string | | | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/rocksky.mdx b/docsite/docs/configuration/clients/rocksky.mdx index 0e1c977f..82d7ae09 100644 --- a/docsite/docs/configuration/clients/rocksky.mdx +++ b/docsite/docs/configuration/clients/rocksky.mdx @@ -6,6 +6,7 @@ toc_max_heading_level: 5 import Bluesky from "@site/src/components/snippets/_bluesky.mdx"; import JsonConfig from '!!raw-loader!@site/../config/rocksky.json.example'; +import ENVConfig from "@site/docs/configuration/clients/_env_configs/_rocksky.md" :::tip[Other Uses] @@ -76,10 +77,5 @@ Keys are now **deprecated** and functionality for them will be removed in a futu - | Environmental Variable | Required? | Default | Description | - | :--------------------- | --------- | ------- | ----------------------------------------------------------------------------------- | - | `ROCKSKY_ID` | Yes | | A unique ID used in MS | - | `ROCKSKY_HANDLE` | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` | - | `ROCKSKY_TOKEN` | Yes | | Access Token obtained from Rocksky | - | `ROCKSKY_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_applemusic.md b/docsite/docs/configuration/sources/_env_configs/_applemusic.md new file mode 100644 index 00000000..ebfe871d --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_applemusic.md @@ -0,0 +1,13 @@ +| Environmental Variable | Type | Default | Description | +| ------------------------------------------ | ------- | ------------------------ | -------------------------------------------------------------------- | +| **`APPLEMUSIC_ID`** | string | | A globally unique ID | +| `APPLEMUSIC_NAME` | string | Value of `APPLEMUSIC_ID` | A vanity name | +| `APPLEMUSIC_ENABLE` | boolean | | | +| `APPLEMUSIC_KEY_ID` | string | | | +| `APPLEMUSIC_TEAM_ID` | string | | | +| `APPLEMUSIC_KEY_P8` | string | | | +| **`APPLEMUSIC_MEDIA_USER_TOKEN`** | string | | | +| `APPLEMUSIC_TOKEN` | string | | | +| `APPLEMUSIC_ORIGIN_HEADER` | string | | Origin header to include in every Apple Music API request. | +| `APPLEMUSIC_RECOVER_UNCHANGED_TOP_HISTORY` | boolean | true | Fixes a quirk where Apple Music's history API hides duplicate plays. | +| `APPLEMUSIC_NORMALIZE_ALBUM` | boolean | true | Removes extraneous suffixes from album data | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_azuracast.md b/docsite/docs/configuration/sources/_env_configs/_azuracast.md new file mode 100644 index 00000000..87af3eef --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_azuracast.md @@ -0,0 +1,10 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | --------- | ------------------- | ------------------------------------------------- | +| **`AZURA_ID`** | string | | A globally unique ID | +| `AZURA_NAME` | string | Value of `AZURA_ID` | A vanity name | +| `AZURA_ENABLE` | boolean | | | +| **`AZURA_STATION`** | string | | The specific station to monitor | +| **`AZURA_URL`** | string | | Base URL of the Azuracast instance | +| `AZURA_KEY` | string | | API Key used to access data about private streams | +| `AZURA_LISTENERS_NUM` | transform | | | +| `AZURA_LIVE` | boolean | | | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_chromecast.md b/docsite/docs/configuration/sources/_env_configs/_chromecast.md new file mode 100644 index 00000000..c0e93f11 --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_chromecast.md @@ -0,0 +1,9 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | --------- | ---------------- | ------------------------------------------------------------------------------------ | +| **`CC_ID`** | string | | A globally unique ID | +| `CC_NAME` | string | Value of `CC_ID` | A vanity name | +| `CC_ENABLE` | boolean | | | +| `CC_BLACKLIST_DEVICES` | transform | | DO NOT scrobble from any cast devices that START WITH these values, case-insensitive | +| `CC_WHITELIST_DEVICES` | transform | | ONLY scrobble from any cast device that START WITH these values, case-insensitive | +| `CC_BLACKLIST_APPS` | transform | | DO NOT scrobble from any application that START WITH these values, case-insensitive | +| `CC_WHITELIST_APPS` | transform | | ONLY scrobble from any application that START WITH these values, case-insensitive | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_deezer.md b/docsite/docs/configuration/sources/_env_configs/_deezer.md new file mode 100644 index 00000000..02369081 --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_deezer.md @@ -0,0 +1,8 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | -------------------------------------------------------------------- | -------------------------------------------------- | +| **`DEEZER_ID`** | string | | A globally unique ID | +| `DEEZER_NAME` | string | Value of `DEEZER_ID` | A vanity name | +| `DEEZER_ENABLE` | boolean | | | +| **`DEEZER_ARL`** | string | | ARL retrieved from Deezer response header | +| `DEEZER_ACCOUNT_ID` | string | | The ID (USER_ID) of the linked account to monitor. | +| `DEEZER_USER_AGENT` | string | Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0 | User agent | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_endpointlfm.md b/docsite/docs/configuration/sources/_env_configs/_endpointlfm.md new file mode 100644 index 00000000..177a4b46 --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_endpointlfm.md @@ -0,0 +1,6 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ----------------- | ------------------------------------------------------------------------ | +| **`LFM_ID`** | string | | A globally unique ID | +| `LFM_NAME` | string | Value of `LFM_ID` | A vanity name | +| `LFM_ENABLE` | boolean | | | +| `LFM_SLUG` | union | | The URL ending that should be used to identify scrobbles for this source | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_endpointlz.md b/docsite/docs/configuration/sources/_env_configs/_endpointlz.md new file mode 100644 index 00000000..7658b826 --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_endpointlz.md @@ -0,0 +1,8 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| **`LZE_ID`** | string | | A globally unique ID | +| `LZE_NAME` | string | Value of `LZE_ID` | A vanity name | +| `LZE_ENABLE` | boolean | | | +| `LZE_SLUG` | string | | The URL ending that should be used to identify scrobbles for this source | +| `LZE_TOKEN` | string | | If an LZ submission request contains this token in the Authorization Header it will be used to match the submission with this Source | +| `LZE_USERNAME` | string | | The listenbrainz "username" to associate with this Source | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_icecast.md b/docsite/docs/configuration/sources/_env_configs/_icecast.md new file mode 100644 index 00000000..a7e3c0c0 --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_icecast.md @@ -0,0 +1,7 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | --------------------- | ---------------------- | +| **`ICECAST_ID`** | string | | A globally unique ID | +| `ICECAST_NAME` | string | Value of `ICECAST_ID` | A vanity name | +| `ICECAST_ENABLE` | boolean | | | +| **`ICECAST_URL`** | string | | The Icecast stream URL | +| `ICECAST_AUTO_MONITOR` | boolean | | | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_jellyfin.md b/docsite/docs/configuration/sources/_env_configs/_jellyfin.md new file mode 100644 index 00000000..f9e77c5e --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_jellyfin.md @@ -0,0 +1,17 @@ +| Environmental Variable | Type | Default | Description | +| -------------------------------- | ------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| **`JELLYFIN_ID`** | string | | A globally unique ID | +| `JELLYFIN_NAME` | string | Value of `JELLYFIN_ID` | A vanity name | +| `JELLYFIN_ENABLE` | boolean | | | +| **`JELLYFIN_USER`** | string | | The username of the user to authenticate for or track scrobbles for | +| `JELLYFIN_PASSWORD` | string | | Password of the username to authenticate for | +| `JELLYFIN_APIKEY` | string | | API Key to authenticate with. | +| **`JELLYFIN_URL`** | string | | HOST:PORT of the Jellyfin server to connect to | +| `JELLYFIN_USERS_ALLOW` | union | | Only scrobble for specific users (case-insensitive) | +| `JELLYFIN_USERS_BLOCK` | union | | Do not scrobble for these users (case-insensitive) | +| `JELLYFIN_DEVICES_ALLOW` | union | | Only scrobble if device or application name contains strings from this list (case-insensitive) | +| `JELLYFIN_DEVICES_BLOCK` | union | | Do not scrobble if device or application name contains strings from this list (case-insensitive) | +| `JELLYFIN_LIBRARIES_ALLOW` | union | | Only scrobble if library name contains string from this list (case-insensitive) | +| `JELLYFIN_LIBRARIES_BLOCK` | union | | Do not scrobble if library name contains strings from this list (case-insensitive) | +| `JELLYFIN_FRONTEND_URL_OVERRIDE` | string | | HOST:PORT of the Jellyfin server that your browser will be able to access from the frontend (and thus load images and links from) | +| `JELLYFIN_MEDIATYPES_ALLOW` | union | | Allow these media types to be scrobbled. | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_jriver.md b/docsite/docs/configuration/sources/_env_configs/_jriver.md new file mode 100644 index 00000000..8346b08c --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_jriver.md @@ -0,0 +1,8 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ------------------------------- | -------------------------------------------------------- | +| **`JRIVER_ID`** | string | | A globally unique ID | +| `JRIVER_NAME` | string | Value of `JRIVER_ID` | A vanity name | +| `JRIVER_ENABLE` | boolean | | | +| **`JRIVER_URL`** | string | http://localhost:52199/MCWS/v1/ | URL of the JRiver HTTP server to connect to | +| `JRIVER_USER` | string | | If you have enabled authentication, the username you set | +| `JRIVER_PASSWORD` | string | | If you have enabled authentication, the password you set | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_kodi.md b/docsite/docs/configuration/sources/_env_configs/_kodi.md new file mode 100644 index 00000000..4c62638d --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_kodi.md @@ -0,0 +1,8 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ----------------------------- | ------------------------------------------------- | +| **`KODI_ID`** | string | | A globally unique ID | +| `KODI_NAME` | string | Value of `KODI_ID` | A vanity name | +| `KODI_ENABLE` | boolean | | | +| **`KODI_URL`** | string | http://localhost:8080/jsonrpc | URL of the Kodi HTTP server to connect to | +| **`KODI_USER`** | string | | The username set for Remote Control via Web Sever | +| **`KODI_PASSWORD`** | string | | The password set for Remote Control via Web Sever | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_koito.md b/docsite/docs/configuration/sources/_env_configs/_koito.md new file mode 100644 index 00000000..1d66908c --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_koito.md @@ -0,0 +1,8 @@ +| Environmental Variable | Type | Default | Description | +| ------------------------ | ------- | -------------------------- | --------------------------------------- | +| **`SOURCE_KOITO_ID`** | string | | A globally unique ID | +| `SOURCE_KOITO_NAME` | string | Value of `SOURCE_KOITO_ID` | A vanity name | +| `SOURCE_KOITO_ENABLE` | boolean | | | +| **`SOURCE_KOITO_URL`** | string | | URL for the Koito server | +| **`SOURCE_KOITO_TOKEN`** | string | | User token for the user to scrobble for | +| **`SOURCE_KOITO_USER`** | string | | Username of the user to scrobble for | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_lastfm.md b/docsite/docs/configuration/sources/_env_configs/_lastfm.md new file mode 100644 index 00000000..ed171d2f --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_lastfm.md @@ -0,0 +1,9 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------------- | ------- | ------------------------------------- | ------------------------------------------------------- | +| **`SOURCE_LASTFM_ID`** | string | | A globally unique ID | +| `SOURCE_LASTFM_NAME` | string | Value of `SOURCE_LASTFM_ID` | A vanity name | +| `SOURCE_LASTFM_ENABLE` | boolean | | | +| **`SOURCE_LASTFM_API_KEY`** | string | | API Key generated from Last.fm/Libre.fm account | +| **`SOURCE_LASTFM_SECRET`** | string | | Secret generated from Last.fm/Libre.fm account | +| `SOURCE_LASTFM_REDIRECT_URI` | string | http://localhost:9078/lastfm/callback | Optional URI to use for callback. | +| `SOURCE_LASTFM_SESSION` | string | | Optional session id returned from a completed auth flow | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_librefm.md b/docsite/docs/configuration/sources/_env_configs/_librefm.md new file mode 100644 index 00000000..cd3c5744 --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_librefm.md @@ -0,0 +1,10 @@ +| Environmental Variable | Type | Default | Description | +| ----------------------------- | ------- | -------------------------------------- | -------------------------------------------------------------- | +| **`SOURCE_LIBREFM_ID`** | string | | A globally unique ID | +| `SOURCE_LIBREFM_NAME` | string | Value of `SOURCE_LIBREFM_ID` | A vanity name | +| `SOURCE_LIBREFM_ENABLE` | boolean | | | +| `SOURCE_LIBREFM_API_KEY` | string | | Optional Secret for Libre.fm account | +| `SOURCE_LIBREFM_SECRET` | string | | Optional Secret for Libre.fm account | +| `SOURCE_LIBREFM_REDIRECT_URI` | string | http://localhost:9078/librefm/callback | Optional URI to use for callback. | +| `SOURCE_LIBREFM_SESSION` | string | | Optional session id returned from a completed auth flow | +| `SOURCE_LIBREFM_URLBASE` | string | https://libre.fm/2.0/ | (Optional) The host and path prefix for your Libre.fm instance | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_listenbrainz.md b/docsite/docs/configuration/sources/_env_configs/_listenbrainz.md new file mode 100644 index 00000000..64f161bc --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_listenbrainz.md @@ -0,0 +1,8 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ----------------------------- | --------------------------------------------------------- | +| **`SOURCE_LZ_ID`** | string | | A globally unique ID | +| `SOURCE_LZ_NAME` | string | Value of `SOURCE_LZ_ID` | A vanity name | +| `SOURCE_LZ_ENABLE` | boolean | | | +| `SOURCE_LZ_URL` | string | https://api.listenbrainz.org/ | URL for the ListenBrainz server, if not using the default | +| **`SOURCE_LZ_TOKEN`** | string | | User token for the user to scrobble for | +| **`SOURCE_LZ_USER`** | string | | Username of the user to scrobble for | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_maloja.md b/docsite/docs/configuration/sources/_env_configs/_maloja.md new file mode 100644 index 00000000..86594513 --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_maloja.md @@ -0,0 +1,7 @@ +| Environmental Variable | Type | Default | Description | +| --------------------------- | ------- | --------------------------- | ------------------------- | +| **`SOURCE_MALOJA_ID`** | string | | A globally unique ID | +| `SOURCE_MALOJA_NAME` | string | Value of `SOURCE_MALOJA_ID` | A vanity name | +| `SOURCE_MALOJA_ENABLE` | boolean | | | +| **`SOURCE_MALOJA_URL`** | string | | URL for maloja server | +| **`SOURCE_MALOJA_API_KEY`** | string | | API Key for Maloja server | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_mopidy.md b/docsite/docs/configuration/sources/_env_configs/_mopidy.md new file mode 100644 index 00000000..97e74009 --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_mopidy.md @@ -0,0 +1,9 @@ +| Environmental Variable | Type | Default | Description | +| ----------------------- | --------- | ------------------------------ | ------------------------------------------------------------------------------------------ | +| **`MOPIDY_ID`** | string | | A globally unique ID | +| `MOPIDY_NAME` | string | Value of `MOPIDY_ID` | A vanity name | +| `MOPIDY_ENABLE` | boolean | | | +| `MOPIDY_URL` | string | ws://localhost:6680/mopidy/ws/ | URL of the Mopidy HTTP server to connect to | +| `MOPIDY_URI_DENYLIST` | transform | | Do not scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive | +| `MOPIDY_URI_ALLOWLIST` | transform | | Only scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive | +| `MOPIDY_ALBUM_DENYLIST` | transform | Soundcloud | Remove album data that matches any case-insensitive string from this list when scrobbling, | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_mpd.md b/docsite/docs/configuration/sources/_env_configs/_mpd.md new file mode 100644 index 00000000..4273b795 --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_mpd.md @@ -0,0 +1,7 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ----------------- | -------------------------------------------------------------------------------------------------------- | +| **`MPD_ID`** | string | | A globally unique ID | +| `MPD_NAME` | string | Value of `MPD_ID` | A vanity name | +| `MPD_ENABLE` | boolean | | | +| `MPD_URL` | string | localhost:6600 | URL:PORT of the MPD server to connect to | +| `MPD_PASSWORD` | string | | Password for the server, if set https://mpd.readthedocs.io/en/stable/user.html#permissions-and-passwords | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_mpris.md b/docsite/docs/configuration/sources/_env_configs/_mpris.md new file mode 100644 index 00000000..2b467d84 --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_mpris.md @@ -0,0 +1,7 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ------------------- | ------------------------------------------------------------------------------- | +| **`MPRIS_ID`** | string | | A globally unique ID | +| `MPRIS_NAME` | string | Value of `MPRIS_ID` | A vanity name | +| `MPRIS_ENABLE` | boolean | | | +| `MPRIS_BLACKLIST` | union | | DO NOT scrobble from any players that START WITH these values, case-insensitive | +| `MPRIS_WHITELIST` | union | | ONLY from any players that START WITH these values, case-insensitive | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_musiccast.md b/docsite/docs/configuration/sources/_env_configs/_musiccast.md new file mode 100644 index 00000000..aad4dbfb --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_musiccast.md @@ -0,0 +1,6 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ------------------- | ------------------------------------------------------------ | +| **`MCAST_ID`** | string | | A globally unique ID | +| `MCAST_NAME` | string | Value of `MCAST_ID` | A vanity name | +| `MCAST_ENABLE` | boolean | | | +| **`MCAST_URL`** | string | | The host or URL of the YamahaExtendedControl endpoint to use | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_musikcube.md b/docsite/docs/configuration/sources/_env_configs/_musikcube.md new file mode 100644 index 00000000..36facc1f --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_musikcube.md @@ -0,0 +1,7 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ------------------- | -------------------------------------------------------------------------------------------- | +| **`MC_ID`** | string | | A globally unique ID | +| `MC_NAME` | string | Value of `MC_ID` | A vanity name | +| `MC_ENABLE` | boolean | | | +| `MC_URL` | string | ws://localhost:7905 | URL of the Musikcube Websocket (Metadata) server to connect to | +| **`MC_PASSWORD`** | string | | Password set in Musikcube https://github.com/clangen/musikcube/wiki/remote-api-documentation | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_plex.md b/docsite/docs/configuration/sources/_env_configs/_plex.md new file mode 100644 index 00000000..1b70457f --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_plex.md @@ -0,0 +1,13 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ------------------ | ------------------------------------------------------------------------------------------------ | +| **`PLEX_ID`** | string | | A globally unique ID | +| `PLEX_NAME` | string | Value of `PLEX_ID` | A vanity name | +| `PLEX_ENABLE` | boolean | | | +| **`PLEX_URL`** | string | | http(s)://HOST:PORT of the Plex server to connect to | +| `PLEX_TOKEN` | string | | | +| `PLEX_USERS_ALLOW` | union | | Only scrobble for specific users (case-insensitive) | +| `PLEX_USERS_BLOCK` | union | | Do not scrobble for these users (case-insensitive) | +| `PLEX_DEVICES_ALLOW` | union | | Only scrobble if device or application name contains strings from this list (case-insensitive) | +| `PLEX_DEVICES_BLOCK` | union | | Do not scrobble if device or application name contains strings from this list (case-insensitive) | +| `PLEX_LIBRARIES_ALLOW` | union | | Only scrobble if library name contains string from this list (case-insensitive) | +| `PLEX_LIBRARIES_BLOCK` | union | | Do not scrobble if library name contains strings from this list (case-insensitive) | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_rocksky.md b/docsite/docs/configuration/sources/_env_configs/_rocksky.md new file mode 100644 index 00000000..b6c7a6e6 --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_rocksky.md @@ -0,0 +1,7 @@ +| Environmental Variable | Type | Default | Description | +| --------------------------- | ------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| **`SOURCE_ROCKSKY_ID`** | string | | A globally unique ID | +| `SOURCE_ROCKSKY_NAME` | string | Value of `SOURCE_ROCKSKY_ID` | A vanity name | +| `SOURCE_ROCKSKY_ENABLE` | boolean | | | +| `SOURCE_ROCKSKY_KEY` | string | | API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account | +| **`SOURCE_ROCKSKY_HANDLE`** | string | | The **fully-qualified** handle for your ATPRoto/Bluesky account, like: | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_sonos.md b/docsite/docs/configuration/sources/_env_configs/_sonos.md new file mode 100644 index 00000000..7fa1f56c --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_sonos.md @@ -0,0 +1,10 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| **`SONOS_ID`** | string | | A globally unique ID | +| `SONOS_NAME` | string | Value of `SONOS_ID` | A vanity name | +| `SONOS_ENABLE` | boolean | | | +| **`SONOS_HOST`** | string | | IP address of any connected Sonos speaker or device | +| `SONOS_DEVICES_ALLOW` | union | | Only scrobble if device name contains strings from this list (case-insensitive) | +| `SONOS_DEVICES_BLOCK` | union | | Do not scrobble if device name contains strings from this list (case-insensitive) | +| `SONOS_GROUPS_ALLOW` | union | | Only scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive) | +| `SONOS_GROUPS_BLOCK` | union | | Do not scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive) | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_spotify.md b/docsite/docs/configuration/sources/_env_configs/_spotify.md new file mode 100644 index 00000000..e547ed8b --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_spotify.md @@ -0,0 +1,8 @@ +| Environmental Variable | Type | Default | Description | +| --------------------------- | ------- | ------------------------------ | -------------------------------------------------------------------- | +| **`SPOTIFY_ID`** | string | | A globally unique ID | +| `SPOTIFY_NAME` | string | Value of `SPOTIFY_ID` | A vanity name | +| `SPOTIFY_ENABLE` | boolean | | | +| **`SPOTIFY_CLIENT_ID`** | string | | spotify client id | +| **`SPOTIFY_CLIENT_SECRET`** | string | | spotify client secret | +| `SPOTIFY_REDIRECT_URI` | string | http://localhost:9078/callback | spotify redirect URI -- required only if not the default shown here. | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_subsonic.md b/docsite/docs/configuration/sources/_env_configs/_subsonic.md new file mode 100644 index 00000000..3d40e633 --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_subsonic.md @@ -0,0 +1,8 @@ +| Environmental Variable | Type | Default | Description | +| ----------------------- | ------- | ---------------------- | ------------------------------------------------- | +| **`SUBSONIC_ID`** | string | | A globally unique ID | +| `SUBSONIC_NAME` | string | Value of `SUBSONIC_ID` | A vanity name | +| `SUBSONIC_ENABLE` | boolean | | | +| **`SUBSONIC_USER`** | string | | Username to login to the server with | +| **`SUBSONIC_PASSWORD`** | string | | Password for the user to login to the server with | +| **`SUBSONIC_URL`** | string | | URL of the subsonic media server to query | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_tealfm.md b/docsite/docs/configuration/sources/_env_configs/_tealfm.md new file mode 100644 index 00000000..80406cb0 --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_tealfm.md @@ -0,0 +1,7 @@ +| Environmental Variable | Type | Default | Description | +| ------------------------------ | ------- | --------------------------- | -------------------------------- | +| **`SOURCE_TEALFM_ID`** | string | | A globally unique ID | +| `SOURCE_TEALFM_NAME` | string | Value of `SOURCE_TEALFM_ID` | A vanity name | +| `SOURCE_TEALFM_ENABLE` | boolean | | | +| **`SOURCE_TEALFM_IDENTIFIER`** | string | | Identify the account to login as | +| `SOURCE_TEALFM_APP_PW` | string | | | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_vlc.md b/docsite/docs/configuration/sources/_env_configs/_vlc.md new file mode 100644 index 00000000..01124b45 --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_vlc.md @@ -0,0 +1,7 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ----------------- | ---------------------------------------- | +| **`VLC_ID`** | string | | A globally unique ID | +| `VLC_NAME` | string | Value of `VLC_ID` | A vanity name | +| `VLC_ENABLE` | boolean | | | +| `VLC_URL` | string | localhost:8080 | URL:PORT of the VLC server to connect to | +| **`VLC_PASSWORD`** | string | | Password for the server | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_webscrobbler.md b/docsite/docs/configuration/sources/_env_configs/_webscrobbler.md new file mode 100644 index 00000000..3d36300c --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_webscrobbler.md @@ -0,0 +1,7 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | --------- | ---------------- | ----------------------------------------------------------- | +| **`WS_ID`** | string | | A globally unique ID | +| `WS_NAME` | string | Value of `WS_ID` | A vanity name | +| `WS_ENABLE` | boolean | | | +| `WS_BLACKLIST` | transform | | Block scrobbling from specific WebScrobbler Connectors | +| `WS_WHITELIST` | transform | | Only allow scrobbling from specific WebScrobbler Connectors | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_ymbridge.md b/docsite/docs/configuration/sources/_env_configs/_ymbridge.md new file mode 100644 index 00000000..2c73caec --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_ymbridge.md @@ -0,0 +1,7 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ---------------------- | --------------------------------------------------------------------------- | +| **`YMBRIDGE_ID`** | string | | A globally unique ID | +| `YMBRIDGE_NAME` | string | Value of `YMBRIDGE_ID` | A vanity name | +| `YMBRIDGE_ENABLE` | boolean | | | +| **`YMBRIDGE_URL`** | string | | URL of the local Python bridge, for example http://yandex-music-bridge:9980 | +| `YMBRIDGE_API_KEY` | string | | Optional API key sent as X-API-Key to the bridge | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_ytmusic.md b/docsite/docs/configuration/sources/_env_configs/_ytmusic.md new file mode 100644 index 00000000..f6a336a4 --- /dev/null +++ b/docsite/docs/configuration/sources/_env_configs/_ytmusic.md @@ -0,0 +1,9 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ----------------- | ------------------------------------------------------------------------------------ | +| **`YTM_ID`** | string | | A globally unique ID | +| `YTM_NAME` | string | Value of `YTM_ID` | A vanity name | +| `YTM_ENABLE` | boolean | | | +| `YTM_REDIRECT_URI` | string | | Google Cloud Console project OAuth Client Authorized redirect URI | +| `YTM_CLIENT_ID` | string | | Google Cloud Console project OAuth Client ID | +| `YTM_CLIENT_SECRET` | string | | Google Cloud Console project OAuth Client Secret | +| `YTM_COOKIE` | string | | The cookie retrieved from the Request Headers of music.youtube.com after logging in. | \ No newline at end of file diff --git a/src/backend/common/infrastructure/config/source/azuracast.ts b/src/backend/common/infrastructure/config/source/azuracast.ts index 0bff578f..964bcfa4 100644 --- a/src/backend/common/infrastructure/config/source/azuracast.ts +++ b/src/backend/common/infrastructure/config/source/azuracast.ts @@ -141,7 +141,7 @@ const envDataSchema = z.object({ AZURA_STATION: azuracastDataSchema.shape.station, AZURA_URL: azuracastDataSchema.shape.url, AZURA_KEY: azuracastDataSchema.shape.apiKey, - AZURA_LISTENERS_NUM: z.string().optional().transform((val) => !isNaN(Number.parseInt(val)) ? Number.parseInt(val) : parseBoolStrict(val)), + AZURA_LISTENERS_NUM: z.string().optional().transform((val) => val === undefined ? undefined : (!isNaN(Number.parseInt(val)) ? Number.parseInt(val) : parseBoolStrict(val))), AZURA_LIVE: z.stringbool().optional() }); diff --git a/src/backend/utils/SchemaDocsStaticUtils.ts b/src/backend/utils/SchemaDocsStaticUtils.ts index bc4c490d..44ffd3d0 100644 --- a/src/backend/utils/SchemaDocsStaticUtils.ts +++ b/src/backend/utils/SchemaDocsStaticUtils.ts @@ -5,21 +5,13 @@ import { clientConfigSchemaMap } from '../common/infrastructure/config/client/cl import { sourceConfigSchemaMap } from '../common/infrastructure/config/source/sources.ts'; import { projectRootDir } from "../common/infrastructure/Atomic.ts"; import { aioConfigSchema } from "../common/infrastructure/config/aioConfig.ts"; -import { envSchemas } from "../common/infrastructure/config/client/maloja.ts"; import { generateCommonComponentEnvConfigSchema } from "../common/infrastructure/config/common.ts"; -import { zodObjectToTableColumns } from "./ZodUtils.ts"; +import { zodObjectToTableColumns, type TableColumn } from "./ZodUtils.ts"; import {markdownTable} from 'markdown-table' mkdirSync(resolve(projectRootDir, 'docsite/static/schemas'), {recursive: true}); -const common = generateCommonComponentEnvConfigSchema('MALOJA'); -const schema = z.object({ - ...common.shape, - ...envSchemas.env.shape -}); -const col = zodObjectToTableColumns(schema, 'out'); - -const mdCols: string[][] = col.map((x) => { +const mdCols = (cols: TableColumn[]): string[][] => cols.map((x) => { const name = `\`${x.title}\``; return [ x.required ? `**${name}**` : name, @@ -27,13 +19,7 @@ const mdCols: string[][] = col.map((x) => { (x.default ?? '').toString(), x.description ?? '' ] -}); -const tableContent = markdownTable([ - ['Environmental Variable', 'Type', 'Default', 'Description'], - ...mdCols -]); - -writeFileSync(resolve(projectRootDir, `docsite/static/schemas/maloja-env-test.md`), tableContent); +}) const generateSchema = (schema: z.ZodType, reused: z.core.ToJSONSchemaParams['reused'] = 'inline') => z.toJSONSchema(schema, { io: "input", @@ -70,11 +56,29 @@ const generateSchema = (schema: z.ZodType, reused: z.core.ToJSONSchemaParams['re const clientEntries = Object.entries(clientConfigSchemaMap); for(const [k,v] of clientEntries) { writeFileSync(resolve(projectRootDir, `docsite/static/schemas/${k}-client.json`), JSON.stringify(generateSchema(z.array(v[0])))); + + const envSchema = v[2]; + const common = generateCommonComponentEnvConfigSchema(envSchema.prefix.toUpperCase()); + const col = zodObjectToTableColumns(z.object({...common.shape,...v[2].env.shape}), 'out'); + const tableContent = markdownTable([ + ['Environmental Variable', 'Type', 'Default', 'Description'], + ...mdCols(col) + ]); + writeFileSync(resolve(projectRootDir, `docsite/docs/configuration/clients/_env_configs/_${k}.md`), tableContent); } const sourcesEntries = Object.entries(sourceConfigSchemaMap); for(const [k,v] of sourcesEntries) { writeFileSync(resolve(projectRootDir, `docsite/static/schemas/${k}-source.json`), JSON.stringify(generateSchema(z.array(v[0])))); + + const envSchema = v[2]; + const common = generateCommonComponentEnvConfigSchema(envSchema.prefix.toUpperCase()); + const col = zodObjectToTableColumns(z.object({...common.shape,...v[2].env.shape}), 'out'); + const tableContent = markdownTable([ + ['Environmental Variable', 'Type', 'Default', 'Description'], + ...mdCols(col) + ]); + writeFileSync(resolve(projectRootDir, `docsite/docs/configuration/sources/_env_configs/_${k}.md`), tableContent); } writeFileSync(resolve(projectRootDir, 'docsite/static/schemas/aio.json'), JSON.stringify(generateSchema(aioConfigSchema, 'ref'))); -- 2.51.2 From 513e2dde151e0371ca0bdef6126543e1a14e8a5d Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 16:37:59 -0400 Subject: [PATCH 15/35] feat: Use env config generated from schema --- docsite/docs/configuration/clients/koito.mdx | 9 ++------- docsite/docs/configuration/clients/lastfm.mdx | 10 ++-------- docsite/docs/configuration/clients/librefm.mdx | 12 ++---------- .../configuration/clients/listenbrainz.mdx | 9 ++------- docsite/docs/configuration/clients/tealfm.mdx | 8 ++------ .../docs/configuration/sources/applemusic.mdx | 15 ++------------- .../docs/configuration/sources/azuracast.mdx | 10 ++-------- docsite/docs/configuration/sources/deezer.mdx | 5 ++--- .../docs/configuration/sources/google-cast.mdx | 11 ++--------- docsite/docs/configuration/sources/icecast.mdx | 7 ++----- .../docs/configuration/sources/jellyfin.mdx | 18 ++---------------- docsite/docs/configuration/sources/jriver.mdx | 9 ++------- docsite/docs/configuration/sources/kodi.mdx | 9 ++------- .../configuration/sources/koito-source.mdx | 9 ++------- .../configuration/sources/lastfm-endpoint.mdx | 8 ++------ .../configuration/sources/lastfm-source.mdx | 10 ++-------- .../configuration/sources/librefm-source.mdx | 12 ++---------- .../sources/listenbrainz-endpoint.mdx | 10 ++-------- .../sources/listenbrainz-source.mdx | 9 ++------- docsite/docs/configuration/sources/maloja.mdx | 8 ++------ docsite/docs/configuration/sources/mopidy.mdx | 3 ++- docsite/docs/configuration/sources/mpd.mdx | 8 ++------ docsite/docs/configuration/sources/mpris.mdx | 9 ++------- .../docs/configuration/sources/musikcube.mdx | 8 ++------ docsite/docs/configuration/sources/plex.mdx | 14 ++------------ .../configuration/sources/rocksky-source.mdx | 8 ++------ docsite/docs/configuration/sources/sonos.mdx | 11 ++--------- docsite/docs/configuration/sources/spotify.mdx | 9 ++------- .../docs/configuration/sources/subsonic.mdx | 9 ++------- .../configuration/sources/tealfm-source.mdx | 8 ++------ docsite/docs/configuration/sources/vlc.mdx | 8 ++------ .../configuration/sources/webscrobbler.mdx | 9 ++------- .../configuration/sources/yamaha-musiccast.mdx | 7 ++----- .../configuration/sources/yandex-music.mdx | 8 ++------ .../configuration/sources/youtube-music.mdx | 11 ++--------- 35 files changed, 70 insertions(+), 258 deletions(-) diff --git a/docsite/docs/configuration/clients/koito.mdx b/docsite/docs/configuration/clients/koito.mdx index 78e440d4..5c9d93e1 100644 --- a/docsite/docs/configuration/clients/koito.mdx +++ b/docsite/docs/configuration/clients/koito.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import JsonConfig from '!!raw-loader!@site/../config/koito.json.example'; +import ENVConfig from "@site/docs/configuration/clients/_env_configs/_koito.md" :::tip[Other Uses] @@ -52,11 +53,5 @@ For Koito versions `<=0.1.7`, you must have the environment variable `KOITO_ALLO ## Configuration - | Environmental Variable | Required? | Default | Description | - | :--------------------- | --------- | ------- | --------------------------------- | - | `KOITO_ID` | Yes | | A unique id | - | `KOITO_TOKEN` | Yes | | API Key from your Koito Account | - | `KOITO_USER` | Yes | | Your Koito username | - | `KOITO_URL` | No | | The base URL for the Koito server | - | `KOITO_NAME` | No | | A vanity name, different than ID | + diff --git a/docsite/docs/configuration/clients/lastfm.mdx b/docsite/docs/configuration/clients/lastfm.mdx index 4f3df2d1..0c1f2103 100644 --- a/docsite/docs/configuration/clients/lastfm.mdx +++ b/docsite/docs/configuration/clients/lastfm.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import JsonConfig from '!!raw-loader!@site/../config/lastfm.json.example'; +import ENVConfig from "@site/docs/configuration/clients/_env_configs/_lastfm.md" :::tip[Other Users] @@ -63,12 +64,5 @@ The Redirect URL can be explicitly specified in the configuration below, or by u ## Configuration - | Environmental Variable | Required? | Default | Description | - | ---------------------- | --------- | --------------------------------------- | ----------------------------------------------------------------------------- | - | `LASTFM_ID` | Yes | | A unique id | - | `LASTFM_API_KEY` | Yes | | Api Key from your API Account | - | `LASTFM_SECRET` | Yes | | Shared secret from your API Account | - | `LASTFM_REDIRECT_URI` | No | `http://localhost:9078/lastfm/callback` | Url to use for authentication. Must include `lastfm/callback` somewhere in it | - | `LASTFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. | - | `LASTFM_NAME` | No | | A vanity name, different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/clients/librefm.mdx b/docsite/docs/configuration/clients/librefm.mdx index 3c5b2e26..a3e37a82 100644 --- a/docsite/docs/configuration/clients/librefm.mdx +++ b/docsite/docs/configuration/clients/librefm.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import JsonConfig from '!!raw-loader!@site/../config/librefm.json.example'; +import ENVConfig from "@site/docs/configuration/clients/_env_configs/_librefm.md" :::tip[Other Users] @@ -54,14 +55,5 @@ https://my.cool.libre.com/2.0/ ## Configuration - | Environmental Variable | Required? | Default | Description | - | ---------------------- | --------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------ | - | `LIBREFM_ID` | Yes | | A unique ID | - | `LIBREFM_ENABLE` | No | | Set to `true` to enable Libre.fm, if not using any of the below variables | - | `LIBREFM_API_KEY` | No | `anything` | Optional API Key | - | `LIBREFM_SECRET` | No | `anything` | Optional shared secret | - | `LIBREFM_REDIRECT_URI` | No | `http://localhost:9078/librefm/callback` | Url to use for authentication. Must include `librefm/callback` somewhere in it | - | `LIBREFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. | - | `LIBREFM_URLBASE` | No | | Use for custom Libre.fm instance only. The host + path prefix EX `https://my.libre.example/2.0/` | - | `LIBREFM_NAME` | No | | A vanity name different than ID | + diff --git a/docsite/docs/configuration/clients/listenbrainz.mdx b/docsite/docs/configuration/clients/listenbrainz.mdx index 9629d208..7360300e 100644 --- a/docsite/docs/configuration/clients/listenbrainz.mdx +++ b/docsite/docs/configuration/clients/listenbrainz.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import JsonConfig from '!!raw-loader!@site/../config/listenbrainz.json.example'; +import ENVConfig from "@site/docs/configuration/clients/_env_configs/_listenbrainz.md" :::tip[Other Users] @@ -23,11 +24,5 @@ On your [profile page](https://listenbrainz.org/profile/) find your **User Token ## Configuration - | Environmental Variable | Required? | Default | Description | - | ---------------------- | --------- | ----------------------------- | ------------------------------- | - | `LZE_ID` | Yes | | A unique ID | - | `LZ_TOKEN` | Yes | | User token from your LZ profile | - | `LZ_USER` | Yes | | Your LZ username | - | `LZ_URL` | No | https://api.listenbrainz.org/ | The base URL for the LZ server | - | `LZ_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/clients/tealfm.mdx b/docsite/docs/configuration/clients/tealfm.mdx index f1879d6a..6ce725b9 100644 --- a/docsite/docs/configuration/clients/tealfm.mdx +++ b/docsite/docs/configuration/clients/tealfm.mdx @@ -9,6 +9,7 @@ import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import JsonConfig from '!!raw-loader!@site/../config/tealfm.json.example'; import Bluesky from "@site/src/components/snippets/_bluesky.mdx"; +import ENVConfig from "@site/docs/configuration/clients/_env_configs/_tealfm.md" :::tip[Other Uses] @@ -78,10 +79,5 @@ If using a handle: If your identifier does not look like a DID and does not include a TLD then multi-scrobbler will automatically append `.bsky.social` to your handle value. - | Environmental Variable | Required? | Default | Description | - | :--------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ | - | `TEAMFM_ID` | Yes | | A unique ID | - | `TEALFM_IDENTIFIER` | Yes | | Your **full** ATProto handle or DID. For Bluesky account this is like `myUser.bsky.social` | - | `TEALFM_APP_PW` | Yes | | Bluesky/ATProto network App Password | - | `TEALFM_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/applemusic.mdx b/docsite/docs/configuration/sources/applemusic.mdx index e3758c13..6cb6a23e 100644 --- a/docsite/docs/configuration/sources/applemusic.mdx +++ b/docsite/docs/configuration/sources/applemusic.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import AppleMusicConfig from '!!raw-loader!@site/../config/applemusic.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_applemusic.md" This Source **monitors your Apple Music listening history** via the official Apple Music API and scrobbles new activity to your configured [Clients](/configuration/clients). @@ -246,17 +247,5 @@ Here is an example of creating a User Stage to only strip suffixes for a specifi ## Configuration Reference - | Environment Variable | Required? | Default | Description | - | ------------------------------------------ | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | `APPLEMUSIC_ID` | Yes | | A unique ID for this source. | - | `APPLEMUSIC_MEDIA_USER_TOKEN` | Yes | | The media-user-token extracted from the browser. | - | `APPLEMUSIC_KEY_ID` | No | | Key ID from your MusicKit key. | - | `APPLEMUSIC_TEAM_ID` | No | | Team ID from your Apple Developer account. | - | `APPLEMUSIC_KEY_P8` | No | | The contents of your MusicKit `.p8` private key file. | - | `APPLEMUSIC_TOKEN` | No | | The authentication JWT extracted from the browser. | - | `APPLEMUSIC_INTERVAL` | No | `60` | Polling interval in seconds. | - | `APPLEMUSIC_ORIGIN_HEADER` | No | | Origin header for API requests, e.g. `https://music.apple.com`. Required when using a browser token. | - | `APPLEMUSIC_RECOVER_UNCHANGED_TOP_HISTORY` | No | `true` | Apple Music deduplicates its history by bumping re-played tracks to the top. If you listen to Song A → Song B → Song A, the history changes from `[A]` to `[A, B]` (bumping A). MS detects this "top-rebound" to recover the intermediate play (B) and the re-listen (A). Disable this only if you see incorrect duplicate scrobbles. | - | `APPLEMUSIC_NORMALIZE_ALBUM` | No | `true` | Strips extraneous `- EP` and `- Single` suffixes from album names | - | `APPLEMUSIC_NAME` | No | | A vanity name different than the ID. | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/azuracast.mdx b/docsite/docs/configuration/sources/azuracast.mdx index 7138317c..7a012cfa 100644 --- a/docsite/docs/configuration/sources/azuracast.mdx +++ b/docsite/docs/configuration/sources/azuracast.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import AzuracastConfig from '!!raw-loader!@site/../config/azuracast.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_azuracast.md" import ShouldScrobble from "@site/src/components/snippets/_should-scrobble-details.mdx" :::tip[Scrobbling Threshold] @@ -112,12 +113,5 @@ For users who aren't administrators of Azuracast: if you can access the station ## Configuration - | Environmental Variable | Required? | Default | Description | - | :--------------------- | :-------- | :------ | ---------------------------------------------------------------------------------------------- | - | `AZURA_ID` | Yes | | A unique ID | - | `AZURA_URL` | Yes | | Azuracast URL *without station name* | - | `AZURA_STATION` | Yes | | The station URL Stub or shortcode | - | `AZURA_LIVE` | No | Yes | Only scrobble when station status is ONLINE | - | `AZURA_LISTENERS_NUM` | No | `true` | Only scrobble if station has any listeners (`true`) or listeners are equal-to/greater-than `X` | - | `AZURA_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/deezer.mdx b/docsite/docs/configuration/sources/deezer.mdx index 51acedda..a690833e 100644 --- a/docsite/docs/configuration/sources/deezer.mdx +++ b/docsite/docs/configuration/sources/deezer.mdx @@ -9,6 +9,7 @@ import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import DeezerConfig from '!!raw-loader!@site/../config/deezer.json.example'; import DeezerDeprecatedConfig from '!!raw-loader!@site/../config/deezer-deprecated.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_deezer.md" @@ -134,9 +135,7 @@ import DeezerDeprecatedConfig from '!!raw-loader!@site/../config/deezer-deprecat ## Configuration - | Environmental Variable | Required? | Default | Description | - | :--------------------- | --------- | ------- | ------------------------------------------------ | - | `DEEZER_ARL` | Yes | | The ARL cookie value retrieved from your browser | + diff --git a/docsite/docs/configuration/sources/google-cast.mdx b/docsite/docs/configuration/sources/google-cast.mdx index 346e1f98..72622cfb 100644 --- a/docsite/docs/configuration/sources/google-cast.mdx +++ b/docsite/docs/configuration/sources/google-cast.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import ChromecastConfig from '!!raw-loader!@site/../config/chromecast.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_chromecast.md" If your media device can be **Cast** to using this button ![Chromecast Icon](https://upload.wikimedia.org/wikipedia/commons/2/26/Chromecast_cast_button_icon.svg) on your phone/computer then multi-scrobbler can monitor it in order to scrobble music you play. @@ -149,13 +150,5 @@ To diagnose bad/incomplete track information or strange MS player behavior pleas [Manually configuring cast device connections](#connecting-devices) is only available through file-based config. ::: - | Environmental Variable | Required? | Default | Description | - | ---------------------- | --------- | ------- | ------------------------------------------------------------------------------------ | - | `CC_ID` | Yes | | A unique ID | - | `CC_ENABLE` | No | | Set to 'true' to enable Cast monitoring without needing to define other ENVs | - | `CC_WHITELIST_DEVICES` | No | | Only scrobble from these Cast devices. Comma-delimited list. EX mini-home, family-tv | - | `CC_BLACKLIST_DEVICES` | No | | Do not scrobble from these Cast devices. Comma-delimited list | - | `CC_WHITELIST_APPS` | No | | Only scrobble from these casted Apps. Comma-delimited list. EX spotify, pandora | - | `CC_BLACKLIST_APPS` | No | | Do not scrobble from these casted Apps. Comma-delimited list | - | `CC_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/icecast.mdx b/docsite/docs/configuration/sources/icecast.mdx index 94f72462..5f4dfbab 100644 --- a/docsite/docs/configuration/sources/icecast.mdx +++ b/docsite/docs/configuration/sources/icecast.mdx @@ -9,6 +9,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import IcecastConfig from '!!raw-loader!@site/../config/icecast.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_icecast.md" import ShouldScrobble from "@site/src/components/snippets/_should-scrobble-details.mdx" Parse metadata from any Icecast Station (streams) to use for scrobbling. @@ -64,9 +65,5 @@ If no `sources` are configured for multi-scrobbler it will automatically try to ## Configuration - | Environmental Variable | Required? | Default | Description | - | :--------------------- | :-------- | :------ | ------------------------------- | - | `ICECAST_ID` | Yes | | A unique ID | - | `ICECAST_URL` | Yes | | Icecast URL | - | `ICECAST_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/jellyfin.mdx b/docsite/docs/configuration/sources/jellyfin.mdx index 376b4dd9..8b9ff1b2 100644 --- a/docsite/docs/configuration/sources/jellyfin.mdx +++ b/docsite/docs/configuration/sources/jellyfin.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import JellyfinConfig from '!!raw-loader!@site/../config/jellyfin.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_jellyfin.md" Must be using Jellyfin 10.7 or greater @@ -52,20 +53,5 @@ Use `JELLYFIN_FRONTEND_URL_OVERRIDE=https://jellyfin.mydomain.com` in order for #### Configuration - | Environmental Variable | Required? | Default | Description | - | -------------------------------- | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | `JELLYFIN_ID` | **Yes** | | A unique ID | - | `JELLYFIN_URL` | **Yes** | | The URL of the Jellyfin server IE `http://localhost:8096` | - | `JELLYFIN_USER` | **Yes** | | The user to authenticate with the API | - | `JELLYFIN_APIKEY` | No | | The API Key to use for authentication **(Must provide either apikey or password)** | - | `JELLYFIN_PASSWORD` | No | | The password of the user to authenticate for. **(Must provide either apikey or password)** | - | `JELLYFIN_USERS_ALLOW` | No | | Comma-separated list of usernames (from Jellyfin) to scrobble for | - | `JELLYFIN_USERS_BLOCK` | No | | Comma-separated list of usernames (from Jellyfin) to disallow scrobble for | - | `JELLYFIN_DEVICES_ALLOW` | No | | Comma-separated list of devices to scrobble from | - | `JELLYFIN_DEVICES_BLOCK` | No | | Comma-separated list of devices to disallow scrobbles from | - | `JELLYFIN_LIBRARIES_ALLOW` | No | | Comma-separated list of libraries to allow scrobbles from | - | `JELLYFIN_LIBRARIES_BLOCK` | No | | Comma-separated list of libraries to disallow scrobbles from | - | `JELLYFIN_MEDIATYPES_ALLOW` | No | | Comma-separated list of [media types](https://github.com/jellyfin/jellyfin-sdk-typescript/blob/master/src/generated-client/models/media-type.ts#L22) to allow scrobbling. Defaults to `audio` | - | `JELLYFIN_FRONTEND_URL_OVERRIDE` | No | | The external URL to use for album art assets instead of `JELLYFIN_URL` | - | `JELLYFIN_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/jriver.mdx b/docsite/docs/configuration/sources/jriver.mdx index 69168e82..e811094d 100644 --- a/docsite/docs/configuration/sources/jriver.mdx +++ b/docsite/docs/configuration/sources/jriver.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import JriverConfig from '!!raw-loader!@site/../config/jriver.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_jriver.md" In order for multi-scrobbler to communicate with JRiver you must have [Web Server Interface](https://wiki.jriver.com/index.php/Web_Service_Interface#Documentation_of_Functions) enabled. This can be found in the JRiver GUI: @@ -50,11 +51,5 @@ If any part of this URL is missing multi-scrobbler will use a default value. Thi ## Configuration - | Environmental Variable | Required | Default | Description | - | ---------------------- | -------- | ------------------------------- | ---------------------------------------------- | - | JRIVER_ID | Yes | | A unique ID | - | JRIVER_URL | Yes | http://localhost:52199/MCWS/v1/ | The URL of the JRiver server | - | JRIVER_USERNAME | No | | If authentication is enabled, the username set | - | JRIVER_PASSWORD | No | | If authenticated is enabled, the password set | - | JRIVER_NAME | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/kodi.mdx b/docsite/docs/configuration/sources/kodi.mdx index 1dba71d5..68771b85 100644 --- a/docsite/docs/configuration/sources/kodi.mdx +++ b/docsite/docs/configuration/sources/kodi.mdx @@ -9,6 +9,7 @@ import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import KodiConfig from '!!raw-loader!@site/../config/kodi.json.example'; import Config from '@site/src/components/GenericConfiguration.mdx'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_kodi.md" In order for multi-scrobbler to communicate with Kodi you must have the [Web Interface](https://kodi.wiki/view/Web_interface) enabled. This can be done in the Kodi GUI: @@ -51,11 +52,5 @@ If any part of this URL is missing multi-scrobbler will use a default value. Thi ## Configuration - | Environmental Variable | Required | Default | Description | - | ---------------------- | -------- | ----------------------------- | ------------------------------- | - | KODI_ID | Yes | | A unique ID | - | KODI_URL | Yes | http://localhost:8080/jsonrpc | The URL of the Kodi server | - | KODI_USERNAME | No | | The username set | - | KODI_PASSWORD | No | | The password set | - | KODI_NAME | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/koito-source.mdx b/docsite/docs/configuration/sources/koito-source.mdx index 4e724d21..7c8df031 100644 --- a/docsite/docs/configuration/sources/koito-source.mdx +++ b/docsite/docs/configuration/sources/koito-source.mdx @@ -9,6 +9,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import KoitoConfig from '!!raw-loader!@site/../config/koito.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_koito.md" This Source monitors a Koito account's scrobble history and then re-scrobbles discovered tracks to configured [Clients.](/configuration/clients) @@ -25,11 +26,5 @@ See the [Koito (Client)](/configuration/clients/koito) configuration for general #### Configuration - | Environmental Variable | Required? | Default | Description | - | :--------------------- | --------- | ------- | --------------------------------- | - | `SOURCE_KOITO_ID` | Yes | | A unique ID | - | `SOURCE_KOITO_TOKEN` | Yes | | API Key from your Koito Account | - | `SOURCE_KOITO_USER` | Yes | | Your Koito username | - | `SOURCE_KOITO_URL` | No | | The base URL for the Koito server | - | `SOURCE_KOITO_NAME` | No | | A vanity name differen than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/lastfm-endpoint.mdx b/docsite/docs/configuration/sources/lastfm-endpoint.mdx index f532f5f8..dc2e77cb 100644 --- a/docsite/docs/configuration/sources/lastfm-endpoint.mdx +++ b/docsite/docs/configuration/sources/lastfm-endpoint.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import LastfmEndpointConfig from '!!raw-loader!@site/../config/endpointlfm.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_endpointlfm.md" This Source enables multi-scrobbler to accept scrobbles from outside applications as if it was a Last.fm server. @@ -39,10 +40,5 @@ http://localhost:9078/api/lastfm/mySlug ## Configuration - | Environmental Variable | Required? | Default | Description | - | :--------------------- | :-------- | ------- | ------------------------------------------------------------------------------------------------------------------ | - | `LFM_ID` | Yes | | A unique ID | - | `LFM_ENABLE` | No | | Use LFM Endpoint as a Source without any other configuration. Only required if slug/token are not provided as ENVs | - | `LFM_SLUG` | No | | (Optional) The URL suffix to use for accepting LFM scrobbles | - | `LFM_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/lastfm-source.mdx b/docsite/docs/configuration/sources/lastfm-source.mdx index ad8d809b..c83077a1 100644 --- a/docsite/docs/configuration/sources/lastfm-source.mdx +++ b/docsite/docs/configuration/sources/lastfm-source.mdx @@ -9,6 +9,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import LastfmConfig from '!!raw-loader!@site/../config/lastfm.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_lastfm.md" This Source **monitors your Last.fm scrobble history** and re-scrobbles new activity to configured [Clients.](/configuration/clients) @@ -24,12 +25,5 @@ To configure this Source use the same registration instructions and configuratio ## Configuration - | Environmental Variable | Required? | Default | Description | - | ---------------------------- | --------- | --------------------------------------- | ----------------------------------------------------------------------------- | - | `SOURCE_LASTFM_ID` | Yes | | A unique ID | - | `SOURCE_LASTFM_API_KEY` | Yes | | Api Key from your API Account | - | `SOURCE_LASTFM_SECRET` | Yes | | Shared secret from your API Account | - | `SOURCE_LASTFM_REDIRECT_URI` | No | `http://localhost:9078/lastfm/callback` | Url to use for authentication. Must include `lastfm/callback` somewhere in it | - | `SOURCE_LASTFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. | - | `SOURCE_LASTFM_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/librefm-source.mdx b/docsite/docs/configuration/sources/librefm-source.mdx index 33da1d75..563d13c8 100644 --- a/docsite/docs/configuration/sources/librefm-source.mdx +++ b/docsite/docs/configuration/sources/librefm-source.mdx @@ -9,6 +9,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import JsonConfig from '!!raw-loader!@site/../config/librefm.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_librefm.md" This Source **monitors your Libre.fm scrobble history** and re-scrobbles new activity to configured [Clients.](/configuration/clients) @@ -23,14 +24,5 @@ To configure this Source use the same registration instructions and configuratio ## Configuration - | Environmental Variable | Required? | Default | Description | - | ----------------------------- | --------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------ | - | `SOURCE_LIBREFM_ID` | Yes | | A unique ID | - | `SOURCE_LIBREFM_ENABLE` | No | | Set to `true` to enable Libre.fm, if not using any of the below variables | - | `SOURCE_LIBREFM_API_KEY` | No | `anything` | Optional API Key | - | `SOURCE_LIBREFM_SECRET` | No | `anything` | Optional shared secret | - | `SOURCE_LIBREFM_REDIRECT_URI` | No | `http://localhost:9078/librefm/callback` | Url to use for authentication. Must include `librefm/callback` somewhere in it | - | `SOURCE_LIBREFM_SESSION` | No | | Session id. Will be generated by authentication flow if not provided. | - | `SOURCE_LIBREFM_URLBASE` | No | | Use for custom Libre.fm instance only. The host + path prefix EX `https://my.libre.example/2.0/` | - | `SOURCE_LIBREFM_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/listenbrainz-endpoint.mdx b/docsite/docs/configuration/sources/listenbrainz-endpoint.mdx index 93306ab7..1aa54eec 100644 --- a/docsite/docs/configuration/sources/listenbrainz-endpoint.mdx +++ b/docsite/docs/configuration/sources/listenbrainz-endpoint.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import ListenbrainzEndpointConfig from '!!raw-loader!@site/../config/endpointlz.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_endpointlz.md" This Source enables multi-scrobbler to accept scrobbles from outside applications **as if it was a Listenbrainz server.** @@ -109,12 +110,5 @@ To troubleshoot any errors, and assuming you are using Home Assistant, view the ## Configuration - | Environmental Variable | Required? | Default | Description | - | :--------------------- | :-------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | - | `LZE_ID` | Yes | | A unique ID | - | `LZE_ENABLE` | No | | Use LZ Endpoint as a Source without any other configuration. Only required if slug/token are not provided as ENVs | - | `LZE_TOKEN` | No | | LZ "Authentication Token" you provided to the scrobbling application | - | `LZE_SLUG` | No | | (Optional) The URL suffix to use for accepting LZ scrobbles | - | `LZE_USERNAME` | No | | (Optional) A fake username that will be returned for token validation and on now-playing responses. If none is provided the Source's name is used instead. | - | `LZE_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/listenbrainz-source.mdx b/docsite/docs/configuration/sources/listenbrainz-source.mdx index a79c3f8d..44c0d26a 100644 --- a/docsite/docs/configuration/sources/listenbrainz-source.mdx +++ b/docsite/docs/configuration/sources/listenbrainz-source.mdx @@ -9,6 +9,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import ListenbrainzConfig from '!!raw-loader!@site/../config/listenbrainz.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_listenbrainz.md" This Source monitors a Listenbrainz account's scrobble history and then re-scrobbles discovered tracks to configured clients. @@ -26,11 +27,5 @@ On your [profile page](https://listenbrainz.org/profile/) find your **User Token ## Configuration - | Environmental Variable | Required? | Default | Description | - | ---------------------- | --------- | ----------------------------- | ------------------------------- | - | `SOURCE_LZ_ID` | Yes | | A unique ID | - | `SOURCE_LZ_TOKEN` | Yes | | User token from your LZ profile | - | `SOURCE_LZ_USER` | Yes | | Your LZ username | - | `SOURCE_LZ_URL` | No | https://api.listenbrainz.org/ | The base URL for the LZ server | - | `SOURCE_LZ_NAME` | No | | A vanity name different than ID |d + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/maloja.mdx b/docsite/docs/configuration/sources/maloja.mdx index 5c2bacd1..db259ee5 100644 --- a/docsite/docs/configuration/sources/maloja.mdx +++ b/docsite/docs/configuration/sources/maloja.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import MalojaConfig from '!!raw-loader!@site/../config/maloja.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_maloja.md" :::tip[Other Uses] @@ -22,10 +23,5 @@ See the [Maloja (Client)](/configuration/clients/maloja) configuration for gener ## Configuration - | Environmental Variable | Required? | Default | Description | - | ----------------------- | --------- | ------- | ------------------------------- | - | `SOURCE_MALOJA_ID` | Yes | | A unique ID | - | `SOURCE_MALOJA_URL` | Yes | | Base URL of your installation | - | `SOURCE_MALOJA_API_KEY` | Yes | | Api Key | - | `SOURCE_MALOJA_NAME` | Yes | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/mopidy.mdx b/docsite/docs/configuration/sources/mopidy.mdx index 6763ec77..43044c52 100644 --- a/docsite/docs/configuration/sources/mopidy.mdx +++ b/docsite/docs/configuration/sources/mopidy.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import MopidyConfig from '!!raw-loader!@site/../config/mopidy.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_mopidy.md" Mopidy is a headless music server that supports playing music from many [standard and non-standard sources such as Pandora, Bandcamp, and Tunein.](https://mopidy.com/ext/) @@ -98,5 +99,5 @@ For certain sources (Soundcloud) Mopidy does not have all track info (Album) and ## Configuration -No ENV support + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/mpd.mdx b/docsite/docs/configuration/sources/mpd.mdx index 79a865d6..07a72b20 100644 --- a/docsite/docs/configuration/sources/mpd.mdx +++ b/docsite/docs/configuration/sources/mpd.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import MPDConfig from '!!raw-loader!@site/../config/mpd.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_mpd.md" MS communicates with MPD using the [TCP client connection.](https://mpd.readthedocs.io/en/stable/user.html#client-connections) @@ -23,10 +24,5 @@ Multi-scrobbler requires only the [`read`](https://mpd.readthedocs.io/en/stable/ ## Configuration - | Environmental Variable | Required? | Default | Description | - | ---------------------- | --------- | ---------------- | ------------------------------- | - | `MPD_ID` | Yes | | A unique ID | - | `MPD_URL` | No | `localhost:6600` | | - | `MPD_PASSWORD` | No | | | - | `MPD_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/mpris.mdx b/docsite/docs/configuration/sources/mpris.mdx index b65b153c..f4eaea46 100644 --- a/docsite/docs/configuration/sources/mpris.mdx +++ b/docsite/docs/configuration/sources/mpris.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import MPRISConfig from '!!raw-loader!@site/../config/mpris.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_mpris.md" [MPRIS](https://specifications.freedesktop.org/mpris-spec/latest) is a standard interface for communicating with Music Players on **linux operating systems.** @@ -71,11 +72,5 @@ MPRIS communication requires multi-scrobbler to have access to the host's dbus-d ## Configuration - | Environmental Variable | Required? | Default | Description | - | ---------------------- | --------- | ------- | -------------------------------------------------------------------------------- | - | MPRIS_ID | Yes | | A unique ID | - | MPRIS_ENABLE | No | | Use MPRIS as a Source (useful when you don't need any other options) | - | MPRIS_BLACKLIST | No | | Comma-delimited list of player names not to scrobble from | - | MPRIS_WHITELIST | No | | Comma-delimited list of players names to ONLY scrobble from. Overrides blacklist | - | MPRIS_NAME | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/musikcube.mdx b/docsite/docs/configuration/sources/musikcube.mdx index 1804236e..abd280a8 100644 --- a/docsite/docs/configuration/sources/musikcube.mdx +++ b/docsite/docs/configuration/sources/musikcube.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import MusikcubeConfig from '!!raw-loader!@site/../config/musikcube.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_musikcube.md" In order to use [Musikcube](https://musikcube.com) configure it to accept [websocket connections](https://github.com/clangen/musikcube/wiki/remote-api-documentation) in **server setup**: @@ -31,10 +32,5 @@ If no URL is provided to MS it will try to use `ws://localhost:7905` ## Configuration - | Environmental Variable | Required? | Default | Description | - | ---------------------- | --------- | --------------------- | ------------------------------------ | - | `MC_ID` | Yes | | A unique ID | - | `MC_URL` | No | `ws://localhost:7905` | Use port set for **metadata server** | - | `MC_PASSWORD` | Yes | | | - | `MC_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/plex.mdx b/docsite/docs/configuration/sources/plex.mdx index 272bab7b..a860daee 100644 --- a/docsite/docs/configuration/sources/plex.mdx +++ b/docsite/docs/configuration/sources/plex.mdx @@ -9,6 +9,7 @@ import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import AzuracastConfig from '!!raw-loader!@site/../config/azuracast.json.example'; import PlexConfig from '!!raw-loader!@site/../config/plex.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_plex.md" Find your [**Plex Token**](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/) and make note of the **URL** and **Port** used to connect to your Plex instance. @@ -194,16 +195,5 @@ To configure MS to use Musicbrainz with your Plex metadata use one of the method #### Configuration - | Environmental Variable | Required? | Default | Description | - | ---------------------- | --------- | ------- | ---------------------------------------------------------------------- | - | `PLEX_ID` | **Yes** | | A unique ID | - | `PLEX_URL` | **Yes** | | The URL of the Plex server IE `http://localhost:32400` | - | `PLEX_TOKEN` | **Yes** | | The **Plex Token** to use with the API | - | `PLEX_USERS_ALLOW` | No | | Comma-separated list of usernames (from Plex) to scrobble for | - | `PLEX_USERS_BLOCK` | No | | Comma-separated list of usernames (from Plex) to disallow scrobble for | - | `PLEX_DEVICES_ALLOW` | No | | Comma-separated list of devices to scrobble from | - | `PLEX_DEVICES_BLOCK` | No | | Comma-separated list of devices to disallow scrobbles from | - | `PLEX_LIBRARIES_ALLOW` | No | | Comma-separated list of libraries to allow scrobbles from | - | `PLEX_LIBRARIES_BLOCK` | No | | Comma-separated list of libraries to disallow scrobbles from | - | `PLEX_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/rocksky-source.mdx b/docsite/docs/configuration/sources/rocksky-source.mdx index ec654c38..f036920a 100644 --- a/docsite/docs/configuration/sources/rocksky-source.mdx +++ b/docsite/docs/configuration/sources/rocksky-source.mdx @@ -9,6 +9,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import JsonConfig from '!!raw-loader!@site/../config/rocksky.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_rocksky.md" :::tip[Other Uses] @@ -33,10 +34,5 @@ See the [Rocksky (Client)](/configuration/clients/rocksky#setup) configuration f * [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"` - | Environmental Variable | Required? | Default | Description | - | :---------------------- | --------- | ------- | ----------------------------------------------------------------------------------- | - | `SOURCE_ROCKSKY_ID` | Yes | | A unique ID | - | `SOURCE_ROCKSKY_HANDLE` | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` | - | `SOURCE_ROCKSKY_KEY` | Yes | | API Key obtained from Rocksky developer dashboard | - | `SOURCE_ROCKSKY_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/sonos.mdx b/docsite/docs/configuration/sources/sonos.mdx index 56bf0b8a..e46e1d23 100644 --- a/docsite/docs/configuration/sources/sonos.mdx +++ b/docsite/docs/configuration/sources/sonos.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import JsonConfig from '!!raw-loader!@site/../config/sonos.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_sonos.md" Multi-scrobbler can use the [Sonos](https://www.sonos.com) [local API](https://docs.sonos.com/docs/control) to monitor what your Sonos system is playing. @@ -90,13 +91,5 @@ The **optional** `devices` and `groups` related properties found in the config b You **must** define the IP address of at least one Sonos/accessory in order for Multi-scrobbler to connect to your Sonos system. - | Environmental Variable | Required? | Default | Description | - | ---------------------- | --------- | ------- | :--------------------------------------------------------------------- | - | `SONOS_ID` | Yes | | A unique ID | - | `SONOS_HOST` | Yes | | The IP address of any active Sonos device/accessory EX `192.168.0.150` | - | `SONOS_DEVICES_ALLOW` | No | | Comma-separated list of Sonos device names to scrobble from | - | `SONOS_DEVICES_BLOCK` | No | | Comma-separated list of Sonos device names to disallow scrobbles from | - | `SONOS_GROUPS_ALLOW` | No | | Comma-separated list of device group names to scrobble from | - | `SONOS_GROUPS_BLOCK` | No | | Comma-separated list of devices group names to disallow scrobbles from | - | `SONOS_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/spotify.mdx b/docsite/docs/configuration/sources/spotify.mdx index a7f5dfd1..b0746e48 100644 --- a/docsite/docs/configuration/sources/spotify.mdx +++ b/docsite/docs/configuration/sources/spotify.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import SpotifyConfig from '!!raw-loader!@site/../config/spotify.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_spotify.md" import IconWarning from '@theme/Admonition/Icon/Warning'; @@ -111,11 +112,5 @@ Use **one** of the following methods to specify a valid Redirect URI and complet ## Configuration - | Environmental Variable | Required? | Default | Description | - | ----------------------- | --------- | -------------------------------- | ------------------------------- | - | `SPOTIFY_ID` | Yes | | A unique ID | - | `SPOTIFY_CLIENT_ID` | Yes | | | - | `SPOTIFY_CLIENT_SECRET` | Yes | | | - | `SPOTIFY_REDIRECT_URI` | No | `http://localhost:9078/callback` | URI must end in `callback` | - | `SPOTIFY_NAME` | No | | A vantiy name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/subsonic.mdx b/docsite/docs/configuration/sources/subsonic.mdx index ddeaddc0..b632a7a9 100644 --- a/docsite/docs/configuration/sources/subsonic.mdx +++ b/docsite/docs/configuration/sources/subsonic.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import SubsonicConfig from '!!raw-loader!@site/../config/subsonic.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_subsonic.md" This Source can be used for any application that implements the [Subsonic API](http://www.subsonic.org/pages/api.jsp) and supports the [`getNowPlaying`](http://www.subsonic.org/pages/api.jsp#getNowPlaying) endpoint (such as [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/)) @@ -47,11 +48,5 @@ If the server properly reports no playing song when playback has stopped or this ## Configuration - | Environmental Variable | Required? | Default | Description | - | ---------------------- | --------- | ------- | ------------------------------------ | - | `SUBSONIC_ID` | Yes | | A unique ID | - | `SUBSONIC_USER` | Yes | | | - | `SUBSONIC_PASSWORD` | Yes | | | - | `SUBSONIC_URL` | Yes | | Base url of your subsonic-api server | - | `SUBSONIC_NAME` | No | | A vanity name different than ID | + diff --git a/docsite/docs/configuration/sources/tealfm-source.mdx b/docsite/docs/configuration/sources/tealfm-source.mdx index bd3a7c18..c35ee85a 100644 --- a/docsite/docs/configuration/sources/tealfm-source.mdx +++ b/docsite/docs/configuration/sources/tealfm-source.mdx @@ -9,6 +9,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import JsonConfig from '!!raw-loader!@site/../config/tealfm.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_tealfm.md" :::tip[Other Uses] @@ -104,10 +105,5 @@ Allow scrobbles from plyr.fm and unknown services: - | Environmental Variable | Required? | Default | Description | - | :------------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ | - | `SOURCE_TEALFM_ID` | Yes | | A unique ID | - | `SOURCE_TEALFM_IDENTIFIER` | Yes | | Your **full** ATProto handle or DID. For Bluesky account this is like `myUser.bsky.social` | - | `SOURCE_TEALFM_APP_PW` | Yes | | Bluesky/ATProto network App Password | - | `SORUCE_TEALFM_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/vlc.mdx b/docsite/docs/configuration/sources/vlc.mdx index 335facdb..49d88bd9 100644 --- a/docsite/docs/configuration/sources/vlc.mdx +++ b/docsite/docs/configuration/sources/vlc.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import JsonConfig from '!!raw-loader!@site/../config/vlc.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_vlc.md" MS communicates with [VLC](https://www.videolan.org/vlc/) using the [Web (`http`) interface module](https://wiki.videolan.org/VLC_HTTP_requests/) @@ -99,10 +100,5 @@ If you find that VLC is incorrectly reporting track information (in its interfac ## Configuration - | Environmental Variable | Required? | Default | Description | - | ---------------------- | --------- | ---------------- | ------------------------------- | - | `VLC_ID` | Yes | | A unique ID | - | `VLC_URL` | No | `localhost:8080` | | - | `VLC_PASSWORD` | Yes | | | - | `VLC_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/webscrobbler.mdx b/docsite/docs/configuration/sources/webscrobbler.mdx index 1a8c54a8..9db16c0b 100644 --- a/docsite/docs/configuration/sources/webscrobbler.mdx +++ b/docsite/docs/configuration/sources/webscrobbler.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import JsonConfig from '!!raw-loader!@site/../config/webscrobbler.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_webscrobbler.md" After installing the [Webscrobbler extension](https://web-scrobbler.com/) open the preferences/settings for it: @@ -87,11 +88,5 @@ This affects **only** MS's behavior and does not affect the general connector be ## Configuration - | Environmental Variable | Required? | Default | Description | - | ---------------------- | --------- | ------- | ------------------------------------------------------------------------ | - | WS_ID | Yes | | A unique ID | - | WS_ENABLE | No | | Set to 'true' to enable WS without needing to define other ENVs | - | WS_WHITELIST | No | | Only scrobble from these WebScrobbler Connectors. Comma-delimited list | - | WS_BLACKLIST | No | | Do not scrobble from these WebScrobbler Connectors. Comma-delimited list | - | WS_NAME | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/yamaha-musiccast.mdx b/docsite/docs/configuration/sources/yamaha-musiccast.mdx index 272b0463..65542243 100644 --- a/docsite/docs/configuration/sources/yamaha-musiccast.mdx +++ b/docsite/docs/configuration/sources/yamaha-musiccast.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import JsonConfig from '!!raw-loader!@site/../config/musiccast.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_musiccast.md" Monitor [MusicCast](https://usa.yamaha.com/products/contents/audio_visual/musiccast/index.html) device/receivers for music played on Network/USB/CD inputs. @@ -31,9 +32,5 @@ EX ## Configuration - | Environmental Variable | Required? | Default | Description | - | :--------------------- | :-------- | ------- | :------------------------------ | - | `MCAST_ID` | Yes | | A unique ID | - | `MCAST_URL` | Yes | | The Musiccast device URL | - | `MCAST_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/yandex-music.mdx b/docsite/docs/configuration/sources/yandex-music.mdx index 03255258..fb1bd986 100644 --- a/docsite/docs/configuration/sources/yandex-music.mdx +++ b/docsite/docs/configuration/sources/yandex-music.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import JsonConfig from '!!raw-loader!@site/../config/ymbridge.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_ymbridge.md" Monitor your [**Yandex Music**](https://music.yandex.com) listening activity in real-time. @@ -72,10 +73,5 @@ If you have issues specifically with the MS Source please mention [**Druidblack* ## Configuration - | Environmental Variable | Required? | Default | Description | - | :--------------------- | :-------- | ------- | :------------------------------------------------------------------------ | - | `YMBRIDGE_ID` | Yes | | A unique ID | - | `YMBRIDGE_URL` | Yes | | `http://URL:PORT` for the `yandex-music-bridge` container | - | `YMBRIDGE_API_KEY` | No | | The same key used for `YM_API_KEY` on the `yandex-music-bridge` container | - | `YMBRIDGE_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/youtube-music.mdx b/docsite/docs/configuration/sources/youtube-music.mdx index f5609dee..ebbdeba5 100644 --- a/docsite/docs/configuration/sources/youtube-music.mdx +++ b/docsite/docs/configuration/sources/youtube-music.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import JsonConfig from '!!raw-loader!@site/../config/ytmusic.json.example'; +import ENVConfig from "@site/docs/configuration/sources/_env_configs/_ytmusic.md" :::warning @@ -179,13 +180,5 @@ Only one of these methods needs to be used. ## Configuration -| Environmental Variable | Required? | Default | Description | -| ---------------------- | --------- | ------- | --------------------------------------------- | -| YTM_ID | Yes | | A unique ID | -| YTM_COOKIE | No | | Value for Cookie Authentication | -| YTM_CLIENT_ID | No | | Client ID for OAuth Athentication | -| YTM_CLIENT_SECRET | No | | Client Secret for OAuth Athentication | -| YTM_REDIRECT_URI | No | | A custom redirect URI for OAuth Athentication | -| YTM_LOG_DIFF | No | false | Log YTM history changes | -| YTM_NAME | No | | A vanity name different than ID | + \ No newline at end of file -- 2.51.2 From a93d02e1b78109bd9b17be2e6b6516e0385d5abf Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 20:50:41 +0000 Subject: [PATCH 16/35] env doc generation fixes --- docsite/docs/configuration/sources/google-cast.mdx | 2 +- docsite/docs/configuration/sources/lastfm-endpoint.mdx | 2 +- docsite/docs/configuration/sources/yamaha-musiccast.mdx | 2 +- src/backend/utils/SchemaDocsStaticUtils.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docsite/docs/configuration/sources/google-cast.mdx b/docsite/docs/configuration/sources/google-cast.mdx index 72622cfb..e819e36e 100644 --- a/docsite/docs/configuration/sources/google-cast.mdx +++ b/docsite/docs/configuration/sources/google-cast.mdx @@ -150,5 +150,5 @@ To diagnose bad/incomplete track information or strange MS player behavior pleas [Manually configuring cast device connections](#connecting-devices) is only available through file-based config. ::: - + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/lastfm-endpoint.mdx b/docsite/docs/configuration/sources/lastfm-endpoint.mdx index dc2e77cb..8369f128 100644 --- a/docsite/docs/configuration/sources/lastfm-endpoint.mdx +++ b/docsite/docs/configuration/sources/lastfm-endpoint.mdx @@ -40,5 +40,5 @@ http://localhost:9078/api/lastfm/mySlug ## Configuration - + \ No newline at end of file diff --git a/docsite/docs/configuration/sources/yamaha-musiccast.mdx b/docsite/docs/configuration/sources/yamaha-musiccast.mdx index 65542243..a27d9f1f 100644 --- a/docsite/docs/configuration/sources/yamaha-musiccast.mdx +++ b/docsite/docs/configuration/sources/yamaha-musiccast.mdx @@ -32,5 +32,5 @@ EX ## Configuration - + \ No newline at end of file diff --git a/src/backend/utils/SchemaDocsStaticUtils.ts b/src/backend/utils/SchemaDocsStaticUtils.ts index 44ffd3d0..ad1efcfa 100644 --- a/src/backend/utils/SchemaDocsStaticUtils.ts +++ b/src/backend/utils/SchemaDocsStaticUtils.ts @@ -14,7 +14,7 @@ mkdirSync(resolve(projectRootDir, 'docsite/static/schemas'), {recursive: true}); const mdCols = (cols: TableColumn[]): string[][] => cols.map((x) => { const name = `\`${x.title}\``; return [ - x.required ? `**${name}**` : name, + x.required ? `_**${name}**_` : name, x.type, (x.default ?? '').toString(), x.description ?? '' -- 2.51.2 From 0d45d06966c43a02b00082ef398331890d68d3ed Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 23:41:12 +0000 Subject: [PATCH 17/35] docs: Italicize required env properties --- .../clients/_env_configs/_discord.md | 2 +- .../clients/_env_configs/_koito.md | 8 ++++---- .../clients/_env_configs/_lastfm.md | 6 +++--- .../clients/_env_configs/_librefm.md | 2 +- .../clients/_env_configs/_listenbrainz.md | 6 +++--- .../clients/_env_configs/_maloja.md | 6 +++--- .../clients/_env_configs/_rocksky.md | 4 ++-- .../clients/_env_configs/_tealfm.md | 14 +++++++------- .../sources/_env_configs/_applemusic.md | 4 ++-- .../sources/_env_configs/_azuracast.md | 6 +++--- .../sources/_env_configs/_chromecast.md | 2 +- .../sources/_env_configs/_deezer.md | 4 ++-- .../sources/_env_configs/_endpointlfm.md | 2 +- .../sources/_env_configs/_endpointlz.md | 2 +- .../sources/_env_configs/_icecast.md | 4 ++-- .../sources/_env_configs/_jellyfin.md | 6 +++--- .../sources/_env_configs/_jriver.md | 4 ++-- .../sources/_env_configs/_kodi.md | 8 ++++---- .../sources/_env_configs/_koito.md | 16 ++++++++-------- .../sources/_env_configs/_lastfm.md | 18 +++++++++--------- .../sources/_env_configs/_librefm.md | 2 +- .../sources/_env_configs/_listenbrainz.md | 16 ++++++++-------- .../sources/_env_configs/_maloja.md | 14 +++++++------- .../sources/_env_configs/_mopidy.md | 2 +- .../configuration/sources/_env_configs/_mpd.md | 2 +- .../sources/_env_configs/_mpris.md | 2 +- .../sources/_env_configs/_musiccast.md | 4 ++-- .../sources/_env_configs/_musikcube.md | 4 ++-- .../sources/_env_configs/_plex.md | 4 ++-- .../sources/_env_configs/_rocksky.md | 14 +++++++------- .../sources/_env_configs/_sonos.md | 4 ++-- .../sources/_env_configs/_spotify.md | 16 ++++++++-------- .../sources/_env_configs/_subsonic.md | 16 ++++++++-------- .../sources/_env_configs/_tealfm.md | 14 +++++++------- .../configuration/sources/_env_configs/_vlc.md | 4 ++-- .../sources/_env_configs/_webscrobbler.md | 2 +- .../sources/_env_configs/_ymbridge.md | 4 ++-- .../sources/_env_configs/_ytmusic.md | 2 +- 38 files changed, 125 insertions(+), 125 deletions(-) diff --git a/docsite/docs/configuration/clients/_env_configs/_discord.md b/docsite/docs/configuration/clients/_env_configs/_discord.md index c9d9aec5..d6c7dc45 100644 --- a/docsite/docs/configuration/clients/_env_configs/_discord.md +++ b/docsite/docs/configuration/clients/_env_configs/_discord.md @@ -1,6 +1,6 @@ | Environmental Variable | Type | Default | Description | | ---------------------------------- | ------- | --------------------- | -------------------- | -| **`DISCORD_ID`** | string | | A globally unique ID | +| _**`DISCORD_ID`**_ | string | | A globally unique ID | | `DISCORD_NAME` | string | Value of `DISCORD_ID` | A vanity name | | `DISCORD_ENABLE` | boolean | | | | `DISCORD_TOKEN` | string | | | diff --git a/docsite/docs/configuration/clients/_env_configs/_koito.md b/docsite/docs/configuration/clients/_env_configs/_koito.md index d88a5fed..9d2ce634 100644 --- a/docsite/docs/configuration/clients/_env_configs/_koito.md +++ b/docsite/docs/configuration/clients/_env_configs/_koito.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ------------------- | --------------------------------------- | -| **`KOITO_ID`** | string | | A globally unique ID | +| _**`KOITO_ID`**_ | string | | A globally unique ID | | `KOITO_NAME` | string | Value of `KOITO_ID` | A vanity name | | `KOITO_ENABLE` | boolean | | | -| **`KOITO_URL`** | string | | URL for the Koito server | -| **`KOITO_TOKEN`** | string | | User token for the user to scrobble for | -| **`KOITO_USER`** | string | | Username of the user to scrobble for | \ No newline at end of file +| _**`KOITO_URL`**_ | string | | URL for the Koito server | +| _**`KOITO_TOKEN`**_ | string | | User token for the user to scrobble for | +| _**`KOITO_USER`**_ | string | | Username of the user to scrobble for | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/_env_configs/_lastfm.md b/docsite/docs/configuration/clients/_env_configs/_lastfm.md index d85806c8..e28ca51f 100644 --- a/docsite/docs/configuration/clients/_env_configs/_lastfm.md +++ b/docsite/docs/configuration/clients/_env_configs/_lastfm.md @@ -1,9 +1,9 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ------------------------------------- | ------------------------------------------------------- | -| **`LASTFM_ID`** | string | | A globally unique ID | +| _**`LASTFM_ID`**_ | string | | A globally unique ID | | `LASTFM_NAME` | string | Value of `LASTFM_ID` | A vanity name | | `LASTFM_ENABLE` | boolean | | | -| **`LASTFM_API_KEY`** | string | | API Key generated from Last.fm/Libre.fm account | -| **`LASTFM_SECRET`** | string | | Secret generated from Last.fm/Libre.fm account | +| _**`LASTFM_API_KEY`**_ | string | | API Key generated from Last.fm/Libre.fm account | +| _**`LASTFM_SECRET`**_ | string | | Secret generated from Last.fm/Libre.fm account | | `LASTFM_REDIRECT_URI` | string | http://localhost:9078/lastfm/callback | Optional URI to use for callback. | | `LASTFM_SESSION` | string | | Optional session id returned from a completed auth flow | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/_env_configs/_librefm.md b/docsite/docs/configuration/clients/_env_configs/_librefm.md index 7a3f0e9a..6fb8139a 100644 --- a/docsite/docs/configuration/clients/_env_configs/_librefm.md +++ b/docsite/docs/configuration/clients/_env_configs/_librefm.md @@ -1,6 +1,6 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | -------------------------------------- | -------------------------------------------------------------- | -| **`LIBREFM_ID`** | string | | A globally unique ID | +| _**`LIBREFM_ID`**_ | string | | A globally unique ID | | `LIBREFM_NAME` | string | Value of `LIBREFM_ID` | A vanity name | | `LIBREFM_ENABLE` | boolean | | | | `LIBREFM_API_KEY` | string | | Optional Secret for Libre.fm account | diff --git a/docsite/docs/configuration/clients/_env_configs/_listenbrainz.md b/docsite/docs/configuration/clients/_env_configs/_listenbrainz.md index 7b61d4a5..c4c5eafd 100644 --- a/docsite/docs/configuration/clients/_env_configs/_listenbrainz.md +++ b/docsite/docs/configuration/clients/_env_configs/_listenbrainz.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ----------------------------- | --------------------------------------------------------- | -| **`LZ_ID`** | string | | A globally unique ID | +| _**`LZ_ID`**_ | string | | A globally unique ID | | `LZ_NAME` | string | Value of `LZ_ID` | A vanity name | | `LZ_ENABLE` | boolean | | | | `LZ_URL` | string | https://api.listenbrainz.org/ | URL for the ListenBrainz server, if not using the default | -| **`LZ_TOKEN`** | string | | User token for the user to scrobble for | -| **`LZ_USER`** | string | | Username of the user to scrobble for | \ No newline at end of file +| _**`LZ_TOKEN`**_ | string | | User token for the user to scrobble for | +| _**`LZ_USER`**_ | string | | Username of the user to scrobble for | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/_env_configs/_maloja.md b/docsite/docs/configuration/clients/_env_configs/_maloja.md index 85d358d8..d346e32a 100644 --- a/docsite/docs/configuration/clients/_env_configs/_maloja.md +++ b/docsite/docs/configuration/clients/_env_configs/_maloja.md @@ -1,7 +1,7 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | -------------------- | ------------------------- | -| **`MALOJA_ID`** | string | | A globally unique ID | +| _**`MALOJA_ID`**_ | string | | A globally unique ID | | `MALOJA_NAME` | string | Value of `MALOJA_ID` | A vanity name | | `MALOJA_ENABLE` | boolean | | | -| **`MALOJA_URL`** | string | | URL for maloja server | -| **`MALOJA_API_KEY`** | string | | API Key for Maloja server | \ No newline at end of file +| _**`MALOJA_URL`**_ | string | | URL for maloja server | +| _**`MALOJA_API_KEY`**_ | string | | API Key for Maloja server | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/_env_configs/_rocksky.md b/docsite/docs/configuration/clients/_env_configs/_rocksky.md index 1b46b56b..f978f310 100644 --- a/docsite/docs/configuration/clients/_env_configs/_rocksky.md +++ b/docsite/docs/configuration/clients/_env_configs/_rocksky.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -| **`ROCKSKY_ID`** | string | | A globally unique ID | +| _**`ROCKSKY_ID`**_ | string | | A globally unique ID | | `ROCKSKY_NAME` | string | Value of `ROCKSKY_ID` | A vanity name | | `ROCKSKY_ENABLE` | boolean | | | | `ROCKSKY_KEY` | string | | API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account | | `ROCKSKY_TOKEN` | string | | Access Token generated from https://rocksky.app/access-tokens in Rocksky for your account | -| **`ROCKSKY_HANDLE`** | string | | The **fully-qualified** handle for your ATPRoto/Bluesky account, like: | \ No newline at end of file +| _**`ROCKSKY_HANDLE`**_ | string | | The **fully-qualified** handle for your ATPRoto/Bluesky account, like: | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/_env_configs/_tealfm.md b/docsite/docs/configuration/clients/_env_configs/_tealfm.md index 51ea4df5..6856dae7 100644 --- a/docsite/docs/configuration/clients/_env_configs/_tealfm.md +++ b/docsite/docs/configuration/clients/_env_configs/_tealfm.md @@ -1,7 +1,7 @@ -| Environmental Variable | Type | Default | Description | -| ----------------------- | ------- | -------------------- | -------------------------------- | -| **`TEALFM_ID`** | string | | A globally unique ID | -| `TEALFM_NAME` | string | Value of `TEALFM_ID` | A vanity name | -| `TEALFM_ENABLE` | boolean | | | -| **`TEALFM_IDENTIFIER`** | string | | Identify the account to login as | -| `TEALFM_APP_PW` | string | | | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ------------------------- | ------- | -------------------- | -------------------------------- | +| _**`TEALFM_ID`**_ | string | | A globally unique ID | +| `TEALFM_NAME` | string | Value of `TEALFM_ID` | A vanity name | +| `TEALFM_ENABLE` | boolean | | | +| _**`TEALFM_IDENTIFIER`**_ | string | | Identify the account to login as | +| `TEALFM_APP_PW` | string | | | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_applemusic.md b/docsite/docs/configuration/sources/_env_configs/_applemusic.md index ebfe871d..b8063aff 100644 --- a/docsite/docs/configuration/sources/_env_configs/_applemusic.md +++ b/docsite/docs/configuration/sources/_env_configs/_applemusic.md @@ -1,12 +1,12 @@ | Environmental Variable | Type | Default | Description | | ------------------------------------------ | ------- | ------------------------ | -------------------------------------------------------------------- | -| **`APPLEMUSIC_ID`** | string | | A globally unique ID | +| _**`APPLEMUSIC_ID`**_ | string | | A globally unique ID | | `APPLEMUSIC_NAME` | string | Value of `APPLEMUSIC_ID` | A vanity name | | `APPLEMUSIC_ENABLE` | boolean | | | | `APPLEMUSIC_KEY_ID` | string | | | | `APPLEMUSIC_TEAM_ID` | string | | | | `APPLEMUSIC_KEY_P8` | string | | | -| **`APPLEMUSIC_MEDIA_USER_TOKEN`** | string | | | +| _**`APPLEMUSIC_MEDIA_USER_TOKEN`**_ | string | | | | `APPLEMUSIC_TOKEN` | string | | | | `APPLEMUSIC_ORIGIN_HEADER` | string | | Origin header to include in every Apple Music API request. | | `APPLEMUSIC_RECOVER_UNCHANGED_TOP_HISTORY` | boolean | true | Fixes a quirk where Apple Music's history API hides duplicate plays. | diff --git a/docsite/docs/configuration/sources/_env_configs/_azuracast.md b/docsite/docs/configuration/sources/_env_configs/_azuracast.md index 87af3eef..d5c69416 100644 --- a/docsite/docs/configuration/sources/_env_configs/_azuracast.md +++ b/docsite/docs/configuration/sources/_env_configs/_azuracast.md @@ -1,10 +1,10 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | --------- | ------------------- | ------------------------------------------------- | -| **`AZURA_ID`** | string | | A globally unique ID | +| _**`AZURA_ID`**_ | string | | A globally unique ID | | `AZURA_NAME` | string | Value of `AZURA_ID` | A vanity name | | `AZURA_ENABLE` | boolean | | | -| **`AZURA_STATION`** | string | | The specific station to monitor | -| **`AZURA_URL`** | string | | Base URL of the Azuracast instance | +| _**`AZURA_STATION`**_ | string | | The specific station to monitor | +| _**`AZURA_URL`**_ | string | | Base URL of the Azuracast instance | | `AZURA_KEY` | string | | API Key used to access data about private streams | | `AZURA_LISTENERS_NUM` | transform | | | | `AZURA_LIVE` | boolean | | | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_chromecast.md b/docsite/docs/configuration/sources/_env_configs/_chromecast.md index c0e93f11..d11c53e5 100644 --- a/docsite/docs/configuration/sources/_env_configs/_chromecast.md +++ b/docsite/docs/configuration/sources/_env_configs/_chromecast.md @@ -1,6 +1,6 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | --------- | ---------------- | ------------------------------------------------------------------------------------ | -| **`CC_ID`** | string | | A globally unique ID | +| _**`CC_ID`**_ | string | | A globally unique ID | | `CC_NAME` | string | Value of `CC_ID` | A vanity name | | `CC_ENABLE` | boolean | | | | `CC_BLACKLIST_DEVICES` | transform | | DO NOT scrobble from any cast devices that START WITH these values, case-insensitive | diff --git a/docsite/docs/configuration/sources/_env_configs/_deezer.md b/docsite/docs/configuration/sources/_env_configs/_deezer.md index 02369081..29d9d297 100644 --- a/docsite/docs/configuration/sources/_env_configs/_deezer.md +++ b/docsite/docs/configuration/sources/_env_configs/_deezer.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | -------------------------------------------------------------------- | -------------------------------------------------- | -| **`DEEZER_ID`** | string | | A globally unique ID | +| _**`DEEZER_ID`**_ | string | | A globally unique ID | | `DEEZER_NAME` | string | Value of `DEEZER_ID` | A vanity name | | `DEEZER_ENABLE` | boolean | | | -| **`DEEZER_ARL`** | string | | ARL retrieved from Deezer response header | +| _**`DEEZER_ARL`**_ | string | | ARL retrieved from Deezer response header | | `DEEZER_ACCOUNT_ID` | string | | The ID (USER_ID) of the linked account to monitor. | | `DEEZER_USER_AGENT` | string | Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0 | User agent | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_endpointlfm.md b/docsite/docs/configuration/sources/_env_configs/_endpointlfm.md index 177a4b46..226e6cda 100644 --- a/docsite/docs/configuration/sources/_env_configs/_endpointlfm.md +++ b/docsite/docs/configuration/sources/_env_configs/_endpointlfm.md @@ -1,6 +1,6 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ----------------- | ------------------------------------------------------------------------ | -| **`LFM_ID`** | string | | A globally unique ID | +| _**`LFM_ID`**_ | string | | A globally unique ID | | `LFM_NAME` | string | Value of `LFM_ID` | A vanity name | | `LFM_ENABLE` | boolean | | | | `LFM_SLUG` | union | | The URL ending that should be used to identify scrobbles for this source | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_endpointlz.md b/docsite/docs/configuration/sources/_env_configs/_endpointlz.md index 7658b826..6a0c0c3b 100644 --- a/docsite/docs/configuration/sources/_env_configs/_endpointlz.md +++ b/docsite/docs/configuration/sources/_env_configs/_endpointlz.md @@ -1,6 +1,6 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| **`LZE_ID`** | string | | A globally unique ID | +| _**`LZE_ID`**_ | string | | A globally unique ID | | `LZE_NAME` | string | Value of `LZE_ID` | A vanity name | | `LZE_ENABLE` | boolean | | | | `LZE_SLUG` | string | | The URL ending that should be used to identify scrobbles for this source | diff --git a/docsite/docs/configuration/sources/_env_configs/_icecast.md b/docsite/docs/configuration/sources/_env_configs/_icecast.md index a7e3c0c0..8c7279ba 100644 --- a/docsite/docs/configuration/sources/_env_configs/_icecast.md +++ b/docsite/docs/configuration/sources/_env_configs/_icecast.md @@ -1,7 +1,7 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | --------------------- | ---------------------- | -| **`ICECAST_ID`** | string | | A globally unique ID | +| _**`ICECAST_ID`**_ | string | | A globally unique ID | | `ICECAST_NAME` | string | Value of `ICECAST_ID` | A vanity name | | `ICECAST_ENABLE` | boolean | | | -| **`ICECAST_URL`** | string | | The Icecast stream URL | +| _**`ICECAST_URL`**_ | string | | The Icecast stream URL | | `ICECAST_AUTO_MONITOR` | boolean | | | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_jellyfin.md b/docsite/docs/configuration/sources/_env_configs/_jellyfin.md index f9e77c5e..d1a17b03 100644 --- a/docsite/docs/configuration/sources/_env_configs/_jellyfin.md +++ b/docsite/docs/configuration/sources/_env_configs/_jellyfin.md @@ -1,12 +1,12 @@ | Environmental Variable | Type | Default | Description | | -------------------------------- | ------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -| **`JELLYFIN_ID`** | string | | A globally unique ID | +| _**`JELLYFIN_ID`**_ | string | | A globally unique ID | | `JELLYFIN_NAME` | string | Value of `JELLYFIN_ID` | A vanity name | | `JELLYFIN_ENABLE` | boolean | | | -| **`JELLYFIN_USER`** | string | | The username of the user to authenticate for or track scrobbles for | +| _**`JELLYFIN_USER`**_ | string | | The username of the user to authenticate for or track scrobbles for | | `JELLYFIN_PASSWORD` | string | | Password of the username to authenticate for | | `JELLYFIN_APIKEY` | string | | API Key to authenticate with. | -| **`JELLYFIN_URL`** | string | | HOST:PORT of the Jellyfin server to connect to | +| _**`JELLYFIN_URL`**_ | string | | HOST:PORT of the Jellyfin server to connect to | | `JELLYFIN_USERS_ALLOW` | union | | Only scrobble for specific users (case-insensitive) | | `JELLYFIN_USERS_BLOCK` | union | | Do not scrobble for these users (case-insensitive) | | `JELLYFIN_DEVICES_ALLOW` | union | | Only scrobble if device or application name contains strings from this list (case-insensitive) | diff --git a/docsite/docs/configuration/sources/_env_configs/_jriver.md b/docsite/docs/configuration/sources/_env_configs/_jriver.md index 8346b08c..7e2da28a 100644 --- a/docsite/docs/configuration/sources/_env_configs/_jriver.md +++ b/docsite/docs/configuration/sources/_env_configs/_jriver.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ------------------------------- | -------------------------------------------------------- | -| **`JRIVER_ID`** | string | | A globally unique ID | +| _**`JRIVER_ID`**_ | string | | A globally unique ID | | `JRIVER_NAME` | string | Value of `JRIVER_ID` | A vanity name | | `JRIVER_ENABLE` | boolean | | | -| **`JRIVER_URL`** | string | http://localhost:52199/MCWS/v1/ | URL of the JRiver HTTP server to connect to | +| _**`JRIVER_URL`**_ | string | http://localhost:52199/MCWS/v1/ | URL of the JRiver HTTP server to connect to | | `JRIVER_USER` | string | | If you have enabled authentication, the username you set | | `JRIVER_PASSWORD` | string | | If you have enabled authentication, the password you set | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_kodi.md b/docsite/docs/configuration/sources/_env_configs/_kodi.md index 4c62638d..cdf8a0cd 100644 --- a/docsite/docs/configuration/sources/_env_configs/_kodi.md +++ b/docsite/docs/configuration/sources/_env_configs/_kodi.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ----------------------------- | ------------------------------------------------- | -| **`KODI_ID`** | string | | A globally unique ID | +| _**`KODI_ID`**_ | string | | A globally unique ID | | `KODI_NAME` | string | Value of `KODI_ID` | A vanity name | | `KODI_ENABLE` | boolean | | | -| **`KODI_URL`** | string | http://localhost:8080/jsonrpc | URL of the Kodi HTTP server to connect to | -| **`KODI_USER`** | string | | The username set for Remote Control via Web Sever | -| **`KODI_PASSWORD`** | string | | The password set for Remote Control via Web Sever | \ No newline at end of file +| _**`KODI_URL`**_ | string | http://localhost:8080/jsonrpc | URL of the Kodi HTTP server to connect to | +| _**`KODI_USER`**_ | string | | The username set for Remote Control via Web Sever | +| _**`KODI_PASSWORD`**_ | string | | The password set for Remote Control via Web Sever | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_koito.md b/docsite/docs/configuration/sources/_env_configs/_koito.md index 1d66908c..01b2f5c5 100644 --- a/docsite/docs/configuration/sources/_env_configs/_koito.md +++ b/docsite/docs/configuration/sources/_env_configs/_koito.md @@ -1,8 +1,8 @@ -| Environmental Variable | Type | Default | Description | -| ------------------------ | ------- | -------------------------- | --------------------------------------- | -| **`SOURCE_KOITO_ID`** | string | | A globally unique ID | -| `SOURCE_KOITO_NAME` | string | Value of `SOURCE_KOITO_ID` | A vanity name | -| `SOURCE_KOITO_ENABLE` | boolean | | | -| **`SOURCE_KOITO_URL`** | string | | URL for the Koito server | -| **`SOURCE_KOITO_TOKEN`** | string | | User token for the user to scrobble for | -| **`SOURCE_KOITO_USER`** | string | | Username of the user to scrobble for | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| -------------------------- | ------- | -------------------------- | --------------------------------------- | +| _**`SOURCE_KOITO_ID`**_ | string | | A globally unique ID | +| `SOURCE_KOITO_NAME` | string | Value of `SOURCE_KOITO_ID` | A vanity name | +| `SOURCE_KOITO_ENABLE` | boolean | | | +| _**`SOURCE_KOITO_URL`**_ | string | | URL for the Koito server | +| _**`SOURCE_KOITO_TOKEN`**_ | string | | User token for the user to scrobble for | +| _**`SOURCE_KOITO_USER`**_ | string | | Username of the user to scrobble for | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_lastfm.md b/docsite/docs/configuration/sources/_env_configs/_lastfm.md index ed171d2f..eb67b024 100644 --- a/docsite/docs/configuration/sources/_env_configs/_lastfm.md +++ b/docsite/docs/configuration/sources/_env_configs/_lastfm.md @@ -1,9 +1,9 @@ -| Environmental Variable | Type | Default | Description | -| ---------------------------- | ------- | ------------------------------------- | ------------------------------------------------------- | -| **`SOURCE_LASTFM_ID`** | string | | A globally unique ID | -| `SOURCE_LASTFM_NAME` | string | Value of `SOURCE_LASTFM_ID` | A vanity name | -| `SOURCE_LASTFM_ENABLE` | boolean | | | -| **`SOURCE_LASTFM_API_KEY`** | string | | API Key generated from Last.fm/Libre.fm account | -| **`SOURCE_LASTFM_SECRET`** | string | | Secret generated from Last.fm/Libre.fm account | -| `SOURCE_LASTFM_REDIRECT_URI` | string | http://localhost:9078/lastfm/callback | Optional URI to use for callback. | -| `SOURCE_LASTFM_SESSION` | string | | Optional session id returned from a completed auth flow | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ----------------------------- | ------- | ------------------------------------- | ------------------------------------------------------- | +| _**`SOURCE_LASTFM_ID`**_ | string | | A globally unique ID | +| `SOURCE_LASTFM_NAME` | string | Value of `SOURCE_LASTFM_ID` | A vanity name | +| `SOURCE_LASTFM_ENABLE` | boolean | | | +| _**`SOURCE_LASTFM_API_KEY`**_ | string | | API Key generated from Last.fm/Libre.fm account | +| _**`SOURCE_LASTFM_SECRET`**_ | string | | Secret generated from Last.fm/Libre.fm account | +| `SOURCE_LASTFM_REDIRECT_URI` | string | http://localhost:9078/lastfm/callback | Optional URI to use for callback. | +| `SOURCE_LASTFM_SESSION` | string | | Optional session id returned from a completed auth flow | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_librefm.md b/docsite/docs/configuration/sources/_env_configs/_librefm.md index cd3c5744..98f64b1f 100644 --- a/docsite/docs/configuration/sources/_env_configs/_librefm.md +++ b/docsite/docs/configuration/sources/_env_configs/_librefm.md @@ -1,6 +1,6 @@ | Environmental Variable | Type | Default | Description | | ----------------------------- | ------- | -------------------------------------- | -------------------------------------------------------------- | -| **`SOURCE_LIBREFM_ID`** | string | | A globally unique ID | +| _**`SOURCE_LIBREFM_ID`**_ | string | | A globally unique ID | | `SOURCE_LIBREFM_NAME` | string | Value of `SOURCE_LIBREFM_ID` | A vanity name | | `SOURCE_LIBREFM_ENABLE` | boolean | | | | `SOURCE_LIBREFM_API_KEY` | string | | Optional Secret for Libre.fm account | diff --git a/docsite/docs/configuration/sources/_env_configs/_listenbrainz.md b/docsite/docs/configuration/sources/_env_configs/_listenbrainz.md index 64f161bc..229a59ee 100644 --- a/docsite/docs/configuration/sources/_env_configs/_listenbrainz.md +++ b/docsite/docs/configuration/sources/_env_configs/_listenbrainz.md @@ -1,8 +1,8 @@ -| Environmental Variable | Type | Default | Description | -| ---------------------- | ------- | ----------------------------- | --------------------------------------------------------- | -| **`SOURCE_LZ_ID`** | string | | A globally unique ID | -| `SOURCE_LZ_NAME` | string | Value of `SOURCE_LZ_ID` | A vanity name | -| `SOURCE_LZ_ENABLE` | boolean | | | -| `SOURCE_LZ_URL` | string | https://api.listenbrainz.org/ | URL for the ListenBrainz server, if not using the default | -| **`SOURCE_LZ_TOKEN`** | string | | User token for the user to scrobble for | -| **`SOURCE_LZ_USER`** | string | | Username of the user to scrobble for | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ----------------------- | ------- | ----------------------------- | --------------------------------------------------------- | +| _**`SOURCE_LZ_ID`**_ | string | | A globally unique ID | +| `SOURCE_LZ_NAME` | string | Value of `SOURCE_LZ_ID` | A vanity name | +| `SOURCE_LZ_ENABLE` | boolean | | | +| `SOURCE_LZ_URL` | string | https://api.listenbrainz.org/ | URL for the ListenBrainz server, if not using the default | +| _**`SOURCE_LZ_TOKEN`**_ | string | | User token for the user to scrobble for | +| _**`SOURCE_LZ_USER`**_ | string | | Username of the user to scrobble for | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_maloja.md b/docsite/docs/configuration/sources/_env_configs/_maloja.md index 86594513..0e76a60a 100644 --- a/docsite/docs/configuration/sources/_env_configs/_maloja.md +++ b/docsite/docs/configuration/sources/_env_configs/_maloja.md @@ -1,7 +1,7 @@ -| Environmental Variable | Type | Default | Description | -| --------------------------- | ------- | --------------------------- | ------------------------- | -| **`SOURCE_MALOJA_ID`** | string | | A globally unique ID | -| `SOURCE_MALOJA_NAME` | string | Value of `SOURCE_MALOJA_ID` | A vanity name | -| `SOURCE_MALOJA_ENABLE` | boolean | | | -| **`SOURCE_MALOJA_URL`** | string | | URL for maloja server | -| **`SOURCE_MALOJA_API_KEY`** | string | | API Key for Maloja server | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ----------------------------- | ------- | --------------------------- | ------------------------- | +| _**`SOURCE_MALOJA_ID`**_ | string | | A globally unique ID | +| `SOURCE_MALOJA_NAME` | string | Value of `SOURCE_MALOJA_ID` | A vanity name | +| `SOURCE_MALOJA_ENABLE` | boolean | | | +| _**`SOURCE_MALOJA_URL`**_ | string | | URL for maloja server | +| _**`SOURCE_MALOJA_API_KEY`**_ | string | | API Key for Maloja server | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_mopidy.md b/docsite/docs/configuration/sources/_env_configs/_mopidy.md index 97e74009..4312f850 100644 --- a/docsite/docs/configuration/sources/_env_configs/_mopidy.md +++ b/docsite/docs/configuration/sources/_env_configs/_mopidy.md @@ -1,6 +1,6 @@ | Environmental Variable | Type | Default | Description | | ----------------------- | --------- | ------------------------------ | ------------------------------------------------------------------------------------------ | -| **`MOPIDY_ID`** | string | | A globally unique ID | +| _**`MOPIDY_ID`**_ | string | | A globally unique ID | | `MOPIDY_NAME` | string | Value of `MOPIDY_ID` | A vanity name | | `MOPIDY_ENABLE` | boolean | | | | `MOPIDY_URL` | string | ws://localhost:6680/mopidy/ws/ | URL of the Mopidy HTTP server to connect to | diff --git a/docsite/docs/configuration/sources/_env_configs/_mpd.md b/docsite/docs/configuration/sources/_env_configs/_mpd.md index 4273b795..3d7ad82f 100644 --- a/docsite/docs/configuration/sources/_env_configs/_mpd.md +++ b/docsite/docs/configuration/sources/_env_configs/_mpd.md @@ -1,6 +1,6 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ----------------- | -------------------------------------------------------------------------------------------------------- | -| **`MPD_ID`** | string | | A globally unique ID | +| _**`MPD_ID`**_ | string | | A globally unique ID | | `MPD_NAME` | string | Value of `MPD_ID` | A vanity name | | `MPD_ENABLE` | boolean | | | | `MPD_URL` | string | localhost:6600 | URL:PORT of the MPD server to connect to | diff --git a/docsite/docs/configuration/sources/_env_configs/_mpris.md b/docsite/docs/configuration/sources/_env_configs/_mpris.md index 2b467d84..11685d97 100644 --- a/docsite/docs/configuration/sources/_env_configs/_mpris.md +++ b/docsite/docs/configuration/sources/_env_configs/_mpris.md @@ -1,6 +1,6 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ------------------- | ------------------------------------------------------------------------------- | -| **`MPRIS_ID`** | string | | A globally unique ID | +| _**`MPRIS_ID`**_ | string | | A globally unique ID | | `MPRIS_NAME` | string | Value of `MPRIS_ID` | A vanity name | | `MPRIS_ENABLE` | boolean | | | | `MPRIS_BLACKLIST` | union | | DO NOT scrobble from any players that START WITH these values, case-insensitive | diff --git a/docsite/docs/configuration/sources/_env_configs/_musiccast.md b/docsite/docs/configuration/sources/_env_configs/_musiccast.md index aad4dbfb..8cfd3c66 100644 --- a/docsite/docs/configuration/sources/_env_configs/_musiccast.md +++ b/docsite/docs/configuration/sources/_env_configs/_musiccast.md @@ -1,6 +1,6 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ------------------- | ------------------------------------------------------------ | -| **`MCAST_ID`** | string | | A globally unique ID | +| _**`MCAST_ID`**_ | string | | A globally unique ID | | `MCAST_NAME` | string | Value of `MCAST_ID` | A vanity name | | `MCAST_ENABLE` | boolean | | | -| **`MCAST_URL`** | string | | The host or URL of the YamahaExtendedControl endpoint to use | \ No newline at end of file +| _**`MCAST_URL`**_ | string | | The host or URL of the YamahaExtendedControl endpoint to use | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_musikcube.md b/docsite/docs/configuration/sources/_env_configs/_musikcube.md index 36facc1f..f20056fb 100644 --- a/docsite/docs/configuration/sources/_env_configs/_musikcube.md +++ b/docsite/docs/configuration/sources/_env_configs/_musikcube.md @@ -1,7 +1,7 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ------------------- | -------------------------------------------------------------------------------------------- | -| **`MC_ID`** | string | | A globally unique ID | +| _**`MC_ID`**_ | string | | A globally unique ID | | `MC_NAME` | string | Value of `MC_ID` | A vanity name | | `MC_ENABLE` | boolean | | | | `MC_URL` | string | ws://localhost:7905 | URL of the Musikcube Websocket (Metadata) server to connect to | -| **`MC_PASSWORD`** | string | | Password set in Musikcube https://github.com/clangen/musikcube/wiki/remote-api-documentation | \ No newline at end of file +| _**`MC_PASSWORD`**_ | string | | Password set in Musikcube https://github.com/clangen/musikcube/wiki/remote-api-documentation | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_plex.md b/docsite/docs/configuration/sources/_env_configs/_plex.md index 1b70457f..74d1d3b3 100644 --- a/docsite/docs/configuration/sources/_env_configs/_plex.md +++ b/docsite/docs/configuration/sources/_env_configs/_plex.md @@ -1,9 +1,9 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ------------------ | ------------------------------------------------------------------------------------------------ | -| **`PLEX_ID`** | string | | A globally unique ID | +| _**`PLEX_ID`**_ | string | | A globally unique ID | | `PLEX_NAME` | string | Value of `PLEX_ID` | A vanity name | | `PLEX_ENABLE` | boolean | | | -| **`PLEX_URL`** | string | | http(s)://HOST:PORT of the Plex server to connect to | +| _**`PLEX_URL`**_ | string | | http(s)://HOST:PORT of the Plex server to connect to | | `PLEX_TOKEN` | string | | | | `PLEX_USERS_ALLOW` | union | | Only scrobble for specific users (case-insensitive) | | `PLEX_USERS_BLOCK` | union | | Do not scrobble for these users (case-insensitive) | diff --git a/docsite/docs/configuration/sources/_env_configs/_rocksky.md b/docsite/docs/configuration/sources/_env_configs/_rocksky.md index b6c7a6e6..e87ad5d9 100644 --- a/docsite/docs/configuration/sources/_env_configs/_rocksky.md +++ b/docsite/docs/configuration/sources/_env_configs/_rocksky.md @@ -1,7 +1,7 @@ -| Environmental Variable | Type | Default | Description | -| --------------------------- | ------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -| **`SOURCE_ROCKSKY_ID`** | string | | A globally unique ID | -| `SOURCE_ROCKSKY_NAME` | string | Value of `SOURCE_ROCKSKY_ID` | A vanity name | -| `SOURCE_ROCKSKY_ENABLE` | boolean | | | -| `SOURCE_ROCKSKY_KEY` | string | | API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account | -| **`SOURCE_ROCKSKY_HANDLE`** | string | | The **fully-qualified** handle for your ATPRoto/Bluesky account, like: | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ----------------------------- | ------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| _**`SOURCE_ROCKSKY_ID`**_ | string | | A globally unique ID | +| `SOURCE_ROCKSKY_NAME` | string | Value of `SOURCE_ROCKSKY_ID` | A vanity name | +| `SOURCE_ROCKSKY_ENABLE` | boolean | | | +| `SOURCE_ROCKSKY_KEY` | string | | API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account | +| _**`SOURCE_ROCKSKY_HANDLE`**_ | string | | The **fully-qualified** handle for your ATPRoto/Bluesky account, like: | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_sonos.md b/docsite/docs/configuration/sources/_env_configs/_sonos.md index 7fa1f56c..34d22e6f 100644 --- a/docsite/docs/configuration/sources/_env_configs/_sonos.md +++ b/docsite/docs/configuration/sources/_env_configs/_sonos.md @@ -1,9 +1,9 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------- | -| **`SONOS_ID`** | string | | A globally unique ID | +| _**`SONOS_ID`**_ | string | | A globally unique ID | | `SONOS_NAME` | string | Value of `SONOS_ID` | A vanity name | | `SONOS_ENABLE` | boolean | | | -| **`SONOS_HOST`** | string | | IP address of any connected Sonos speaker or device | +| _**`SONOS_HOST`**_ | string | | IP address of any connected Sonos speaker or device | | `SONOS_DEVICES_ALLOW` | union | | Only scrobble if device name contains strings from this list (case-insensitive) | | `SONOS_DEVICES_BLOCK` | union | | Do not scrobble if device name contains strings from this list (case-insensitive) | | `SONOS_GROUPS_ALLOW` | union | | Only scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive) | diff --git a/docsite/docs/configuration/sources/_env_configs/_spotify.md b/docsite/docs/configuration/sources/_env_configs/_spotify.md index e547ed8b..5eab79b9 100644 --- a/docsite/docs/configuration/sources/_env_configs/_spotify.md +++ b/docsite/docs/configuration/sources/_env_configs/_spotify.md @@ -1,8 +1,8 @@ -| Environmental Variable | Type | Default | Description | -| --------------------------- | ------- | ------------------------------ | -------------------------------------------------------------------- | -| **`SPOTIFY_ID`** | string | | A globally unique ID | -| `SPOTIFY_NAME` | string | Value of `SPOTIFY_ID` | A vanity name | -| `SPOTIFY_ENABLE` | boolean | | | -| **`SPOTIFY_CLIENT_ID`** | string | | spotify client id | -| **`SPOTIFY_CLIENT_SECRET`** | string | | spotify client secret | -| `SPOTIFY_REDIRECT_URI` | string | http://localhost:9078/callback | spotify redirect URI -- required only if not the default shown here. | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ----------------------------- | ------- | ------------------------------ | -------------------------------------------------------------------- | +| _**`SPOTIFY_ID`**_ | string | | A globally unique ID | +| `SPOTIFY_NAME` | string | Value of `SPOTIFY_ID` | A vanity name | +| `SPOTIFY_ENABLE` | boolean | | | +| _**`SPOTIFY_CLIENT_ID`**_ | string | | spotify client id | +| _**`SPOTIFY_CLIENT_SECRET`**_ | string | | spotify client secret | +| `SPOTIFY_REDIRECT_URI` | string | http://localhost:9078/callback | spotify redirect URI -- required only if not the default shown here. | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_subsonic.md b/docsite/docs/configuration/sources/_env_configs/_subsonic.md index 3d40e633..552047c4 100644 --- a/docsite/docs/configuration/sources/_env_configs/_subsonic.md +++ b/docsite/docs/configuration/sources/_env_configs/_subsonic.md @@ -1,8 +1,8 @@ -| Environmental Variable | Type | Default | Description | -| ----------------------- | ------- | ---------------------- | ------------------------------------------------- | -| **`SUBSONIC_ID`** | string | | A globally unique ID | -| `SUBSONIC_NAME` | string | Value of `SUBSONIC_ID` | A vanity name | -| `SUBSONIC_ENABLE` | boolean | | | -| **`SUBSONIC_USER`** | string | | Username to login to the server with | -| **`SUBSONIC_PASSWORD`** | string | | Password for the user to login to the server with | -| **`SUBSONIC_URL`** | string | | URL of the subsonic media server to query | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ------------------------- | ------- | ---------------------- | ------------------------------------------------- | +| _**`SUBSONIC_ID`**_ | string | | A globally unique ID | +| `SUBSONIC_NAME` | string | Value of `SUBSONIC_ID` | A vanity name | +| `SUBSONIC_ENABLE` | boolean | | | +| _**`SUBSONIC_USER`**_ | string | | Username to login to the server with | +| _**`SUBSONIC_PASSWORD`**_ | string | | Password for the user to login to the server with | +| _**`SUBSONIC_URL`**_ | string | | URL of the subsonic media server to query | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_tealfm.md b/docsite/docs/configuration/sources/_env_configs/_tealfm.md index 80406cb0..ad15a3fc 100644 --- a/docsite/docs/configuration/sources/_env_configs/_tealfm.md +++ b/docsite/docs/configuration/sources/_env_configs/_tealfm.md @@ -1,7 +1,7 @@ -| Environmental Variable | Type | Default | Description | -| ------------------------------ | ------- | --------------------------- | -------------------------------- | -| **`SOURCE_TEALFM_ID`** | string | | A globally unique ID | -| `SOURCE_TEALFM_NAME` | string | Value of `SOURCE_TEALFM_ID` | A vanity name | -| `SOURCE_TEALFM_ENABLE` | boolean | | | -| **`SOURCE_TEALFM_IDENTIFIER`** | string | | Identify the account to login as | -| `SOURCE_TEALFM_APP_PW` | string | | | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| -------------------------------- | ------- | --------------------------- | -------------------------------- | +| _**`SOURCE_TEALFM_ID`**_ | string | | A globally unique ID | +| `SOURCE_TEALFM_NAME` | string | Value of `SOURCE_TEALFM_ID` | A vanity name | +| `SOURCE_TEALFM_ENABLE` | boolean | | | +| _**`SOURCE_TEALFM_IDENTIFIER`**_ | string | | Identify the account to login as | +| `SOURCE_TEALFM_APP_PW` | string | | | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_vlc.md b/docsite/docs/configuration/sources/_env_configs/_vlc.md index 01124b45..5418e50e 100644 --- a/docsite/docs/configuration/sources/_env_configs/_vlc.md +++ b/docsite/docs/configuration/sources/_env_configs/_vlc.md @@ -1,7 +1,7 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ----------------- | ---------------------------------------- | -| **`VLC_ID`** | string | | A globally unique ID | +| _**`VLC_ID`**_ | string | | A globally unique ID | | `VLC_NAME` | string | Value of `VLC_ID` | A vanity name | | `VLC_ENABLE` | boolean | | | | `VLC_URL` | string | localhost:8080 | URL:PORT of the VLC server to connect to | -| **`VLC_PASSWORD`** | string | | Password for the server | \ No newline at end of file +| _**`VLC_PASSWORD`**_ | string | | Password for the server | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_webscrobbler.md b/docsite/docs/configuration/sources/_env_configs/_webscrobbler.md index 3d36300c..79a7e4b1 100644 --- a/docsite/docs/configuration/sources/_env_configs/_webscrobbler.md +++ b/docsite/docs/configuration/sources/_env_configs/_webscrobbler.md @@ -1,6 +1,6 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | --------- | ---------------- | ----------------------------------------------------------- | -| **`WS_ID`** | string | | A globally unique ID | +| _**`WS_ID`**_ | string | | A globally unique ID | | `WS_NAME` | string | Value of `WS_ID` | A vanity name | | `WS_ENABLE` | boolean | | | | `WS_BLACKLIST` | transform | | Block scrobbling from specific WebScrobbler Connectors | diff --git a/docsite/docs/configuration/sources/_env_configs/_ymbridge.md b/docsite/docs/configuration/sources/_env_configs/_ymbridge.md index 2c73caec..5c6a0c35 100644 --- a/docsite/docs/configuration/sources/_env_configs/_ymbridge.md +++ b/docsite/docs/configuration/sources/_env_configs/_ymbridge.md @@ -1,7 +1,7 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ---------------------- | --------------------------------------------------------------------------- | -| **`YMBRIDGE_ID`** | string | | A globally unique ID | +| _**`YMBRIDGE_ID`**_ | string | | A globally unique ID | | `YMBRIDGE_NAME` | string | Value of `YMBRIDGE_ID` | A vanity name | | `YMBRIDGE_ENABLE` | boolean | | | -| **`YMBRIDGE_URL`** | string | | URL of the local Python bridge, for example http://yandex-music-bridge:9980 | +| _**`YMBRIDGE_URL`**_ | string | | URL of the local Python bridge, for example http://yandex-music-bridge:9980 | | `YMBRIDGE_API_KEY` | string | | Optional API key sent as X-API-Key to the bridge | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_ytmusic.md b/docsite/docs/configuration/sources/_env_configs/_ytmusic.md index f6a336a4..445e1f6d 100644 --- a/docsite/docs/configuration/sources/_env_configs/_ytmusic.md +++ b/docsite/docs/configuration/sources/_env_configs/_ytmusic.md @@ -1,6 +1,6 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ----------------- | ------------------------------------------------------------------------------------ | -| **`YTM_ID`** | string | | A globally unique ID | +| _**`YTM_ID`**_ | string | | A globally unique ID | | `YTM_NAME` | string | Value of `YTM_ID` | A vanity name | | `YTM_ENABLE` | boolean | | | | `YTM_REDIRECT_URI` | string | | Google Cloud Console project OAuth Client Authorized redirect URI | -- 2.51.2 From 699bd02782f18373485cd67e48d29aee00454dc2 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 23:41:56 +0000 Subject: [PATCH 18/35] refactor: Convert validation structures to dynamically import --- .../common/infrastructure/config/aioConfig.ts | 14 +- .../infrastructure/config/source/sources.ts | 144 +++--------- .../config/source/sourcesMap.ts | 212 ++++++++++++++++++ src/backend/index.ts | 4 +- src/backend/sources/ScrobbleSources.ts | 9 +- src/backend/tests/config/config.test.ts | 4 +- src/backend/utils/SchemaDocsStaticUtils.ts | 18 +- 7 files changed, 268 insertions(+), 137 deletions(-) create mode 100644 src/backend/common/infrastructure/config/source/sourcesMap.ts diff --git a/src/backend/common/infrastructure/config/aioConfig.ts b/src/backend/common/infrastructure/config/aioConfig.ts index 36175c03..630272a6 100644 --- a/src/backend/common/infrastructure/config/aioConfig.ts +++ b/src/backend/common/infrastructure/config/aioConfig.ts @@ -4,7 +4,6 @@ import {commonClientOptionsSchema} from "./client/index.ts"; import {requestRetryOptionsSchema} from "./common.ts"; import {webhookConfigSchema} from "./health/webhooks.ts"; import {commonSourceOptionsSchema, fileLogOptionsSchema, logLevelSchema, sourceRetryOptionsSchema} from "./source/index.ts"; -import {sourceAIOConfigSchema} from "./source/sources.ts"; import {cacheConfigUserSchema} from "../Atomic.ts"; import {retentionConfigDurationValueSchema} from "./database.ts"; @@ -29,6 +28,8 @@ const transformOptionsSchema = z.object({ ttl: z.string().optional(), }); +const looseComponent = z.looseObject({ type: z.string(), configureAs: z.string().optional() }); + const transformerCommonConfigSchema = z.object({ defaults: z.record(z.string(), z.any()).optional(), data: z.record(z.string(), z.any()).optional(), @@ -40,8 +41,8 @@ const transformerCommonConfigSchema = z.object({ export const aioConfigSchema = z.object({ sourceDefaults: sourceDefaultsSchema.optional(), clientDefaults: clientDefaultsSchema.optional(), - sources: z.array(sourceAIOConfigSchema).optional(), - clients: z.array(clientAIOConfigSchema).optional(), + sources: z.array(looseComponent).optional(), + clients: z.array(looseComponent).optional(), webhooks: z.array(webhookConfigSchema).optional(), @@ -134,13 +135,6 @@ export const aioClientRelaxedConfigSchema = z.object({ export type AIOClientRelaxedConfig = z.infer; -export const aioSourceConfigSchema = z.object({ - sourceDefaults: sourceRetryOptionsSchema.optional(), - sources: z.array(sourceAIOConfigSchema).optional(), -}); - -export type AIOSourceConfig = z.infer; - export const aioSourceRelaxedConfigSchema = z.object({ sourceDefaults: sourceRetryOptionsSchema.optional(), sources: z.array(z.looseObject({ type: z.string(), configureAs: z.string().optional() })).optional(), diff --git a/src/backend/common/infrastructure/config/source/sources.ts b/src/backend/common/infrastructure/config/source/sources.ts index 43081a23..3572203b 100644 --- a/src/backend/common/infrastructure/config/source/sources.ts +++ b/src/backend/common/infrastructure/config/source/sources.ts @@ -1,37 +1,34 @@ import * as z from "zod"; -import {azuracastSourceAIOConfigSchema, azuracastSourceConfigSchema, envSchemas as azuracastEnvSchemas, type AzuracastSourceAIOConfig, type AzuracastSourceConfig} from "./azuracast.ts"; -import {chromecastSourceAIOConfigSchema, chromecastSourceConfigSchema, envSchemas as chromecastEnvSchemas, type ChromecastSourceAIOConfig, type ChromecastSourceConfig} from "./chromecast.ts"; -import {listenbrainzEndpointSourceAIOConfigSchema, listenbrainzEndpointSourceConfigSchema, envSchemas as endpointlzEnvSchemas, type ListenbrainzEndpointSourceAIOConfig, type ListenbrainzEndpointSourceConfig} from "./endpointlz.ts"; -import {lastFmEndpointSourceAIOConfigSchema, lastFmEndpointSourceConfigSchema, envSchemas as endpointlfmEnvSchemas, type LastFMEndpointSourceAIOConfig, type LastFMEndpointSourceConfig} from "./endpointlfm.ts"; -import {deezerInternalSourceConfigSchema, deezerInternalAIOConfigSchema, envSchemas as deezerEnvSchemas, type DeezerInternalSourceConfig, type DeezerInternalAIOConfig} from "./deezer.ts"; -import {jellyApiSourceAIOConfigSchema, jellyApiSourceConfigSchema, envSchemas as jellyfinEnvSchemas, type JellyApiSourceAIOConfig, type JellyApiSourceConfig} from "./jellyfin.ts"; -import {jRiverSourceAIOConfigSchema, jRiverSourceConfigSchema, envSchemas as jriverEnvSchemas, type JRiverSourceAIOConfig, type JRiverSourceConfig} from "./jriver.ts"; -import {kodiSourceAIOConfigSchema, kodiSourceConfigSchema, envSchemas as kodiEnvSchemas, type KodiSourceAIOConfig, type KodiSourceConfig} from "./kodi.ts"; -import {lastFmSouceAIOConfigSchema, lastfmSourceConfigSchema, envSchemas as lastfmEnvSchemas, type LastFmSouceAIOConfig, type LastfmSourceConfig} from "./lastfm.ts"; -import {listenBrainzSourceAIOConfigSchema, listenBrainzSourceConfigSchema, envSchemas as listenBrainzEnvSchemas, type ListenBrainzSourceAIOConfig, type ListenBrainzSourceConfig} from "./listenbrainz.ts"; -import {mopidySourceAIOConfigSchema, mopidySourceConfigSchema, envSchemas as mopidyEnvSchemas, type MopidySourceAIOConfig, type MopidySourceConfig} from "./mopidy.ts"; -import {mpdSourceAIOConfigSchema, mpdSourceConfigSchema, envSchemas as mpdEnvSchemas, type MPDSourceAIOConfig, type MPDSourceConfig} from "./mpd.ts"; -import {mprisSourceAIOConfigSchema, mprisSourceConfigSchema, envSchemas as mprisEnvSchemas, type MPRISSourceAIOConfig, type MPRISSourceConfig} from "./mpris.ts"; -import {musikcubeSourceAIOConfigSchema, musikcubeSourceConfigSchema, envSchemas as musikcubeEnvSchemas, type MusikcubeSourceAIOConfig, type MusikcubeSourceConfig} from "./musikcube.ts"; -import {musicCastSourceConfigSchema, musicCastSourceAIOConfigSchema, envSchemas as musicCastEnvSchemas, type MusicCastSourceConfig, type MusicCastSourceAIOConfig} from "./musiccast.ts"; -import {plexApiSourceConfigSchema, plexApiSourceAIOConfigSchema, envSchemas as plexEnvSchemas, type PlexApiSourceConfig, type PlexApiSourceAIOConfig} from "./plex.ts"; -import {spotifySourceAIOConfigSchema, spotifySourceConfigSchema, envSchemas as spotifyEnvSchemas, type SpotifySourceAIOConfig, type SpotifySourceConfig} from "./spotify.ts"; -import {subsonicSourceAIOConfigSchema, subSonicSourceConfigSchema, envSchemas as subsonicEnvSchemas, type SubsonicSourceAIOConfig, type SubSonicSourceConfig} from "./subsonic.ts"; -import {vlcSourceAIOConfigSchema, vlcSourceConfigSchema, envSchemas as vlcEnvSchemas, type VLCSourceAIOConfig, type VLCSourceConfig} from "./vlc.ts"; -import {webScrobblerSourceAIOConfigSchema, webScrobblerSourceConfigSchema, envSchemas as webScrobblerEnvSchemas, type WebScrobblerSourceAIOConfig, type WebScrobblerSourceConfig} from "./webscrobbler.ts"; -import {ytMusicSourceAIOConfigSchema, ytMusicSourceConfigSchema, envSchemas as ytMusicEnvSchemas, type YTMusicSourceAIOConfig, type YTMusicSourceConfig} from "./ytmusic.ts"; -import {yandexMusicBridgeSourceAIOConfigSchema, yandexMusicBridgeSourceConfigSchema, envSchemas as ymbridgeEnvSchemas, type YandexMusicBridgeSourceAIOConfig, type YandexMusicBridgeSourceConfig} from "./ymbridge.ts"; -import {icecastSourceAIOConfigSchema, icecastSourceConfigSchema, envSchemas as icecastEnvSchemas, type IcecastSourceAIOConfig, type IcecastSourceConfig} from "./icecast.ts"; -import {koitoSourceAIOConfigSchema, koitoSourceConfigSchema, envSchemas as koitoEnvSchemas, type KoitoSourceAIOConfig, type KoitoSourceConfig} from "./koito.ts"; -import {malojaSourceAIOConfigSchema, malojaSourceConfigSchema, envSchemas as malojaEnvSchemas, type MalojaSourceAIOConfig, type MalojaSourceConfig} from "./maloja.ts"; -import {tealSourceAIOConfigSchema, tealSourceConfigSchema, envSchemas as tealEnvSchemas, type TealSourceAIOConfig, type TealSourceConfig} from "./tealfm.ts"; -import {rockskySourceAIOConfigSchema, rockskySourceConfigSchema, envSchemas as rockskyEnvSchemas, type RockskySourceAIOConfig, type RockskySourceConfig} from "./rocksky.ts"; -import {librefmSouceAIOConfigSchema, librefmSourceConfigSchema, envSchemas as librefmEnvSchemas, type LibrefmSouceAIOConfig, type LibrefmSourceConfig} from "./librefm.ts"; -import {sonosSourceAIOConfigSchema, sonosSourceConfigSchema, envSchemas as sonosEnvSchemas, type SonosSourceAIOConfig, type SonosSourceConfig} from "./sonos.ts"; -import {appleMusicSourceAIOConfigSchema, appleMusicSourceConfigSchema, envSchemas as appleMusicEnvSchemas, type AppleMusicSourceAIOConfig, type AppleMusicSourceConfig} from "./applemusic.ts"; -import type { SourceType } from "../../../../../core/Atomic.ts"; -import type { CommonSourceConfig, EnvSourceSchema } from "./index.ts"; -import { SimpleError } from "../../../errors/MSErrors.ts"; +import {azuracastSourceAIOConfigSchema, azuracastSourceConfigSchema} from "./azuracast.ts"; +import {chromecastSourceAIOConfigSchema, chromecastSourceConfigSchema} from "./chromecast.ts"; +import {listenbrainzEndpointSourceAIOConfigSchema, listenbrainzEndpointSourceConfigSchema} from "./endpointlz.ts"; +import {lastFmEndpointSourceAIOConfigSchema, lastFmEndpointSourceConfigSchema} from "./endpointlfm.ts"; +import {deezerInternalSourceConfigSchema, deezerInternalAIOConfigSchema} from "./deezer.ts"; +import {jellyApiSourceAIOConfigSchema, jellyApiSourceConfigSchema} from "./jellyfin.ts"; +import {jRiverSourceAIOConfigSchema, jRiverSourceConfigSchema} from "./jriver.ts"; +import {kodiSourceAIOConfigSchema, kodiSourceConfigSchema} from "./kodi.ts"; +import {lastFmSouceAIOConfigSchema, lastfmSourceConfigSchema} from "./lastfm.ts"; +import {listenBrainzSourceAIOConfigSchema, listenBrainzSourceConfigSchema} from "./listenbrainz.ts"; +import {mopidySourceAIOConfigSchema, mopidySourceConfigSchema} from "./mopidy.ts"; +import {mpdSourceAIOConfigSchema, mpdSourceConfigSchema} from "./mpd.ts"; +import {mprisSourceAIOConfigSchema, mprisSourceConfigSchema} from "./mpris.ts"; +import {musikcubeSourceAIOConfigSchema, musikcubeSourceConfigSchema} from "./musikcube.ts"; +import {musicCastSourceConfigSchema, musicCastSourceAIOConfigSchema} from "./musiccast.ts"; +import {plexApiSourceConfigSchema, plexApiSourceAIOConfigSchema} from "./plex.ts"; +import {spotifySourceAIOConfigSchema, spotifySourceConfigSchema} from "./spotify.ts"; +import {subsonicSourceAIOConfigSchema, subSonicSourceConfigSchema} from "./subsonic.ts"; +import {vlcSourceAIOConfigSchema, vlcSourceConfigSchema} from "./vlc.ts"; +import {webScrobblerSourceAIOConfigSchema, webScrobblerSourceConfigSchema} from "./webscrobbler.ts"; +import {ytMusicSourceAIOConfigSchema, ytMusicSourceConfigSchema} from "./ytmusic.ts"; +import {yandexMusicBridgeSourceAIOConfigSchema, yandexMusicBridgeSourceConfigSchema} from "./ymbridge.ts"; +import {icecastSourceAIOConfigSchema, icecastSourceConfigSchema} from "./icecast.ts"; +import {koitoSourceAIOConfigSchema, koitoSourceConfigSchema} from "./koito.ts"; +import {malojaSourceAIOConfigSchema, malojaSourceConfigSchema} from "./maloja.ts"; +import {tealSourceAIOConfigSchema, tealSourceConfigSchema} from "./tealfm.ts"; +import {rockskySourceAIOConfigSchema, rockskySourceConfigSchema} from "./rocksky.ts"; +import {librefmSouceAIOConfigSchema, librefmSourceConfigSchema} from "./librefm.ts"; +import {sonosSourceAIOConfigSchema, sonosSourceConfigSchema} from "./sonos.ts"; +import {appleMusicSourceAIOConfigSchema, appleMusicSourceConfigSchema} from "./applemusic.ts"; export const sourceConfigSchema = z.union([ spotifySourceConfigSchema, @@ -101,83 +98,4 @@ export const sourceAIOConfigSchema = z.union([ appleMusicSourceAIOConfigSchema, ]); -export type SourceAIOConfig = z.infer; - -export interface SourceTypeConfigMap extends Record>]> { - spotify: [SpotifySourceConfig, SpotifySourceAIOConfig, Partial>], - plex: [PlexApiSourceConfig, PlexApiSourceAIOConfig, Partial>], - deezer: [DeezerInternalSourceConfig, DeezerInternalAIOConfig, Partial>], - endpointlz: [ListenbrainzEndpointSourceConfig, ListenbrainzEndpointSourceAIOConfig, Partial>], - endpointlfm: [LastFMEndpointSourceConfig, LastFMEndpointSourceAIOConfig, Partial>], - icecast: [IcecastSourceConfig, IcecastSourceAIOConfig, Partial>], - subsonic: [SubSonicSourceConfig, SubsonicSourceAIOConfig, Partial>], - jellyfin: [JellyApiSourceConfig, JellyApiSourceAIOConfig, Partial>], - lastfm: [LastfmSourceConfig, LastFmSouceAIOConfig, Partial>], - librefm: [LibrefmSourceConfig, LibrefmSouceAIOConfig, Partial>], - ytmusic: [YTMusicSourceConfig, YTMusicSourceAIOConfig, Partial>], - ymbridge: [YandexMusicBridgeSourceConfig, YandexMusicBridgeSourceAIOConfig, Partial>], - maloja: [MalojaSourceConfig, MalojaSourceAIOConfig, Partial>], - mpris: [MPRISSourceConfig, MPRISSourceAIOConfig, Partial>], - mopidy: [MopidySourceConfig, MopidySourceAIOConfig, Partial>], - listenbrainz: [ListenBrainzSourceConfig, ListenBrainzSourceAIOConfig, Partial>], - jriver: [JRiverSourceConfig, JRiverSourceAIOConfig, Partial>], - kodi: [KodiSourceConfig, KodiSourceAIOConfig, Partial>], - chromecast: [ChromecastSourceConfig, ChromecastSourceAIOConfig, Partial>], - webscrobbler: [WebScrobblerSourceConfig, WebScrobblerSourceAIOConfig, Partial>], - musikcube: [MusikcubeSourceConfig, MusikcubeSourceAIOConfig, Partial>], - musiccast: [MusicCastSourceConfig, MusicCastSourceAIOConfig, Partial>], - mpd: [MPDSourceConfig, MPDSourceAIOConfig, Partial>], - vlc: [VLCSourceConfig, VLCSourceAIOConfig, Partial>], - azuracast: [AzuracastSourceConfig, AzuracastSourceAIOConfig, Partial>], - koito: [KoitoSourceConfig, KoitoSourceAIOConfig, Partial>], - tealfm: [TealSourceConfig, TealSourceAIOConfig, Partial>], - rocksky: [RockskySourceConfig, RockskySourceAIOConfig, Partial>], - sonos: [SonosSourceConfig, SonosSourceAIOConfig, Partial>], - applemusic: [AppleMusicSourceConfig, AppleMusicSourceAIOConfig, Partial>] -} - -export const sourceConfigSchemaMap: { [K in keyof SourceTypeConfigMap]: [z.ZodType, z.ZodType, EnvSourceSchema] } = { - spotify: [spotifySourceConfigSchema, spotifySourceAIOConfigSchema, spotifyEnvSchemas], - plex: [plexApiSourceConfigSchema, plexApiSourceAIOConfigSchema, plexEnvSchemas], - deezer: [deezerInternalSourceConfigSchema, deezerInternalAIOConfigSchema, deezerEnvSchemas], - endpointlz: [listenbrainzEndpointSourceConfigSchema, listenbrainzEndpointSourceAIOConfigSchema, endpointlzEnvSchemas], - endpointlfm: [lastFmEndpointSourceConfigSchema, lastFmEndpointSourceAIOConfigSchema, endpointlfmEnvSchemas], - icecast: [icecastSourceConfigSchema, icecastSourceAIOConfigSchema, icecastEnvSchemas], - subsonic: [subSonicSourceConfigSchema, subsonicSourceAIOConfigSchema, subsonicEnvSchemas], - jellyfin: [jellyApiSourceConfigSchema, jellyApiSourceAIOConfigSchema, jellyfinEnvSchemas], - lastfm: [lastfmSourceConfigSchema, lastFmSouceAIOConfigSchema, lastfmEnvSchemas], - librefm: [librefmSourceConfigSchema, librefmSouceAIOConfigSchema, librefmEnvSchemas], - ytmusic: [ytMusicSourceConfigSchema, ytMusicSourceAIOConfigSchema, ytMusicEnvSchemas], - ymbridge: [yandexMusicBridgeSourceConfigSchema, yandexMusicBridgeSourceAIOConfigSchema, ymbridgeEnvSchemas], - maloja: [malojaSourceConfigSchema, malojaSourceAIOConfigSchema, malojaEnvSchemas], - mpris: [mprisSourceConfigSchema, mprisSourceAIOConfigSchema, mprisEnvSchemas], - mopidy: [mopidySourceConfigSchema, mopidySourceAIOConfigSchema, mopidyEnvSchemas], - listenbrainz: [listenBrainzSourceConfigSchema, listenBrainzSourceAIOConfigSchema, listenBrainzEnvSchemas], - jriver: [jRiverSourceConfigSchema, jRiverSourceAIOConfigSchema, jriverEnvSchemas], - kodi: [kodiSourceConfigSchema, kodiSourceAIOConfigSchema, kodiEnvSchemas], - chromecast: [chromecastSourceConfigSchema, chromecastSourceAIOConfigSchema, chromecastEnvSchemas], - webscrobbler: [webScrobblerSourceConfigSchema, webScrobblerSourceAIOConfigSchema, webScrobblerEnvSchemas], - musikcube: [musikcubeSourceConfigSchema, musikcubeSourceAIOConfigSchema, musikcubeEnvSchemas], - musiccast: [musicCastSourceConfigSchema, musicCastSourceAIOConfigSchema, musicCastEnvSchemas], - mpd: [mpdSourceConfigSchema, mpdSourceAIOConfigSchema, mpdEnvSchemas], - vlc: [vlcSourceConfigSchema, vlcSourceAIOConfigSchema, vlcEnvSchemas], - azuracast: [azuracastSourceConfigSchema, azuracastSourceAIOConfigSchema, azuracastEnvSchemas], - koito: [koitoSourceConfigSchema, koitoSourceAIOConfigSchema, koitoEnvSchemas], - tealfm: [tealSourceConfigSchema, tealSourceAIOConfigSchema, tealEnvSchemas], - rocksky: [rockskySourceConfigSchema, rockskySourceAIOConfigSchema, rockskyEnvSchemas], - sonos: [sonosSourceConfigSchema, sonosSourceAIOConfigSchema, sonosEnvSchemas], - applemusic: [appleMusicSourceConfigSchema, appleMusicSourceAIOConfigSchema, appleMusicEnvSchemas] -}; - -export const validateSourceJson = (sourceType: T, json: object): SourceTypeConfigMap[T][0] => { - if(sourceConfigSchemaMap[sourceType] === undefined) { - throw new SimpleError(`No Source has a 'type' of '${sourceType}'`); - } - return sourceConfigSchemaMap[sourceType][0].parse(json) -}; -export const validateSourceAIOJson = (sourceType: T, json: object): SourceTypeConfigMap[T][1] => { - if(sourceConfigSchemaMap[sourceType] === undefined) { - throw new SimpleError(`No Source has a 'type' of '${sourceType}'`); - } - return sourceConfigSchemaMap[sourceType][1].parse(json) -}; \ No newline at end of file +export type SourceAIOConfig = z.infer; \ No newline at end of file diff --git a/src/backend/common/infrastructure/config/source/sourcesMap.ts b/src/backend/common/infrastructure/config/source/sourcesMap.ts new file mode 100644 index 00000000..1a7aeb11 --- /dev/null +++ b/src/backend/common/infrastructure/config/source/sourcesMap.ts @@ -0,0 +1,212 @@ +import type { EnvSourceSchema } from "./index.ts"; +import type {ZodType, ZodObject} from 'zod'; +import type { AzuracastSourceAIOConfig, AzuracastSourceConfig} from "./azuracast.ts"; +import type { ChromecastSourceAIOConfig, ChromecastSourceConfig} from "./chromecast.ts"; +import type { ListenbrainzEndpointSourceAIOConfig, ListenbrainzEndpointSourceConfig} from "./endpointlz.ts"; +import type { LastFMEndpointSourceAIOConfig, LastFMEndpointSourceConfig} from "./endpointlfm.ts"; +import type { DeezerInternalSourceConfig, DeezerInternalAIOConfig} from "./deezer.ts"; +import type { JellyApiSourceAIOConfig, JellyApiSourceConfig} from "./jellyfin.ts"; +import type { JRiverSourceAIOConfig, JRiverSourceConfig} from "./jriver.ts"; +import type { KodiSourceAIOConfig, KodiSourceConfig} from "./kodi.ts"; +import type { LastFmSouceAIOConfig, LastfmSourceConfig} from "./lastfm.ts"; +import type { ListenBrainzSourceAIOConfig, ListenBrainzSourceConfig} from "./listenbrainz.ts"; +import type { MopidySourceAIOConfig, MopidySourceConfig} from "./mopidy.ts"; +import type { MPDSourceAIOConfig, MPDSourceConfig} from "./mpd.ts"; +import type { MPRISSourceAIOConfig, MPRISSourceConfig} from "./mpris.ts"; +import type { MusikcubeSourceAIOConfig, MusikcubeSourceConfig} from "./musikcube.ts"; +import type { MusicCastSourceConfig, MusicCastSourceAIOConfig} from "./musiccast.ts"; +import type { PlexApiSourceConfig, PlexApiSourceAIOConfig} from "./plex.ts"; +import type { SpotifySourceAIOConfig, SpotifySourceConfig} from "./spotify.ts"; +import type { SubsonicSourceAIOConfig, SubSonicSourceConfig} from "./subsonic.ts"; +import type { VLCSourceAIOConfig, VLCSourceConfig} from "./vlc.ts"; +import type { WebScrobblerSourceAIOConfig, WebScrobblerSourceConfig} from "./webscrobbler.ts"; +import type { YTMusicSourceAIOConfig, YTMusicSourceConfig} from "./ytmusic.ts"; +import type { YandexMusicBridgeSourceAIOConfig, YandexMusicBridgeSourceConfig} from "./ymbridge.ts"; +import type { IcecastSourceAIOConfig, IcecastSourceConfig} from "./icecast.ts"; +import type { KoitoSourceAIOConfig, KoitoSourceConfig} from "./koito.ts"; +import type { MalojaSourceAIOConfig, MalojaSourceConfig} from "./maloja.ts"; +import type { TealSourceAIOConfig, TealSourceConfig} from "./tealfm.ts"; +import type { RockskySourceAIOConfig, RockskySourceConfig} from "./rocksky.ts"; +import type { LibrefmSouceAIOConfig, LibrefmSourceConfig} from "./librefm.ts"; +import type { SonosSourceAIOConfig, SonosSourceConfig} from "./sonos.ts"; +import type { AppleMusicSourceAIOConfig, AppleMusicSourceConfig} from "./applemusic.ts"; +import type { SourceType } from "../../../../../core/Atomic.ts"; +import type { CommonSourceConfig } from "./index.ts"; +import type { SourceAIOConfig } from "./sources.ts"; +import { SimpleError } from "../../../errors/MSErrors.ts"; + +export interface SourceTypeConfigMap extends Record>]> { + spotify: [SpotifySourceConfig, SpotifySourceAIOConfig, Partial>]; + plex: [PlexApiSourceConfig, PlexApiSourceAIOConfig, Partial>]; + deezer: [DeezerInternalSourceConfig, DeezerInternalAIOConfig, Partial>]; + endpointlz: [ListenbrainzEndpointSourceConfig, ListenbrainzEndpointSourceAIOConfig, Partial>]; + endpointlfm: [LastFMEndpointSourceConfig, LastFMEndpointSourceAIOConfig, Partial>]; + icecast: [IcecastSourceConfig, IcecastSourceAIOConfig, Partial>]; + subsonic: [SubSonicSourceConfig, SubsonicSourceAIOConfig, Partial>]; + jellyfin: [JellyApiSourceConfig, JellyApiSourceAIOConfig, Partial>]; + lastfm: [LastfmSourceConfig, LastFmSouceAIOConfig, Partial>]; + librefm: [LibrefmSourceConfig, LibrefmSouceAIOConfig, Partial>]; + ytmusic: [YTMusicSourceConfig, YTMusicSourceAIOConfig, Partial>]; + ymbridge: [YandexMusicBridgeSourceConfig, YandexMusicBridgeSourceAIOConfig, Partial>]; + maloja: [MalojaSourceConfig, MalojaSourceAIOConfig, Partial>]; + mpris: [MPRISSourceConfig, MPRISSourceAIOConfig, Partial>]; + mopidy: [MopidySourceConfig, MopidySourceAIOConfig, Partial>]; + listenbrainz: [ListenBrainzSourceConfig, ListenBrainzSourceAIOConfig, Partial>]; + jriver: [JRiverSourceConfig, JRiverSourceAIOConfig, Partial>]; + kodi: [KodiSourceConfig, KodiSourceAIOConfig, Partial>]; + chromecast: [ChromecastSourceConfig, ChromecastSourceAIOConfig, Partial>]; + webscrobbler: [WebScrobblerSourceConfig, WebScrobblerSourceAIOConfig, Partial>]; + musikcube: [MusikcubeSourceConfig, MusikcubeSourceAIOConfig, Partial>]; + musiccast: [MusicCastSourceConfig, MusicCastSourceAIOConfig, Partial>]; + mpd: [MPDSourceConfig, MPDSourceAIOConfig, Partial>]; + vlc: [VLCSourceConfig, VLCSourceAIOConfig, Partial>]; + azuracast: [AzuracastSourceConfig, AzuracastSourceAIOConfig, Partial>]; + koito: [KoitoSourceConfig, KoitoSourceAIOConfig, Partial>]; + tealfm: [TealSourceConfig, TealSourceAIOConfig, Partial>]; + rocksky: [RockskySourceConfig, RockskySourceAIOConfig, Partial>]; + sonos: [SonosSourceConfig, SonosSourceAIOConfig, Partial>]; + applemusic: [AppleMusicSourceConfig, AppleMusicSourceAIOConfig, Partial>]; +} + +export const sourceConfigSchemaMapAsync: { [K in keyof SourceTypeConfigMap]: () => Promise<[ZodType, ZodType, EnvSourceSchema]> } = { + spotify: async () => { + const {spotifySourceConfigSchema, spotifySourceAIOConfigSchema, envSchemas } = (await import('./spotify.ts')); + return [spotifySourceConfigSchema, spotifySourceAIOConfigSchema, envSchemas] + }, + plex: async () => { + const {plexApiSourceConfigSchema, plexApiSourceAIOConfigSchema, envSchemas } = (await import('./plex.ts')); + return [plexApiSourceConfigSchema, plexApiSourceAIOConfigSchema, envSchemas] + }, + deezer: async () => { + const {deezerInternalSourceConfigSchema, deezerInternalAIOConfigSchema, envSchemas } = (await import('./deezer.ts')); + return [deezerInternalSourceConfigSchema, deezerInternalAIOConfigSchema, envSchemas] + }, + endpointlz: async () => { + const {listenbrainzEndpointSourceConfigSchema, listenbrainzEndpointSourceAIOConfigSchema, envSchemas } = (await import('./endpointlz.ts')); + return [listenbrainzEndpointSourceConfigSchema, listenbrainzEndpointSourceAIOConfigSchema, envSchemas] + }, + endpointlfm: async () => { + const {lastFmEndpointSourceConfigSchema, lastFmEndpointSourceAIOConfigSchema, envSchemas } = (await import('./endpointlfm.ts')); + return [lastFmEndpointSourceConfigSchema, lastFmEndpointSourceAIOConfigSchema, envSchemas] + }, + icecast: async () => { + const {icecastSourceConfigSchema, icecastSourceAIOConfigSchema, envSchemas } = (await import('./icecast.ts')); + return [icecastSourceConfigSchema, icecastSourceAIOConfigSchema, envSchemas] + }, + subsonic: async () => { + const {subSonicSourceConfigSchema, subsonicSourceAIOConfigSchema, envSchemas } = (await import('./subsonic.ts')); + return [subSonicSourceConfigSchema, subsonicSourceAIOConfigSchema, envSchemas] + }, + jellyfin: async () => { + const {jellyApiSourceConfigSchema, jellyApiSourceAIOConfigSchema, envSchemas } = (await import('./jellyfin.ts')); + return [jellyApiSourceConfigSchema, jellyApiSourceAIOConfigSchema, envSchemas] + }, + lastfm: async () => { + const {lastfmSourceConfigSchema, lastFmSouceAIOConfigSchema, envSchemas } = (await import('./lastfm.ts')); + return [lastfmSourceConfigSchema, lastFmSouceAIOConfigSchema, envSchemas] + }, + librefm: async () => { + const {librefmSourceConfigSchema, librefmSouceAIOConfigSchema, envSchemas } = (await import('./librefm.ts')); + return [librefmSourceConfigSchema, librefmSouceAIOConfigSchema, envSchemas] + }, + ytmusic: async () => { + const {ytMusicSourceConfigSchema, ytMusicSourceAIOConfigSchema, envSchemas } = (await import('./ytmusic.ts')); + return [ytMusicSourceConfigSchema, ytMusicSourceAIOConfigSchema, envSchemas] + }, + ymbridge: async () => { + const {yandexMusicBridgeSourceConfigSchema, yandexMusicBridgeSourceAIOConfigSchema, envSchemas } = (await import('./ymbridge.ts')); + return [yandexMusicBridgeSourceConfigSchema, yandexMusicBridgeSourceAIOConfigSchema, envSchemas] + }, + maloja: async () => { + const {malojaSourceConfigSchema, malojaSourceAIOConfigSchema, envSchemas } = (await import('./maloja.ts')); + return [malojaSourceConfigSchema, malojaSourceAIOConfigSchema, envSchemas] + }, + mpris: async () => { + const {mprisSourceConfigSchema, mprisSourceAIOConfigSchema, envSchemas } = (await import('./mpris.ts')); + return [mprisSourceConfigSchema, mprisSourceAIOConfigSchema, envSchemas] + }, + mopidy: async () => { + const {mopidySourceConfigSchema, mopidySourceAIOConfigSchema, envSchemas } = (await import('./mopidy.ts')); + return [mopidySourceConfigSchema, mopidySourceAIOConfigSchema, envSchemas] + }, + listenbrainz: async () => { + const {listenBrainzSourceConfigSchema, listenBrainzSourceAIOConfigSchema, envSchemas } = (await import('./listenbrainz.ts')); + return [listenBrainzSourceConfigSchema, listenBrainzSourceAIOConfigSchema, envSchemas] + }, + jriver: async () => { + const {jRiverSourceConfigSchema, jRiverSourceAIOConfigSchema, envSchemas } = (await import('./jriver.ts')); + return [jRiverSourceConfigSchema, jRiverSourceAIOConfigSchema, envSchemas] + }, + kodi: async () => { + const {kodiSourceConfigSchema, kodiSourceAIOConfigSchema, envSchemas } = (await import('./kodi.ts')); + return [kodiSourceConfigSchema, kodiSourceAIOConfigSchema, envSchemas] + }, + chromecast: async () => { + const {chromecastSourceConfigSchema, chromecastSourceAIOConfigSchema, envSchemas } = (await import('./chromecast.ts')); + return [chromecastSourceConfigSchema, chromecastSourceAIOConfigSchema, envSchemas] + }, + webscrobbler: async () => { + const {webScrobblerSourceConfigSchema, webScrobblerSourceAIOConfigSchema, envSchemas } = (await import('./webscrobbler.ts')); + return [webScrobblerSourceConfigSchema, webScrobblerSourceAIOConfigSchema, envSchemas] + }, + musikcube: async () => { + const {musikcubeSourceConfigSchema, musikcubeSourceAIOConfigSchema, envSchemas } = (await import('./musikcube.ts')); + return [musikcubeSourceConfigSchema, musikcubeSourceAIOConfigSchema, envSchemas] + }, + musiccast: async () => { + const {musicCastSourceConfigSchema, musicCastSourceAIOConfigSchema, envSchemas } = (await import('./musiccast.ts')); + return [musicCastSourceConfigSchema, musicCastSourceAIOConfigSchema, envSchemas] + }, + mpd: async () => { + const {mpdSourceConfigSchema, mpdSourceAIOConfigSchema, envSchemas } = (await import('./mpd.ts')); + return [mpdSourceConfigSchema, mpdSourceAIOConfigSchema, envSchemas] + }, + vlc: async () => { + const {vlcSourceConfigSchema, vlcSourceAIOConfigSchema, envSchemas } = (await import('./vlc.ts')); + return [vlcSourceConfigSchema, vlcSourceAIOConfigSchema, envSchemas] + }, + azuracast: async () => { + const {azuracastSourceConfigSchema, azuracastSourceAIOConfigSchema, envSchemas } = (await import('./azuracast.ts')); + return [azuracastSourceConfigSchema, azuracastSourceAIOConfigSchema, envSchemas] + }, + koito: async () => { + const {koitoSourceConfigSchema, koitoSourceAIOConfigSchema, envSchemas } = (await import('./koito.ts')); + return [koitoSourceConfigSchema, koitoSourceAIOConfigSchema, envSchemas] + }, + tealfm: async () => { + const {tealSourceConfigSchema, tealSourceAIOConfigSchema, envSchemas } = (await import('./tealfm.ts')); + return [tealSourceConfigSchema, tealSourceAIOConfigSchema, envSchemas] + }, + rocksky: async () => { + const {rockskySourceConfigSchema, rockskySourceAIOConfigSchema, envSchemas } = (await import('./rocksky.ts')); + return [rockskySourceConfigSchema, rockskySourceAIOConfigSchema, envSchemas] + }, + sonos: async () => { + const {sonosSourceConfigSchema, sonosSourceAIOConfigSchema, envSchemas } = (await import('./sonos.ts')); + return [sonosSourceConfigSchema, sonosSourceAIOConfigSchema, envSchemas] + }, + applemusic: async () => { + const {appleMusicSourceConfigSchema, appleMusicSourceAIOConfigSchema, envSchemas } = (await import('./applemusic.ts')); + return [appleMusicSourceConfigSchema, appleMusicSourceAIOConfigSchema, envSchemas] + }, +}; + +export const validateSourceJson = async (sourceType: T, json: object): Promise => { + if(sourceConfigSchemaMapAsync[sourceType] === undefined) { + throw new SimpleError(`No Source has a 'type' of '${sourceType}'`); + } + return (await sourceConfigSchemaMapAsync[sourceType]())[0].parse(json); +}; +export const validateSourceAIOJson = async (sourceType: T, json: object): Promise => { + if(sourceConfigSchemaMapAsync[sourceType] === undefined) { + throw new SimpleError(`No Source has a 'type' of '${sourceType}'`); + } + return (await sourceConfigSchemaMapAsync[sourceType]())[1].parse(json) +}; + +export const getSourceEnvSchema = async (sourceType: T): Promise> => { + if(sourceConfigSchemaMapAsync[sourceType] === undefined) { + throw new SimpleError(`No Source has a 'type' of '${sourceType}'`); + } + return (await sourceConfigSchemaMapAsync[sourceType]())[2]; +}; \ No newline at end of file diff --git a/src/backend/index.ts b/src/backend/index.ts index 2ce4c471..e4a207e2 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -194,11 +194,11 @@ const dataDir = getDataDir(); /* * setup clients * */ - await scrobbleClients.buildClientsFromConfig(notifiers); + await scrobbleClients.buildClientsFromConfig(); /* * setup sources * */ - await scrobbleSources.buildSourcesFromConfig([]); + await scrobbleSources.buildSourcesFromConfig(); // check ambiguous client/source types like this for now const lastfmSources = scrobbleSources.getByType('lastfm'); diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts index a81d53cd..837c567a 100644 --- a/src/backend/sources/ScrobbleSources.ts +++ b/src/backend/sources/ScrobbleSources.ts @@ -6,7 +6,6 @@ import { isSourceType } from "../../core/Atomic.ts"; import { sourceTypes } from "../../core/Atomic.ts"; import type {SourceType} from "../../core/Atomic.ts"; import {aioSourceRelaxedConfigSchema, type AIOSourceRelaxedConfig, type SourceDefaults} from "../common/infrastructure/config/aioConfig.ts"; -import {sourceConfigSchemaMap, validateSourceAIOJson, validateSourceJson, type SourceTypeConfigMap} from "../common/infrastructure/config/source/sources.ts"; import type { WildcardEmitter } from "../common/WildcardEmitter.ts"; import { pick } from '../../core/DataUtils.ts'; import { readJson } from '../utils/DataUtils.ts'; @@ -16,6 +15,8 @@ import type {CommonSourceConfig, CommonSourceOptions} from '../common/infrastruc import type {ExternalMetadataTerm, PlayTransformHooks} from '../../core/Transform.ts'; import { prettifyError, ZodError } from 'zod'; import { commonComponentEnvConfigToConfigPrimitives, generateCommonComponentEnvConfigSchema, type CommonConfigPrimitives } from '../common/infrastructure/config/common.ts'; +import { getSourceEnvSchema, validateSourceAIOJson, validateSourceJson } from '../common/infrastructure/config/source/sourcesMap.ts'; +import type { SourceTypeConfigMap } from "../common/infrastructure/config/source/sourcesMap.ts"; type UnparsedConfig = {config: object, type: SourceType, source?: 'file' | 'aio' | 'env', pos: string}; @@ -217,7 +218,7 @@ export default class ScrobbleSources { const sourceStr = `${entry.source} ${entry.pos}`; switch (entry.source) { case 'env': { - const envSchema = sourceConfigSchemaMap[configType][2]; + const envSchema = await getSourceEnvSchema(configType); // sourceConfigSchemaMap[configType][2]; const primitiveSchema = generateCommonComponentEnvConfigSchema(envSchema.prefix.toUpperCase()); const parsed = primitiveSchema.parse(entry.config); const primitives: CommonConfigPrimitives = commonComponentEnvConfigToConfigPrimitives(envSchema.prefix.toUpperCase(), parsed); @@ -241,7 +242,7 @@ export default class ScrobbleSources { this.logger.debug(`Skipping ${configType} Config ${entry.source} ${entry.pos} because it is configured as a Source`); continue; } - const parsed = entry.source === 'file' ? validateSourceJson(entry.type, entry.config) : validateSourceAIOJson(entry.type, entry.config); + const parsed = entry.source === 'file' ? (await validateSourceJson(entry.type, entry.config)) : (await validateSourceAIOJson(entry.type, entry.config)); parsedConfig = { ...parsed, source: sourceStr @@ -277,7 +278,7 @@ export default class ScrobbleSources { ) => { for (const s of strongConfigs) { try { - const config = sourceConfigSchemaMap[sourceType][0].parse(s); + const config = await validateSourceJson(sourceType, s); // sourceConfigSchemaMap[sourceType][0].parse(s); const compositeOptions = { ...defaults, ...config.options }; const newComponent = new Ctor(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); newComponent.logger.info(`Source added from ${s.source}`); diff --git a/src/backend/tests/config/config.test.ts b/src/backend/tests/config/config.test.ts index dabc7163..b23d6b57 100644 --- a/src/backend/tests/config/config.test.ts +++ b/src/backend/tests/config/config.test.ts @@ -12,7 +12,7 @@ import { clientTypes } from "../../../core/Atomic.ts"; import { projectRootDir } from "../../common/infrastructure/Atomic.ts"; import { sourceTypes } from "../../../core/Atomic.ts"; import { difference } from '../../utils.ts'; -import { validateSourceJson } from '../../common/infrastructure/config/source/sources.ts'; +import { validateSourceJson } from '../../common/infrastructure/config/source/sourcesMap.ts'; import { readJson } from '../../utils/DataUtils.ts'; import { prettifyError, ZodError } from 'zod'; import { validateClientJson } from '../../common/infrastructure/config/client/clients.ts'; @@ -68,7 +68,7 @@ describe('Sample Configs', function () { fileContents = fileContents.filter(x => x.configureAs === undefined || x.configureAs === 'source'); for (const [i,rawConf] of fileContents.entries()) { try { - validateSourceJson(componentType, rawConf); + await validateSourceJson(componentType, rawConf); } catch (e) { if(e instanceof ZodError) { expect.fail(`Validation failed for config entry ${i}:\n${prettifyError(e)}`); diff --git a/src/backend/utils/SchemaDocsStaticUtils.ts b/src/backend/utils/SchemaDocsStaticUtils.ts index ad1efcfa..616a98c5 100644 --- a/src/backend/utils/SchemaDocsStaticUtils.ts +++ b/src/backend/utils/SchemaDocsStaticUtils.ts @@ -2,12 +2,13 @@ import { writeFileSync, mkdirSync } from "node:fs"; import { resolve } from "path"; import * as z from 'zod'; import { clientConfigSchemaMap } from '../common/infrastructure/config/client/clients.ts'; -import { sourceConfigSchemaMap } from '../common/infrastructure/config/source/sources.ts'; import { projectRootDir } from "../common/infrastructure/Atomic.ts"; import { aioConfigSchema } from "../common/infrastructure/config/aioConfig.ts"; import { generateCommonComponentEnvConfigSchema } from "../common/infrastructure/config/common.ts"; import { zodObjectToTableColumns, type TableColumn } from "./ZodUtils.ts"; import {markdownTable} from 'markdown-table' +import { sourceAIOConfigSchema } from "../common/infrastructure/config/source/sources.ts"; +import { sourceConfigSchemaMapAsync } from "../common/infrastructure/config/source/sourcesMap.ts"; mkdirSync(resolve(projectRootDir, 'docsite/static/schemas'), {recursive: true}); @@ -67,13 +68,14 @@ for(const [k,v] of clientEntries) { writeFileSync(resolve(projectRootDir, `docsite/docs/configuration/clients/_env_configs/_${k}.md`), tableContent); } -const sourcesEntries = Object.entries(sourceConfigSchemaMap); +const sourcesEntries = Object.entries(sourceConfigSchemaMapAsync); for(const [k,v] of sourcesEntries) { - writeFileSync(resolve(projectRootDir, `docsite/static/schemas/${k}-source.json`), JSON.stringify(generateSchema(z.array(v[0])))); + const [fileSchema, aioSchema, envSchemas] = await v(); + writeFileSync(resolve(projectRootDir, `docsite/static/schemas/${k}-source.json`), JSON.stringify(generateSchema(z.array(fileSchema)))); - const envSchema = v[2]; + const envSchema = envSchemas; const common = generateCommonComponentEnvConfigSchema(envSchema.prefix.toUpperCase()); - const col = zodObjectToTableColumns(z.object({...common.shape,...v[2].env.shape}), 'out'); + const col = zodObjectToTableColumns(z.object({...common.shape,...envSchema.env.shape}), 'out'); const tableContent = markdownTable([ ['Environmental Variable', 'Type', 'Default', 'Description'], ...mdCols(col) @@ -81,4 +83,8 @@ for(const [k,v] of sourcesEntries) { writeFileSync(resolve(projectRootDir, `docsite/docs/configuration/sources/_env_configs/_${k}.md`), tableContent); } -writeFileSync(resolve(projectRootDir, 'docsite/static/schemas/aio.json'), JSON.stringify(generateSchema(aioConfigSchema, 'ref'))); +const aioStrongConfigSchema = aioConfigSchema.extend({ + sources: z.array(sourceAIOConfigSchema).optional() +}) + +writeFileSync(resolve(projectRootDir, 'docsite/static/schemas/aio.json'), JSON.stringify(generateSchema(aioStrongConfigSchema, 'ref'))); -- 2.51.2 From 02ab1571b677594c886f5a9a139589fbd4fc22e1 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 23:57:59 +0000 Subject: [PATCH 19/35] refactor: Convert validation client structures to dynamically import --- .../common/infrastructure/config/aioConfig.ts | 8 -- .../infrastructure/config/client/clients.ts | 56 +++---------- .../config/client/clientsMap.ts | 79 +++++++++++++++++++ src/backend/scrobblers/ScrobbleClients.ts | 8 +- src/backend/sources/ScrobbleSources.ts | 4 +- src/backend/tests/config/config.test.ts | 4 +- src/backend/utils/SchemaDocsStaticUtils.ts | 11 +-- 7 files changed, 102 insertions(+), 68 deletions(-) create mode 100644 src/backend/common/infrastructure/config/client/clientsMap.ts diff --git a/src/backend/common/infrastructure/config/aioConfig.ts b/src/backend/common/infrastructure/config/aioConfig.ts index 630272a6..dcd21126 100644 --- a/src/backend/common/infrastructure/config/aioConfig.ts +++ b/src/backend/common/infrastructure/config/aioConfig.ts @@ -1,5 +1,4 @@ import * as z from "zod"; -import {clientAIOConfigSchema} from "./client/clients.ts"; import {commonClientOptionsSchema} from "./client/index.ts"; import {requestRetryOptionsSchema} from "./common.ts"; import {webhookConfigSchema} from "./health/webhooks.ts"; @@ -118,13 +117,6 @@ export const aioConfigSchema = z.object({ export type AIOConfig = z.infer; -export const aioClientConfigSchema = z.object({ - clientDefaults: requestRetryOptionsSchema.optional(), - clients: z.array(clientAIOConfigSchema).optional(), -}); - -export type AIOClientConfig = z.infer; - export const aioClientRelaxedConfigSchema = z.object({ clientDefaults: requestRetryOptionsSchema.optional(), clients: z.array(z.looseObject({ type: z.string(), configureAs: z.string().optional() })).optional(), diff --git a/src/backend/common/infrastructure/config/client/clients.ts b/src/backend/common/infrastructure/config/client/clients.ts index 9c34eded..cda106cd 100644 --- a/src/backend/common/infrastructure/config/client/clients.ts +++ b/src/backend/common/infrastructure/config/client/clients.ts @@ -1,15 +1,12 @@ import * as z from "zod"; -import {koitoClientAIOConfigSchema, koitoClientConfigSchema, envSchemas as koitoEnvSchemas, type KoitoClientAIOConfig, type KoitoClientConfig} from "./koito.ts"; -import {lastfmClientAIOConfigSchema, lastfmClientConfigSchema, envSchemas as lastfmEnvSchemas, type LastfmClientAIOConfig, type LastfmClientConfig} from "./lastfm.ts"; -import {listenBrainzClientAIOConfigSchema, listenBrainzClientConfigSchema, envSchemas as listenBrainzEnvSchemas, type ListenBrainzClientAIOConfig, type ListenBrainzClientConfig} from "./listenbrainz.ts"; -import {malojaClientAIOConfigSchema, malojaClientConfigSchema, envSchemas as malojaEnvSchemas, type MalojaClientAIOConfig, type MalojaClientConfig} from "./maloja.ts"; -import {tealClientAIOConfigSchema, tealClientConfigSchema, envSchemas as tealEnvSchemas, type TealClientAIOConfig, type TealClientConfig} from "./tealfm.ts"; -import {rockSkyClientAIOConfigSchema, rockSkyClientConfigSchema, envSchemas as rockSkyEnvSchemas, type RockSkyClientAIOConfig, type RockSkyClientConfig} from "./rocksky.ts"; -import {librefmClientAIOConfigSchema, librefmClientConfigSchema, envSchemas as librefmEnvSchemas, type LibrefmClientAIOConfig, type LibrefmClientConfig} from "./librefm.ts"; -import {discordClientAIOConfigSchema, discordClientConfigSchema, envSchemas as discordEnvSchemas, type DiscordClientAIOConfig, type DiscordClientConfig} from "./discord.ts"; -import type { CommonClientConfig, EnvClientSchema } from "./index.ts"; -import type { ClientType } from "../../../../../core/Atomic.ts"; -import { SimpleError } from "../../../errors/MSErrors.ts"; +import {koitoClientAIOConfigSchema, koitoClientConfigSchema} from "./koito.ts"; +import {lastfmClientAIOConfigSchema, lastfmClientConfigSchema} from "./lastfm.ts"; +import {listenBrainzClientAIOConfigSchema, listenBrainzClientConfigSchema} from "./listenbrainz.ts"; +import {malojaClientAIOConfigSchema, malojaClientConfigSchema} from "./maloja.ts"; +import {tealClientAIOConfigSchema, tealClientConfigSchema} from "./tealfm.ts"; +import {rockSkyClientAIOConfigSchema, rockSkyClientConfigSchema} from "./rocksky.ts"; +import {librefmClientAIOConfigSchema, librefmClientConfigSchema} from "./librefm.ts"; +import {discordClientAIOConfigSchema, discordClientConfigSchema} from "./discord.ts"; export const clientConfigSchema = z.union([ malojaClientConfigSchema, @@ -35,39 +32,4 @@ export const clientAIOConfigSchema = z.union([ discordClientAIOConfigSchema, ]); -export type ClientAIOConfig = z.infer; - -export interface ClientTypeConfigMap extends Record>]> { - maloja: [MalojaClientConfig,MalojaClientAIOConfig, Partial>], - lastfm: [LastfmClientConfig,LastfmClientAIOConfig, Partial>], - librefm: [LibrefmClientConfig,LibrefmClientAIOConfig, Partial>], - listenbrainz: [ListenBrainzClientConfig,ListenBrainzClientAIOConfig, Partial>], - koito: [KoitoClientConfig,KoitoClientAIOConfig, Partial>], - tealfm: [TealClientConfig,TealClientAIOConfig, Partial>], - rocksky: [RockSkyClientConfig,RockSkyClientAIOConfig, Partial>], - discord: [DiscordClientConfig,DiscordClientAIOConfig, Partial>] -} - -export const clientConfigSchemaMap: { [K in keyof ClientTypeConfigMap]: [z.ZodType,z.ZodType,EnvClientSchema] } = { - maloja: [malojaClientConfigSchema,malojaClientAIOConfigSchema,malojaEnvSchemas], - lastfm: [lastfmClientConfigSchema,lastfmClientAIOConfigSchema,lastfmEnvSchemas], - librefm: [librefmClientConfigSchema,librefmClientAIOConfigSchema,librefmEnvSchemas], - listenbrainz: [listenBrainzClientConfigSchema,listenBrainzClientAIOConfigSchema,listenBrainzEnvSchemas], - koito: [koitoClientConfigSchema,koitoClientAIOConfigSchema,koitoEnvSchemas], - tealfm: [tealClientConfigSchema,tealClientAIOConfigSchema,tealEnvSchemas], - rocksky: [rockSkyClientConfigSchema,rockSkyClientAIOConfigSchema,rockSkyEnvSchemas], - discord: [discordClientConfigSchema,discordClientAIOConfigSchema,discordEnvSchemas] -} - -export const validateClientJson = (clientType: T, json: object): ClientTypeConfigMap[T][0] => { - if(clientConfigSchemaMap[clientType] === undefined) { - throw new SimpleError(`No Client has a 'type' of '${clientType}'`); - } - return clientConfigSchemaMap[clientType][0].parse(json); -} -export const validateClientAIOJson = (clientType: T, json: object): ClientTypeConfigMap[T][1] => { - if(clientConfigSchemaMap[clientType] === undefined) { - throw new SimpleError(`No Client has a 'type' of '${clientType}'`); - } - return clientConfigSchemaMap[clientType][1].parse(json); -} \ No newline at end of file +export type ClientAIOConfig = z.infer; \ No newline at end of file diff --git a/src/backend/common/infrastructure/config/client/clientsMap.ts b/src/backend/common/infrastructure/config/client/clientsMap.ts new file mode 100644 index 00000000..2d64ef2f --- /dev/null +++ b/src/backend/common/infrastructure/config/client/clientsMap.ts @@ -0,0 +1,79 @@ +import type { KoitoClientAIOConfig, KoitoClientConfig} from "./koito.ts"; +import type { LastfmClientAIOConfig, LastfmClientConfig} from "./lastfm.ts"; +import type { ListenBrainzClientAIOConfig, ListenBrainzClientConfig} from "./listenbrainz.ts"; +import type { MalojaClientAIOConfig, MalojaClientConfig} from "./maloja.ts"; +import type { TealClientAIOConfig, TealClientConfig} from "./tealfm.ts"; +import type { RockSkyClientAIOConfig, RockSkyClientConfig} from "./rocksky.ts"; +import type { LibrefmClientAIOConfig, LibrefmClientConfig} from "./librefm.ts"; +import type { DiscordClientAIOConfig, DiscordClientConfig} from "./discord.ts"; +import type { ClientAIOConfig } from "./clients.ts"; +import type { CommonClientConfig, EnvClientSchema } from "./index.ts"; +import type {ZodType, ZodObject} from 'zod'; +import type { ClientType } from "../../../../../core/Atomic.ts"; +import { SimpleError } from "../../../errors/MSErrors.ts"; + + +export interface ClientTypeConfigMap extends Record>]> { + maloja: [MalojaClientConfig,MalojaClientAIOConfig, Partial>], + lastfm: [LastfmClientConfig,LastfmClientAIOConfig, Partial>], + librefm: [LibrefmClientConfig,LibrefmClientAIOConfig, Partial>], + listenbrainz: [ListenBrainzClientConfig,ListenBrainzClientAIOConfig, Partial>], + koito: [KoitoClientConfig,KoitoClientAIOConfig, Partial>], + tealfm: [TealClientConfig,TealClientAIOConfig, Partial>], + rocksky: [RockSkyClientConfig,RockSkyClientAIOConfig, Partial>], + discord: [DiscordClientConfig,DiscordClientAIOConfig, Partial>] +} + +export const clientConfigSchemaMapAsync: { [K in keyof ClientTypeConfigMap]:() => Promise<[ZodType,ZodType,EnvClientSchema]> } = { + maloja: async() => { + const {malojaClientConfigSchema, malojaClientAIOConfigSchema, envSchemas} = (await import('./maloja.ts')); + return [malojaClientConfigSchema,malojaClientAIOConfigSchema,envSchemas] + }, + lastfm: async() => { + const {lastfmClientConfigSchema, lastfmClientAIOConfigSchema, envSchemas} = (await import('./lastfm.ts')); + return [lastfmClientConfigSchema,lastfmClientAIOConfigSchema,envSchemas] + }, + librefm: async() => { + const {librefmClientConfigSchema, librefmClientAIOConfigSchema, envSchemas} = (await import('./librefm.ts')); + return [librefmClientConfigSchema,librefmClientAIOConfigSchema,envSchemas] + }, + listenbrainz: async() => { + const {listenBrainzClientConfigSchema, listenBrainzClientAIOConfigSchema, envSchemas} = (await import('./listenbrainz.ts')); + return [listenBrainzClientConfigSchema,listenBrainzClientAIOConfigSchema,envSchemas] + }, + koito: async() => { + const {koitoClientConfigSchema, koitoClientAIOConfigSchema, envSchemas} = (await import('./koito.ts')); + return [koitoClientConfigSchema,koitoClientAIOConfigSchema,envSchemas] + }, + tealfm: async() => { + const {tealClientConfigSchema, tealClientAIOConfigSchema, envSchemas} = (await import('./tealfm.ts')); + return [tealClientConfigSchema,tealClientAIOConfigSchema,envSchemas] + }, + rocksky: async() => { + const {rockSkyClientConfigSchema, rockSkyClientAIOConfigSchema, envSchemas} = (await import('./rocksky.ts')); + return [rockSkyClientConfigSchema,rockSkyClientAIOConfigSchema,envSchemas] + }, + discord: async() => { + const {discordClientConfigSchema, discordClientAIOConfigSchema, envSchemas} = (await import('./discord.ts')); + return [discordClientConfigSchema,discordClientAIOConfigSchema,envSchemas] + } +} + +export const validateClientJson = async (clientType: T, json: object): Promise => { + if(clientConfigSchemaMapAsync[clientType] === undefined) { + throw new SimpleError(`No Client has a 'type' of '${clientType}'`); + } + return (await clientConfigSchemaMapAsync[clientType]())[0].parse(json); +} +export const validateClientAIOJson = async (clientType: T, json: object): Promise => { + if(clientConfigSchemaMapAsync[clientType] === undefined) { + throw new SimpleError(`No Client has a 'type' of '${clientType}'`); + } + return (await clientConfigSchemaMapAsync[clientType]())[1].parse(json); +} +export const getClientEnvSchema = async (clientType: T): Promise> => { + if(clientConfigSchemaMapAsync[clientType] === undefined) { + throw new SimpleError(`No Client has a 'type' of '${clientType}'`); + } + return (await clientConfigSchemaMapAsync[clientType]())[2]; +}; \ No newline at end of file diff --git a/src/backend/scrobblers/ScrobbleClients.ts b/src/backend/scrobblers/ScrobbleClients.ts index c4763cc6..b95fa92f 100644 --- a/src/backend/scrobblers/ScrobbleClients.ts +++ b/src/backend/scrobblers/ScrobbleClients.ts @@ -7,7 +7,6 @@ import { isClientType } from '../../core/Atomic.ts'; import { clientTypes } from "../../core/Atomic.ts"; import type {ClientType} from "../../core/Atomic.ts"; import {aioClientRelaxedConfigSchema, type AIOClientRelaxedConfig, type ClientDefaults} from "../common/infrastructure/config/aioConfig.ts"; -import {clientConfigSchemaMap, validateClientAIOJson, validateClientJson, type ClientTypeConfigMap} from "../common/infrastructure/config/client/clients.ts"; import type { WildcardEmitter } from "../common/WildcardEmitter.ts"; import { pick } from '../../core/DataUtils.ts'; import { readJson } from '../utils/DataUtils.ts'; @@ -18,6 +17,7 @@ import { normalizeStr, type StringNormalizationOptions } from '../utils/StringUt import { prettifyError, ZodError } from 'zod'; import { commonComponentEnvConfigToConfigPrimitives, generateCommonComponentEnvConfigSchema, transformPresetEnv, type CommonConfigPrimitives } from '../common/infrastructure/config/common.ts'; import type { CommonClientConfig } from '../common/infrastructure/config/client/index.ts'; +import { getClientEnvSchema, validateClientAIOJson, validateClientJson, type ClientTypeConfigMap } from '../common/infrastructure/config/client/clientsMap.ts'; type UnparsedConfig = {config: object, type: ClientType, source?: 'file' | 'aio' | 'env', pos: string}; @@ -198,7 +198,7 @@ export default class ScrobbleClients { const sourceStr = `${entry.source} ${entry.pos}`; switch (entry.source) { case 'env': { - const envSchema = clientConfigSchemaMap[clientType][2]; + const envSchema = await getClientEnvSchema(clientType); const primitiveSchema = generateCommonComponentEnvConfigSchema(envSchema.prefix.toUpperCase()); const parsed = primitiveSchema.parse(entry.config); const primitives: CommonConfigPrimitives = commonComponentEnvConfigToConfigPrimitives(envSchema.prefix.toUpperCase(), parsed); @@ -222,7 +222,7 @@ export default class ScrobbleClients { this.logger.debug(`Skipping ${clientType} Config ${entry.source} ${entry.pos} because it is configured as a Source`); continue; } - const parsed = entry.source === 'file' ? validateClientJson(entry.type, entry.config) : validateClientAIOJson(entry.type, entry.config); + const parsed = entry.source === 'file' ? (await validateClientJson(entry.type, entry.config)) : (await validateClientAIOJson(entry.type, entry.config)); parsedConfig = { ...parsed, source: sourceStr @@ -259,7 +259,7 @@ export default class ScrobbleClients { ) => { for (const s of strongConfigs) { try { - const config = clientConfigSchemaMap[clientType][0].parse(s); + const config = await validateClientJson(clientType, s); const compositeOptions = { ...clientDefaults, ...config.options }; const newClient = new Ctor(...buildArgs(config, compositeOptions)); newClient.logger.info(`Client added from ${s.source}`); diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts index 837c567a..b93b2837 100644 --- a/src/backend/sources/ScrobbleSources.ts +++ b/src/backend/sources/ScrobbleSources.ts @@ -218,7 +218,7 @@ export default class ScrobbleSources { const sourceStr = `${entry.source} ${entry.pos}`; switch (entry.source) { case 'env': { - const envSchema = await getSourceEnvSchema(configType); // sourceConfigSchemaMap[configType][2]; + const envSchema = await getSourceEnvSchema(configType); const primitiveSchema = generateCommonComponentEnvConfigSchema(envSchema.prefix.toUpperCase()); const parsed = primitiveSchema.parse(entry.config); const primitives: CommonConfigPrimitives = commonComponentEnvConfigToConfigPrimitives(envSchema.prefix.toUpperCase(), parsed); @@ -278,7 +278,7 @@ export default class ScrobbleSources { ) => { for (const s of strongConfigs) { try { - const config = await validateSourceJson(sourceType, s); // sourceConfigSchemaMap[sourceType][0].parse(s); + const config = await validateSourceJson(sourceType, s); const compositeOptions = { ...defaults, ...config.options }; const newComponent = new Ctor(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); newComponent.logger.info(`Source added from ${s.source}`); diff --git a/src/backend/tests/config/config.test.ts b/src/backend/tests/config/config.test.ts index b23d6b57..650b647d 100644 --- a/src/backend/tests/config/config.test.ts +++ b/src/backend/tests/config/config.test.ts @@ -15,7 +15,7 @@ import { difference } from '../../utils.ts'; import { validateSourceJson } from '../../common/infrastructure/config/source/sourcesMap.ts'; import { readJson } from '../../utils/DataUtils.ts'; import { prettifyError, ZodError } from 'zod'; -import { validateClientJson } from '../../common/infrastructure/config/client/clients.ts'; +import { validateClientJson } from '../../common/infrastructure/config/client/clientsMap.ts'; chai.use(asPromised); @@ -122,7 +122,7 @@ describe('Sample Configs', function () { fileContents = fileContents.filter(x => x.configureAs === undefined || x.configureAs === 'client'); for (const [i,rawConf] of fileContents.entries()) { try { - validateClientJson(componentType, rawConf); + await validateClientJson(componentType, rawConf); } catch (e) { if(e instanceof ZodError) { expect.fail(`Validation failed for config entry ${i}:\n${prettifyError(e)}`); diff --git a/src/backend/utils/SchemaDocsStaticUtils.ts b/src/backend/utils/SchemaDocsStaticUtils.ts index 616a98c5..37985598 100644 --- a/src/backend/utils/SchemaDocsStaticUtils.ts +++ b/src/backend/utils/SchemaDocsStaticUtils.ts @@ -1,7 +1,7 @@ import { writeFileSync, mkdirSync } from "node:fs"; import { resolve } from "path"; import * as z from 'zod'; -import { clientConfigSchemaMap } from '../common/infrastructure/config/client/clients.ts'; +import { clientConfigSchemaMapAsync } from '../common/infrastructure/config/client/clientsMap.ts'; import { projectRootDir } from "../common/infrastructure/Atomic.ts"; import { aioConfigSchema } from "../common/infrastructure/config/aioConfig.ts"; import { generateCommonComponentEnvConfigSchema } from "../common/infrastructure/config/common.ts"; @@ -54,13 +54,14 @@ const generateSchema = (schema: z.ZodType, reused: z.core.ToJSONSchemaParams['re } }) -const clientEntries = Object.entries(clientConfigSchemaMap); +const clientEntries = Object.entries(clientConfigSchemaMapAsync); for(const [k,v] of clientEntries) { - writeFileSync(resolve(projectRootDir, `docsite/static/schemas/${k}-client.json`), JSON.stringify(generateSchema(z.array(v[0])))); + const [fileSchema, aioSchema, envSchemas] = await v(); + writeFileSync(resolve(projectRootDir, `docsite/static/schemas/${k}-client.json`), JSON.stringify(generateSchema(z.array(fileSchema)))); - const envSchema = v[2]; + const envSchema = envSchemas; const common = generateCommonComponentEnvConfigSchema(envSchema.prefix.toUpperCase()); - const col = zodObjectToTableColumns(z.object({...common.shape,...v[2].env.shape}), 'out'); + const col = zodObjectToTableColumns(z.object({...common.shape,...envSchema.env.shape}), 'out'); const tableContent = markdownTable([ ['Environmental Variable', 'Type', 'Default', 'Description'], ...mdCols(col) -- 2.51.2 From bf9cdc141c964bfb5b3b23b155012739f04a2467 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Sat, 1 Aug 2026 00:05:03 +0000 Subject: [PATCH 20/35] re-add clients to aio json schema --- src/backend/utils/SchemaDocsStaticUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/backend/utils/SchemaDocsStaticUtils.ts b/src/backend/utils/SchemaDocsStaticUtils.ts index 37985598..b93e6ccc 100644 --- a/src/backend/utils/SchemaDocsStaticUtils.ts +++ b/src/backend/utils/SchemaDocsStaticUtils.ts @@ -9,6 +9,7 @@ import { zodObjectToTableColumns, type TableColumn } from "./ZodUtils.ts"; import {markdownTable} from 'markdown-table' import { sourceAIOConfigSchema } from "../common/infrastructure/config/source/sources.ts"; import { sourceConfigSchemaMapAsync } from "../common/infrastructure/config/source/sourcesMap.ts"; +import { clientAIOConfigSchema } from "../common/infrastructure/config/client/clients.ts"; mkdirSync(resolve(projectRootDir, 'docsite/static/schemas'), {recursive: true}); @@ -85,7 +86,8 @@ for(const [k,v] of sourcesEntries) { } const aioStrongConfigSchema = aioConfigSchema.extend({ - sources: z.array(sourceAIOConfigSchema).optional() + sources: z.array(sourceAIOConfigSchema).optional(), + clients: z.array(clientAIOConfigSchema).optional() }) writeFileSync(resolve(projectRootDir, 'docsite/static/schemas/aio.json'), JSON.stringify(generateSchema(aioStrongConfigSchema, 'ref'))); -- 2.51.2 From 31546ed4c1ee37be6e3c3a80c44b6cccbd3268f7 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Sat, 1 Aug 2026 15:09:52 +0000 Subject: [PATCH 21/35] fix: Early return on component add to prevent dynamic loading --- src/backend/scrobblers/ScrobbleClients.ts | 4 +++- src/backend/sources/ScrobbleSources.ts | 4 +++- src/backend/sources/SonosSource.ts | 3 --- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/backend/scrobblers/ScrobbleClients.ts b/src/backend/scrobblers/ScrobbleClients.ts index b95fa92f..4c04df04 100644 --- a/src/backend/scrobblers/ScrobbleClients.ts +++ b/src/backend/scrobblers/ScrobbleClients.ts @@ -246,7 +246,9 @@ export default class ScrobbleClients { } } - await this.addClient(clientType, strongConfigs, clientDefaults); + if(strongConfigs.length > 0) { + await this.addClient(clientType, strongConfigs, clientDefaults); + } } } diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts index b93b2837..c331b359 100644 --- a/src/backend/sources/ScrobbleSources.ts +++ b/src/backend/sources/ScrobbleSources.ts @@ -266,7 +266,9 @@ export default class ScrobbleSources { } } - await this.addSource(configType, strongConfigs, sourceDefaults); + if(strongConfigs.length > 0) { + await this.addSource(configType, strongConfigs, sourceDefaults); + } } } diff --git a/src/backend/sources/SonosSource.ts b/src/backend/sources/SonosSource.ts index 09cac3ae..161542f1 100644 --- a/src/backend/sources/SonosSource.ts +++ b/src/backend/sources/SonosSource.ts @@ -15,7 +15,6 @@ import type {SonosSourceConfig} from "../common/infrastructure/config/source/son import type { SonosDevice} from '@svrooij/sonos'; import { SonosManager } from '@svrooij/sonos'; import type {SonosState} from "@svrooij/sonos/lib/models/sonos-state.js"; -import { GroupTransportState } from "@svrooij/sonos/lib/models/transport-state.js"; import type {Track} from "@svrooij/sonos/lib/models/track.js"; import { parseDurationFromTimestamp } from "../utils/TimeUtils.ts"; import { FixedSizeList } from "fixed-size-list"; @@ -37,8 +36,6 @@ export interface SimpleDeviceState { state: SonosState } -GroupTransportState.GroupPlaying - const CLIENT_PLAYER_STATE: Record = { 'GROUP_PLAYING': REPORTED_PLAYER_STATUSES.playing, 'PLAYING': REPORTED_PLAYER_STATUSES.playing, -- 2.51.2 From 7575e8bff7925dc89dc641647a99d2705077c339 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Sat, 1 Aug 2026 17:31:03 +0000 Subject: [PATCH 22/35] fix: Fix unhandled ZodTransform type generation --- src/backend/utils/ZodUtils.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/backend/utils/ZodUtils.ts b/src/backend/utils/ZodUtils.ts index 9e61d3be..3da07421 100644 --- a/src/backend/utils/ZodUtils.ts +++ b/src/backend/utils/ZodUtils.ts @@ -11,14 +11,22 @@ export interface TableColumn { export type PipeUnwrapDirection = 'in' | 'out'; +const otherPipeDirection: Record = { in: 'out', out: 'in' }; + const unwrapZodType = (schema: z.ZodTypeAny, pipeDirection: PipeUnwrapDirection = 'in'): z.ZodTypeAny => { let current = schema; while (current instanceof z.ZodOptional || current instanceof z.ZodNullable || current instanceof z.ZodDefault || current instanceof z.ZodPipe) { - current = (current instanceof z.ZodPipe ? current[pipeDirection] : current.unwrap()) as z.ZodTypeAny; + if (current instanceof z.ZodPipe) { + const side = current[pipeDirection]; + // ZodTransform carries no schema of its own, so its side of the pipe can't tell us + // anything about the resulting shape, fall back to the other side of the pipe. + current = (side instanceof z.ZodTransform ? current[otherPipeDirection[pipeDirection]] : side) as z.ZodTypeAny; + } else { + current = current.unwrap() as z.ZodTypeAny; + } } return current; }; - const getExplicitDefault = (schema: z.ZodTypeAny): unknown => { let current = schema; while (true) { -- 2.51.2 From 1d1b3b6f562a012731e0f4721d7ce5e5dc266983 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Sat, 1 Aug 2026 17:31:28 +0000 Subject: [PATCH 23/35] docs: Update env descriptions for id, name, and enable --- src/backend/common/infrastructure/config/common.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/common/infrastructure/config/common.ts b/src/backend/common/infrastructure/config/common.ts index 2365be15..1746d1b5 100644 --- a/src/backend/common/infrastructure/config/common.ts +++ b/src/backend/common/infrastructure/config/common.ts @@ -184,9 +184,9 @@ export type CommonComponentEnvShape = { export const generateCommonComponentEnvConfigSchema = (prefix: T) => z.object({ - [`${prefix}_ID`]: z.string().meta({description: 'A globally unique ID'}), - [`${prefix}_NAME`]: z.string().optional().meta({description: 'A vanity name', default: `Value of \`${prefix}_ID\``}), - [`${prefix}_ENABLE`]: z.stringbool().optional() + [`${prefix}_ID`]: z.string().meta({description: 'A globally unique ID EX `myComponentId`', examples: ['myComponentId']}), + [`${prefix}_NAME`]: z.string().optional().meta({description: 'A vanity name EX `My Cool Component`', default: `Value of \`${prefix}_ID\``, examples: ['My Cool Component']}), + [`${prefix}_ENABLE`]: z.stringbool().optional().meta({description: 'Should this component be used?', default: true}), } as CommonComponentEnvShape); export type CommonComponentEnvConfigParsed = { -- 2.51.2 From ad632f38d84a19f4658eb156f98ce779ba9a7b38 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Sat, 1 Aug 2026 17:31:37 +0000 Subject: [PATCH 24/35] docs: regenerate env docs --- .../clients/_env_configs/_discord.md | 24 +++++++++---------- .../clients/_env_configs/_koito.md | 6 ++--- .../clients/_env_configs/_lastfm.md | 6 ++--- .../clients/_env_configs/_librefm.md | 6 ++--- .../clients/_env_configs/_listenbrainz.md | 6 ++--- .../clients/_env_configs/_maloja.md | 14 +++++------ .../clients/_env_configs/_rocksky.md | 6 ++--- .../clients/_env_configs/_tealfm.md | 14 +++++------ .../sources/_env_configs/_applemusic.md | 6 ++--- .../sources/_env_configs/_azuracast.md | 20 ++++++++-------- .../sources/_env_configs/_chromecast.md | 18 +++++++------- .../sources/_env_configs/_deezer.md | 6 ++--- .../sources/_env_configs/_endpointlfm.md | 6 ++--- .../sources/_env_configs/_endpointlz.md | 6 ++--- .../sources/_env_configs/_icecast.md | 14 +++++------ .../sources/_env_configs/_jellyfin.md | 6 ++--- .../sources/_env_configs/_jriver.md | 6 ++--- .../sources/_env_configs/_kodi.md | 6 ++--- .../sources/_env_configs/_koito.md | 6 ++--- .../sources/_env_configs/_lastfm.md | 6 ++--- .../sources/_env_configs/_librefm.md | 6 ++--- .../sources/_env_configs/_listenbrainz.md | 6 ++--- .../sources/_env_configs/_maloja.md | 14 +++++------ .../sources/_env_configs/_mopidy.md | 18 +++++++------- .../sources/_env_configs/_mpd.md | 6 ++--- .../sources/_env_configs/_mpris.md | 6 ++--- .../sources/_env_configs/_musiccast.md | 6 ++--- .../sources/_env_configs/_musikcube.md | 6 ++--- .../sources/_env_configs/_plex.md | 6 ++--- .../sources/_env_configs/_rocksky.md | 6 ++--- .../sources/_env_configs/_sonos.md | 6 ++--- .../sources/_env_configs/_spotify.md | 6 ++--- .../sources/_env_configs/_subsonic.md | 6 ++--- .../sources/_env_configs/_tealfm.md | 14 +++++------ .../sources/_env_configs/_vlc.md | 6 ++--- .../sources/_env_configs/_webscrobbler.md | 14 +++++------ .../sources/_env_configs/_ymbridge.md | 6 ++--- .../sources/_env_configs/_ytmusic.md | 6 ++--- 38 files changed, 166 insertions(+), 166 deletions(-) diff --git a/docsite/docs/configuration/clients/_env_configs/_discord.md b/docsite/docs/configuration/clients/_env_configs/_discord.md index d6c7dc45..f2a81607 100644 --- a/docsite/docs/configuration/clients/_env_configs/_discord.md +++ b/docsite/docs/configuration/clients/_env_configs/_discord.md @@ -1,12 +1,12 @@ -| Environmental Variable | Type | Default | Description | -| ---------------------------------- | ------- | --------------------- | -------------------- | -| _**`DISCORD_ID`**_ | string | | A globally unique ID | -| `DISCORD_NAME` | string | Value of `DISCORD_ID` | A vanity name | -| `DISCORD_ENABLE` | boolean | | | -| `DISCORD_TOKEN` | string | | | -| `DISCORD_ARTWORK` | union | | | -| `DISCORD_APPLICATION_ID` | string | | | -| `DISCORD_IPC_LOCATIONS` | union | | | -| `DISCORD_ARTWORK_DEFAULT_URL` | string | | | -| `DISCORD_STATUS_OVERRIDE_ALLOW` | union | | | -| `DISCORD_LISTENING_ACTIVITY_ALLOW` | union | | | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ---------------------------------- | ------- | --------------------- | --------------------------------------- | +| _**`DISCORD_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `DISCORD_NAME` | string | Value of `DISCORD_ID` | A vanity name EX `My Cool Component` | +| `DISCORD_ENABLE` | boolean | true | Should this component be used? | +| `DISCORD_TOKEN` | string | | | +| `DISCORD_ARTWORK` | union | | | +| `DISCORD_APPLICATION_ID` | string | | | +| `DISCORD_IPC_LOCATIONS` | union | | | +| `DISCORD_ARTWORK_DEFAULT_URL` | string | | | +| `DISCORD_STATUS_OVERRIDE_ALLOW` | union | | | +| `DISCORD_LISTENING_ACTIVITY_ALLOW` | union | | | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/_env_configs/_koito.md b/docsite/docs/configuration/clients/_env_configs/_koito.md index 9d2ce634..541fec39 100644 --- a/docsite/docs/configuration/clients/_env_configs/_koito.md +++ b/docsite/docs/configuration/clients/_env_configs/_koito.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ------------------- | --------------------------------------- | -| _**`KOITO_ID`**_ | string | | A globally unique ID | -| `KOITO_NAME` | string | Value of `KOITO_ID` | A vanity name | -| `KOITO_ENABLE` | boolean | | | +| _**`KOITO_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `KOITO_NAME` | string | Value of `KOITO_ID` | A vanity name EX `My Cool Component` | +| `KOITO_ENABLE` | boolean | true | Should this component be used? | | _**`KOITO_URL`**_ | string | | URL for the Koito server | | _**`KOITO_TOKEN`**_ | string | | User token for the user to scrobble for | | _**`KOITO_USER`**_ | string | | Username of the user to scrobble for | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/_env_configs/_lastfm.md b/docsite/docs/configuration/clients/_env_configs/_lastfm.md index e28ca51f..b9e6ab0d 100644 --- a/docsite/docs/configuration/clients/_env_configs/_lastfm.md +++ b/docsite/docs/configuration/clients/_env_configs/_lastfm.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ------------------------------------- | ------------------------------------------------------- | -| _**`LASTFM_ID`**_ | string | | A globally unique ID | -| `LASTFM_NAME` | string | Value of `LASTFM_ID` | A vanity name | -| `LASTFM_ENABLE` | boolean | | | +| _**`LASTFM_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `LASTFM_NAME` | string | Value of `LASTFM_ID` | A vanity name EX `My Cool Component` | +| `LASTFM_ENABLE` | boolean | true | Should this component be used? | | _**`LASTFM_API_KEY`**_ | string | | API Key generated from Last.fm/Libre.fm account | | _**`LASTFM_SECRET`**_ | string | | Secret generated from Last.fm/Libre.fm account | | `LASTFM_REDIRECT_URI` | string | http://localhost:9078/lastfm/callback | Optional URI to use for callback. | diff --git a/docsite/docs/configuration/clients/_env_configs/_librefm.md b/docsite/docs/configuration/clients/_env_configs/_librefm.md index 6fb8139a..7954a6fe 100644 --- a/docsite/docs/configuration/clients/_env_configs/_librefm.md +++ b/docsite/docs/configuration/clients/_env_configs/_librefm.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | -------------------------------------- | -------------------------------------------------------------- | -| _**`LIBREFM_ID`**_ | string | | A globally unique ID | -| `LIBREFM_NAME` | string | Value of `LIBREFM_ID` | A vanity name | -| `LIBREFM_ENABLE` | boolean | | | +| _**`LIBREFM_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `LIBREFM_NAME` | string | Value of `LIBREFM_ID` | A vanity name EX `My Cool Component` | +| `LIBREFM_ENABLE` | boolean | true | Should this component be used? | | `LIBREFM_API_KEY` | string | | Optional Secret for Libre.fm account | | `LIBREFM_SECRET` | string | | Optional Secret for Libre.fm account | | `LIBREFM_REDIRECT_URI` | string | http://localhost:9078/librefm/callback | Optional URI to use for callback. | diff --git a/docsite/docs/configuration/clients/_env_configs/_listenbrainz.md b/docsite/docs/configuration/clients/_env_configs/_listenbrainz.md index c4c5eafd..222e0c10 100644 --- a/docsite/docs/configuration/clients/_env_configs/_listenbrainz.md +++ b/docsite/docs/configuration/clients/_env_configs/_listenbrainz.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ----------------------------- | --------------------------------------------------------- | -| _**`LZ_ID`**_ | string | | A globally unique ID | -| `LZ_NAME` | string | Value of `LZ_ID` | A vanity name | -| `LZ_ENABLE` | boolean | | | +| _**`LZ_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `LZ_NAME` | string | Value of `LZ_ID` | A vanity name EX `My Cool Component` | +| `LZ_ENABLE` | boolean | true | Should this component be used? | | `LZ_URL` | string | https://api.listenbrainz.org/ | URL for the ListenBrainz server, if not using the default | | _**`LZ_TOKEN`**_ | string | | User token for the user to scrobble for | | _**`LZ_USER`**_ | string | | Username of the user to scrobble for | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/_env_configs/_maloja.md b/docsite/docs/configuration/clients/_env_configs/_maloja.md index d346e32a..990610f2 100644 --- a/docsite/docs/configuration/clients/_env_configs/_maloja.md +++ b/docsite/docs/configuration/clients/_env_configs/_maloja.md @@ -1,7 +1,7 @@ -| Environmental Variable | Type | Default | Description | -| ---------------------- | ------- | -------------------- | ------------------------- | -| _**`MALOJA_ID`**_ | string | | A globally unique ID | -| `MALOJA_NAME` | string | Value of `MALOJA_ID` | A vanity name | -| `MALOJA_ENABLE` | boolean | | | -| _**`MALOJA_URL`**_ | string | | URL for maloja server | -| _**`MALOJA_API_KEY`**_ | string | | API Key for Maloja server | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | -------------------- | --------------------------------------- | +| _**`MALOJA_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `MALOJA_NAME` | string | Value of `MALOJA_ID` | A vanity name EX `My Cool Component` | +| `MALOJA_ENABLE` | boolean | true | Should this component be used? | +| _**`MALOJA_URL`**_ | string | | URL for maloja server | +| _**`MALOJA_API_KEY`**_ | string | | API Key for Maloja server | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/_env_configs/_rocksky.md b/docsite/docs/configuration/clients/_env_configs/_rocksky.md index f978f310..97f507aa 100644 --- a/docsite/docs/configuration/clients/_env_configs/_rocksky.md +++ b/docsite/docs/configuration/clients/_env_configs/_rocksky.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -| _**`ROCKSKY_ID`**_ | string | | A globally unique ID | -| `ROCKSKY_NAME` | string | Value of `ROCKSKY_ID` | A vanity name | -| `ROCKSKY_ENABLE` | boolean | | | +| _**`ROCKSKY_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `ROCKSKY_NAME` | string | Value of `ROCKSKY_ID` | A vanity name EX `My Cool Component` | +| `ROCKSKY_ENABLE` | boolean | true | Should this component be used? | | `ROCKSKY_KEY` | string | | API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account | | `ROCKSKY_TOKEN` | string | | Access Token generated from https://rocksky.app/access-tokens in Rocksky for your account | | _**`ROCKSKY_HANDLE`**_ | string | | The **fully-qualified** handle for your ATPRoto/Bluesky account, like: | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/_env_configs/_tealfm.md b/docsite/docs/configuration/clients/_env_configs/_tealfm.md index 6856dae7..3b59266f 100644 --- a/docsite/docs/configuration/clients/_env_configs/_tealfm.md +++ b/docsite/docs/configuration/clients/_env_configs/_tealfm.md @@ -1,7 +1,7 @@ -| Environmental Variable | Type | Default | Description | -| ------------------------- | ------- | -------------------- | -------------------------------- | -| _**`TEALFM_ID`**_ | string | | A globally unique ID | -| `TEALFM_NAME` | string | Value of `TEALFM_ID` | A vanity name | -| `TEALFM_ENABLE` | boolean | | | -| _**`TEALFM_IDENTIFIER`**_ | string | | Identify the account to login as | -| `TEALFM_APP_PW` | string | | | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ------------------------- | ------- | -------------------- | --------------------------------------- | +| _**`TEALFM_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `TEALFM_NAME` | string | Value of `TEALFM_ID` | A vanity name EX `My Cool Component` | +| `TEALFM_ENABLE` | boolean | true | Should this component be used? | +| _**`TEALFM_IDENTIFIER`**_ | string | | Identify the account to login as | +| `TEALFM_APP_PW` | string | | | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_applemusic.md b/docsite/docs/configuration/sources/_env_configs/_applemusic.md index b8063aff..b70d8a1d 100644 --- a/docsite/docs/configuration/sources/_env_configs/_applemusic.md +++ b/docsite/docs/configuration/sources/_env_configs/_applemusic.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ------------------------------------------ | ------- | ------------------------ | -------------------------------------------------------------------- | -| _**`APPLEMUSIC_ID`**_ | string | | A globally unique ID | -| `APPLEMUSIC_NAME` | string | Value of `APPLEMUSIC_ID` | A vanity name | -| `APPLEMUSIC_ENABLE` | boolean | | | +| _**`APPLEMUSIC_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `APPLEMUSIC_NAME` | string | Value of `APPLEMUSIC_ID` | A vanity name EX `My Cool Component` | +| `APPLEMUSIC_ENABLE` | boolean | true | Should this component be used? | | `APPLEMUSIC_KEY_ID` | string | | | | `APPLEMUSIC_TEAM_ID` | string | | | | `APPLEMUSIC_KEY_P8` | string | | | diff --git a/docsite/docs/configuration/sources/_env_configs/_azuracast.md b/docsite/docs/configuration/sources/_env_configs/_azuracast.md index d5c69416..33705a6c 100644 --- a/docsite/docs/configuration/sources/_env_configs/_azuracast.md +++ b/docsite/docs/configuration/sources/_env_configs/_azuracast.md @@ -1,10 +1,10 @@ -| Environmental Variable | Type | Default | Description | -| ---------------------- | --------- | ------------------- | ------------------------------------------------- | -| _**`AZURA_ID`**_ | string | | A globally unique ID | -| `AZURA_NAME` | string | Value of `AZURA_ID` | A vanity name | -| `AZURA_ENABLE` | boolean | | | -| _**`AZURA_STATION`**_ | string | | The specific station to monitor | -| _**`AZURA_URL`**_ | string | | Base URL of the Azuracast instance | -| `AZURA_KEY` | string | | API Key used to access data about private streams | -| `AZURA_LISTENERS_NUM` | transform | | | -| `AZURA_LIVE` | boolean | | | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ------------------- | ------------------------------------------------- | +| _**`AZURA_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `AZURA_NAME` | string | Value of `AZURA_ID` | A vanity name EX `My Cool Component` | +| `AZURA_ENABLE` | boolean | true | Should this component be used? | +| _**`AZURA_STATION`**_ | string | | The specific station to monitor | +| _**`AZURA_URL`**_ | string | | Base URL of the Azuracast instance | +| `AZURA_KEY` | string | | API Key used to access data about private streams | +| `AZURA_LISTENERS_NUM` | string | | | +| `AZURA_LIVE` | boolean | | | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_chromecast.md b/docsite/docs/configuration/sources/_env_configs/_chromecast.md index d11c53e5..6b6a5c8f 100644 --- a/docsite/docs/configuration/sources/_env_configs/_chromecast.md +++ b/docsite/docs/configuration/sources/_env_configs/_chromecast.md @@ -1,9 +1,9 @@ -| Environmental Variable | Type | Default | Description | -| ---------------------- | --------- | ---------------- | ------------------------------------------------------------------------------------ | -| _**`CC_ID`**_ | string | | A globally unique ID | -| `CC_NAME` | string | Value of `CC_ID` | A vanity name | -| `CC_ENABLE` | boolean | | | -| `CC_BLACKLIST_DEVICES` | transform | | DO NOT scrobble from any cast devices that START WITH these values, case-insensitive | -| `CC_WHITELIST_DEVICES` | transform | | ONLY scrobble from any cast device that START WITH these values, case-insensitive | -| `CC_BLACKLIST_APPS` | transform | | DO NOT scrobble from any application that START WITH these values, case-insensitive | -| `CC_WHITELIST_APPS` | transform | | ONLY scrobble from any application that START WITH these values, case-insensitive | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ---------------- | ------------------------------------------------------------------------------------ | +| _**`CC_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `CC_NAME` | string | Value of `CC_ID` | A vanity name EX `My Cool Component` | +| `CC_ENABLE` | boolean | true | Should this component be used? | +| `CC_BLACKLIST_DEVICES` | string | | DO NOT scrobble from any cast devices that START WITH these values, case-insensitive | +| `CC_WHITELIST_DEVICES` | string | | ONLY scrobble from any cast device that START WITH these values, case-insensitive | +| `CC_BLACKLIST_APPS` | string | | DO NOT scrobble from any application that START WITH these values, case-insensitive | +| `CC_WHITELIST_APPS` | string | | ONLY scrobble from any application that START WITH these values, case-insensitive | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_deezer.md b/docsite/docs/configuration/sources/_env_configs/_deezer.md index 29d9d297..309e30b8 100644 --- a/docsite/docs/configuration/sources/_env_configs/_deezer.md +++ b/docsite/docs/configuration/sources/_env_configs/_deezer.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | -------------------------------------------------------------------- | -------------------------------------------------- | -| _**`DEEZER_ID`**_ | string | | A globally unique ID | -| `DEEZER_NAME` | string | Value of `DEEZER_ID` | A vanity name | -| `DEEZER_ENABLE` | boolean | | | +| _**`DEEZER_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `DEEZER_NAME` | string | Value of `DEEZER_ID` | A vanity name EX `My Cool Component` | +| `DEEZER_ENABLE` | boolean | true | Should this component be used? | | _**`DEEZER_ARL`**_ | string | | ARL retrieved from Deezer response header | | `DEEZER_ACCOUNT_ID` | string | | The ID (USER_ID) of the linked account to monitor. | | `DEEZER_USER_AGENT` | string | Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0 | User agent | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_endpointlfm.md b/docsite/docs/configuration/sources/_env_configs/_endpointlfm.md index 226e6cda..5f4a999c 100644 --- a/docsite/docs/configuration/sources/_env_configs/_endpointlfm.md +++ b/docsite/docs/configuration/sources/_env_configs/_endpointlfm.md @@ -1,6 +1,6 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ----------------- | ------------------------------------------------------------------------ | -| _**`LFM_ID`**_ | string | | A globally unique ID | -| `LFM_NAME` | string | Value of `LFM_ID` | A vanity name | -| `LFM_ENABLE` | boolean | | | +| _**`LFM_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `LFM_NAME` | string | Value of `LFM_ID` | A vanity name EX `My Cool Component` | +| `LFM_ENABLE` | boolean | true | Should this component be used? | | `LFM_SLUG` | union | | The URL ending that should be used to identify scrobbles for this source | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_endpointlz.md b/docsite/docs/configuration/sources/_env_configs/_endpointlz.md index 6a0c0c3b..8cc4681d 100644 --- a/docsite/docs/configuration/sources/_env_configs/_endpointlz.md +++ b/docsite/docs/configuration/sources/_env_configs/_endpointlz.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| _**`LZE_ID`**_ | string | | A globally unique ID | -| `LZE_NAME` | string | Value of `LZE_ID` | A vanity name | -| `LZE_ENABLE` | boolean | | | +| _**`LZE_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `LZE_NAME` | string | Value of `LZE_ID` | A vanity name EX `My Cool Component` | +| `LZE_ENABLE` | boolean | true | Should this component be used? | | `LZE_SLUG` | string | | The URL ending that should be used to identify scrobbles for this source | | `LZE_TOKEN` | string | | If an LZ submission request contains this token in the Authorization Header it will be used to match the submission with this Source | | `LZE_USERNAME` | string | | The listenbrainz "username" to associate with this Source | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_icecast.md b/docsite/docs/configuration/sources/_env_configs/_icecast.md index 8c7279ba..83fa1d75 100644 --- a/docsite/docs/configuration/sources/_env_configs/_icecast.md +++ b/docsite/docs/configuration/sources/_env_configs/_icecast.md @@ -1,7 +1,7 @@ -| Environmental Variable | Type | Default | Description | -| ---------------------- | ------- | --------------------- | ---------------------- | -| _**`ICECAST_ID`**_ | string | | A globally unique ID | -| `ICECAST_NAME` | string | Value of `ICECAST_ID` | A vanity name | -| `ICECAST_ENABLE` | boolean | | | -| _**`ICECAST_URL`**_ | string | | The Icecast stream URL | -| `ICECAST_AUTO_MONITOR` | boolean | | | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | --------------------- | --------------------------------------- | +| _**`ICECAST_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `ICECAST_NAME` | string | Value of `ICECAST_ID` | A vanity name EX `My Cool Component` | +| `ICECAST_ENABLE` | boolean | true | Should this component be used? | +| _**`ICECAST_URL`**_ | string | | The Icecast stream URL | +| `ICECAST_AUTO_MONITOR` | boolean | | | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_jellyfin.md b/docsite/docs/configuration/sources/_env_configs/_jellyfin.md index d1a17b03..92d7c0e7 100644 --- a/docsite/docs/configuration/sources/_env_configs/_jellyfin.md +++ b/docsite/docs/configuration/sources/_env_configs/_jellyfin.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | -------------------------------- | ------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -| _**`JELLYFIN_ID`**_ | string | | A globally unique ID | -| `JELLYFIN_NAME` | string | Value of `JELLYFIN_ID` | A vanity name | -| `JELLYFIN_ENABLE` | boolean | | | +| _**`JELLYFIN_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `JELLYFIN_NAME` | string | Value of `JELLYFIN_ID` | A vanity name EX `My Cool Component` | +| `JELLYFIN_ENABLE` | boolean | true | Should this component be used? | | _**`JELLYFIN_USER`**_ | string | | The username of the user to authenticate for or track scrobbles for | | `JELLYFIN_PASSWORD` | string | | Password of the username to authenticate for | | `JELLYFIN_APIKEY` | string | | API Key to authenticate with. | diff --git a/docsite/docs/configuration/sources/_env_configs/_jriver.md b/docsite/docs/configuration/sources/_env_configs/_jriver.md index 7e2da28a..cb26cb03 100644 --- a/docsite/docs/configuration/sources/_env_configs/_jriver.md +++ b/docsite/docs/configuration/sources/_env_configs/_jriver.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ------------------------------- | -------------------------------------------------------- | -| _**`JRIVER_ID`**_ | string | | A globally unique ID | -| `JRIVER_NAME` | string | Value of `JRIVER_ID` | A vanity name | -| `JRIVER_ENABLE` | boolean | | | +| _**`JRIVER_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `JRIVER_NAME` | string | Value of `JRIVER_ID` | A vanity name EX `My Cool Component` | +| `JRIVER_ENABLE` | boolean | true | Should this component be used? | | _**`JRIVER_URL`**_ | string | http://localhost:52199/MCWS/v1/ | URL of the JRiver HTTP server to connect to | | `JRIVER_USER` | string | | If you have enabled authentication, the username you set | | `JRIVER_PASSWORD` | string | | If you have enabled authentication, the password you set | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_kodi.md b/docsite/docs/configuration/sources/_env_configs/_kodi.md index cdf8a0cd..f186dc09 100644 --- a/docsite/docs/configuration/sources/_env_configs/_kodi.md +++ b/docsite/docs/configuration/sources/_env_configs/_kodi.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ----------------------------- | ------------------------------------------------- | -| _**`KODI_ID`**_ | string | | A globally unique ID | -| `KODI_NAME` | string | Value of `KODI_ID` | A vanity name | -| `KODI_ENABLE` | boolean | | | +| _**`KODI_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `KODI_NAME` | string | Value of `KODI_ID` | A vanity name EX `My Cool Component` | +| `KODI_ENABLE` | boolean | true | Should this component be used? | | _**`KODI_URL`**_ | string | http://localhost:8080/jsonrpc | URL of the Kodi HTTP server to connect to | | _**`KODI_USER`**_ | string | | The username set for Remote Control via Web Sever | | _**`KODI_PASSWORD`**_ | string | | The password set for Remote Control via Web Sever | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_koito.md b/docsite/docs/configuration/sources/_env_configs/_koito.md index 01b2f5c5..c156aae3 100644 --- a/docsite/docs/configuration/sources/_env_configs/_koito.md +++ b/docsite/docs/configuration/sources/_env_configs/_koito.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | -------------------------- | ------- | -------------------------- | --------------------------------------- | -| _**`SOURCE_KOITO_ID`**_ | string | | A globally unique ID | -| `SOURCE_KOITO_NAME` | string | Value of `SOURCE_KOITO_ID` | A vanity name | -| `SOURCE_KOITO_ENABLE` | boolean | | | +| _**`SOURCE_KOITO_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `SOURCE_KOITO_NAME` | string | Value of `SOURCE_KOITO_ID` | A vanity name EX `My Cool Component` | +| `SOURCE_KOITO_ENABLE` | boolean | true | Should this component be used? | | _**`SOURCE_KOITO_URL`**_ | string | | URL for the Koito server | | _**`SOURCE_KOITO_TOKEN`**_ | string | | User token for the user to scrobble for | | _**`SOURCE_KOITO_USER`**_ | string | | Username of the user to scrobble for | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_lastfm.md b/docsite/docs/configuration/sources/_env_configs/_lastfm.md index eb67b024..36bd2058 100644 --- a/docsite/docs/configuration/sources/_env_configs/_lastfm.md +++ b/docsite/docs/configuration/sources/_env_configs/_lastfm.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ----------------------------- | ------- | ------------------------------------- | ------------------------------------------------------- | -| _**`SOURCE_LASTFM_ID`**_ | string | | A globally unique ID | -| `SOURCE_LASTFM_NAME` | string | Value of `SOURCE_LASTFM_ID` | A vanity name | -| `SOURCE_LASTFM_ENABLE` | boolean | | | +| _**`SOURCE_LASTFM_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `SOURCE_LASTFM_NAME` | string | Value of `SOURCE_LASTFM_ID` | A vanity name EX `My Cool Component` | +| `SOURCE_LASTFM_ENABLE` | boolean | true | Should this component be used? | | _**`SOURCE_LASTFM_API_KEY`**_ | string | | API Key generated from Last.fm/Libre.fm account | | _**`SOURCE_LASTFM_SECRET`**_ | string | | Secret generated from Last.fm/Libre.fm account | | `SOURCE_LASTFM_REDIRECT_URI` | string | http://localhost:9078/lastfm/callback | Optional URI to use for callback. | diff --git a/docsite/docs/configuration/sources/_env_configs/_librefm.md b/docsite/docs/configuration/sources/_env_configs/_librefm.md index 98f64b1f..d166c498 100644 --- a/docsite/docs/configuration/sources/_env_configs/_librefm.md +++ b/docsite/docs/configuration/sources/_env_configs/_librefm.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ----------------------------- | ------- | -------------------------------------- | -------------------------------------------------------------- | -| _**`SOURCE_LIBREFM_ID`**_ | string | | A globally unique ID | -| `SOURCE_LIBREFM_NAME` | string | Value of `SOURCE_LIBREFM_ID` | A vanity name | -| `SOURCE_LIBREFM_ENABLE` | boolean | | | +| _**`SOURCE_LIBREFM_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `SOURCE_LIBREFM_NAME` | string | Value of `SOURCE_LIBREFM_ID` | A vanity name EX `My Cool Component` | +| `SOURCE_LIBREFM_ENABLE` | boolean | true | Should this component be used? | | `SOURCE_LIBREFM_API_KEY` | string | | Optional Secret for Libre.fm account | | `SOURCE_LIBREFM_SECRET` | string | | Optional Secret for Libre.fm account | | `SOURCE_LIBREFM_REDIRECT_URI` | string | http://localhost:9078/librefm/callback | Optional URI to use for callback. | diff --git a/docsite/docs/configuration/sources/_env_configs/_listenbrainz.md b/docsite/docs/configuration/sources/_env_configs/_listenbrainz.md index 229a59ee..c9de7fe5 100644 --- a/docsite/docs/configuration/sources/_env_configs/_listenbrainz.md +++ b/docsite/docs/configuration/sources/_env_configs/_listenbrainz.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ----------------------- | ------- | ----------------------------- | --------------------------------------------------------- | -| _**`SOURCE_LZ_ID`**_ | string | | A globally unique ID | -| `SOURCE_LZ_NAME` | string | Value of `SOURCE_LZ_ID` | A vanity name | -| `SOURCE_LZ_ENABLE` | boolean | | | +| _**`SOURCE_LZ_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `SOURCE_LZ_NAME` | string | Value of `SOURCE_LZ_ID` | A vanity name EX `My Cool Component` | +| `SOURCE_LZ_ENABLE` | boolean | true | Should this component be used? | | `SOURCE_LZ_URL` | string | https://api.listenbrainz.org/ | URL for the ListenBrainz server, if not using the default | | _**`SOURCE_LZ_TOKEN`**_ | string | | User token for the user to scrobble for | | _**`SOURCE_LZ_USER`**_ | string | | Username of the user to scrobble for | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_maloja.md b/docsite/docs/configuration/sources/_env_configs/_maloja.md index 0e76a60a..c0e6fc2d 100644 --- a/docsite/docs/configuration/sources/_env_configs/_maloja.md +++ b/docsite/docs/configuration/sources/_env_configs/_maloja.md @@ -1,7 +1,7 @@ -| Environmental Variable | Type | Default | Description | -| ----------------------------- | ------- | --------------------------- | ------------------------- | -| _**`SOURCE_MALOJA_ID`**_ | string | | A globally unique ID | -| `SOURCE_MALOJA_NAME` | string | Value of `SOURCE_MALOJA_ID` | A vanity name | -| `SOURCE_MALOJA_ENABLE` | boolean | | | -| _**`SOURCE_MALOJA_URL`**_ | string | | URL for maloja server | -| _**`SOURCE_MALOJA_API_KEY`**_ | string | | API Key for Maloja server | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ----------------------------- | ------- | --------------------------- | --------------------------------------- | +| _**`SOURCE_MALOJA_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `SOURCE_MALOJA_NAME` | string | Value of `SOURCE_MALOJA_ID` | A vanity name EX `My Cool Component` | +| `SOURCE_MALOJA_ENABLE` | boolean | true | Should this component be used? | +| _**`SOURCE_MALOJA_URL`**_ | string | | URL for maloja server | +| _**`SOURCE_MALOJA_API_KEY`**_ | string | | API Key for Maloja server | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_mopidy.md b/docsite/docs/configuration/sources/_env_configs/_mopidy.md index 4312f850..ef74f580 100644 --- a/docsite/docs/configuration/sources/_env_configs/_mopidy.md +++ b/docsite/docs/configuration/sources/_env_configs/_mopidy.md @@ -1,9 +1,9 @@ -| Environmental Variable | Type | Default | Description | -| ----------------------- | --------- | ------------------------------ | ------------------------------------------------------------------------------------------ | -| _**`MOPIDY_ID`**_ | string | | A globally unique ID | -| `MOPIDY_NAME` | string | Value of `MOPIDY_ID` | A vanity name | -| `MOPIDY_ENABLE` | boolean | | | -| `MOPIDY_URL` | string | ws://localhost:6680/mopidy/ws/ | URL of the Mopidy HTTP server to connect to | -| `MOPIDY_URI_DENYLIST` | transform | | Do not scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive | -| `MOPIDY_URI_ALLOWLIST` | transform | | Only scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive | -| `MOPIDY_ALBUM_DENYLIST` | transform | Soundcloud | Remove album data that matches any case-insensitive string from this list when scrobbling, | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ----------------------- | ------- | ------------------------------ | ------------------------------------------------------------------------------------------ | +| _**`MOPIDY_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `MOPIDY_NAME` | string | Value of `MOPIDY_ID` | A vanity name EX `My Cool Component` | +| `MOPIDY_ENABLE` | boolean | true | Should this component be used? | +| `MOPIDY_URL` | string | ws://localhost:6680/mopidy/ws/ | URL of the Mopidy HTTP server to connect to | +| `MOPIDY_URI_DENYLIST` | string | | Do not scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive | +| `MOPIDY_URI_ALLOWLIST` | string | | Only scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive | +| `MOPIDY_ALBUM_DENYLIST` | string | Soundcloud | Remove album data that matches any case-insensitive string from this list when scrobbling, | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_mpd.md b/docsite/docs/configuration/sources/_env_configs/_mpd.md index 3d7ad82f..685b06b0 100644 --- a/docsite/docs/configuration/sources/_env_configs/_mpd.md +++ b/docsite/docs/configuration/sources/_env_configs/_mpd.md @@ -1,7 +1,7 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ----------------- | -------------------------------------------------------------------------------------------------------- | -| _**`MPD_ID`**_ | string | | A globally unique ID | -| `MPD_NAME` | string | Value of `MPD_ID` | A vanity name | -| `MPD_ENABLE` | boolean | | | +| _**`MPD_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `MPD_NAME` | string | Value of `MPD_ID` | A vanity name EX `My Cool Component` | +| `MPD_ENABLE` | boolean | true | Should this component be used? | | `MPD_URL` | string | localhost:6600 | URL:PORT of the MPD server to connect to | | `MPD_PASSWORD` | string | | Password for the server, if set https://mpd.readthedocs.io/en/stable/user.html#permissions-and-passwords | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_mpris.md b/docsite/docs/configuration/sources/_env_configs/_mpris.md index 11685d97..160a018c 100644 --- a/docsite/docs/configuration/sources/_env_configs/_mpris.md +++ b/docsite/docs/configuration/sources/_env_configs/_mpris.md @@ -1,7 +1,7 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ------------------- | ------------------------------------------------------------------------------- | -| _**`MPRIS_ID`**_ | string | | A globally unique ID | -| `MPRIS_NAME` | string | Value of `MPRIS_ID` | A vanity name | -| `MPRIS_ENABLE` | boolean | | | +| _**`MPRIS_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `MPRIS_NAME` | string | Value of `MPRIS_ID` | A vanity name EX `My Cool Component` | +| `MPRIS_ENABLE` | boolean | true | Should this component be used? | | `MPRIS_BLACKLIST` | union | | DO NOT scrobble from any players that START WITH these values, case-insensitive | | `MPRIS_WHITELIST` | union | | ONLY from any players that START WITH these values, case-insensitive | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_musiccast.md b/docsite/docs/configuration/sources/_env_configs/_musiccast.md index 8cfd3c66..68041922 100644 --- a/docsite/docs/configuration/sources/_env_configs/_musiccast.md +++ b/docsite/docs/configuration/sources/_env_configs/_musiccast.md @@ -1,6 +1,6 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ------------------- | ------------------------------------------------------------ | -| _**`MCAST_ID`**_ | string | | A globally unique ID | -| `MCAST_NAME` | string | Value of `MCAST_ID` | A vanity name | -| `MCAST_ENABLE` | boolean | | | +| _**`MCAST_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `MCAST_NAME` | string | Value of `MCAST_ID` | A vanity name EX `My Cool Component` | +| `MCAST_ENABLE` | boolean | true | Should this component be used? | | _**`MCAST_URL`**_ | string | | The host or URL of the YamahaExtendedControl endpoint to use | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_musikcube.md b/docsite/docs/configuration/sources/_env_configs/_musikcube.md index f20056fb..621f946e 100644 --- a/docsite/docs/configuration/sources/_env_configs/_musikcube.md +++ b/docsite/docs/configuration/sources/_env_configs/_musikcube.md @@ -1,7 +1,7 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ------------------- | -------------------------------------------------------------------------------------------- | -| _**`MC_ID`**_ | string | | A globally unique ID | -| `MC_NAME` | string | Value of `MC_ID` | A vanity name | -| `MC_ENABLE` | boolean | | | +| _**`MC_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `MC_NAME` | string | Value of `MC_ID` | A vanity name EX `My Cool Component` | +| `MC_ENABLE` | boolean | true | Should this component be used? | | `MC_URL` | string | ws://localhost:7905 | URL of the Musikcube Websocket (Metadata) server to connect to | | _**`MC_PASSWORD`**_ | string | | Password set in Musikcube https://github.com/clangen/musikcube/wiki/remote-api-documentation | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_plex.md b/docsite/docs/configuration/sources/_env_configs/_plex.md index 74d1d3b3..3d50bf1c 100644 --- a/docsite/docs/configuration/sources/_env_configs/_plex.md +++ b/docsite/docs/configuration/sources/_env_configs/_plex.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ------------------ | ------------------------------------------------------------------------------------------------ | -| _**`PLEX_ID`**_ | string | | A globally unique ID | -| `PLEX_NAME` | string | Value of `PLEX_ID` | A vanity name | -| `PLEX_ENABLE` | boolean | | | +| _**`PLEX_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `PLEX_NAME` | string | Value of `PLEX_ID` | A vanity name EX `My Cool Component` | +| `PLEX_ENABLE` | boolean | true | Should this component be used? | | _**`PLEX_URL`**_ | string | | http(s)://HOST:PORT of the Plex server to connect to | | `PLEX_TOKEN` | string | | | | `PLEX_USERS_ALLOW` | union | | Only scrobble for specific users (case-insensitive) | diff --git a/docsite/docs/configuration/sources/_env_configs/_rocksky.md b/docsite/docs/configuration/sources/_env_configs/_rocksky.md index e87ad5d9..da073429 100644 --- a/docsite/docs/configuration/sources/_env_configs/_rocksky.md +++ b/docsite/docs/configuration/sources/_env_configs/_rocksky.md @@ -1,7 +1,7 @@ | Environmental Variable | Type | Default | Description | | ----------------------------- | ------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -| _**`SOURCE_ROCKSKY_ID`**_ | string | | A globally unique ID | -| `SOURCE_ROCKSKY_NAME` | string | Value of `SOURCE_ROCKSKY_ID` | A vanity name | -| `SOURCE_ROCKSKY_ENABLE` | boolean | | | +| _**`SOURCE_ROCKSKY_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `SOURCE_ROCKSKY_NAME` | string | Value of `SOURCE_ROCKSKY_ID` | A vanity name EX `My Cool Component` | +| `SOURCE_ROCKSKY_ENABLE` | boolean | true | Should this component be used? | | `SOURCE_ROCKSKY_KEY` | string | | API Key generated from [API Applications](https://docs.rocksky.app/migrating-from-listenbrainz-to-rocksky-1040189m0) in Rocksky for your account | | _**`SOURCE_ROCKSKY_HANDLE`**_ | string | | The **fully-qualified** handle for your ATPRoto/Bluesky account, like: | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_sonos.md b/docsite/docs/configuration/sources/_env_configs/_sonos.md index 34d22e6f..6608751d 100644 --- a/docsite/docs/configuration/sources/_env_configs/_sonos.md +++ b/docsite/docs/configuration/sources/_env_configs/_sonos.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------- | -| _**`SONOS_ID`**_ | string | | A globally unique ID | -| `SONOS_NAME` | string | Value of `SONOS_ID` | A vanity name | -| `SONOS_ENABLE` | boolean | | | +| _**`SONOS_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `SONOS_NAME` | string | Value of `SONOS_ID` | A vanity name EX `My Cool Component` | +| `SONOS_ENABLE` | boolean | true | Should this component be used? | | _**`SONOS_HOST`**_ | string | | IP address of any connected Sonos speaker or device | | `SONOS_DEVICES_ALLOW` | union | | Only scrobble if device name contains strings from this list (case-insensitive) | | `SONOS_DEVICES_BLOCK` | union | | Do not scrobble if device name contains strings from this list (case-insensitive) | diff --git a/docsite/docs/configuration/sources/_env_configs/_spotify.md b/docsite/docs/configuration/sources/_env_configs/_spotify.md index 5eab79b9..56a423f3 100644 --- a/docsite/docs/configuration/sources/_env_configs/_spotify.md +++ b/docsite/docs/configuration/sources/_env_configs/_spotify.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ----------------------------- | ------- | ------------------------------ | -------------------------------------------------------------------- | -| _**`SPOTIFY_ID`**_ | string | | A globally unique ID | -| `SPOTIFY_NAME` | string | Value of `SPOTIFY_ID` | A vanity name | -| `SPOTIFY_ENABLE` | boolean | | | +| _**`SPOTIFY_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `SPOTIFY_NAME` | string | Value of `SPOTIFY_ID` | A vanity name EX `My Cool Component` | +| `SPOTIFY_ENABLE` | boolean | true | Should this component be used? | | _**`SPOTIFY_CLIENT_ID`**_ | string | | spotify client id | | _**`SPOTIFY_CLIENT_SECRET`**_ | string | | spotify client secret | | `SPOTIFY_REDIRECT_URI` | string | http://localhost:9078/callback | spotify redirect URI -- required only if not the default shown here. | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_subsonic.md b/docsite/docs/configuration/sources/_env_configs/_subsonic.md index 552047c4..94e061d6 100644 --- a/docsite/docs/configuration/sources/_env_configs/_subsonic.md +++ b/docsite/docs/configuration/sources/_env_configs/_subsonic.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ------------------------- | ------- | ---------------------- | ------------------------------------------------- | -| _**`SUBSONIC_ID`**_ | string | | A globally unique ID | -| `SUBSONIC_NAME` | string | Value of `SUBSONIC_ID` | A vanity name | -| `SUBSONIC_ENABLE` | boolean | | | +| _**`SUBSONIC_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `SUBSONIC_NAME` | string | Value of `SUBSONIC_ID` | A vanity name EX `My Cool Component` | +| `SUBSONIC_ENABLE` | boolean | true | Should this component be used? | | _**`SUBSONIC_USER`**_ | string | | Username to login to the server with | | _**`SUBSONIC_PASSWORD`**_ | string | | Password for the user to login to the server with | | _**`SUBSONIC_URL`**_ | string | | URL of the subsonic media server to query | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_tealfm.md b/docsite/docs/configuration/sources/_env_configs/_tealfm.md index ad15a3fc..d10d475e 100644 --- a/docsite/docs/configuration/sources/_env_configs/_tealfm.md +++ b/docsite/docs/configuration/sources/_env_configs/_tealfm.md @@ -1,7 +1,7 @@ -| Environmental Variable | Type | Default | Description | -| -------------------------------- | ------- | --------------------------- | -------------------------------- | -| _**`SOURCE_TEALFM_ID`**_ | string | | A globally unique ID | -| `SOURCE_TEALFM_NAME` | string | Value of `SOURCE_TEALFM_ID` | A vanity name | -| `SOURCE_TEALFM_ENABLE` | boolean | | | -| _**`SOURCE_TEALFM_IDENTIFIER`**_ | string | | Identify the account to login as | -| `SOURCE_TEALFM_APP_PW` | string | | | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| -------------------------------- | ------- | --------------------------- | --------------------------------------- | +| _**`SOURCE_TEALFM_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `SOURCE_TEALFM_NAME` | string | Value of `SOURCE_TEALFM_ID` | A vanity name EX `My Cool Component` | +| `SOURCE_TEALFM_ENABLE` | boolean | true | Should this component be used? | +| _**`SOURCE_TEALFM_IDENTIFIER`**_ | string | | Identify the account to login as | +| `SOURCE_TEALFM_APP_PW` | string | | | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_vlc.md b/docsite/docs/configuration/sources/_env_configs/_vlc.md index 5418e50e..ebfa95e1 100644 --- a/docsite/docs/configuration/sources/_env_configs/_vlc.md +++ b/docsite/docs/configuration/sources/_env_configs/_vlc.md @@ -1,7 +1,7 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ----------------- | ---------------------------------------- | -| _**`VLC_ID`**_ | string | | A globally unique ID | -| `VLC_NAME` | string | Value of `VLC_ID` | A vanity name | -| `VLC_ENABLE` | boolean | | | +| _**`VLC_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `VLC_NAME` | string | Value of `VLC_ID` | A vanity name EX `My Cool Component` | +| `VLC_ENABLE` | boolean | true | Should this component be used? | | `VLC_URL` | string | localhost:8080 | URL:PORT of the VLC server to connect to | | _**`VLC_PASSWORD`**_ | string | | Password for the server | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_webscrobbler.md b/docsite/docs/configuration/sources/_env_configs/_webscrobbler.md index 79a7e4b1..48a0daa7 100644 --- a/docsite/docs/configuration/sources/_env_configs/_webscrobbler.md +++ b/docsite/docs/configuration/sources/_env_configs/_webscrobbler.md @@ -1,7 +1,7 @@ -| Environmental Variable | Type | Default | Description | -| ---------------------- | --------- | ---------------- | ----------------------------------------------------------- | -| _**`WS_ID`**_ | string | | A globally unique ID | -| `WS_NAME` | string | Value of `WS_ID` | A vanity name | -| `WS_ENABLE` | boolean | | | -| `WS_BLACKLIST` | transform | | Block scrobbling from specific WebScrobbler Connectors | -| `WS_WHITELIST` | transform | | Only allow scrobbling from specific WebScrobbler Connectors | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ---------------- | ----------------------------------------------------------- | +| _**`WS_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `WS_NAME` | string | Value of `WS_ID` | A vanity name EX `My Cool Component` | +| `WS_ENABLE` | boolean | true | Should this component be used? | +| `WS_BLACKLIST` | string | | Block scrobbling from specific WebScrobbler Connectors | +| `WS_WHITELIST` | string | | Only allow scrobbling from specific WebScrobbler Connectors | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_ymbridge.md b/docsite/docs/configuration/sources/_env_configs/_ymbridge.md index 5c6a0c35..1f0e75a7 100644 --- a/docsite/docs/configuration/sources/_env_configs/_ymbridge.md +++ b/docsite/docs/configuration/sources/_env_configs/_ymbridge.md @@ -1,7 +1,7 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ---------------------- | --------------------------------------------------------------------------- | -| _**`YMBRIDGE_ID`**_ | string | | A globally unique ID | -| `YMBRIDGE_NAME` | string | Value of `YMBRIDGE_ID` | A vanity name | -| `YMBRIDGE_ENABLE` | boolean | | | +| _**`YMBRIDGE_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `YMBRIDGE_NAME` | string | Value of `YMBRIDGE_ID` | A vanity name EX `My Cool Component` | +| `YMBRIDGE_ENABLE` | boolean | true | Should this component be used? | | _**`YMBRIDGE_URL`**_ | string | | URL of the local Python bridge, for example http://yandex-music-bridge:9980 | | `YMBRIDGE_API_KEY` | string | | Optional API key sent as X-API-Key to the bridge | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_ytmusic.md b/docsite/docs/configuration/sources/_env_configs/_ytmusic.md index 445e1f6d..39f639fd 100644 --- a/docsite/docs/configuration/sources/_env_configs/_ytmusic.md +++ b/docsite/docs/configuration/sources/_env_configs/_ytmusic.md @@ -1,8 +1,8 @@ | Environmental Variable | Type | Default | Description | | ---------------------- | ------- | ----------------- | ------------------------------------------------------------------------------------ | -| _**`YTM_ID`**_ | string | | A globally unique ID | -| `YTM_NAME` | string | Value of `YTM_ID` | A vanity name | -| `YTM_ENABLE` | boolean | | | +| _**`YTM_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `YTM_NAME` | string | Value of `YTM_ID` | A vanity name EX `My Cool Component` | +| `YTM_ENABLE` | boolean | true | Should this component be used? | | `YTM_REDIRECT_URI` | string | | Google Cloud Console project OAuth Client Authorized redirect URI | | `YTM_CLIENT_ID` | string | | Google Cloud Console project OAuth Client ID | | `YTM_CLIENT_SECRET` | string | | Google Cloud Console project OAuth Client Secret | -- 2.51.2 From 425a0b28078df9c6356b5b93a8fca56feea8a1da Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Sat, 1 Aug 2026 17:47:44 +0000 Subject: [PATCH 25/35] Partial fix for flattening complex zod types for docs --- src/backend/utils/SchemaDocsStaticUtils.ts | 2 +- src/backend/utils/ZodUtils.ts | 40 +++++++++++++++++++--- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/backend/utils/SchemaDocsStaticUtils.ts b/src/backend/utils/SchemaDocsStaticUtils.ts index b93e6ccc..0d1993cf 100644 --- a/src/backend/utils/SchemaDocsStaticUtils.ts +++ b/src/backend/utils/SchemaDocsStaticUtils.ts @@ -17,7 +17,7 @@ const mdCols = (cols: TableColumn[]): string[][] => cols.map((x) => { const name = `\`${x.title}\``; return [ x.required ? `_**${name}**_` : name, - x.type, + Array.isArray(x.type) ? x.type.join(' OR ') : x.type, (x.default ?? '').toString(), x.description ?? '' ] diff --git a/src/backend/utils/ZodUtils.ts b/src/backend/utils/ZodUtils.ts index 3da07421..0a4b1c63 100644 --- a/src/backend/utils/ZodUtils.ts +++ b/src/backend/utils/ZodUtils.ts @@ -4,7 +4,7 @@ import { parseArrayFromMaybeString, parseBoolOrArrayFromMaybeString } from './St export interface TableColumn { title: string required: boolean - type: string + type: string | string[] default: unknown description: string | undefined } @@ -13,13 +13,13 @@ export type PipeUnwrapDirection = 'in' | 'out'; const otherPipeDirection: Record = { in: 'out', out: 'in' }; -const unwrapZodType = (schema: z.ZodTypeAny, pipeDirection: PipeUnwrapDirection = 'in'): z.ZodTypeAny => { +const unwrapWrappers = (schema: z.ZodTypeAny, pipeDirection: PipeUnwrapDirection): z.ZodTypeAny => { let current = schema; while (current instanceof z.ZodOptional || current instanceof z.ZodNullable || current instanceof z.ZodDefault || current instanceof z.ZodPipe) { if (current instanceof z.ZodPipe) { const side = current[pipeDirection]; // ZodTransform carries no schema of its own, so its side of the pipe can't tell us - // anything about the resulting shape, fall back to the other side of the pipe. + // anything about the resulting shape -- fall back to the other side of the pipe. current = (side instanceof z.ZodTransform ? current[otherPipeDirection[pipeDirection]] : side) as z.ZodTypeAny; } else { current = current.unwrap() as z.ZodTypeAny; @@ -27,6 +27,38 @@ const unwrapZodType = (schema: z.ZodTypeAny, pipeDirection: PipeUnwrapDirection } return current; }; + +const literalValues = (schema: z.ZodLiteral): string[] => schema.def.values.map(String); + +/** + * Describes the final, meaningful type(s) accepted by a schema, unwrapping any + * optional/nullable/default/pipe wrappers first. + * + * ZodLiteral and ZodEnum resolve to their actual allowed value(s) rather than their + * type name, since "literal"/"enum" tell an end user nothing about what to enter. + * ZodUnion resolves to the flattened, deduped description of all its members. Any + * other type (object, array, promise, string, number, etc) is left as its type name -- + * it's not broken down any further. + */ +const describeType = (schema: z.ZodTypeAny, pipeDirection: PipeUnwrapDirection): string | string[] => { + const core = unwrapWrappers(schema, pipeDirection); + if (core instanceof z.ZodLiteral) { + return literalValues(core); + } + if (core instanceof z.ZodEnum) { + return Object.values(core.def.entries).map(String); + } + if (core instanceof z.ZodUnion) { + return [...new Set(core.options.flatMap((opt) => describeType(opt as z.ZodTypeAny, pipeDirection)))]; + } + return core.def.type; +}; + +const unwrapZodType = (schema: z.ZodTypeAny, pipeDirection: PipeUnwrapDirection = 'in'): string | string[] => { + const described = describeType(schema, pipeDirection); + return Array.isArray(described) && described.length === 1 ? described[0] : described; +}; + const getExplicitDefault = (schema: z.ZodTypeAny): unknown => { let current = schema; while (true) { @@ -54,7 +86,7 @@ export const zodObjectToTableColumns = (schema: z.Z return { title: (meta?.title as string | undefined) ?? key, required: !fieldSchema.isOptional(), - type: unwrapZodType(fieldSchema, pipeDirection).def.type, + type: unwrapZodType(fieldSchema, pipeDirection), default: getExplicitDefault(fieldSchema) ?? (meta?.default as unknown), description: meta?.description as string | undefined }; -- 2.51.2 From ff7360fcede4d8e5edfb567efbd6c2f6a70c828c Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Sat, 1 Aug 2026 19:07:35 +0000 Subject: [PATCH 26/35] docs: More env row generation improvements * Always use `out` of common envs * Wrapper function for normalizing descriptions with comma-separate lists --- src/backend/utils/SchemaDocsStaticUtils.ts | 6 ++++-- src/backend/utils/StringUtils.ts | 4 +++- src/backend/utils/ZodUtils.ts | 15 ++++++++++++--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/backend/utils/SchemaDocsStaticUtils.ts b/src/backend/utils/SchemaDocsStaticUtils.ts index 0d1993cf..76a5d4bf 100644 --- a/src/backend/utils/SchemaDocsStaticUtils.ts +++ b/src/backend/utils/SchemaDocsStaticUtils.ts @@ -62,7 +62,8 @@ for(const [k,v] of clientEntries) { const envSchema = envSchemas; const common = generateCommonComponentEnvConfigSchema(envSchema.prefix.toUpperCase()); - const col = zodObjectToTableColumns(z.object({...common.shape,...envSchema.env.shape}), 'out'); + const commonCol = zodObjectToTableColumns(z.object(common.shape), 'out'); + const col = [...commonCol,...zodObjectToTableColumns(z.object(envSchema.env.shape), envSchemas.pipe ?? 'out')] const tableContent = markdownTable([ ['Environmental Variable', 'Type', 'Default', 'Description'], ...mdCols(col) @@ -77,7 +78,8 @@ for(const [k,v] of sourcesEntries) { const envSchema = envSchemas; const common = generateCommonComponentEnvConfigSchema(envSchema.prefix.toUpperCase()); - const col = zodObjectToTableColumns(z.object({...common.shape,...envSchema.env.shape}), 'out'); + const commonCol = zodObjectToTableColumns(z.object(common.shape), 'out'); + const col = [...commonCol,...zodObjectToTableColumns(z.object(envSchema.env.shape), envSchemas.pipe ?? 'out')] const tableContent = markdownTable([ ['Environmental Variable', 'Type', 'Default', 'Description'], ...mdCols(col) diff --git a/src/backend/utils/StringUtils.ts b/src/backend/utils/StringUtils.ts index d9fa1026..8affc5a3 100644 --- a/src/backend/utils/StringUtils.ts +++ b/src/backend/utils/StringUtils.ts @@ -502,4 +502,6 @@ export const stringToDeterministicNumber = (str: string): number => { } // Convert to unsigned 32-bit integer to avoid negative values return hash >>> 0; -} \ No newline at end of file +} + +export const commaSeparatedListReplace = (str: string): string => str.replace('list', 'comma-delimited list'); \ No newline at end of file diff --git a/src/backend/utils/ZodUtils.ts b/src/backend/utils/ZodUtils.ts index 0a4b1c63..f8f00b54 100644 --- a/src/backend/utils/ZodUtils.ts +++ b/src/backend/utils/ZodUtils.ts @@ -1,5 +1,5 @@ import z from 'zod'; -import { parseArrayFromMaybeString, parseBoolOrArrayFromMaybeString } from './StringUtils.ts'; +import { commaSeparatedListReplace, parseArrayFromMaybeString, parseBoolOrArrayFromMaybeString } from './StringUtils.ts'; export interface TableColumn { title: string @@ -92,5 +92,14 @@ export const zodObjectToTableColumns = (schema: z.Z }; }); -export const transformSplitMaybeString = z.transform((val: string) => parseArrayFromMaybeString(val)); -export const transformSplitMaybeStringOrBoolean = z.transform((val: string | string[] | boolean) => parseBoolOrArrayFromMaybeString(val)); \ No newline at end of file +export const transformSplitMaybeString = z.transform((val: string) => val === undefined ? undefined : parseArrayFromMaybeString(val)); +export const transformSplitMaybeStringOrBoolean = z.transform((val: string | true) => val === undefined ? undefined : parseBoolOrArrayFromMaybeString(val)); + +export const envMetaNormalize = (meta: z.GlobalMeta): z.GlobalMeta => { + if(meta.description !== undefined) { + return { + ...meta, + description: commaSeparatedListReplace(meta.description) + } + } +} \ No newline at end of file -- 2.51.2 From 46a061a6020bcca40ebe9dc98a104cf5d190a10d Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Sat, 1 Aug 2026 19:07:49 +0000 Subject: [PATCH 27/35] docs: Update comma-separated list zod types for envs --- .../clients/_env_configs/_discord.md | 24 +++++----- .../sources/_env_configs/_chromecast.md | 18 ++++---- .../sources/_env_configs/_endpointlfm.md | 2 +- .../sources/_env_configs/_jellyfin.md | 34 +++++++------- .../sources/_env_configs/_mpris.md | 14 +++--- .../sources/_env_configs/_plex.md | 26 +++++------ .../sources/_env_configs/_sonos.md | 20 ++++----- .../infrastructure/config/client/index.ts | 2 + .../config/source/chromecast.ts | 24 +++++----- .../config/source/endpointlfm.ts | 2 +- .../infrastructure/config/source/index.ts | 2 + .../infrastructure/config/source/jellyfin.ts | 44 ++++++++++--------- .../infrastructure/config/source/mpris.ts | 9 ++-- .../infrastructure/config/source/plex.ts | 39 ++++++++-------- .../infrastructure/config/source/sonos.ts | 26 ++++++----- 15 files changed, 149 insertions(+), 137 deletions(-) diff --git a/docsite/docs/configuration/clients/_env_configs/_discord.md b/docsite/docs/configuration/clients/_env_configs/_discord.md index f2a81607..35f8d1f8 100644 --- a/docsite/docs/configuration/clients/_env_configs/_discord.md +++ b/docsite/docs/configuration/clients/_env_configs/_discord.md @@ -1,12 +1,12 @@ -| Environmental Variable | Type | Default | Description | -| ---------------------------------- | ------- | --------------------- | --------------------------------------- | -| _**`DISCORD_ID`**_ | string | | A globally unique ID EX `myComponentId` | -| `DISCORD_NAME` | string | Value of `DISCORD_ID` | A vanity name EX `My Cool Component` | -| `DISCORD_ENABLE` | boolean | true | Should this component be used? | -| `DISCORD_TOKEN` | string | | | -| `DISCORD_ARTWORK` | union | | | -| `DISCORD_APPLICATION_ID` | string | | | -| `DISCORD_IPC_LOCATIONS` | union | | | -| `DISCORD_ARTWORK_DEFAULT_URL` | string | | | -| `DISCORD_STATUS_OVERRIDE_ALLOW` | union | | | -| `DISCORD_LISTENING_ACTIVITY_ALLOW` | union | | | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ---------------------------------- | -------------------------- | --------------------- | --------------------------------------- | +| _**`DISCORD_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `DISCORD_NAME` | string | Value of `DISCORD_ID` | A vanity name EX `My Cool Component` | +| `DISCORD_ENABLE` | boolean | true | Should this component be used? | +| `DISCORD_TOKEN` | string | | | +| `DISCORD_ARTWORK` | boolean OR string OR array | | | +| `DISCORD_APPLICATION_ID` | string | | | +| `DISCORD_IPC_LOCATIONS` | string OR array | | | +| `DISCORD_ARTWORK_DEFAULT_URL` | string | | | +| `DISCORD_STATUS_OVERRIDE_ALLOW` | string OR array | | | +| `DISCORD_LISTENING_ACTIVITY_ALLOW` | string OR array | | | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_chromecast.md b/docsite/docs/configuration/sources/_env_configs/_chromecast.md index 6b6a5c8f..29b1342a 100644 --- a/docsite/docs/configuration/sources/_env_configs/_chromecast.md +++ b/docsite/docs/configuration/sources/_env_configs/_chromecast.md @@ -1,9 +1,9 @@ -| Environmental Variable | Type | Default | Description | -| ---------------------- | ------- | ---------------- | ------------------------------------------------------------------------------------ | -| _**`CC_ID`**_ | string | | A globally unique ID EX `myComponentId` | -| `CC_NAME` | string | Value of `CC_ID` | A vanity name EX `My Cool Component` | -| `CC_ENABLE` | boolean | true | Should this component be used? | -| `CC_BLACKLIST_DEVICES` | string | | DO NOT scrobble from any cast devices that START WITH these values, case-insensitive | -| `CC_WHITELIST_DEVICES` | string | | ONLY scrobble from any cast device that START WITH these values, case-insensitive | -| `CC_BLACKLIST_APPS` | string | | DO NOT scrobble from any application that START WITH these values, case-insensitive | -| `CC_WHITELIST_APPS` | string | | ONLY scrobble from any application that START WITH these values, case-insensitive | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ---------------- | ----------------------------------------------------------------------------------------------- | +| _**`CC_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `CC_NAME` | string | Value of `CC_ID` | A vanity name EX `My Cool Component` | +| `CC_ENABLE` | boolean | true | Should this component be used? | +| `CC_BLACKLIST_DEVICES` | string | | DO NOT scrobble from any cast devices on this comma-delimited list that START WITH these values | +| `CC_WHITELIST_DEVICES` | string | | ONLY scrobble from any cast device on this comma-delimited list that START WITH these values | +| `CC_BLACKLIST_APPS` | string | | DO NOT scrobble from any application on this comma-delimited list that START WITH these values | +| `CC_WHITELIST_APPS` | string | | ONLY scrobble from any application on this comma-delimited list that START WITH these values | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_endpointlfm.md b/docsite/docs/configuration/sources/_env_configs/_endpointlfm.md index 5f4a999c..df70de16 100644 --- a/docsite/docs/configuration/sources/_env_configs/_endpointlfm.md +++ b/docsite/docs/configuration/sources/_env_configs/_endpointlfm.md @@ -3,4 +3,4 @@ | _**`LFM_ID`**_ | string | | A globally unique ID EX `myComponentId` | | `LFM_NAME` | string | Value of `LFM_ID` | A vanity name EX `My Cool Component` | | `LFM_ENABLE` | boolean | true | Should this component be used? | -| `LFM_SLUG` | union | | The URL ending that should be used to identify scrobbles for this source | \ No newline at end of file +| `LFM_SLUG` | string | | The URL ending that should be used to identify scrobbles for this source | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_jellyfin.md b/docsite/docs/configuration/sources/_env_configs/_jellyfin.md index 92d7c0e7..6c6a1409 100644 --- a/docsite/docs/configuration/sources/_env_configs/_jellyfin.md +++ b/docsite/docs/configuration/sources/_env_configs/_jellyfin.md @@ -1,17 +1,17 @@ -| Environmental Variable | Type | Default | Description | -| -------------------------------- | ------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -| _**`JELLYFIN_ID`**_ | string | | A globally unique ID EX `myComponentId` | -| `JELLYFIN_NAME` | string | Value of `JELLYFIN_ID` | A vanity name EX `My Cool Component` | -| `JELLYFIN_ENABLE` | boolean | true | Should this component be used? | -| _**`JELLYFIN_USER`**_ | string | | The username of the user to authenticate for or track scrobbles for | -| `JELLYFIN_PASSWORD` | string | | Password of the username to authenticate for | -| `JELLYFIN_APIKEY` | string | | API Key to authenticate with. | -| _**`JELLYFIN_URL`**_ | string | | HOST:PORT of the Jellyfin server to connect to | -| `JELLYFIN_USERS_ALLOW` | union | | Only scrobble for specific users (case-insensitive) | -| `JELLYFIN_USERS_BLOCK` | union | | Do not scrobble for these users (case-insensitive) | -| `JELLYFIN_DEVICES_ALLOW` | union | | Only scrobble if device or application name contains strings from this list (case-insensitive) | -| `JELLYFIN_DEVICES_BLOCK` | union | | Do not scrobble if device or application name contains strings from this list (case-insensitive) | -| `JELLYFIN_LIBRARIES_ALLOW` | union | | Only scrobble if library name contains string from this list (case-insensitive) | -| `JELLYFIN_LIBRARIES_BLOCK` | union | | Do not scrobble if library name contains strings from this list (case-insensitive) | -| `JELLYFIN_FRONTEND_URL_OVERRIDE` | string | | HOST:PORT of the Jellyfin server that your browser will be able to access from the frontend (and thus load images and links from) | -| `JELLYFIN_MEDIATYPES_ALLOW` | union | | Allow these media types to be scrobbled. | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| -------------------------------- | -------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| _**`JELLYFIN_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `JELLYFIN_NAME` | string | Value of `JELLYFIN_ID` | A vanity name EX `My Cool Component` | +| `JELLYFIN_ENABLE` | boolean | true | Should this component be used? | +| _**`JELLYFIN_USER`**_ | string | | The username of the user to authenticate for or track scrobbles for | +| `JELLYFIN_PASSWORD` | string | | Password of the username to authenticate for | +| `JELLYFIN_APIKEY` | string | | API Key to authenticate with. | +| _**`JELLYFIN_URL`**_ | string | | HOST:PORT of the Jellyfin server to connect to | +| `JELLYFIN_USERS_ALLOW` | string OR true | | Only scrobble for specific users from this comma-delimited list. If `true`, scrobble for all users. | +| `JELLYFIN_USERS_BLOCK` | string | | Do not scrobble for users from this comma-delimited list | +| `JELLYFIN_DEVICES_ALLOW` | string | | Only scrobble if device or application name contains strings from this comma-delimited list | +| `JELLYFIN_DEVICES_BLOCK` | string | | Do not scrobble if device or application name contains strings from this comma-delimited list | +| `JELLYFIN_LIBRARIES_ALLOW` | string | | Only scrobble if library name contains string from this comma-delimited list | +| `JELLYFIN_LIBRARIES_BLOCK` | string | | Do not scrobble if library name contains strings from this comma-delimited list | +| `JELLYFIN_FRONTEND_URL_OVERRIDE` | string | | HOST:PORT of the Jellyfin server that your browser will be able to access from the frontend (and thus load images and links from) | +| `JELLYFIN_MEDIATYPES_ALLOW` | string | | Allow media types from this comma-delimited list to be scrobbled | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_mpris.md b/docsite/docs/configuration/sources/_env_configs/_mpris.md index 160a018c..973fc9dc 100644 --- a/docsite/docs/configuration/sources/_env_configs/_mpris.md +++ b/docsite/docs/configuration/sources/_env_configs/_mpris.md @@ -1,7 +1,7 @@ -| Environmental Variable | Type | Default | Description | -| ---------------------- | ------- | ------------------- | ------------------------------------------------------------------------------- | -| _**`MPRIS_ID`**_ | string | | A globally unique ID EX `myComponentId` | -| `MPRIS_NAME` | string | Value of `MPRIS_ID` | A vanity name EX `My Cool Component` | -| `MPRIS_ENABLE` | boolean | true | Should this component be used? | -| `MPRIS_BLACKLIST` | union | | DO NOT scrobble from any players that START WITH these values, case-insensitive | -| `MPRIS_WHITELIST` | union | | ONLY from any players that START WITH these values, case-insensitive | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ------------------- | -------------------------------------------------------------------------------------- | +| _**`MPRIS_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `MPRIS_NAME` | string | Value of `MPRIS_ID` | A vanity name EX `My Cool Component` | +| `MPRIS_ENABLE` | boolean | true | Should this component be used? | +| `MPRIS_BLACKLIST` | string | | DO NOT scrobble from any players that START WITH values from this comma-delimited list | +| `MPRIS_WHITELIST` | string | | ONLY scrobble from any players that START WITH values from this comma-delimited list | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_plex.md b/docsite/docs/configuration/sources/_env_configs/_plex.md index 3d50bf1c..e91438a3 100644 --- a/docsite/docs/configuration/sources/_env_configs/_plex.md +++ b/docsite/docs/configuration/sources/_env_configs/_plex.md @@ -1,13 +1,13 @@ -| Environmental Variable | Type | Default | Description | -| ---------------------- | ------- | ------------------ | ------------------------------------------------------------------------------------------------ | -| _**`PLEX_ID`**_ | string | | A globally unique ID EX `myComponentId` | -| `PLEX_NAME` | string | Value of `PLEX_ID` | A vanity name EX `My Cool Component` | -| `PLEX_ENABLE` | boolean | true | Should this component be used? | -| _**`PLEX_URL`**_ | string | | http(s)://HOST:PORT of the Plex server to connect to | -| `PLEX_TOKEN` | string | | | -| `PLEX_USERS_ALLOW` | union | | Only scrobble for specific users (case-insensitive) | -| `PLEX_USERS_BLOCK` | union | | Do not scrobble for these users (case-insensitive) | -| `PLEX_DEVICES_ALLOW` | union | | Only scrobble if device or application name contains strings from this list (case-insensitive) | -| `PLEX_DEVICES_BLOCK` | union | | Do not scrobble if device or application name contains strings from this list (case-insensitive) | -| `PLEX_LIBRARIES_ALLOW` | union | | Only scrobble if library name contains string from this list (case-insensitive) | -| `PLEX_LIBRARIES_BLOCK` | union | | Do not scrobble if library name contains strings from this list (case-insensitive) | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ------------------ | --------------------------------------------------------------------------------------------- | +| _**`PLEX_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `PLEX_NAME` | string | Value of `PLEX_ID` | A vanity name EX `My Cool Component` | +| `PLEX_ENABLE` | boolean | true | Should this component be used? | +| _**`PLEX_URL`**_ | string | | http(s)://HOST:PORT of the Plex server to connect to | +| `PLEX_TOKEN` | string | | | +| `PLEX_USERS_ALLOW` | string | | Only scrobble for specific users from this comma-delimited list | +| `PLEX_USERS_BLOCK` | string | | Do not scrobble for users from this comma-delimited list | +| `PLEX_DEVICES_ALLOW` | string | | Do not scrobble if device or application name contains strings from this comma-delimited list | +| `PLEX_DEVICES_BLOCK` | string | | Only scrobble if device or application name contains strings from this comma-delimited list | +| `PLEX_LIBRARIES_ALLOW` | string | | Only scrobble if library name contains string from this comma-delimited list | +| `PLEX_LIBRARIES_BLOCK` | string | | Do not scrobble if library name contains strings from this comma-delimited list | \ No newline at end of file diff --git a/docsite/docs/configuration/sources/_env_configs/_sonos.md b/docsite/docs/configuration/sources/_env_configs/_sonos.md index 6608751d..24482159 100644 --- a/docsite/docs/configuration/sources/_env_configs/_sonos.md +++ b/docsite/docs/configuration/sources/_env_configs/_sonos.md @@ -1,10 +1,10 @@ -| Environmental Variable | Type | Default | Description | -| ---------------------- | ------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------- | -| _**`SONOS_ID`**_ | string | | A globally unique ID EX `myComponentId` | -| `SONOS_NAME` | string | Value of `SONOS_ID` | A vanity name EX `My Cool Component` | -| `SONOS_ENABLE` | boolean | true | Should this component be used? | -| _**`SONOS_HOST`**_ | string | | IP address of any connected Sonos speaker or device | -| `SONOS_DEVICES_ALLOW` | union | | Only scrobble if device name contains strings from this list (case-insensitive) | -| `SONOS_DEVICES_BLOCK` | union | | Do not scrobble if device name contains strings from this list (case-insensitive) | -| `SONOS_GROUPS_ALLOW` | union | | Only scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive) | -| `SONOS_GROUPS_BLOCK` | union | | Do not scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive) | \ No newline at end of file +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | ------------------- | -------------------------------------------------------------------------------------------------------------------- | +| _**`SONOS_ID`**_ | string | | A globally unique ID EX `myComponentId` | +| `SONOS_NAME` | string | Value of `SONOS_ID` | A vanity name EX `My Cool Component` | +| `SONOS_ENABLE` | boolean | true | Should this component be used? | +| _**`SONOS_HOST`**_ | string | | IP address of any connected Sonos speaker or device | +| `SONOS_DEVICES_ALLOW` | string | | Only scrobble if device name contains strings from this comma-delimited list | +| `SONOS_DEVICES_BLOCK` | string | | Do not scrobble if device name contains strings from this comma-delimited list | +| `SONOS_GROUPS_ALLOW` | string | | Only scrobble if the name of a group the playing device belongs to contains strings from this comma-delimited list | +| `SONOS_GROUPS_BLOCK` | string | | Do not scrobble if the name of a group the playing device belongs to contains strings from this comma-delimited list | \ No newline at end of file diff --git a/src/backend/common/infrastructure/config/client/index.ts b/src/backend/common/infrastructure/config/client/index.ts index 9bb0d173..967d7381 100644 --- a/src/backend/common/infrastructure/config/client/index.ts +++ b/src/backend/common/infrastructure/config/client/index.ts @@ -2,6 +2,7 @@ import * as z from "zod"; import {playTransformOptionsSchema} from "../../../../../core/Transform.ts"; import {commonConfigSchema, requestRetryOptionsSchema, monitorOptionsSchema, type CommonComponentEnvShape} from "../common.ts"; import {retentionConfigDurationValueSchema} from "../database.ts"; +import type { PipeUnwrapDirection } from "../../../../utils/ZodUtils.ts"; /** * Scrobble matching (between new source track and existing client scrobbles) logging options. Used for debugging. @@ -187,6 +188,7 @@ export type CommonClientConfig = Omit, export interface EnvClientSchema { env: Y + pipe?: PipeUnwrapDirection prefix: string toConfig: (parsed: z.output) => Partial> } \ No newline at end of file diff --git a/src/backend/common/infrastructure/config/source/chromecast.ts b/src/backend/common/infrastructure/config/source/chromecast.ts index dd70c271..f33e646e 100644 --- a/src/backend/common/infrastructure/config/source/chromecast.ts +++ b/src/backend/common/infrastructure/config/source/chromecast.ts @@ -1,6 +1,6 @@ import * as z from "zod"; import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; -import { transformSplitMaybeString } from "../../../../utils/ZodUtils.ts"; +import { envMetaNormalize, transformSplitMaybeString } from "../../../../utils/ZodUtils.ts"; export const chromecastDeviceInfoSchema = z.object({ /** @@ -35,12 +35,12 @@ export const chromecastDataSchema = z.object({ * @examples [["home-mini","family-tv"]] * */ blacklistDevices: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "DO NOT scrobble from any cast devices that START WITH these values, case-insensitive", + description: "DO NOT scrobble from any cast devices on this list that START WITH these values", examples: [["home-mini", "family-tv"]] }), /** - * ONLY scrobble from any cast device that START WITH these values, case-insensitive + * ONLY scrobble from any cast device that START WITH these values * * If whitelist is present then blacklist is ignored * @@ -49,29 +49,29 @@ export const chromecastDataSchema = z.object({ * @examples [["home-mini","family-tv"]] * */ whitelistDevices: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "ONLY scrobble from any cast device that START WITH these values, case-insensitive", + description: "ONLY scrobble from any cast device on this list that START WITH these values", examples: [["home-mini", "family-tv"]] }), /** - * DO NOT scrobble from any application that START WITH these values, case-insensitive + * DO NOT scrobble from any application that START WITH these values * * @examples [["spotify","pandora"]] * */ blacklistApps: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "DO NOT scrobble from any application that START WITH these values, case-insensitive", + description: "DO NOT scrobble from any application on this list that START WITH these values", examples: [["spotify", "pandora"]] }), /** - * ONLY scrobble from any application that START WITH these values, case-insensitive + * ONLY scrobble from any application that START WITH these values * * If whitelist is present then blacklist is ignored * * @examples [["spotify","pandora"]] * */ whitelistApps: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "ONLY scrobble from any application that START WITH these values, case-insensitive", + description: "ONLY scrobble from any application on this list that START WITH these values", examples: [["spotify", "pandora"]] }), @@ -134,10 +134,10 @@ export const chromecastDataSchema = z.object({ export type ChromecastData = z.infer; const envDataSchema = z.object({ - CC_BLACKLIST_DEVICES: z.string().optional().pipe(transformSplitMaybeString).meta(chromecastDataSchema.shape.blacklistDevices.meta()), - CC_WHITELIST_DEVICES: z.string().optional().pipe(transformSplitMaybeString).meta(chromecastDataSchema.shape.whitelistDevices.meta()), - CC_BLACKLIST_APPS: z.string().optional().pipe(transformSplitMaybeString).meta(chromecastDataSchema.shape.blacklistApps.meta()), - CC_WHITELIST_APPS: z.string().optional().pipe(transformSplitMaybeString).meta(chromecastDataSchema.shape.whitelistApps.meta()), + CC_BLACKLIST_DEVICES: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(chromecastDataSchema.shape.blacklistDevices.meta())), + CC_WHITELIST_DEVICES: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(chromecastDataSchema.shape.whitelistDevices.meta())), + CC_BLACKLIST_APPS: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(chromecastDataSchema.shape.blacklistApps.meta())), + CC_WHITELIST_APPS: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(chromecastDataSchema.shape.whitelistApps.meta())), }); export const envSchemas: EnvSourceSchema = { diff --git a/src/backend/common/infrastructure/config/source/endpointlfm.ts b/src/backend/common/infrastructure/config/source/endpointlfm.ts index e4d60591..eacea887 100644 --- a/src/backend/common/infrastructure/config/source/endpointlfm.ts +++ b/src/backend/common/infrastructure/config/source/endpointlfm.ts @@ -15,7 +15,7 @@ export const lastFmEndpointDataSchema = z.object({ * * If no slug is found from an extension's incoming webhook event the first Last.fm source without a slug will be used * */ - slug: z.union([z.string(), z.null()]).optional().meta({ + slug: z.string().optional().meta({ description: "The URL ending that should be used to identify scrobbles for this source" }), }); diff --git a/src/backend/common/infrastructure/config/source/index.ts b/src/backend/common/infrastructure/config/source/index.ts index 7e6bd447..5698d38b 100644 --- a/src/backend/common/infrastructure/config/source/index.ts +++ b/src/backend/common/infrastructure/config/source/index.ts @@ -2,6 +2,7 @@ import * as z from "zod"; import {requestRetryOptionsSchema, commonConfigSchema, monitorOptionsSchema} from "../common.ts"; import {retentionConfigDurationValueSchema} from "../database.ts"; import {playTransformOptionsSchema} from "../../../../../core/Transform.ts"; +import type { PipeUnwrapDirection } from "../../../../utils/ZodUtils.ts"; export const sourceRetryOptionsSchema = z.object({ ...requestRetryOptionsSchema.shape, @@ -208,6 +209,7 @@ export type CommonSourceConfig = Omit, export interface EnvSourceSchema { env: Y + pipe?: PipeUnwrapDirection prefix: string, toConfig: (parsed: z.output) => Partial> } \ No newline at end of file diff --git a/src/backend/common/infrastructure/config/source/jellyfin.ts b/src/backend/common/infrastructure/config/source/jellyfin.ts index b006c135..d86acc4c 100644 --- a/src/backend/common/infrastructure/config/source/jellyfin.ts +++ b/src/backend/common/infrastructure/config/source/jellyfin.ts @@ -1,6 +1,8 @@ import * as z from "zod"; import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, type EnvSourceSchema} from "./index.ts"; +import { envMetaNormalize, transformSplitMaybeString, transformSplitMaybeStringOrBoolean } from "../../../../utils/ZodUtils.ts"; +export const jellyfinMediaTypesSchema = z.enum(['unknown','video','audio','photo','book','musicvideo']); export const jellyApiDataSchema = z.object({ ...commonSourceDataSchema.shape, /** @@ -33,44 +35,44 @@ export const jellyApiDataSchema = z.object({ }), /** - * Only scrobble for specific users (case-insensitive) + * Only scrobble for specific users * * If `true` MS will scrobble activity from all users * */ usersAllow: z.union([z.string(), z.literal(true), z.array(z.string())]).optional().meta({ - description: "Only scrobble for specific users (case-insensitive)" + description: "Only scrobble for specific users from this list. If `true`, scrobble for all users." }), /** - * Do not scrobble for these users (case-insensitive) + * Do not scrobble for these users * */ usersBlock: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "Do not scrobble for these users (case-insensitive)" + description: "Do not scrobble for users from this list" }), /** - * Only scrobble if device or application name contains strings from this list (case-insensitive) + * Only scrobble if device or application name contains strings from this list * */ devicesAllow: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "Only scrobble if device or application name contains strings from this list (case-insensitive)" + description: "Only scrobble if device or application name contains strings from this list" }), /** - * Do not scrobble if device or application name contains strings from this list (case-insensitive) + * Do not scrobble if device or application name contains strings from this list * */ devicesBlock: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "Do not scrobble if device or application name contains strings from this list (case-insensitive)" + description: "Do not scrobble if device or application name contains strings from this list" }), /** - * Only scrobble if library name contains string from this list (case-insensitive) + * Only scrobble if library name contains string from this list * */ librariesAllow: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "Only scrobble if library name contains string from this list (case-insensitive)" + description: "Only scrobble if library name contains string from this list" }), /** - * Do not scrobble if library name contains strings from this list (case-insensitive) + * Do not scrobble if library name contains strings from this list * */ librariesBlock: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "Do not scrobble if library name contains strings from this list (case-insensitive)" + description: "Do not scrobble if library name contains strings from this list" }), /** @@ -103,8 +105,8 @@ export const jellyApiDataSchema = z.object({ * See https://github.com/jellyfin/jellyfin-sdk-typescript/blob/master/src/generated-client/models/media-type.ts#L22 for possible types * */ - allowMediaTypes: z.union([z.array(z.string()), z.string()]).optional().meta({ - description: "Allow these media types to be scrobbled." + allowMediaTypes: z.union([z.array(jellyfinMediaTypesSchema), jellyfinMediaTypesSchema]).optional().meta({ + description: "Allow media types from this list to be scrobbled" }), /** @@ -126,14 +128,14 @@ const envDataSchema = z.object({ JELLYFIN_PASSWORD: jellyApiDataSchema.shape.password, JELLYFIN_APIKEY: jellyApiDataSchema.shape.apiKey, JELLYFIN_URL: jellyApiDataSchema.shape.url, - JELLYFIN_USERS_ALLOW: jellyApiDataSchema.shape.usersAllow, - JELLYFIN_USERS_BLOCK: jellyApiDataSchema.shape.usersBlock, - JELLYFIN_DEVICES_ALLOW: jellyApiDataSchema.shape.devicesAllow, - JELLYFIN_DEVICES_BLOCK: jellyApiDataSchema.shape.devicesBlock, - JELLYFIN_LIBRARIES_ALLOW: jellyApiDataSchema.shape.librariesAllow, - JELLYFIN_LIBRARIES_BLOCK: jellyApiDataSchema.shape.librariesBlock, + JELLYFIN_USERS_ALLOW: z.union([z.string(),z.literal(true)]).optional().pipe(transformSplitMaybeStringOrBoolean).meta(envMetaNormalize(jellyApiDataSchema.shape.usersAllow.meta())), + JELLYFIN_USERS_BLOCK: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(jellyApiDataSchema.shape.usersBlock.meta())), + JELLYFIN_DEVICES_ALLOW: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(jellyApiDataSchema.shape.devicesAllow.meta())), + JELLYFIN_DEVICES_BLOCK: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(jellyApiDataSchema.shape.devicesBlock.meta())), + JELLYFIN_LIBRARIES_ALLOW: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(jellyApiDataSchema.shape.librariesAllow.meta())), + JELLYFIN_LIBRARIES_BLOCK: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(jellyApiDataSchema.shape.librariesBlock.meta())), JELLYFIN_FRONTEND_URL_OVERRIDE: jellyApiDataSchema.shape.frontendUrlOverride, - JELLYFIN_MEDIATYPES_ALLOW: jellyApiDataSchema.shape.allowMediaTypes, + JELLYFIN_MEDIATYPES_ALLOW: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(jellyApiDataSchema.shape.allowMediaTypes.meta())), }); export const envSchemas: EnvSourceSchema = { diff --git a/src/backend/common/infrastructure/config/source/mpris.ts b/src/backend/common/infrastructure/config/source/mpris.ts index a3bfb83f..1189518d 100644 --- a/src/backend/common/infrastructure/config/source/mpris.ts +++ b/src/backend/common/infrastructure/config/source/mpris.ts @@ -1,5 +1,6 @@ import * as z from "zod"; import {commonSourceConfigSchema, commonSourceDataSchema, type EnvSourceSchema} from "./index.ts"; +import { envMetaNormalize, transformSplitMaybeString } from "../../../../utils/ZodUtils.ts"; export const PLAYBACK_STATUS_PLAYING = 'Playing'; export const PLAYBACK_STATUS_PAUSED = 'Paused'; @@ -37,7 +38,7 @@ export const mprisDataSchema = z.object({ * @examples [["spotify","vlc"]] * */ blacklist: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "DO NOT scrobble from any players that START WITH these values, case-insensitive", + description: "DO NOT scrobble from any players that START WITH values from this list", examples: [["spotify", "vlc"]] }), @@ -49,7 +50,7 @@ export const mprisDataSchema = z.object({ * @examples [["spotify","vlc"]] * */ whitelist: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "ONLY from any players that START WITH these values, case-insensitive", + description: "ONLY scrobble from any players that START WITH values from this list", examples: [["spotify", "vlc"]] }), }); @@ -57,8 +58,8 @@ export const mprisDataSchema = z.object({ export type MPRISData = z.infer; const envDataSchema = z.object({ - MPRIS_BLACKLIST: mprisDataSchema.shape.blacklist, - MPRIS_WHITELIST: mprisDataSchema.shape.whitelist, + MPRIS_BLACKLIST: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(mprisDataSchema.shape.blacklist.meta())), + MPRIS_WHITELIST: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(mprisDataSchema.shape.whitelist.meta())), }); export const envSchemas: EnvSourceSchema = { diff --git a/src/backend/common/infrastructure/config/source/plex.ts b/src/backend/common/infrastructure/config/source/plex.ts index 3660fd4b..4969836a 100644 --- a/src/backend/common/infrastructure/config/source/plex.ts +++ b/src/backend/common/infrastructure/config/source/plex.ts @@ -1,6 +1,8 @@ import * as z from "zod"; import {pollingOptionsSchema} from "../common.ts"; import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, type EnvSourceSchema} from "./index.ts"; +import { envMetaNormalize, transformSplitMaybeString } from "../../../../utils/ZodUtils.ts"; +import { commaSeparatedListReplace } from "../../../../utils/StringUtils.ts"; export const plexApiDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -14,44 +16,44 @@ export const plexApiDataSchema = z.object({ }), /** - * Only scrobble for specific users (case-insensitive) + * Only scrobble for specific users * * If `true` MS will scrobble activity from all users * */ usersAllow: z.union([z.string(), z.literal(true), z.array(z.string())]).optional().meta({ - description: "Only scrobble for specific users (case-insensitive)" + description: "Only scrobble for specific users from this list" }), /** - * Do not scrobble for these users (case-insensitive) + * Do not scrobble for these users * */ usersBlock: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "Do not scrobble for these users (case-insensitive)" + description: "Do not scrobble for users from this list" }), /** - * Only scrobble if device or application name contains strings from this list (case-insensitive) + * Only scrobble if device or application name contains strings from this list * */ devicesAllow: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "Only scrobble if device or application name contains strings from this list (case-insensitive)" + description: "Only scrobble if device or application name contains strings from this list" }), /** - * Do not scrobble if device or application name contains strings from this list (case-insensitive) + * Do not scrobble if device or application name contains strings from this list * */ devicesBlock: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "Do not scrobble if device or application name contains strings from this list (case-insensitive)" + description: "Do not scrobble if device or application name contains strings from this list" }), /** - * Only scrobble if library name contains string from this list (case-insensitive) + * Only scrobble if library name contains string from this list * */ librariesAllow: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "Only scrobble if library name contains string from this list (case-insensitive)" + description: "Only scrobble if library name contains string from this list" }), /** - * Do not scrobble if library name contains strings from this list (case-insensitive) + * Do not scrobble if library name contains strings from this list * */ librariesBlock: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "Do not scrobble if library name contains strings from this list (case-insensitive)" + description: "Do not scrobble if library name contains strings from this list" }), }); @@ -60,16 +62,17 @@ export type PlexApiData = z.infer; const envDataSchema = z.object({ PLEX_URL: plexApiDataSchema.shape.url, PLEX_TOKEN: plexApiDataSchema.shape.token, - PLEX_USERS_ALLOW: plexApiDataSchema.shape.usersAllow, - PLEX_USERS_BLOCK: plexApiDataSchema.shape.usersBlock, - PLEX_DEVICES_ALLOW: plexApiDataSchema.shape.devicesAllow, - PLEX_DEVICES_BLOCK: plexApiDataSchema.shape.devicesBlock, - PLEX_LIBRARIES_ALLOW: plexApiDataSchema.shape.librariesAllow, - PLEX_LIBRARIES_BLOCK: plexApiDataSchema.shape.librariesBlock, + PLEX_USERS_ALLOW: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(plexApiDataSchema.shape.usersAllow.meta())), + PLEX_USERS_BLOCK: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(plexApiDataSchema.shape.usersBlock.meta())), + PLEX_DEVICES_ALLOW: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(plexApiDataSchema.shape.devicesBlock.meta())), + PLEX_DEVICES_BLOCK: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(plexApiDataSchema.shape.devicesAllow.meta())), + PLEX_LIBRARIES_ALLOW: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(plexApiDataSchema.shape.librariesAllow.meta())), + PLEX_LIBRARIES_BLOCK: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(plexApiDataSchema.shape.librariesBlock.meta())), }); export const envSchemas: EnvSourceSchema = { env: envDataSchema, + pipe: 'in', prefix: 'PLEX', toConfig: (partial) => ({ data: { diff --git a/src/backend/common/infrastructure/config/source/sonos.ts b/src/backend/common/infrastructure/config/source/sonos.ts index b2548b43..62809260 100644 --- a/src/backend/common/infrastructure/config/source/sonos.ts +++ b/src/backend/common/infrastructure/config/source/sonos.ts @@ -1,6 +1,7 @@ import * as z from "zod"; import {pollingOptionsSchema} from "../common.ts"; import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, type EnvSourceSchema} from "./index.ts"; +import { envMetaNormalize, transformSplitMaybeString } from "../../../../utils/ZodUtils.ts"; export const sonosDataSchema = z.object({ ...commonSourceDataSchema.shape, @@ -16,29 +17,29 @@ export const sonosDataSchema = z.object({ }), /** - * Only scrobble if device name contains strings from this list (case-insensitive) + * Only scrobble if device name contains strings from this list * */ devicesAllow: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "Only scrobble if device name contains strings from this list (case-insensitive)" + description: "Only scrobble if device name contains strings from this list" }), /** - * Do not scrobble if device name contains strings from this list (case-insensitive) + * Do not scrobble if device name contains strings from this list * */ devicesBlock: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "Do not scrobble if device name contains strings from this list (case-insensitive)" + description: "Do not scrobble if device name contains strings from this list" }), /** - * Only scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive) + * Only scrobble if the name of a group the playing device belongs to contains strings from this list * */ groupsAllow: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "Only scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive)" + description: "Only scrobble if the name of a group the playing device belongs to contains strings from this list" }), /** - * Do not scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive) + * Do not scrobble if the name of a group the playing device belongs to contains strings from this list * */ groupsBlock: z.union([z.string(), z.array(z.string())]).optional().meta({ - description: "Do not scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive)" + description: "Do not scrobble if the name of a group the playing device belongs to contains strings from this list" }), }); @@ -46,15 +47,16 @@ export type SonosData = z.infer; const envDataSchema = z.object({ SONOS_HOST: sonosDataSchema.shape.host, - SONOS_DEVICES_ALLOW: sonosDataSchema.shape.devicesAllow, - SONOS_DEVICES_BLOCK: sonosDataSchema.shape.devicesBlock, - SONOS_GROUPS_ALLOW: sonosDataSchema.shape.groupsAllow, - SONOS_GROUPS_BLOCK: sonosDataSchema.shape.groupsBlock, + SONOS_DEVICES_ALLOW: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(sonosDataSchema.shape.devicesAllow.meta())), + SONOS_DEVICES_BLOCK: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(sonosDataSchema.shape.devicesBlock.meta())), + SONOS_GROUPS_ALLOW: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(sonosDataSchema.shape.groupsAllow.meta())), + SONOS_GROUPS_BLOCK: z.string().optional().pipe(transformSplitMaybeString).meta(envMetaNormalize(sonosDataSchema.shape.groupsBlock.meta())), }); export const envSchemas: EnvSourceSchema = { env: envDataSchema, prefix: 'SONOS', + pipe: 'in', toConfig: (partial) => ({ data: { host: partial.SONOS_HOST, -- 2.51.2 From b2ce42fecf497117668a3038acb4059da48e21a7 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Sat, 1 Aug 2026 19:11:24 +0000 Subject: [PATCH 28/35] simplify jellyfin mediatype usage for now --- .../common/infrastructure/config/source/jellyfin.ts | 7 ++++--- src/backend/common/infrastructure/config/source/plex.ts | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/common/infrastructure/config/source/jellyfin.ts b/src/backend/common/infrastructure/config/source/jellyfin.ts index d86acc4c..13321d54 100644 --- a/src/backend/common/infrastructure/config/source/jellyfin.ts +++ b/src/backend/common/infrastructure/config/source/jellyfin.ts @@ -2,7 +2,8 @@ import * as z from "zod"; import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, type EnvSourceSchema} from "./index.ts"; import { envMetaNormalize, transformSplitMaybeString, transformSplitMaybeStringOrBoolean } from "../../../../utils/ZodUtils.ts"; -export const jellyfinMediaTypesSchema = z.enum(['unknown','video','audio','photo','book','musicvideo']); +//export const jellyfinMediaTypesSchema = z.enum(['unknown','video','audio','photo','book','musicvideo']); +//export type JellyfinMediaType = z.infer; export const jellyApiDataSchema = z.object({ ...commonSourceDataSchema.shape, /** @@ -105,7 +106,7 @@ export const jellyApiDataSchema = z.object({ * See https://github.com/jellyfin/jellyfin-sdk-typescript/blob/master/src/generated-client/models/media-type.ts#L22 for possible types * */ - allowMediaTypes: z.union([z.array(jellyfinMediaTypesSchema), jellyfinMediaTypesSchema]).optional().meta({ + allowMediaTypes: z.union([z.array(z.string()), z.string()]).optional().meta({ description: "Allow media types from this list to be scrobbled" }), @@ -147,7 +148,7 @@ export const envSchemas: EnvSourceSchema Date: Sun, 2 Aug 2026 20:21:55 +0000 Subject: [PATCH 29/35] fix: Skip ambigious configs during source parsing if configureAs is not present --- src/backend/sources/ScrobbleSources.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts index c331b359..153ab7c4 100644 --- a/src/backend/sources/ScrobbleSources.ts +++ b/src/backend/sources/ScrobbleSources.ts @@ -2,7 +2,7 @@ import { childLogger, type Logger } from '@foxxmd/logging'; import type EventEmitter from "events"; import type {InternalConfig, InternalConfigOptional} from "../common/infrastructure/Atomic.ts"; -import { isSourceType } from "../../core/Atomic.ts"; +import { clientTypes, isSourceType } from "../../core/Atomic.ts"; import { sourceTypes } from "../../core/Atomic.ts"; import type {SourceType} from "../../core/Atomic.ts"; import {aioSourceRelaxedConfigSchema, type AIOSourceRelaxedConfig, type SourceDefaults} from "../common/infrastructure/config/aioConfig.ts"; @@ -238,8 +238,10 @@ export default class ScrobbleSources { } break; case 'file': case 'aio': { - if ('configureAs' in entry.config && entry.config.configureAs === 'client') { - this.logger.debug(`Skipping ${configType} Config ${entry.source} ${entry.pos} because it is configured as a Source`); + if (('configureAs' in entry.config && entry.config.configureAs === 'client') + // @ts-expect-error could be a client type + || (clientTypes.includes(entry.type) && entry.config.configureAs !== 'source')) { + this.logger.debug(`Skipping ${configType} Config ${entry.source} ${entry.pos} because it is configured as a Client`); continue; } const parsed = entry.source === 'file' ? (await validateSourceJson(entry.type, entry.config)) : (await validateSourceAIOJson(entry.type, entry.config)); -- 2.51.2 From 6772e9934d0f68fbbc9e19b0b08d79f2183b1709 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Mon, 3 Aug 2026 13:38:49 +0000 Subject: [PATCH 30/35] refactor!: Enforce mandatory id for file/aio --- .../common/infrastructure/config/common.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/backend/common/infrastructure/config/common.ts b/src/backend/common/infrastructure/config/common.ts index 1746d1b5..0acc997b 100644 --- a/src/backend/common/infrastructure/config/common.ts +++ b/src/backend/common/infrastructure/config/common.ts @@ -3,6 +3,8 @@ import * as z from "zod"; import type { PlayTransformHooks, ExternalMetadataTerm } from "../../../../core/Transform.ts"; import type { CommonClientOptions } from "./client/index.ts"; import type { MarkRequired } from "ts-essentials"; +import type { ClientType, SourceType } from "../../../../core/Atomic.ts"; +import { capitalize } from "../../../../core/StringUtils.ts"; export const commonConfigPrimitivesSchema = z.object({ name: z.string().optional(), @@ -24,7 +26,7 @@ export const commonConfigSchema = z.object({ * * If no id is given the name of this component will be used. */ - id: z.string().optional().meta({ + id: z.string().meta({ description: "A UNIQUE identifier for this Source/Client", examples: ["fooGlobalA"] }), @@ -145,6 +147,18 @@ export const monitorOptionsSchema = z.object({ }) export type MonitorOptions = z.infer; +export type UnparsedConfig = {config: object, type: T, source?: 'file' | 'aio' | 'env', pos: string}; + +export const generateConfigLocation = (configType: string, config: UnparsedConfig): string => { + if(config.source === 'file') { + return `${capitalize(configType)} #${config.pos} in ${config.type}.json`; + } + if(config.source === 'aio') { + return `${capitalize(configType)} ${config.type} #${config.pos} in config.json`; + } + return `${capitalize(configType)} ${config.type} from ENV`; +} + export const transformPresetEnv = (prefix: string, existing: T = undefined): undefined | T => { const env = process.env[`${prefix}_TRANSFORMS`]; -- 2.51.2 From 792ccc3c09295298b0811fb5ed735f92ad9025e5 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Mon, 3 Aug 2026 13:39:14 +0000 Subject: [PATCH 31/35] feat: Improve location wording for config errors --- src/backend/scrobblers/ScrobbleClients.ts | 19 +++++++++---------- src/backend/sources/ScrobbleSources.ts | 19 +++++++++---------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/backend/scrobblers/ScrobbleClients.ts b/src/backend/scrobblers/ScrobbleClients.ts index 4c04df04..be39182f 100644 --- a/src/backend/scrobblers/ScrobbleClients.ts +++ b/src/backend/scrobblers/ScrobbleClients.ts @@ -15,11 +15,11 @@ import clone from 'clone'; import { stripIndents } from 'common-tags'; import { normalizeStr, type StringNormalizationOptions } from '../utils/StringUtils.ts'; import { prettifyError, ZodError } from 'zod'; -import { commonComponentEnvConfigToConfigPrimitives, generateCommonComponentEnvConfigSchema, transformPresetEnv, type CommonConfigPrimitives } from '../common/infrastructure/config/common.ts'; +import { commonComponentEnvConfigToConfigPrimitives, generateCommonComponentEnvConfigSchema, generateConfigLocation, transformPresetEnv, type CommonConfigPrimitives, type UnparsedConfig } from '../common/infrastructure/config/common.ts'; import type { CommonClientConfig } from '../common/infrastructure/config/client/index.ts'; import { getClientEnvSchema, validateClientAIOJson, validateClientJson, type ClientTypeConfigMap } from '../common/infrastructure/config/client/clientsMap.ts'; -type UnparsedConfig = {config: object, type: ClientType, source?: 'file' | 'aio' | 'env', pos: string}; +type UnparsedClientConfig = UnparsedConfig; type CommonParsedConfig = (CommonClientConfig & {source: string}); @@ -97,7 +97,7 @@ export default class ScrobbleClients { } buildClientsFromConfig = async () => { - const unparsedConfigs: UnparsedConfig[] = []; + const unparsedConfigs: UnparsedClientConfig[] = []; let configFile; try { @@ -175,7 +175,7 @@ export default class ScrobbleClients { } else if (rawClientConfigs === null) { this.logger.warn(`${clientType}.json contained no data`); } else if (typeof rawClientConfigs === 'object') { - clientUnparsedConfigs.push({ config: rawClientConfigs, type: clientType, source: 'file', pos: `object` }) + clientUnparsedConfigs.push({ config: rawClientConfigs, type: clientType, source: 'file', pos: `1` }) } else { this.logger.error(`All top level data from ${clientType}.json must be an object or an array of objects, will not parse configs from file`); } @@ -195,7 +195,6 @@ export default class ScrobbleClients { for (const entry of clientUnparsedConfigs) { let parsedConfig: CommonParsedConfig; try { - const sourceStr = `${entry.source} ${entry.pos}`; switch (entry.source) { case 'env': { const envSchema = await getClientEnvSchema(clientType); @@ -209,7 +208,7 @@ export default class ScrobbleClients { name: `${clientType} - ${entry.source}${entry.pos !== '' ? ` - ${entry.pos}` : ''} `, ...primitives, data, - source: sourceStr, + source: generateConfigLocation('client', entry), options: { ...options, ...(transformOptions ?? {}) @@ -219,18 +218,18 @@ export default class ScrobbleClients { case 'file': case 'aio': { if ('configureAs' in entry.config && entry.config.configureAs === 'source') { - this.logger.debug(`Skipping ${clientType} Config ${entry.source} ${entry.pos} because it is configured as a Source`); + this.logger.debug(`Skipping ${generateConfigLocation('client', entry)} because it is configured as a Source`); continue; } const parsed = entry.source === 'file' ? (await validateClientJson(entry.type, entry.config)) : (await validateClientAIOJson(entry.type, entry.config)); parsedConfig = { ...parsed, - source: sourceStr + source: generateConfigLocation('client', entry) } } break; } } catch (e) { - const msg = `Failed to validate ${clientType} Config ${entry.source} ${entry.pos}`; + const msg = `Failed to validate ${generateConfigLocation('client', entry)}`; if (e instanceof ZodError) { this.logger.error(`${msg}:\n${prettifyError(e)}`); } else { @@ -267,7 +266,7 @@ export default class ScrobbleClients { newClient.logger.info(`Client added from ${s.source}`); this.clients.push(newClient); } catch (e) { - this.logger.error(new Error(`Client from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + this.logger.error(new Error(`${s.source} was not added due to unrecoverable errors`, { cause: e })); } } } diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts index 153ab7c4..7181053e 100644 --- a/src/backend/sources/ScrobbleSources.ts +++ b/src/backend/sources/ScrobbleSources.ts @@ -14,11 +14,11 @@ import { nonEmptyStringOrDefault } from '../../core/StringUtils.ts'; import type {CommonSourceConfig, CommonSourceOptions} from '../common/infrastructure/config/source/index.ts'; import type {ExternalMetadataTerm, PlayTransformHooks} from '../../core/Transform.ts'; import { prettifyError, ZodError } from 'zod'; -import { commonComponentEnvConfigToConfigPrimitives, generateCommonComponentEnvConfigSchema, type CommonConfigPrimitives } from '../common/infrastructure/config/common.ts'; +import { commonComponentEnvConfigToConfigPrimitives, generateCommonComponentEnvConfigSchema, generateConfigLocation, type CommonConfigPrimitives, type UnparsedConfig } from '../common/infrastructure/config/common.ts'; import { getSourceEnvSchema, validateSourceAIOJson, validateSourceJson } from '../common/infrastructure/config/source/sourcesMap.ts'; import type { SourceTypeConfigMap } from "../common/infrastructure/config/source/sourcesMap.ts"; -type UnparsedConfig = {config: object, type: SourceType, source?: 'file' | 'aio' | 'env', pos: string}; +type UnparsedSourceConfig = UnparsedConfig; type CommonParsedConfig = (CommonSourceConfig & {source: string}); @@ -115,7 +115,7 @@ export default class ScrobbleSources { } buildSourcesFromConfig = async () => { - const unparsedConfigs: UnparsedConfig[] = []; + const unparsedConfigs: UnparsedSourceConfig[] = []; let configFile; try { @@ -195,7 +195,7 @@ export default class ScrobbleSources { } else if (rawConfigs === null) { this.logger.warn(`${configType}.json contained no data`); } else if (typeof rawConfigs === 'object') { - sourceUnparsedConfigs.push({ config: rawConfigs, type: configType, source: 'file', pos: `object` }) + sourceUnparsedConfigs.push({ config: rawConfigs, type: configType, source: 'file', pos: `1` }) } else { this.logger.error(`All top level data from ${configType}.json must be an object or an array of objects, will not parse configs from file`); } @@ -215,7 +215,6 @@ export default class ScrobbleSources { for (const entry of sourceUnparsedConfigs) { let parsedConfig: CommonParsedConfig; try { - const sourceStr = `${entry.source} ${entry.pos}`; switch (entry.source) { case 'env': { const envSchema = await getSourceEnvSchema(configType); @@ -229,7 +228,7 @@ export default class ScrobbleSources { name: `${configType} - ${entry.source}${entry.pos !== '' ? ` - ${entry.pos}` : ''} `, ...primitives, data, - source: sourceStr, + source: generateConfigLocation('source', entry), options: { ...options, ...(transformOptions ?? {}) @@ -241,18 +240,18 @@ export default class ScrobbleSources { if (('configureAs' in entry.config && entry.config.configureAs === 'client') // @ts-expect-error could be a client type || (clientTypes.includes(entry.type) && entry.config.configureAs !== 'source')) { - this.logger.debug(`Skipping ${configType} Config ${entry.source} ${entry.pos} because it is configured as a Client`); + this.logger.debug(`Skipping ${generateConfigLocation('source', entry)} because it is configured as a Client`); continue; } const parsed = entry.source === 'file' ? (await validateSourceJson(entry.type, entry.config)) : (await validateSourceAIOJson(entry.type, entry.config)); parsedConfig = { ...parsed, - source: sourceStr + source: generateConfigLocation('source', entry) } } break; } } catch (e) { - const msg = `Failed to validate ${configType} Config ${entry.source} ${entry.pos}`; + const msg = `Failed to validate ${generateConfigLocation('source', entry)}`; if (e instanceof ZodError) { this.logger.error(`${msg}:\n${prettifyError(e)}`); } else { @@ -288,7 +287,7 @@ export default class ScrobbleSources { newComponent.logger.info(`Source added from ${s.source}`); this.sources.push(newComponent); } catch (e) { - this.logger.error(new Error(`Source from ${s.source} was not added due to unrecoverable errors`, { cause: e })); + this.logger.error(new Error(`${s.source} was not added due to unrecoverable errors`, { cause: e })); } } } -- 2.51.2 From d296b825d6ff75475f255c599ddf825c5861bc07 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Mon, 3 Aug 2026 14:31:24 +0000 Subject: [PATCH 32/35] Id logging and schema cleanup * Guard against undefined name when logging component disabled * Add id/name hint to config location * inherit name/id field from common schema --- .../infrastructure/config/client/index.ts | 9 ------- .../common/infrastructure/config/common.ts | 26 ++++++++++++++++--- .../infrastructure/config/source/index.ts | 13 +++------- src/backend/scrobblers/ScrobbleClients.ts | 2 +- src/backend/sources/ScrobbleSources.ts | 2 +- 5 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/backend/common/infrastructure/config/client/index.ts b/src/backend/common/infrastructure/config/client/index.ts index 967d7381..0d6d704b 100644 --- a/src/backend/common/infrastructure/config/client/index.ts +++ b/src/backend/common/infrastructure/config/client/index.ts @@ -166,15 +166,6 @@ export type CommonClientOptions = z.infer; export const commonClientConfigSchema = z.object({ ...commonConfigSchema.shape, - /** - * Vanity name for this client - * - * @examples ["MyConfig"] - * */ - name: z.string().meta({ - description: "Vanity name for this client.", - examples: ["Foxx's Cool Client"] - }), /** * Specific data required to configure this client * */ diff --git a/src/backend/common/infrastructure/config/common.ts b/src/backend/common/infrastructure/config/common.ts index 0acc997b..4f314cdf 100644 --- a/src/backend/common/infrastructure/config/common.ts +++ b/src/backend/common/infrastructure/config/common.ts @@ -19,7 +19,15 @@ export const commonDataSchema = z.record(z.string(), z.any()); // keyOmit<{ [key export type CommonData = z.infer; export const commonConfigSchema = z.object({ - name: z.string().optional(), + /** + * Vanity name for this Source/Client + * + * @examples ["My Cool Component"] + * */ + name: z.string().optional().meta({ + description: "Vanity name for this Source/Client", + examples: ["Foxx's Cool Client"] + }), /** A UNIQUE identifier for this Source/Client * * It should be unique for the given Source/Client type. No other Source/Client of the same type should have this ID. This ID will be used to register this Source/Client in the database so that it can be identified even if you change the name of the component. @@ -150,13 +158,23 @@ export type MonitorOptions = z.infer; export type UnparsedConfig = {config: object, type: T, source?: 'file' | 'aio' | 'env', pos: string}; export const generateConfigLocation = (configType: string, config: UnparsedConfig): string => { + const identifiers: string[] = []; + if(config.config !== undefined) { + if('id' in config.config) { + identifiers.push(`ID ${config.config.id}`); + } + if('name' in config.config) { + identifiers.push(`Name ${config.config.name}`); + } + } + if(config.source === 'file') { - return `${capitalize(configType)} #${config.pos} in ${config.type}.json`; + return `${capitalize(configType)} #${config.pos}${identifiers.length > 0 ? ` (${identifiers.join(',')})` : ''} in ${config.type}.json`; } if(config.source === 'aio') { - return `${capitalize(configType)} ${config.type} #${config.pos} in config.json`; + return `${capitalize(configType)} ${config.type} #${config.pos}${identifiers.length > 0 ? ` (${identifiers.join(',')})` : ''} in config.json`; } - return `${capitalize(configType)} ${config.type} from ENV`; + return `${capitalize(configType)} ${config.type}${identifiers.length > 0 ? ` (${identifiers.join(',')})` : ''} from ENV`; } export const transformPresetEnv = (prefix: string, existing: T = undefined): undefined | T => { diff --git a/src/backend/common/infrastructure/config/source/index.ts b/src/backend/common/infrastructure/config/source/index.ts index 5698d38b..641daf60 100644 --- a/src/backend/common/infrastructure/config/source/index.ts +++ b/src/backend/common/infrastructure/config/source/index.ts @@ -186,20 +186,13 @@ export type CommonSourceData = {}; export const commonSourceConfigSchema = z.object({ ...commonConfigSchema.shape, /** - * Vanity anme for this source. - * */ - name: z.string().optional().meta({ - description: "Vanity Name for this source.", - examples: ["Foxx's Cool Source"] - }), - /** - * Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients. + * Restrict scrobbling tracks played from this source to Clients with IDs from this list. If list is empty is not present Source scrobbles to all configured Clients. * * @examples [["MyMalojaConfigName","MyLastFMConfigName"]] * */ clients: z.array(z.string()).optional().meta({ - description: "Restrict scrobbling tracks played from this source to Clients with names from this list.", - examples: [["MyMalojaConfigName","MyLastFMConfigName"]] + description: "Restrict scrobbling tracks played from this Source to Clients with IDs from this list.", + examples: [["MyMalojaConfigId","MyLastFMConfigId"]] }), data: commonSourceDataSchema.optional(), options: commonSourceOptionsSchema.optional(), diff --git a/src/backend/scrobblers/ScrobbleClients.ts b/src/backend/scrobblers/ScrobbleClients.ts index be39182f..eca9d64a 100644 --- a/src/backend/scrobblers/ScrobbleClients.ts +++ b/src/backend/scrobblers/ScrobbleClients.ts @@ -239,7 +239,7 @@ export default class ScrobbleClients { } if (parsedConfig.enable === false) { - this.logger.debug(`Not using Config ${parsedConfig.id} (${parsedConfig.name}) because it was marked as not enabled.`); + this.logger.debug(`Not using Config ${parsedConfig.id}${parsedConfig.name !== undefined ? ` (${parsedConfig.name}) ` :''} because it was marked as not enabled.`); } else { strongConfigs.push(parsedConfig); } diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts index 7181053e..9e4b93c2 100644 --- a/src/backend/sources/ScrobbleSources.ts +++ b/src/backend/sources/ScrobbleSources.ts @@ -261,7 +261,7 @@ export default class ScrobbleSources { } if (parsedConfig.enable === false) { - this.logger.debug(`Not using Config ${parsedConfig.id} (${parsedConfig.name}) because it was marked as not enabled.`); + this.logger.debug(`Not using Config ${parsedConfig.id}${parsedConfig.name !== undefined ? ` (${parsedConfig.name}) ` :''} because it was marked as not enabled.`); } else { strongConfigs.push(parsedConfig); } -- 2.51.2 From dd22418c410a0a4cc6e3728828023a5b51cc24d3 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Mon, 3 Aug 2026 14:56:33 +0000 Subject: [PATCH 33/35] fix(tests): Fix missing required id --- .../tests/component/transformers.test.ts | 32 ++++++++++++++++--- src/backend/tests/jellyfin/jellyfin.test.ts | 1 + src/backend/tests/musiccast/musiccast.test.ts | 1 + src/backend/tests/plex/plex.test.ts | 1 + src/backend/tests/scrobbler/TestScrobbler.ts | 9 +++--- .../tests/scrobbler/scrobblers.test.ts | 8 ++--- src/backend/tests/sonos/sonos.test.ts | 1 + src/backend/tests/source/source.test.ts | 21 ++++++------ src/backend/tests/subsonic/subsonic.test.ts | 1 + src/backend/tests/tealfm/tealfm.test.ts | 2 +- src/backend/tests/ytm/ytm.test.ts | 5 +-- 11 files changed, 55 insertions(+), 27 deletions(-) diff --git a/src/backend/tests/component/transformers.test.ts b/src/backend/tests/component/transformers.test.ts index 52ecfc03..0a99dbb1 100644 --- a/src/backend/tests/component/transformers.test.ts +++ b/src/backend/tests/component/transformers.test.ts @@ -23,6 +23,7 @@ import { COMPONENT_STATE, type ComponentState } from "../../../core/Api.ts"; chai.use(asPromised); +const componentId = () => `test-${Date.now()}`; class TestComponent extends AbstractComponent { public getRunningState(): ComponentState { return COMPONENT_STATE.RUNNING; @@ -33,12 +34,12 @@ class TestComponent extends AbstractComponent { protected getIdentifier(): string { return 'test'; } - constructor(config?: AbstractComponentConfig) { - super(config ?? {}); + constructor(config?: Omit) { + super({id: `test-${Date.now()}`, ...(config ?? {})}); } } -const createTestComponent = (config?: AbstractComponentConfig): TestComponent => { +const createTestComponent = (config?: Omit): TestComponent => { const component = new TestComponent(config); component.logger = childLogger(loggerTest, 'App'); return component; @@ -52,7 +53,9 @@ const memorycache = () => new Cacheable({primary: initMemoryCache()}) describe('Play Transforms', function () { beforeEach(function () { - component.config = {}; + component.config = { + id: componentId() + }; component.transformRules = {}; }); describe('Transform Config Parsing', function () { @@ -65,6 +68,7 @@ describe('Play Transforms', function () { it('Converts single object hook into hook array', function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: { @@ -110,6 +114,7 @@ describe('Play Transforms', function () { it(`Allows user 'type'`, function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: { @@ -130,6 +135,7 @@ describe('Play Transforms', function () { it('Accepts hook array', function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: [ @@ -156,6 +162,7 @@ describe('Play Transforms', function () { it('Converts transform config into real S&P data', function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: { @@ -176,6 +183,7 @@ describe('Play Transforms', function () { it('Converts transform config into real S&P data with default being empty string', function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: { @@ -199,6 +207,7 @@ describe('Play Transforms', function () { it('Respects transform config when it is already S&P data', function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: { @@ -235,6 +244,7 @@ describe('Play Transforms', function () { it(`Allows non-user Stage Type ${t}`, function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: { @@ -271,6 +281,7 @@ describe('Play Transforms', function () { describe('User Play Transforming', function () { it('Transforms when hook is present', async function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: { @@ -289,6 +300,7 @@ describe('Play Transforms', function () { it('Transforms consecutively when hook is present with multiple values', async function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: { @@ -307,6 +319,7 @@ describe('Play Transforms', function () { it('Transforms using parsed regex', async function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: { @@ -331,6 +344,7 @@ describe('Play Transforms', function () { it('Transforms using parsed regex to get primary artist from delimited artist string', async function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: { @@ -355,6 +369,7 @@ describe('Play Transforms', function () { it('Removes title when transform replaces with empty string', async function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: { @@ -373,6 +388,7 @@ describe('Play Transforms', function () { it('Removes album when transform replaces with empty string', async function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: { @@ -391,6 +407,7 @@ describe('Play Transforms', function () { it('Removes an artist when transform replaces with empty string', async function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: { @@ -469,6 +486,7 @@ describe('Play Transforms', function () { describe('On Hook', function () { it('Does not run hook if when conditions do not match', async function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: { @@ -493,6 +511,7 @@ describe('Play Transforms', function () { it('Does run hook if when conditions matches', async function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: { @@ -519,6 +538,7 @@ describe('Play Transforms', function () { describe('On Search-And-Replace', function () { it('Does not run hook if when conditions do not match', async function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: { @@ -548,6 +568,7 @@ describe('Play Transforms', function () { it('Does run hook if when conditions matches', async function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: { @@ -582,6 +603,7 @@ describe('Play Transforms', function () { it('Accumulates transforms within a single stage', async function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: [ @@ -616,6 +638,7 @@ describe('Play Transforms', function () { it('Accumulates transforms across multiple stages', async function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: [ @@ -651,6 +674,7 @@ describe('Play Transforms', function () { it('Re-uses steps without modifying other Play properties', async function () { component.config = { + id: componentId(), options: { playTransform: { preCompare: [ diff --git a/src/backend/tests/jellyfin/jellyfin.test.ts b/src/backend/tests/jellyfin/jellyfin.test.ts index 31010cdb..0c9362d8 100644 --- a/src/backend/tests/jellyfin/jellyfin.test.ts +++ b/src/backend/tests/jellyfin/jellyfin.test.ts @@ -30,6 +30,7 @@ interface TestFixture { const createJfApi = (data: JellyApiData): JellyfinApiSource => { const jf = new JellyfinApiSource('Test', { + id: `test-${Date.now()}`, data, options: {} }, { localUrl: new URL('http://test'), configDir: 'test', logger: loggerTest, version: 'test' }, new EventEmitter()); diff --git a/src/backend/tests/musiccast/musiccast.test.ts b/src/backend/tests/musiccast/musiccast.test.ts index 7b28a18b..0fc41a13 100644 --- a/src/backend/tests/musiccast/musiccast.test.ts +++ b/src/backend/tests/musiccast/musiccast.test.ts @@ -13,6 +13,7 @@ const TEST_IP = '192.168.10.101'; const createSource = (data: MusicCastData = { url: TEST_IP }): MusicCastSource => { const source = new MusicCastSource('Test', { + id: `test-${Date.now()}`, data, options: {} }, { localUrl: new URL('http://test'), configDir: 'test', logger: loggerTest, version: 'test' }, new EventEmitter()); diff --git a/src/backend/tests/plex/plex.test.ts b/src/backend/tests/plex/plex.test.ts index 9b722fd4..44cefccb 100644 --- a/src/backend/tests/plex/plex.test.ts +++ b/src/backend/tests/plex/plex.test.ts @@ -15,6 +15,7 @@ const validSession = validSessionResponse.object.mediaContainer.metadata[0]; const createSource = async (data: PlexApiData, authedUser: string | false = 'MyUser'): Promise => { const source = new PlexApiSource('Test', { + id: `test-${Date.now()}`, data, options: {} }, { localUrl: new URL('http://test'), configDir: 'test', logger: loggerTest, version: 'test' }, new EventEmitter()); diff --git a/src/backend/tests/scrobbler/TestScrobbler.ts b/src/backend/tests/scrobbler/TestScrobbler.ts index 5c1d30b7..f0c96795 100644 --- a/src/backend/tests/scrobbler/TestScrobbler.ts +++ b/src/backend/tests/scrobbler/TestScrobbler.ts @@ -11,6 +11,7 @@ import type {RepositoryCreatePlayOpts} from "../../common/database/drizzle/repos import type { DrizzleQueueRepository } from "../../common/database/drizzle/repositories/QueueRepository.ts"; import type {PlaySelect} from "../../common/database/drizzle/drizzleTypes.ts"; import dayjs from "dayjs"; +import type { MarkOptional, MarkRequired } from "ts-essentials"; export class TestScrobbler extends AbstractScrobbleClient { @@ -20,9 +21,9 @@ export class TestScrobbler extends AbstractScrobbleClient { public playRepoTest: DrizzlePlayRepository; public queueRepoTest: DrizzleQueueRepository; - constructor(config: CommonClientConfig = {name: 'test'}) { + constructor(config: MarkOptional = {name: 'test'}) { const logger = loggerNoop; - super('test', 'Test', {name: 'test', ...config}, new EventEmitter(), logger); + super('test', 'Test', {name: 'test', id: `test-${Date.now()}`, ...config}, new EventEmitter(), logger); this.supportsNowPlaying = false; this.getScrobblesForTimeRange = async (_) => { return this.testRecentScrobbles; @@ -73,10 +74,10 @@ export class TestAuthScrobbler extends TestScrobbler { } } -export type TestNowPlayingConfig = CommonClientConfig & {options?: CommonClientOptions & NowPlayingOptions}; +export type TestNowPlayingConfig = MarkOptional & {options?: CommonClientOptions & NowPlayingOptions}; export class NowPlayingScrobbler extends TestScrobbler { - declare config: TestNowPlayingConfig + declare config: MarkRequired constructor(config?: TestNowPlayingConfig) { super(config); diff --git a/src/backend/tests/scrobbler/scrobblers.test.ts b/src/backend/tests/scrobbler/scrobblers.test.ts index 7560bda9..382ab00c 100644 --- a/src/backend/tests/scrobbler/scrobblers.test.ts +++ b/src/backend/tests/scrobbler/scrobblers.test.ts @@ -91,9 +91,7 @@ describe('Networking', function () { const authScrobbler = new TestAuthScrobbler(); try { await authScrobbler.testAuth(); - } catch (e) { - - } + } catch (e) { /* empty */ } assert.isTrue(authScrobbler.authGated()); assert.isFalse(authScrobbler.authFailure); } @@ -111,9 +109,7 @@ describe('Networking', function () { const authScrobbler = new TestAuthScrobbler(); try { await authScrobbler.testAuth(); - } catch (e) { - - } + } catch (e) { /* empty */ } assert.isTrue(authScrobbler.authGated()); assert.isTrue(authScrobbler.authFailure); } diff --git a/src/backend/tests/sonos/sonos.test.ts b/src/backend/tests/sonos/sonos.test.ts index a752f41c..02d33030 100644 --- a/src/backend/tests/sonos/sonos.test.ts +++ b/src/backend/tests/sonos/sonos.test.ts @@ -12,6 +12,7 @@ dotenv.config({ path: envPath }); const createSource = async (data: SonosData): Promise => { const source = new SonosSource('Test', { + id: `test-${Date.now()}`, data, options: {} }, { localUrl: new URL('http://test'), configDir: 'test', logger: loggerDebug, version: 'test' }, new EventEmitter()); diff --git a/src/backend/tests/source/source.test.ts b/src/backend/tests/source/source.test.ts index 800fe161..56488e6e 100644 --- a/src/backend/tests/source/source.test.ts +++ b/src/backend/tests/source/source.test.ts @@ -20,26 +20,27 @@ import { sleep } from "../../utils.ts"; import DeezerInternalSource from "../../sources/DeezerInternalSource.ts"; import type {DeezerInternalSourceOptions} from "../../common/infrastructure/config/source/deezer.ts"; import { artistCreditsToNames } from "../../../core/StringUtils.ts"; +import type { MarkOptional } from "ts-essentials"; chai.use(asPromised); const emitter = new EventEmitter(); const generateSource = async () => { - const source = new TestSource('spotify', 'test-basic', {}, {localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test'}, emitter); + const source = new TestSource('spotify', 'test-basic', {id: `test-${Date.now()}`}, {localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test'}, emitter); await source.initialize(); return source; } -const generateMemorySource = async (config: SourceConfig = {}) => { - const s = new TestMemorySource('spotify', 'test-memory', config, {localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test'}, emitter); +const generateMemorySource = async (config: MarkOptional = {}) => { + const s = new TestMemorySource('spotify', 'test-memory', {id: `test-${Date.now()}`, ...config}, {localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test'}, emitter); await s.initialize(); // s.buildTransformRules(); s.scheduler.stop(); return s; } -const generateMemoryPositionalSource = async (config: SourceConfig = {}) => { - const s = new TestMemoryPositionalSource('spotify', 'test-positional', config, {localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test'}, emitter); +const generateMemoryPositionalSource = async (config: MarkOptional = {}) => { + const s = new TestMemoryPositionalSource('spotify', 'test-positional', {id: `test-${Date.now()}`, ...config}, {localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test'}, emitter); await s.initialize(); //s.buildTransformRules(); s.scheduler.stop(); @@ -201,7 +202,7 @@ describe('Player Cleanup', function () { setRtTick(1); }); - const cleanedUpDuration = async (generateSource: (config: SourceConfig) => Promise) => { + const cleanedUpDuration = async (generateSource: (config: MarkOptional) => Promise) => { await using source = await generateSource({data: {staleAfter: 21, orphanedAfter: 40}, options: {}}); const initialDate = dayjs(); const initialState = generatePlayerStateData({position: 0, playData: {duration: 50}, stateUpdatedAt: initialDate, status: REPORTED_PLAYER_STATUSES.playing}); @@ -244,7 +245,7 @@ describe('Player Cleanup', function () { await cleanedUpDuration(generateMemoryPositionalSource); }); - const noScrobbleRediscoveryOnActive = async (generateSource: (config: SourceConfig) => Promise) => { + const noScrobbleRediscoveryOnActive = async (generateSource: (config: MarkOptional) => Promise) => { await using source = await generateSource({data: {staleAfter: 21, orphanedAfter: 40}, options: {}}); const initialDate = dayjs(); @@ -312,7 +313,7 @@ describe('Player Cleanup', function () { await noScrobbleRediscoveryOnActive(generateMemoryPositionalSource); }); - const noScrobbleStale = async (generateSource: (config: SourceConfig) => Promise) => { + const noScrobbleStale = async (generateSource: (config: MarkOptional) => Promise) => { await using source = await generateSource({data: {staleAfter: 21, orphanedAfter: 40}, options: {}}); const initialDate = dayjs(); @@ -357,7 +358,7 @@ describe('Player Cleanup', function () { await noScrobbleStale(generateMemoryPositionalSource); }); - const scrobbleRediscoveryOnActive = async (generateSource: (config: SourceConfig) => Promise) => { + const scrobbleRediscoveryOnActive = async (generateSource: (config: MarkOptional) => Promise) => { await using source = await generateSource({data: {staleAfter: 21, orphanedAfter: 40}, options: {}}); const initialDate = dayjs(); @@ -440,7 +441,7 @@ class DeezerTestSource extends DeezerInternalSource { } const generateDeezerSource = async (options: DeezerInternalSourceOptions = {}) => { - const source = new DeezerTestSource('test', {data: {arl: 'test'}, options}, {localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test'}, emitter); + const source = new DeezerTestSource('test', {id: `test-${Date.now()}`,data: {arl: 'test'}, options}, {localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test'}, emitter); await source.initialize(); return source; } diff --git a/src/backend/tests/subsonic/subsonic.test.ts b/src/backend/tests/subsonic/subsonic.test.ts index 94313dbb..8d8b578e 100644 --- a/src/backend/tests/subsonic/subsonic.test.ts +++ b/src/backend/tests/subsonic/subsonic.test.ts @@ -48,6 +48,7 @@ const formatPlay = (...args: Parameters) => const createSource = (detectStaleNowPlayingFromMinutesAgo?: boolean) => { const config: SubSonicSourceConfig = { + id: `test-${Date.now()}`, data: { url: 'https://example.com', user: 'user', diff --git a/src/backend/tests/tealfm/tealfm.test.ts b/src/backend/tests/tealfm/tealfm.test.ts index b75a331a..9827de03 100644 --- a/src/backend/tests/tealfm/tealfm.test.ts +++ b/src/backend/tests/tealfm/tealfm.test.ts @@ -108,7 +108,7 @@ describe('#tealfmCar', function() { this.timeout(100000); const tfm = new TealScrobbler('test', - {name: 'test', data: {identifier: 'test', appPassword: 'test'}}, + {id: `test-${Date.now()}`, name: 'test', data: {identifier: 'test', appPassword: 'test'}}, {configDir: 'test', localUrl: new URL('https://example.com'), version: 'test'}, new EventEmitter(), loggerDebug diff --git a/src/backend/tests/ytm/ytm.test.ts b/src/backend/tests/ytm/ytm.test.ts index 65bbd226..4fa61e3f 100644 --- a/src/backend/tests/ytm/ytm.test.ts +++ b/src/backend/tests/ytm/ytm.test.ts @@ -11,11 +11,12 @@ import type {YTMusicSourceConfig} from '../../common/infrastructure/config/sourc import { sleep } from '../../utils.ts'; import dayjs from 'dayjs'; import type {ApiResponse} from 'youtubei.js'; +import type { MarkOptional } from 'ts-essentials'; chai.use(asPromised); const createYtSource = async (opts?: { - config?: YTMusicSourceConfig + config?: MarkOptional, emitter?: EventEmitter }) => { const { @@ -26,7 +27,7 @@ const createYtSource = async (opts?: { }, emitter = new EventEmitter } = opts || {}; - const source = new YTMusicSource('test', config, { localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test' }, emitter); + const source = new YTMusicSource('test', {id: `test-${Date.now()}`, ...config}, { localUrl: new URL('https://example.com'), configDir: 'fake', logger: loggerTest, version: 'test' }, emitter); await source.buildDatabase(); source.buildTransformRules(); return source; -- 2.51.2 From 9f71fbef971ad8dd08ccf3d0ebf820341cefc03a Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Mon, 3 Aug 2026 15:21:11 +0000 Subject: [PATCH 34/35] fix: prevent creating source/client with same id --- src/backend/scrobblers/ScrobbleClients.ts | 17 +++++++++++++---- src/backend/sources/ScrobbleSources.ts | 18 ++++++++++++++---- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/backend/scrobblers/ScrobbleClients.ts b/src/backend/scrobblers/ScrobbleClients.ts index eca9d64a..ae4d1580 100644 --- a/src/backend/scrobblers/ScrobbleClients.ts +++ b/src/backend/scrobblers/ScrobbleClients.ts @@ -191,7 +191,7 @@ export default class ScrobbleClients { }) } - const strongConfigs: CommonParsedConfig[] = []; + let strongConfigs: CommonParsedConfig[] = []; for (const entry of clientUnparsedConfigs) { let parsedConfig: CommonParsedConfig; try { @@ -238,11 +238,20 @@ export default class ScrobbleClients { continue; } + const existingById = strongConfigs.find(x => x.id === parsedConfig.id); + if(undefined !== existingById) { + this.logger.error(stripIndents`There are two ${clientType} Sources that have the same ID: + ${existingById.source} + ${parsedConfig.source} + BOTH of these Clients will be disabled to prevent tainting database history. Correct this issue by using a different ID for at least one of them.`); + strongConfigs = strongConfigs.filter(x => x.id !== parsedConfig.id); + continue; + } if (parsedConfig.enable === false) { - this.logger.debug(`Not using Config ${parsedConfig.id}${parsedConfig.name !== undefined ? ` (${parsedConfig.name}) ` :''} because it was marked as not enabled.`); - } else { - strongConfigs.push(parsedConfig); + this.logger.debug(`Not using Config ${parsedConfig.source} because it was marked as not enabled.`); + continue; } + strongConfigs.push(parsedConfig); } if(strongConfigs.length > 0) { diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts index 9e4b93c2..18c575a2 100644 --- a/src/backend/sources/ScrobbleSources.ts +++ b/src/backend/sources/ScrobbleSources.ts @@ -17,6 +17,7 @@ import { prettifyError, ZodError } from 'zod'; import { commonComponentEnvConfigToConfigPrimitives, generateCommonComponentEnvConfigSchema, generateConfigLocation, type CommonConfigPrimitives, type UnparsedConfig } from '../common/infrastructure/config/common.ts'; import { getSourceEnvSchema, validateSourceAIOJson, validateSourceJson } from '../common/infrastructure/config/source/sourcesMap.ts'; import type { SourceTypeConfigMap } from "../common/infrastructure/config/source/sourcesMap.ts"; +import { stripIndents } from 'common-tags'; type UnparsedSourceConfig = UnparsedConfig; @@ -211,7 +212,7 @@ export default class ScrobbleSources { }) } - const strongConfigs: CommonParsedConfig[] = []; + let strongConfigs: CommonParsedConfig[] = []; for (const entry of sourceUnparsedConfigs) { let parsedConfig: CommonParsedConfig; try { @@ -260,11 +261,20 @@ export default class ScrobbleSources { continue; } + const existingById = strongConfigs.find(x => x.id === parsedConfig.id); + if(undefined !== existingById) { + this.logger.error(stripIndents`There are two ${configType} Sources that have the same ID: + ${existingById.source} + ${parsedConfig.source} + BOTH of these Sources will be disabled to prevent tainting database history. Correct this issue by using a different ID for at least one of them.`); + strongConfigs = strongConfigs.filter(x => x.id !== parsedConfig.id); + continue; + } if (parsedConfig.enable === false) { - this.logger.debug(`Not using Config ${parsedConfig.id}${parsedConfig.name !== undefined ? ` (${parsedConfig.name}) ` :''} because it was marked as not enabled.`); - } else { - strongConfigs.push(parsedConfig); + this.logger.debug(`Not using Config ${parsedConfig.source} because it was marked as not enabled.`); + continue; } + strongConfigs.push(parsedConfig); } if(strongConfigs.length > 0) { -- 2.51.2 From e1324921e9b599c108e46ff92fc3e35e879688a6 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Mon, 3 Aug 2026 15:38:35 +0000 Subject: [PATCH 35/35] fix: Fix missing name passed to component classes --- src/backend/scrobblers/ScrobbleClients.ts | 17 +++++++++-------- src/backend/sources/ScrobbleSources.ts | 3 ++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/backend/scrobblers/ScrobbleClients.ts b/src/backend/scrobblers/ScrobbleClients.ts index ae4d1580..b8c250b5 100644 --- a/src/backend/scrobblers/ScrobbleClients.ts +++ b/src/backend/scrobblers/ScrobbleClients.ts @@ -224,6 +224,7 @@ export default class ScrobbleClients { const parsed = entry.source === 'file' ? (await validateClientJson(entry.type, entry.config)) : (await validateClientAIOJson(entry.type, entry.config)); parsedConfig = { ...parsed, + name: parsed.name ?? parsed.id, source: generateConfigLocation('client', entry) } } break; @@ -285,42 +286,42 @@ export default class ScrobbleClients { case 'discord': { const DiscordScrobbler = (await import('./DiscordScrobbler.ts')).default; await this.instantiateClients('discord', strongConfigs, clientDefaults, DiscordScrobbler, - (config, options) => [config.name, { ...config, options }, {}, this.emitter, this.logger]); + (config, options) => [config.name ?? config.id, { ...config, options }, {}, this.emitter, this.logger]); } break; case 'koito': { const KoitoScrobbler = (await import('./KoitoScrobbler.ts')).default; await this.instantiateClients('koito', strongConfigs, clientDefaults, KoitoScrobbler, - (config, options) => [config.name, { ...config, options: { ...options, configDir: this.internalConfig.configDir } }, {}, this.emitter, this.logger]); + (config, options) => [config.name ?? config.id, { ...config, options: { ...options, configDir: this.internalConfig.configDir } }, {}, this.emitter, this.logger]); } break; case 'lastfm': { const LastfmScrobbler = (await import('./LastfmScrobbler.ts')).default; await this.instantiateClients('lastfm', strongConfigs, clientDefaults, LastfmScrobbler, - (config, options) => [config.name, { ...config, options }, this.internalConfig, this.emitter, this.logger]); + (config, options) => [config.name ?? config.id, { ...config, options }, this.internalConfig, this.emitter, this.logger]); } break; case 'librefm': { const LibrefmScrobbler = (await import('./LibrefmScrobbler.ts')).default; await this.instantiateClients('librefm', strongConfigs, clientDefaults, LibrefmScrobbler, - (config, options) => [config.name, { ...config, options }, this.internalConfig, this.emitter, this.logger]); + (config, options) => [config.name ?? config.id, { ...config, options }, this.internalConfig, this.emitter, this.logger]); } break; case 'listenbrainz': { const ListenbrainzScrobbler = (await import('./ListenbrainzScrobbler.ts')).default; await this.instantiateClients('listenbrainz', strongConfigs, clientDefaults, ListenbrainzScrobbler, - (config, options) => [config.name, { ...config, options }, this.internalConfig, this.emitter, this.logger]); + (config, options) => [config.name ?? config.id, { ...config, options }, this.internalConfig, this.emitter, this.logger]); } break; case 'maloja': { const MalojaScrobbler = (await import('./MalojaScrobbler.ts')).default; await this.instantiateClients('maloja', strongConfigs, clientDefaults, MalojaScrobbler, - (config, options) => [config.name, { ...config, options }, this.emitter, this.logger]); + (config, options) => [config.name ?? config.id, { ...config, options }, this.emitter, this.logger]); } break; case 'rocksky': { const RockskyScrobbler = (await import('./RockskyScrobbler.ts')).default; await this.instantiateClients('rocksky', strongConfigs, clientDefaults, RockskyScrobbler, - (config, options) => [config.name, { ...config, options }, this.internalConfig, this.emitter, this.logger]); + (config, options) => [config.name ?? config.id, { ...config, options }, this.internalConfig, this.emitter, this.logger]); } break; case 'tealfm': { const TealScrobbler = (await import('./TealfmScrobbler.ts')).default; await this.instantiateClients('tealfm', strongConfigs, clientDefaults, TealScrobbler, - (config, options) => [config.name, { ...config, options }, this.internalConfig, this.emitter, this.logger]); + (config, options) => [config.name ?? config.id, { ...config, options }, this.internalConfig, this.emitter, this.logger]); } break; } } diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts index 18c575a2..4a4cc352 100644 --- a/src/backend/sources/ScrobbleSources.ts +++ b/src/backend/sources/ScrobbleSources.ts @@ -247,6 +247,7 @@ export default class ScrobbleSources { const parsed = entry.source === 'file' ? (await validateSourceJson(entry.type, entry.config)) : (await validateSourceAIOJson(entry.type, entry.config)); parsedConfig = { ...parsed, + name: parsed.name ?? parsed.id, source: generateConfigLocation('source', entry) } } break; @@ -293,7 +294,7 @@ export default class ScrobbleSources { try { const config = await validateSourceJson(sourceType, s); const compositeOptions = { ...defaults, ...config.options }; - const newComponent = new Ctor(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); + const newComponent = new Ctor(config.name ?? config.id, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); newComponent.logger.info(`Source added from ${s.source}`); this.sources.push(newComponent); } catch (e) {