diff --git a/docsite/docs/configuration/sources/lastfm-endpoint.mdx b/docsite/docs/configuration/sources/lastfm-endpoint.mdx
index bf99c870..6359d922 100644
--- a/docsite/docs/configuration/sources/lastfm-endpoint.mdx
+++ b/docsite/docs/configuration/sources/lastfm-endpoint.mdx
@@ -41,6 +41,6 @@ http://localhost:9078/api/lastfm/mySlug
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
- | `LFMENDPOINT_ENABLE` | No | | Use LFM Endpoint as a Source without any other configuration. Only required if slug/token are not provided as ENVs |
+ | `LFM_ENABLE` | No | | Use LFM Endpoint as a Source without any other configuration. Only required if slug/token are not provided as ENVs |
| `LFM_SLUG` | No | | (Optional) The URL suffix to use for accepting LFM scrobbles |
\ No newline at end of file
diff --git a/docsite/docs/configuration/sources/listenbrainz-endpoint.mdx b/docsite/docs/configuration/sources/listenbrainz-endpoint.mdx
index ce44dc3c..976b84bd 100644
--- a/docsite/docs/configuration/sources/listenbrainz-endpoint.mdx
+++ b/docsite/docs/configuration/sources/listenbrainz-endpoint.mdx
@@ -111,7 +111,7 @@ To troubleshoot any errors, and assuming you are using Home Assistant, view the
| Environmental Variable | Required? | Default | Description |
| :--------------------- | :-------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
- | `LZENDPOINT_ENABLE` | No | | Use LZ Endpoint as a Source without any other configuration. Only required if slug/token are not provided as ENVs |
+ | `LZE_ENABLE` | No | | Use LZ Endpoint as a Source without any other configuration. Only required if slug/token are not provided as ENVs |
| `LZE_TOKEN` | No | | LZ "Authentication Token" you provided to the scrobbling application |
| `LZE_SLUG` | No | | (Optional) The URL suffix to use for accepting LZ scrobbles |
| `LZE_USERNAME` | No | | (Optional) A fake username that will be returned for token validation and on now-playing responses. If none is provided the Source's name is used instead. |
diff --git a/docsite/docs/updating/upgradePath/from0140.mdx b/docsite/docs/updating/upgradePath/from0140.mdx
index 02df4733..a7edac3a 100644
--- a/docsite/docs/updating/upgradePath/from0140.mdx
+++ b/docsite/docs/updating/upgradePath/from0140.mdx
@@ -202,4 +202,11 @@ Remove the `connection` property from the `auth` object. If using `file` the ent
```
-
\ No newline at end of file
+
+
+### Renamed ENV Keys
+
+In an effort to standard ENV prefixes some ENV keys have been renamed:
+
+* `LZENDPOINT_ENABLE` => `LZE_ENABLE`
+* `LFMENDPOINT_ENABLE` => `LFM_ENABLE`
\ 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 d3ec34f5..f9f6fea6 100644
--- a/src/backend/common/infrastructure/config/common.ts
+++ b/src/backend/common/infrastructure/config/common.ts
@@ -1,5 +1,11 @@
import { keyOmit } from "../Atomic.js";
+export interface CommonConfigPrimitives {
+ name?: string
+ id?: string
+ enable?: boolean
+}
+
export interface CommonConfig {
name?: string
/** A UNIQUE identifier for this Source/Client
diff --git a/src/backend/scrobblers/ScrobbleClients.ts b/src/backend/scrobblers/ScrobbleClients.ts
index af6d8d75..cc81b898 100644
--- a/src/backend/scrobblers/ScrobbleClients.ts
+++ b/src/backend/scrobblers/ScrobbleClients.ts
@@ -11,8 +11,8 @@ import { ListenBrainzClientConfig, ListenBrainzData } from "../common/infrastruc
import { MalojaClientConfig, MalojaData } from "../common/infrastructure/config/client/maloja.js";
import { WildcardEmitter } from "../common/WildcardEmitter.js";
import { Notifiers } from "../notifier/Notifiers.js";
-import { isDebugMode, parseBool } from "../utils.js";
-import { readJson } from '../utils/DataUtils.js';
+import { isDebugMode, nonEmptyObj, parseBool, removeUndefinedKeys } from "../utils.js";
+import { getCommonComponentEnvConfig, readJson } from '../utils/DataUtils.js';
import { validateJson } from "../utils/ValidationUtils.js";
import AbstractScrobbleClient from "./AbstractScrobbleClient.js";
import { KoitoClientConfig, KoitoData } from '../common/infrastructure/config/client/koito.js';
@@ -171,138 +171,149 @@ export default class ScrobbleClients {
for (const clientType of clientTypes) {
const defaultConfigureAs = 'client';
switch (clientType) {
- case 'maloja':
+ case 'maloja': {
// env builder for single user mode
- const url = process.env.MALOJA_URL;
- const apiKey = process.env.MALOJA_API_KEY;
- if (url !== undefined || apiKey !== undefined) {
- const malojaData: MalojaData = {
- url,
- apiKey
- }
+ 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: malojaData,
+ data: data,
+ ...p,
options: transformPresetEnv('MALOJA')
})
}
- break;
- case 'lastfm':
- const lfm: LastfmData = {
+ } 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
- };
- if (!Object.values(lfm).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('LASTFM');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'lastfm',
name: 'unnamed-lfm',
source: 'ENV',
mode: 'single',
configureAs: 'client',
- data: lfm,
+ data: data,
+ ...p,
options: transformPresetEnv('LASTFM')
})
}
- break;
+ } break;
case 'librefm': {
- const shouldUse = parseBool(process.env.LIBRFM_ENABLE)
- const libre: LibrefmData = {
+ 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,
- };
- if (!Object.values(libre).every(x => x === undefined) || shouldUse) {
+ }, false);
+ const p = getCommonComponentEnvConfig('LIBREFM');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'librefm',
name: 'unnamed-librefm',
source: 'ENV',
mode: 'single',
configureAs: 'client',
- data: libre,
+ data: data,
+ ...p,
options: transformPresetEnv('LIBREFM')
})
}
} break;
- case 'listenbrainz':
- const lz: ListenBrainzData = {
+ case 'listenbrainz': {
+ const data = removeUndefinedKeys({
url: process.env.LZ_URL,
token: process.env.LZ_TOKEN,
username: process.env.LZ_USER
- };
- if (!Object.values(lz).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('LZ');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'listenbrainz',
name: 'unnamed-lz',
source: 'ENV',
mode: 'single',
configureAs: 'client',
- data: lz,
+ data: data,
+ ...p,
options: transformPresetEnv('LZ')
})
}
- break;
- case 'koito':
- const koit: KoitoData = {
+ } break;
+ case 'koito': {
+ const data = removeUndefinedKeys({
url: process.env.KOITO_URL,
token: process.env.KOITO_TOKEN,
username: process.env.KOITO_USER
- };
- if (!Object.values(koit).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('KOITO');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'koito',
name: 'unnamed-koito',
source: 'ENV',
mode: 'single',
configureAs: 'client',
- data: koit,
+ data: data,
+ ...p,
options: transformPresetEnv('KOITO')
})
}
- break;
- case 'tealfm':
- const teal: TealData = {
+ } break;
+ case 'tealfm': {
+ const data: TealData = removeUndefinedKeys({
identifier: process.env.TEALFM_IDENTIFIER,
appPassword: process.env.TEALFM_APP_PW,
- };
- if (!Object.values(teal).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('TEALFM');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'tealfm',
name: 'unnamed-tealfm',
source: 'ENV',
mode: 'single',
configureAs: 'client',
- data: teal,
+ data: data,
+ ...p,
options: transformPresetEnv('TEALFM')
})
}
- break;
- case 'rocksky':
- const rocksky: RockSkyData = {
+ } break;
+ case 'rocksky': {
+ const data: RockSkyData = removeUndefinedKeys({
key: process.env.ROCKSKY_KEY,
handle: process.env.ROCKSKY_HANDLE
- };
- if (!Object.values(rocksky).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('ROCKSKY');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'rocksky',
name: 'unnamed-rocksky',
source: 'ENV',
mode: 'single',
configureAs: 'client',
- data: rocksky,
+ data: data,
+ ...p,
options: transformPresetEnv('ROCKSKY')
})
}
- break;
+ } break;
case 'discord': {
- const discord: DiscordData = {
+ const data: DiscordData = removeUndefinedKeys({
token: process.env.DISCORD_TOKEN,
artwork: process.env.DISCORD_ARTWORK,
applicationId: process.env.DISCORD_APPLICATION_ID,
@@ -310,15 +321,17 @@ export default class ScrobbleClients {
artworkDefaultUrl: process.env.DISCORD_ARTWORK_DEFAULT_URL,
statusOverrideAllow: process.env.DISCORD_STATUS_OVERRIDE_ALLOW,
listeningActivityAllow: process.env.DISCORD_LISTENING_ACTIVITY_ALLOW
- }
- if (!Object.values(discord).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('DISCORD');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'discord',
name: 'unnamed-discord',
source: 'ENV',
mode: 'single',
configureAs: 'client',
- data: discord,
+ data: data,
+ ...p,
options: transformPresetEnv('DISCORD')
})
}
diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts
index db80b941..b3d4fc7d 100644
--- a/src/backend/sources/ScrobbleSources.ts
+++ b/src/backend/sources/ScrobbleSources.ts
@@ -36,8 +36,8 @@ import { YTMusicData, YTMusicSourceConfig } from "../common/infrastructure/confi
import { YandexMusicBridgeData, YandexMusicBridgeSourceConfig } from "../common/infrastructure/config/source/ymbridge.js";
import { SonosData, SonosSourceConfig } from "../common/infrastructure/config/source/sonos.js";
import { WildcardEmitter } from "../common/WildcardEmitter.js";
-import { parseBool } from "../utils.js";
-import { readJson } from '../utils/DataUtils.js';
+import { nonEmptyObj, parseBool, removeUndefinedKeys } from "../utils.js";
+import { getCommonComponentEnvConfig, readJson } from '../utils/DataUtils.js';
import { validateJson } from "../utils/ValidationUtils.js";
import AbstractSource from "./AbstractSource.js";
import { nonEmptyStringOrDefault } from '../../core/StringUtils.js';
@@ -277,26 +277,28 @@ export default class ScrobbleSources {
let defaultConfigureAs: ConfigureAsSource = 'source';
// env builder for single user mode
switch (sourceType) {
- case 'spotify':
- const s: SpotifySourceData = {
+ 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,
- };
- if (!Object.values(s).every(x => x === undefined && x !== null)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('SPOTIFY');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'spotify',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: s,
+ data: data,
+ ...p,
options: transformPresetEnv('SPOTIFY')
})
}
- break;
- case 'plex':
- const p: PlexApiData = {
+ } break;
+ case 'plex': {
+ const data: PlexApiData = removeUndefinedKeys({
url: process.env.PLEX_URL,
token: process.env.PLEX_TOKEN,
usersAllow: process.env.PLEX_USERS_ALLOW,
@@ -305,39 +307,43 @@ export default class ScrobbleSources {
devicesBlock: process.env.PLEX_DEVICES_BLOCK,
librariesAllow: process.env.PLEX_LIBRARIES_ALLOW,
librariesBlock: process.env.PLEX_LIBRARIES_BLOCK
- };
- if (!Object.values(p).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('PLEX');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'plex',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: p,
+ data: data,
+ ...p,
options: transformPresetEnv('PLEX')
})
}
- break;
- case 'subsonic':
- const sub: SubsonicData = {
+ } break;
+ case 'subsonic': {
+ const data: SubsonicData = removeUndefinedKeys({
user: process.env.SUBSONIC_USER,
password: process.env.SUBSONIC_PASSWORD,
url: process.env.SUBSONIC_URL,
- };
- if (!Object.values(sub).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('SUBSONIC');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'subsonic',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: sub as SubsonicData,
+ data: data,
+ ...p,
options: transformPresetEnv('SUBSONIC')
})
}
- break;
- case 'jellyfin':
- const j: JellyApiData = {
+ } break;
+ case 'jellyfin': {
+ const data: JellyApiData = removeUndefinedKeys({
user: process.env.JELLYFIN_USER,
password: process.env.JELLYFIN_PASSWORD,
apiKey: process.env.JELLYFIN_APIKEY,
@@ -350,294 +356,317 @@ export default class ScrobbleSources {
librariesBlock: process.env.JELLYFIN_LIBRARIES_BLOCK,
frontendUrlOverride: process.env.JELLYFIN_FRONTEND_URL_OVERRIDE,
allowMediaTypes: process.env.JELLYFIN_MEDIATYPES_ALLOW
- };
- if (!Object.values(j).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('JELLYFIN');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'jellyfin',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: j,
+ data: data,
+ ...p,
options: transformPresetEnv('JELLYFIN')
})
}
- break;
+ } break;
case 'lastfm':
{
- const lfm: LastfmData = {
+ 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,
- };
- if (!Object.values(lfm).every(x => x === undefined)) {
+ }, 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: lfm,
+ data: data,
+ ...p,
options: transformPresetEnv('SOURCE_LASTFM')
})
}
}
break;
- case 'deezer':
- const d = {
+ 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
- };
- if (!Object.values(d).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('DEEZER');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'deezer',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: d,
+ data: data,
+ ...p,
options: transformPresetEnv('DEEZER')
});
}
- break;
- case 'mpris':
- const shouldUse = parseBool(process.env.MPRIS_ENABLE);
- const mp: MPRISData = {
+ } break;
+ case 'mpris': {
+ const data: MPRISData = removeUndefinedKeys({
blacklist: process.env.MPRIS_BLACKLIST,
whitelist: process.env.MPRIS_WHITELIST
- }
- if (!Object.values(mp).every(x => x === undefined) || shouldUse) {
+ }, false);
+ const p = getCommonComponentEnvConfig('MPRIS');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'mpris',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: mp as MPRISData,
+ data: data,
+ ...p,
options: transformPresetEnv('MPRIS')
});
}
- break;
+ } break;
case 'maloja': {
- // env builder for single user mode
- const url = process.env.SOURCE_MALOJA_URL;
- const apiKey = process.env.SOURCE_MALOJA_API_KEY;
- if (url !== undefined || apiKey !== undefined) {
- const malojaData: MalojaData = {
- url,
- apiKey
- }
+ const data = removeUndefinedKeys({
+ url: process.env.MALOJA_URL,
+ apiKey: process.env.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: malojaData,
+ data: data,
+ ...p,
options: transformPresetEnv('SOURCE_MALOJA')
})
}
}
break;
case 'librefm':{
- const shouldUse = parseBool(process.env.LIBRFM_ENABLE)
- const libre: LibrefmData = {
+ 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,
- };
- if (!Object.values(libre).every(x => x === undefined) || shouldUse) {
+ }, 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: libre,
+ data: data,
+ ...p,
options: transformPresetEnv('SOURCE_LIBREFM')
})
}
}
break;
case 'listenbrainz': {
- const lz: ListenBrainzData = {
+ const data: ListenBrainzData = removeUndefinedKeys({
url: process.env.SOURCE_LZ_URL,
token: process.env.SOURCE_LZ_TOKEN,
username: process.env.SOURCE_LZ_USER
- };
- if (!Object.values(lz).every(x => x === undefined)) {
+ }, 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: lz,
+ data: data,
+ ...p,
options: transformPresetEnv('SOURCE_LZ')
})
}
}
break;
case 'koito': {
- const koit: KoitoData = {
+ const data: KoitoData = removeUndefinedKeys({
url: process.env.SOURCE_KOITO_URL,
token: process.env.SOURCE_KOITO_TOKEN,
username: process.env.SOURCE_KOITO_USER
- };
- if (!Object.values(koit).every(x => x === undefined)) {
+ }, 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: koit,
+ data: data,
+ ...p,
options: transformPresetEnv('SOURCE_KOITO')
})
}
}
break;
case 'tealfm': {
- const teal: TealData = {
+ const data: TealData = removeUndefinedKeys({
identifier: process.env.SOURCE_TEALFM_IDENTIFIER,
appPassword: process.env.SOURCE_TEALFM_APP_PW,
- };
- if (!Object.values(teal).every(x => x === undefined)) {
+ }, 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: teal,
+ data: data,
+ ...p,
options: transformPresetEnv('SOURCE_TEALFM')
})
}
}
break;
case 'rocksky': {
- const rocksky: RockSkyData = {
+ const data: RockSkyData = removeUndefinedKeys({
key: process.env.SOURCE_ROCKSKY_KEY,
handle: process.env.SOURCE_ROCKSKY_HANDLE
- };
- if (!Object.values(rocksky).every(x => x === undefined)) {
+ }, 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: rocksky,
+ data: data,
+ ...p,
options: transformPresetEnv('SOURCE_ROCKSKY')
})
}
}
break;
- case 'endpointlz':
- const lzShouldUse = parseBool(process.env.LZENDPOINT_ENABLE);
- const lze: ListenbrainzEndpointData = {
+ case 'endpointlz': {
+ const data: ListenbrainzEndpointData = removeUndefinedKeys({
slug: process.env.LZE_SLUG,
token: process.env.LZE_TOKEN,
username: process.env.LZE_USERNAME
- }
- if (!Object.values(lze).every(x => x === undefined) || lzShouldUse) {
+ }, false);
+ const p = getCommonComponentEnvConfig('LZE');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'endpointlz',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: lze as ListenbrainzEndpointData,
+ data: data,
+ ...p,
options: transformPresetEnv('LZE')
});
}
- break;
- case 'endpointlfm':
- const lfmShouldUse = parseBool(process.env.LFMENDPOINT_ENABLE);
- const lfme: LastFMEndpointData = {
+ } break;
+ case 'endpointlfm': {
+ const data: LastFMEndpointData = removeUndefinedKeys({
slug: process.env.LFM_SLUG,
- }
- if (!Object.values(lfme).every(x => x === undefined) || lfmShouldUse) {
+ }, false);
+ const p = getCommonComponentEnvConfig('LFM');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'endpointlfm',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: lfme as LastFMEndpointData,
+ data: data,
+ ...p,
options: transformPresetEnv('LFM')
});
}
- break;
+ } break;
case 'icecast': {
const scrobbleStart = parseBool(process.env.ICECAST_SCROBBLE_START);
- const icecast: IcecastData = {
+ const data: IcecastData = removeUndefinedKeys({
url: process.env.ICECAST_URL,
- }
- if (!Object.values(icecast).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('ICECAST');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'icecast',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: icecast as IcecastData,
+ data: data,
+ ...p,
options: transformPresetEnv('ICECAST', {
systemScrobble: scrobbleStart
})
});
}
} break;
- case 'jriver':
- const jr: JRiverData = {
+ case 'jriver': {
+ const data: JRiverData = removeUndefinedKeys({
url: process.env.JRIVER_URL,
username: process.env.JRIVER_USER,
password: process.env.JRIVER_PASSWORD
- }
- if (!Object.values(jr).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('JRIVER');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'jriver',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: jr as JRiverData,
+ data: data,
+ ...p,
options: transformPresetEnv('JRIVER')
});
}
- break;
- case 'kodi':
- const ko: KodiData = {
+ } break;
+ case 'kodi': {
+ const data: KodiData = removeUndefinedKeys({
url: process.env.KODI_URL,
username: process.env.KODI_USER,
password: process.env.KODI_PASSWORD
- }
- if (!Object.values(ko).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('KODI');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'kodi',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: ko as KodiData,
+ data: data,
+ ...p,
options: transformPresetEnv('KODI')
});
}
- break;
- case 'webscrobbler':
- const wsShouldUse = parseBool(process.env.WS_ENABLE);
- const ws: WebScrobblerData = {
+ } break;
+ case 'webscrobbler': {
+ const data: WebScrobblerData = removeUndefinedKeys({
blacklist: process.env.WS_BLACKLIST,
whitelist: process.env.WS_WHITELIST
- }
- if (!Object.values(ws).every(x => x === undefined) || wsShouldUse) {
+ }, false);
+ const p = getCommonComponentEnvConfig('WS');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'webscrobbler',
name: 'unnamed',
@@ -645,22 +674,23 @@ export default class ScrobbleSources {
mode: 'single',
configureAs: defaultConfigureAs,
data: {
- blacklist: ws.blacklist !== undefined ? (ws.blacklist as string).split(',') : [],
- whitelist: ws.whitelist !== undefined ? (ws.whitelist as string).split(',') : [],
+ 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 ccShouldUse = parseBool(process.env.CC_ENABLE);
- const cc: ChromecastData = {
+ } 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
- }
- if (!Object.values(cc).every(x => x === undefined) || ccShouldUse) {
+ }, false);
+ const p = getCommonComponentEnvConfig('CC');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'chromecast',
name: 'unnamed',
@@ -668,160 +698,177 @@ export default class ScrobbleSources {
mode: 'single',
configureAs: defaultConfigureAs,
data: {
- blacklistDevices: cc.blacklistDevices !== undefined ? (cc.blacklistDevices as string).split(',') : [],
- whitelistDevices: cc.whitelistDevices !== undefined ? (cc.whitelistDevices as string).split(',') : [],
- blacklistApps: cc.blacklistApps !== undefined ? (cc.blacklistApps as string).split(',') : [],
- whitelistApps: cc.whitelistApps !== undefined ? (cc.whitelistApps as string).split(',') : [],
+ 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 musecase: MusicCastData = {
+ } break;
+ case 'musiccast': {
+ const data: MusicCastData = removeUndefinedKeys({
url: process.env.MCAST_URL,
- }
- if (!Object.values(musecase).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('MCAST');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'musiccast',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: musecase as MusicCastData,
+ data: data,
+ ...p,
options: transformPresetEnv('MCAST')
});
}
- break;
- case 'musikcube':
- const mc: MusikcubeData = {
+ } break;
+ case 'musikcube': {
+ const data: MusikcubeData = removeUndefinedKeys({
url: process.env.MC_URL,
password: process.env.MC_PASSWORD
- }
- if (!Object.values(mc).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('MC');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'musikcube',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: mc as MusikcubeData,
+ data: data as MusikcubeData,
+ ...p,
options: transformPresetEnv('MC')
});
}
- break;
- case 'mpd':
- const mpd: MPDData = {
+ } break;
+ case 'mpd': {
+ const data: MPDData = removeUndefinedKeys({
url: process.env.MPD_URL,
password: process.env.MPD_PASSWORD
- }
- if (!Object.values(mpd).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('MPD');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'mpd',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: mpd as MPDData,
+ data: data,
+ ...p,
options: transformPresetEnv('MPD')
});
}
- break;
- case 'vlc':
- const vlc: VLCData = {
+ } break;
+ case 'vlc': {
+ const data: VLCData = removeUndefinedKeys({
url: process.env.VLC_URL,
password: process.env.VLC_PASSWORD
- }
- if (!Object.values(vlc).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('VLC');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'vlc',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: vlc as VLCData,
+ data: data,
+ ...p,
options: transformPresetEnv('VLC')
});
}
- break;
- case 'ytmusic':
- const ytm: YTMusicData = {
+ } 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
- }
- if (!Object.values(ytm).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('YTM');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'ytmusic',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: ytm as YTMusicData,
+ data: data,
+ ...p,
options: transformPresetEnv('YTM')
});
}
- break;
+ } break;
case 'azuracast': {
- const azura: AzuracastData = {
+ 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() !== '') {
- azura.monitorWhenListeners = !isNaN(Number.parseInt(listenerNum)) ? Number.parseInt(listenerNum) : parseBool(listenerNum);
+ data.monitorWhenListeners = !isNaN(Number.parseInt(listenerNum)) ? Number.parseInt(listenerNum) : parseBool(listenerNum);
}
const live = process.env.AZURA_LIVE ?? '';
if(live.trim() !== '') {
- azura.monitorWhenLive = parseBool(live);
+ data.monitorWhenLive = parseBool(live);
}
- if (!Object.values(azura).every(x => x === undefined)) {
+ const p = getCommonComponentEnvConfig('AZURA');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'azuracast',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: azura as unknown as AzuracastData,
+ data: data,
+ ...p,
options: transformPresetEnv('AZURA')
});
}
- } break;
+ } break;
case 'sonos': {
- const sonos: SonosData = {
+ 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
- }
- if (!Object.values(sonos).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('SONOS');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'sonos',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: sonos as unknown as SonosData,
+ data: data,
+ ...p,
options: transformPresetEnv('SONOS')
});
}
} break;
case 'ymbridge': {
- const yandex: YandexMusicBridgeData = {
+ const data: YandexMusicBridgeData = removeUndefinedKeys({
url: process.env.YMBRIDGE_URL,
apiKey: process.env.YMBRIDGE_API_KEY,
- }
- if (!Object.values(yandex).every(x => x === undefined)) {
+ }, false);
+ const p = getCommonComponentEnvConfig('YMBRIDGE');
+ if (nonEmptyObj(data) || nonEmptyObj(p)) {
configs.push({
type: 'ymbridge',
name: 'unnamed',
source: 'ENV',
mode: 'single',
configureAs: defaultConfigureAs,
- data: yandex,
+ data: data,
+ ...p,
options: transformPresetEnv('YMBRIDGE')
});
}
diff --git a/src/backend/utils.ts b/src/backend/utils.ts
index b184348f..56711494 100644
--- a/src/backend/utils.ts
+++ b/src/backend/utils.ts
@@ -200,7 +200,7 @@ export const spreadDelay = (retries: any, multiplier: any) => {
return s;
}
-export const removeUndefinedKeys = >(obj: T): T | undefined => {
+export const removeUndefinedKeys = >(obj: T, returnUndefined: boolean = true): T | undefined => {
const newObj: any = {};
Object.keys(obj).forEach((key) => {
if(Array.isArray(obj[key])) {
@@ -214,7 +214,10 @@ export const removeUndefinedKeys = >(obj: T): T |
}
});
if(Object.keys(newObj).length === 0) {
- return undefined;
+ if(returnUndefined) {
+ return undefined;
+ }
+ return newObj;
}
Object.keys(newObj).forEach(key => {
if(newObj[key] === undefined || (null !== newObj[key] && typeof newObj[key] === 'object' && Object.keys(newObj[key]).length === 0)) {
@@ -526,6 +529,10 @@ export const isEmptyArrayOrUndefined = (arr: unknown[] | undefined,
return true;
}
+export const nonEmptyObj = (obj: object): boolean => {
+ return Object.keys(obj).length > 0;
+}
+
/**
* Runs the function `fn`
* and retries automatically if it fails.
diff --git a/src/backend/utils/DataUtils.ts b/src/backend/utils/DataUtils.ts
index 176f1adb..60480bac 100644
--- a/src/backend/utils/DataUtils.ts
+++ b/src/backend/utils/DataUtils.ts
@@ -2,6 +2,9 @@ import JSON5 from "json5";
import { constants, promises } from "fs";
import { MaybeLogger } from '../common/MaybeLogger.js';
import { deepEqual } from 'fast-equals';
+import { CommonConfigPrimitives } from "../common/infrastructure/config/common.js";
+import { parseBoolStrict, removeUndefinedKeys } from "../utils.js";
+import { nonEmptyStringOrDefault } from "../../core/StringUtils.js";
export const asArray = (data: T | T[]): T[] => {
if (Array.isArray(data)) {
@@ -175,4 +178,13 @@ export const objectsEqual = (a: object, b: object) => {
} catch (e) {
throw new Error('Could not compare objects', {cause: e});
}
+}
+
+export const getCommonComponentEnvConfig = (prefix: string): Partial => {
+ const e = nonEmptyStringOrDefault(process.env[`${prefix}_ENABLE`], undefined);
+ return removeUndefinedKeys({
+ id: nonEmptyStringOrDefault(process.env[`${prefix}_ID`], undefined),
+ name: nonEmptyStringOrDefault(process.env[`${prefix}_NAME`], undefined),
+ enable: e !== undefined ? parseBoolStrict(e) : undefined
+ }, false);
}
\ No newline at end of file