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