prismcast 1.1.0 → 1.2.1

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 (136) hide show
  1. package/README.md +20 -4
  2. package/dist/app.js +5 -20
  3. package/dist/app.js.map +1 -1
  4. package/dist/browser/cdp.js +1 -4
  5. package/dist/browser/cdp.js.map +1 -1
  6. package/dist/browser/channelSelection.d.ts +5 -0
  7. package/dist/browser/channelSelection.js +803 -53
  8. package/dist/browser/channelSelection.js.map +1 -1
  9. package/dist/browser/display.js +1 -4
  10. package/dist/browser/display.js.map +1 -1
  11. package/dist/browser/index.js +10 -28
  12. package/dist/browser/index.js.map +1 -1
  13. package/dist/browser/video.d.ts +24 -10
  14. package/dist/browser/video.js +101 -44
  15. package/dist/browser/video.js.map +1 -1
  16. package/dist/channels/index.d.ts +1 -17
  17. package/dist/channels/index.js +246 -137
  18. package/dist/channels/index.js.map +1 -1
  19. package/dist/config/index.js +10 -9
  20. package/dist/config/index.js.map +1 -1
  21. package/dist/config/presets.js +1 -4
  22. package/dist/config/presets.js.map +1 -1
  23. package/dist/config/profiles.d.ts +14 -13
  24. package/dist/config/profiles.js +47 -287
  25. package/dist/config/profiles.js.map +1 -1
  26. package/dist/config/providers.d.ts +75 -0
  27. package/dist/config/providers.js +283 -0
  28. package/dist/config/providers.js.map +1 -0
  29. package/dist/config/sites.d.ts +20 -0
  30. package/dist/config/sites.js +350 -0
  31. package/dist/config/sites.js.map +1 -0
  32. package/dist/config/userChannels.d.ts +15 -3
  33. package/dist/config/userChannels.js +85 -33
  34. package/dist/config/userChannels.js.map +1 -1
  35. package/dist/config/userConfig.d.ts +1 -0
  36. package/dist/config/userConfig.js +12 -26
  37. package/dist/config/userConfig.js.map +1 -1
  38. package/dist/hdhr/channelMap.js +3 -6
  39. package/dist/hdhr/channelMap.js.map +1 -1
  40. package/dist/hdhr/deviceId.js +1 -4
  41. package/dist/hdhr/deviceId.js.map +1 -1
  42. package/dist/hdhr/discover.js.map +1 -1
  43. package/dist/hdhr/index.js +1 -4
  44. package/dist/hdhr/index.js.map +1 -1
  45. package/dist/index.js +4 -16
  46. package/dist/index.js.map +1 -1
  47. package/dist/routes/assets.js +1 -4
  48. package/dist/routes/assets.js.map +1 -1
  49. package/dist/routes/auth.js +4 -2
  50. package/dist/routes/auth.js.map +1 -1
  51. package/dist/routes/channels.js +2 -2
  52. package/dist/routes/channels.js.map +1 -1
  53. package/dist/routes/components.js.map +1 -1
  54. package/dist/routes/config.js +163 -34
  55. package/dist/routes/config.js.map +1 -1
  56. package/dist/routes/health.js +1 -4
  57. package/dist/routes/health.js.map +1 -1
  58. package/dist/routes/hls.js +1 -4
  59. package/dist/routes/hls.js.map +1 -1
  60. package/dist/routes/index.js +1 -4
  61. package/dist/routes/index.js.map +1 -1
  62. package/dist/routes/logs.js +3 -12
  63. package/dist/routes/logs.js.map +1 -1
  64. package/dist/routes/mpegts.js +1 -4
  65. package/dist/routes/mpegts.js.map +1 -1
  66. package/dist/routes/play.js +1 -4
  67. package/dist/routes/play.js.map +1 -1
  68. package/dist/routes/playlist.js +2 -5
  69. package/dist/routes/playlist.js.map +1 -1
  70. package/dist/routes/root.js +43 -12
  71. package/dist/routes/root.js.map +1 -1
  72. package/dist/routes/streams.js +2 -8
  73. package/dist/routes/streams.js.map +1 -1
  74. package/dist/routes/theme.js +1 -4
  75. package/dist/routes/theme.js.map +1 -1
  76. package/dist/routes/ui.js +1 -4
  77. package/dist/routes/ui.js.map +1 -1
  78. package/dist/service/commands.js +1 -4
  79. package/dist/service/commands.js.map +1 -1
  80. package/dist/service/generators.js +4 -16
  81. package/dist/service/generators.js.map +1 -1
  82. package/dist/streaming/clients.js.map +1 -1
  83. package/dist/streaming/fmp4Segmenter.d.ts +1 -0
  84. package/dist/streaming/fmp4Segmenter.js +7 -12
  85. package/dist/streaming/fmp4Segmenter.js.map +1 -1
  86. package/dist/streaming/hls.d.ts +3 -0
  87. package/dist/streaming/hls.js +39 -21
  88. package/dist/streaming/hls.js.map +1 -1
  89. package/dist/streaming/hlsSegments.js +1 -4
  90. package/dist/streaming/hlsSegments.js.map +1 -1
  91. package/dist/streaming/lifecycle.js +1 -4
  92. package/dist/streaming/lifecycle.js.map +1 -1
  93. package/dist/streaming/monitor.d.ts +1 -0
  94. package/dist/streaming/monitor.js +296 -76
  95. package/dist/streaming/monitor.js.map +1 -1
  96. package/dist/streaming/mp4Parser.js.map +1 -1
  97. package/dist/streaming/mpegts.js +1 -4
  98. package/dist/streaming/mpegts.js.map +1 -1
  99. package/dist/streaming/registry.d.ts +7 -0
  100. package/dist/streaming/registry.js +10 -0
  101. package/dist/streaming/registry.js.map +1 -1
  102. package/dist/streaming/setup.d.ts +4 -1
  103. package/dist/streaming/setup.js +35 -33
  104. package/dist/streaming/setup.js.map +1 -1
  105. package/dist/streaming/showInfo.js +3 -6
  106. package/dist/streaming/showInfo.js.map +1 -1
  107. package/dist/streaming/statusEmitter.d.ts +2 -0
  108. package/dist/streaming/statusEmitter.js +2 -4
  109. package/dist/streaming/statusEmitter.js.map +1 -1
  110. package/dist/types/index.d.ts +34 -2
  111. package/dist/utils/errors.js +1 -4
  112. package/dist/utils/errors.js.map +1 -1
  113. package/dist/utils/evaluate.js +3 -6
  114. package/dist/utils/evaluate.js.map +1 -1
  115. package/dist/utils/ffmpeg.js +2 -8
  116. package/dist/utils/ffmpeg.js.map +1 -1
  117. package/dist/utils/fileLogger.js +9 -30
  118. package/dist/utils/fileLogger.js.map +1 -1
  119. package/dist/utils/format.d.ts +7 -0
  120. package/dist/utils/format.js +20 -0
  121. package/dist/utils/format.js.map +1 -1
  122. package/dist/utils/html.js +1 -4
  123. package/dist/utils/html.js.map +1 -1
  124. package/dist/utils/logEmitter.js +1 -4
  125. package/dist/utils/logEmitter.js.map +1 -1
  126. package/dist/utils/logger.js +4 -16
  127. package/dist/utils/logger.js.map +1 -1
  128. package/dist/utils/m3u.js +1 -4
  129. package/dist/utils/m3u.js.map +1 -1
  130. package/dist/utils/morganStream.js +1 -4
  131. package/dist/utils/morganStream.js.map +1 -1
  132. package/dist/utils/platform.js.map +1 -1
  133. package/dist/utils/retry.js +1 -4
  134. package/dist/utils/retry.js.map +1 -1
  135. package/dist/utils/streamContext.js.map +1 -1
  136. package/package.json +3 -3
