prismcast 1.7.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 +24 -13
- 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/login.js +11 -8
- package/dist/browser/login.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 +2 -0
- package/dist/channels/index.js +438 -252
- package/dist/channels/index.js.map +1 -1
- package/dist/config/health.d.ts +3 -3
- package/dist/config/health.js +13 -18
- package/dist/config/health.js.map +1 -1
- package/dist/config/index.js +11 -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/profiles.js +10 -10
- package/dist/config/profiles.js.map +1 -1
- 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/services.js +758 -0
- 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 +89 -27
- package/dist/config/userChannels.js +400 -184
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +13 -9
- package/dist/config/userConfig.js +78 -60
- package/dist/config/userConfig.js.map +1 -1
- package/dist/config/userProfiles.d.ts +31 -15
- package/dist/config/userProfiles.js +154 -123
- 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/discover.js +7 -15
- package/dist/hdhr/discover.js.map +1 -1
- package/dist/hdhr/index.js +6 -9
- package/dist/hdhr/index.js.map +1 -1
- package/dist/native/index.d.ts +2 -2
- package/dist/native/index.js +17 -27
- 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.d.ts +2 -2
- package/dist/native/proxy.js +4 -4
- package/dist/native/proxy.js.map +1 -1
- package/dist/routes/assets.js +29 -51
- 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/channels.js +5 -11
- package/dist/routes/channels.js.map +1 -1
- package/dist/routes/components.d.ts +0 -8
- package/dist/routes/components.js +0 -19
- package/dist/routes/components.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 +748 -420
- package/dist/routes/config/channels/routes.js.map +1 -1
- package/dist/routes/config/channels/table.d.ts +21 -6
- package/dist/routes/config/channels/table.js +456 -193
- 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} +47 -42
- package/dist/routes/config/services.js.map +1 -0
- package/dist/routes/config/settings.js +88 -49
- 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/icons.d.ts +16 -0
- package/dist/routes/icons.js +44 -0
- package/dist/routes/icons.js.map +1 -0
- 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 +6 -4
- package/dist/routes/playlist.js +98 -45
- package/dist/routes/playlist.js.map +1 -1
- package/dist/routes/root/content.js +94 -55
- package/dist/routes/root/content.js.map +1 -1
- package/dist/routes/root/scripts/channels.js +620 -478
- package/dist/routes/root/scripts/channels.js.map +1 -1
- package/dist/routes/root/scripts/config.js +884 -964
- package/dist/routes/root/scripts/config.js.map +1 -1
- package/dist/routes/root/scripts/shared.js +468 -188
- package/dist/routes/root/scripts/shared.js.map +1 -1
- package/dist/routes/root/scripts/status.js +252 -207
- package/dist/routes/root/scripts/status.js.map +1 -1
- package/dist/routes/root/styles.js +57 -8
- package/dist/routes/root/styles.js.map +1 -1
- package/dist/routes/services.d.ts +6 -0
- package/dist/routes/{providers.js → services.js} +43 -43
- package/dist/routes/services.js.map +1 -0
- package/dist/routes/ui.d.ts +1 -1
- package/dist/routes/ui.js +3 -1
- package/dist/routes/ui.js.map +1 -1
- package/dist/service/commands.js +8 -43
- 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/codec.d.ts +20 -0
- package/dist/streaming/codec.js +68 -0
- package/dist/streaming/codec.js.map +1 -0
- package/dist/streaming/fmp4Segmenter.d.ts +2 -2
- package/dist/streaming/fmp4Segmenter.js.map +1 -1
- package/dist/streaming/hls.d.ts +4 -4
- package/dist/streaming/hls.js +35 -29
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsResume.js +3 -3
- 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.d.ts +15 -14
- package/dist/streaming/preroll.js +32 -12
- package/dist/streaming/preroll.js.map +1 -1
- package/dist/streaming/registry.d.ts +2 -2
- package/dist/streaming/registry.js +4 -8
- package/dist/streaming/registry.js.map +1 -1
- package/dist/streaming/setup.d.ts +4 -4
- package/dist/streaming/setup.js +28 -34
- 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 +18 -11
- package/dist/types/config.d.ts +3 -2
- package/dist/types/index.d.ts +4 -3
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- 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 +8 -16
- package/dist/types/streaming.js +5 -1
- package/dist/types/streaming.js.map +1 -1
- package/dist/upgrade/commands.js +1 -16
- package/dist/upgrade/commands.js.map +1 -1
- package/dist/utils/chromeFetch.js +1 -1
- package/dist/utils/cliOutput.d.ts +10 -0
- package/dist/utils/cliOutput.js +17 -0
- package/dist/utils/cliOutput.js.map +1 -0
- 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 +21 -4
- package/dist/utils/format.js +45 -7
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.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/retry.js +2 -4
- package/dist/utils/retry.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 +0 -678
- 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
package/dist/config/providers.js
DELETED
|
@@ -1,678 +0,0 @@
|
|
|
1
|
-
import { DOMAIN_CONFIG, getDomainConfig } from "./sites.js";
|
|
2
|
-
import { LOG, extractDomain } from "../utils/index.js";
|
|
3
|
-
import { PREDEFINED_CHANNELS } from "../channels/index.js";
|
|
4
|
-
import { getProfileForChannel } from "./profiles.js";
|
|
5
|
-
import { getUserDomains } from "./userProfiles.js";
|
|
6
|
-
/* Provider groups allow multiple streaming providers to offer the same content. For example, ESPN can be watched via ESPN.com (native) or Disney+.
|
|
7
|
-
*
|
|
8
|
-
* All variant relationships — predefined and user-defined — are expressed via the canonicalKey field on Channel. The flattener sets canonicalKey on predefined
|
|
9
|
-
* variant entries, the browse modal sets it on user variant entries, and the one-time migration stamps it on pre-existing user entries. buildProviderGroups
|
|
10
|
-
* scans all channels once and groups by canonicalKey. One field, one mechanism, one code path.
|
|
11
|
-
*
|
|
12
|
-
* User overrides: When a user defines a channel with the same key as a predefined channel, both versions appear in the provider dropdown. The user's custom version
|
|
13
|
-
* 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
|
|
14
|
-
* user's version. This allows users to switch between their custom definition and the original at any time.
|
|
15
|
-
*
|
|
16
|
-
* User selections are stored in channels.json (in the data directory) under the `providerSelections` key and persist across restarts.
|
|
17
|
-
*/
|
|
18
|
-
// Suffix appended to channel keys to reference the original predefined channel when a user has overridden it. For example, "espn:predefined" references the original
|
|
19
|
-
// predefined ESPN channel when the user has created a custom "espn" entry.
|
|
20
|
-
const PREDEFINED_SUFFIX = ":predefined";
|
|
21
|
-
/**
|
|
22
|
-
* 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
|
|
23
|
-
* predefined channel — the original predefined entry gets this suffix to coexist with the user's custom version in the provider dropdown. Functions that look up
|
|
24
|
-
* channel data by key must strip the suffix to find the actual channel entry.
|
|
25
|
-
* @param key - The channel key, possibly with :predefined suffix.
|
|
26
|
-
* @returns The base key without the suffix.
|
|
27
|
-
*/
|
|
28
|
-
function stripPredefinedSuffix(key) {
|
|
29
|
-
return key.endsWith(PREDEFINED_SUFFIX) ? key.slice(0, -PREDEFINED_SUFFIX.length) : key;
|
|
30
|
-
}
|
|
31
|
-
// Module-level storage for provider groups, keyed by canonical channel key.
|
|
32
|
-
const providerGroups = new Map();
|
|
33
|
-
// Reference to the channels map for inheritance resolution.
|
|
34
|
-
let channelsRef = {};
|
|
35
|
-
// User's provider selections, keyed by canonical channel key. Values are the selected provider key (e.g., "espn-disneyplus").
|
|
36
|
-
let providerSelections = new Map();
|
|
37
|
-
// Provider Tag System.
|
|
38
|
-
// Module-level state for the provider filter. Empty array means "no filter" (all providers shown). Non-empty means only these tags are active.
|
|
39
|
-
let enabledProviders = [];
|
|
40
|
-
/**
|
|
41
|
-
* Derives the provider tag for a channel from its URL domain, falling back to "direct" if no provider tag is configured. Checks the channel's explicit profile
|
|
42
|
-
* first (for user-defined profiles with custom providerTag), then the URL domain via getDomainConfig().
|
|
43
|
-
* @param channel - The channel to derive a tag for.
|
|
44
|
-
* @returns The provider tag string.
|
|
45
|
-
*/
|
|
46
|
-
function resolveProviderTag(channel) {
|
|
47
|
-
// If the channel specifies a user-defined profile, use that profile's providerTag rather than deriving from the URL. This ensures channels with explicit profile
|
|
48
|
-
// assignments are grouped under the correct provider filter even when their URL domain has a different built-in providerTag.
|
|
49
|
-
if (channel.profile) {
|
|
50
|
-
const profileProvider = resolveUserProfileProvider(channel.profile);
|
|
51
|
-
if (profileProvider?.providerTag) {
|
|
52
|
-
return profileProvider.providerTag;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
const config = getDomainConfig(channel.url);
|
|
56
|
-
return config?.providerTag ?? "direct";
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Gets the provider tag for a channel key. For channels in a provider group, reads the pre-computed tag from the group variant entry (computed at group-building
|
|
60
|
-
* time by buildProviderGroups). For standalone channels not in any group, derives the tag from the channel's URL domain. This function should not be called with
|
|
61
|
-
* :predefined synthetic keys — those only exist inside provider groups and their tags are available via the group's variant entries.
|
|
62
|
-
* @param key - The channel key.
|
|
63
|
-
* @returns The provider tag string.
|
|
64
|
-
*/
|
|
65
|
-
export function getProviderTagForChannel(key) {
|
|
66
|
-
const effectiveKey = stripPredefinedSuffix(key);
|
|
67
|
-
const group = providerGroups.get(effectiveKey);
|
|
68
|
-
// For channels in a group, read the pre-computed tag from the variant entry.
|
|
69
|
-
if (group) {
|
|
70
|
-
const variant = group.variants.find((v) => (v.key === key) || (v.key === effectiveKey));
|
|
71
|
-
if (variant) {
|
|
72
|
-
return variant.tag;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
// For standalone channels not in any group, derive from the channel's URL domain.
|
|
76
|
-
const channel = channelsRef[effectiveKey] ?? PREDEFINED_CHANNELS[effectiveKey];
|
|
77
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
78
|
-
if (!channel) {
|
|
79
|
-
return "direct";
|
|
80
|
-
}
|
|
81
|
-
return resolveProviderTag(channel);
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Returns the auth domain for a channel key. Domain is the natural auth boundary — browser cookies and sessions scope to it. Multi-channel providers work correctly
|
|
85
|
-
* because all their channels share one domain, and canonical channels work correctly because each has its own domain.
|
|
86
|
-
* @param key - The channel key.
|
|
87
|
-
* @returns The extracted domain from the channel's URL, or empty string if the channel or URL cannot be resolved.
|
|
88
|
-
*/
|
|
89
|
-
export function getAuthDomainForChannel(key) {
|
|
90
|
-
const effectiveKey = stripPredefinedSuffix(key);
|
|
91
|
-
const channel = channelsRef[effectiveKey] ?? PREDEFINED_CHANNELS[effectiveKey];
|
|
92
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
93
|
-
if (!channel?.url) {
|
|
94
|
-
return "";
|
|
95
|
-
}
|
|
96
|
-
return extractDomain(channel.url);
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Returns all provider tags for a channel (canonical tag + all variant suffix tags). Used to determine which providers offer this channel.
|
|
100
|
-
* @param canonicalKey - The canonical channel key.
|
|
101
|
-
* @returns Array of provider tag strings.
|
|
102
|
-
*/
|
|
103
|
-
export function getChannelProviderTags(canonicalKey) {
|
|
104
|
-
const group = providerGroups.get(canonicalKey);
|
|
105
|
-
// For grouped channels, collect tags directly from the pre-computed variant entries.
|
|
106
|
-
if (group) {
|
|
107
|
-
const tags = new Set();
|
|
108
|
-
for (const variant of group.variants) {
|
|
109
|
-
// Skip predefined suffix variants — they share the canonical's tag (but derived from the predefined URL, which may differ from the user override).
|
|
110
|
-
if (variant.key.endsWith(PREDEFINED_SUFFIX)) {
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
tags.add(variant.tag);
|
|
114
|
-
}
|
|
115
|
-
return [...tags];
|
|
116
|
-
}
|
|
117
|
-
// Standalone channel — derive tag from the channel directly.
|
|
118
|
-
return [getProviderTagForChannel(canonicalKey)];
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Scans all provider groups and collects unique provider tags with display names. Display names are derived from the provider field in DOMAIN_CONFIG entries that
|
|
122
|
-
* have a providerTag.
|
|
123
|
-
* @returns Array of { displayName, domain, iconUrl, tag } objects sorted alphabetically by display name, with "direct" always first.
|
|
124
|
-
*/
|
|
125
|
-
export function getAllProviderTags() {
|
|
126
|
-
const tags = new Set();
|
|
127
|
-
// Scan all channels (not just grouped ones) to find all provider tags.
|
|
128
|
-
const allKeys = new Set([...Object.keys(channelsRef), ...Object.keys(PREDEFINED_CHANNELS)]);
|
|
129
|
-
for (const key of allKeys) {
|
|
130
|
-
// Skip variant keys — they are covered by getChannelProviderTags() on the canonical.
|
|
131
|
-
const group = providerGroups.get(key);
|
|
132
|
-
if (group && (group.canonicalKey !== key)) {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
const channelTags = getChannelProviderTags(key);
|
|
136
|
-
for (const tag of channelTags) {
|
|
137
|
-
tags.add(tag);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
// Scan user domain mappings for provider tags that may not appear in any channel yet (e.g., newly created profiles with no channels assigned).
|
|
141
|
-
const userDomains = getUserDomains();
|
|
142
|
-
for (const config of Object.values(userDomains)) {
|
|
143
|
-
if (config.providerTag) {
|
|
144
|
-
tags.add(config.providerTag);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
// Build tag metadata maps from DOMAIN_CONFIG entries. Collects display name, domain, and icon URL for each provider tag. First match wins for each tag.
|
|
148
|
-
const tagMeta = new Map();
|
|
149
|
-
tagMeta.set("direct", { displayName: "Channel Website" });
|
|
150
|
-
for (const [domain, config] of Object.entries(DOMAIN_CONFIG)) {
|
|
151
|
-
if (config.providerTag && config.provider && !tagMeta.has(config.providerTag)) {
|
|
152
|
-
tagMeta.set(config.providerTag, { displayName: config.provider, domain, iconUrl: config.iconUrl });
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
// Scan user domain mappings for metadata not covered by built-in DOMAIN_CONFIG.
|
|
156
|
-
for (const [domain, config] of Object.entries(userDomains)) {
|
|
157
|
-
if (config.providerTag && config.provider && !tagMeta.has(config.providerTag)) {
|
|
158
|
-
tagMeta.set(config.providerTag, { displayName: config.provider, domain, iconUrl: config.iconUrl });
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
// Build result with metadata.
|
|
162
|
-
const result = [];
|
|
163
|
-
for (const tag of tags) {
|
|
164
|
-
const meta = tagMeta.get(tag);
|
|
165
|
-
result.push({ displayName: meta?.displayName ?? tag, domain: meta?.domain, iconUrl: meta?.iconUrl, tag });
|
|
166
|
-
}
|
|
167
|
-
// Sort alphabetically by display name, but keep "direct" first.
|
|
168
|
-
result.sort((a, b) => {
|
|
169
|
-
if (a.tag === "direct") {
|
|
170
|
-
return -1;
|
|
171
|
-
}
|
|
172
|
-
if (b.tag === "direct") {
|
|
173
|
-
return 1;
|
|
174
|
-
}
|
|
175
|
-
return a.displayName.localeCompare(b.displayName);
|
|
176
|
-
});
|
|
177
|
-
return result;
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Gets the current enabled provider tags.
|
|
181
|
-
* @returns Copy of the enabled providers array. Empty means no filter (all shown).
|
|
182
|
-
*/
|
|
183
|
-
export function getEnabledProviders() {
|
|
184
|
-
return [...enabledProviders];
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Sets the enabled provider tags. Empty array means "no filter" (all providers shown).
|
|
188
|
-
* @param tags - The provider tags to enable.
|
|
189
|
-
*/
|
|
190
|
-
export function setEnabledProviders(tags) {
|
|
191
|
-
enabledProviders = [...tags];
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Checks if a provider tag is currently enabled. Returns true if the tag is enabled, if no filter is active (empty set), or if the tag is "direct".
|
|
195
|
-
* @param tag - The provider tag to check.
|
|
196
|
-
* @returns True if the provider is available.
|
|
197
|
-
*/
|
|
198
|
-
export function isProviderTagEnabled(tag) {
|
|
199
|
-
// No filter active — all providers are enabled.
|
|
200
|
-
if (enabledProviders.length === 0) {
|
|
201
|
-
return true;
|
|
202
|
-
}
|
|
203
|
-
// "direct" is always enabled.
|
|
204
|
-
if (tag === "direct") {
|
|
205
|
-
return true;
|
|
206
|
-
}
|
|
207
|
-
return enabledProviders.includes(tag);
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Centralized availability check for the provider filter. Returns true if the channel has at least one variant whose provider tag is enabled.
|
|
211
|
-
* @param canonicalKey - The canonical channel key.
|
|
212
|
-
* @returns True if the channel passes the provider filter.
|
|
213
|
-
*/
|
|
214
|
-
export function isChannelAvailableByProvider(canonicalKey) {
|
|
215
|
-
// No filter active — all channels are available.
|
|
216
|
-
if (enabledProviders.length === 0) {
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
const tags = getChannelProviderTags(canonicalKey);
|
|
220
|
-
return tags.some((tag) => isProviderTagEnabled(tag));
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Checks if a channel in the merged map is a user override of a predefined channel. This uses object reference comparison — getAllChannels() spreads
|
|
224
|
-
* PREDEFINED_CHANNELS directly into the result, so if the reference differs, a user channel has replaced the predefined one.
|
|
225
|
-
* @param key - The channel key to check.
|
|
226
|
-
* @param channels - The merged channel map.
|
|
227
|
-
* @returns True if the channel is a user override of a predefined channel.
|
|
228
|
-
*/
|
|
229
|
-
function isUserOverride(key, channels) {
|
|
230
|
-
const predefined = PREDEFINED_CHANNELS[key];
|
|
231
|
-
// A channel is an override if: (1) a predefined version exists, and (2) the merged map has a different object reference.
|
|
232
|
-
return Boolean(predefined) && (channels[key] !== predefined);
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Builds provider groups by scanning all channels and grouping by canonicalKey. Variant entries declare their canonical via the canonicalKey field (set by the
|
|
236
|
-
* 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
|
|
237
|
-
* pass over the merged channel map — one field, one mechanism for both predefined and user-defined variant relationships.
|
|
238
|
-
*
|
|
239
|
-
* User overrides of predefined channels (same key, different object reference) produce a two-entry group with "Custom" and the original predefined version,
|
|
240
|
-
* even for single-provider channels that don't have canonicalKey-based variants.
|
|
241
|
-
* @param channels - The merged channel map (predefined + user channels).
|
|
242
|
-
*/
|
|
243
|
-
export function buildProviderGroups(channels) {
|
|
244
|
-
channelsRef = channels;
|
|
245
|
-
providerGroups.clear();
|
|
246
|
-
// Pass 1: Collect variant keys grouped by their canonical key. Entries without canonicalKey are canonicals or standalone channels.
|
|
247
|
-
const variantsByCanonical = new Map();
|
|
248
|
-
for (const [key, channel] of Object.entries(channels)) {
|
|
249
|
-
if (!channel.canonicalKey) {
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
const existing = variantsByCanonical.get(channel.canonicalKey);
|
|
253
|
-
if (existing) {
|
|
254
|
-
existing.push(key);
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
variantsByCanonical.set(channel.canonicalKey, [key]);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
// Pass 2: Build groups from the collected variants.
|
|
261
|
-
for (const [canonicalKey, variantKeys] of variantsByCanonical) {
|
|
262
|
-
const canonical = channels[canonicalKey];
|
|
263
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
264
|
-
if (!canonical) {
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
const variants = [];
|
|
268
|
-
// Handle user override of the canonical entry. The :predefined variant's tag is derived from the predefined channel (not the user override) so that provider
|
|
269
|
-
// filtering correctly reflects the predefined channel's provider, not the user's custom URL.
|
|
270
|
-
if (isUserOverride(canonicalKey, channels)) {
|
|
271
|
-
const predefined = PREDEFINED_CHANNELS[canonicalKey];
|
|
272
|
-
variants.push({ key: canonicalKey, label: "Custom (" + extractDomain(canonical.url) + ")", tag: resolveProviderTag(canonical) });
|
|
273
|
-
variants.push({ key: canonicalKey + PREDEFINED_SUFFIX, label: predefined.provider ?? getProviderDisplayName(predefined.url),
|
|
274
|
-
tag: resolveProviderTag(predefined) });
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
variants.push({ key: canonicalKey, label: getChannelProviderLabel(canonical), tag: resolveProviderTag(canonical) });
|
|
278
|
-
}
|
|
279
|
-
variantKeys.sort();
|
|
280
|
-
for (const variantKey of variantKeys) {
|
|
281
|
-
const variant = channels[variantKey];
|
|
282
|
-
variants.push({ key: variantKey, label: getChannelProviderLabel(variant), tag: resolveProviderTag(variant) });
|
|
283
|
-
}
|
|
284
|
-
const group = { canonicalKey, variants };
|
|
285
|
-
// Map canonical and all variant keys to this group for easy lookup.
|
|
286
|
-
providerGroups.set(canonicalKey, group);
|
|
287
|
-
for (const variantKey of variantKeys) {
|
|
288
|
-
providerGroups.set(variantKey, group);
|
|
289
|
-
}
|
|
290
|
-
LOG.debug("config:general", "Provider group '%s': variants=%s.", canonicalKey, variants.map((v) => v.key).join(", "));
|
|
291
|
-
}
|
|
292
|
-
// Pass 3: Create groups for user overrides of single-provider predefined channels. These don't have canonicalKey-based variants but the user's custom version
|
|
293
|
-
// should be toggleable against the predefined original.
|
|
294
|
-
for (const key of Object.keys(channels)) {
|
|
295
|
-
if (providerGroups.has(key)) {
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
if (!isUserOverride(key, channels)) {
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
const userChannel = channels[key];
|
|
302
|
-
const predefined = PREDEFINED_CHANNELS[key];
|
|
303
|
-
const variants = [
|
|
304
|
-
{ key, label: "Custom (" + extractDomain(userChannel.url) + ")", tag: resolveProviderTag(userChannel) },
|
|
305
|
-
{ key: key + PREDEFINED_SUFFIX, label: predefined.provider ?? getProviderDisplayName(predefined.url), tag: resolveProviderTag(predefined) }
|
|
306
|
-
];
|
|
307
|
-
const group = { canonicalKey: key, variants };
|
|
308
|
-
providerGroups.set(key, group);
|
|
309
|
-
LOG.debug("config:general", "Provider group '%s' (override): variants=%s.", key, variants.map((v) => v.key).join(", "));
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* Resolves a URL to a friendly provider display name. Checks built-in DOMAIN_CONFIG first for a stable, well-known provider name, then falls back to
|
|
314
|
-
* getDomainConfig() which includes user domain mappings. This ordering prevents user domain overrides from corrupting display labels for predefined channel
|
|
315
|
-
* variants — a user mapping a built-in domain to a custom profile should not rename every provider dropdown entry that uses that domain.
|
|
316
|
-
* @param url - The URL to resolve a provider display name for.
|
|
317
|
-
* @returns The provider display name, or the concise domain if no provider name is configured.
|
|
318
|
-
*/
|
|
319
|
-
export function getProviderDisplayName(url) {
|
|
320
|
-
// Prefer built-in DOMAIN_CONFIG provider names for stable display. Check by full hostname first (for subdomain-specific entries like tv.youtube.com), then by
|
|
321
|
-
// concise domain (e.g., disneyplus.com).
|
|
322
|
-
try {
|
|
323
|
-
const hostname = new URL(url).hostname;
|
|
324
|
-
const builtinFull = DOMAIN_CONFIG[hostname];
|
|
325
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
326
|
-
if (builtinFull?.provider) {
|
|
327
|
-
return builtinFull.provider;
|
|
328
|
-
}
|
|
329
|
-
const concise = extractDomain(url);
|
|
330
|
-
const builtinConcise = DOMAIN_CONFIG[concise];
|
|
331
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
332
|
-
if (builtinConcise?.provider) {
|
|
333
|
-
return builtinConcise.provider;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
catch {
|
|
337
|
-
// Invalid URL — fall through to getDomainConfig.
|
|
338
|
-
}
|
|
339
|
-
// For domains not in DOMAIN_CONFIG, fall back to getDomainConfig() which includes user domain mappings.
|
|
340
|
-
const config = getDomainConfig(url);
|
|
341
|
-
return config?.provider ?? extractDomain(url);
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
|
-
* Resolves provider identity (tag and display name) for a user-defined profile by scanning its domain mappings. Returns the first matching domain config's
|
|
345
|
-
* providerTag and provider name. This is the single source of truth for "profile key → provider identity" resolution, used by both tag and label lookups to avoid
|
|
346
|
-
* duplicating the domain scan logic.
|
|
347
|
-
* @param profileKey - The user profile key to resolve.
|
|
348
|
-
* @returns The provider identity from the profile's domain mappings, or undefined if no matching domain mapping exists.
|
|
349
|
-
*/
|
|
350
|
-
function resolveUserProfileProvider(profileKey) {
|
|
351
|
-
const userDomains = getUserDomains();
|
|
352
|
-
for (const config of Object.values(userDomains)) {
|
|
353
|
-
if (config.profile === profileKey) {
|
|
354
|
-
return { provider: config.provider, providerTag: config.providerTag };
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
return undefined;
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* Resolves the provider display label for a channel. Checks in order: explicit `provider` field on the channel, the channel's explicit profile resolved via
|
|
361
|
-
* user domain mappings, then URL-based built-in display name. This ensures channels assigned to user-defined profiles show the profile's provider name rather
|
|
362
|
-
* than the built-in name for the URL domain.
|
|
363
|
-
* @param channel - The channel to resolve a label for.
|
|
364
|
-
* @returns The provider display label.
|
|
365
|
-
*/
|
|
366
|
-
export function getChannelProviderLabel(channel) {
|
|
367
|
-
if (channel.provider) {
|
|
368
|
-
return channel.provider;
|
|
369
|
-
}
|
|
370
|
-
// If the channel specifies a user-defined profile, use that profile's provider name from domain mappings.
|
|
371
|
-
if (channel.profile) {
|
|
372
|
-
const profileProvider = resolveUserProfileProvider(channel.profile);
|
|
373
|
-
if (profileProvider?.provider) {
|
|
374
|
-
return profileProvider.provider;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
return getProviderDisplayName(channel.url);
|
|
378
|
-
}
|
|
379
|
-
// 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
|
|
380
|
-
// playlist endpoint's query parameter validation.
|
|
381
|
-
export const VALID_SORT_FIELDS = new Set(["channelNumber", "channelSelector", "hdhrEnabled", "key", "name", "profile", "provider", "stationId"]);
|
|
382
|
-
/**
|
|
383
|
-
* 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
|
|
384
|
-
* string comparison. Provider values use the display label for human-meaningful sort order. This is the single source of truth for channel sort key extraction,
|
|
385
|
-
* shared by both the server-side table renderer and the M3U playlist generator.
|
|
386
|
-
* @param channel - Fallback channel definition, used only when the selected provider variant cannot be resolved (e.g., key not in the merged channel map).
|
|
387
|
-
* @param key - The canonical channel key. Used for key-based sorting and to resolve the selected provider variant internally.
|
|
388
|
-
* @param field - The sort field to extract.
|
|
389
|
-
* @returns A lowercase string suitable for comparison-based sorting.
|
|
390
|
-
*/
|
|
391
|
-
export function getChannelSortKey(channel, key, field) {
|
|
392
|
-
// Resolve the selected provider variant so all sort keys reflect the user's provider selection. For URL-dependent fields (profile, provider), this is essential —
|
|
393
|
-
// 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
|
|
394
|
-
// (name, stationId, channelNumber), the flattener eagerly sets these on all entries, so the resolved channel has identical values regardless of variant.
|
|
395
|
-
const effective = getResolvedChannel(resolveProviderKey(key)) ?? channel;
|
|
396
|
-
switch (field) {
|
|
397
|
-
case "channelNumber": {
|
|
398
|
-
const num = effective.channelNumber;
|
|
399
|
-
return num ? String(num).padStart(6, "0") : "zzzzzz";
|
|
400
|
-
}
|
|
401
|
-
case "channelSelector": {
|
|
402
|
-
return (effective.channelSelector ?? "").toLowerCase();
|
|
403
|
-
}
|
|
404
|
-
case "hdhrEnabled": {
|
|
405
|
-
// Sort enabled channels before disabled. "0" (enabled/absent) sorts before "1" (disabled).
|
|
406
|
-
return (effective.hdhrEnabled === false) ? "1" : "0";
|
|
407
|
-
}
|
|
408
|
-
case "key": {
|
|
409
|
-
return key.toLowerCase();
|
|
410
|
-
}
|
|
411
|
-
case "name": {
|
|
412
|
-
return (effective.name ?? key).toLowerCase();
|
|
413
|
-
}
|
|
414
|
-
case "profile": {
|
|
415
|
-
// Explicit profile: sort by its name.
|
|
416
|
-
if (effective.profile) {
|
|
417
|
-
return effective.profile.toLowerCase();
|
|
418
|
-
}
|
|
419
|
-
// Auto-detected: check whether the profile resolves to a real provider or falls back to default. Only apply the ! prefix for non-default auto profiles so
|
|
420
|
-
// they sort between explicit profiles and empty profiles.
|
|
421
|
-
const resolved = getProfileForChannel(effective);
|
|
422
|
-
if (resolved.profileName === "default") {
|
|
423
|
-
return "";
|
|
424
|
-
}
|
|
425
|
-
const label = getChannelProviderLabel(effective);
|
|
426
|
-
return label ? ("!" + label.toLowerCase()) : "";
|
|
427
|
-
}
|
|
428
|
-
case "provider": {
|
|
429
|
-
return getChannelProviderLabel(effective).toLowerCase();
|
|
430
|
-
}
|
|
431
|
-
case "stationId": {
|
|
432
|
-
const id = effective.stationId;
|
|
433
|
-
return id ? id.padStart(6, "0") : "zzzzzz";
|
|
434
|
-
}
|
|
435
|
-
default: {
|
|
436
|
-
return key.toLowerCase();
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
/**
|
|
441
|
-
* 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
|
|
442
|
-
* 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
|
|
443
|
-
* render, client re-sort, and M3U playlist — to prevent ordering divergence.
|
|
444
|
-
* @param channelA - First channel definition.
|
|
445
|
-
* @param keyA - First channel key.
|
|
446
|
-
* @param channelB - Second channel definition.
|
|
447
|
-
* @param keyB - Second channel key.
|
|
448
|
-
* @param field - The sort field to compare.
|
|
449
|
-
* @param direction - Sort direction for the primary field.
|
|
450
|
-
* @returns A negative, zero, or positive number for sort ordering.
|
|
451
|
-
*/
|
|
452
|
-
export function compareChannelSort(channelA, keyA, channelB, keyB, field, direction) {
|
|
453
|
-
const valA = getChannelSortKey(channelA, keyA, field);
|
|
454
|
-
const valB = getChannelSortKey(channelB, keyB, field);
|
|
455
|
-
const cmp = (direction === "asc") ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
|
456
|
-
if (cmp !== 0) {
|
|
457
|
-
return cmp;
|
|
458
|
-
}
|
|
459
|
-
// Tiebreaker: channel name ascending regardless of primary direction.
|
|
460
|
-
const nameA = (channelA.name ?? keyA).toLowerCase();
|
|
461
|
-
const nameB = (channelB.name ?? keyB).toLowerCase();
|
|
462
|
-
return nameA.localeCompare(nameB);
|
|
463
|
-
}
|
|
464
|
-
/**
|
|
465
|
-
* Gets the provider group for a channel key. Works with both canonical and variant keys.
|
|
466
|
-
* @param key - Any channel key in the group.
|
|
467
|
-
* @returns The provider group if the channel is part of a multi-provider group, undefined otherwise.
|
|
468
|
-
*/
|
|
469
|
-
export function getProviderGroup(key) {
|
|
470
|
-
return providerGroups.get(key);
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
* Checks if a channel key is a non-canonical provider variant. Used to filter variants from channel listings.
|
|
474
|
-
* @param key - The channel key to check.
|
|
475
|
-
* @returns True if the key is a variant (not canonical) in a provider group.
|
|
476
|
-
*/
|
|
477
|
-
export function isProviderVariant(key) {
|
|
478
|
-
const group = providerGroups.get(key);
|
|
479
|
-
return (group !== undefined) && (group.canonicalKey !== key);
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* Checks if a channel has multiple provider options. Used to determine whether to show a provider dropdown in the UI.
|
|
483
|
-
* @param key - The channel key to check.
|
|
484
|
-
* @returns True if the channel has more than one provider variant.
|
|
485
|
-
*/
|
|
486
|
-
export function hasMultipleProviders(key) {
|
|
487
|
-
const group = providerGroups.get(key);
|
|
488
|
-
return (group !== undefined) && (group.variants.length > 1);
|
|
489
|
-
}
|
|
490
|
-
/**
|
|
491
|
-
* 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.
|
|
492
|
-
* Handles the PREDEFINED_SUFFIX used when a user has overridden a predefined channel.
|
|
493
|
-
* @param key - Any channel key.
|
|
494
|
-
* @returns The canonical key for the channel's provider group, or the input key if not part of a group.
|
|
495
|
-
*/
|
|
496
|
-
export function getCanonicalKey(key) {
|
|
497
|
-
// Strip predefined suffix if present before looking up the group.
|
|
498
|
-
const baseKey = key.endsWith(PREDEFINED_SUFFIX) ? key.slice(0, -PREDEFINED_SUFFIX.length) : key;
|
|
499
|
-
const group = providerGroups.get(baseKey);
|
|
500
|
-
return group?.canonicalKey ?? baseKey;
|
|
501
|
-
}
|
|
502
|
-
/**
|
|
503
|
-
* Sets the user's provider selections. Called when loading from channels.json.
|
|
504
|
-
* @param selections - Provider selections keyed by canonical channel key.
|
|
505
|
-
*/
|
|
506
|
-
export function setProviderSelections(selections) {
|
|
507
|
-
providerSelections = new Map(Object.entries(selections));
|
|
508
|
-
}
|
|
509
|
-
/**
|
|
510
|
-
* Gets all provider selections.
|
|
511
|
-
* @returns Copy of the provider selections object.
|
|
512
|
-
*/
|
|
513
|
-
export function getProviderSelections() {
|
|
514
|
-
return Object.fromEntries(providerSelections);
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* Gets the provider selection for a specific channel.
|
|
518
|
-
* @param canonicalKey - The canonical channel key.
|
|
519
|
-
* @returns The selected provider key, or undefined if using the default.
|
|
520
|
-
*/
|
|
521
|
-
export function getProviderSelection(canonicalKey) {
|
|
522
|
-
return providerSelections.get(canonicalKey);
|
|
523
|
-
}
|
|
524
|
-
/**
|
|
525
|
-
* Sets the provider selection for a channel.
|
|
526
|
-
* @param canonicalKey - The canonical channel key.
|
|
527
|
-
* @param providerKey - The selected provider key.
|
|
528
|
-
*/
|
|
529
|
-
export function setProviderSelection(canonicalKey, providerKey) {
|
|
530
|
-
// If selecting the canonical (default), remove the selection instead of storing it.
|
|
531
|
-
if (providerKey === canonicalKey) {
|
|
532
|
-
providerSelections.delete(canonicalKey);
|
|
533
|
-
}
|
|
534
|
-
else {
|
|
535
|
-
providerSelections.set(canonicalKey, providerKey);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
/**
|
|
539
|
-
* Resolves a canonical channel key to the actual channel key based on user selection. If the user has selected a specific provider for this channel, returns that
|
|
540
|
-
* provider's key. Otherwise returns the canonical key (default provider). When the provider filter is active, falls back to the first enabled variant if the stored
|
|
541
|
-
* selection's provider is filtered out.
|
|
542
|
-
* @param canonicalKey - The canonical channel key.
|
|
543
|
-
* @returns The resolved provider key to use for streaming.
|
|
544
|
-
*/
|
|
545
|
-
export function resolveProviderKey(canonicalKey) {
|
|
546
|
-
const selection = providerSelections.get(canonicalKey);
|
|
547
|
-
// No selection stored — use the canonical key (default provider).
|
|
548
|
-
if (!selection) {
|
|
549
|
-
// If the canonical's provider tag is filtered out, find the first enabled variant.
|
|
550
|
-
if ((enabledProviders.length > 0) && !isProviderTagEnabled(getProviderTagForChannel(canonicalKey))) {
|
|
551
|
-
return findFirstEnabledVariant(canonicalKey) ?? canonicalKey;
|
|
552
|
-
}
|
|
553
|
-
return canonicalKey;
|
|
554
|
-
}
|
|
555
|
-
// Handle :predefined suffix — validate that the base key exists in PREDEFINED_CHANNELS.
|
|
556
|
-
if (selection.endsWith(PREDEFINED_SUFFIX)) {
|
|
557
|
-
const baseKey = selection.slice(0, -PREDEFINED_SUFFIX.length);
|
|
558
|
-
// Runtime check needed — TypeScript thinks Record indexing always returns a value, but the key may not exist.
|
|
559
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
560
|
-
if (PREDEFINED_CHANNELS[baseKey]) {
|
|
561
|
-
return selection;
|
|
562
|
-
}
|
|
563
|
-
// Predefined channel was removed. Fall through to the invalid selection warning.
|
|
564
|
-
// Runtime check needed — TypeScript thinks Record indexing always returns a value, but the key may not exist.
|
|
565
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
566
|
-
}
|
|
567
|
-
else if (channelsRef[selection]) {
|
|
568
|
-
// Normal selection — validate it exists in the merged channels. If its provider tag is filtered out, find the first enabled variant instead.
|
|
569
|
-
if ((enabledProviders.length > 0) && !isProviderTagEnabled(getProviderTagForChannel(selection))) {
|
|
570
|
-
return findFirstEnabledVariant(canonicalKey) ?? selection;
|
|
571
|
-
}
|
|
572
|
-
return selection;
|
|
573
|
-
}
|
|
574
|
-
// Selection is invalid (provider removed). Clear it and log a warning.
|
|
575
|
-
LOG.warn("Provider selection '%s' for channel '%s' no longer exists. Using default.", selection, canonicalKey);
|
|
576
|
-
providerSelections.delete(canonicalKey);
|
|
577
|
-
return canonicalKey;
|
|
578
|
-
}
|
|
579
|
-
/**
|
|
580
|
-
* Finds the first enabled variant for a channel when the current selection's provider is filtered out. Iterates the group's variants and returns the first whose
|
|
581
|
-
* provider tag is enabled.
|
|
582
|
-
* @param canonicalKey - The canonical channel key.
|
|
583
|
-
* @returns The first enabled variant key, or undefined if none are enabled.
|
|
584
|
-
*/
|
|
585
|
-
function findFirstEnabledVariant(canonicalKey) {
|
|
586
|
-
const group = providerGroups.get(canonicalKey);
|
|
587
|
-
if (!group) {
|
|
588
|
-
return undefined;
|
|
589
|
-
}
|
|
590
|
-
for (const variant of group.variants) {
|
|
591
|
-
if (variant.key.endsWith(PREDEFINED_SUFFIX)) {
|
|
592
|
-
continue;
|
|
593
|
-
}
|
|
594
|
-
if (isProviderTagEnabled(variant.tag)) {
|
|
595
|
-
return variant.key;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
return undefined;
|
|
599
|
-
}
|
|
600
|
-
/**
|
|
601
|
-
* Applies variant inheritance: the variant's own properties take precedence, but identity fields fall through from the base channel when not set on the variant.
|
|
602
|
-
* Identity fields are channel-level metadata that apply regardless of provider: name, stationId, tvgShift, and channelNumber. `channelSelector` is deliberately
|
|
603
|
-
* NOT inherited — it is provider-specific (e.g., fox.com uses station codes like "FOXD2C" while Sling uses guide names like "FOX"), so each variant must define
|
|
604
|
-
* its own. This is the single source of truth for variant inheritance rules.
|
|
605
|
-
* @param variant - The variant channel definition.
|
|
606
|
-
* @param base - The canonical (base) channel to inherit from.
|
|
607
|
-
* @returns A new Channel with inheritance applied.
|
|
608
|
-
*/
|
|
609
|
-
function applyVariantInheritance(variant, base) {
|
|
610
|
-
return {
|
|
611
|
-
...variant,
|
|
612
|
-
channelNumber: variant.channelNumber ?? base.channelNumber,
|
|
613
|
-
hdhrEnabled: variant.hdhrEnabled ?? base.hdhrEnabled,
|
|
614
|
-
name: variant.name ?? base.name,
|
|
615
|
-
stationId: variant.stationId ?? base.stationId,
|
|
616
|
-
tvgShift: variant.tvgShift ?? base.tvgShift
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
/**
|
|
620
|
-
* Gets a channel with inheritance applied. For provider variants, this merges the variant's properties with inherited properties from the canonical entry
|
|
621
|
-
* using the live channel data (which includes user overrides). Use `resolvePredefinedVariant()` when you need resolution against pure predefined data.
|
|
622
|
-
* @param key - The channel key (canonical or variant).
|
|
623
|
-
* @returns The complete channel with inheritance applied, or undefined if the channel doesn't exist.
|
|
624
|
-
*/
|
|
625
|
-
export function getResolvedChannel(key) {
|
|
626
|
-
// Handle predefined suffix — return the original predefined channel when user has overridden the canonical but selects the predefined provider.
|
|
627
|
-
if (key.endsWith(PREDEFINED_SUFFIX)) {
|
|
628
|
-
const baseKey = key.slice(0, -PREDEFINED_SUFFIX.length);
|
|
629
|
-
return PREDEFINED_CHANNELS[baseKey];
|
|
630
|
-
}
|
|
631
|
-
const channel = channelsRef[key];
|
|
632
|
-
// Runtime check needed even though TypeScript thinks channel is always defined (Record indexing quirk).
|
|
633
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
634
|
-
if (!channel) {
|
|
635
|
-
return undefined;
|
|
636
|
-
}
|
|
637
|
-
const group = providerGroups.get(key);
|
|
638
|
-
// If not part of a group or is the canonical entry, return as-is.
|
|
639
|
-
if (!group || (group.canonicalKey === key)) {
|
|
640
|
-
return channel;
|
|
641
|
-
}
|
|
642
|
-
// This is a variant — merge with canonical entry.
|
|
643
|
-
const canonical = channelsRef[group.canonicalKey];
|
|
644
|
-
// Runtime check — canonical entry should exist if the group exists, but we check defensively.
|
|
645
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
646
|
-
if (!canonical) {
|
|
647
|
-
// Canonical entry missing (shouldn't happen), return variant as-is.
|
|
648
|
-
return channel;
|
|
649
|
-
}
|
|
650
|
-
return applyVariantInheritance(channel, canonical);
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
|
-
* 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
|
|
654
|
-
* variant's predefined definition, the custom override can be removed and the provider selection switched to that variant. For canonical keys, returns the raw
|
|
655
|
-
* predefined channel. For variant keys, applies the same inheritance rules as `getResolvedChannel()` but against `PREDEFINED_CHANNELS` instead of `channelsRef`.
|
|
656
|
-
* @param key - The channel key (canonical or variant).
|
|
657
|
-
* @returns The channel with inheritance applied against predefined data, or undefined if the key has no predefined definition.
|
|
658
|
-
*/
|
|
659
|
-
export function resolvePredefinedVariant(key) {
|
|
660
|
-
const channel = PREDEFINED_CHANNELS[key];
|
|
661
|
-
// Runtime check — the key may not exist in PREDEFINED_CHANNELS.
|
|
662
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
663
|
-
if (!channel) {
|
|
664
|
-
return undefined;
|
|
665
|
-
}
|
|
666
|
-
const group = providerGroups.get(key);
|
|
667
|
-
// If not part of a group or is the canonical entry, return the predefined channel as-is.
|
|
668
|
-
if (!group || (group.canonicalKey === key)) {
|
|
669
|
-
return channel;
|
|
670
|
-
}
|
|
671
|
-
const canonical = PREDEFINED_CHANNELS[group.canonicalKey];
|
|
672
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
673
|
-
if (!canonical) {
|
|
674
|
-
return channel;
|
|
675
|
-
}
|
|
676
|
-
return applyVariantInheritance(channel, canonical);
|
|
677
|
-
}
|
|
678
|
-
//# sourceMappingURL=providers.js.map
|