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.
Files changed (168) hide show
  1. package/README.md +6 -6
  2. package/dist/app.js +15 -5
  3. package/dist/app.js.map +1 -1
  4. package/dist/browser/cdp.d.ts +4 -6
  5. package/dist/browser/cdp.js +13 -17
  6. package/dist/browser/cdp.js.map +1 -1
  7. package/dist/browser/index.js +3 -3
  8. package/dist/browser/index.js.map +1 -1
  9. package/dist/browser/precaching.d.ts +1 -1
  10. package/dist/browser/precaching.js +23 -23
  11. package/dist/browser/precaching.js.map +1 -1
  12. package/dist/browser/tuning/directv.js +4 -7
  13. package/dist/browser/tuning/directv.js.map +1 -1
  14. package/dist/browser/tuning/fox.js +1 -1
  15. package/dist/browser/tuning/fox.js.map +1 -1
  16. package/dist/browser/tuning/hbo.js +1 -1
  17. package/dist/browser/tuning/hbo.js.map +1 -1
  18. package/dist/browser/tuning/hulu.js +3 -3
  19. package/dist/browser/tuning/hulu.js.map +1 -1
  20. package/dist/browser/tuning/sling.js +2 -2
  21. package/dist/browser/tuning/sling.js.map +1 -1
  22. package/dist/channels/index.d.ts +1 -1
  23. package/dist/channels/index.js +416 -416
  24. package/dist/channels/index.js.map +1 -1
  25. package/dist/config/health.d.ts +3 -3
  26. package/dist/config/health.js +3 -3
  27. package/dist/config/index.js +2 -2
  28. package/dist/config/index.js.map +1 -1
  29. package/dist/config/persistence.d.ts +59 -0
  30. package/dist/config/persistence.js +127 -0
  31. package/dist/config/persistence.js.map +1 -0
  32. package/dist/config/servicePacks.d.ts +46 -0
  33. package/dist/config/{providerPacks.js → servicePacks.js} +28 -31
  34. package/dist/config/servicePacks.js.map +1 -0
  35. package/dist/config/services.d.ts +202 -0
  36. package/dist/config/{providers.js → services.js} +277 -210
  37. package/dist/config/services.js.map +1 -0
  38. package/dist/config/sites.js +61 -61
  39. package/dist/config/sites.js.map +1 -1
  40. package/dist/config/userChannels.d.ts +36 -27
  41. package/dist/config/userChannels.js +300 -212
  42. package/dist/config/userChannels.js.map +1 -1
  43. package/dist/config/userConfig.d.ts +11 -9
  44. package/dist/config/userConfig.js +53 -60
  45. package/dist/config/userConfig.js.map +1 -1
  46. package/dist/config/userProfiles.d.ts +31 -15
  47. package/dist/config/userProfiles.js +139 -96
  48. package/dist/config/userProfiles.js.map +1 -1
  49. package/dist/hdhr/channelMap.d.ts +1 -1
  50. package/dist/hdhr/channelMap.js +4 -7
  51. package/dist/hdhr/channelMap.js.map +1 -1
  52. package/dist/hdhr/index.js +6 -9
  53. package/dist/hdhr/index.js.map +1 -1
  54. package/dist/native/index.js +3 -3
  55. package/dist/native/index.js.map +1 -1
  56. package/dist/native/intercept.js +86 -89
  57. package/dist/native/intercept.js.map +1 -1
  58. package/dist/native/probe.js +2 -2
  59. package/dist/native/probe.js.map +1 -1
  60. package/dist/native/proxy.js +4 -4
  61. package/dist/native/proxy.js.map +1 -1
  62. package/dist/routes/assets.js +3 -3
  63. package/dist/routes/assets.js.map +1 -1
  64. package/dist/routes/auth.js +3 -3
  65. package/dist/routes/auth.js.map +1 -1
  66. package/dist/routes/config/channels/index.d.ts +1 -1
  67. package/dist/routes/config/channels/index.js +1 -1
  68. package/dist/routes/config/channels/index.js.map +1 -1
  69. package/dist/routes/config/channels/routes.js +488 -460
  70. package/dist/routes/config/channels/routes.js.map +1 -1
  71. package/dist/routes/config/channels/table.d.ts +4 -4
  72. package/dist/routes/config/channels/table.js +206 -147
  73. package/dist/routes/config/channels/table.js.map +1 -1
  74. package/dist/routes/config/index.d.ts +2 -2
  75. package/dist/routes/config/index.js +3 -3
  76. package/dist/routes/config/index.js.map +1 -1
  77. package/dist/routes/config/{providers.js → services.js} +44 -39
  78. package/dist/routes/config/services.js.map +1 -0
  79. package/dist/routes/config/settings.js +47 -29
  80. package/dist/routes/config/settings.js.map +1 -1
  81. package/dist/routes/debug.js +5 -7
  82. package/dist/routes/debug.js.map +1 -1
  83. package/dist/routes/health.js +1 -1
  84. package/dist/routes/health.js.map +1 -1
  85. package/dist/routes/index.js +2 -2
  86. package/dist/routes/index.js.map +1 -1
  87. package/dist/routes/logs.js +4 -4
  88. package/dist/routes/logs.js.map +1 -1
  89. package/dist/routes/playlist.d.ts +4 -4
  90. package/dist/routes/playlist.js +56 -44
  91. package/dist/routes/playlist.js.map +1 -1
  92. package/dist/routes/root/content.js +58 -58
  93. package/dist/routes/root/content.js.map +1 -1
  94. package/dist/routes/root/scripts/channels.js +529 -572
  95. package/dist/routes/root/scripts/channels.js.map +1 -1
  96. package/dist/routes/root/scripts/config.js +838 -992
  97. package/dist/routes/root/scripts/config.js.map +1 -1
  98. package/dist/routes/root/scripts/shared.js +431 -197
  99. package/dist/routes/root/scripts/shared.js.map +1 -1
  100. package/dist/routes/root/scripts/status.js +247 -196
  101. package/dist/routes/root/scripts/status.js.map +1 -1
  102. package/dist/routes/root/styles.js +12 -4
  103. package/dist/routes/root/styles.js.map +1 -1
  104. package/dist/routes/services.d.ts +6 -0
  105. package/dist/routes/{providers.js → services.js} +43 -43
  106. package/dist/routes/services.js.map +1 -0
  107. package/dist/routes/ui.js +2 -0
  108. package/dist/routes/ui.js.map +1 -1
  109. package/dist/service/commands.js +5 -8
  110. package/dist/service/commands.js.map +1 -1
  111. package/dist/service/generators.d.ts +1 -1
  112. package/dist/service/generators.js +143 -41
  113. package/dist/service/generators.js.map +1 -1
  114. package/dist/streaming/hls.d.ts +4 -4
  115. package/dist/streaming/hls.js +24 -23
  116. package/dist/streaming/hls.js.map +1 -1
  117. package/dist/streaming/hlsResume.js +1 -1
  118. package/dist/streaming/hlsResume.js.map +1 -1
  119. package/dist/streaming/monitor.d.ts +2 -2
  120. package/dist/streaming/monitor.js +19 -13
  121. package/dist/streaming/monitor.js.map +1 -1
  122. package/dist/streaming/preroll.js +1 -1
  123. package/dist/streaming/preroll.js.map +1 -1
  124. package/dist/streaming/registry.js +4 -8
  125. package/dist/streaming/registry.js.map +1 -1
  126. package/dist/streaming/setup.d.ts +2 -2
  127. package/dist/streaming/setup.js +23 -22
  128. package/dist/streaming/setup.js.map +1 -1
  129. package/dist/streaming/showInfo.js +8 -10
  130. package/dist/streaming/showInfo.js.map +1 -1
  131. package/dist/streaming/statusEmitter.d.ts +4 -3
  132. package/dist/streaming/statusEmitter.js +3 -3
  133. package/dist/streaming/statusEmitter.js.map +1 -1
  134. package/dist/types/channels.d.ts +15 -11
  135. package/dist/types/config.d.ts +2 -2
  136. package/dist/types/index.d.ts +2 -2
  137. package/dist/types/profiles.d.ts +9 -9
  138. package/dist/types/selection.d.ts +1 -1
  139. package/dist/types/shared.d.ts +1 -1
  140. package/dist/types/streaming.d.ts +1 -1
  141. package/dist/utils/chromeFetch.js +1 -1
  142. package/dist/utils/debugFilter.js +3 -3
  143. package/dist/utils/debugFilter.js.map +1 -1
  144. package/dist/utils/fileLogger.js +4 -3
  145. package/dist/utils/fileLogger.js.map +1 -1
  146. package/dist/utils/format.d.ts +7 -1
  147. package/dist/utils/format.js +21 -1
  148. package/dist/utils/format.js.map +1 -1
  149. package/dist/utils/logger.js +17 -27
  150. package/dist/utils/logger.js.map +1 -1
  151. package/dist/utils/morganStream.js +2 -2
  152. package/dist/utils/morganStream.js.map +1 -1
  153. package/dist/utils/platform.js +3 -2
  154. package/dist/utils/platform.js.map +1 -1
  155. package/dist/utils/streamContext.d.ts +7 -0
  156. package/dist/utils/streamContext.js +10 -1
  157. package/dist/utils/streamContext.js.map +1 -1
  158. package/dist/utils/version.js +4 -4
  159. package/dist/utils/version.js.map +1 -1
  160. package/package.json +5 -7
  161. package/dist/config/providerPacks.d.ts +0 -46
  162. package/dist/config/providerPacks.js.map +0 -1
  163. package/dist/config/providers.d.ts +0 -175
  164. package/dist/config/providers.js.map +0 -1
  165. package/dist/routes/config/providers.js.map +0 -1
  166. package/dist/routes/providers.d.ts +0 -6
  167. package/dist/routes/providers.js.map +0 -1
  168. /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
