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