prismcast 1.8.0 → 1.9.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.js +15 -5
- package/dist/app.js.map +1 -1
- package/dist/browser/cdp.d.ts +4 -6
- package/dist/browser/cdp.js +13 -17
- package/dist/browser/cdp.js.map +1 -1
- package/dist/browser/index.js +3 -3
- package/dist/browser/index.js.map +1 -1
- 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/directv.js +4 -7
- package/dist/browser/tuning/directv.js.map +1 -1
- package/dist/browser/tuning/fox.js +1 -1
- package/dist/browser/tuning/fox.js.map +1 -1
- package/dist/browser/tuning/hbo.js +1 -1
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/tuning/hulu.js +3 -3
- package/dist/browser/tuning/hulu.js.map +1 -1
- package/dist/browser/tuning/sling.js +2 -2
- package/dist/browser/tuning/sling.js.map +1 -1
- package/dist/channels/index.d.ts +1 -1
- package/dist/channels/index.js +416 -416
- package/dist/channels/index.js.map +1 -1
- package/dist/config/health.d.ts +3 -3
- package/dist/config/health.js +3 -3
- package/dist/config/index.js +2 -2
- package/dist/config/index.js.map +1 -1
- package/dist/config/persistence.d.ts +59 -0
- package/dist/config/persistence.js +127 -0
- package/dist/config/persistence.js.map +1 -0
- package/dist/config/servicePacks.d.ts +46 -0
- package/dist/config/{providerPacks.js → servicePacks.js} +28 -31
- package/dist/config/servicePacks.js.map +1 -0
- package/dist/config/services.d.ts +202 -0
- package/dist/config/{providers.js → services.js} +277 -210
- package/dist/config/services.js.map +1 -0
- package/dist/config/sites.js +61 -61
- package/dist/config/sites.js.map +1 -1
- package/dist/config/userChannels.d.ts +36 -27
- package/dist/config/userChannels.js +300 -212
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +11 -9
- package/dist/config/userConfig.js +53 -60
- package/dist/config/userConfig.js.map +1 -1
- package/dist/config/userProfiles.d.ts +31 -15
- package/dist/config/userProfiles.js +139 -96
- package/dist/config/userProfiles.js.map +1 -1
- package/dist/hdhr/channelMap.d.ts +1 -1
- package/dist/hdhr/channelMap.js +4 -7
- package/dist/hdhr/channelMap.js.map +1 -1
- package/dist/hdhr/index.js +6 -9
- package/dist/hdhr/index.js.map +1 -1
- package/dist/native/index.js +3 -3
- package/dist/native/index.js.map +1 -1
- package/dist/native/intercept.js +86 -89
- package/dist/native/intercept.js.map +1 -1
- package/dist/native/probe.js +2 -2
- package/dist/native/probe.js.map +1 -1
- package/dist/native/proxy.js +4 -4
- package/dist/native/proxy.js.map +1 -1
- package/dist/routes/assets.js +3 -3
- package/dist/routes/assets.js.map +1 -1
- package/dist/routes/auth.js +3 -3
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/config/channels/index.d.ts +1 -1
- 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.js +488 -460
- package/dist/routes/config/channels/routes.js.map +1 -1
- package/dist/routes/config/channels/table.d.ts +4 -4
- package/dist/routes/config/channels/table.js +206 -147
- package/dist/routes/config/channels/table.js.map +1 -1
- package/dist/routes/config/index.d.ts +2 -2
- package/dist/routes/config/index.js +3 -3
- package/dist/routes/config/index.js.map +1 -1
- package/dist/routes/config/{providers.js → services.js} +44 -39
- package/dist/routes/config/services.js.map +1 -0
- package/dist/routes/config/settings.js +47 -29
- package/dist/routes/config/settings.js.map +1 -1
- package/dist/routes/debug.js +5 -7
- 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/index.js +2 -2
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/logs.js +4 -4
- package/dist/routes/logs.js.map +1 -1
- package/dist/routes/playlist.d.ts +4 -4
- package/dist/routes/playlist.js +56 -44
- package/dist/routes/playlist.js.map +1 -1
- package/dist/routes/root/content.js +58 -58
- package/dist/routes/root/content.js.map +1 -1
- package/dist/routes/root/scripts/channels.js +529 -572
- package/dist/routes/root/scripts/channels.js.map +1 -1
- package/dist/routes/root/scripts/config.js +838 -992
- package/dist/routes/root/scripts/config.js.map +1 -1
- package/dist/routes/root/scripts/shared.js +431 -197
- package/dist/routes/root/scripts/shared.js.map +1 -1
- package/dist/routes/root/scripts/status.js +247 -196
- package/dist/routes/root/scripts/status.js.map +1 -1
- package/dist/routes/root/styles.js +12 -4
- package/dist/routes/root/styles.js.map +1 -1
- package/dist/routes/services.d.ts +6 -0
- package/dist/routes/{providers.js → services.js} +43 -43
- package/dist/routes/services.js.map +1 -0
- package/dist/routes/ui.js +2 -0
- package/dist/routes/ui.js.map +1 -1
- package/dist/service/commands.js +5 -8
- package/dist/service/commands.js.map +1 -1
- package/dist/service/generators.d.ts +1 -1
- package/dist/service/generators.js +143 -41
- package/dist/service/generators.js.map +1 -1
- package/dist/streaming/hls.d.ts +4 -4
- package/dist/streaming/hls.js +24 -23
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsResume.js +1 -1
- package/dist/streaming/hlsResume.js.map +1 -1
- package/dist/streaming/monitor.d.ts +2 -2
- package/dist/streaming/monitor.js +19 -13
- package/dist/streaming/monitor.js.map +1 -1
- package/dist/streaming/preroll.js +1 -1
- package/dist/streaming/preroll.js.map +1 -1
- package/dist/streaming/registry.js +4 -8
- package/dist/streaming/registry.js.map +1 -1
- package/dist/streaming/setup.d.ts +2 -2
- package/dist/streaming/setup.js +23 -22
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.js +8 -10
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +4 -3
- package/dist/streaming/statusEmitter.js +3 -3
- package/dist/streaming/statusEmitter.js.map +1 -1
- package/dist/types/channels.d.ts +15 -11
- package/dist/types/config.d.ts +2 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/profiles.d.ts +9 -9
- package/dist/types/selection.d.ts +1 -1
- package/dist/types/shared.d.ts +1 -1
- package/dist/types/streaming.d.ts +1 -1
- package/dist/utils/chromeFetch.js +1 -1
- package/dist/utils/debugFilter.js +3 -3
- package/dist/utils/debugFilter.js.map +1 -1
- package/dist/utils/fileLogger.js +4 -3
- package/dist/utils/fileLogger.js.map +1 -1
- package/dist/utils/format.d.ts +7 -1
- package/dist/utils/format.js +21 -1
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/logger.js +17 -27
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/morganStream.js +2 -2
- package/dist/utils/morganStream.js.map +1 -1
- package/dist/utils/platform.js +3 -2
- package/dist/utils/platform.js.map +1 -1
- 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/version.js +4 -4
- package/dist/utils/version.js.map +1 -1
- package/package.json +5 -7
- 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.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,6 +1,6 @@
|
|
|
1
1
|
/* Copyright(C) 2024-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
2
|
*
|
|
3
|
-
*
|
|
3
|
+
* services.ts: Service group management for multi-service channels.
|
|
4
4
|
*/
|
|
5
5
|
import { CHANNEL_IDENTITY_FIELDS, PREDEFINED_CHANNELS } from "../channels/index.js";
|
|
6
6
|
import { DOMAIN_CONFIG, getDomainConfig } from "./sites.js";
|
|
@@ -8,24 +8,24 @@ import { LOG, extractDomain } from "../utils/index.js";
|
|
|
8
8
|
import { getChannelEffectiveTags } from "./userChannels.js";
|
|
9
9
|
import { getProfileForChannel } from "./profiles.js";
|
|
10
10
|
import { getUserDomains } from "./userProfiles.js";
|
|
11
|
-
/*
|
|
11
|
+
/* Service groups allow multiple streaming services to offer the same content. For example, ESPN can be watched via ESPN.com (native) or Disney+.
|
|
12
12
|
*
|
|
13
13
|
* All variant relationships — predefined and user-defined — are expressed via the canonicalKey field on Channel. The flattener sets canonicalKey on predefined
|
|
14
|
-
* variant entries, the browse modal sets it on user variant entries, and the one-time migration stamps it on pre-existing user entries.
|
|
14
|
+
* variant entries, the browse modal sets it on user variant entries, and the one-time migration stamps it on pre-existing user entries. buildServiceGroups
|
|
15
15
|
* scans all channels once and groups by canonicalKey. One field, one mechanism, one code path.
|
|
16
16
|
*
|
|
17
|
-
* User overrides: When a user defines a channel with the same key as a predefined channel, both versions appear in the
|
|
17
|
+
* 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
|
|
18
18
|
* is shown first (labeled "Custom") and is the default. The original predefined version uses a special key suffix (PREDEFINED_SUFFIX) to distinguish it from the
|
|
19
19
|
* user's version. This allows users to switch between their custom definition and the original at any time.
|
|
20
20
|
*
|
|
21
|
-
* User selections are stored in channels.json (in the data directory) under the `
|
|
21
|
+
* User selections are stored in channels.json (in the data directory) under the `serviceSelections` key and persist across restarts.
|
|
22
22
|
*/
|
|
23
23
|
// 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
24
|
// predefined ESPN channel when the user has created a custom "espn" entry.
|
|
25
25
|
const PREDEFINED_SUFFIX = ":predefined";
|
|
26
26
|
/**
|
|
27
27
|
* 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 — the original predefined entry gets this suffix to coexist with the user's custom version in the
|
|
28
|
+
* 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
29
|
* channel data by key must strip the suffix to find the actual channel entry.
|
|
30
30
|
* @param key - The channel key, possibly with :predefined suffix.
|
|
31
31
|
* @returns The base key without the suffix.
|
|
@@ -33,43 +33,43 @@ const PREDEFINED_SUFFIX = ":predefined";
|
|
|
33
33
|
function stripPredefinedSuffix(key) {
|
|
34
34
|
return key.endsWith(PREDEFINED_SUFFIX) ? key.slice(0, -PREDEFINED_SUFFIX.length) : key;
|
|
35
35
|
}
|
|
36
|
-
// Module-level storage for
|
|
37
|
-
const
|
|
36
|
+
// Module-level storage for service groups, keyed by canonical channel key.
|
|
37
|
+
const serviceGroups = new Map();
|
|
38
38
|
// Reference to the channels map for inheritance resolution.
|
|
39
39
|
let channelsRef = {};
|
|
40
|
-
// User's
|
|
41
|
-
let
|
|
42
|
-
//
|
|
43
|
-
// Module-level state for the
|
|
44
|
-
let
|
|
45
|
-
/**
|
|
46
|
-
* Derives the
|
|
47
|
-
* first (for user-defined profiles with custom
|
|
40
|
+
// User's service selections, keyed by canonical channel key. Values are the selected service key (e.g., "espn-disneyplus").
|
|
41
|
+
let serviceSelections = new Map();
|
|
42
|
+
// Service Tag System.
|
|
43
|
+
// Module-level state for the service filter. Empty array means "no filter" (all services shown). Non-empty means only these tags are active.
|
|
44
|
+
let enabledServices = [];
|
|
45
|
+
/**
|
|
46
|
+
* Derives the service tag for a channel from its URL domain, falling back to "direct" if no service tag is configured. Checks the channel's explicit profile
|
|
47
|
+
* first (for user-defined profiles with custom serviceTag), then the URL domain via getDomainConfig().
|
|
48
48
|
* @param channel - The channel to derive a tag for.
|
|
49
|
-
* @returns The
|
|
49
|
+
* @returns The service tag string.
|
|
50
50
|
*/
|
|
51
|
-
function
|
|
52
|
-
// If the channel specifies a user-defined profile, use that profile's
|
|
53
|
-
// assignments are grouped under the correct
|
|
51
|
+
function resolveServiceTag(channel) {
|
|
52
|
+
// If the channel specifies a user-defined profile, use that profile's serviceTag rather than deriving from the URL. This ensures channels with explicit profile
|
|
53
|
+
// assignments are grouped under the correct service filter even when their URL domain has a different built-in serviceTag.
|
|
54
54
|
if (channel.profile) {
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
57
|
-
return
|
|
55
|
+
const profileService = resolveUserProfileService(channel.profile);
|
|
56
|
+
if (profileService?.serviceTag) {
|
|
57
|
+
return profileService.serviceTag;
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
const config = getDomainConfig(channel.url);
|
|
61
|
-
return config?.
|
|
61
|
+
return config?.serviceTag ?? "direct";
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
|
-
* Gets the
|
|
65
|
-
* time by
|
|
66
|
-
* :predefined synthetic keys — those only exist inside
|
|
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
|
+
* 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 — 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
|
-
* @returns The
|
|
68
|
+
* @returns The service tag string.
|
|
69
69
|
*/
|
|
70
|
-
export function
|
|
70
|
+
export function getServiceTagForChannel(key) {
|
|
71
71
|
const effectiveKey = stripPredefinedSuffix(key);
|
|
72
|
-
const group =
|
|
72
|
+
const group = serviceGroups.get(effectiveKey);
|
|
73
73
|
// For channels in a group, read the pre-computed tag from the variant entry.
|
|
74
74
|
if (group) {
|
|
75
75
|
const variant = group.variants.find((v) => (v.key === key) || (v.key === effectiveKey));
|
|
@@ -83,10 +83,10 @@ export function getProviderTagForChannel(key) {
|
|
|
83
83
|
if (!channel) {
|
|
84
84
|
return "direct";
|
|
85
85
|
}
|
|
86
|
-
return
|
|
86
|
+
return resolveServiceTag(channel);
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
|
-
* Returns the auth domain for a channel key. Domain is the natural auth boundary — browser cookies and sessions scope to it. Multi-channel
|
|
89
|
+
* 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
90
|
* because all their channels share one domain, and canonical channels work correctly because each has its own domain.
|
|
91
91
|
* @param key - The channel key.
|
|
92
92
|
* @returns The extracted domain from the channel's URL, or empty string if the channel or URL cannot be resolved.
|
|
@@ -101,12 +101,12 @@ export function getAuthDomainForChannel(key) {
|
|
|
101
101
|
return extractDomain(channel.url);
|
|
102
102
|
}
|
|
103
103
|
/**
|
|
104
|
-
* Returns all
|
|
104
|
+
* Returns all service tags for a channel (canonical tag + all variant suffix tags). Used to determine which services offer this channel.
|
|
105
105
|
* @param canonicalKey - The canonical channel key.
|
|
106
|
-
* @returns Array of
|
|
106
|
+
* @returns Array of service tag strings.
|
|
107
107
|
*/
|
|
108
|
-
export function
|
|
109
|
-
const group =
|
|
108
|
+
export function getChannelServiceTags(canonicalKey) {
|
|
109
|
+
const group = serviceGroups.get(canonicalKey);
|
|
110
110
|
// For grouped channels, collect tags directly from the pre-computed variant entries.
|
|
111
111
|
if (group) {
|
|
112
112
|
const tags = new Set();
|
|
@@ -120,47 +120,47 @@ export function getChannelProviderTags(canonicalKey) {
|
|
|
120
120
|
return [...tags];
|
|
121
121
|
}
|
|
122
122
|
// Standalone channel — derive tag from the channel directly.
|
|
123
|
-
return [
|
|
123
|
+
return [getServiceTagForChannel(canonicalKey)];
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
|
-
* Scans all
|
|
127
|
-
* have a
|
|
126
|
+
* Scans all service groups and collects unique service tags with display names. Display names are derived from the service field in DOMAIN_CONFIG entries that
|
|
127
|
+
* have a serviceTag.
|
|
128
128
|
* @returns Array of { displayName, domain, iconUrl, tag } objects sorted alphabetically by display name, with "direct" always first.
|
|
129
129
|
*/
|
|
130
|
-
export function
|
|
130
|
+
export function getAllServiceTags() {
|
|
131
131
|
const tags = new Set();
|
|
132
|
-
// Scan all channels (not just grouped ones) to find all
|
|
132
|
+
// Scan all channels (not just grouped ones) to find all service tags.
|
|
133
133
|
const allKeys = new Set([...Object.keys(channelsRef), ...Object.keys(PREDEFINED_CHANNELS)]);
|
|
134
134
|
for (const key of allKeys) {
|
|
135
|
-
// Skip variant keys — they are covered by
|
|
136
|
-
const group =
|
|
135
|
+
// Skip variant keys — they are covered by getChannelServiceTags() on the canonical.
|
|
136
|
+
const group = serviceGroups.get(key);
|
|
137
137
|
if (group && (group.canonicalKey !== key)) {
|
|
138
138
|
continue;
|
|
139
139
|
}
|
|
140
|
-
const channelTags =
|
|
140
|
+
const channelTags = getChannelServiceTags(key);
|
|
141
141
|
for (const tag of channelTags) {
|
|
142
142
|
tags.add(tag);
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
-
// Scan user domain mappings for
|
|
145
|
+
// Scan user domain mappings for service tags that may not appear in any channel yet (e.g., newly created profiles with no channels assigned).
|
|
146
146
|
const userDomains = getUserDomains();
|
|
147
147
|
for (const config of Object.values(userDomains)) {
|
|
148
|
-
if (config.
|
|
149
|
-
tags.add(config.
|
|
148
|
+
if (config.serviceTag) {
|
|
149
|
+
tags.add(config.serviceTag);
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
-
// Build tag metadata maps from DOMAIN_CONFIG entries. Collects display name, domain, and icon URL for each
|
|
152
|
+
// Build tag metadata maps from DOMAIN_CONFIG entries. Collects display name, domain, and icon URL for each service tag. First match wins for each tag.
|
|
153
153
|
const tagMeta = new Map();
|
|
154
154
|
tagMeta.set("direct", { displayName: "Channel Website" });
|
|
155
155
|
for (const [domain, config] of Object.entries(DOMAIN_CONFIG)) {
|
|
156
|
-
if (config.
|
|
157
|
-
tagMeta.set(config.
|
|
156
|
+
if (config.serviceTag && config.service && !tagMeta.has(config.serviceTag)) {
|
|
157
|
+
tagMeta.set(config.serviceTag, { displayName: config.service, domain, iconUrl: config.iconUrl });
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
// Scan user domain mappings for metadata not covered by built-in DOMAIN_CONFIG.
|
|
161
161
|
for (const [domain, config] of Object.entries(userDomains)) {
|
|
162
|
-
if (config.
|
|
163
|
-
tagMeta.set(config.
|
|
162
|
+
if (config.serviceTag && config.service && !tagMeta.has(config.serviceTag)) {
|
|
163
|
+
tagMeta.set(config.serviceTag, { displayName: config.service, domain, iconUrl: config.iconUrl });
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
// Build result with metadata.
|
|
@@ -182,47 +182,47 @@ export function getAllProviderTags() {
|
|
|
182
182
|
return result;
|
|
183
183
|
}
|
|
184
184
|
/**
|
|
185
|
-
* Gets the current enabled
|
|
186
|
-
* @returns Copy of the enabled
|
|
185
|
+
* Gets the current enabled service tags.
|
|
186
|
+
* @returns Copy of the enabled services array. Empty means no filter (all shown).
|
|
187
187
|
*/
|
|
188
|
-
export function
|
|
189
|
-
return [...
|
|
188
|
+
export function getEnabledServices() {
|
|
189
|
+
return [...enabledServices];
|
|
190
190
|
}
|
|
191
191
|
/**
|
|
192
|
-
* Sets the enabled
|
|
193
|
-
* @param tags - The
|
|
192
|
+
* Sets the enabled service tags. Empty array means "no filter" (all services shown).
|
|
193
|
+
* @param tags - The service tags to enable.
|
|
194
194
|
*/
|
|
195
|
-
export function
|
|
196
|
-
|
|
195
|
+
export function setEnabledServices(tags) {
|
|
196
|
+
enabledServices = [...tags];
|
|
197
197
|
}
|
|
198
198
|
/**
|
|
199
|
-
* Checks if a
|
|
200
|
-
* @param tag - The
|
|
201
|
-
* @returns True if the
|
|
199
|
+
* 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".
|
|
200
|
+
* @param tag - The service tag to check.
|
|
201
|
+
* @returns True if the service is available.
|
|
202
202
|
*/
|
|
203
|
-
export function
|
|
204
|
-
// No filter active — all
|
|
205
|
-
if (
|
|
203
|
+
export function isServiceTagEnabled(tag) {
|
|
204
|
+
// No filter active — all services are enabled.
|
|
205
|
+
if (enabledServices.length === 0) {
|
|
206
206
|
return true;
|
|
207
207
|
}
|
|
208
208
|
// "direct" is always enabled.
|
|
209
209
|
if (tag === "direct") {
|
|
210
210
|
return true;
|
|
211
211
|
}
|
|
212
|
-
return
|
|
212
|
+
return enabledServices.includes(tag);
|
|
213
213
|
}
|
|
214
214
|
/**
|
|
215
|
-
* Centralized availability check for the
|
|
215
|
+
* Centralized availability check for the service filter. Returns true if the channel has at least one variant whose service tag is enabled.
|
|
216
216
|
* @param canonicalKey - The canonical channel key.
|
|
217
|
-
* @returns True if the channel passes the
|
|
217
|
+
* @returns True if the channel passes the service filter.
|
|
218
218
|
*/
|
|
219
|
-
export function
|
|
219
|
+
export function isChannelAvailableByService(canonicalKey) {
|
|
220
220
|
// No filter active — all channels are available.
|
|
221
|
-
if (
|
|
221
|
+
if (enabledServices.length === 0) {
|
|
222
222
|
return true;
|
|
223
223
|
}
|
|
224
|
-
const tags =
|
|
225
|
-
return tags.some((tag) =>
|
|
224
|
+
const tags = getChannelServiceTags(canonicalKey);
|
|
225
|
+
return tags.some((tag) => isServiceTagEnabled(tag));
|
|
226
226
|
}
|
|
227
227
|
/**
|
|
228
228
|
* Checks if a channel in the merged map is a user override of a predefined channel. This uses object reference comparison — getAllChannels() spreads
|
|
@@ -237,17 +237,22 @@ function isUserOverride(key, channels) {
|
|
|
237
237
|
return Boolean(predefined) && (channels[key] !== predefined);
|
|
238
238
|
}
|
|
239
239
|
/**
|
|
240
|
-
* Builds
|
|
240
|
+
* Builds service groups by scanning all channels and grouping by canonicalKey. Variant entries declare their canonical via the canonicalKey field (set by the
|
|
241
241
|
* flattener for predefined channels, by the browse modal for user channels, and by the one-time migration for pre-existing user entries). This is a single
|
|
242
242
|
* pass over the merged channel map — one field, one mechanism for both predefined and user-defined variant relationships.
|
|
243
243
|
*
|
|
244
244
|
* User overrides of predefined channels (same key, different object reference) produce a two-entry group with "Custom" and the original predefined version,
|
|
245
|
-
* even for single-
|
|
245
|
+
* even for single-service channels that don't have canonicalKey-based variants.
|
|
246
|
+
*
|
|
247
|
+
* After building groups, validates stored service selections against the new variant structure and reverts any stale selections to the canonical default. This
|
|
248
|
+
* catches all staleness sources in one place: service key renames across versions, variant removal in code, user-deleted custom variants, and reverted
|
|
249
|
+
* predefined overrides.
|
|
246
250
|
* @param channels - The merged channel map (predefined + user channels).
|
|
251
|
+
* @returns Canonical keys whose service selections were stale and reverted. Empty array if all selections are valid. The caller decides whether to persist.
|
|
247
252
|
*/
|
|
248
|
-
export function
|
|
253
|
+
export function buildServiceGroups(channels) {
|
|
249
254
|
channelsRef = channels;
|
|
250
|
-
|
|
255
|
+
serviceGroups.clear();
|
|
251
256
|
// Pass 1: Collect variant keys grouped by their canonical key. Entries without canonicalKey are canonicals or standalone channels.
|
|
252
257
|
const variantsByCanonical = new Map();
|
|
253
258
|
for (const [key, channel] of Object.entries(channels)) {
|
|
@@ -270,34 +275,50 @@ export function buildProviderGroups(channels) {
|
|
|
270
275
|
continue;
|
|
271
276
|
}
|
|
272
277
|
const variants = [];
|
|
273
|
-
// Handle user override of the canonical entry.
|
|
274
|
-
//
|
|
278
|
+
// 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 — no "Custom" variant needed, just the normal service label with a visual override indicator in the table; (B) the user set a genuinely
|
|
280
|
+
// non-standard URL — "Custom (domain)" is a real service variant and the :predefined entry gives access to the original service URL.
|
|
275
281
|
if (isUserOverride(canonicalKey, channels)) {
|
|
276
282
|
const predefined = PREDEFINED_CHANNELS[canonicalKey];
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
283
|
+
const userDomain = extractDomain(canonical.url);
|
|
284
|
+
const knownDomains = new Set([extractDomain(predefined.url)]);
|
|
285
|
+
for (const variantKey of variantKeys) {
|
|
286
|
+
knownDomains.add(extractDomain(channels[variantKey].url));
|
|
287
|
+
}
|
|
288
|
+
if (knownDomains.has(userDomain)) {
|
|
289
|
+
// 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
|
|
290
|
+
// table renderer handles the visual distinction.
|
|
291
|
+
variants.push({ key: canonicalKey, label: getChannelServiceLabel(canonical), tag: resolveServiceTag(canonical) });
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// Scenario B: genuinely custom URL. "Custom (domain)" is a real service variant. The :predefined entry gives the user a path back to the original
|
|
295
|
+
// predefined service URL without permanently reverting their other customizations.
|
|
296
|
+
variants.push({ key: canonicalKey, label: "Custom (" + extractDomain(canonical.url) + ")", tag: resolveServiceTag(canonical) });
|
|
297
|
+
variants.push({ key: canonicalKey + PREDEFINED_SUFFIX, label: predefined.service ?? getServiceDisplayName(predefined.url),
|
|
298
|
+
tag: resolveServiceTag(predefined) });
|
|
299
|
+
}
|
|
280
300
|
}
|
|
281
301
|
else {
|
|
282
|
-
variants.push({ key: canonicalKey, label:
|
|
302
|
+
variants.push({ key: canonicalKey, label: getChannelServiceLabel(canonical), tag: resolveServiceTag(canonical) });
|
|
283
303
|
}
|
|
284
304
|
variantKeys.sort();
|
|
285
305
|
for (const variantKey of variantKeys) {
|
|
286
306
|
const variant = channels[variantKey];
|
|
287
|
-
variants.push({ key: variantKey, label:
|
|
307
|
+
variants.push({ key: variantKey, label: getChannelServiceLabel(variant), tag: resolveServiceTag(variant) });
|
|
288
308
|
}
|
|
289
309
|
const group = { canonicalKey, variants };
|
|
290
310
|
// Map canonical and all variant keys to this group for easy lookup.
|
|
291
|
-
|
|
311
|
+
serviceGroups.set(canonicalKey, group);
|
|
292
312
|
for (const variantKey of variantKeys) {
|
|
293
|
-
|
|
313
|
+
serviceGroups.set(variantKey, group);
|
|
294
314
|
}
|
|
295
|
-
LOG.debug("config:general", "
|
|
315
|
+
LOG.debug("config:general", "Service group '%s': variants=%s.", canonicalKey, variants.map((v) => v.key).join(", "));
|
|
296
316
|
}
|
|
297
|
-
// Pass 3: Create groups for user overrides of single-
|
|
298
|
-
//
|
|
317
|
+
// 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
|
+
// 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
|
+
// because there is only one service and no dropdown is needed; the modified-dot indicator in the table renderer signals the override.
|
|
299
320
|
for (const key of Object.keys(channels)) {
|
|
300
|
-
if (
|
|
321
|
+
if (serviceGroups.has(key)) {
|
|
301
322
|
continue;
|
|
302
323
|
}
|
|
303
324
|
if (!isUserOverride(key, channels)) {
|
|
@@ -305,37 +326,99 @@ export function buildProviderGroups(channels) {
|
|
|
305
326
|
}
|
|
306
327
|
const userChannel = channels[key];
|
|
307
328
|
const predefined = PREDEFINED_CHANNELS[key];
|
|
329
|
+
// Scenario A: URL domain matches the predefined service. No service group needed — renders as a single-service channel with a modified-dot indicator.
|
|
330
|
+
if (extractDomain(userChannel.url) === extractDomain(predefined.url)) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
// Scenario B: genuinely custom URL. Create a 2-entry group so the user can switch between their custom URL and the predefined service.
|
|
308
334
|
const variants = [
|
|
309
|
-
{ key, label: "Custom (" + extractDomain(userChannel.url) + ")", tag:
|
|
310
|
-
{ key: key + PREDEFINED_SUFFIX, label: predefined.
|
|
335
|
+
{ key, label: "Custom (" + extractDomain(userChannel.url) + ")", tag: resolveServiceTag(userChannel) },
|
|
336
|
+
{ key: key + PREDEFINED_SUFFIX, label: predefined.service ?? getServiceDisplayName(predefined.url), tag: resolveServiceTag(predefined) }
|
|
311
337
|
];
|
|
312
338
|
const group = { canonicalKey: key, variants };
|
|
313
|
-
|
|
314
|
-
LOG.debug("config:general", "
|
|
339
|
+
serviceGroups.set(key, group);
|
|
340
|
+
LOG.debug("config:general", "Service group '%s' (override): variants=%s.", key, variants.map((v) => v.key).join(", "));
|
|
341
|
+
}
|
|
342
|
+
// Build the domain-to-predefined-channel reverse index. This enables the manual add form to show an inline hint when the entered URL matches a predefined
|
|
343
|
+
// channel. Scans only canonical entries in PREDEFINED_CHANNELS (variants share the canonical's URL domain and would produce duplicate results).
|
|
344
|
+
predefinedByDomain.clear();
|
|
345
|
+
for (const [key, channel] of Object.entries(PREDEFINED_CHANNELS)) {
|
|
346
|
+
if (channel.canonicalKey) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const domain = extractDomain(channel.url);
|
|
350
|
+
const group = serviceGroups.get(key);
|
|
351
|
+
const entry = { canonicalKey: key, name: channel.name ?? key, serviceCount: group?.variants.length ?? 1 };
|
|
352
|
+
const existing = predefinedByDomain.get(domain);
|
|
353
|
+
if (existing) {
|
|
354
|
+
existing.push(entry);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
predefinedByDomain.set(domain, [entry]);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Validate stored service selections against the rebuilt groups. Any selection whose variant key no longer exists in the group is stale and reverted to the
|
|
361
|
+
// canonical default. This handles all staleness sources: service key renames across versions, variant removal, user-deleted custom variants, and reverted
|
|
362
|
+
// predefined overrides. The caller decides whether to persist based on whether any keys were cleaned.
|
|
363
|
+
const staleKeys = [];
|
|
364
|
+
for (const [canonicalKey, selection] of serviceSelections) {
|
|
365
|
+
const group = serviceGroups.get(canonicalKey);
|
|
366
|
+
if (!group?.variants.some((v) => v.key === selection)) {
|
|
367
|
+
LOG.warn("Service selection '%s' for channel '%s' is no longer valid. Reverting to default.", selection, canonicalKey);
|
|
368
|
+
serviceSelections.delete(canonicalKey);
|
|
369
|
+
staleKeys.push(canonicalKey);
|
|
370
|
+
}
|
|
315
371
|
}
|
|
372
|
+
return staleKeys;
|
|
316
373
|
}
|
|
374
|
+
// Reverse index mapping concise domains to predefined channel summaries. Built by buildServiceGroups() and queried by findPredefinedByDomain() and
|
|
375
|
+
// getPredefinedDomainMap().
|
|
376
|
+
const predefinedByDomain = new Map();
|
|
317
377
|
/**
|
|
318
|
-
*
|
|
378
|
+
* Returns predefined channels whose canonical URL domain matches the given URL's domain. Used by the manual add form to show an inline hint when the user
|
|
379
|
+
* enters a URL that has predefined channels available. Returns an empty array when no predefined channels match.
|
|
380
|
+
* @param url - The URL to match against predefined channel domains.
|
|
381
|
+
* @returns Array of matching predefined channel summaries with canonical key, display name, and available service count.
|
|
382
|
+
*/
|
|
383
|
+
export function findPredefinedByDomain(url) {
|
|
384
|
+
try {
|
|
385
|
+
const domain = extractDomain(url);
|
|
386
|
+
return predefinedByDomain.get(domain) ?? [];
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return [];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Returns the full domain-to-predefined-channel index for client-side embedding. Used by the channels panel to embed predefined match data so the manual add
|
|
394
|
+
* form can show inline hints without a server round-trip. The returned object is keyed by concise domain with arrays of channel summaries as values.
|
|
395
|
+
* @returns Record mapping domains to predefined channel summaries.
|
|
396
|
+
*/
|
|
397
|
+
export function getPredefinedDomainMap() {
|
|
398
|
+
return Object.fromEntries(predefinedByDomain);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* 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
|
|
319
402
|
* getDomainConfig() which includes user domain mappings. This ordering prevents user domain overrides from corrupting display labels for predefined channel
|
|
320
|
-
* variants — a user mapping a built-in domain to a custom profile should not rename every
|
|
321
|
-
* @param url - The URL to resolve a
|
|
322
|
-
* @returns The
|
|
403
|
+
* variants — a user mapping a built-in domain to a custom profile should not rename every service dropdown entry that uses that domain.
|
|
404
|
+
* @param url - The URL to resolve a service display name for.
|
|
405
|
+
* @returns The service display name, or the concise domain if no service name is configured.
|
|
323
406
|
*/
|
|
324
|
-
export function
|
|
325
|
-
// Prefer built-in DOMAIN_CONFIG
|
|
407
|
+
export function getServiceDisplayName(url) {
|
|
408
|
+
// Prefer built-in DOMAIN_CONFIG service names for stable display. Check by full hostname first (for subdomain-specific entries like tv.youtube.com), then by
|
|
326
409
|
// concise domain (e.g., disneyplus.com).
|
|
327
410
|
try {
|
|
328
411
|
const hostname = new URL(url).hostname;
|
|
329
412
|
const builtinFull = DOMAIN_CONFIG[hostname];
|
|
330
413
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
331
|
-
if (builtinFull?.
|
|
332
|
-
return builtinFull.
|
|
414
|
+
if (builtinFull?.service) {
|
|
415
|
+
return builtinFull.service;
|
|
333
416
|
}
|
|
334
417
|
const concise = extractDomain(url);
|
|
335
418
|
const builtinConcise = DOMAIN_CONFIG[concise];
|
|
336
419
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
337
|
-
if (builtinConcise?.
|
|
338
|
-
return builtinConcise.
|
|
420
|
+
if (builtinConcise?.service) {
|
|
421
|
+
return builtinConcise.service;
|
|
339
422
|
}
|
|
340
423
|
}
|
|
341
424
|
catch {
|
|
@@ -343,61 +426,61 @@ export function getProviderDisplayName(url) {
|
|
|
343
426
|
}
|
|
344
427
|
// For domains not in DOMAIN_CONFIG, fall back to getDomainConfig() which includes user domain mappings.
|
|
345
428
|
const config = getDomainConfig(url);
|
|
346
|
-
return config?.
|
|
429
|
+
return config?.service ?? extractDomain(url);
|
|
347
430
|
}
|
|
348
431
|
/**
|
|
349
|
-
* Resolves
|
|
350
|
-
*
|
|
432
|
+
* 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 → service identity" resolution, used by both tag and label lookups to avoid
|
|
351
434
|
* duplicating the domain scan logic.
|
|
352
435
|
* @param profileKey - The user profile key to resolve.
|
|
353
|
-
* @returns The
|
|
436
|
+
* @returns The service identity from the profile's domain mappings, or undefined if no matching domain mapping exists.
|
|
354
437
|
*/
|
|
355
|
-
function
|
|
438
|
+
function resolveUserProfileService(profileKey) {
|
|
356
439
|
const userDomains = getUserDomains();
|
|
357
440
|
for (const config of Object.values(userDomains)) {
|
|
358
441
|
if (config.profile === profileKey) {
|
|
359
|
-
return {
|
|
442
|
+
return { service: config.service, serviceTag: config.serviceTag };
|
|
360
443
|
}
|
|
361
444
|
}
|
|
362
445
|
return undefined;
|
|
363
446
|
}
|
|
364
447
|
/**
|
|
365
|
-
* Resolves the
|
|
366
|
-
* user domain mappings, then URL-based built-in display name. This ensures channels assigned to user-defined profiles show the profile's
|
|
448
|
+
* Resolves the service display label for a channel. Checks in order: explicit `service` field on the channel, the channel's explicit profile resolved via
|
|
449
|
+
* user domain mappings, then URL-based built-in display name. This ensures channels assigned to user-defined profiles show the profile's service name rather
|
|
367
450
|
* than the built-in name for the URL domain.
|
|
368
451
|
* @param channel - The channel to resolve a label for.
|
|
369
|
-
* @returns The
|
|
452
|
+
* @returns The service display label.
|
|
370
453
|
*/
|
|
371
|
-
export function
|
|
372
|
-
if (channel.
|
|
373
|
-
return channel.
|
|
454
|
+
export function getChannelServiceLabel(channel) {
|
|
455
|
+
if (channel.service) {
|
|
456
|
+
return channel.service;
|
|
374
457
|
}
|
|
375
|
-
// If the channel specifies a user-defined profile, use that profile's
|
|
458
|
+
// If the channel specifies a user-defined profile, use that profile's service name from domain mappings.
|
|
376
459
|
if (channel.profile) {
|
|
377
|
-
const
|
|
378
|
-
if (
|
|
379
|
-
return
|
|
460
|
+
const profileService = resolveUserProfileService(channel.profile);
|
|
461
|
+
if (profileService?.service) {
|
|
462
|
+
return profileService.service;
|
|
380
463
|
}
|
|
381
464
|
}
|
|
382
|
-
return
|
|
465
|
+
return getServiceDisplayName(channel.url);
|
|
383
466
|
}
|
|
384
467
|
// Valid sort field values for the channels table. Exported as the single source of truth for sort field validation, shared by the config POST handler and the
|
|
385
468
|
// playlist endpoint's query parameter validation.
|
|
386
|
-
export const VALID_SORT_FIELDS = new Set(["channelNumber", "channelSelector", "hdhrEnabled", "key", "name", "profile", "
|
|
469
|
+
export const VALID_SORT_FIELDS = new Set(["channelNumber", "channelSelector", "hdhrEnabled", "key", "name", "profile", "service", "stationId", "tags"]);
|
|
387
470
|
/**
|
|
388
471
|
* Extracts a sortable string value from a channel for the specified sort field. Channel numbers are zero-padded to 6 digits for correct numeric ordering within a
|
|
389
|
-
* string comparison.
|
|
472
|
+
* string comparison. Service values use the display label for human-meaningful sort order. This is the single source of truth for channel sort key extraction,
|
|
390
473
|
* shared by both the server-side table renderer and the M3U playlist generator.
|
|
391
|
-
* @param channel - Fallback channel definition, used only when the selected
|
|
392
|
-
* @param key - The canonical channel key. Used for key-based sorting and to resolve the selected
|
|
474
|
+
* @param channel - Fallback channel definition, used only when the selected service variant cannot be resolved (e.g., key not in the merged channel map).
|
|
475
|
+
* @param key - The canonical channel key. Used for key-based sorting and to resolve the selected service variant internally.
|
|
393
476
|
* @param field - The sort field to extract.
|
|
394
477
|
* @returns A lowercase string suitable for comparison-based sorting.
|
|
395
478
|
*/
|
|
396
479
|
export function getChannelSortKey(channel, key, field) {
|
|
397
|
-
// Resolve the selected
|
|
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 —
|
|
398
481
|
// 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
|
|
399
482
|
// (name, stationId, channelNumber), the flattener eagerly sets these on all entries, so the resolved channel has identical values regardless of variant.
|
|
400
|
-
const effective = getResolvedChannel(
|
|
483
|
+
const effective = getResolvedChannel(resolveServiceKey(key)) ?? channel;
|
|
401
484
|
switch (field) {
|
|
402
485
|
case "channelNumber": {
|
|
403
486
|
const num = effective.channelNumber;
|
|
@@ -421,17 +504,17 @@ export function getChannelSortKey(channel, key, field) {
|
|
|
421
504
|
if (effective.profile) {
|
|
422
505
|
return effective.profile.toLowerCase();
|
|
423
506
|
}
|
|
424
|
-
// Auto-detected: check whether the profile resolves to a real
|
|
507
|
+
// Auto-detected: check whether the profile resolves to a real service or falls back to default. Only apply the ! prefix for non-default auto profiles so
|
|
425
508
|
// they sort between explicit profiles and empty profiles.
|
|
426
509
|
const resolved = getProfileForChannel(effective);
|
|
427
510
|
if (resolved.profileName === "default") {
|
|
428
511
|
return "";
|
|
429
512
|
}
|
|
430
|
-
const label =
|
|
513
|
+
const label = getChannelServiceLabel(effective);
|
|
431
514
|
return label ? ("!" + label.toLowerCase()) : "";
|
|
432
515
|
}
|
|
433
|
-
case "
|
|
434
|
-
return
|
|
516
|
+
case "service": {
|
|
517
|
+
return getChannelServiceLabel(effective).toLowerCase();
|
|
435
518
|
}
|
|
436
519
|
case "stationId": {
|
|
437
520
|
const id = effective.stationId;
|
|
@@ -471,128 +554,112 @@ export function compareChannelSort(channelA, keyA, channelB, keyB, field, direct
|
|
|
471
554
|
return nameA.localeCompare(nameB);
|
|
472
555
|
}
|
|
473
556
|
/**
|
|
474
|
-
* Gets the
|
|
557
|
+
* Gets the service group for a channel key. Works with both canonical and variant keys.
|
|
475
558
|
* @param key - Any channel key in the group.
|
|
476
|
-
* @returns The
|
|
559
|
+
* @returns The service group if the channel is part of a multi-service group, undefined otherwise.
|
|
477
560
|
*/
|
|
478
|
-
export function
|
|
479
|
-
return
|
|
561
|
+
export function getServiceGroup(key) {
|
|
562
|
+
return serviceGroups.get(key);
|
|
480
563
|
}
|
|
481
564
|
/**
|
|
482
|
-
* Checks if a channel key is a non-canonical
|
|
565
|
+
* Checks if a channel key is a non-canonical service variant. Used to filter variants from channel listings.
|
|
483
566
|
* @param key - The channel key to check.
|
|
484
|
-
* @returns True if the key is a variant (not canonical) in a
|
|
567
|
+
* @returns True if the key is a variant (not canonical) in a service group.
|
|
485
568
|
*/
|
|
486
|
-
export function
|
|
487
|
-
const group =
|
|
569
|
+
export function isServiceVariant(key) {
|
|
570
|
+
const group = serviceGroups.get(key);
|
|
488
571
|
return (group !== undefined) && (group.canonicalKey !== key);
|
|
489
572
|
}
|
|
490
573
|
/**
|
|
491
|
-
* Checks if a channel has multiple
|
|
574
|
+
* Checks if a channel has multiple service options. Used to determine whether to show a service dropdown in the UI.
|
|
492
575
|
* @param key - The channel key to check.
|
|
493
|
-
* @returns True if the channel has more than one
|
|
576
|
+
* @returns True if the channel has more than one service variant.
|
|
494
577
|
*/
|
|
495
|
-
export function
|
|
496
|
-
const group =
|
|
578
|
+
export function hasMultipleServices(key) {
|
|
579
|
+
const group = serviceGroups.get(key);
|
|
497
580
|
return (group !== undefined) && (group.variants.length > 1);
|
|
498
581
|
}
|
|
499
582
|
/**
|
|
500
583
|
* Gets the canonical key for any channel key. For variant keys, returns the canonical key. For non-grouped or canonical keys, returns the input unchanged.
|
|
501
584
|
* Handles the PREDEFINED_SUFFIX used when a user has overridden a predefined channel.
|
|
502
585
|
* @param key - Any channel key.
|
|
503
|
-
* @returns The canonical key for the channel's
|
|
586
|
+
* @returns The canonical key for the channel's service group, or the input key if not part of a group.
|
|
504
587
|
*/
|
|
505
588
|
export function getCanonicalKey(key) {
|
|
506
589
|
// Strip predefined suffix if present before looking up the group.
|
|
507
590
|
const baseKey = key.endsWith(PREDEFINED_SUFFIX) ? key.slice(0, -PREDEFINED_SUFFIX.length) : key;
|
|
508
|
-
const group =
|
|
591
|
+
const group = serviceGroups.get(baseKey);
|
|
509
592
|
return group?.canonicalKey ?? baseKey;
|
|
510
593
|
}
|
|
511
594
|
/**
|
|
512
|
-
* Sets the user's
|
|
513
|
-
* @param selections -
|
|
595
|
+
* Sets the user's service selections. Called when loading from channels.json.
|
|
596
|
+
* @param selections - Service selections keyed by canonical channel key.
|
|
514
597
|
*/
|
|
515
|
-
export function
|
|
516
|
-
|
|
598
|
+
export function setServiceSelections(selections) {
|
|
599
|
+
serviceSelections = new Map(Object.entries(selections));
|
|
517
600
|
}
|
|
518
601
|
/**
|
|
519
|
-
* Gets all
|
|
520
|
-
* @returns Copy of the
|
|
602
|
+
* Gets all service selections.
|
|
603
|
+
* @returns Copy of the service selections object.
|
|
521
604
|
*/
|
|
522
|
-
export function
|
|
523
|
-
return Object.fromEntries(
|
|
605
|
+
export function getServiceSelections() {
|
|
606
|
+
return Object.fromEntries(serviceSelections);
|
|
524
607
|
}
|
|
525
608
|
/**
|
|
526
|
-
* Gets the
|
|
609
|
+
* Gets the service selection for a specific channel.
|
|
527
610
|
* @param canonicalKey - The canonical channel key.
|
|
528
|
-
* @returns The selected
|
|
611
|
+
* @returns The selected service key, or undefined if using the default.
|
|
529
612
|
*/
|
|
530
|
-
export function
|
|
531
|
-
return
|
|
613
|
+
export function getServiceSelection(canonicalKey) {
|
|
614
|
+
return serviceSelections.get(canonicalKey);
|
|
532
615
|
}
|
|
533
616
|
/**
|
|
534
|
-
* Sets the
|
|
617
|
+
* Sets the service selection for a channel.
|
|
535
618
|
* @param canonicalKey - The canonical channel key.
|
|
536
|
-
* @param
|
|
619
|
+
* @param serviceKey - The selected service key.
|
|
537
620
|
*/
|
|
538
|
-
export function
|
|
621
|
+
export function setServiceSelection(canonicalKey, serviceKey) {
|
|
539
622
|
// If selecting the canonical (default), remove the selection instead of storing it.
|
|
540
|
-
if (
|
|
541
|
-
|
|
623
|
+
if (serviceKey === canonicalKey) {
|
|
624
|
+
serviceSelections.delete(canonicalKey);
|
|
542
625
|
}
|
|
543
626
|
else {
|
|
544
|
-
|
|
627
|
+
serviceSelections.set(canonicalKey, serviceKey);
|
|
545
628
|
}
|
|
546
629
|
}
|
|
547
630
|
/**
|
|
548
|
-
* Resolves a canonical channel key to the actual channel key based on user selection. If the user has selected a specific
|
|
549
|
-
*
|
|
550
|
-
* selection's
|
|
631
|
+
* 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
|
+
* 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
|
+
* selection's service is filtered out.
|
|
634
|
+
*
|
|
635
|
+
* This function is a pure resolver with no side effects. Stale selection cleanup is handled by buildServiceGroups(), which validates all stored selections
|
|
636
|
+
* against the rebuilt variant structure every time groups are rebuilt - both at startup and after runtime channel mutations.
|
|
551
637
|
* @param canonicalKey - The canonical channel key.
|
|
552
|
-
* @returns The resolved
|
|
638
|
+
* @returns The resolved service key to use for streaming.
|
|
553
639
|
*/
|
|
554
|
-
export function
|
|
555
|
-
const selection =
|
|
556
|
-
// No selection stored — use the canonical key (default
|
|
640
|
+
export function resolveServiceKey(canonicalKey) {
|
|
641
|
+
const selection = serviceSelections.get(canonicalKey);
|
|
642
|
+
// 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.
|
|
557
643
|
if (!selection) {
|
|
558
|
-
|
|
559
|
-
if ((enabledProviders.length > 0) && !isProviderTagEnabled(getProviderTagForChannel(canonicalKey))) {
|
|
644
|
+
if ((enabledServices.length > 0) && !isServiceTagEnabled(getServiceTagForChannel(canonicalKey))) {
|
|
560
645
|
return findFirstEnabledVariant(canonicalKey) ?? canonicalKey;
|
|
561
646
|
}
|
|
562
647
|
return canonicalKey;
|
|
563
648
|
}
|
|
564
|
-
//
|
|
565
|
-
if (
|
|
566
|
-
|
|
567
|
-
// Runtime check needed — TypeScript thinks Record indexing always returns a value, but the key may not exist.
|
|
568
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
569
|
-
if (PREDEFINED_CHANNELS[baseKey]) {
|
|
570
|
-
return selection;
|
|
571
|
-
}
|
|
572
|
-
// Predefined channel was removed. Fall through to the invalid selection warning.
|
|
573
|
-
// Runtime check needed — TypeScript thinks Record indexing always returns a value, but the key may not exist.
|
|
574
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
575
|
-
}
|
|
576
|
-
else if (channelsRef[selection]) {
|
|
577
|
-
// Normal selection — validate it exists in the merged channels. If its provider tag is filtered out, find the first enabled variant instead.
|
|
578
|
-
if ((enabledProviders.length > 0) && !isProviderTagEnabled(getProviderTagForChannel(selection))) {
|
|
579
|
-
return findFirstEnabledVariant(canonicalKey) ?? selection;
|
|
580
|
-
}
|
|
581
|
-
return selection;
|
|
649
|
+
// Valid selection — if its service tag is filtered out, fall back to the first enabled variant.
|
|
650
|
+
if ((enabledServices.length > 0) && !isServiceTagEnabled(getServiceTagForChannel(selection))) {
|
|
651
|
+
return findFirstEnabledVariant(canonicalKey) ?? selection;
|
|
582
652
|
}
|
|
583
|
-
|
|
584
|
-
LOG.warn("Provider selection '%s' for channel '%s' no longer exists. Using default.", selection, canonicalKey);
|
|
585
|
-
providerSelections.delete(canonicalKey);
|
|
586
|
-
return canonicalKey;
|
|
653
|
+
return selection;
|
|
587
654
|
}
|
|
588
655
|
/**
|
|
589
|
-
* Finds the first enabled variant for a channel when the current selection's
|
|
590
|
-
*
|
|
656
|
+
* Finds the first enabled variant for a channel when the current selection's service is filtered out. Iterates the group's variants and returns the first whose
|
|
657
|
+
* service tag is enabled.
|
|
591
658
|
* @param canonicalKey - The canonical channel key.
|
|
592
659
|
* @returns The first enabled variant key, or undefined if none are enabled.
|
|
593
660
|
*/
|
|
594
661
|
function findFirstEnabledVariant(canonicalKey) {
|
|
595
|
-
const group =
|
|
662
|
+
const group = serviceGroups.get(canonicalKey);
|
|
596
663
|
if (!group) {
|
|
597
664
|
return undefined;
|
|
598
665
|
}
|
|
@@ -600,21 +667,21 @@ function findFirstEnabledVariant(canonicalKey) {
|
|
|
600
667
|
if (variant.key.endsWith(PREDEFINED_SUFFIX)) {
|
|
601
668
|
continue;
|
|
602
669
|
}
|
|
603
|
-
if (
|
|
670
|
+
if (isServiceTagEnabled(variant.tag)) {
|
|
604
671
|
return variant.key;
|
|
605
672
|
}
|
|
606
673
|
}
|
|
607
674
|
return undefined;
|
|
608
675
|
}
|
|
609
676
|
/**
|
|
610
|
-
* Applies variant inheritance: the variant contributes
|
|
611
|
-
* canonical base. Identity fields describe what the channel IS — name, station ID, tags, channel number — and are independent of which
|
|
677
|
+
* Applies variant inheritance: the variant contributes service-specific fields (url, channelSelector, profile, etc.) while identity fields always come from the
|
|
678
|
+
* canonical base. Identity fields describe what the channel IS — name, station ID, tags, channel number — and are independent of which service serves it. The
|
|
612
679
|
* spread brings in all variant fields, then identity fields are unconditionally overwritten from the canonical. This ensures user overrides on the canonical
|
|
613
|
-
* (e.g., renaming a channel or adding tags) propagate to all
|
|
614
|
-
* The field list comes from CHANNEL_IDENTITY_FIELDS — the single source of truth for the identity/
|
|
615
|
-
* @param variant - The variant channel definition (
|
|
680
|
+
* (e.g., renaming a channel or adding tags) propagate to all service variants, rather than being masked by stale flattener-copied values on the variant.
|
|
681
|
+
* The field list comes from CHANNEL_IDENTITY_FIELDS — the single source of truth for the identity/service-specific separation.
|
|
682
|
+
* @param variant - The variant channel definition (service-specific fields).
|
|
616
683
|
* @param base - The canonical (base) channel with user overrides applied (identity fields).
|
|
617
|
-
* @returns A new Channel with identity fields from the canonical and
|
|
684
|
+
* @returns A new Channel with identity fields from the canonical and service-specific fields from the variant.
|
|
618
685
|
*/
|
|
619
686
|
function applyVariantInheritance(variant, base) {
|
|
620
687
|
const result = { ...variant };
|
|
@@ -630,13 +697,13 @@ function copyField(target, source, key) {
|
|
|
630
697
|
target[key] = source[key];
|
|
631
698
|
}
|
|
632
699
|
/**
|
|
633
|
-
* Gets a channel with inheritance applied. For
|
|
700
|
+
* Gets a channel with inheritance applied. For service variants, this merges the variant's properties with inherited properties from the canonical entry
|
|
634
701
|
* using the live channel data (which includes user overrides). Use `resolvePredefinedVariant()` when you need resolution against pure predefined data.
|
|
635
702
|
* @param key - The channel key (canonical or variant).
|
|
636
703
|
* @returns The complete channel with inheritance applied, or undefined if the channel doesn't exist.
|
|
637
704
|
*/
|
|
638
705
|
export function getResolvedChannel(key) {
|
|
639
|
-
// Handle predefined suffix — return the original predefined channel when user has overridden the canonical but selects the predefined
|
|
706
|
+
// Handle predefined suffix — return the original predefined channel when user has overridden the canonical but selects the predefined service.
|
|
640
707
|
if (key.endsWith(PREDEFINED_SUFFIX)) {
|
|
641
708
|
const baseKey = key.slice(0, -PREDEFINED_SUFFIX.length);
|
|
642
709
|
return PREDEFINED_CHANNELS[baseKey];
|
|
@@ -647,7 +714,7 @@ export function getResolvedChannel(key) {
|
|
|
647
714
|
if (!channel) {
|
|
648
715
|
return undefined;
|
|
649
716
|
}
|
|
650
|
-
const group =
|
|
717
|
+
const group = serviceGroups.get(key);
|
|
651
718
|
// If not part of a group or is the canonical entry, return as-is.
|
|
652
719
|
if (!group || (group.canonicalKey === key)) {
|
|
653
720
|
return channel;
|
|
@@ -664,7 +731,7 @@ export function getResolvedChannel(key) {
|
|
|
664
731
|
}
|
|
665
732
|
/**
|
|
666
733
|
* Resolves a variant channel key against pure predefined data (ignoring user overrides). This is used for revert detection — when the user's edits match a
|
|
667
|
-
* variant's predefined definition, the custom override can be removed and the
|
|
734
|
+
* variant's predefined definition, the custom override can be removed and the service selection switched to that variant. For canonical keys, returns the raw
|
|
668
735
|
* predefined channel. For variant keys, applies the same inheritance rules as `getResolvedChannel()` but against `PREDEFINED_CHANNELS` instead of `channelsRef`.
|
|
669
736
|
* @param key - The channel key (canonical or variant).
|
|
670
737
|
* @returns The channel with inheritance applied against predefined data, or undefined if the key has no predefined definition.
|
|
@@ -676,7 +743,7 @@ export function resolvePredefinedVariant(key) {
|
|
|
676
743
|
if (!channel) {
|
|
677
744
|
return undefined;
|
|
678
745
|
}
|
|
679
|
-
const group =
|
|
746
|
+
const group = serviceGroups.get(key);
|
|
680
747
|
// If not part of a group or is the canonical entry, return the predefined channel as-is.
|
|
681
748
|
if (!group || (group.canonicalKey === key)) {
|
|
682
749
|
return channel;
|
|
@@ -688,4 +755,4 @@ export function resolvePredefinedVariant(key) {
|
|
|
688
755
|
}
|
|
689
756
|
return applyVariantInheritance(channel, canonical);
|
|
690
757
|
}
|
|
691
|
-
//# sourceMappingURL=
|
|
758
|
+
//# sourceMappingURL=services.js.map
|