@@ -0,0 +1,350 @@
1
+ import { extractDomain } from "../utils/index.js";
2
+ /*
3
+ * Streaming sites implement their video players in wildly different ways. Some use standard HTML5 video with keyboard shortcuts, others embed players in iframes,
4
+ * and many have unique quirks like auto-muting or requiring specific fullscreen methods. Rather than scattering site-specific conditionals throughout the streaming
5
+ * code, we define "site profiles" that describe each site's behavior in a declarative way.
6
+ *
7
+ * The profile system has three components:
8
+ *
9
+ * 1. SITE_PROFILES: Named behavior configurations describing how to handle different player implementations. Profiles can inherit from other profiles using the
10
+ * "extends" property, allowing us to define base profiles for common patterns (like "keyboardFullscreen" for sites using the f key) and then extend them with
11
+ * site-specific variations.
12
+ *
13
+ * 2. DOMAIN_CONFIG: A mapping from domain patterns to site profiles and provider display names. When streaming a URL, we check if it matches any known domain and
14
+ * use the corresponding profile. Provider display names give friendly labels (e.g., "Hulu" instead of "hulu.com") for the UI source column and provider
15
+ * dropdowns. This is the primary mechanism for automatically selecting the right behavior and generating friendly display names.
16
+ *
17
+ * 3. Channel-level profile hints: Individual channel definitions can specify an explicit profile name, overriding URL-based detection. This is useful when a
18
+ * channel's URL doesn't match the expected domain pattern, or when the same domain serves multiple channel types that need different handling.
19
+ *
20
+ * Profile resolution happens at stream startup and the resolved profile is passed through the entire streaming pipeline. The profile flags control:
21
+ * - How fullscreen is triggered (keyboard shortcut vs JavaScript API)
22
+ * - Whether to search for video elements in iframes
23
+ * - Which video element to select when multiple exist
24
+ * - Whether to wait for network activity to settle before playback
25
+ * - Whether to lock volume properties to prevent auto-muting
26
+ * - Whether the page is static content (no video element expected)
27
+ *
28
+ * When adding support for a new streaming site, first check if an existing profile matches its behavior. Only create a new profile if the site requires unique
29
+ * handling not covered by existing profiles.
30
+ */
31
+ /*
32
+ * Each profile defines a set of behavior flags that control how we interact with the video player. Profiles are organized in an inheritance hierarchy based on
33
+ * behavior patterns rather than site ownership. This makes it easier to identify the right profile when adding new channels.
34
+ *
35
+ * Base profiles (no extends):
36
+ * - keyboardFullscreen: Sites using the f key for fullscreen toggle
37
+ * - fullscreenApi: Sites requiring the JavaScript requestFullscreen() API
38
+ * - staticPage: Non-video pages captured as static visual content
39
+ *
40
+ * Derived profiles (extends a base):
41
+ * - keyboardDynamic: Keyboard fullscreen + network idle wait (extends keyboardFullscreen)
42
+ * - keyboardMultiVideo: Keyboard fullscreen + multi-video selection (extends keyboardFullscreen)
43
+ * - keyboardIframe: Keyboard fullscreen + iframe handling (extends keyboardFullscreen)
44
+ * - keyboardDynamicMultiVideo: Keyboard + network idle + multi-video selection (extends keyboardDynamic)
45
+ * - clickToPlayKeyboard: Click to start playback + keyboard fullscreen (extends keyboardFullscreen)
46
+ * - brightcove: Brightcove players using API fullscreen + network idle wait (extends fullscreenApi)
47
+ * - clickToPlayApi: Click to start playback + API fullscreen (extends fullscreenApi)
48
+ * - disneyNow: DisneyNOW player with play button overlay + multi-video (extends clickToPlayApi)
49
+ * - embeddedPlayer: Iframe-based players using fullscreen API (extends fullscreenApi)
50
+ * - apiMultiVideo: API fullscreen + multi-video + tile-based channel selection (extends fullscreenApi)
51
+ * - huluLive: Hulu Live TV with guide grid channel selection + fullscreen button (extends fullscreenApi)
52
+ * - embeddedDynamicMultiVideo: Embedded + network idle + multi-video selection (extends embeddedPlayer)
53
+ * - embeddedVolumeLock: Embedded + volume property locking (extends embeddedPlayer)
54
+ * - youtubeTV: YouTube TV with non-virtualized EPG grid channel selection (extends fullscreenApi)
55
+ *
56
+ * Each profile includes a description field documenting its purpose. This is metadata only - it's stripped during profile resolution and exists purely for
57
+ * documentation.
58
+ */
59
+ export const SITE_PROFILES = {
60
+ // Profile for multi-channel live TV pages that present a grid or shelf of live channel tiles requiring tile-based selection followed by a play button click. Uses
61
+ // the fullscreen API and multi-video selection to find the actively playing stream after channel selection. Does not use iframe handling or network idle wait
62
+ // because these sites serve video directly in the main page and have persistent connections that prevent network idle.
63
+ apiMultiVideo: {
64
+ category: "multiChannel",
65
+ channelSelection: { strategy: "tileClick" },
66
+ description: "Multi-channel sites with tile-based channel grid. Requires Channel Selector set to the CSS selector for the channel tile.",
67
+ extends: "fullscreenApi",
68
+ selectReadyVideo: true,
69
+ summary: "Multi-channel (tile selection, needs selector)"
70
+ },
71
+ // Profile for sites using the Brightcove player platform. Brightcove players require waiting for network activity to settle before the video player is fully
72
+ // initialized. The player dynamically loads its configuration and stream manifest, so waitForNetworkIdle ensures we don't try to interact with the player before
73
+ // it's ready. Uses the JavaScript fullscreen API rather than keyboard shortcuts because Brightcove intercepts keyboard events.
74
+ brightcove: {
75
+ category: "api",
76
+ description: "Brightcove player sites requiring network idle wait and API fullscreen.",
77
+ extends: "fullscreenApi",
78
+ summary: "Brightcove players (network wait)",
79
+ waitForNetworkIdle: true
80
+ },
81
+ // Profile for sites that require clicking to start playback. Some players don't autoplay and need user interaction to begin. Uses the JavaScript fullscreen API.
82
+ // Set clickSelector in the profile or channel definition to specify a play button element; otherwise clicks the video element directly.
83
+ clickToPlayApi: {
84
+ category: "api",
85
+ clickToPlay: true,
86
+ description: "Sites requiring a click to start playback, using the JavaScript fullscreen API. Use clickSelector for play button overlays.",
87
+ extends: "fullscreenApi",
88
+ summary: "Click-to-play (API fullscreen)"
89
+ },
90
+ // Profile for sites that require clicking to start playback, using keyboard 'f' for fullscreen. Use this when clickToPlayApi doesn't work for fullscreen but the
91
+ // site responds to the 'f' key. Set clickSelector in the profile or channel definition to specify a play button element.
92
+ clickToPlayKeyboard: {
93
+ category: "keyboard",
94
+ clickToPlay: true,
95
+ description: "Sites requiring a click to start playback, using the 'f' key for fullscreen. Use clickSelector for play button overlays.",
96
+ extends: "keyboardFullscreen",
97
+ summary: "Click-to-play ('f' key fullscreen)"
98
+ },
99
+ // Profile for DisneyNOW (disneynow.com) which has a play button overlay that must be clicked to start playback and multiple video elements on the page.
100
+ disneyNow: {
101
+ category: "api",
102
+ clickSelector: ".overlay__button button",
103
+ description: "DisneyNOW player with play button overlay and multiple video elements.",
104
+ extends: "clickToPlayApi",
105
+ selectReadyVideo: true,
106
+ summary: "DisneyNOW player"
107
+ },
108
+ // Profile for iframe-embedded players that also have multiple video elements (ads, placeholders, main content) and need network activity to settle. The
109
+ // selectReadyVideo flag ensures we find the video with actual content rather than an ad placeholder. Combines iframe handling with API-based fullscreen.
110
+ embeddedDynamicMultiVideo: {
111
+ category: "api",
112
+ description: "Iframe-embedded players with multiple video elements requiring network idle wait.",
113
+ extends: "embeddedPlayer",
114
+ selectReadyVideo: true,
115
+ summary: "Embedded multi-video (network wait)",
116
+ waitForNetworkIdle: true
117
+ },
118
+ // Intermediate profile for sites that both embed their player in an iframe AND require the JavaScript fullscreen API. Many modern players use this architecture
119
+ // to isolate ad content and use programmatic fullscreen rather than keyboard shortcuts. This profile combines iframe handling with API-based fullscreen.
120
+ embeddedPlayer: {
121
+ category: "api",
122
+ description: "Intermediate base profile for iframe-embedded players using fullscreen API.",
123
+ extends: "fullscreenApi",
124
+ needsIframeHandling: true,
125
+ summary: "Embedded iframe players"
126
+ },
127
+ // Profile for iframe-embedded players that aggressively mute audio after page load - likely to comply with autoplay policies or for accessibility reasons. Some
128
+ // sites set video.muted = true even after we unmute it. The lockVolumeProperties flag uses Object.defineProperty to override the muted and volume getters/setters,
129
+ // preventing the site from re-muting the video.
130
+ embeddedVolumeLock: {
131
+ category: "api",
132
+ description: "Iframe-embedded players that aggressively mute audio after page load.",
133
+ extends: "embeddedPlayer",
134
+ lockVolumeProperties: true,
135
+ summary: "Embedded players that auto-mute"
136
+ },
137
+ // Base profile for sites that require the JavaScript fullscreen API (element.requestFullscreen()) instead of keyboard shortcuts. Many modern players intercept
138
+ // keyboard events for their own controls, making the f key unreliable. Calling requestFullscreen() directly on the video element bypasses the player's keyboard
139
+ // handling and reliably enters fullscreen mode.
140
+ fullscreenApi: {
141
+ category: "api",
142
+ description: "Base profile for sites requiring the JavaScript fullscreen API.",
143
+ summary: "Sites needing JavaScript fullscreen",
144
+ useRequestFullscreen: true
145
+ },
146
+ // Profile for HBO Max live channels (play.hbomax.com). The HBO brand page contains a "Distribution Channels" rail showing all 5 live linear channels (HBO, HBO
147
+ // Hits, HBO Drama, HBO Comedy, HBO Movies) as tiles. The hboGrid strategy discovers the HBO tab URL from the homepage menu bar, navigates to it, then scrapes the
148
+ // channel rail for the watch URL matching the channelSelector name. Extends fullscreenApi for requestFullscreen() behavior inherited by the watch page.
149
+ hboMax: {
150
+ category: "multiChannel",
151
+ channelSelection: { strategy: "hboGrid" },
152
+ description: "HBO Max with live channel rail selection. Set Channel Selector to the channel name (e.g., HBO, HBO Hits).",
153
+ extends: "fullscreenApi",
154
+ summary: "HBO Max (live channels, needs selector)"
155
+ },
156
+ // Profile for Hulu Live TV which presents a guide grid of live channels. The channel list is revealed by clicking a tab (listSelector), then the desired channel
157
+ // is found by matching img.alt text. Uses the fullscreen API (inherited from fullscreenApi) plus a dedicated fullscreen button selector for the player's native
158
+ // maximize control. Requires selectReadyVideo because the page may have multiple video elements (ads, previews, main content). Uses waitForNetworkIdle because
159
+ // Hulu's SPA has heavy async initialization that often prevents the load event from firing within the retryOperation timeout; the graceful networkidle2 fallback
160
+ // in navigateToPage() allows execution to continue to channel selection even when background requests are still pending.
161
+ huluLive: {
162
+ category: "multiChannel",
163
+ channelSelection: { listSelector: "#CHANNELS", playSelector: "[data-testid=\"generic-tile-thumbnail\"]", strategy: "guideGrid" },
164
+ description: "Hulu Live TV with guide grid channel selection. Requires Channel Selector set to the channel name matching the guide grid image alt text.",
165
+ extends: "fullscreenApi",
166
+ fullscreenSelector: "[aria-label=\"Maximize\"]",
167
+ selectReadyVideo: true,
168
+ summary: "Hulu Live TV (guide grid, needs selector)",
169
+ waitForNetworkIdle: true
170
+ },
171
+ // Profile for sites that use keyboard fullscreen and also need time for network activity to settle before the player is fully initialized. These sites dynamically
172
+ // load their player and content. The waitForNetworkIdle flag ensures we don't try to interact with the player until all initial network requests have completed.
173
+ keyboardDynamic: {
174
+ category: "keyboard",
175
+ description: "Keyboard fullscreen sites requiring network idle wait for dynamic content loading.",
176
+ extends: "keyboardFullscreen",
177
+ summary: "Dynamic sites ('f' key fullscreen)",
178
+ waitForNetworkIdle: true
179
+ },
180
+ // Profile for multi-channel player pages that use keyboard fullscreen and need both network idle wait and multi-video selection. These pages present multiple
181
+ // channels to choose from, and the channelSelector property in the channel definition specifies which one to select. Extends keyboardDynamic to inherit network
182
+ // idle wait behavior. Uses thumbnailRow strategy for channel selection (find channel by thumbnail image URL, click adjacent show entry).
183
+ keyboardDynamicMultiVideo: {
184
+ category: "multiChannel",
185
+ channelSelection: { strategy: "thumbnailRow" },
186
+ description: "Multi-channel sites with thumbnail row layout. Requires Channel Selector set to the channel's thumbnail image URL.",
187
+ extends: "keyboardDynamic",
188
+ selectReadyVideo: true,
189
+ summary: "Multi-channel (thumbnail row, needs selector)"
190
+ },
191
+ // Base profile for sites that respond to the f key for fullscreen toggle. This is the most common fullscreen mechanism, following YouTube-style keyboard
192
+ // shortcuts. The f key is sent as a keyboard event to the page, triggering the player's built-in fullscreen toggle. This works with most standard video players.
193
+ keyboardFullscreen: {
194
+ category: "keyboard",
195
+ description: "Base profile for sites that respond to the f key for fullscreen toggle.",
196
+ fullscreenKey: "f",
197
+ summary: "Standard 'f' key fullscreen"
198
+ },
199
+ // Profile for sites using keyboard fullscreen with video players embedded in iframes. The video element is not directly in the main page DOM, so we need to search
200
+ // through all frames to find it. Once found, the player responds to the standard f key for fullscreen.
201
+ keyboardIframe: {
202
+ category: "keyboard",
203
+ description: "Keyboard fullscreen sites with video embedded in iframes.",
204
+ extends: "keyboardFullscreen",
205
+ needsIframeHandling: true,
206
+ summary: "Iframe players ('f' key fullscreen)"
207
+ },
208
+ // Profile for sites using keyboard fullscreen that load multiple video elements simultaneously - placeholder videos, ad videos, and the main content. We must find
209
+ // the video element that has actually loaded playable data (readyState >= 3) rather than just taking the first video element.
210
+ keyboardMultiVideo: {
211
+ category: "keyboard",
212
+ description: "Keyboard fullscreen sites with multiple video elements requiring ready-state selection.",
213
+ extends: "keyboardFullscreen",
214
+ selectReadyVideo: true,
215
+ summary: "Multi-video sites ('f' key fullscreen)"
216
+ },
217
+ // Profile for non-video pages that should be captured as static visual content. Examples include weather displays (weatherscan.net), maps (windy.com), and
218
+ // diagnostic pages. The noVideo flag tells the streaming code not to wait for a video element or set up playback monitoring - just capture whatever is displayed.
219
+ staticPage: {
220
+ category: "special",
221
+ description: "Base profile for non-video pages captured as static visual content.",
222
+ noVideo: true,
223
+ summary: "Static pages (no video)"
224
+ },
225
+ // Profile for YouTube TV (tv.youtube.com/live). The guide grid renders all ~256 channel rows in the DOM simultaneously (no virtualization), each containing a
226
+ // direct watch URL. The youtubeGrid strategy performs a single querySelector to find the target channel's watch link via aria-label, extracts the URL, and
227
+ // navigates directly — no scrolling, clicking, or timing workarounds needed. Uses selectReadyVideo because the watch page has ~36 video elements (live preview
228
+ // thumbnails from the guide) but only one active stream with readyState >= 3 and videoWidth > 0. Extends fullscreenApi because requestFullscreen() works
229
+ // directly on the active video element without gesture requirements.
230
+ youtubeTV: {
231
+ category: "multiChannel",
232
+ channelSelection: { strategy: "youtubeGrid" },
233
+ description: "YouTube TV with EPG grid channel selection. Use the guide name or a network name (e.g., NBC) for locals. PBS auto-resolves to major affiliates.",
234
+ extends: "fullscreenApi",
235
+ selectReadyVideo: true,
236
+ summary: "YouTube TV (guide grid, needs selector)"
237
+ }
238
+ };
239
+ /* This mapping associates domain keys with site profiles and provider display names. Most keys are concise second-level domains (e.g., "nbc.com", "foodnetwork.com")
240
+ * matching the output of extractDomain(). Keys can also be full hostnames (e.g., "tv.youtube.com") for subdomain-specific overrides — getDomainConfig() tries the
241
+ * full hostname first, then falls back to the concise domain, so "tv.youtube.com" takes precedence over "youtube.com" when the URL matches.
242
+ *
243
+ * Domains without a profile entry will use DEFAULT_SITE_PROFILE, which works for most standard video players. Domains without a provider entry will display the
244
+ * concise domain string (e.g., "hulu.com") in the UI.
245
+ */
246
+ export const DOMAIN_CONFIG = {
247
+ "abc.com": { profile: "keyboardMultiVideo", provider: "ABC.com" },
248
+ "aetv.com": { profile: "fullscreenApi", provider: "A&E" },
249
+ "bet.com": { profile: "fullscreenApi", provider: "BET.com" },
250
+ "c-span.org": { profile: "brightcove", provider: "C-SPAN.org" },
251
+ "cbs.com": { profile: "keyboardIframe", provider: "CBS.com" },
252
+ "cnbc.com": { profile: "fullscreenApi", provider: "CNBC.com" },
253
+ "cnn.com": { profile: "fullscreenApi", provider: "CNN.com" },
254
+ "disneynow.com": { profile: "disneyNow", provider: "DisneyNOW" },
255
+ "disneyplus.com": { profile: "apiMultiVideo", provider: "Disney+ (Grid)" },
256
+ "espn.com": { profile: "keyboardMultiVideo", provider: "ESPN.com" },
257
+ "foodnetwork.com": { profile: "fullscreenApi", provider: "Food Network" },
258
+ "foxbusiness.com": { profile: "embeddedDynamicMultiVideo", provider: "Fox Business" },
259
+ "foxnews.com": { profile: "embeddedDynamicMultiVideo", provider: "Fox News" },
260
+ "foxsports.com": { profile: "fullscreenApi", provider: "Fox Sports" },
261
+ "france24.com": { profile: "embeddedVolumeLock", provider: "France 24" },
262
+ "fyi.tv": { profile: "fullscreenApi", provider: "FYI" },
263
+ "golfchannel.com": { profile: "fullscreenApi", provider: "Golf Channel" },
264
+ "hbomax.com": { profile: "hboMax", provider: "HBO Max" },
265
+ "history.com": { profile: "fullscreenApi", provider: "History.com" },
266
+ "hulu.com": { profile: "huluLive", provider: "Hulu (Grid)" },
267
+ "lakeshorepbs.org": { profile: "embeddedPlayer", provider: "Lakeshore PBS" },
268
+ "ms.now": { profile: "keyboardDynamic", provider: "MSNOW" },
269
+ "mylifetime.com": { profile: "fullscreenApi", provider: "Lifetime" },
270
+ "nationalgeographic.com": { profile: "keyboardDynamicMultiVideo", provider: "Nat Geo" },
271
+ "nba.com": { profile: "fullscreenApi", provider: "NBA.com" },
272
+ "nbc.com": { maxContinuousPlayback: 4, profile: "keyboardDynamic", provider: "NBC.com" },
273
+ "paramountplus.com": { profile: "fullscreenApi", provider: "Paramount+" },
274
+ "sling.com": { profile: "embeddedVolumeLock", provider: "Sling TV" },
275
+ "tbs.com": { profile: "fullscreenApi", provider: "TBS.com" },
276
+ "tntdrama.com": { profile: "fullscreenApi", provider: "TNT" },
277
+ "trutv.com": { profile: "fullscreenApi", provider: "truTV" },
278
+ "tv.youtube.com": { profile: "youtubeTV", provider: "YouTube TV" },
279
+ "usanetwork.com": { profile: "keyboardDynamicMultiVideo", provider: "USA Network (Grid)" },
280
+ "vh1.com": { profile: "fullscreenApi", provider: "VH1.com" },
281
+ "watchhallmarktv.com": { profile: "fullscreenApi", provider: "Hallmark" },
282
+ "weatherscan.net": { profile: "staticPage", provider: "Weatherscan" },
283
+ "windy.com": { profile: "staticPage", provider: "Windy" },
284
+ "wttw.com": { profile: "fullscreenApi", provider: "WTTW" },
285
+ "youtube.com": { profile: "keyboardDynamic", provider: "YouTube" }
286
+ };
287
+ /**
288
+ * Resolves a URL to its DOMAIN_CONFIG entry by trying the full hostname first for subdomain-specific overrides, then falling back to the concise domain (last two
289
+ * hostname parts). This allows entries like "tv.youtube.com" to override the base "youtube.com" entry when the URL matches the more-specific subdomain.
290
+ * @param url - The URL to resolve a domain configuration for.
291
+ * @returns The matching DomainConfig entry, or undefined if no match is found.
292
+ */
293
+ export function getDomainConfig(url) {
294
+ try {
295
+ const hostname = new URL(url).hostname;
296
+ // Try the full hostname first for subdomain-specific overrides (e.g., "tv.youtube.com" before "youtube.com").
297
+ const hostnameMatch = DOMAIN_CONFIG[hostname];
298
+ if (hostnameMatch) {
299
+ return hostnameMatch;
300
+ }
301
+ }
302
+ catch {
303
+ // Invalid URL — fall through to concise domain lookup.
304
+ }
305
+ return DOMAIN_CONFIG[extractDomain(url)];
306
+ }
307
+ /* The default profile provides baseline behavior for sites not explicitly listed in the domain mapping or channel definitions. These settings work for most
308
+ * standard HTML5 video players that follow common conventions. Each flag is explicitly set to its default value for documentation purposes and to ensure
309
+ * predictable behavior - we don't rely on implicit defaults.
310
+ *
311
+ * Sites matching the default profile:
312
+ * - Use standard HTML5 video without iframe embedding
313
+ * - Have a single video element on the page
314
+ * - Don't require clicking to start playback
315
+ * - Don't auto-mute aggressively
316
+ * - Don't require waiting for network activity
317
+ * - Have video content (not static pages)
318
+ *
319
+ * Neither keyboard fullscreen nor API fullscreen is enabled by default because many sites work fine without explicit fullscreen triggering - the video is already
320
+ * displayed at full size in the viewport. Fullscreen is only needed when the player has visible controls or surrounding content that we want to hide.
321
+ */
322
+ export const DEFAULT_SITE_PROFILE = {
323
+ // No channel selection - single-channel sites don't need it.
324
+ channelSelection: { strategy: "none" },
325
+ // No channel selector - this is only used for multi-channel player pages.
326
+ channelSelector: null,
327
+ // No click selector - when clickToPlay is true, click the video element by default.
328
+ clickSelector: null,
329
+ // Don't click to play - most sites start automatically or via other mechanisms.
330
+ clickToPlay: false,
331
+ // No fullscreen key - many players work without explicit fullscreen.
332
+ fullscreenKey: null,
333
+ // No fullscreen button selector - most sites don't have a dedicated fullscreen button we need to click.
334
+ fullscreenSelector: null,
335
+ // Don't lock volume properties - most sites don't aggressively mute.
336
+ lockVolumeProperties: false,
337
+ // No continuous playback limit - most sites allow indefinite streaming.
338
+ maxContinuousPlayback: null,
339
+ // Don't search iframes - assume video is in main page DOM.
340
+ needsIframeHandling: false,
341
+ // Expect video content - wait for video element.
342
+ noVideo: false,
343
+ // Use first video element - assume only one video exists.
344
+ selectReadyVideo: false,
345
+ // Don't use requestFullscreen() API.
346
+ useRequestFullscreen: false,
347
+ // Don't wait for network idle - assume player is ready on page load.
348
+ waitForNetworkIdle: false
349
+ };
350
+ //# sourceMappingURL=sites.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sites.js","sourceRoot":"","sources":["../../src/config/sites.ts"],"names":[],"mappings":"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"}
@@ -14,6 +14,7 @@ export interface UserChannelsLoadResult {
14
14
  channels: UserChannelMap;
15
15
  parseError: boolean;
16
16
  parseErrorMessage?: string;
17
+ providerSelections: Record<string, string>;
17
18
  }
