prismcast 1.5.2 → 1.7.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 (239) hide show
  1. package/README.md +44 -28
  2. package/dist/app.d.ts +5 -0
  3. package/dist/app.js +63 -5
  4. package/dist/app.js.map +1 -1
  5. package/dist/browser/cdp.js +1 -1
  6. package/dist/browser/cdp.js.map +1 -1
  7. package/dist/browser/channelSelection.d.ts +10 -34
  8. package/dist/browser/channelSelection.js +31 -96
  9. package/dist/browser/channelSelection.js.map +1 -1
  10. package/dist/browser/display.d.ts +19 -0
  11. package/dist/browser/display.js +16 -11
  12. package/dist/browser/display.js.map +1 -1
  13. package/dist/browser/index.d.ts +31 -54
  14. package/dist/browser/index.js +334 -272
  15. package/dist/browser/index.js.map +1 -1
  16. package/dist/browser/login.d.ts +65 -0
  17. package/dist/browser/login.js +193 -0
  18. package/dist/browser/login.js.map +1 -0
  19. package/dist/browser/precaching.js +15 -3
  20. package/dist/browser/precaching.js.map +1 -1
  21. package/dist/browser/tuning/comcastPolymer.d.ts +23 -0
  22. package/dist/browser/tuning/comcastPolymer.js +655 -0
  23. package/dist/browser/tuning/comcastPolymer.js.map +1 -0
  24. package/dist/browser/tuning/cox.d.ts +2 -0
  25. package/dist/browser/tuning/cox.js +14 -0
  26. package/dist/browser/tuning/cox.js.map +1 -0
  27. package/dist/browser/tuning/directv.js +14 -1
  28. package/dist/browser/tuning/directv.js.map +1 -1
  29. package/dist/browser/tuning/fox.js +14 -2
  30. package/dist/browser/tuning/fox.js.map +1 -1
  31. package/dist/browser/tuning/hbo.js +12 -1
  32. package/dist/browser/tuning/hbo.js.map +1 -1
  33. package/dist/browser/tuning/hulu.js +24 -1
  34. package/dist/browser/tuning/hulu.js.map +1 -1
  35. package/dist/browser/tuning/shared.d.ts +46 -0
  36. package/dist/browser/tuning/shared.js +100 -0
  37. package/dist/browser/tuning/shared.js.map +1 -0
  38. package/dist/browser/tuning/sling.js +35 -5
  39. package/dist/browser/tuning/sling.js.map +1 -1
  40. package/dist/browser/tuning/spectrum.js +13 -1
  41. package/dist/browser/tuning/spectrum.js.map +1 -1
  42. package/dist/browser/tuning/thumbnailRow.js +1 -1
  43. package/dist/browser/tuning/thumbnailRow.js.map +1 -1
  44. package/dist/browser/tuning/tileClick.js +1 -1
  45. package/dist/browser/tuning/tileClick.js.map +1 -1
  46. package/dist/browser/tuning/xfinity.d.ts +2 -0
  47. package/dist/browser/tuning/xfinity.js +14 -0
  48. package/dist/browser/tuning/xfinity.js.map +1 -0
  49. package/dist/browser/tuning/youtubeTv.js +15 -1
  50. package/dist/browser/tuning/youtubeTv.js.map +1 -1
  51. package/dist/browser/video.d.ts +20 -0
  52. package/dist/browser/video.js +168 -124
  53. package/dist/browser/video.js.map +1 -1
  54. package/dist/channels/index.d.ts +2 -2
  55. package/dist/channels/index.js +2075 -683
  56. package/dist/channels/index.js.map +1 -1
  57. package/dist/config/health.d.ts +28 -28
  58. package/dist/config/health.js +72 -54
  59. package/dist/config/health.js.map +1 -1
  60. package/dist/config/index.js +29 -64
  61. package/dist/config/index.js.map +1 -1
  62. package/dist/config/paths.d.ts +12 -0
  63. package/dist/config/paths.js +16 -0
  64. package/dist/config/paths.js.map +1 -1
  65. package/dist/config/profiles.d.ts +3 -2
  66. package/dist/config/profiles.js +45 -34
  67. package/dist/config/profiles.js.map +1 -1
  68. package/dist/config/providerPacks.js +4 -1
  69. package/dist/config/providerPacks.js.map +1 -1
  70. package/dist/config/providers.d.ts +19 -5
  71. package/dist/config/providers.js +123 -93
  72. package/dist/config/providers.js.map +1 -1
  73. package/dist/config/sites.d.ts +28 -0
  74. package/dist/config/sites.js +104 -127
  75. package/dist/config/sites.js.map +1 -1
  76. package/dist/config/userChannels.d.ts +53 -2
  77. package/dist/config/userChannels.js +222 -13
  78. package/dist/config/userChannels.js.map +1 -1
  79. package/dist/config/userConfig.d.ts +2 -1
  80. package/dist/config/userConfig.js +6 -15
  81. package/dist/config/userConfig.js.map +1 -1
  82. package/dist/config/userProfiles.d.ts +0 -1
  83. package/dist/config/userProfiles.js +18 -10
  84. package/dist/config/userProfiles.js.map +1 -1
  85. package/dist/hdhr/channelMap.d.ts +1 -1
  86. package/dist/hdhr/channelMap.js +4 -4
  87. package/dist/hdhr/channelMap.js.map +1 -1
  88. package/dist/index.js +25 -22
  89. package/dist/index.js.map +1 -1
  90. package/dist/native/decrypt.d.ts +32 -0
  91. package/dist/native/decrypt.js +85 -0
  92. package/dist/native/decrypt.js.map +1 -0
  93. package/dist/native/index.d.ts +61 -0
  94. package/dist/native/index.js +342 -0
  95. package/dist/native/index.js.map +1 -0
  96. package/dist/native/intercept.d.ts +35 -0
  97. package/dist/native/intercept.js +184 -0
  98. package/dist/native/intercept.js.map +1 -0
  99. package/dist/native/probe.d.ts +49 -0
  100. package/dist/native/probe.js +294 -0
  101. package/dist/native/probe.js.map +1 -0
  102. package/dist/native/proxy.d.ts +54 -0
  103. package/dist/native/proxy.js +947 -0
  104. package/dist/native/proxy.js.map +1 -0
  105. package/dist/routes/components.d.ts +43 -0
  106. package/dist/routes/components.js +122 -0
  107. package/dist/routes/components.js.map +1 -1
  108. package/dist/routes/config/channels/index.d.ts +2 -2
  109. package/dist/routes/config/channels/index.js +1 -1
  110. package/dist/routes/config/channels/index.js.map +1 -1
  111. package/dist/routes/config/channels/routes.js +443 -65
  112. package/dist/routes/config/channels/routes.js.map +1 -1
  113. package/dist/routes/config/channels/table.d.ts +76 -0
  114. package/dist/routes/config/channels/table.js +369 -89
  115. package/dist/routes/config/channels/table.js.map +1 -1
  116. package/dist/routes/config/index.d.ts +1 -1
  117. package/dist/routes/config/index.js +1 -1
  118. package/dist/routes/config/index.js.map +1 -1
  119. package/dist/routes/config/providers.d.ts +6 -6
  120. package/dist/routes/config/providers.js +73 -99
  121. package/dist/routes/config/providers.js.map +1 -1
  122. package/dist/routes/debug.js +5 -5
  123. package/dist/routes/debug.js.map +1 -1
  124. package/dist/routes/hls.js +12 -2
  125. package/dist/routes/hls.js.map +1 -1
  126. package/dist/routes/index.d.ts +0 -15
  127. package/dist/routes/index.js +2 -16
  128. package/dist/routes/index.js.map +1 -1
  129. package/dist/routes/providers.js +106 -3
  130. package/dist/routes/providers.js.map +1 -1
  131. package/dist/routes/root/content.js +124 -47
  132. package/dist/routes/root/content.js.map +1 -1
  133. package/dist/routes/root/index.js +2 -1
  134. package/dist/routes/root/index.js.map +1 -1
  135. package/dist/routes/root/scripts/channels.js +772 -312
  136. package/dist/routes/root/scripts/channels.js.map +1 -1
  137. package/dist/routes/root/scripts/config.js +596 -544
  138. package/dist/routes/root/scripts/config.js.map +1 -1
  139. package/dist/routes/root/scripts/index.d.ts +1 -0
  140. package/dist/routes/root/scripts/index.js +1 -0
  141. package/dist/routes/root/scripts/index.js.map +1 -1
  142. package/dist/routes/root/scripts/shared.d.ts +6 -0
  143. package/dist/routes/root/scripts/shared.js +391 -0
  144. package/dist/routes/root/scripts/shared.js.map +1 -0
  145. package/dist/routes/root/scripts/status.js +49 -33
  146. package/dist/routes/root/scripts/status.js.map +1 -1
  147. package/dist/routes/root/styles.js +117 -10
  148. package/dist/routes/root/styles.js.map +1 -1
  149. package/dist/service/commands.js +2 -1
  150. package/dist/service/commands.js.map +1 -1
  151. package/dist/streaming/fmp4Segmenter.d.ts +5 -0
  152. package/dist/streaming/fmp4Segmenter.js +146 -42
  153. package/dist/streaming/fmp4Segmenter.js.map +1 -1
  154. package/dist/streaming/hls.d.ts +24 -19
  155. package/dist/streaming/hls.js +603 -297
  156. package/dist/streaming/hls.js.map +1 -1
  157. package/dist/streaming/hlsResume.d.ts +16 -3
  158. package/dist/streaming/hlsResume.js +35 -8
  159. package/dist/streaming/hlsResume.js.map +1 -1
  160. package/dist/streaming/hlsSegments.d.ts +41 -2
  161. package/dist/streaming/hlsSegments.js +131 -28
  162. package/dist/streaming/hlsSegments.js.map +1 -1
  163. package/dist/streaming/lifecycle.js +27 -13
  164. package/dist/streaming/lifecycle.js.map +1 -1
  165. package/dist/streaming/monitor.d.ts +1 -0
  166. package/dist/streaming/monitor.js +862 -474
  167. package/dist/streaming/monitor.js.map +1 -1
  168. package/dist/streaming/mp4Parser.d.ts +52 -6
  169. package/dist/streaming/mp4Parser.js +156 -30
  170. package/dist/streaming/mp4Parser.js.map +1 -1
  171. package/dist/streaming/mpegts.d.ts +2 -2
  172. package/dist/streaming/mpegts.js +156 -109
  173. package/dist/streaming/mpegts.js.map +1 -1
  174. package/dist/streaming/playlistBuilder.d.ts +38 -0
  175. package/dist/streaming/playlistBuilder.js +66 -0
  176. package/dist/streaming/playlistBuilder.js.map +1 -0
  177. package/dist/streaming/preroll.d.ts +126 -0
  178. package/dist/streaming/preroll.js +434 -0
  179. package/dist/streaming/preroll.js.map +1 -0
  180. package/dist/streaming/pretune.d.ts +8 -0
  181. package/dist/streaming/pretune.js +226 -0
  182. package/dist/streaming/pretune.js.map +1 -0
  183. package/dist/streaming/recovery.d.ts +0 -6
  184. package/dist/streaming/recovery.js +0 -8
  185. package/dist/streaming/recovery.js.map +1 -1
  186. package/dist/streaming/registry.d.ts +57 -9
  187. package/dist/streaming/registry.js +26 -7
  188. package/dist/streaming/registry.js.map +1 -1
  189. package/dist/streaming/setup.d.ts +8 -1
  190. package/dist/streaming/setup.js +71 -27
  191. package/dist/streaming/setup.js.map +1 -1
  192. package/dist/streaming/showInfo.d.ts +29 -4
  193. package/dist/streaming/showInfo.js +210 -38
  194. package/dist/streaming/showInfo.js.map +1 -1
  195. package/dist/streaming/statusEmitter.d.ts +20 -6
  196. package/dist/streaming/statusEmitter.js +31 -7
  197. package/dist/streaming/statusEmitter.js.map +1 -1
  198. package/dist/types/channels.d.ts +32 -0
  199. package/dist/types/config.d.ts +2 -2
  200. package/dist/types/index.d.ts +2 -2
  201. package/dist/types/index.js.map +1 -1
  202. package/dist/types/profiles.d.ts +13 -1
  203. package/dist/types/selection.d.ts +5 -1
  204. package/dist/types/shared.d.ts +1 -1
  205. package/dist/types/streaming.d.ts +7 -0
  206. package/dist/utils/chromeFetch.d.ts +20 -0
  207. package/dist/utils/chromeFetch.js +43 -0
  208. package/dist/utils/chromeFetch.js.map +1 -0
  209. package/dist/utils/debugFilter.d.ts +0 -5
  210. package/dist/utils/debugFilter.js +20 -11
  211. package/dist/utils/debugFilter.js.map +1 -1
  212. package/dist/utils/delay.d.ts +28 -0
  213. package/dist/utils/delay.js +35 -1
  214. package/dist/utils/delay.js.map +1 -1
  215. package/dist/utils/evaluate.js +6 -11
  216. package/dist/utils/evaluate.js.map +1 -1
  217. package/dist/utils/ffmpeg.d.ts +14 -12
  218. package/dist/utils/ffmpeg.js +114 -134
  219. package/dist/utils/ffmpeg.js.map +1 -1
  220. package/dist/utils/format.d.ts +6 -0
  221. package/dist/utils/format.js +8 -0
  222. package/dist/utils/format.js.map +1 -1
  223. package/dist/utils/index.d.ts +3 -0
  224. package/dist/utils/index.js +3 -0
  225. package/dist/utils/index.js.map +1 -1
  226. package/dist/utils/logEmitter.js +1 -1
  227. package/dist/utils/logEmitter.js.map +1 -1
  228. package/dist/utils/pid.d.ts +40 -0
  229. package/dist/utils/pid.js +83 -0
  230. package/dist/utils/pid.js.map +1 -0
  231. package/dist/utils/retry.d.ts +15 -9
  232. package/dist/utils/retry.js +7 -23
  233. package/dist/utils/retry.js.map +1 -1
  234. package/dist/utils/timing.d.ts +5 -0
  235. package/dist/utils/timing.js +13 -0
  236. package/dist/utils/timing.js.map +1 -0
  237. package/dist/utils/version.js +2 -2
  238. package/dist/utils/version.js.map +1 -1
  239. package/package.json +7 -7
