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.
- package/README.md +20 -4
- package/dist/app.js +5 -20
- package/dist/app.js.map +1 -1
- package/dist/browser/cdp.js +1 -4
- package/dist/browser/cdp.js.map +1 -1
- package/dist/browser/channelSelection.d.ts +5 -0
- package/dist/browser/channelSelection.js +803 -53
- package/dist/browser/channelSelection.js.map +1 -1
- package/dist/browser/display.js +1 -4
- package/dist/browser/display.js.map +1 -1
- package/dist/browser/index.js +10 -28
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/video.d.ts +24 -10
- package/dist/browser/video.js +101 -44
- package/dist/browser/video.js.map +1 -1
- package/dist/channels/index.d.ts +1 -17
- package/dist/channels/index.js +246 -137
- package/dist/channels/index.js.map +1 -1
- package/dist/config/index.js +10 -9
- package/dist/config/index.js.map +1 -1
- package/dist/config/presets.js +1 -4
- package/dist/config/presets.js.map +1 -1
- package/dist/config/profiles.d.ts +14 -13
- package/dist/config/profiles.js +47 -287
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/providers.d.ts +75 -0
- package/dist/config/providers.js +283 -0
- package/dist/config/providers.js.map +1 -0
- package/dist/config/sites.d.ts +20 -0
- package/dist/config/sites.js +350 -0
- package/dist/config/sites.js.map +1 -0
- package/dist/config/userChannels.d.ts +15 -3
- package/dist/config/userChannels.js +85 -33
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +1 -0
- package/dist/config/userConfig.js +12 -26
- package/dist/config/userConfig.js.map +1 -1
- package/dist/hdhr/channelMap.js +3 -6
- package/dist/hdhr/channelMap.js.map +1 -1
- package/dist/hdhr/deviceId.js +1 -4
- package/dist/hdhr/deviceId.js.map +1 -1
- package/dist/hdhr/discover.js.map +1 -1
- package/dist/hdhr/index.js +1 -4
- package/dist/hdhr/index.js.map +1 -1
- package/dist/index.js +4 -16
- package/dist/index.js.map +1 -1
- package/dist/routes/assets.js +1 -4
- package/dist/routes/assets.js.map +1 -1
- package/dist/routes/auth.js +4 -2
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/channels.js +2 -2
- package/dist/routes/channels.js.map +1 -1
- package/dist/routes/components.js.map +1 -1
- package/dist/routes/config.js +163 -34
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/health.js +1 -4
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/hls.js +1 -4
- package/dist/routes/hls.js.map +1 -1
- package/dist/routes/index.js +1 -4
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/logs.js +3 -12
- package/dist/routes/logs.js.map +1 -1
- package/dist/routes/mpegts.js +1 -4
- package/dist/routes/mpegts.js.map +1 -1
- package/dist/routes/play.js +1 -4
- package/dist/routes/play.js.map +1 -1
- package/dist/routes/playlist.js +2 -5
- package/dist/routes/playlist.js.map +1 -1
- package/dist/routes/root.js +43 -12
- package/dist/routes/root.js.map +1 -1
- package/dist/routes/streams.js +2 -8
- package/dist/routes/streams.js.map +1 -1
- package/dist/routes/theme.js +1 -4
- package/dist/routes/theme.js.map +1 -1
- package/dist/routes/ui.js +1 -4
- package/dist/routes/ui.js.map +1 -1
- package/dist/service/commands.js +1 -4
- package/dist/service/commands.js.map +1 -1
- package/dist/service/generators.js +4 -16
- package/dist/service/generators.js.map +1 -1
- package/dist/streaming/clients.js.map +1 -1
- package/dist/streaming/fmp4Segmenter.d.ts +1 -0
- package/dist/streaming/fmp4Segmenter.js +7 -12
- package/dist/streaming/fmp4Segmenter.js.map +1 -1
- package/dist/streaming/hls.d.ts +3 -0
- package/dist/streaming/hls.js +39 -21
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsSegments.js +1 -4
- package/dist/streaming/hlsSegments.js.map +1 -1
- package/dist/streaming/lifecycle.js +1 -4
- package/dist/streaming/lifecycle.js.map +1 -1
- package/dist/streaming/monitor.d.ts +1 -0
- package/dist/streaming/monitor.js +296 -76
- package/dist/streaming/monitor.js.map +1 -1
- package/dist/streaming/mp4Parser.js.map +1 -1
- package/dist/streaming/mpegts.js +1 -4
- package/dist/streaming/mpegts.js.map +1 -1
- package/dist/streaming/registry.d.ts +7 -0
- package/dist/streaming/registry.js +10 -0
- package/dist/streaming/registry.js.map +1 -1
- package/dist/streaming/setup.d.ts +4 -1
- package/dist/streaming/setup.js +35 -33
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.js +3 -6
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +2 -0
- package/dist/streaming/statusEmitter.js +2 -4
- package/dist/streaming/statusEmitter.js.map +1 -1
- package/dist/types/index.d.ts +34 -2
- package/dist/utils/errors.js +1 -4
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/evaluate.js +3 -6
- package/dist/utils/evaluate.js.map +1 -1
- package/dist/utils/ffmpeg.js +2 -8
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/fileLogger.js +9 -30
- package/dist/utils/fileLogger.js.map +1 -1
- package/dist/utils/format.d.ts +7 -0
- package/dist/utils/format.js +20 -0
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/html.js +1 -4
- package/dist/utils/html.js.map +1 -1
- package/dist/utils/logEmitter.js +1 -4
- package/dist/utils/logEmitter.js.map +1 -1
- package/dist/utils/logger.js +4 -16
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/m3u.js +1 -4
- package/dist/utils/m3u.js.map +1 -1
- package/dist/utils/morganStream.js +1 -4
- package/dist/utils/morganStream.js.map +1 -1
- package/dist/utils/platform.js.map +1 -1
- package/dist/utils/retry.js +1 -4
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/streamContext.js.map +1 -1
- 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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
54
|
-
|
|
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 = { ...
|
|
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
|
-
*
|
|
156
|
-
*
|
|
157
|
-
|
|
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
|