18
19
  /**
19
20
  * Returns the path to the user channels file.
@@ -32,12 +33,13 @@ export declare function hasChannelsParseError(): boolean;
32
33
  export declare function getChannelsParseErrorMessage(): string | undefined;
33
34
  /**
34
35
  * Loads user channels from the channels file. Returns an empty map if the file doesn't exist, and sets parseError if the file exists but contains invalid JSON.
35
- * @returns The loaded channels with parse status.
36
+ * The file can contain a special `providerSelections` key with user's provider preferences, which is extracted separately from channels.
37
+ * @returns The loaded channels with parse status and provider selections.
36
38
  */
37
39
  export declare function loadUserChannels(): Promise<UserChannelsLoadResult>;
38
40
  /**
39
41
  * Saves user channels to the channels file and updates the in-memory cache. Changes take effect immediately for new stream requests without requiring a server
40
- * restart. Creates the data directory if it doesn't exist.
42
+ * restart. Creates the data directory if it doesn't exist. Provider selections are also saved if any exist.
41
43
  * @param channels - The channels to save.
42
44
  * @throws If the file cannot be written.
43
45
  */
@@ -54,7 +56,7 @@ export declare function deleteUserChannel(key: string): Promise<void>;
54
56
  */
55
57
  export declare function resetUserChannels(): Promise<void>;