@@ -0,0 +1,655 @@
1
+ import { LOG, delay, formatError } from "../../utils/index.js";
2
+ import { CONFIG } from "../../config/index.js";
3
+ import { logAvailableChannels } from "./shared.js";
4
+ // URL pattern for the channelmap API response. Matched against response URLs to identify channelmap data. Shared across all Comcast Polymer providers - the same
5
+ // backend serves both Xfinity and Cox subscribers, returning subscriber-specific entitled channel lineups based on authentication.
6
+ const CHANNELMAP_API_PATTERN = "xtvapi.cloudtv.comcast.net/channelmap";
7
+ // Polling interval for cold-cache wait. 200ms balances responsiveness against CPU overhead.
8
+ const CACHE_POLL_INTERVAL = 200;
9
+ // Broadcast network verbose names from callSignVoiceOverHint, mapped to standard abbreviations. Used to identify local affiliates and set the channelSelector to the
10
+ // network abbreviation rather than the verbose name or callSign.
11
+ const BROADCAST_HINTS = {
12
+ "american broadcasting company": "ABC",
13
+ "columbia broadcasting system": "CBS",
14
+ "fox broadcasting company": "Fox",
15
+ "national broadcasting company": "NBC",
16
+ "public broadcasting service": "PBS"
17
+ };
18
+ // Audio-only channel brand prefixes to skip during channelmap processing. These channels produce static screens with music and are not useful for video capture.
19
+ // Matched against the lowercased branchOf/company/callSign field using prefix comparison to catch genre variants (e.g., "Music Choice Hip-Hop").
20
+ const AUDIO_ONLY_PREFIXES = ["music choice", "stingray"];
21
+ // Base technology suffixes for Comcast callSigns. Each base suffix may have a Pacific timezone variant with "p" appended (e.g., "hd" -> "hdp", "h" -> "hp").
22
+ // Single-character base suffixes also support a reverse convention where "p" is prepended (e.g., "h" -> "ph") - observed in STARZ Encore channels (STZEAPH).
23
+ // The reverse convention only applies to single-character suffixes; multi-character suffixes always use the forward convention (e.g., "hdp" not "phd").
24
+ // CALLSIGN_SUFFIXES is derived from this array with all Pacific variants and base suffixes sorted by length descending to ensure longest-match-first stripping
25
+ // (e.g., CNNHDP must strip "hdp" to CNN, not "hd" to CNNP). Intentionally excludes "d" as a base suffix - it appears as a meaningful part of callSigns (ESPND)
26
+ // rather than a pure technology suffix, causing cache key collisions with unrelated channels.
27
+ const BASE_SUFFIXES = ["str", "hd", "h", "dt"];
28
+ // Full suffix list for stripping, derived from BASE_SUFFIXES. Forward Pacific variants (base + "p") are generated for all base suffixes. Reverse Pacific
29
+ // variants ("p" + base) are generated only for single-character base suffixes where the convention is observed. Sorted by length descending to ensure
30
+ // longest-match-first ordering, with alphabetical tiebreaking within the same length.
31
+ const CALLSIGN_SUFFIXES = [...new Set([
32
+ ...BASE_SUFFIXES.map((s) => s + "p"),
33
+ ...BASE_SUFFIXES.filter((s) => s.length === 1).map((s) => "p" + s),
34
+ ...BASE_SUFFIXES
35
+ ])].sort((a, b) => (b.length - a.length) || a.localeCompare(b));
36
+ // Duration in milliseconds to poll for the Watch Now modal button. Matches the dismiss poll constants in video.ts.
37
+ const WATCH_NOW_POLL_DURATION = 5000;
38
+ // Interval in milliseconds between Watch Now modal poll checks. Matches the dismiss poll constants in video.ts.
39
+ const WATCH_NOW_POLL_INTERVAL = 500;
40
+ /**
41
+ * Strips common Comcast callSign suffixes to produce a normalized lookup key. Strips the first matching suffix from the end of the callSign, but only when the
42
+ * remaining string is at least 2 characters. Examples: CNNHD->CNN, AESTR->AE, CNNHDP->CNN, BRAVOHP->BRAVO, STZEAPH->STZEA, KNXVDTP->KNXV.
43
+ * @param callSign - The raw callSign from the channelmap API.
44
+ * @returns The stripped callSign, or the original if no suffix matched or stripping would produce a string shorter than 2 characters.
45
+ */
46
+ function stripCallSignSuffix(callSign) {
47
+ const lower = callSign.toLowerCase();
48
+ for (const suffix of CALLSIGN_SUFFIXES) {
49
+ if (lower.endsWith(suffix) && ((lower.length - suffix.length) >= 2)) {
50
+ return lower.slice(0, -suffix.length);
51
+ }
52
+ }
53
+ return lower;
54
+ }
55
+ /**
56
+ * Determines whether a callSign represents a Pacific timezone variant. A callSign is Pacific if it ends with "p" adjacent to a recognized base technology
57
+ * suffix: forward (base + "p", e.g., "hdp", "hp") for all base suffixes, or reverse ("p" + base, e.g., "ph") for single-character base suffixes only. Both
58
+ * conventions appear in the Comcast channelmap - AETVHDP and BRAVOHP use the forward convention, while STZEAPH uses the reverse convention. Derived from
59
+ * BASE_SUFFIXES so adding a new technology suffix automatically enables Pacific detection.
60
+ * @param callSign - The raw callSign from the channelmap API.
61
+ * @returns True if the callSign ends with a recognized Pacific suffix (e.g., "hdp", "hp", "ph").
62
+ */
63
+ function isPacificCallSign(callSign) {
64
+ const lower = callSign.toLowerCase();
65
+ return BASE_SUFFIXES.some((base) => {
66
+ const forward = base + "p";
67
+ if (lower.endsWith(forward) && ((lower.length - forward.length) >= 2)) {
68
+ return true;
69
+ }
70
+ // Reverse convention (e.g., "ph") only applies to single-character base suffixes. Multi-character reverse suffixes like "phd" would falsely match
71
+ // callSigns whose base happens to end in "p" (e.g., a hypothetical ALPHD matching "p" + "hd").
72
+ if (base.length === 1) {
73
+ const reverse = "p" + base;
74
+ return lower.endsWith(reverse) && ((lower.length - reverse.length) >= 2);
75
+ }
76
+ return false;
77
+ });
78
+ }
79
+ /**
80
+ * Creates a complete ProviderModule for a Comcast Polymer SPA provider. Each invocation creates isolated per-provider state (channel cache, channelmap response
81
+ * cache, enumeration flag) so multiple providers sharing the same platform do not interfere with each other.
82
+ * @param config - Provider-specific configuration values.
83
+ * @returns A fully configured ProviderModule ready for registration in the coordinator.
84
+ */
85
+ export function createComcastPolymerProvider(config) {
86
+ // Per-provider mutable state. Each provider instance gets its own channel cache and channelmap response cache, isolated via closure scope.
87
+ // Unified channel cache. Maps lowercased lookup keys to combined discovery and tuning data. Multiple keys may reference the same entry (callSign, stripped
88
+ // callSign, branchOf name). Populated during channelmap API interception or discovery. Cleared on browser disconnect via clearCache().
89
+ const channelCache = new Map();
90
+ // Tracks whether the channel cache has been fully enumerated from a complete channelmap API response. Guards getCachedChannels to avoid returning partial data.
91
+ let fullyEnumerated = false;
92
+ // Cached channelmap API response for CDP request interception on warm tunes. Stored on the first successful API response. The exact URL and headers are replayed
93
+ // so that only the correct request is intercepted (the SPA makes multiple requests to URLs matching the channelmap pattern, but only one is the actual channel
94
+ // lineup).
95
+ let cachedBody = null;
96
+ let cachedUrl = null;
97
+ let cachedHeaders = {};
98
+ // Tracks which pages have response interception listeners registered to avoid duplicate registrations.
99
+ const pagesWithListeners = new WeakSet();
100
+ /**
101
+ * Determines whether a non-Pacific entry should override an existing Pacific entry on a shared cache key. Shared keys (stripped callSign, branchOf) should
102
+ * resolve to the primary East feed, not the Pacific variant. When a non-Pacific entry encounters a shared key held by a Pacific entry, the non-Pacific entry
103
+ * takes priority. This mirrors the broadcast affiliate pattern where local entries override national entries.
104
+ * @param existingEntry - The entry currently holding the cache key, or undefined if the key is unclaimed.
105
+ * @param newEntry - The entry attempting to claim the cache key.
106
+ * @returns True if the new entry should claim the key (key is unclaimed, or existing entry is Pacific and new entry is not).
107
+ */
108
+ function shouldClaimSharedKey(existingEntry, newEntry) {
109
+ if (!existingEntry) {
110
+ return true;
111
+ }
112
+ return existingEntry.pacific && !newEntry.pacific;
113
+ }
114
+ /**
115
+ * Caches a single channel entry with multi-key lookups. Creates up to four cache keys per channel: (a) lowercased callSign, (b) stripped callSign with common
116
+ * suffixes removed, (c) lowercased branchOf/company/callSign, and (d) broadcast network abbreviation for local affiliates.
117
+ *
118
+ * Priority rules for shared keys (stripped, branchOf): non-Pacific entries override Pacific entries, otherwise first-write-wins. This ensures that
119
+ * display-name-based lookups (e.g., channelSelector "A&E") resolve to the primary East feed regardless of processing order. Primary callSign keys always use
120
+ * first-write-wins since each callSign is unique. Broadcast network keys use unconditional write so local affiliates override national feeds.
121
+ * @param callSign - The raw callSign string (already validated non-null by caller).
122
+ * @param branchOf - The branchOf/company/callSign value from the API.
123
+ * @param entry - The unified cache entry to store.
124
+ * @param affiliate - The broadcast network abbreviation (e.g., "NBC") or undefined for non-broadcast channels.
125
+ * @returns True if the entry was cached (at least one key written), false if all keys were already occupied.
126
+ */
127
+ function cacheChannelEntry(callSign, branchOf, entry, affiliate) {
128
+ const callSignLower = callSign.toLowerCase();
129
+ let keysWritten = 0;
130
+ // Primary key: lowercased callSign (e.g., "cnnhd"). Always first-write-wins since each callSign is unique.
131
+ if (!channelCache.has(callSignLower)) {
132
+ channelCache.set(callSignLower, entry);
133
+ keysWritten++;
134
+ }
135
+ // Secondary key: stripped callSign with common suffixes removed (e.g., "cnn" from "cnnhd" or "cnnhdp"). Non-Pacific overrides Pacific.
136
+ const strippedKey = stripCallSignSuffix(callSign);
137
+ if ((strippedKey !== callSignLower) && shouldClaimSharedKey(channelCache.get(strippedKey), entry)) {
138
+ channelCache.set(strippedKey, entry);
139
+ keysWritten++;
140
+ }
141
+ // Tertiary key: lowercased branchOf/company/callSign (e.g., "cnn", "animal planet"). Non-Pacific overrides Pacific.
142
+ if (branchOf) {
143
+ const branchKey = branchOf.toLowerCase();
144
+ if ((branchKey !== callSignLower) && (branchKey !== strippedKey) && shouldClaimSharedKey(channelCache.get(branchKey), entry)) {
145
+ channelCache.set(branchKey, entry);
146
+ keysWritten++;
147
+ }
148
+ }
149
+ // Broadcast network key (e.g., "nbc", "abc"). Unconditional write ensures local affiliates override any previously-cached national entry for the same network.
150
+ if (affiliate) {
151
+ channelCache.set(affiliate.toLowerCase(), entry);
152
+ keysWritten++;
153
+ }
154
+ return keysWritten > 0;
155
+ }
156
+ /**
157
+ * Processes a channelmap API response and populates the unified channel cache using three-pass tiered filtering. Channels are prioritized: non-TVE HD entitled
158
+ * first (cable HD - optimal for a home-network server), then TVE entitled (fallback for channels without a cable HD entry), then non-TVE SD entitled (safety net
159
+ * for SD-only channels). All passes share a single seenCallSigns set for exact callSign deduplication. Cross-tier dedup is handled naturally by
160
+ * cacheChannelEntry's first-write-wins guards - lower-tier entries cannot overwrite cache keys already occupied by higher-tier entries.
161
+ *
162
+ * Local affiliates are detected via callSignVoiceOverHint matching against BROADCAST_HINTS and preferred over national feeds (e.g., local KOMOD over national
163
+ * WLSD1 for ABC). Local identification uses a pre-scan of non-TVE entries - stations present in the non-TVE lineup are local to the subscriber's market.
164
+ * Sub-feeds (DATLN, TODAY) sharing the same voiceOverHint as the primary affiliate are deduplicated by the broadcast network key guard.
165
+ *
166
+ * Pacific timezone variants (callSigns ending with a Pacific suffix like "hdp", "hp", or "ph") are detected via isPacificCallSign and labeled with " (Pacific)"
167
+ * in the display name, matching the convention from generatePacificDefinitions() in the predefined channel system. Non-Pacific entries take priority over Pacific
168
+ * entries for shared cache keys (stripped callSign, branchOf) via shouldClaimSharedKey, ensuring that display-name-based lookups resolve to the East feed.
169
+ * @param data - The parsed channelmap API response.
170
+ */
171
+ function processChannelmapResponse(data) {
172
+ const channels = data._embedded?.channels;
173
+ if (!Array.isArray(channels)) {
174
+ return;
175
+ }
176
+ // Pre-scan for non-TVE local broadcast callSigns. These identify which entries are local affiliates vs national feeds. Non-TVE entries are always local to the
177
+ // subscriber's market, so an entry whose callSign also appears in the non-TVE lineup is the local affiliate for that broadcast network.
178
+ const localBroadcastCallSigns = new Set();
179
+ for (const channel of channels) {
180
+ if (channel.isTve || !channel.callSign || !channel.callSignVoiceOverHint) {
181
+ continue;
182
+ }
183
+ const hint = channel.callSignVoiceOverHint.toLowerCase();
184
+ if (BROADCAST_HINTS[hint]) {
185
+ localBroadcastCallSigns.add(channel.callSign.toLowerCase());
186
+ }
187
+ }
188
+ // Track callSigns we have already processed to enforce first-write-wins deduplication across all passes.
189
+ const seenCallSigns = new Set();
190
+ let count = 0;
191
+ // Three-pass tiered filtering. Each pass processes channels matching specific criteria. All passes share seenCallSigns for exact callSign dedup and use the
192
+ // same entry creation and caching logic. Cross-tier dedup is handled by cacheChannelEntry's first-write-wins guards on stripped callSign and branchOf keys.
193
+ const passes = [
194
+ // Pass 1: Non-TVE HD entitled. Cable HD channels - the primary version for a home-network server. Includes channels like PBS (KCTSD) and CW (KSTWD) that
195
+ // have no TVE entries at all, plus HD cable variants of channels that also have TVE entries.
196
+ { filter: (ch) => !ch.isTve && (ch.isHD === true) && (ch.entitled === true) },
197
+ // Pass 2: TVE entitled. TV Everywhere channels that work from any network. Serves as fallback for channels without a cable HD entry.
198
+ { filter: (ch) => (ch.isTve === true) && (ch.entitled === true) },
199
+ // Pass 3: Non-TVE SD entitled. Safety net for SD-only channels with no HD or TVE variant (e.g., PBS World, local digital subchannels).
200
+ { filter: (ch) => !ch.isTve && !ch.isHD && (ch.entitled === true) }
201
+ ];
202
+ for (const pass of passes) {
203
+ for (const channel of channels) {
204
+ if (!channel.callSign || !pass.filter(channel)) {
205
+ continue;
206
+ }
207
+ const callSignLower = channel.callSign.toLowerCase();
208
+ // Deduplicate by callSign - first occurrence wins (typically the lowest channel number).
209
+ if (seenCallSigns.has(callSignLower)) {
210
+ continue;
211
+ }
212
+ seenCallSigns.add(callSignLower);
213
+ // Skip audio-only channels (Music Choice, Stingray) - they produce static screens with music and are not useful for video capture.
214
+ const branchOf = channel["branchOf/company/callSign"] ?? "";
215
+ const branchLower = branchOf.toLowerCase();
216
+ if (AUDIO_ONLY_PREFIXES.some((prefix) => branchLower.startsWith(prefix))) {
217
+ continue;
218
+ }
219
+ // Determine the display name. For broadcast affiliates identified by callSignVoiceOverHint, use the network abbreviation. For cable channels, use the
220
+ // branchOf/company/callSign value (the network's display name). The channelSelector always uses the raw callSign for guaranteed uniqueness - branchOf
221
+ // names like "Cinemax" or "BET" are shared across multiple distinct channels (MAXHD, MXPLD, MXACD, etc.).
222
+ const voiceOverHint = (channel.callSignVoiceOverHint ?? "").toLowerCase();
223
+ let displayName = (branchOf.length > 0) ? branchOf : (channel.callSignVoiceOverHint ?? channel.callSign);
224
+ let affiliate;
225
+ const broadcastNetwork = BROADCAST_HINTS[voiceOverHint];
226
+ if (broadcastNetwork) {
227
+ // Broadcast affiliate handling: prefer local affiliates over national feeds. The channelmap API may include both a national feed (e.g., WLSD1 for
228
+ // ABC) and the subscriber's local affiliate (e.g., KOMOD for ABC). We identify local affiliates by checking whether their callSign appears in the
229
+ // non-TVE entries, which are always local to the subscriber's market.
230
+ const networkKey = broadcastNetwork.toLowerCase();
231
+ const isLocal = localBroadcastCallSigns.has(callSignLower);
232
+ const existingEntry = channelCache.get(networkKey);
233
+ if (existingEntry) {
234
+ // Network already cached. Override only when this entry is a better match: local replacing national, or lower channel number when both are local
235
+ // (e.g., KBTCD ch 108 over KCTSD ch 109 for PBS).
236
+ const existingIsLocal = localBroadcastCallSigns.has(existingEntry.callSign.toLowerCase());
237
+ const newNumber = channel.number ?? Infinity;
238
+ if (!isLocal || (existingIsLocal && (newNumber >= existingEntry.channelNumber))) {
239
+ continue;
240
+ }
241
+ // Better affiliate found: remove the old entry's cache keys so it doesn't appear as a duplicate in channel discovery.
242
+ const oldCallSign = existingEntry.callSign.toLowerCase();
243
+ channelCache.delete(oldCallSign);
244
+ const oldStripped = stripCallSignSuffix(existingEntry.callSign);
245
+ if (oldStripped !== oldCallSign) {
246
+ channelCache.delete(oldStripped);
247
+ }
248
+ count--;
249
+ }
250
+ displayName = broadcastNetwork;
251
+ affiliate = broadcastNetwork;
252
+ }
253
+ // Detect Pacific timezone variants by callSign suffix. Append " (Pacific)" to the display name to distinguish them in channel discovery, matching the
254
+ // naming convention established by generatePacificDefinitions() in the predefined channel system.
255
+ const pacific = isPacificCallSign(channel.callSign);
256
+ if (pacific) {
257
+ displayName += " (Pacific)";
258
+ }
259
+ const discovered = {
260
+ ...(affiliate ? { affiliate } : {}),
261
+ channelSelector: channel.callSign,
262
+ name: displayName
263
+ };
264
+ const entry = {
265
+ callSign: channel.callSign,
266
+ channelNumber: channel.number ?? Infinity,
267
+ discovered,
268
+ pacific
269
+ };
270
+ if (cacheChannelEntry(channel.callSign, branchOf, entry, affiliate)) {
271
+ count++;
272
+ }
273
+ }
274
+ }
275
+ if (count > 0) {
276
+ fullyEnumerated = true;
277
+ LOG.debug(config.debugCategory, "Channelmap API: cached %s channels (%s cache keys).", count, channelCache.size);
278
+ }
279
+ }
280
+ /**
281
+ * Sets up response interception on the page to capture the channelmap API response. The API fires automatically on guide page load and returns the complete
282
+ * channel lineup in a single response. Uses a WeakSet to prevent duplicate listener registration on the same page.
283
+ * @param page - The Puppeteer page object.
284
+ */
285
+ function setupChannelmapInterception(page) {
286
+ if (pagesWithListeners.has(page)) {
287
+ return;
288
+ }
289
+ pagesWithListeners.add(page);
290
+ page.on("response", (response) => {
291
+ const url = response.url();
292
+ if (!url.includes(CHANNELMAP_API_PATTERN) || (response.status() !== 200)) {
293
+ return;
294
+ }
295
+ void response.text().then((text) => {
296
+ // Cache the exact URL, headers, and body for CDP request interception on warm tunes. The exact URL ensures we only intercept the correct channelmap
297
+ // request - the SPA makes multiple requests to URLs matching the pattern, but only this one returns the channel lineup. We strip content-encoding and
298
+ // content-length because response.text() returns the decoded (decompressed) body - replaying encoding headers with decoded content would cause
299
+ // double-decompression.
300
+ cachedUrl = url;
301
+ cachedBody = text;
302
+ const headers = response.headers();
303
+ delete headers["content-encoding"];
304
+ delete headers["content-length"];
305
+ cachedHeaders = headers;
306
+ const data = JSON.parse(text);
307
+ processChannelmapResponse(data);
308
+ }).catch(() => {
309
+ // Response parsing failed - cold cache fallback handles it.
310
+ });
311
+ });
312
+ }
313
+ /**
314
+ * Looks up a channel in the unified cache using three-tier matching:
315
+ *
316
+ * 1. Exact match: cache key equals the lowercased input (matches callSigns, stripped callSigns, and branchOf names).
317
+ * 2. Suffix-tolerant: input + "hd" or input + "d" matches a cache key (e.g., "cnn" -> "cnnhd", "espn" -> "espnd").
318
+ * 3. Display name iteration: iterate all cache entries, check if discovered.name (lowercased) equals input. Catches verbose display names not covered by cache keys.
319
+ *
320
+ * When a non-exact match succeeds, the result is cached under the input key for O(1) lookup on subsequent calls.
321
+ * @param channelName - The channelSelector value (e.g., "CNN", "CNNHD", "Animal Planet").
322
+ * @returns The matching cache entry or null if no match is found.
323
+ */
324
+ function findChannel(channelName) {
325
+ const lower = channelName.toLowerCase();
326
+ // Tier 1: Exact match on any cache key (callSigns, stripped names, branchOf names).
327
+ const exact = channelCache.get(lower);
328
+ if (exact) {
329
+ return exact;
330
+ }
331
+ // Tier 2: Suffix-tolerant. Try appending common technology suffixes.
332
+ const hdMatch = channelCache.get(lower + "hd");
333
+ if (hdMatch) {
334
+ channelCache.set(lower, hdMatch);
335
+ return hdMatch;
336
+ }
337
+ const dMatch = channelCache.get(lower + "d");
338
+ if (dMatch) {
339
+ channelCache.set(lower, dMatch);
340
+ return dMatch;
341
+ }
342
+ // Tier 3: Display name iteration. Check if any entry's discovered.name matches the input. This catches verbose display names (e.g., "Cable News Network") that
343
+ // may not have a matching cache key.
344
+ const seen = new Set();
345
+ for (const entry of channelCache.values()) {
346
+ if (seen.has(entry)) {
347
+ continue;
348
+ }
349
+ seen.add(entry);
350
+ if (entry.discovered.name.toLowerCase() === lower) {
351
+ channelCache.set(lower, entry);
352
+ return entry;
353
+ }
354
+ }
355
+ return null;
356
+ }
357
+ /**
358
+ * Sets up channelmap API interception for the upcoming page navigation and, on warm tunes, enables CDP request interception to serve the cached channelmap
359
+ * response instantly. Like DirecTV's resolveDirectUrl, this always returns null - all tuning happens in the strategy's execute function via
360
+ * `_watchChannelEventHandler`.
361
+ * @param _channelSelector - The channel selector string (unused - channel lookup happens in the strategy).
362
+ * @param page - The Puppeteer page for response interception setup.
363
+ * @returns Always null - no direct URL navigation.
364
+ */
365
+ async function resolveDirectUrl(_channelSelector, page) {
366
+ // Set up response interception so the channelmap API response is captured during guide page navigation.
367
+ setupChannelmapInterception(page);
368
+ // On warm cache, intercept the channelmap API request and serve the cached response immediately. This eliminates the 3-5s network round-trip on warm tunes.
369
+ // We match on the exact URL captured during the cold tune - the SPA makes multiple requests to URLs matching the channelmap pattern, but only one is the
370
+ // channel lineup.
371
+ if (cachedBody && cachedUrl) {
372
+ const targetUrl = cachedUrl;
373
+ const body = cachedBody;
374
+ const headers = cachedHeaders;
375
+ await page.setRequestInterception(true);
376
+ page.on("request", (request) => {
377
+ if (request.url() === targetUrl) {
378
+ LOG.debug(config.debugCategory, "Channelmap API: served from cache.");
379
+ void request.respond({
380
+ body,
381
+ headers,
382
+ status: 200
383
+ }).catch(() => {
384
+ // Serve failed - fall back to the network so the cold-cache path takes over.
385
+ void request.continue().catch(() => {
386
+ // Page closed - ignore.
387
+ });
388
+ });
389
+ return;
390
+ }
391
+ void request.continue().catch(() => {
392
+ // Page closed - ignore.
393
+ });
394
+ });
395
+ }
396
+ return null;
397
+ }
398
+ /**
399
+ * Polls for a visible "Watch Now" modal button and clicks it if found. Some entertainment channels (e.g., Discovery) display this modal after
400
+ * `_watchChannelEventHandler` switches channels. News channels (e.g., CNN) auto-play without any modal. Runs as a fire-and-forget background task alongside
401
+ * `waitForVideoReady` - channels without modals resolve via `waitForVideoReady` alone, while channels with modals get unblocked by the click.
402
+ *
403
+ * The button requires JS-based detection because multiple `button.style-scope.tv-button` elements exist on the page with zero dimensions. We filter by text
404
+ * content ("Watch Now") and non-zero bounding rect to find the visible one.
405
+ * @param page - The Puppeteer page object.
406
+ */
407
+ async function watchNowModalPoll(page) {
408
+ const checks = Math.ceil(WATCH_NOW_POLL_DURATION / WATCH_NOW_POLL_INTERVAL);
409
+ for (let i = 0; i < checks; i++) {
410
+ // Delay between checks, but not before the first one - the first check is immediate.
411
+ if (i > 0) {
412
+ // eslint-disable-next-line no-await-in-loop
413
+ await delay(WATCH_NOW_POLL_INTERVAL);
414
+ }
415
+ try {
416
+ // eslint-disable-next-line no-await-in-loop
417
+ const clicked = await page.evaluate(() => {
418
+ const buttons = Array.from(document.querySelectorAll("button.style-scope.tv-button"));
419
+ for (const button of buttons) {
420
+ const text = button.textContent.trim();
421
+ const rect = button.getBoundingClientRect();
422
+ if ((text === "Watch Now") && (rect.width > 0)) {
423
+ button.click();
424
+ return true;
425
+ }
426
+ }
427
+ return false;
428
+ });
429
+ if (clicked) {
430
+ LOG.debug(config.debugCategory, "Watch Now modal dismissed.");
431
+ return;
432
+ }
433
+ }
434
+ catch {
435
+ // Page closed or navigated away - exit silently.
436
+ return;
437
+ }
438
+ }
439
+ }
440
+ /**
441
+ * Derives a deduplicated DiscoveredChannel array from the unified channel cache. Multiple cache keys may point to the same ChannelEntry, so we deduplicate
442
+ * via Set reference equality. Sorts by name before returning.
443
+ * @returns Sorted, deduplicated array of discovered channels.
444
+ */
445
+ function buildDiscoveredChannels() {
446
+ const channels = [];
447
+ const seen = new Set();
448
+ for (const entry of channelCache.values()) {
449
+ if (seen.has(entry)) {
450
+ continue;
451
+ }
452
+ seen.add(entry);
453
+ channels.push(entry.discovered);
454
+ }
455
+ channels.sort((a, b) => a.name.localeCompare(b.name));
456
+ return channels;
457
+ }
458
+ /**
459
+ * In-page SPA tuning strategy. Waits for the Polymer SPA's `TV-APP` element and its `channelMap.channels` to populate, then invokes
460
+ * `_watchChannelEventHandler(null, { channel })` to switch channels in-page. This is the execute path for both warm and cold cache scenarios - the guide URL
461
+ * navigation loads the SPA, and this strategy performs the channel switch within it.
462
+ * @param page - The Puppeteer page object (on the guide page after SPA load).
463
+ * @param profile - The resolved site profile with a non-null channelSelector.
464
+ * @returns Result object with success status and optional failure reason.
465
+ */
466
+ async function directStrategy(page, profile) {
467
+ const channelName = profile.channelSelector;
468
+ const timeout = CONFIG.streaming.videoTimeout;
469
+ // Wait for the TV-APP Polymer element to be present in the DOM.
470
+ try {
471
+ await page.waitForSelector("tv-app", { timeout });
472
+ }
473
+ catch {
474
+ return { reason: config.label + " guide page did not load within timeout.", success: false };
475
+ }
476
+ // Wait for the SPA's channelMap.channels to populate. The channelmap API fires during page load and the SPA stores the response in this property.
477
+ try {
478
+ await page.waitForFunction(() => {
479
+ const tvApp = document.querySelector("tv-app");
480
+ if (!tvApp) {
481
+ return false;
482
+ }
483
+ const channels = tvApp.channelMap?.channels;
484
+ return (channels !== null) && (channels !== undefined) && (typeof channels === "object") && (Object.keys(channels).length > 0);
485
+ }, { timeout });
486
+ }
487
+ catch {
488
+ // If we served a cached response and the SPA couldn't process it, the cached data is likely stale or corrupt. Clear it so the next tune falls through to
489
+ // a fresh network fetch.
490
+ invalidateCachedResponse();
491
+ return { reason: config.label + " channel lineup did not load within timeout.", success: false };
492
+ }
493
+ // Poll for Node-side cache population. The channelmap API response is intercepted by setupChannelmapInterception (called by resolveDirectUrl before we got
494
+ // here). We need the Node-side cache for findChannel's three-tier matching.
495
+ const deadline = Date.now() + timeout;
496
+ while ((channelCache.size === 0) && (Date.now() < deadline)) {
497
+ // eslint-disable-next-line no-await-in-loop
498
+ await delay(CACHE_POLL_INTERVAL);
499
+ }
500
+ if (channelCache.size === 0) {
501
+ return { reason: config.label + " channel lineup API did not respond within timeout.", success: false };
502
+ }
503
+ // Look up the target channel in the Node-side cache.
504
+ const entry = findChannel(channelName);
505
+ if (!entry) {
506
+ logAvailableChannels({
507
+ availableChannels: buildDiscoveredChannels().map((ch) => ch.name + " (" + ch.channelSelector + ")").sort(),
508
+ channelName,
509
+ guideUrl: config.guideUrl,
510
+ presetSuffix: config.presetSuffix,
511
+ providerName: config.label
512
+ });
513
+ return { reason: "Channel " + channelName + " was not found in the " + config.label + " channel lineup.", success: false };
514
+ }
515
+ // Call _watchChannelEventHandler on the TV-APP element to switch channels in-page. The SPA's channelMap.channels is keyed by internal ID tags, not callSigns,
516
+ // so we iterate the values to find the matching callSign.
517
+ const targetCallSign = entry.callSign;
518
+ try {
519
+ const found = await page.evaluate((callSign) => {
520
+ const tvApp = document.querySelector("tv-app");
521
+ if (!tvApp) {
522
+ return false;
523
+ }
524
+ const channelMap = tvApp.channelMap?.channels;
525
+ if (!channelMap) {
526
+ return false;
527
+ }
528
+ // Find the channel object by matching callSign. The channelMap is keyed by internal ID tags, so we iterate values.
529
+ for (const channel of Object.values(channelMap)) {
530
+ if (channel?.callSign === callSign) {
531
+ tvApp._watchChannelEventHandler(null, { channel });
532
+ return true;
533
+ }
534
+ }
535
+ return false;
536
+ }, targetCallSign);
537
+ if (!found) {
538
+ // The Node-side cache had this callSign but the SPA's channelMap doesn't - the cached response is stale. Clear it so the next tune fetches fresh data.
539
+ invalidateCachedResponse();
540
+ return { reason: "Channel " + targetCallSign + " is no longer available in the " + config.label + " channel lineup.", success: false };
541
+ }
542
+ }
543
+ catch (error) {
544
+ return { reason: "Failed to switch " + config.label + " channel: " + formatError(error) + ".", success: false };
545
+ }
546
+ LOG.debug(config.debugCategory, "Invoked _watchChannelEventHandler for %s (callSign: %s).", channelName, targetCallSign);
547
+ // Launch fire-and-forget Watch Now modal poll. Some entertainment channels show a modal after channel switch that must be clicked to start playback.
548
+ void watchNowModalPoll(page);
549
+ return { success: true };
550
+ }
551
+ /**
552
+ * Discovers all channels by navigating to the guide page and intercepting the channelmap API response. Returns cached results if the cache is already fully
553
+ * populated. Sets handlesOwnNavigation so the route handler does not navigate before calling this function.
554
+ * @param page - The Puppeteer page object (fresh page, not yet navigated).
555
+ * @returns Array of discovered channels.
556
+ */
557
+ async function discoverChannels(page) {
558
+ // Return from cache if already fully enumerated.
559
+ if (fullyEnumerated && (channelCache.size > 0)) {
560
+ return buildDiscoveredChannels();
561
+ }
562
+ // Set up response interception before navigation so we capture the channelmap API response during page load.
563
+ setupChannelmapInterception(page);
564
+ try {
565
+ await page.goto(config.guideUrl, { timeout: CONFIG.streaming.navigationTimeout, waitUntil: "networkidle2" });
566
+ }
567
+ catch (error) {
568
+ LOG.warn("Failed to load %s guide page: %s.", config.label, formatError(error));
569
+ return [];
570
+ }
571
+ // Poll for cache population.
572
+ const deadline = Date.now() + CONFIG.streaming.videoTimeout;
573
+ while ((channelCache.size === 0) && (Date.now() < deadline)) {
574
+ // eslint-disable-next-line no-await-in-loop
575
+ await delay(CACHE_POLL_INTERVAL);
576
+ }
577
+ if (channelCache.size === 0) {
578
+ LOG.warn("%s channel lineup did not return any channels.", config.label);
579
+ return [];
580
+ }
581
+ return buildDiscoveredChannels();
582
+ }
583
+ /**
584
+ * Returns cached discovered channels from the unified channel cache, or null if the cache is empty or has not been fully enumerated.
585
+ * @returns Sorted array of discovered channels or null.
586
+ */
587
+ function getCachedChannels() {
588
+ if (!fullyEnumerated || (channelCache.size === 0)) {
589
+ return null;
590
+ }
591
+ return buildDiscoveredChannels();
592
+ }
593
+ /**
594
+ * Clears the cached channelmap API response so the next tune fetches fresh data from the network. Called when the strategy detects that the cached response
595
+ * produced a failure - either the SPA couldn't populate channelMap.channels, or a callSign present in the Node-side cache was missing from the SPA's channelMap.
596
+ * The current tune still fails (the channelmap request already fired), but the next tune self-heals via the cold-cache path.
597
+ */
598
+ function invalidateCachedResponse() {
599
+ if (!cachedBody) {
600
+ return;
601
+ }
602
+ cachedBody = null;
603
+ cachedHeaders = {};
604
+ cachedUrl = null;
605
+ LOG.debug(config.debugCategory, "Channelmap API: invalidated cached response - next tune will fetch from network.");
606
+ }
607
+ /**
608
+ * Clears the channel cache. Called by clearChannelSelectionCaches() in the coordinator when the browser restarts, since a fresh browser session may have
609
+ * different channel availability.
610
+ */
611
+ function clearCache() {
612
+ cachedBody = null;
613
+ cachedHeaders = {};
614
+ cachedUrl = null;
615
+ channelCache.clear();
616
+ fullyEnumerated = false;
617
+ }
618
+ /**
619
+ * Invalidates the cached channel entry for the given channel selector. Called when a stream fails to start after tuning, allowing the next tune attempt to
620
+ * re-resolve via fresh channelmap data.
621
+ * @param channelSelector - The channel selector string to invalidate.
622
+ */
623
+ function invalidateDirectUrl(channelSelector) {
624
+ channelCache.delete(channelSelector.toLowerCase());
625
+ }
626
+ return {
627
+ discoverChannels,
628
+ getCachedChannels,
629
+ guideUrl: config.guideUrl,
630
+ handlesOwnNavigation: true,
631
+ label: config.label,
632
+ noDirectTuneOptimization: true,
633
+ // Profile for Comcast Polymer SPA live channels. The channelmap API at xtvapi.cloudtv.comcast.net returns the complete channel lineup. Tuning uses in-page SPA
634
+ // channel switching: after the guide page loads the Polymer SPA, the strategy calls `_watchChannelEventHandler(null, { channel })` on the `TV-APP` element to
635
+ // switch channels in ~2-3 seconds without page navigation. Extends fullscreenApi for requestFullscreen() behavior on the player page.
636
+ profile: {
637
+ category: "multiChannel",
638
+ channelSelection: { strategy: config.strategyName },
639
+ description: config.profileDescription,
640
+ extends: "fullscreenApi",
641
+ summary: config.profileSummary
642
+ },
643
+ profileName: config.profileName,
644
+ slug: config.slug,
645
+ strategy: {
646
+ clearCache,
647
+ execute: directStrategy,
648
+ invalidateDirectUrl,
649
+ resolveDirectUrl
650
+ },
651
+ strategyName: config.strategyName,
652
+ tinySegmentThreshold: 150
653
+ };
654
+ }
655
+ //# sourceMappingURL=comcastPolymer.js.map