prismcast 1.9.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/dist/app.d.ts +1 -1
- package/dist/app.js +27 -9
- package/dist/app.js.map +1 -1
- package/dist/browser/cdp.js +7 -7
- 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 +42 -43
- 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.js +4 -4
- 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 +61 -62
- package/dist/browser/tuning/directv.js.map +1 -1
- package/dist/browser/tuning/fox.d.ts +1 -1
- package/dist/browser/tuning/fox.js +171 -14
- package/dist/browser/tuning/fox.js.map +1 -1
- package/dist/browser/tuning/hbo.d.ts +1 -1
- package/dist/browser/tuning/hbo.js +6 -6
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/tuning/hulu.d.ts +1 -1
- package/dist/browser/tuning/hulu.js +117 -110
- 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 +28 -28
- 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 +73 -40
- 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 +4 -3
- package/dist/config/health.js +75 -62
- package/dist/config/health.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +19 -12
- 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 +110 -12
- package/dist/config/persistence.js +351 -32
- package/dist/config/persistence.js.map +1 -1
- 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 +2 -2
- package/dist/config/servicePacks.js +28 -28
- package/dist/config/servicePacks.js.map +1 -1
- package/dist/config/services.d.ts +62 -36
- package/dist/config/services.js +150 -136
- package/dist/config/services.js.map +1 -1
- package/dist/config/sites.d.ts +3 -3
- package/dist/config/sites.js +16 -16
- package/dist/config/sites.js.map +1 -1
- package/dist/config/userChannels.d.ts +500 -41
- package/dist/config/userChannels.js +1201 -312
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +103 -10
- package/dist/config/userConfig.js +277 -117
- package/dist/config/userConfig.js.map +1 -1
- package/dist/config/userProfiles.d.ts +4 -2
- package/dist/config/userProfiles.js +72 -26
- package/dist/config/userProfiles.js.map +1 -1
- package/dist/hdhr/channelMap.js +8 -11
- 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 +6 -10
- 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 +42 -64
- package/dist/native/index.js.map +1 -1
- package/dist/native/probe.d.ts +27 -9
- package/dist/native/probe.js +158 -67
- package/dist/native/probe.js.map +1 -1
- package/dist/native/proxy.d.ts +4 -2
- package/dist/native/proxy.js +52 -49
- 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/auth.js +6 -10
- 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 +1 -1
- 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 +9 -9
- package/dist/routes/config/channels/table.js +124 -76
- 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 +2 -2
- package/dist/routes/config/index.js.map +1 -1
- package/dist/routes/config/services.js +69 -68
- package/dist/routes/config/services.js.map +1 -1
- package/dist/routes/config/settings.d.ts +1 -1
- package/dist/routes/config/settings.js +45 -38
- package/dist/routes/config/settings.js.map +1 -1
- package/dist/routes/debug.js +12 -11
- package/dist/routes/debug.js.map +1 -1
- package/dist/routes/icons.js +2 -2
- package/dist/routes/icons.js.map +1 -1
- package/dist/routes/logs.js +19 -30
- package/dist/routes/logs.js.map +1 -1
- package/dist/routes/mpegts.js +1 -1
- package/dist/routes/playlist.d.ts +1 -1
- package/dist/routes/playlist.js +37 -26
- package/dist/routes/playlist.js.map +1 -1
- package/dist/routes/root/content.js +27 -6
- 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 +16 -16
- package/dist/routes/root/scripts/channels.js.map +1 -1
- package/dist/routes/root/scripts/config.js +59 -59
- 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 +24 -10
- 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 +86 -488
- package/dist/routes/root/scripts/status.js.map +1 -1
- package/dist/routes/root/styles.js +5 -5
- package/dist/routes/services.js +18 -15
- package/dist/routes/services.js.map +1 -1
- 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/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 +171 -188
- 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 +499 -383
- 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 +3 -3
- package/dist/streaming/hls.js +81 -70
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsResume.d.ts +3 -3
- package/dist/streaming/hlsResume.js +5 -5
- 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 +80 -54
- 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 +39 -41
- 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/setup.d.ts +9 -8
- package/dist/streaming/setup.js +97 -49
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.d.ts +9 -3
- package/dist/streaming/showInfo.js +26 -18
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +4 -4
- package/dist/streaming/statusEmitter.js +2 -2
- package/dist/types/channels.d.ts +133 -34
- package/dist/types/channels.js +39 -1
- package/dist/types/channels.js.map +1 -1
- package/dist/types/config.d.ts +20 -1
- 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 +5 -4
- package/dist/types/selection.d.ts +48 -4
- package/dist/types/streaming.d.ts +1 -1
- 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 +1 -1
- 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 +6 -4
- 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 +58 -30
- package/dist/utils/fileLogger.js.map +1 -1
- package/dist/utils/format.d.ts +1 -1
- package/dist/utils/format.js +2 -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 +16 -20
- 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 +2 -2
- 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 -3
- 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/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 +14 -27
- package/dist/utils/version.js.map +1 -1
- package/package.json +13 -7
- package/dist/native/intercept.d.ts +0 -35
- package/dist/native/intercept.js +0 -181
- package/dist/native/intercept.js.map +0 -1
- package/dist/routes/config/channels/routes.js +0 -1505
- package/dist/routes/config/channels/routes.js.map +0 -1
package/dist/config/services.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
/* Copyright(C) 2024-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
-
*
|
|
3
|
-
* services.ts: Service group management for multi-service channels.
|
|
4
|
-
*/
|
|
5
|
-
import { CHANNEL_IDENTITY_FIELDS, PREDEFINED_CHANNELS } from "../channels/index.js";
|
|
6
1
|
import { DOMAIN_CONFIG, getDomainConfig } from "./sites.js";
|
|
7
2
|
import { LOG, extractDomain } from "../utils/index.js";
|
|
8
|
-
import { getChannelEffectiveTags } from "./userChannels.js";
|
|
3
|
+
import { getChannelEffectiveTags, getEffectiveHdhrEnabled, mutateChannels, pickIdentity } from "./userChannels.js";
|
|
4
|
+
import { CONFIG } from "./index.js";
|
|
5
|
+
import { PREDEFINED_CHANNELS } from "../channels/index.js";
|
|
9
6
|
import { getProfileForChannel } from "./profiles.js";
|
|
10
7
|
import { getUserDomains } from "./userProfiles.js";
|
|
8
|
+
import { mutateConfig } from "./userConfig.js";
|
|
11
9
|
/* Service groups allow multiple streaming services to offer the same content. For example, ESPN can be watched via ESPN.com (native) or Disney+.
|
|
12
10
|
*
|
|
13
|
-
* All variant relationships
|
|
14
|
-
* variant entries, the browse modal sets it on user variant entries, and the
|
|
11
|
+
* All variant relationships - predefined and user-defined - are expressed via the canonicalKey field on Channel. The flattener sets canonicalKey on predefined
|
|
12
|
+
* variant entries, the browse modal sets it on user variant entries, and the schema-version migration stamps it on entries that lack it. buildServiceGroups
|
|
15
13
|
* scans all channels once and groups by canonicalKey. One field, one mechanism, one code path.
|
|
16
14
|
*
|
|
17
15
|
* User overrides: When a user defines a channel with the same key as a predefined channel, both versions appear in the service dropdown. The user's custom version
|
|
@@ -21,11 +19,12 @@ import { getUserDomains } from "./userProfiles.js";
|
|
|
21
19
|
* User selections are stored in channels.json (in the data directory) under the `serviceSelections` key and persist across restarts.
|
|
22
20
|
*/
|
|
23
21
|
// Suffix appended to channel keys to reference the original predefined channel when a user has overridden it. For example, "espn:predefined" references the original
|
|
24
|
-
// predefined ESPN channel when the user has created a custom "espn" entry.
|
|
25
|
-
|
|
22
|
+
// predefined ESPN channel when the user has created a custom "espn" entry. Exported so other modules (e.g., channelForm) can detect synthetic entries via the
|
|
23
|
+
// canonical constant instead of stringly-typed substring checks.
|
|
24
|
+
export const PREDEFINED_SUFFIX = ":predefined";
|
|
26
25
|
/**
|
|
27
26
|
* Strips the :predefined suffix from a channel key if present, returning the base key. Synthetic keys like "pbs:predefined" are created when a user overrides a
|
|
28
|
-
* predefined channel
|
|
27
|
+
* predefined channel - the original predefined entry gets this suffix to coexist with the user's custom version in the service dropdown. Functions that look up
|
|
29
28
|
* channel data by key must strip the suffix to find the actual channel entry.
|
|
30
29
|
* @param key - The channel key, possibly with :predefined suffix.
|
|
31
30
|
* @returns The base key without the suffix.
|
|
@@ -35,7 +34,8 @@ function stripPredefinedSuffix(key) {
|
|
|
35
34
|
}
|
|
36
35
|
// Module-level storage for service groups, keyed by canonical channel key.
|
|
37
36
|
const serviceGroups = new Map();
|
|
38
|
-
// Reference to the channels map
|
|
37
|
+
// Reference to the resolved channels map (post canonical+variant inheritance). All consumer reads go through here, so it stores ResolvedChannel values where
|
|
38
|
+
// every entry has both identity and binding populated.
|
|
39
39
|
let channelsRef = {};
|
|
40
40
|
// User's service selections, keyed by canonical channel key. Values are the selected service key (e.g., "espn-disneyplus").
|
|
41
41
|
let serviceSelections = new Map();
|
|
@@ -63,7 +63,7 @@ function resolveServiceTag(channel) {
|
|
|
63
63
|
/**
|
|
64
64
|
* Gets the service tag for a channel key. For channels in a service group, reads the pre-computed tag from the group variant entry (computed at group-building
|
|
65
65
|
* time by buildServiceGroups). For standalone channels not in any group, derives the tag from the channel's URL domain. This function should not be called with
|
|
66
|
-
* :predefined synthetic keys
|
|
66
|
+
* :predefined synthetic keys - those only exist inside service groups and their tags are available via the group's variant entries.
|
|
67
67
|
* @param key - The channel key.
|
|
68
68
|
* @returns The service tag string.
|
|
69
69
|
*/
|
|
@@ -79,14 +79,13 @@ export function getServiceTagForChannel(key) {
|
|
|
79
79
|
}
|
|
80
80
|
// For standalone channels not in any group, derive from the channel's URL domain.
|
|
81
81
|
const channel = channelsRef[effectiveKey] ?? PREDEFINED_CHANNELS[effectiveKey];
|
|
82
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
83
82
|
if (!channel) {
|
|
84
83
|
return "direct";
|
|
85
84
|
}
|
|
86
85
|
return resolveServiceTag(channel);
|
|
87
86
|
}
|
|
88
87
|
/**
|
|
89
|
-
* Returns the auth domain for a channel key. Domain is the natural auth boundary
|
|
88
|
+
* Returns the auth domain for a channel key. Domain is the natural auth boundary - browser cookies and sessions scope to it. Multi-channel services work correctly
|
|
90
89
|
* because all their channels share one domain, and canonical channels work correctly because each has its own domain.
|
|
91
90
|
* @param key - The channel key.
|
|
92
91
|
* @returns The extracted domain from the channel's URL, or empty string if the channel or URL cannot be resolved.
|
|
@@ -94,7 +93,6 @@ export function getServiceTagForChannel(key) {
|
|
|
94
93
|
export function getAuthDomainForChannel(key) {
|
|
95
94
|
const effectiveKey = stripPredefinedSuffix(key);
|
|
96
95
|
const channel = channelsRef[effectiveKey] ?? PREDEFINED_CHANNELS[effectiveKey];
|
|
97
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
98
96
|
if (!channel?.url) {
|
|
99
97
|
return "";
|
|
100
98
|
}
|
|
@@ -111,7 +109,7 @@ export function getChannelServiceTags(canonicalKey) {
|
|
|
111
109
|
if (group) {
|
|
112
110
|
const tags = new Set();
|
|
113
111
|
for (const variant of group.variants) {
|
|
114
|
-
// Skip predefined suffix variants
|
|
112
|
+
// Skip predefined suffix variants - they share the canonical's tag (but derived from the predefined URL, which may differ from the user override).
|
|
115
113
|
if (variant.key.endsWith(PREDEFINED_SUFFIX)) {
|
|
116
114
|
continue;
|
|
117
115
|
}
|
|
@@ -119,7 +117,7 @@ export function getChannelServiceTags(canonicalKey) {
|
|
|
119
117
|
}
|
|
120
118
|
return [...tags];
|
|
121
119
|
}
|
|
122
|
-
// Standalone channel
|
|
120
|
+
// Standalone channel - derive tag from the channel directly.
|
|
123
121
|
return [getServiceTagForChannel(canonicalKey)];
|
|
124
122
|
}
|
|
125
123
|
/**
|
|
@@ -132,7 +130,7 @@ export function getAllServiceTags() {
|
|
|
132
130
|
// Scan all channels (not just grouped ones) to find all service tags.
|
|
133
131
|
const allKeys = new Set([...Object.keys(channelsRef), ...Object.keys(PREDEFINED_CHANNELS)]);
|
|
134
132
|
for (const key of allKeys) {
|
|
135
|
-
// Skip variant keys
|
|
133
|
+
// Skip variant keys - they are covered by getChannelServiceTags() on the canonical.
|
|
136
134
|
const group = serviceGroups.get(key);
|
|
137
135
|
if (group && (group.canonicalKey !== key)) {
|
|
138
136
|
continue;
|
|
@@ -189,11 +187,31 @@ export function getEnabledServices() {
|
|
|
189
187
|
return [...enabledServices];
|
|
190
188
|
}
|
|
191
189
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
190
|
+
* Hydrates the in-memory enabled-services cache from a serialized list. Called by initializeUserChannels at startup with the validated tags list and by the
|
|
191
|
+
* consistency probe's auto-fix after stripping unknown tags. Never called by route code directly - mutations go through mutateEnabledServices, which is the
|
|
192
|
+
* single async path that updates both the cache and the persisted file in one atomic operation.
|
|
193
|
+
* @param tags - The service tags to load into the cache. Empty array means "no filter" (all services shown).
|
|
194
194
|
*/
|
|
195
195
|
export function setEnabledServices(tags) {
|
|
196
196
|
enabledServices = [...tags];
|
|
197
|
+
CONFIG.channels.enabledServices = [...tags];
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Persists a new enabled-services list through the file store. Goes through mutateConfig so the file write, snapshot machinery, and post-mutate cache update
|
|
201
|
+
* all run uniformly - after the call returns, both disk and the module-state cache reflect the new value. Empty array means "no filter" (all services shown).
|
|
202
|
+
* @param tags - The new enabled service tags.
|
|
203
|
+
* @throws FileStoreParseError if config.json contains invalid JSON and the .bak rotation is also unparseable.
|
|
204
|
+
*/
|
|
205
|
+
export async function mutateEnabledServices(tags) {
|
|
206
|
+
const next = [...tags];
|
|
207
|
+
await mutateConfig((config) => {
|
|
208
|
+
config.channels ??= {};
|
|
209
|
+
config.channels.enabledServices = next;
|
|
210
|
+
});
|
|
211
|
+
// Module-state cache hydration after successful write. Mirrors the post-mutate hydration done in mutateChannels for serviceSelections and tagRegistry,
|
|
212
|
+
// keeping the in-memory view consistent with what was just persisted.
|
|
213
|
+
enabledServices = next;
|
|
214
|
+
CONFIG.channels.enabledServices = next;
|
|
197
215
|
}
|
|
198
216
|
/**
|
|
199
217
|
* Checks if a service tag is currently enabled. Returns true if the tag is enabled, if no filter is active (empty set), or if the tag is "direct".
|
|
@@ -201,7 +219,7 @@ export function setEnabledServices(tags) {
|
|
|
201
219
|
* @returns True if the service is available.
|
|
202
220
|
*/
|
|
203
221
|
export function isServiceTagEnabled(tag) {
|
|
204
|
-
// No filter active
|
|
222
|
+
// No filter active - all services are enabled.
|
|
205
223
|
if (enabledServices.length === 0) {
|
|
206
224
|
return true;
|
|
207
225
|
}
|
|
@@ -217,7 +235,7 @@ export function isServiceTagEnabled(tag) {
|
|
|
217
235
|
* @returns True if the channel passes the service filter.
|
|
218
236
|
*/
|
|
219
237
|
export function isChannelAvailableByService(canonicalKey) {
|
|
220
|
-
// No filter active
|
|
238
|
+
// No filter active - all channels are available.
|
|
221
239
|
if (enabledServices.length === 0) {
|
|
222
240
|
return true;
|
|
223
241
|
}
|
|
@@ -225,7 +243,7 @@ export function isChannelAvailableByService(canonicalKey) {
|
|
|
225
243
|
return tags.some((tag) => isServiceTagEnabled(tag));
|
|
226
244
|
}
|
|
227
245
|
/**
|
|
228
|
-
* Checks if a channel in the merged map is a user override of a predefined channel. This uses object reference comparison
|
|
246
|
+
* Checks if a channel in the merged map is a user override of a predefined channel. This uses object reference comparison - getAllChannels() spreads
|
|
229
247
|
* PREDEFINED_CHANNELS directly into the result, so if the reference differs, a user channel has replaced the predefined one.
|
|
230
248
|
* @param key - The channel key to check.
|
|
231
249
|
* @param channels - The merged channel map.
|
|
@@ -237,16 +255,14 @@ function isUserOverride(key, channels) {
|
|
|
237
255
|
return Boolean(predefined) && (channels[key] !== predefined);
|
|
238
256
|
}
|
|
239
257
|
/**
|
|
240
|
-
* Builds service groups by scanning all channels and grouping by canonicalKey.
|
|
241
|
-
*
|
|
242
|
-
* pass over the merged channel map — one field, one mechanism for both predefined and user-defined variant relationships.
|
|
258
|
+
* Builds service groups by scanning all channels and grouping by canonicalKey. The flattener sets canonicalKey on predefined variants, the browse modal sets it
|
|
259
|
+
* on user variants, and the schema-version migration stamps it on entries that lack it. One field, one mechanism, one pass.
|
|
243
260
|
*
|
|
244
261
|
* User overrides of predefined channels (same key, different object reference) produce a two-entry group with "Custom" and the original predefined version,
|
|
245
262
|
* even for single-service channels that don't have canonicalKey-based variants.
|
|
246
263
|
*
|
|
247
|
-
* After
|
|
248
|
-
*
|
|
249
|
-
* predefined overrides.
|
|
264
|
+
* After grouping, every stored service selection is validated against the rebuilt variant structure. Selections that no longer correspond to a real variant are
|
|
265
|
+
* reverted to the canonical default. This is the single resolution boundary for stale selections; read-side resolvers stay pure.
|
|
250
266
|
* @param channels - The merged channel map (predefined + user channels).
|
|
251
267
|
* @returns Canonical keys whose service selections were stale and reverted. Empty array if all selections are valid. The caller decides whether to persist.
|
|
252
268
|
*/
|
|
@@ -270,20 +286,24 @@ export function buildServiceGroups(channels) {
|
|
|
270
286
|
// Pass 2: Build groups from the collected variants.
|
|
271
287
|
for (const [canonicalKey, variantKeys] of variantsByCanonical) {
|
|
272
288
|
const canonical = channels[canonicalKey];
|
|
273
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
274
289
|
if (!canonical) {
|
|
275
290
|
continue;
|
|
276
291
|
}
|
|
277
292
|
const variants = [];
|
|
278
293
|
// Handle user override of the canonical entry. Two scenarios: (A) the user customized properties (station ID, tags, etc.) but the URL still matches a known
|
|
279
|
-
// service domain
|
|
280
|
-
// non-standard URL
|
|
281
|
-
|
|
282
|
-
|
|
294
|
+
// service domain - no "Custom" variant needed, just the normal service label with a visual override indicator in the table; (B) the user set a genuinely
|
|
295
|
+
// non-standard URL - "Custom (domain)" is a real service variant and the :predefined entry gives access to the original service URL.
|
|
296
|
+
// isUserOverride returned true means predefined exists for canonicalKey (it's defined as `Boolean(predefined) && (channels[key] !== predefined)`). Look it
|
|
297
|
+
// up here and narrow away the undefined so the scenarios below can rely on the predefined reference.
|
|
298
|
+
const predefined = isUserOverride(canonicalKey, channels) ? PREDEFINED_CHANNELS[canonicalKey] : undefined;
|
|
299
|
+
if (predefined) {
|
|
283
300
|
const userDomain = extractDomain(canonical.url);
|
|
284
301
|
const knownDomains = new Set([extractDomain(predefined.url)]);
|
|
285
302
|
for (const variantKey of variantKeys) {
|
|
286
|
-
|
|
303
|
+
const variantChannel = channels[variantKey];
|
|
304
|
+
if (variantChannel) {
|
|
305
|
+
knownDomains.add(extractDomain(variantChannel.url));
|
|
306
|
+
}
|
|
287
307
|
}
|
|
288
308
|
if (knownDomains.has(userDomain)) {
|
|
289
309
|
// Scenario A: property override on a known service. The canonical gets the same label as if it weren't overridden. The modified-dot indicator in the
|
|
@@ -304,7 +324,10 @@ export function buildServiceGroups(channels) {
|
|
|
304
324
|
variantKeys.sort();
|
|
305
325
|
for (const variantKey of variantKeys) {
|
|
306
326
|
const variant = channels[variantKey];
|
|
307
|
-
|
|
327
|
+
// Defensive: every variantKey in variantKeys came from Pass 1's scan of this same channels map, so the lookup succeeds. The narrowing keeps the types honest.
|
|
328
|
+
if (variant) {
|
|
329
|
+
variants.push({ key: variantKey, label: getChannelServiceLabel(variant), tag: resolveServiceTag(variant) });
|
|
330
|
+
}
|
|
308
331
|
}
|
|
309
332
|
const group = { canonicalKey, variants };
|
|
310
333
|
// Map canonical and all variant keys to this group for easy lookup.
|
|
@@ -314,7 +337,7 @@ export function buildServiceGroups(channels) {
|
|
|
314
337
|
}
|
|
315
338
|
LOG.debug("config:general", "Service group '%s': variants=%s.", canonicalKey, variants.map((v) => v.key).join(", "));
|
|
316
339
|
}
|
|
317
|
-
// Pass 3: Create groups for user overrides of single-service predefined channels. Only Scenario B (genuinely custom URL) gets a service group
|
|
340
|
+
// Pass 3: Create groups for user overrides of single-service predefined channels. Only Scenario B (genuinely custom URL) gets a service group - the user needs
|
|
318
341
|
// a dropdown to switch between their custom URL and the predefined service. Scenario A (property override on the same domain) skips group creation entirely
|
|
319
342
|
// because there is only one service and no dropdown is needed; the modified-dot indicator in the table renderer signals the override.
|
|
320
343
|
for (const key of Object.keys(channels)) {
|
|
@@ -326,7 +349,12 @@ export function buildServiceGroups(channels) {
|
|
|
326
349
|
}
|
|
327
350
|
const userChannel = channels[key];
|
|
328
351
|
const predefined = PREDEFINED_CHANNELS[key];
|
|
329
|
-
//
|
|
352
|
+
// Defensive: isUserOverride returning true means both predefined and the user entry exist. The narrowing here satisfies the type-checker and guards
|
|
353
|
+
// against an impossible edge case.
|
|
354
|
+
if (!userChannel || !predefined) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
// Scenario A: URL domain matches the predefined service. No service group needed - renders as a single-service channel with a modified-dot indicator.
|
|
330
358
|
if (extractDomain(userChannel.url) === extractDomain(predefined.url)) {
|
|
331
359
|
continue;
|
|
332
360
|
}
|
|
@@ -343,7 +371,7 @@ export function buildServiceGroups(channels) {
|
|
|
343
371
|
// channel. Scans only canonical entries in PREDEFINED_CHANNELS (variants share the canonical's URL domain and would produce duplicate results).
|
|
344
372
|
predefinedByDomain.clear();
|
|
345
373
|
for (const [key, channel] of Object.entries(PREDEFINED_CHANNELS)) {
|
|
346
|
-
if (channel.canonicalKey) {
|
|
374
|
+
if (channel.canonicalKey !== undefined) {
|
|
347
375
|
continue;
|
|
348
376
|
}
|
|
349
377
|
const domain = extractDomain(channel.url);
|
|
@@ -357,9 +385,8 @@ export function buildServiceGroups(channels) {
|
|
|
357
385
|
predefinedByDomain.set(domain, [entry]);
|
|
358
386
|
}
|
|
359
387
|
}
|
|
360
|
-
// Validate stored service selections against the rebuilt groups. Any selection whose variant key
|
|
361
|
-
// canonical default.
|
|
362
|
-
// predefined overrides. The caller decides whether to persist based on whether any keys were cleaned.
|
|
388
|
+
// Validate stored service selections against the rebuilt groups. Any selection whose variant key is not present in the group is stale and reverted to the
|
|
389
|
+
// canonical default. The caller decides whether to persist based on whether any keys were cleaned.
|
|
363
390
|
const staleKeys = [];
|
|
364
391
|
for (const [canonicalKey, selection] of serviceSelections) {
|
|
365
392
|
const group = serviceGroups.get(canonicalKey);
|
|
@@ -400,7 +427,7 @@ export function getPredefinedDomainMap() {
|
|
|
400
427
|
/**
|
|
401
428
|
* Resolves a URL to a friendly service display name. Checks built-in DOMAIN_CONFIG first for a stable, well-known service name, then falls back to
|
|
402
429
|
* getDomainConfig() which includes user domain mappings. This ordering prevents user domain overrides from corrupting display labels for predefined channel
|
|
403
|
-
* variants
|
|
430
|
+
* variants - a user mapping a built-in domain to a custom profile should not rename every service dropdown entry that uses that domain.
|
|
404
431
|
* @param url - The URL to resolve a service display name for.
|
|
405
432
|
* @returns The service display name, or the concise domain if no service name is configured.
|
|
406
433
|
*/
|
|
@@ -410,19 +437,17 @@ export function getServiceDisplayName(url) {
|
|
|
410
437
|
try {
|
|
411
438
|
const hostname = new URL(url).hostname;
|
|
412
439
|
const builtinFull = DOMAIN_CONFIG[hostname];
|
|
413
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
414
440
|
if (builtinFull?.service) {
|
|
415
441
|
return builtinFull.service;
|
|
416
442
|
}
|
|
417
443
|
const concise = extractDomain(url);
|
|
418
444
|
const builtinConcise = DOMAIN_CONFIG[concise];
|
|
419
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
420
445
|
if (builtinConcise?.service) {
|
|
421
446
|
return builtinConcise.service;
|
|
422
447
|
}
|
|
423
448
|
}
|
|
424
449
|
catch {
|
|
425
|
-
// Invalid URL
|
|
450
|
+
// Invalid URL - fall through to getDomainConfig.
|
|
426
451
|
}
|
|
427
452
|
// For domains not in DOMAIN_CONFIG, fall back to getDomainConfig() which includes user domain mappings.
|
|
428
453
|
const config = getDomainConfig(url);
|
|
@@ -430,7 +455,7 @@ export function getServiceDisplayName(url) {
|
|
|
430
455
|
}
|
|
431
456
|
/**
|
|
432
457
|
* Resolves service identity (tag and display name) for a user-defined profile by scanning its domain mappings. Returns the first matching domain config's
|
|
433
|
-
* serviceTag and service name. This is the single source of truth for "profile key
|
|
458
|
+
* serviceTag and service name. This is the single source of truth for "profile key -> service identity" resolution, used by both tag and label lookups to avoid
|
|
434
459
|
* duplicating the domain scan logic.
|
|
435
460
|
* @param profileKey - The user profile key to resolve.
|
|
436
461
|
* @returns The service identity from the profile's domain mappings, or undefined if no matching domain mapping exists.
|
|
@@ -477,7 +502,7 @@ export const VALID_SORT_FIELDS = new Set(["channelNumber", "channelSelector", "h
|
|
|
477
502
|
* @returns A lowercase string suitable for comparison-based sorting.
|
|
478
503
|
*/
|
|
479
504
|
export function getChannelSortKey(channel, key, field) {
|
|
480
|
-
// Resolve the selected service variant so all sort keys reflect the user's service selection. For URL-dependent fields (profile, service), this is essential
|
|
505
|
+
// Resolve the selected service variant so all sort keys reflect the user's service selection. For URL-dependent fields (profile, service), this is essential -
|
|
481
506
|
// a canonical's URL may differ from the selected variant's (e.g., bbcnews canonical uses cox but the user selected the directv variant). For identity fields
|
|
482
507
|
// (name, stationId, channelNumber), the flattener eagerly sets these on all entries, so the resolved channel has identical values regardless of variant.
|
|
483
508
|
const effective = getResolvedChannel(resolveServiceKey(key)) ?? channel;
|
|
@@ -490,8 +515,9 @@ export function getChannelSortKey(channel, key, field) {
|
|
|
490
515
|
return (effective.channelSelector ?? "").toLowerCase();
|
|
491
516
|
}
|
|
492
517
|
case "hdhrEnabled": {
|
|
493
|
-
// Sort enabled channels before disabled. "0" (enabled/absent) sorts before "1" (disabled).
|
|
494
|
-
|
|
518
|
+
// Sort enabled channels before disabled. "0" (enabled/absent) sorts before "1" (disabled). The effective-view helper centralizes the implicit-true
|
|
519
|
+
// convention so the sort key here, the table's checked attribute, and every other consumer agree on the meaning of an absent value.
|
|
520
|
+
return getEffectiveHdhrEnabled(effective) ? "0" : "1";
|
|
495
521
|
}
|
|
496
522
|
case "key": {
|
|
497
523
|
return key.toLowerCase();
|
|
@@ -531,8 +557,8 @@ export function getChannelSortKey(channel, key, field) {
|
|
|
531
557
|
}
|
|
532
558
|
/**
|
|
533
559
|
* Compares two channels for sorting by the specified field and direction with a built-in channel name tiebreaker. The tiebreaker is always ascending so that rows
|
|
534
|
-
* within each group maintain a consistent alphabetical order regardless of the primary sort direction. This is the single comparator for all sort sites
|
|
535
|
-
* render, client re-sort, and M3U playlist
|
|
560
|
+
* within each group maintain a consistent alphabetical order regardless of the primary sort direction. This is the single comparator for all sort sites - server HTML
|
|
561
|
+
* render, client re-sort, and M3U playlist - to prevent ordering divergence.
|
|
536
562
|
* @param channelA - First channel definition.
|
|
537
563
|
* @param keyA - First channel key.
|
|
538
564
|
* @param channelB - Second channel definition.
|
|
@@ -592,14 +618,15 @@ export function getCanonicalKey(key) {
|
|
|
592
618
|
return group?.canonicalKey ?? baseKey;
|
|
593
619
|
}
|
|
594
620
|
/**
|
|
595
|
-
*
|
|
621
|
+
* Hydrates the in-memory selections cache from a serialized record. Called by mutateChannels' post-write hook (with the freshly-written disk state) and by
|
|
622
|
+
* initializeUserChannels at startup. Never called by route code directly - mutations go through setServiceSelection (single) or mutateServiceSelections (bulk).
|
|
596
623
|
* @param selections - Service selections keyed by canonical channel key.
|
|
597
624
|
*/
|
|
598
625
|
export function setServiceSelections(selections) {
|
|
599
626
|
serviceSelections = new Map(Object.entries(selections));
|
|
600
627
|
}
|
|
601
628
|
/**
|
|
602
|
-
* Gets all service selections.
|
|
629
|
+
* Gets all service selections from the in-memory cache. Cache is hydrated from disk on every successful mutate, so this is always consistent with the file.
|
|
603
630
|
* @returns Copy of the service selections object.
|
|
604
631
|
*/
|
|
605
632
|
export function getServiceSelections() {
|
|
@@ -614,39 +641,66 @@ export function getServiceSelection(canonicalKey) {
|
|
|
614
641
|
return serviceSelections.get(canonicalKey);
|
|
615
642
|
}
|
|
616
643
|
/**
|
|
617
|
-
*
|
|
644
|
+
* Persists a single service selection through the file store. Goes through mutateChannels so the file write, integrity validation, and post-mutate cache
|
|
645
|
+
* hydration all run uniformly - after the call returns, both disk and the module-state Map reflect the new value. Selecting the canonical key itself (the
|
|
646
|
+
* default service) deletes the selection rather than storing a redundant entry.
|
|
647
|
+
*
|
|
648
|
+
* For bulk updates, prefer mutateServiceSelections to coalesce multiple changes into a single atomic write.
|
|
618
649
|
* @param canonicalKey - The canonical channel key.
|
|
619
|
-
* @param serviceKey - The selected service key.
|
|
650
|
+
* @param serviceKey - The selected service key. When equal to canonicalKey, the selection is removed.
|
|
651
|
+
* @throws FileStoreParseError if channels.json contains invalid JSON and the .bak rotation is also unparseable.
|
|
620
652
|
*/
|
|
621
|
-
export function setServiceSelection(canonicalKey, serviceKey) {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
653
|
+
export async function setServiceSelection(canonicalKey, serviceKey) {
|
|
654
|
+
await mutateChannels((data) => {
|
|
655
|
+
if (serviceKey === canonicalKey) {
|
|
656
|
+
Reflect.deleteProperty(data.serviceSelections, canonicalKey);
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
data.serviceSelections[canonicalKey] = serviceKey;
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Bulk variant of setServiceSelection. Applies multiple selection changes inside a single mutate transaction so all changes land atomically with one disk
|
|
665
|
+
* write. Use this whenever a single user action (e.g., browse-modal submit, bulk service assignment) updates more than one selection - serial awaits over
|
|
666
|
+
* setServiceSelection would produce N writes and N intermediate disk states.
|
|
667
|
+
*
|
|
668
|
+
* Each entry follows the same canonical-key-equals-service-key convention as setServiceSelection: when the value matches the key, the selection is removed.
|
|
669
|
+
* @param updates - Object mapping canonical channel keys to their new service keys.
|
|
670
|
+
* @throws FileStoreParseError if channels.json contains invalid JSON and the .bak rotation is also unparseable.
|
|
671
|
+
*/
|
|
672
|
+
export async function mutateServiceSelections(updates) {
|
|
673
|
+
await mutateChannels((data) => {
|
|
674
|
+
for (const [canonicalKey, serviceKey] of Object.entries(updates)) {
|
|
675
|
+
if (serviceKey === canonicalKey) {
|
|
676
|
+
Reflect.deleteProperty(data.serviceSelections, canonicalKey);
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
data.serviceSelections[canonicalKey] = serviceKey;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
});
|
|
629
683
|
}
|
|
630
684
|
/**
|
|
631
685
|
* Resolves a canonical channel key to the actual channel key based on user selection. If the user has selected a specific service for this channel, returns that
|
|
632
686
|
* service's key. Otherwise returns the canonical key (default service). When the service filter is active, falls back to the first enabled variant if the stored
|
|
633
687
|
* selection's service is filtered out.
|
|
634
688
|
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
689
|
+
* Pure resolver with no side effects. Stale selection cleanup belongs to buildServiceGroups(), which validates all stored selections against the rebuilt variant
|
|
690
|
+
* structure on startup and after runtime channel mutations. Reads are safe to call from any context, including inside other mutations.
|
|
637
691
|
* @param canonicalKey - The canonical channel key.
|
|
638
692
|
* @returns The resolved service key to use for streaming.
|
|
639
693
|
*/
|
|
640
694
|
export function resolveServiceKey(canonicalKey) {
|
|
641
695
|
const selection = serviceSelections.get(canonicalKey);
|
|
642
|
-
// No selection stored
|
|
696
|
+
// No selection stored - use the canonical key (default service). If the canonical's service tag is filtered out, fall back to the first enabled variant.
|
|
643
697
|
if (!selection) {
|
|
644
698
|
if ((enabledServices.length > 0) && !isServiceTagEnabled(getServiceTagForChannel(canonicalKey))) {
|
|
645
699
|
return findFirstEnabledVariant(canonicalKey) ?? canonicalKey;
|
|
646
700
|
}
|
|
647
701
|
return canonicalKey;
|
|
648
702
|
}
|
|
649
|
-
// Valid selection
|
|
703
|
+
// Valid selection - if its service tag is filtered out, fall back to the first enabled variant.
|
|
650
704
|
if ((enabledServices.length > 0) && !isServiceTagEnabled(getServiceTagForChannel(selection))) {
|
|
651
705
|
return findFirstEnabledVariant(canonicalKey) ?? selection;
|
|
652
706
|
}
|
|
@@ -674,85 +728,45 @@ function findFirstEnabledVariant(canonicalKey) {
|
|
|
674
728
|
return undefined;
|
|
675
729
|
}
|
|
676
730
|
/**
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
* The
|
|
682
|
-
* @param variant - The variant channel definition (service-specific fields).
|
|
683
|
-
* @param base - The canonical (base) channel with user overrides applied (identity fields).
|
|
684
|
-
* @returns A new Channel with identity fields from the canonical and service-specific fields from the variant.
|
|
685
|
-
*/
|
|
686
|
-
function applyVariantInheritance(variant, base) {
|
|
687
|
-
const result = { ...variant };
|
|
688
|
-
for (const field of CHANNEL_IDENTITY_FIELDS) {
|
|
689
|
-
copyField(result, base, field);
|
|
690
|
-
}
|
|
691
|
-
return result;
|
|
692
|
-
}
|
|
693
|
-
// Type-safe field copy using a generic to preserve the field-value relationship. TypeScript can't prove that result[field] and base[field] have compatible types
|
|
694
|
-
// when field is a union of literal keys (each key maps to a different type). The generic K narrows to a single key per call, making the assignment type-safe.
|
|
695
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- K is necessary to correlate the key type between target and source assignments.
|
|
696
|
-
function copyField(target, source, key) {
|
|
697
|
-
target[key] = source[key];
|
|
698
|
-
}
|
|
699
|
-
/**
|
|
700
|
-
* Gets a channel with inheritance applied. For service variants, this merges the variant's properties with inherited properties from the canonical entry
|
|
701
|
-
* using the live channel data (which includes user overrides). Use `resolvePredefinedVariant()` when you need resolution against pure predefined data.
|
|
702
|
-
* @param key - The channel key (canonical or variant).
|
|
703
|
-
* @returns The complete channel with inheritance applied, or undefined if the channel doesn't exist.
|
|
731
|
+
* Gets a channel with inheritance applied. Variant inheritance is resolved at load time by resolveStoredChannel in userChannels.ts - entries in channelsRef
|
|
732
|
+
* are already fully merged with their canonical (variant values win when set, canonical fills in the rest). This function is a thin accessor that also
|
|
733
|
+
* handles the synthetic :predefined suffix used when a user overrides a canonical but the service dropdown references the original predefined variant.
|
|
734
|
+
* @param key - The channel key (canonical, variant, or :predefined suffix).
|
|
735
|
+
* @returns The complete channel, or undefined if the channel doesn't exist.
|
|
704
736
|
*/
|
|
705
737
|
export function getResolvedChannel(key) {
|
|
706
|
-
// Handle predefined suffix
|
|
738
|
+
// Handle the :predefined suffix - return the original predefined channel when the user has overridden the canonical but selects the predefined service. The
|
|
739
|
+
// suffix is only meaningful for canonical keys, so the lookup target is structurally a CanonicalChannel (which is a valid ResolvedChannel).
|
|
707
740
|
if (key.endsWith(PREDEFINED_SUFFIX)) {
|
|
708
|
-
const
|
|
709
|
-
return
|
|
710
|
-
}
|
|
711
|
-
const channel = channelsRef[key];
|
|
712
|
-
// Runtime check needed even though TypeScript thinks channel is always defined (Record indexing quirk).
|
|
713
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
714
|
-
if (!channel) {
|
|
715
|
-
return undefined;
|
|
716
|
-
}
|
|
717
|
-
const group = serviceGroups.get(key);
|
|
718
|
-
// If not part of a group or is the canonical entry, return as-is.
|
|
719
|
-
if (!group || (group.canonicalKey === key)) {
|
|
720
|
-
return channel;
|
|
721
|
-
}
|
|
722
|
-
// This is a variant — merge with canonical entry.
|
|
723
|
-
const canonical = channelsRef[group.canonicalKey];
|
|
724
|
-
// Runtime check — canonical entry should exist if the group exists, but we check defensively.
|
|
725
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
726
|
-
if (!canonical) {
|
|
727
|
-
// Canonical entry missing (shouldn't happen), return variant as-is.
|
|
728
|
-
return channel;
|
|
741
|
+
const predefined = PREDEFINED_CHANNELS[key.slice(0, -PREDEFINED_SUFFIX.length)];
|
|
742
|
+
return (predefined && (predefined.canonicalKey === undefined)) ? predefined : undefined;
|
|
729
743
|
}
|
|
730
|
-
return
|
|
744
|
+
return channelsRef[key];
|
|
731
745
|
}
|
|
732
746
|
/**
|
|
733
|
-
* Resolves a variant channel key against pure predefined data (ignoring user overrides).
|
|
734
|
-
*
|
|
735
|
-
*
|
|
747
|
+
* Resolves a variant channel key against pure predefined data (ignoring user overrides). Used for revert detection: when an edit's values match a variant's
|
|
748
|
+
* predefined definition, the custom override can be dropped and the service selection switched to that variant. Predefined variant entries carry only
|
|
749
|
+
* service-specific fields and canonicalKey; identity inherits from the canonical. This resolver layers the variant's service fields onto the canonical so
|
|
750
|
+
* findMatchingVariant can compare form values against a fully-populated variant view.
|
|
736
751
|
* @param key - The channel key (canonical or variant).
|
|
737
|
-
* @returns The
|
|
752
|
+
* @returns The resolved predefined channel, or undefined when the key has no predefined definition.
|
|
738
753
|
*/
|
|
739
754
|
export function resolvePredefinedVariant(key) {
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
743
|
-
if (!channel) {
|
|
755
|
+
const entry = PREDEFINED_CHANNELS[key];
|
|
756
|
+
if (!entry) {
|
|
744
757
|
return undefined;
|
|
745
758
|
}
|
|
746
|
-
|
|
747
|
-
//
|
|
748
|
-
if (
|
|
749
|
-
return
|
|
759
|
+
// Canonical entries (and standalones, but those are not in PREDEFINED_CHANNELS) carry full identity already. Narrow on the canonicalKey discriminator to
|
|
760
|
+
// confirm: undefined canonicalKey means CanonicalChannel.
|
|
761
|
+
if (entry.canonicalKey === undefined) {
|
|
762
|
+
return entry;
|
|
750
763
|
}
|
|
751
|
-
|
|
752
|
-
//
|
|
753
|
-
|
|
754
|
-
|
|
764
|
+
// Predefined variant: identity inherits from the canonical, binding comes from the variant. We start with canonical identity ONLY (via pickIdentity) so the
|
|
765
|
+
// canonical service's binding does not leak into a different service's variant. The variant's own binding fields then populate via spread.
|
|
766
|
+
const canonical = PREDEFINED_CHANNELS[entry.canonicalKey];
|
|
767
|
+
if (!canonical || (canonical.canonicalKey !== undefined)) {
|
|
768
|
+
return undefined;
|
|
755
769
|
}
|
|
756
|
-
return
|
|
770
|
+
return { ...pickIdentity(canonical), ...entry };
|
|
757
771
|
}
|
|
758
772
|
//# sourceMappingURL=services.js.map
|