56
58
  /**
57
- * Initializes user channels by loading them from the file. This should be called once at server startup.
59
+ * Initializes user channels by loading them from the file. This should be called once at server startup. Also builds provider groups and loads provider selections.
58
60
  */
59
61
  export declare function initializeUserChannels(): Promise<void>;
60
62
  /**
@@ -68,6 +70,11 @@ export declare function initializeUserChannels(): Promise<void>;
68
70
  *
69
71
  * The enabled field reflects whether the channel is available for streaming. Predefined-only channels can be disabled via configuration; user and override
70
72
  * channels are always enabled.
73
+ *
74
+ * Provider variants (non-canonical keys in provider groups) are filtered out from this listing — they are accessed via the provider selection mechanism instead.
75
+ *
76
+ * IMPORTANT: This function preserves object references from PREDEFINED_CHANNELS and loadedUserChannels. The provider system (providers.ts) relies on this behavior
77
+ * to detect user overrides via reference comparison. Do not clone channel objects when building the listing.
71
78
  * @returns Sorted array of channel listing entries.
72
79
  */
73
80
  export declare function getChannelListing(): ChannelListingEntry[];
@@ -164,3 +171,8 @@ export interface ChannelsValidationResult {
164
171
  * @returns Validation result with errors if invalid.
165
172
  */
166
173
  export declare function validateImportedChannels(data: unknown, validProfiles: string[]): ChannelsValidationResult;
174
+ /**
175
+ * Saves the current provider selections to the channels file. This triggers a full file save including all user channels.
176
+ * @throws If the file cannot be written.
177
+ */
178
+ export declare function saveProviderSelections(): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { buildProviderGroups, getProviderSelections, isProviderVariant, setProviderSelections } from "./providers.js";
1
2
  import { CONFIG } from "./index.js";
2
3
  import { LOG } from "../utils/index.js";
3
4
  import { PREDEFINED_CHANNELS } from "../channels/index.js";
@@ -5,10 +6,7 @@ import fs from "node:fs";
5
6
  import os from "node:os";
6
7
  import path from "node:path";
7
8
  const { promises: fsPromises } = fs;
8
- /*
9
- * CHANNELS FILE PATH
10
- *
11
- * The channels file is stored in the same data directory as the config file (~/.prismcast).
9
+ /* The channels file is stored in the same data directory as the config file (~/.prismcast).
12
10
  */
13
11
  const dataDir = path.join(os.homedir(), ".prismcast");
14
12
  const channelsFilePath = path.join(dataDir, "channels.json");
@@ -19,10 +17,7 @@ const channelsFilePath = path.join(dataDir, "channels.json");
19
17
  export function getUserChannelsFilePath() {
20
18
  return channelsFilePath;
21
19
  }
22
- /*
23
- * CHANNELS FILE OPERATIONS
24
- *
25
- * These functions handle reading and writing the channels file. All operations are async and handle errors gracefully.
20
+ /* These functions handle reading and writing the channels file. All operations are async and handle errors gracefully.
26
21
  */
27
22
  // Module-level storage for loaded user channels. This is populated at startup and used by getAllChannels().
28
23
  let loadedUserChannels = {};
@@ -44,34 +39,54 @@ export function getChannelsParseErrorMessage() {
44
39
  }
45
40
  /**
46
41
  * Loads user channels from the channels file. Returns an empty map if the file doesn't exist, and sets parseError if the file exists but contains invalid JSON.
47
- * @returns The loaded channels with parse status.
42
+ * The file can contain a special `providerSelections` key with user's provider preferences, which is extracted separately from channels.
43
+ * @returns The loaded channels with parse status and provider selections.
48
44
  */
49
45
  export async function loadUserChannels() {
50
46
  try {
51
47
  const content = await fsPromises.readFile(channelsFilePath, "utf-8");
52
48
  try {
53
- const channels = JSON.parse(content);
54
- return { channels, parseError: false };
49
+ const parsed = JSON.parse(content);
50
+ // Extract providerSelections if present — it's not a channel, it's metadata.
51
+ const providerSelections = {};
52
+ const channels = {};
53
+ for (const [key, value] of Object.entries(parsed)) {
54
+ if (key === "providerSelections") {
55
+ // Copy provider selections if it's an object.
56
+ if ((typeof value === "object") && (value !== null) && !Array.isArray(value)) {
57
+ for (const [selKey, selValue] of Object.entries(value)) {
58
+ if (typeof selValue === "string") {
59
+ providerSelections[selKey] = selValue;
60
+ }
61
+ }
62
+ }
63
+ }
64
+ else if ((typeof value === "object") && (value !== null) && !Array.isArray(value)) {
65
+ // It's a channel definition.
66
+ channels[key] = value;
67
+ }
68
+ }
69
+ return { channels, parseError: false, providerSelections };
55
70
  }
56
71
  catch (parseError) {
57
72
  const message = (parseError instanceof Error) ? parseError.message : String(parseError);
58
73
  LOG.warn("Invalid JSON in channels file %s: %s. Using predefined channels only.", channelsFilePath, message);
59
- return { channels: {}, parseError: true, parseErrorMessage: message };
74
+ return { channels: {}, parseError: true, parseErrorMessage: message, providerSelections: {} };
60
75
  }
61
76
  }
62
77
  catch (error) {
63
78
  // File doesn't exist - this is normal, use predefined channels only.
64
79
  if (error.code === "ENOENT") {
65
- return { channels: {}, parseError: false };
80
+ return { channels: {}, parseError: false, providerSelections: {} };
66
81
  }
67
82
  // Other read errors - log and use predefined channels.
68
83
  LOG.warn("Failed to read channels file %s: %s. Using predefined channels only.", channelsFilePath, (error instanceof Error) ? error.message : String(error));
69
- return { channels: {}, parseError: false };
84
+ return { channels: {}, parseError: false, providerSelections: {} };
70
85
  }
71
86
  }
72
87
  /**
73
88
  * Saves user channels to the channels file and updates the in-memory cache. Changes take effect immediately for new stream requests without requiring a server
74
- * restart. Creates the data directory if it doesn't exist.
89
+ * restart. Creates the data directory if it doesn't exist. Provider selections are also saved if any exist.
75
90
  * @param channels - The channels to save.
76
91
  * @throws If the file cannot be written.
77
92
  */
@@ -84,11 +99,24 @@ export async function saveUserChannels(channels) {
84
99
  for (const key of sortedKeys) {
85
100
  sortedChannels[key] = channels[key];
86
101
  }
102
+ // Include provider selections if any exist.
103
+ const selections = getProviderSelections();
104
+ if (Object.keys(selections).length > 0) {
105
+ // Sort provider selections for consistent output.
106
+ const sortedSelections = {};
107
+ const selectionKeys = Object.keys(selections).sort();
108
+ for (const key of selectionKeys) {
109
+ sortedSelections[key] = selections[key];
110
+ }
111
+ sortedChannels.providerSelections = sortedSelections;
112
+ }
87
113
  // Write channels with pretty formatting for readability.
88
114
  const content = JSON.stringify(sortedChannels, null, 2);
89
115
  await fsPromises.writeFile(channelsFilePath, content + "\n", "utf-8");
90
116
  // Update in-memory cache so changes take effect immediately for new stream requests.
91
- loadedUserChannels = { ...sortedChannels };
117
+ loadedUserChannels = { ...channels };
118
+ // Refresh provider groups so channelsRef reflects the new channel data. This ensures getResolvedChannel() returns correct data after modifications.
119
+ buildProviderGroups(getMergedChannelMap());
92
120
  // Clear any previous parse error since we're writing valid data.
93
121
  userChannelsParseError = false;
94
122
  userChannelsParseErrorMessage = undefined;
@@ -128,19 +156,21 @@ export async function resetUserChannels() {
128
156
  throw error;
129
157
  }
130
158
  }
131
- /*
132
- * CHANNEL INITIALIZATION
133
- *
134
- * User channels are loaded at server startup and stored in module-level state. This avoids repeated file reads during request handling.
159
+ /* User channels are loaded at server startup and stored in module-level state. This avoids repeated file reads during request handling.
135
160
  */
136
161
  /**
137
- * Initializes user channels by loading them from the file. This should be called once at server startup.
162
+ * Initializes user channels by loading them from the file. This should be called once at server startup. Also builds provider groups and loads provider selections.
138
163
  */
139
164
  export async function initializeUserChannels() {
140
165
  const result = await loadUserChannels();
141
166
  loadedUserChannels = result.channels;
142
167
  userChannelsParseError = result.parseError;
143
168
  userChannelsParseErrorMessage = result.parseErrorMessage;
169
+ // Load provider selections from the file.
170
+ setProviderSelections(result.providerSelections);
171
+ // Build the merged channels map and then build provider groups.
172
+ const mergedChannels = getMergedChannelMap();
173
+ buildProviderGroups(mergedChannels);
144
174
  const userCount = Object.keys(loadedUserChannels).length;
145
175
  const predefinedCount = Object.keys(PREDEFINED_CHANNELS).length;
146
176
  const totalCount = userCount + predefinedCount;
@@ -151,10 +181,18 @@ export async function initializeUserChannels() {
151
181
  LOG.info("Loaded %d channels.", totalCount);
152
182
  }
153
183
  }
154
- /*
155
- * CHANNEL LISTING AND MERGING
156
- *
157
- * The getChannelListing() function is the single source of truth for merging predefined channels with user channels. It returns enriched entries with source
184
+ /**
185
+ * Returns the merged channel map (predefined + user) without filtering by enabled status or provider variants. Used internally for building provider groups.
186
+ * @returns The complete merged channel map.
187
+ */
188
+ function getMergedChannelMap() {
189
+ const result = { ...PREDEFINED_CHANNELS };
190
+ for (const [key, channel] of Object.entries(loadedUserChannels)) {
191
+ result[key] = channel;
192
+ }
193
+ return result;
194
+ }
195
+ /* The getChannelListing() function is the single source of truth for merging predefined channels with user channels. It returns enriched entries with source
158
196
  * classification and enabled status. All other channel retrieval functions that need merged data build on top of it.
159
197
  */
160
198
  /**
@@ -168,12 +206,21 @@ export async function initializeUserChannels() {
168
206
  *
169
207
  * The enabled field reflects whether the channel is available for streaming. Predefined-only channels can be disabled via configuration; user and override
170
208
  * channels are always enabled.
209
+ *
210
+ * Provider variants (non-canonical keys in provider groups) are filtered out from this listing — they are accessed via the provider selection mechanism instead.
211
+ *
212
+ * IMPORTANT: This function preserves object references from PREDEFINED_CHANNELS and loadedUserChannels. The provider system (providers.ts) relies on this behavior
213
+ * to detect user overrides via reference comparison. Do not clone channel objects when building the listing.
171
214
  * @returns Sorted array of channel listing entries.
172
215
  */
173
216
  export function getChannelListing() {
174
217
  const allKeys = new Set([...Object.keys(PREDEFINED_CHANNELS), ...Object.keys(loadedUserChannels)]);
175
218
  const listing = [];
176
219
  for (const key of allKeys) {
220
+ // Skip provider variants — they're accessed via provider selection, not as separate channels.
221
+ if (isProviderVariant(key)) {
222
+ continue;
223
+ }
177
224
  const isPredefined = key in PREDEFINED_CHANNELS;
178
225
  const isUser = key in loadedUserChannels;
179
226
  // Determine source classification. User channel data takes precedence on key conflicts.
@@ -243,10 +290,7 @@ export function isUserChannel(key) {
243
290
  export function isOverrideChannel(key) {
244
291
  return isPredefinedChannel(key) && isUserChannel(key);
245
292
  }
246
- /*
247
- * DISABLED PREDEFINED CHANNELS
248
- *
249
- * Users can disable predefined channels to exclude them from the playlist and block streaming. Disabled channels appear grayed out in the UI with an option to
293
+ /* Users can disable predefined channels to exclude them from the playlist and block streaming. Disabled channels appear grayed out in the UI with an option to
250
294
  * re-enable. This is useful for users who don't want certain predefined channels cluttering their channel list.
251
295
  */
252
296
  /**
@@ -288,10 +332,7 @@ export function getPredefinedChannels() {
288
332
  export function isChannelAvailable(key) {
289
333
  return key in getAllChannels();
290
334
  }
291
- /*
292
- * CHANNEL VALIDATION
293
- *
294
- * These functions validate channel data before saving.
335
+ /* These functions validate channel data before saving.
295
336
  */
296
337
  /**
297
338
  * Validates a channel key for format and uniqueness.
@@ -470,4 +511,15 @@ export function validateImportedChannels(data, validProfiles) {
470
511
  }
471
512
  return { channels, errors, valid: errors.length === 0 };
472
513
  }
514
+ /* Provider selections are stored in the channels.json file alongside user channels. When a selection changes, we save the entire file (channels + selections)
515
+ * to persist the change.
516
+ */
517
+ /**
518
+ * Saves the current provider selections to the channels file. This triggers a full file save including all user channels.
519
+ * @throws If the file cannot be written.
520
+ */
521
+ export async function saveProviderSelections() {
522
+ // Simply save the user channels — the saveUserChannels function includes provider selections automatically.
523
+ await saveUserChannels(loadedUserChannels);
524
+ }
473
525
  //# sourceMappingURL=userChannels.js.map