prismcast 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/app.d.ts +1 -1
- package/dist/app.js +41 -13
- package/dist/app.js.map +1 -1
- package/dist/browser/cdp.d.ts +4 -6
- package/dist/browser/cdp.js +18 -22
- package/dist/browser/cdp.js.map +1 -1
- package/dist/browser/channelSelection.d.ts +41 -11
- package/dist/browser/channelSelection.js +75 -24
- package/dist/browser/channelSelection.js.map +1 -1
- package/dist/browser/display.d.ts +1 -1
- package/dist/browser/index.d.ts +8 -8
- package/dist/browser/index.js +44 -45
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/login.d.ts +2 -2
- package/dist/browser/login.js +1 -1
- package/dist/browser/manifestInterceptor.d.ts +77 -0
- package/dist/browser/manifestInterceptor.js +323 -0
- package/dist/browser/manifestInterceptor.js.map +1 -0
- package/dist/browser/precaching.d.ts +1 -1
- package/dist/browser/precaching.js +23 -23
- package/dist/browser/precaching.js.map +1 -1
- package/dist/browser/tuning/comcastPolymer.d.ts +1 -1
- package/dist/browser/tuning/cox.d.ts +1 -1
- package/dist/browser/tuning/directv.d.ts +1 -1
- package/dist/browser/tuning/directv.js +65 -69
- package/dist/browser/tuning/directv.js.map +1 -1
- package/dist/browser/tuning/fox.d.ts +1 -1
- package/dist/browser/tuning/fox.js +172 -15
- package/dist/browser/tuning/fox.js.map +1 -1
- package/dist/browser/tuning/hbo.d.ts +1 -1
- package/dist/browser/tuning/hbo.js +7 -7
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/tuning/hulu.d.ts +1 -1
- package/dist/browser/tuning/hulu.js +120 -113
- package/dist/browser/tuning/hulu.js.map +1 -1
- package/dist/browser/tuning/shared.d.ts +3 -3
- package/dist/browser/tuning/shared.js +5 -5
- package/dist/browser/tuning/sling.d.ts +1 -1
- package/dist/browser/tuning/sling.js +30 -30
- package/dist/browser/tuning/sling.js.map +1 -1
- package/dist/browser/tuning/spectrum.d.ts +1 -1
- package/dist/browser/tuning/spectrum.js +21 -22
- package/dist/browser/tuning/spectrum.js.map +1 -1
- package/dist/browser/tuning/thumbnailRow.d.ts +1 -1
- package/dist/browser/tuning/thumbnailRow.js +1 -1
- package/dist/browser/tuning/tileClick.d.ts +1 -1
- package/dist/browser/tuning/tileClick.js +5 -5
- package/dist/browser/tuning/xfinity.d.ts +1 -1
- package/dist/browser/tuning/youtubeTv.d.ts +1 -1
- package/dist/browser/tuning/youtubeTv.js +17 -18
- package/dist/browser/tuning/youtubeTv.js.map +1 -1
- package/dist/browser/video.d.ts +20 -6
- package/dist/browser/video.js +30 -38
- package/dist/browser/video.js.map +1 -1
- package/dist/channels/index.d.ts +1 -2
- package/dist/channels/index.js +475 -442
- package/dist/channels/index.js.map +1 -1
- package/dist/config/channelForm.d.ts +58 -0
- package/dist/config/channelForm.js +129 -0
- package/dist/config/channelForm.js.map +1 -0
- package/dist/config/consistencyProbe.d.ts +7 -0
- package/dist/config/consistencyProbe.js +143 -0
- package/dist/config/consistencyProbe.js.map +1 -0
- package/dist/config/health.d.ts +7 -6
- package/dist/config/health.js +78 -65
- package/dist/config/health.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +21 -14
- package/dist/config/index.js.map +1 -1
- package/dist/config/paths.d.ts +9 -3
- package/dist/config/paths.js +12 -4
- package/dist/config/paths.js.map +1 -1
- package/dist/config/persistence.context.d.ts +8 -0
- package/dist/config/persistence.context.js +30 -0
- package/dist/config/persistence.context.js.map +1 -0
- package/dist/config/persistence.d.ts +157 -0
- package/dist/config/persistence.js +446 -0
- package/dist/config/persistence.js.map +1 -0
- package/dist/config/presets.d.ts +14 -4
- package/dist/config/presets.js +84 -75
- package/dist/config/presets.js.map +1 -1
- package/dist/config/profiles.d.ts +2 -2
- package/dist/config/profiles.js +7 -9
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/servicePacks.d.ts +46 -0
- package/dist/config/{providerPacks.js → servicePacks.js} +50 -53
- package/dist/config/servicePacks.js.map +1 -0
- package/dist/config/services.d.ts +228 -0
- package/dist/config/services.js +772 -0
- package/dist/config/services.js.map +1 -0
- package/dist/config/sites.d.ts +3 -3
- package/dist/config/sites.js +75 -75
- package/dist/config/sites.js.map +1 -1
- package/dist/config/userChannels.d.ts +522 -54
- package/dist/config/userChannels.js +1343 -366
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +108 -13
- package/dist/config/userConfig.js +296 -143
- package/dist/config/userConfig.js.map +1 -1
- package/dist/config/userProfiles.d.ts +34 -16
- package/dist/config/userProfiles.js +189 -100
- package/dist/config/userProfiles.js.map +1 -1
- package/dist/hdhr/channelMap.d.ts +1 -1
- package/dist/hdhr/channelMap.js +10 -16
- package/dist/hdhr/channelMap.js.map +1 -1
- package/dist/hdhr/deviceId.js +10 -9
- package/dist/hdhr/deviceId.js.map +1 -1
- package/dist/hdhr/discover.js +4 -5
- package/dist/hdhr/discover.js.map +1 -1
- package/dist/hdhr/index.js +12 -19
- package/dist/hdhr/index.js.map +1 -1
- package/dist/index.js +12 -12
- package/dist/index.js.map +1 -1
- package/dist/native/codecInference.d.ts +44 -0
- package/dist/native/codecInference.js +327 -0
- package/dist/native/codecInference.js.map +1 -0
- package/dist/native/decrypt.d.ts +1 -1
- package/dist/native/index.d.ts +5 -5
- package/dist/native/index.js +45 -67
- package/dist/native/index.js.map +1 -1
- package/dist/native/probe.d.ts +27 -9
- package/dist/native/probe.js +159 -68
- package/dist/native/probe.js.map +1 -1
- package/dist/native/proxy.d.ts +4 -2
- package/dist/native/proxy.js +56 -53
- package/dist/native/proxy.js.map +1 -1
- package/dist/native/tokenExpiry.d.ts +22 -0
- package/dist/native/tokenExpiry.js +153 -0
- package/dist/native/tokenExpiry.js.map +1 -0
- package/dist/routes/assets.js +3 -3
- package/dist/routes/assets.js.map +1 -1
- package/dist/routes/auth.js +9 -13
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/components.js +2 -2
- package/dist/routes/components.js.map +1 -1
- package/dist/routes/config/channels/endpoints/browse.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/browse.js +195 -0
- package/dist/routes/config/channels/endpoints/browse.js.map +1 -0
- package/dist/routes/config/channels/endpoints/bulk.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/bulk.js +128 -0
- package/dist/routes/config/channels/endpoints/bulk.js.map +1 -0
- package/dist/routes/config/channels/endpoints/crud.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/crud.js +504 -0
- package/dist/routes/config/channels/endpoints/crud.js.map +1 -0
- package/dist/routes/config/channels/endpoints/importExport.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/importExport.js +131 -0
- package/dist/routes/config/channels/endpoints/importExport.js.map +1 -0
- package/dist/routes/config/channels/endpoints/predefined.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/predefined.js +78 -0
- package/dist/routes/config/channels/endpoints/predefined.js.map +1 -0
- package/dist/routes/config/channels/endpoints/prefs.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/prefs.js +58 -0
- package/dist/routes/config/channels/endpoints/prefs.js.map +1 -0
- package/dist/routes/config/channels/endpoints/service.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/service.js +149 -0
- package/dist/routes/config/channels/endpoints/service.js.map +1 -0
- package/dist/routes/config/channels/endpoints/tags.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/tags.js +185 -0
- package/dist/routes/config/channels/endpoints/tags.js.map +1 -0
- package/dist/routes/config/channels/http/handler.d.ts +10 -0
- package/dist/routes/config/channels/http/handler.js +20 -0
- package/dist/routes/config/channels/http/handler.js.map +1 -0
- package/dist/routes/config/channels/http/playlistHint.d.ts +25 -0
- package/dist/routes/config/channels/http/playlistHint.js +40 -0
- package/dist/routes/config/channels/http/playlistHint.js.map +1 -0
- package/dist/routes/config/channels/http/serviceWarning.d.ts +10 -0
- package/dist/routes/config/channels/http/serviceWarning.js +28 -0
- package/dist/routes/config/channels/http/serviceWarning.js.map +1 -0
- package/dist/routes/config/channels/index.d.ts +3 -3
- package/dist/routes/config/channels/index.js +2 -2
- package/dist/routes/config/channels/index.js.map +1 -1
- package/dist/routes/config/channels/{routes.d.ts → setup.d.ts} +1 -1
- package/dist/routes/config/channels/setup.js +23 -0
- package/dist/routes/config/channels/setup.js.map +1 -0
- package/dist/routes/config/channels/table.d.ts +13 -13
- package/dist/routes/config/channels/table.js +291 -184
- package/dist/routes/config/channels/table.js.map +1 -1
- package/dist/routes/config/http/envelope.d.ts +111 -0
- package/dist/routes/config/http/envelope.js +103 -0
- package/dist/routes/config/http/envelope.js.map +1 -0
- package/dist/routes/config/index.d.ts +9 -9
- package/dist/routes/config/index.js +5 -5
- package/dist/routes/config/index.js.map +1 -1
- package/dist/routes/config/{providers.js → services.js} +105 -99
- package/dist/routes/config/services.js.map +1 -0
- package/dist/routes/config/settings.d.ts +1 -1
- package/dist/routes/config/settings.js +85 -60
- package/dist/routes/config/settings.js.map +1 -1
- package/dist/routes/debug.js +17 -18
- package/dist/routes/debug.js.map +1 -1
- package/dist/routes/health.js +1 -1
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/icons.js +2 -2
- package/dist/routes/icons.js.map +1 -1
- package/dist/routes/index.js +2 -2
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/logs.js +23 -34
- package/dist/routes/logs.js.map +1 -1
- package/dist/routes/mpegts.js +1 -1
- package/dist/routes/playlist.d.ts +5 -5
- package/dist/routes/playlist.js +84 -61
- package/dist/routes/playlist.js.map +1 -1
- package/dist/routes/root/content.js +82 -61
- package/dist/routes/root/content.js.map +1 -1
- package/dist/routes/root/index.js +2 -5
- package/dist/routes/root/index.js.map +1 -1
- package/dist/routes/root/scripts/channels.js +538 -581
- package/dist/routes/root/scripts/channels.js.map +1 -1
- package/dist/routes/root/scripts/config.js +868 -1022
- package/dist/routes/root/scripts/config.js.map +1 -1
- package/dist/routes/root/scripts/index.d.ts +4 -4
- package/dist/routes/root/scripts/shared.js +445 -197
- package/dist/routes/root/scripts/shared.js.map +1 -1
- package/dist/routes/root/scripts/status.d.ts +4 -0
- package/dist/routes/root/scripts/status.handlers.d.ts +182 -0
- package/dist/routes/root/scripts/status.handlers.js +713 -0
- package/dist/routes/root/scripts/status.handlers.js.map +1 -0
- package/dist/routes/root/scripts/status.js +85 -436
- package/dist/routes/root/scripts/status.js.map +1 -1
- package/dist/routes/root/styles.js +16 -8
- package/dist/routes/root/styles.js.map +1 -1
- package/dist/routes/services.d.ts +6 -0
- package/dist/routes/{providers.js → services.js} +58 -55
- package/dist/routes/services.js.map +1 -0
- package/dist/routes/sse.d.ts +16 -0
- package/dist/routes/sse.js +29 -0
- package/dist/routes/sse.js.map +1 -0
- package/dist/routes/streams.js +10 -23
- package/dist/routes/streams.js.map +1 -1
- package/dist/routes/ui.d.ts +2 -2
- package/dist/routes/ui.js +2 -0
- package/dist/routes/ui.js.map +1 -1
- package/dist/routes/upgrade.js +6 -7
- package/dist/routes/upgrade.js.map +1 -1
- package/dist/service/commands.context.d.ts +6 -0
- package/dist/service/commands.context.js +47 -0
- package/dist/service/commands.context.js.map +1 -0
- package/dist/service/commands.d.ts +53 -8
- package/dist/service/commands.js +172 -192
- package/dist/service/commands.js.map +1 -1
- package/dist/service/generators.context.d.ts +6 -0
- package/dist/service/generators.context.js +45 -0
- package/dist/service/generators.context.js.map +1 -0
- package/dist/service/generators.d.ts +56 -16
- package/dist/service/generators.js +509 -291
- package/dist/service/generators.js.map +1 -1
- package/dist/service/index.d.ts +2 -2
- package/dist/streaming/clients.d.ts +2 -2
- package/dist/streaming/clients.js +2 -2
- package/dist/streaming/codec.d.ts +2 -2
- package/dist/streaming/codec.js +0 -1
- package/dist/streaming/codec.js.map +1 -1
- package/dist/streaming/fmp4Segmenter.d.ts +2 -2
- package/dist/streaming/fmp4Segmenter.js +12 -12
- package/dist/streaming/hls.d.ts +7 -7
- package/dist/streaming/hls.js +104 -92
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsResume.d.ts +3 -3
- package/dist/streaming/hlsResume.js +6 -6
- package/dist/streaming/hlsResume.js.map +1 -1
- package/dist/streaming/hlsSegments.d.ts +5 -2
- package/dist/streaming/hlsSegments.js +14 -9
- package/dist/streaming/hlsSegments.js.map +1 -1
- package/dist/streaming/monitor.d.ts +12 -12
- package/dist/streaming/monitor.js +98 -66
- package/dist/streaming/monitor.js.map +1 -1
- package/dist/streaming/mp4Parser.d.ts +34 -4
- package/dist/streaming/mp4Parser.js +111 -29
- package/dist/streaming/mp4Parser.js.map +1 -1
- package/dist/streaming/mpegts.d.ts +1 -1
- package/dist/streaming/mpegts.js +13 -10
- package/dist/streaming/mpegts.js.map +1 -1
- package/dist/streaming/playlistBuilder.d.ts +1 -1
- package/dist/streaming/playlistBuilder.js +2 -2
- package/dist/streaming/preroll.d.ts +9 -9
- package/dist/streaming/preroll.js +40 -42
- package/dist/streaming/preroll.js.map +1 -1
- package/dist/streaming/pretune.js +4 -4
- package/dist/streaming/pretune.js.map +1 -1
- package/dist/streaming/recovery.d.ts +3 -3
- package/dist/streaming/recovery.js +4 -4
- package/dist/streaming/registry.d.ts +6 -6
- package/dist/streaming/registry.js +4 -8
- package/dist/streaming/registry.js.map +1 -1
- package/dist/streaming/setup.d.ts +11 -10
- package/dist/streaming/setup.js +120 -71
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.d.ts +9 -3
- package/dist/streaming/showInfo.js +33 -27
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +8 -7
- package/dist/streaming/statusEmitter.js +5 -5
- package/dist/streaming/statusEmitter.js.map +1 -1
- package/dist/types/channels.d.ts +139 -36
- package/dist/types/channels.js +39 -1
- package/dist/types/channels.js.map +1 -1
- package/dist/types/config.d.ts +22 -3
- package/dist/types/index.d.ts +9 -8
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/profiles.d.ts +14 -13
- package/dist/types/selection.d.ts +49 -5
- package/dist/types/shared.d.ts +1 -1
- package/dist/types/streaming.d.ts +2 -2
- package/dist/upgrade/commands.context.d.ts +6 -0
- package/dist/upgrade/commands.context.js +29 -0
- package/dist/upgrade/commands.context.js.map +1 -0
- package/dist/upgrade/commands.d.ts +29 -2
- package/dist/upgrade/commands.js +87 -134
- package/dist/upgrade/commands.js.map +1 -1
- package/dist/upgrade/detection.context.d.ts +6 -0
- package/dist/upgrade/detection.context.js +24 -0
- package/dist/upgrade/detection.context.js.map +1 -0
- package/dist/upgrade/detection.d.ts +140 -10
- package/dist/upgrade/detection.js +153 -66
- package/dist/upgrade/detection.js.map +1 -1
- package/dist/upgrade/index.d.ts +2 -2
- package/dist/utils/chromeFetch.d.ts +1 -1
- package/dist/utils/chromeFetch.js +2 -2
- package/dist/utils/clock.d.ts +14 -0
- package/dist/utils/clock.js +28 -0
- package/dist/utils/clock.js.map +1 -0
- package/dist/utils/debugFilter.d.ts +1 -1
- package/dist/utils/debugFilter.js +8 -6
- package/dist/utils/debugFilter.js.map +1 -1
- package/dist/utils/delay.d.ts +2 -3
- package/dist/utils/delay.js +10 -16
- package/dist/utils/delay.js.map +1 -1
- package/dist/utils/evaluate.js +12 -14
- package/dist/utils/evaluate.js.map +1 -1
- package/dist/utils/ffmpeg.context.d.ts +6 -0
- package/dist/utils/ffmpeg.context.js +31 -0
- package/dist/utils/ffmpeg.context.js.map +1 -0
- package/dist/utils/ffmpeg.d.ts +85 -29
- package/dist/utils/ffmpeg.js +126 -129
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/fileLogger.d.ts +18 -2
- package/dist/utils/fileLogger.js +60 -31
- package/dist/utils/fileLogger.js.map +1 -1
- package/dist/utils/format.d.ts +7 -1
- package/dist/utils/format.js +22 -2
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/html.js +1 -3
- package/dist/utils/html.js.map +1 -1
- package/dist/utils/index.d.ts +21 -20
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.js +31 -45
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/m3u.d.ts +18 -3
- package/dist/utils/m3u.js +28 -14
- package/dist/utils/m3u.js.map +1 -1
- package/dist/utils/memo.d.ts +19 -0
- package/dist/utils/memo.js +53 -0
- package/dist/utils/memo.js.map +1 -0
- package/dist/utils/morganStream.js +4 -4
- package/dist/utils/morganStream.js.map +1 -1
- package/dist/utils/pid.d.ts +2 -2
- package/dist/utils/pid.js +2 -2
- package/dist/utils/platform.d.ts +1 -1
- package/dist/utils/platform.js +3 -2
- package/dist/utils/platform.js.map +1 -1
- package/dist/utils/retry.d.ts +2 -0
- package/dist/utils/retry.js +9 -7
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/sanitize.d.ts +2 -2
- package/dist/utils/sanitize.js +4 -4
- package/dist/utils/streamContext.d.ts +7 -0
- package/dist/utils/streamContext.js +10 -1
- package/dist/utils/streamContext.js.map +1 -1
- package/dist/utils/timing.d.ts +6 -3
- package/dist/utils/timing.js +10 -6
- package/dist/utils/timing.js.map +1 -1
- package/dist/utils/version.d.ts +3 -2
- package/dist/utils/version.js +15 -28
- package/dist/utils/version.js.map +1 -1
- package/package.json +14 -10
- package/dist/config/providerPacks.d.ts +0 -46
- package/dist/config/providerPacks.js.map +0 -1
- package/dist/config/providers.d.ts +0 -175
- package/dist/config/providers.js +0 -691
- package/dist/config/providers.js.map +0 -1
- package/dist/native/intercept.d.ts +0 -35
- package/dist/native/intercept.js +0 -184
- package/dist/native/intercept.js.map +0 -1
- package/dist/routes/config/channels/routes.js +0 -1477
- package/dist/routes/config/channels/routes.js.map +0 -1
- package/dist/routes/config/providers.js.map +0 -1
- package/dist/routes/providers.d.ts +0 -6
- package/dist/routes/providers.js.map +0 -1
- /package/dist/routes/config/{providers.d.ts → services.d.ts} +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { createFileStore } from "./persistence.js";
|
|
2
|
+
import { LOG } from "../utils/index.js";
|
|
3
|
+
import { getConfigFilePath } from "./paths.js";
|
|
4
4
|
import { getValidPresetIds } from "./presets.js";
|
|
5
|
-
|
|
5
|
+
import { isDeepStrictEqual } from "node:util";
|
|
6
6
|
/**
|
|
7
7
|
* Metadata for all configurable settings, organized by category.
|
|
8
8
|
*/
|
|
@@ -31,16 +31,28 @@ export const CONFIG_METADATA = {
|
|
|
31
31
|
],
|
|
32
32
|
channels: [
|
|
33
33
|
{
|
|
34
|
-
description: "Speed up your first channel tune by loading
|
|
35
|
-
"first tune, which may add a few extra seconds. Enabling precaching fetches lineups at startup so even your very first tune is fast.
|
|
36
|
-
"enabled in the Channels tab
|
|
34
|
+
description: "Speed up your first channel tune by loading service lineups when PrismCast starts. Normally, each service's lineup is loaded on your " +
|
|
35
|
+
"first tune, which may add a few extra seconds. Enabling precaching fetches lineups at startup so even your very first tune is fast. Services not " +
|
|
36
|
+
"enabled in the Channels tab service filter are skipped at startup. Ensure you've logged into each service before enabling.",
|
|
37
37
|
envVar: null,
|
|
38
38
|
label: "Channel Lineup Precaching",
|
|
39
39
|
listItemsKey: "providerModules",
|
|
40
|
-
path: "channels.
|
|
40
|
+
path: "channels.precacheServices",
|
|
41
41
|
type: "checkboxList"
|
|
42
42
|
}
|
|
43
43
|
],
|
|
44
|
+
channelsDvr: [
|
|
45
|
+
{
|
|
46
|
+
description: "TCP port for the user's Channels DVR API. PrismCast polls Channels DVR over HTTP for show-info, device-mapping discovery, and pretune " +
|
|
47
|
+
"scheduling. Defaults to 8089, which is the canonical Channels DVR port. Override only when the user has changed the DVR's listen port from its default.",
|
|
48
|
+
envVar: "CHANNELS_DVR_PORT",
|
|
49
|
+
label: "Channels DVR Port",
|
|
50
|
+
max: 65535,
|
|
51
|
+
min: 1,
|
|
52
|
+
path: "channelsDvr.port",
|
|
53
|
+
type: "port"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
44
56
|
hdhr: [
|
|
45
57
|
{
|
|
46
58
|
description: "Enable HDHomeRun emulation for Plex integration. When enabled, PrismCast runs a second HTTP server that emulates an HDHomeRun tuner, " +
|
|
@@ -467,49 +479,164 @@ export const CONFIG_METADATA = {
|
|
|
467
479
|
]
|
|
468
480
|
};
|
|
469
481
|
/* The config file path is resolved via the centralized paths module (config/paths.ts). The data directory is initialized at startup before config loading.
|
|
482
|
+
* Configuration persistence uses a transactional file store that provides atomic writes, serialized mutations, corruption protection, and backup rotation.
|
|
483
|
+
* All config modifications go through mutateConfig(), which prevents the class of bugs where a corrupt file gets silently overwritten with nearly-empty data.
|
|
484
|
+
*/
|
|
485
|
+
/* Current schema version for config.json. Migrations are declared in configMigrations below; the framework runs them in order from the file's stored version
|
|
486
|
+
* up to this constant, stamps the new version after each, and records the audit trail in migrationsApplied.
|
|
487
|
+
*
|
|
488
|
+
* Version history:
|
|
489
|
+
* 1 - Original. Provider-themed channel field names ("enabledProviders", "precacheProviders") and "foxcom" service tag still present.
|
|
490
|
+
* 2 - Service-themed naming. Renames "enabledProviders" -> "enabledServices", "precacheProviders" -> "precacheServices", and "foxcom" -> "foxone" inside
|
|
491
|
+
* the channels.enabledServices array. Companion to channels.json v3 which renames foxcom in channel keys and selections.
|
|
492
|
+
* 3 - DVR connection namespace. Moves the top-level `dvrHost` field into `channelsDvr.host`, splitting any legacy `host:port` value so the host portion
|
|
493
|
+
* lands at `channelsDvr.host` (host-only) and the port portion lands at `channelsDvr.port` only when the user has not already customized the port.
|
|
470
494
|
*/
|
|
471
|
-
|
|
495
|
+
const CURRENT_CONFIG_SCHEMA_VERSION = 3;
|
|
496
|
+
/* Declarative schema migrations. The file store framework runs these in order from the file's stored schemaVersion up to CURRENT_CONFIG_SCHEMA_VERSION,
|
|
497
|
+
* stamping the new version and recording the description in migrationsApplied after each application. Apply functions mutate the data in place.
|
|
472
498
|
*/
|
|
499
|
+
const configMigrations = {
|
|
500
|
+
2: {
|
|
501
|
+
apply: applyChannelsProviderRenameMigration,
|
|
502
|
+
description: "Rename legacy provider-themed channel field names and foxcom service tag to foxone"
|
|
503
|
+
},
|
|
504
|
+
3: {
|
|
505
|
+
apply: applyDvrHostNamespaceMigration,
|
|
506
|
+
description: "Move dvrHost into channelsDvr.host (split legacy host:port format)"
|
|
507
|
+
}
|
|
508
|
+
};
|
|
473
509
|
/**
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
510
|
+
* Renames the legacy provider-themed channel fields to their current service-themed names: `enabledProviders` -> `enabledServices`,
|
|
511
|
+
* `precacheProviders` -> `precacheServices`, and the `"foxcom"` tag -> `"foxone"` inside `enabledServices`. When both legacy and current names coexist on disk
|
|
512
|
+
* (a rare hand-edited collision), the current name wins and the legacy key is deleted - operator intent on the new name takes precedence over the auto-rename.
|
|
513
|
+
*
|
|
514
|
+
* Behavioral contract: this is a pure transformation on the on-disk shape. Configs without a `channels` block are left untouched (early return); configs that
|
|
515
|
+
* already have the v2 shape (no legacy keys present) are unchanged at the field level (idempotent), though the foxcom-to-foxone map runs unconditionally over
|
|
516
|
+
* the present `enabledServices` array because that operation is itself a no-op for already-migrated tags.
|
|
517
|
+
*
|
|
518
|
+
* Exported for unit-test coverage of the rename cases (each rename, collision wins, foxcom remap, no-channels early return). Production callers reach this
|
|
519
|
+
* only through the schema migration runner, never directly.
|
|
520
|
+
*
|
|
521
|
+
* @param data - The pre-migration UserConfig data, mutated in place.
|
|
477
522
|
*/
|
|
478
|
-
export
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
523
|
+
export function applyChannelsProviderRenameMigration(data) {
|
|
524
|
+
// Cast to a permissive shape because the legacy provider-themed keys are not declared on UserChannelsConfig - they only exist on older on-disk files.
|
|
525
|
+
const channels = data.channels;
|
|
526
|
+
if (!channels) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
// Rename enabledProviders -> enabledServices. If both are present (rare hand-edited case) the current name wins.
|
|
530
|
+
if (Array.isArray(channels["enabledProviders"])) {
|
|
531
|
+
if (!Array.isArray(channels["enabledServices"])) {
|
|
532
|
+
channels["enabledServices"] = channels["enabledProviders"];
|
|
533
|
+
}
|
|
534
|
+
delete channels["enabledProviders"];
|
|
535
|
+
}
|
|
536
|
+
// Rename precacheProviders -> precacheServices.
|
|
537
|
+
if (Array.isArray(channels["precacheProviders"])) {
|
|
538
|
+
if (!Array.isArray(channels["precacheServices"])) {
|
|
539
|
+
channels["precacheServices"] = channels["precacheProviders"];
|
|
489
540
|
}
|
|
541
|
+
delete channels["precacheProviders"];
|
|
490
542
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
return { config: {}, parseError: false };
|
|
495
|
-
}
|
|
496
|
-
// Other read errors - log and use defaults.
|
|
497
|
-
LOG.warn("Failed to read configuration file %s: %s. Using defaults.", getConfigFilePath(), (error instanceof Error) ? error.message : String(error));
|
|
498
|
-
return { config: {}, parseError: false };
|
|
543
|
+
// Rename "foxcom" -> "foxone" inside the enabledServices service-tag filter.
|
|
544
|
+
if (Array.isArray(channels["enabledServices"])) {
|
|
545
|
+
channels["enabledServices"] = channels["enabledServices"].map((tag) => (tag === "foxcom") ? "foxone" : tag);
|
|
499
546
|
}
|
|
500
547
|
}
|
|
501
548
|
/**
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
549
|
+
* Splits a legacy `dvrHost` value into the new `channelsDvr.host` / `channelsDvr.port` namespace. The host portion is host-only by invariant; legacy values
|
|
550
|
+
* carrying an embedded port (e.g., `192.168.1.5:8089`) are split so the host lands at `channelsDvr.host` and the port lands at `channelsDvr.port` IFF the
|
|
551
|
+
* user has not already customized the port. When both an embedded port and an explicit user-set port exist, the explicit port wins (a warning is logged) and
|
|
552
|
+
* the embedded port is discarded - the user's deliberate choice takes precedence over implicit legacy data.
|
|
553
|
+
*
|
|
554
|
+
* Splits at the LAST colon so bracket-wrapped IPv6 forms like `[::1]:8089` survive (the production setDvrHost invariant rejects bare-colon values, but disk
|
|
555
|
+
* files may carry hand-edited content the framework cannot vet). When the trailing portion does not parse as a valid port number (1..65535), the entire input
|
|
556
|
+
* is treated as a host-only value rather than fabricating a bogus port.
|
|
557
|
+
*
|
|
558
|
+
* Exported for unit-test coverage of the splitting cases (host-only, host+default-port, host+non-default-port collision). Production callers reach this only
|
|
559
|
+
* through the schema migration runner, never directly.
|
|
560
|
+
*
|
|
561
|
+
* @param data - The pre-migration UserConfig data, mutated in place.
|
|
505
562
|
*/
|
|
506
|
-
export
|
|
507
|
-
//
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
563
|
+
export function applyDvrHostNamespaceMigration(data) {
|
|
564
|
+
// The legacy field is not declared on the current UserConfig; we read it through a permissive cast to access the on-disk pre-migration shape. After migration
|
|
565
|
+
// the field is deleted so it cannot leak into the post-migration write path.
|
|
566
|
+
const legacy = data;
|
|
567
|
+
const dvrHost = legacy.dvrHost;
|
|
568
|
+
if (typeof dvrHost !== "string") {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
let host = dvrHost;
|
|
572
|
+
let embeddedPort;
|
|
573
|
+
// Split at the last colon so bracket-wrapped IPv6 (e.g., [::1]:8089) survives. If the trailing portion does not parse as a valid TCP port, treat the whole
|
|
574
|
+
// input as host-only - avoid fabricating a port from a malformed string.
|
|
575
|
+
const colonIdx = dvrHost.lastIndexOf(":");
|
|
576
|
+
if (colonIdx >= 0) {
|
|
577
|
+
const tail = dvrHost.slice(colonIdx + 1);
|
|
578
|
+
const parsed = Number(tail);
|
|
579
|
+
if (Number.isInteger(parsed) && (parsed >= 1) && (parsed <= 65535)) {
|
|
580
|
+
host = dvrHost.slice(0, colonIdx);
|
|
581
|
+
embeddedPort = parsed;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
data.channelsDvr ??= {};
|
|
585
|
+
data.channelsDvr.host = host;
|
|
586
|
+
// Migrate the embedded port only when the user has not customized the port already. A user-set port reflects an explicit choice that overrides whatever the
|
|
587
|
+
// legacy host:port string encoded - prefer the explicit value and log the conflict so it is auditable.
|
|
588
|
+
if (embeddedPort !== undefined) {
|
|
589
|
+
const userPort = data.channelsDvr.port;
|
|
590
|
+
if (userPort === undefined) {
|
|
591
|
+
data.channelsDvr.port = embeddedPort;
|
|
592
|
+
}
|
|
593
|
+
else if (userPort !== embeddedPort) {
|
|
594
|
+
LOG.warn("Schema migration v3: legacy dvrHost \"%s\" carried embedded port %d but channelsDvr.port is already set to %d. " +
|
|
595
|
+
"Keeping the explicit port; embedded port discarded.", dvrHost, embeddedPort, userPort);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
delete legacy.dvrHost;
|
|
599
|
+
}
|
|
600
|
+
// Transactional store instance for config.json. The beforeWrite hook is the single chokepoint where the persisted shape is normalized: filterDefaults() runs on
|
|
601
|
+
// every save so the file on disk contains only non-default values, regardless of which call site initiated the write. Schema migrations run automatically via
|
|
602
|
+
// the file store framework's migration runner before the data reaches mergeConfiguration.
|
|
603
|
+
const configStore = createFileStore({
|
|
604
|
+
beforeWrite: (data) => filterDefaults(data),
|
|
605
|
+
currentSchemaVersion: CURRENT_CONFIG_SCHEMA_VERSION,
|
|
606
|
+
defaultValue: () => ({ schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION }),
|
|
607
|
+
getSchemaVersion: (data) => data.schemaVersion ?? 1,
|
|
608
|
+
label: "configuration",
|
|
609
|
+
migrations: configMigrations,
|
|
610
|
+
parse: (raw) => JSON.parse(raw),
|
|
611
|
+
path: getConfigFilePath,
|
|
612
|
+
recordMigration: (data, description) => {
|
|
613
|
+
data.migrationsApplied ??= [];
|
|
614
|
+
data.migrationsApplied.push(description);
|
|
615
|
+
},
|
|
616
|
+
setSchemaVersion: (data, version) => { data.schemaVersion = version; }
|
|
617
|
+
});
|
|
618
|
+
/**
|
|
619
|
+
* Reads the current configuration from disk without acquiring the serialization lock. Returns the parsed (and migrated) config with parse status. Use this for
|
|
620
|
+
* read-only access (export endpoints, startup initialization). For modifications, use mutateConfig() instead.
|
|
621
|
+
* @returns The loaded configuration with parse status.
|
|
622
|
+
*/
|
|
623
|
+
export async function readConfig() {
|
|
624
|
+
const result = await configStore.read();
|
|
625
|
+
return {
|
|
626
|
+
config: result.data,
|
|
627
|
+
parseError: result.parseError,
|
|
628
|
+
parseErrorMessage: result.parseErrorMessage,
|
|
629
|
+
recoveredFromBackup: result.recoveredFromBackup
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Serialized read-modify-write operation on config.json. The mutation function receives the current config (already migrated to the latest schema version) and
|
|
634
|
+
* modifies it in place. The store handles atomicity, serialization, corruption guard, backup, schema migration, and filterDefaults via the framework.
|
|
635
|
+
* @param fn - Mutation function. Receives current config. Modify in place; return value is ignored.
|
|
636
|
+
* @throws FileStoreParseError if config.json contains invalid JSON and no usable backup exists.
|
|
637
|
+
*/
|
|
638
|
+
export async function mutateConfig(fn) {
|
|
639
|
+
await configStore.mutate(fn);
|
|
513
640
|
}
|
|
514
641
|
/* These functions detect which settings are overridden by environment variables, so the UI can disable those fields and show appropriate warnings.
|
|
515
642
|
*/
|
|
@@ -543,11 +670,15 @@ export const DEFAULTS = {
|
|
|
543
670
|
channelSortDirection: "asc",
|
|
544
671
|
channelSortField: "name",
|
|
545
672
|
disabledPredefined: [],
|
|
546
|
-
|
|
547
|
-
|
|
673
|
+
enabledServices: [],
|
|
674
|
+
precacheServices: [],
|
|
548
675
|
setupCompleted: false,
|
|
549
676
|
visibleColumns: []
|
|
550
677
|
},
|
|
678
|
+
channelsDvr: {
|
|
679
|
+
host: "",
|
|
680
|
+
port: 8089
|
|
681
|
+
},
|
|
551
682
|
hdhr: {
|
|
552
683
|
deviceId: "",
|
|
553
684
|
enabled: true,
|
|
@@ -638,7 +769,7 @@ function parseEnvValue(value, type) {
|
|
|
638
769
|
return value.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
|
|
639
770
|
}
|
|
640
771
|
case "path": {
|
|
641
|
-
// An empty path env var means "use default"
|
|
772
|
+
// An empty path env var means "use default" - return null so the downstream code sees the same sentinel as an unset config field.
|
|
642
773
|
return (value.trim() === "") ? null : value;
|
|
643
774
|
}
|
|
644
775
|
default: {
|
|
@@ -672,14 +803,11 @@ export function getNestedValue(obj, settingPath) {
|
|
|
672
803
|
export function setNestedValue(obj, settingPath, value) {
|
|
673
804
|
const parts = settingPath.split(".");
|
|
674
805
|
let current = obj;
|
|
675
|
-
for (
|
|
676
|
-
|
|
677
|
-
if (current[part] === undefined) {
|
|
678
|
-
current[part] = {};
|
|
679
|
-
}
|
|
806
|
+
for (const part of parts.slice(0, -1)) {
|
|
807
|
+
current[part] ??= {};
|
|
680
808
|
current = current[part];
|
|
681
809
|
}
|
|
682
|
-
current[parts
|
|
810
|
+
current[parts.at(-1) ?? ""] = value;
|
|
683
811
|
}
|
|
684
812
|
/**
|
|
685
813
|
* Merges user configuration with defaults, environment overrides, and CLI overrides to produce the final configuration.
|
|
@@ -700,39 +828,18 @@ export function mergeConfiguration(userConfig, cliOverrides) {
|
|
|
700
828
|
}
|
|
701
829
|
}
|
|
702
830
|
}
|
|
703
|
-
/*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
831
|
+
/* Hydrate fields that live outside CONFIG_METADATA (auto-discovery results like channelsDvr.host, separately-managed lists like channels.disabledPredefined,
|
|
832
|
+
* the persisted debug filter pattern) and array fields whose CONFIG_METADATA hydration would alias the parsed UserConfig blob into runtime CONFIG. The
|
|
833
|
+
* registry pairs each path with a predicate (ignored when undefined) and an optional defensive-copy hook. Adding a new field here is one HYDRATED_FIELDS
|
|
834
|
+
* entry; the drift-check test in userConfig.merge.test.ts asserts that PRESERVED_FIELDS partitions exactly into HYDRATED_FIELDS plus PERSISTENCE_ONLY_FIELDS,
|
|
835
|
+
* so a future preservation entry cannot be added without an explicit hydration classification. See the section comment above HYDRATED_FIELDS for the full
|
|
836
|
+
* design intent.
|
|
709
837
|
*/
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
}
|
|
716
|
-
if (Array.isArray(userConfig.channels?.precacheProviders)) {
|
|
717
|
-
config.channels.precacheProviders = [...userConfig.channels.precacheProviders];
|
|
718
|
-
}
|
|
719
|
-
if (Array.isArray(userConfig.channels?.visibleColumns)) {
|
|
720
|
-
config.channels.visibleColumns = [...userConfig.channels.visibleColumns];
|
|
721
|
-
}
|
|
722
|
-
if (Array.isArray(userConfig.streaming?.captureCodecs)) {
|
|
723
|
-
config.streaming.captureCodecs = [...userConfig.streaming.captureCodecs];
|
|
724
|
-
}
|
|
725
|
-
if ((typeof userConfig.channels?.channelSortField === "string") && (userConfig.channels.channelSortField.length > 0)) {
|
|
726
|
-
config.channels.channelSortField = userConfig.channels.channelSortField;
|
|
727
|
-
}
|
|
728
|
-
if ((typeof userConfig.channels?.channelSortDirection === "string") && (userConfig.channels.channelSortDirection.length > 0)) {
|
|
729
|
-
config.channels.channelSortDirection = userConfig.channels.channelSortDirection;
|
|
730
|
-
}
|
|
731
|
-
if ((typeof userConfig.hdhr?.deviceId === "string") && (userConfig.hdhr.deviceId.length > 0)) {
|
|
732
|
-
config.hdhr.deviceId = userConfig.hdhr.deviceId;
|
|
733
|
-
}
|
|
734
|
-
if ((typeof userConfig.logging?.debugFilter === "string") && (userConfig.logging.debugFilter.length > 0)) {
|
|
735
|
-
config.logging.debugFilter = userConfig.logging.debugFilter;
|
|
838
|
+
for (const field of HYDRATED_FIELDS) {
|
|
839
|
+
const userValue = getNestedValue(userConfig, field.path);
|
|
840
|
+
if (field.shouldHydrate(userValue, undefined)) {
|
|
841
|
+
setNestedValue(config, field.path, field.copy ? field.copy(userValue) : userValue);
|
|
842
|
+
}
|
|
736
843
|
}
|
|
737
844
|
// Apply environment variable overrides.
|
|
738
845
|
for (const settings of Object.values(CONFIG_METADATA)) {
|
|
@@ -773,7 +880,7 @@ const SETTINGS_TAB_SECTIONS = [
|
|
|
773
880
|
{
|
|
774
881
|
displayName: "Startup",
|
|
775
882
|
id: "startup",
|
|
776
|
-
paths: ["channels.
|
|
883
|
+
paths: ["channels.precacheServices"]
|
|
777
884
|
},
|
|
778
885
|
{
|
|
779
886
|
displayName: "Capture",
|
|
@@ -790,6 +897,7 @@ const SETTINGS_TAB_SECTIONS = [
|
|
|
790
897
|
/* Display metadata for Advanced tab sections. The category field must match a key in CONFIG_METADATA. Entries are sorted alphabetically by category.
|
|
791
898
|
*/
|
|
792
899
|
const ADVANCED_SECTION_META = [
|
|
900
|
+
{ category: "channelsDvr", displayName: "Channels DVR" },
|
|
793
901
|
{ category: "hls", displayName: "HLS" },
|
|
794
902
|
{ category: "logging", displayName: "Logging" },
|
|
795
903
|
{ category: "paths", displayName: "Paths" },
|
|
@@ -865,25 +973,20 @@ export function getAdvancedSections() {
|
|
|
865
973
|
const settingsTabPaths = SETTINGS_TAB_SECTIONS.flatMap((s) => s.paths);
|
|
866
974
|
// Get all paths that belong in Advanced (not in Settings).
|
|
867
975
|
const advancedPaths = getAllSettingPaths().filter((p) => !settingsTabPaths.includes(p));
|
|
868
|
-
//
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
byCategory.set(category, []);
|
|
876
|
-
}
|
|
877
|
-
byCategory.get(category)?.push(setting);
|
|
878
|
-
}
|
|
879
|
-
}
|
|
976
|
+
// Pair every advanced path with its resolved setting and category prefix, then drop entries whose category is empty or whose setting lookup failed. The
|
|
977
|
+
// surviving pairs feed Map.groupBy below for the actual grouping.
|
|
978
|
+
const settingsByPath = advancedPaths
|
|
979
|
+
.map((path) => ({ category: path.split(".")[0] ?? "", setting: getSettingByPath(path) }))
|
|
980
|
+
.filter((entry) => Boolean(entry.category && entry.setting));
|
|
981
|
+
// Group by category (first path segment) using the standard ESNext.Collection grouper.
|
|
982
|
+
const byCategory = Map.groupBy(settingsByPath, (entry) => entry.category);
|
|
880
983
|
// Return sections in the defined order with display names.
|
|
881
984
|
return ADVANCED_SECTION_META
|
|
882
985
|
.filter((meta) => byCategory.has(meta.category))
|
|
883
986
|
.map((meta) => ({
|
|
884
987
|
displayName: meta.displayName,
|
|
885
988
|
id: meta.category,
|
|
886
|
-
settings: byCategory.get(meta.category) ?? []
|
|
989
|
+
settings: byCategory.get(meta.category)?.map((entry) => entry.setting) ?? []
|
|
887
990
|
}));
|
|
888
991
|
}
|
|
889
992
|
/* When saving user configuration, we only want to persist values that differ from defaults. This keeps the config file clean and makes it easy to see what the user has
|
|
@@ -929,8 +1032,84 @@ export function isEqualToDefault(value, defaultValue) {
|
|
|
929
1032
|
return false;
|
|
930
1033
|
}
|
|
931
1034
|
// Compare as strings for consistent comparison across types (handles number/string coercion). Config values are always primitives.
|
|
932
|
-
|
|
1035
|
+
const primitive = value;
|
|
1036
|
+
const defaultPrimitive = defaultValue;
|
|
1037
|
+
return String(primitive) === String(defaultPrimitive);
|
|
933
1038
|
}
|
|
1039
|
+
// Predicate: any array, including empty. Used by HYDRATED_FIELDS to mirror the historical mergeConfiguration() inline-block behavior of unconditionally
|
|
1040
|
+
// hydrating any array-typed value present on disk; downstream validateConfiguration() normalizes empty-array edge cases (e.g., captureCodecs forces h264).
|
|
1041
|
+
const isArrayValue = (value) => Array.isArray(value);
|
|
1042
|
+
// Predicate: any non-empty array. Used for fields where the user's empty-array state coincides with the default-empty-array state (disabledPredefined,
|
|
1043
|
+
// enabledServices, precacheServices, visibleColumns, migrationsApplied). The defaultValue argument is intentionally unused.
|
|
1044
|
+
const isNonEmptyArray = (value) => Array.isArray(value) && (value.length > 0);
|
|
1045
|
+
// Predicate: any non-empty string. Used for fields whose default is the empty string and whose presence-on-disk should follow the same rule (hdhr.deviceId,
|
|
1046
|
+
// logging.debugFilter, channelsDvr.host).
|
|
1047
|
+
const isNonEmptyString = (value) => (typeof value === "string") && (value.length > 0);
|
|
1048
|
+
// Predicate: any number. Used for schemaVersion - the framework-managed integer that any non-undefined value must round-trip through filterDefaults.
|
|
1049
|
+
// Predicate: any number including NaN. Number.isFinite() would tighten the contract if NaN must be rejected.
|
|
1050
|
+
const isNumber = (value) => typeof value === "number";
|
|
1051
|
+
// Predicate: a string that differs from the default by simple equality. Used for channelSortField / channelSortDirection - both have meaningful default
|
|
1052
|
+
// values ("name", "asc") that should be stripped on save while any other valid string is preserved.
|
|
1053
|
+
const differsFromStringDefault = (value, defaultValue) => (typeof value === "string") && (value !== defaultValue);
|
|
1054
|
+
// Predicate: an array whose contents differ from the default by SORTED-element comparison. Used for streaming.captureCodecs - the default ["h264", "hevc"]
|
|
1055
|
+
// reordered by the user (["hevc", "h264"]) is semantically the same configuration and should not write to disk; only a different SET of codecs counts as a
|
|
1056
|
+
// customization worth preserving.
|
|
1057
|
+
const differsFromSortedArrayDefault = (value, defaultValue) => {
|
|
1058
|
+
return Array.isArray(value) && Array.isArray(defaultValue) && !isDeepStrictEqual(value.toSorted(), defaultValue.toSorted());
|
|
1059
|
+
};
|
|
1060
|
+
/**
|
|
1061
|
+
* Allowlist of config paths that filterDefaults() preserves outside the CONFIG_METADATA-driven loop. See the section comment above for the two distinct
|
|
1062
|
+
* reasons a path lives here rather than in CONFIG_METADATA. The list is alphabetized by path so a future maintainer can locate any entry by paging through
|
|
1063
|
+
* the registry; ordering does not affect runtime semantics because each entry's preserve check is independent.
|
|
1064
|
+
*/
|
|
1065
|
+
export const PRESERVED_FIELDS = [
|
|
1066
|
+
{ path: "channels.channelSortDirection", shouldPreserve: differsFromStringDefault },
|
|
1067
|
+
{ path: "channels.channelSortField", shouldPreserve: differsFromStringDefault },
|
|
1068
|
+
{ path: "channels.disabledPredefined", shouldPreserve: isNonEmptyArray },
|
|
1069
|
+
{ path: "channels.enabledServices", shouldPreserve: isNonEmptyArray },
|
|
1070
|
+
{ path: "channels.precacheServices", shouldPreserve: isNonEmptyArray },
|
|
1071
|
+
{ path: "channels.visibleColumns", shouldPreserve: isNonEmptyArray },
|
|
1072
|
+
{ path: "channelsDvr.host", shouldPreserve: isNonEmptyString },
|
|
1073
|
+
{ path: "hdhr.deviceId", shouldPreserve: isNonEmptyString },
|
|
1074
|
+
{ path: "logging.debugFilter", shouldPreserve: isNonEmptyString },
|
|
1075
|
+
{ path: "migrationsApplied", shouldPreserve: isNonEmptyArray },
|
|
1076
|
+
{ path: "schemaVersion", shouldPreserve: isNumber },
|
|
1077
|
+
{ path: "streaming.captureCodecs", shouldPreserve: differsFromSortedArrayDefault }
|
|
1078
|
+
];
|
|
1079
|
+
// Defensive spread copy for array fields. Pulled out as a const so the registry below reads as a declarative table.
|
|
1080
|
+
const spreadArray = (value) => [...value];
|
|
1081
|
+
/**
|
|
1082
|
+
* Allowlist of config paths that mergeConfiguration() hydrates from the persisted UserConfig into the runtime CONFIG outside the CONFIG_METADATA-driven loop.
|
|
1083
|
+
* Each entry corresponds to a PRESERVED_FIELDS entry that lives on the runtime Config type (the persistence-only entries schemaVersion / migrationsApplied are
|
|
1084
|
+
* declared in PERSISTENCE_ONLY_FIELDS instead). The list is alphabetized by path; ordering does not affect runtime semantics. A drift-check test in
|
|
1085
|
+
* userConfig.merge.test.ts asserts that PRESERVED_FIELDS partitions exactly into HYDRATED_FIELDS plus PERSISTENCE_ONLY_FIELDS, so a future preservation entry
|
|
1086
|
+
* cannot be added without explicitly classifying it as runtime-hydrated or persistence-only.
|
|
1087
|
+
*/
|
|
1088
|
+
export const HYDRATED_FIELDS = [
|
|
1089
|
+
{ path: "channels.channelSortDirection", shouldHydrate: isNonEmptyString },
|
|
1090
|
+
{ path: "channels.channelSortField", shouldHydrate: isNonEmptyString },
|
|
1091
|
+
{ copy: spreadArray, path: "channels.disabledPredefined", shouldHydrate: isArrayValue },
|
|
1092
|
+
{ copy: spreadArray, path: "channels.enabledServices", shouldHydrate: isArrayValue },
|
|
1093
|
+
{ copy: spreadArray, path: "channels.precacheServices", shouldHydrate: isArrayValue },
|
|
1094
|
+
{ copy: spreadArray, path: "channels.visibleColumns", shouldHydrate: isArrayValue },
|
|
1095
|
+
{ path: "channelsDvr.host", shouldHydrate: isNonEmptyString },
|
|
1096
|
+
{ path: "hdhr.deviceId", shouldHydrate: isNonEmptyString },
|
|
1097
|
+
{ path: "logging.debugFilter", shouldHydrate: isNonEmptyString },
|
|
1098
|
+
{ copy: spreadArray, path: "streaming.captureCodecs", shouldHydrate: isArrayValue }
|
|
1099
|
+
];
|
|
1100
|
+
/**
|
|
1101
|
+
* Allowlist of PRESERVED_FIELDS entries that exist only on the persisted UserConfig shape and have NO runtime CONFIG counterpart. These are framework-managed
|
|
1102
|
+
* file-store metadata - the schema-migration runner owns schemaVersion (so the file-store framework can decide whether migrations have run) and migrationsApplied
|
|
1103
|
+
* (the audit trail of which migrations have applied). Neither belongs in runtime CONFIG; both must round-trip through filterDefaults so saves do not strip them.
|
|
1104
|
+
*
|
|
1105
|
+
* The drift-check test consumes this constant to assert that every PRESERVED_FIELDS entry is classified as either runtime-hydrated (HYDRATED_FIELDS) or
|
|
1106
|
+
* persistence-only (here), and that the two sets are disjoint. Adding a new PRESERVED_FIELDS entry without classifying it here or in HYDRATED_FIELDS fails the
|
|
1107
|
+
* drift check at test time, surfacing the architectural intent that every preserved field has a declared destination.
|
|
1108
|
+
*/
|
|
1109
|
+
export const PERSISTENCE_ONLY_FIELDS = [
|
|
1110
|
+
"migrationsApplied",
|
|
1111
|
+
"schemaVersion"
|
|
1112
|
+
];
|
|
934
1113
|
/**
|
|
935
1114
|
* Filters a user configuration object to remove values that match the defaults. This produces a minimal config file containing only the settings the user has actually
|
|
936
1115
|
* customized. Empty nested objects are also removed.
|
|
@@ -939,9 +1118,20 @@ export function isEqualToDefault(value, defaultValue) {
|
|
|
939
1118
|
*/
|
|
940
1119
|
export function filterDefaults(config) {
|
|
941
1120
|
const filtered = {};
|
|
942
|
-
//
|
|
1121
|
+
// Build the set of paths owned by PRESERVED_FIELDS so the metadata-driven loop can skip them. The PRESERVED_FIELDS predicate is the sole arbiter for those
|
|
1122
|
+
// paths because String() coercion in isEqualToDefault produces false positives for array-shaped values (e.g., a reordered captureCodecs list coerces to a
|
|
1123
|
+
// different comma-joined string than the default and would survive the loop, even when the predicate would correctly classify it as default-equal).
|
|
1124
|
+
// Skipping the loop for those paths and letting the predicate decide is the only way to make the predicate authoritative without restructuring either
|
|
1125
|
+
// registry. Paths in PRESERVED_FIELDS that are NOT in CONFIG_METADATA (channelsDvr.host, hdhr.deviceId, etc.) are unaffected - the loop never visited them
|
|
1126
|
+
// before either.
|
|
1127
|
+
const preservedPaths = new Set(PRESERVED_FIELDS.map((field) => field.path));
|
|
1128
|
+
// Iterate over all known settings and check if the value differs from the default. Paths managed by PRESERVED_FIELDS are skipped here so the predicate below
|
|
1129
|
+
// has unilateral control over their inclusion.
|
|
943
1130
|
for (const settings of Object.values(CONFIG_METADATA)) {
|
|
944
1131
|
for (const setting of settings) {
|
|
1132
|
+
if (preservedPaths.has(setting.path)) {
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
945
1135
|
const value = getNestedValue(config, setting.path);
|
|
946
1136
|
// Skip undefined values (setting not present in config).
|
|
947
1137
|
if (value === undefined) {
|
|
@@ -954,51 +1144,14 @@ export function filterDefaults(config) {
|
|
|
954
1144
|
}
|
|
955
1145
|
}
|
|
956
1146
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
const configEnabledProviders = getNestedValue(config, "channels.enabledProviders");
|
|
966
|
-
if (Array.isArray(configEnabledProviders) && (configEnabledProviders.length > 0)) {
|
|
967
|
-
setNestedValue(filtered, "channels.enabledProviders", configEnabledProviders);
|
|
968
|
-
}
|
|
969
|
-
const configPrecacheProviders = getNestedValue(config, "channels.precacheProviders");
|
|
970
|
-
if (Array.isArray(configPrecacheProviders) && (configPrecacheProviders.length > 0)) {
|
|
971
|
-
setNestedValue(filtered, "channels.precacheProviders", configPrecacheProviders);
|
|
972
|
-
}
|
|
973
|
-
const configVisibleColumns = getNestedValue(config, "channels.visibleColumns");
|
|
974
|
-
if (Array.isArray(configVisibleColumns) && (configVisibleColumns.length > 0)) {
|
|
975
|
-
setNestedValue(filtered, "channels.visibleColumns", configVisibleColumns);
|
|
976
|
-
}
|
|
977
|
-
// Preserve captureCodecs when it differs from the default. The default includes all recognized codecs, so a shorter list means the user has disabled one.
|
|
978
|
-
const configCaptureCodecs = getNestedValue(config, "streaming.captureCodecs");
|
|
979
|
-
const defaultCaptureCodecs = getNestedValue(DEFAULTS, "streaming.captureCodecs");
|
|
980
|
-
if (Array.isArray(configCaptureCodecs) && (JSON.stringify(configCaptureCodecs.slice().sort()) !== JSON.stringify(defaultCaptureCodecs.slice().sort()))) {
|
|
981
|
-
setNestedValue(filtered, "streaming.captureCodecs", configCaptureCodecs);
|
|
982
|
-
}
|
|
983
|
-
const configSortField = getNestedValue(config, "channels.channelSortField");
|
|
984
|
-
if ((typeof configSortField === "string") && (configSortField !== "name")) {
|
|
985
|
-
setNestedValue(filtered, "channels.channelSortField", configSortField);
|
|
986
|
-
}
|
|
987
|
-
const configSortDirection = getNestedValue(config, "channels.channelSortDirection");
|
|
988
|
-
if ((typeof configSortDirection === "string") && (configSortDirection !== "asc")) {
|
|
989
|
-
setNestedValue(filtered, "channels.channelSortDirection", configSortDirection);
|
|
990
|
-
}
|
|
991
|
-
const configDeviceId = getNestedValue(config, "hdhr.deviceId");
|
|
992
|
-
if ((typeof configDeviceId === "string") && (configDeviceId.length > 0)) {
|
|
993
|
-
setNestedValue(filtered, "hdhr.deviceId", configDeviceId);
|
|
994
|
-
}
|
|
995
|
-
const configDebugFilter = getNestedValue(config, "logging.debugFilter");
|
|
996
|
-
if ((typeof configDebugFilter === "string") && (configDebugFilter.length > 0)) {
|
|
997
|
-
setNestedValue(filtered, "logging.debugFilter", configDebugFilter);
|
|
998
|
-
}
|
|
999
|
-
// Preserve the auto-discovered DVR host across settings saves.
|
|
1000
|
-
if ((typeof config.dvrHost === "string") && (config.dvrHost.length > 0)) {
|
|
1001
|
-
filtered.dvrHost = config.dvrHost;
|
|
1147
|
+
// Apply the explicit preservation registry. Each entry is consulted with its current value plus the default-at-path; the predicate decides whether to
|
|
1148
|
+
// write through. See PRESERVED_FIELDS above for the entries and their rationale.
|
|
1149
|
+
for (const field of PRESERVED_FIELDS) {
|
|
1150
|
+
const value = getNestedValue(config, field.path);
|
|
1151
|
+
const defaultValue = getNestedValue(DEFAULTS, field.path);
|
|
1152
|
+
if (field.shouldPreserve(value, defaultValue)) {
|
|
1153
|
+
setNestedValue(filtered, field.path, value);
|
|
1154
|
+
}
|
|
1002
1155
|
}
|
|
1003
1156
|
// Remove any empty nested objects that resulted from filtering.
|
|
1004
1157
|
return removeEmptyObjects(filtered);
|