- * providers.ts: Provider group management for multi-provider channels.
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
- /* Provider groups allow multiple streaming providers to offer the same content. For example, ESPN can be watched via ESPN.com (native) or Disney+.
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. buildProviderGroups
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 provider dropdown. The user's custom version
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 `providerSelections` key and persist across restarts.
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 provider dropdown. Functions that look up
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 provider groups, keyed by canonical channel key.
37
- const providerGroups = new Map();
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 provider selections, keyed by canonical channel key. Values are the selected provider key (e.g., "espn-disneyplus").
41
- let providerSelections = new Map();
42
- // Provider Tag System.
43
- // Module-level state for the provider filter. Empty array means "no filter" (all providers shown). Non-empty means only these tags are active.
44
- let enabledProviders = [];
45
- /**
46
- * 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
47
- * first (for user-defined profiles with custom providerTag), then the URL domain via getDomainConfig().
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 provider tag string.
49
+ * @returns The service tag string.
50
50
  */
51
- function resolveProviderTag(channel) {
52
- // 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
53
- // assignments are grouped under the correct provider filter even when their URL domain has a different built-in providerTag.
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 profileProvider = resolveUserProfileProvider(channel.profile);
56
- if (profileProvider?.providerTag) {
57
- return profileProvider.providerTag;
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?.providerTag ?? "direct";
61
+ return config?.serviceTag ?? "direct";
62
62
  }
63
63
  /**
64
- * 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
65
- * 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
66
- * :predefined synthetic keys — those only exist inside provider groups and their tags are available via the group's variant entries.
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 provider tag string.
68
+ * @returns The service tag string.
69
69
  */
70
- export function getProviderTagForChannel(key) {
70
+ export function getServiceTagForChannel(key) {
71
71
  const effectiveKey = stripPredefinedSuffix(key);
72
- const group = providerGroups.get(effectiveKey);
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 resolveProviderTag(channel);
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 providers work correctly
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 provider tags for a channel (canonical tag + all variant suffix tags). Used to determine which providers offer this channel.
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 provider tag strings.
106
+ * @returns Array of service tag strings.
107
107
  */
108
- export function getChannelProviderTags(canonicalKey) {
109
- const group = providerGroups.get(canonicalKey);
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 [getProviderTagForChannel(canonicalKey)];
123
+ return [getServiceTagForChannel(canonicalKey)];
124
124
  }
125
125
  /**
126
- * 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
127
- * have a providerTag.
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 getAllProviderTags() {
130
+ export function getAllServiceTags() {
131
131
  const tags = new Set();
132
- // Scan all channels (not just grouped ones) to find all provider tags.
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 getChannelProviderTags() on the canonical.
136
- const group = providerGroups.get(key);
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 = getChannelProviderTags(key);
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 provider tags that may not appear in any channel yet (e.g., newly created profiles with no channels assigned).
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.providerTag) {
149
- tags.add(config.providerTag);
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 provider tag. First match wins for each tag.
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.providerTag && config.provider && !tagMeta.has(config.providerTag)) {
157
- tagMeta.set(config.providerTag, { displayName: config.provider, domain, iconUrl: config.iconUrl });
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.providerTag && config.provider && !tagMeta.has(config.providerTag)) {
163
- tagMeta.set(config.providerTag, { displayName: config.provider, domain, iconUrl: config.iconUrl });
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 provider tags.
186
- * @returns Copy of the enabled providers array. Empty means no filter (all shown).
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 getEnabledProviders() {
189
- return [...enabledProviders];
188
+ export function getEnabledServices() {
189
+ return [...enabledServices];
190
190
  }
191
191
  /**
192
- * Sets the enabled provider tags. Empty array means "no filter" (all providers shown).
193
- * @param tags - The provider tags to enable.
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 setEnabledProviders(tags) {
196
- enabledProviders = [...tags];
195
+ export function setEnabledServices(tags) {
196
+ enabledServices = [...tags];
197
197
  }
198
198
  /**
199
- * 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".
200
- * @param tag - The provider tag to check.
201
- * @returns True if the provider is available.
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 isProviderTagEnabled(tag) {
204
- // No filter active — all providers are enabled.
205
- if (enabledProviders.length === 0) {
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 enabledProviders.includes(tag);
212
+ return enabledServices.includes(tag);
213
213
  }
214
214
  /**
215
- * Centralized availability check for the provider filter. Returns true if the channel has at least one variant whose provider tag is enabled.
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 provider filter.
217
+ * @returns True if the channel passes the service filter.
218
218
  */
219
- export function isChannelAvailableByProvider(canonicalKey) {
219
+ export function isChannelAvailableByService(canonicalKey) {
220
220
  // No filter active — all channels are available.
221
- if (enabledProviders.length === 0) {
221
+ if (enabledServices.length === 0) {
222
222
  return true;
223
223
  }
224
- const tags = getChannelProviderTags(canonicalKey);
225
- return tags.some((tag) => isProviderTagEnabled(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 provider groups by scanning all channels and grouping by canonicalKey. Variant entries declare their canonical via the canonicalKey field (set by the
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-provider channels that don't have canonicalKey-based variants.
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 buildProviderGroups(channels) {
253
+ export function buildServiceGroups(channels) {
249
254
  channelsRef = channels;
250
- providerGroups.clear();
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. The :predefined variant's tag is derived from the predefined channel (not the user override) so that provider
274
- // filtering correctly reflects the predefined channel's provider, not the user's custom URL.
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
- variants.push({ key: canonicalKey, label: "Custom (" + extractDomain(canonical.url) + ")", tag: resolveProviderTag(canonical) });
278
- variants.push({ key: canonicalKey + PREDEFINED_SUFFIX, label: predefined.provider ?? getProviderDisplayName(predefined.url),
279
- tag: resolveProviderTag(predefined) });
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: getChannelProviderLabel(canonical), tag: resolveProviderTag(canonical) });
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: getChannelProviderLabel(variant), tag: resolveProviderTag(variant) });
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
- providerGroups.set(canonicalKey, group);
311
+ serviceGroups.set(canonicalKey, group);
292
312
  for (const variantKey of variantKeys) {
293
- providerGroups.set(variantKey, group);
313
+ serviceGroups.set(variantKey, group);
294
314
  }
295
- LOG.debug("config:general", "Provider group '%s': variants=%s.", canonicalKey, variants.map((v) => v.key).join(", "));
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-provider predefined channels. These don't have canonicalKey-based variants but the user's custom version
298
- // should be toggleable against the predefined original.
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 (providerGroups.has(key)) {
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: resolveProviderTag(userChannel) },
310
- { key: key + PREDEFINED_SUFFIX, label: predefined.provider ?? getProviderDisplayName(predefined.url), tag: resolveProviderTag(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
- providerGroups.set(key, group);
314
- LOG.debug("config:general", "Provider group '%s' (override): variants=%s.", key, variants.map((v) => v.key).join(", "));
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
- * 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
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 provider dropdown entry that uses that domain.
321
- * @param url - The URL to resolve a provider display name for.
322
- * @returns The provider display name, or the concise domain if no provider name is configured.
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 getProviderDisplayName(url) {
325
- // 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
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?.provider) {
332
- return builtinFull.provider;
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?.provider) {
338
- return builtinConcise.provider;
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?.provider ?? extractDomain(url);
429
+ return config?.service ?? extractDomain(url);
347
430
  }
348
431
  /**
349
- * Resolves provider identity (tag and display name) for a user-defined profile by scanning its domain mappings. Returns the first matching domain config's
350
- * 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
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 provider identity from the profile's domain mappings, or undefined if no matching domain mapping exists.
436
+ * @returns The service identity from the profile's domain mappings, or undefined if no matching domain mapping exists.
354
437
  */
355
- function resolveUserProfileProvider(profileKey) {
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 { provider: config.provider, providerTag: config.providerTag };
442
+ return { service: config.service, serviceTag: config.serviceTag };
360
443
  }
361
444
  }
362
445
  return undefined;
363
446
  }
364
447
  /**
365
- * Resolves the provider display label for a channel. Checks in order: explicit `provider` field on the channel, the channel's explicit profile resolved via
366
- * 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
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 provider display label.
452
+ * @returns The service display label.
370
453
  */
371
- export function getChannelProviderLabel(channel) {
372
- if (channel.provider) {
373
- return channel.provider;
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 provider name from domain mappings.
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 profileProvider = resolveUserProfileProvider(channel.profile);
378
- if (profileProvider?.provider) {
379
- return profileProvider.provider;
460
+ const profileService = resolveUserProfileService(channel.profile);
461
+ if (profileService?.service) {
462
+ return profileService.service;
380
463
  }
381
464
  }
382
- return getProviderDisplayName(channel.url);
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", "provider", "stationId", "tags"]);
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. Provider values use the display label for human-meaningful sort order. This is the single source of truth for channel sort key extraction,
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 provider variant cannot be resolved (e.g., key not in the merged channel map).
392
- * @param key - The canonical channel key. Used for key-based sorting and to resolve the selected provider variant internally.
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 provider variant so all sort keys reflect the user's provider selection. For URL-dependent fields (profile, provider), this is essential —
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(resolveProviderKey(key)) ?? channel;
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 provider or falls back to default. Only apply the ! prefix for non-default auto profiles so
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 = getChannelProviderLabel(effective);
513
+ const label = getChannelServiceLabel(effective);
431
514
  return label ? ("!" + label.toLowerCase()) : "";
432
515
  }
433
- case "provider": {
434
- return getChannelProviderLabel(effective).toLowerCase();
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 provider group for a channel key. Works with both canonical and variant keys.
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 provider group if the channel is part of a multi-provider group, undefined otherwise.
559
+ * @returns The service group if the channel is part of a multi-service group, undefined otherwise.
477
560
  */
478
- export function getProviderGroup(key) {
479
- return providerGroups.get(key);
561
+ export function getServiceGroup(key) {
562
+ return serviceGroups.get(key);
480
563
  }
481
564
  /**
482
- * Checks if a channel key is a non-canonical provider variant. Used to filter variants from channel listings.
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 provider group.
567
+ * @returns True if the key is a variant (not canonical) in a service group.
485
568
  */
486
- export function isProviderVariant(key) {
487
- const group = providerGroups.get(key);
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 provider options. Used to determine whether to show a provider dropdown in the UI.
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 provider variant.
576
+ * @returns True if the channel has more than one service variant.
494
577
  */
495
- export function hasMultipleProviders(key) {
496
- const group = providerGroups.get(key);
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 provider group, or the input key if not part of a group.
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 = providerGroups.get(baseKey);
591
+ const group = serviceGroups.get(baseKey);
509
592
  return group?.canonicalKey ?? baseKey;
510
593
  }
511
594
  /**
512
- * Sets the user's provider selections. Called when loading from channels.json.
513
- * @param selections - Provider selections keyed by canonical channel key.
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 setProviderSelections(selections) {
516
- providerSelections = new Map(Object.entries(selections));
598
+ export function setServiceSelections(selections) {
599
+ serviceSelections = new Map(Object.entries(selections));
517
600
  }
518
601
  /**
519
- * Gets all provider selections.
520
- * @returns Copy of the provider selections object.
602
+ * Gets all service selections.
603
+ * @returns Copy of the service selections object.
521
604
  */
522
- export function getProviderSelections() {
523
- return Object.fromEntries(providerSelections);
605
+ export function getServiceSelections() {
606
+ return Object.fromEntries(serviceSelections);
524
607
  }
525
608
  /**
526
- * Gets the provider selection for a specific channel.
609
+ * Gets the service selection for a specific channel.
527
610
  * @param canonicalKey - The canonical channel key.
528
- * @returns The selected provider key, or undefined if using the default.
611
+ * @returns The selected service key, or undefined if using the default.
529
612
  */
530
- export function getProviderSelection(canonicalKey) {
531
- return providerSelections.get(canonicalKey);
613
+ export function getServiceSelection(canonicalKey) {
614
+ return serviceSelections.get(canonicalKey);
532
615
  }
533
616
  /**
534
- * Sets the provider selection for a channel.
617
+ * Sets the service selection for a channel.
535
618
  * @param canonicalKey - The canonical channel key.
536
- * @param providerKey - The selected provider key.
619
+ * @param serviceKey - The selected service key.
537
620
  */
538
- export function setProviderSelection(canonicalKey, providerKey) {
621
+ export function setServiceSelection(canonicalKey, serviceKey) {
539
622
  // If selecting the canonical (default), remove the selection instead of storing it.
540
- if (providerKey === canonicalKey) {
541
- providerSelections.delete(canonicalKey);
623
+ if (serviceKey === canonicalKey) {
624
+ serviceSelections.delete(canonicalKey);
542
625
  }
543
626
  else {
544
- providerSelections.set(canonicalKey, providerKey);
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 provider for this channel, returns that
549
- * 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
550
- * selection's provider is filtered out.
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 provider key to use for streaming.
638
+ * @returns The resolved service key to use for streaming.
553
639
  */
554
- export function resolveProviderKey(canonicalKey) {
555
- const selection = providerSelections.get(canonicalKey);
556
- // No selection stored — use the canonical key (default provider).
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
- // If the canonical's provider tag is filtered out, find the first enabled variant.
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
- // Handle :predefined suffix — validate that the base key exists in PREDEFINED_CHANNELS.
565
- if (selection.endsWith(PREDEFINED_SUFFIX)) {
566
- const baseKey = selection.slice(0, -PREDEFINED_SUFFIX.length);
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
- // Selection is invalid (provider removed). Clear it and log a warning.
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 provider is filtered out. Iterates the group's variants and returns the first whose
590
- * provider tag is enabled.
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 = providerGroups.get(canonicalKey);
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 (isProviderTagEnabled(variant.tag)) {
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 provider-specific fields (url, channelSelector, profile, etc.) while identity fields always come from the
611
- * canonical base. Identity fields describe what the channel IS — name, station ID, tags, channel number — and are independent of which provider serves it. The
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 provider variants, rather than being masked by stale flattener-copied values on the variant.
614
- * The field list comes from CHANNEL_IDENTITY_FIELDS — the single source of truth for the identity/provider-specific separation.
615
- * @param variant - The variant channel definition (provider-specific fields).
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 provider-specific fields from the variant.
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 provider variants, this merges the variant's properties with inherited properties from the canonical entry
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 provider.
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 = providerGroups.get(key);
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 provider selection switched to that variant. For canonical keys, returns the raw
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 = providerGroups.get(key);
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=providers.js.map
758
+ //# sourceMappingURL=services.js.map