prismcast 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/app.d.ts +1 -1
- package/dist/app.js +41 -13
- package/dist/app.js.map +1 -1
- package/dist/browser/cdp.d.ts +4 -6
- package/dist/browser/cdp.js +18 -22
- package/dist/browser/cdp.js.map +1 -1
- package/dist/browser/channelSelection.d.ts +41 -11
- package/dist/browser/channelSelection.js +75 -24
- package/dist/browser/channelSelection.js.map +1 -1
- package/dist/browser/display.d.ts +1 -1
- package/dist/browser/index.d.ts +8 -8
- package/dist/browser/index.js +44 -45
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/login.d.ts +2 -2
- package/dist/browser/login.js +1 -1
- package/dist/browser/manifestInterceptor.d.ts +77 -0
- package/dist/browser/manifestInterceptor.js +323 -0
- package/dist/browser/manifestInterceptor.js.map +1 -0
- package/dist/browser/precaching.d.ts +1 -1
- package/dist/browser/precaching.js +23 -23
- package/dist/browser/precaching.js.map +1 -1
- package/dist/browser/tuning/comcastPolymer.d.ts +1 -1
- package/dist/browser/tuning/cox.d.ts +1 -1
- package/dist/browser/tuning/directv.d.ts +1 -1
- package/dist/browser/tuning/directv.js +65 -69
- package/dist/browser/tuning/directv.js.map +1 -1
- package/dist/browser/tuning/fox.d.ts +1 -1
- package/dist/browser/tuning/fox.js +172 -15
- package/dist/browser/tuning/fox.js.map +1 -1
- package/dist/browser/tuning/hbo.d.ts +1 -1
- package/dist/browser/tuning/hbo.js +7 -7
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/tuning/hulu.d.ts +1 -1
- package/dist/browser/tuning/hulu.js +120 -113
- package/dist/browser/tuning/hulu.js.map +1 -1
- package/dist/browser/tuning/shared.d.ts +3 -3
- package/dist/browser/tuning/shared.js +5 -5
- package/dist/browser/tuning/sling.d.ts +1 -1
- package/dist/browser/tuning/sling.js +30 -30
- package/dist/browser/tuning/sling.js.map +1 -1
- package/dist/browser/tuning/spectrum.d.ts +1 -1
- package/dist/browser/tuning/spectrum.js +21 -22
- package/dist/browser/tuning/spectrum.js.map +1 -1
- package/dist/browser/tuning/thumbnailRow.d.ts +1 -1
- package/dist/browser/tuning/thumbnailRow.js +1 -1
- package/dist/browser/tuning/tileClick.d.ts +1 -1
- package/dist/browser/tuning/tileClick.js +5 -5
- package/dist/browser/tuning/xfinity.d.ts +1 -1
- package/dist/browser/tuning/youtubeTv.d.ts +1 -1
- package/dist/browser/tuning/youtubeTv.js +17 -18
- package/dist/browser/tuning/youtubeTv.js.map +1 -1
- package/dist/browser/video.d.ts +20 -6
- package/dist/browser/video.js +30 -38
- package/dist/browser/video.js.map +1 -1
- package/dist/channels/index.d.ts +1 -2
- package/dist/channels/index.js +475 -442
- package/dist/channels/index.js.map +1 -1
- package/dist/config/channelForm.d.ts +58 -0
- package/dist/config/channelForm.js +129 -0
- package/dist/config/channelForm.js.map +1 -0
- package/dist/config/consistencyProbe.d.ts +7 -0
- package/dist/config/consistencyProbe.js +143 -0
- package/dist/config/consistencyProbe.js.map +1 -0
- package/dist/config/health.d.ts +7 -6
- package/dist/config/health.js +78 -65
- package/dist/config/health.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +21 -14
- package/dist/config/index.js.map +1 -1
- package/dist/config/paths.d.ts +9 -3
- package/dist/config/paths.js +12 -4
- package/dist/config/paths.js.map +1 -1
- package/dist/config/persistence.context.d.ts +8 -0
- package/dist/config/persistence.context.js +30 -0
- package/dist/config/persistence.context.js.map +1 -0
- package/dist/config/persistence.d.ts +157 -0
- package/dist/config/persistence.js +446 -0
- package/dist/config/persistence.js.map +1 -0
- package/dist/config/presets.d.ts +14 -4
- package/dist/config/presets.js +84 -75
- package/dist/config/presets.js.map +1 -1
- package/dist/config/profiles.d.ts +2 -2
- package/dist/config/profiles.js +7 -9
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/servicePacks.d.ts +46 -0
- package/dist/config/{providerPacks.js → servicePacks.js} +50 -53
- package/dist/config/servicePacks.js.map +1 -0
- package/dist/config/services.d.ts +228 -0
- package/dist/config/services.js +772 -0
- package/dist/config/services.js.map +1 -0
- package/dist/config/sites.d.ts +3 -3
- package/dist/config/sites.js +75 -75
- package/dist/config/sites.js.map +1 -1
- package/dist/config/userChannels.d.ts +522 -54
- package/dist/config/userChannels.js +1343 -366
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +108 -13
- package/dist/config/userConfig.js +296 -143
- package/dist/config/userConfig.js.map +1 -1
- package/dist/config/userProfiles.d.ts +34 -16
- package/dist/config/userProfiles.js +189 -100
- package/dist/config/userProfiles.js.map +1 -1
- package/dist/hdhr/channelMap.d.ts +1 -1
- package/dist/hdhr/channelMap.js +10 -16
- package/dist/hdhr/channelMap.js.map +1 -1
- package/dist/hdhr/deviceId.js +10 -9
- package/dist/hdhr/deviceId.js.map +1 -1
- package/dist/hdhr/discover.js +4 -5
- package/dist/hdhr/discover.js.map +1 -1
- package/dist/hdhr/index.js +12 -19
- package/dist/hdhr/index.js.map +1 -1
- package/dist/index.js +12 -12
- package/dist/index.js.map +1 -1
- package/dist/native/codecInference.d.ts +44 -0
- package/dist/native/codecInference.js +327 -0
- package/dist/native/codecInference.js.map +1 -0
- package/dist/native/decrypt.d.ts +1 -1
- package/dist/native/index.d.ts +5 -5
- package/dist/native/index.js +45 -67
- package/dist/native/index.js.map +1 -1
- package/dist/native/probe.d.ts +27 -9
- package/dist/native/probe.js +159 -68
- package/dist/native/probe.js.map +1 -1
- package/dist/native/proxy.d.ts +4 -2
- package/dist/native/proxy.js +56 -53
- package/dist/native/proxy.js.map +1 -1
- package/dist/native/tokenExpiry.d.ts +22 -0
- package/dist/native/tokenExpiry.js +153 -0
- package/dist/native/tokenExpiry.js.map +1 -0
- package/dist/routes/assets.js +3 -3
- package/dist/routes/assets.js.map +1 -1
- package/dist/routes/auth.js +9 -13
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/components.js +2 -2
- package/dist/routes/components.js.map +1 -1
- package/dist/routes/config/channels/endpoints/browse.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/browse.js +195 -0
- package/dist/routes/config/channels/endpoints/browse.js.map +1 -0
- package/dist/routes/config/channels/endpoints/bulk.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/bulk.js +128 -0
- package/dist/routes/config/channels/endpoints/bulk.js.map +1 -0
- package/dist/routes/config/channels/endpoints/crud.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/crud.js +504 -0
- package/dist/routes/config/channels/endpoints/crud.js.map +1 -0
- package/dist/routes/config/channels/endpoints/importExport.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/importExport.js +131 -0
- package/dist/routes/config/channels/endpoints/importExport.js.map +1 -0
- package/dist/routes/config/channels/endpoints/predefined.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/predefined.js +78 -0
- package/dist/routes/config/channels/endpoints/predefined.js.map +1 -0
- package/dist/routes/config/channels/endpoints/prefs.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/prefs.js +58 -0
- package/dist/routes/config/channels/endpoints/prefs.js.map +1 -0
- package/dist/routes/config/channels/endpoints/service.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/service.js +149 -0
- package/dist/routes/config/channels/endpoints/service.js.map +1 -0
- package/dist/routes/config/channels/endpoints/tags.d.ts +6 -0
- package/dist/routes/config/channels/endpoints/tags.js +185 -0
- package/dist/routes/config/channels/endpoints/tags.js.map +1 -0
- package/dist/routes/config/channels/http/handler.d.ts +10 -0
- package/dist/routes/config/channels/http/handler.js +20 -0
- package/dist/routes/config/channels/http/handler.js.map +1 -0
- package/dist/routes/config/channels/http/playlistHint.d.ts +25 -0
- package/dist/routes/config/channels/http/playlistHint.js +40 -0
- package/dist/routes/config/channels/http/playlistHint.js.map +1 -0
- package/dist/routes/config/channels/http/serviceWarning.d.ts +10 -0
- package/dist/routes/config/channels/http/serviceWarning.js +28 -0
- package/dist/routes/config/channels/http/serviceWarning.js.map +1 -0
- package/dist/routes/config/channels/index.d.ts +3 -3
- package/dist/routes/config/channels/index.js +2 -2
- package/dist/routes/config/channels/index.js.map +1 -1
- package/dist/routes/config/channels/{routes.d.ts → setup.d.ts} +1 -1
- package/dist/routes/config/channels/setup.js +23 -0
- package/dist/routes/config/channels/setup.js.map +1 -0
- package/dist/routes/config/channels/table.d.ts +13 -13
- package/dist/routes/config/channels/table.js +291 -184
- package/dist/routes/config/channels/table.js.map +1 -1
- package/dist/routes/config/http/envelope.d.ts +111 -0
- package/dist/routes/config/http/envelope.js +103 -0
- package/dist/routes/config/http/envelope.js.map +1 -0
- package/dist/routes/config/index.d.ts +9 -9
- package/dist/routes/config/index.js +5 -5
- package/dist/routes/config/index.js.map +1 -1
- package/dist/routes/config/{providers.js → services.js} +105 -99
- package/dist/routes/config/services.js.map +1 -0
- package/dist/routes/config/settings.d.ts +1 -1
- package/dist/routes/config/settings.js +85 -60
- package/dist/routes/config/settings.js.map +1 -1
- package/dist/routes/debug.js +17 -18
- package/dist/routes/debug.js.map +1 -1
- package/dist/routes/health.js +1 -1
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/icons.js +2 -2
- package/dist/routes/icons.js.map +1 -1
- package/dist/routes/index.js +2 -2
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/logs.js +23 -34
- package/dist/routes/logs.js.map +1 -1
- package/dist/routes/mpegts.js +1 -1
- package/dist/routes/playlist.d.ts +5 -5
- package/dist/routes/playlist.js +84 -61
- package/dist/routes/playlist.js.map +1 -1
- package/dist/routes/root/content.js +82 -61
- package/dist/routes/root/content.js.map +1 -1
- package/dist/routes/root/index.js +2 -5
- package/dist/routes/root/index.js.map +1 -1
- package/dist/routes/root/scripts/channels.js +538 -581
- package/dist/routes/root/scripts/channels.js.map +1 -1
- package/dist/routes/root/scripts/config.js +868 -1022
- package/dist/routes/root/scripts/config.js.map +1 -1
- package/dist/routes/root/scripts/index.d.ts +4 -4
- package/dist/routes/root/scripts/shared.js +445 -197
- package/dist/routes/root/scripts/shared.js.map +1 -1
- package/dist/routes/root/scripts/status.d.ts +4 -0
- package/dist/routes/root/scripts/status.handlers.d.ts +182 -0
- package/dist/routes/root/scripts/status.handlers.js +713 -0
- package/dist/routes/root/scripts/status.handlers.js.map +1 -0
- package/dist/routes/root/scripts/status.js +85 -436
- package/dist/routes/root/scripts/status.js.map +1 -1
- package/dist/routes/root/styles.js +16 -8
- package/dist/routes/root/styles.js.map +1 -1
- package/dist/routes/services.d.ts +6 -0
- package/dist/routes/{providers.js → services.js} +58 -55
- package/dist/routes/services.js.map +1 -0
- package/dist/routes/sse.d.ts +16 -0
- package/dist/routes/sse.js +29 -0
- package/dist/routes/sse.js.map +1 -0
- package/dist/routes/streams.js +10 -23
- package/dist/routes/streams.js.map +1 -1
- package/dist/routes/ui.d.ts +2 -2
- package/dist/routes/ui.js +2 -0
- package/dist/routes/ui.js.map +1 -1
- package/dist/routes/upgrade.js +6 -7
- package/dist/routes/upgrade.js.map +1 -1
- package/dist/service/commands.context.d.ts +6 -0
- package/dist/service/commands.context.js +47 -0
- package/dist/service/commands.context.js.map +1 -0
- package/dist/service/commands.d.ts +53 -8
- package/dist/service/commands.js +172 -192
- package/dist/service/commands.js.map +1 -1
- package/dist/service/generators.context.d.ts +6 -0
- package/dist/service/generators.context.js +45 -0
- package/dist/service/generators.context.js.map +1 -0
- package/dist/service/generators.d.ts +56 -16
- package/dist/service/generators.js +509 -291
- package/dist/service/generators.js.map +1 -1
- package/dist/service/index.d.ts +2 -2
- package/dist/streaming/clients.d.ts +2 -2
- package/dist/streaming/clients.js +2 -2
- package/dist/streaming/codec.d.ts +2 -2
- package/dist/streaming/codec.js +0 -1
- package/dist/streaming/codec.js.map +1 -1
- package/dist/streaming/fmp4Segmenter.d.ts +2 -2
- package/dist/streaming/fmp4Segmenter.js +12 -12
- package/dist/streaming/hls.d.ts +7 -7
- package/dist/streaming/hls.js +104 -92
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsResume.d.ts +3 -3
- package/dist/streaming/hlsResume.js +6 -6
- package/dist/streaming/hlsResume.js.map +1 -1
- package/dist/streaming/hlsSegments.d.ts +5 -2
- package/dist/streaming/hlsSegments.js +14 -9
- package/dist/streaming/hlsSegments.js.map +1 -1
- package/dist/streaming/monitor.d.ts +12 -12
- package/dist/streaming/monitor.js +98 -66
- package/dist/streaming/monitor.js.map +1 -1
- package/dist/streaming/mp4Parser.d.ts +34 -4
- package/dist/streaming/mp4Parser.js +111 -29
- package/dist/streaming/mp4Parser.js.map +1 -1
- package/dist/streaming/mpegts.d.ts +1 -1
- package/dist/streaming/mpegts.js +13 -10
- package/dist/streaming/mpegts.js.map +1 -1
- package/dist/streaming/playlistBuilder.d.ts +1 -1
- package/dist/streaming/playlistBuilder.js +2 -2
- package/dist/streaming/preroll.d.ts +9 -9
- package/dist/streaming/preroll.js +40 -42
- package/dist/streaming/preroll.js.map +1 -1
- package/dist/streaming/pretune.js +4 -4
- package/dist/streaming/pretune.js.map +1 -1
- package/dist/streaming/recovery.d.ts +3 -3
- package/dist/streaming/recovery.js +4 -4
- package/dist/streaming/registry.d.ts +6 -6
- package/dist/streaming/registry.js +4 -8
- package/dist/streaming/registry.js.map +1 -1
- package/dist/streaming/setup.d.ts +11 -10
- package/dist/streaming/setup.js +120 -71
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.d.ts +9 -3
- package/dist/streaming/showInfo.js +33 -27
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +8 -7
- package/dist/streaming/statusEmitter.js +5 -5
- package/dist/streaming/statusEmitter.js.map +1 -1
- package/dist/types/channels.d.ts +139 -36
- package/dist/types/channels.js +39 -1
- package/dist/types/channels.js.map +1 -1
- package/dist/types/config.d.ts +22 -3
- package/dist/types/index.d.ts +9 -8
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/profiles.d.ts +14 -13
- package/dist/types/selection.d.ts +49 -5
- package/dist/types/shared.d.ts +1 -1
- package/dist/types/streaming.d.ts +2 -2
- package/dist/upgrade/commands.context.d.ts +6 -0
- package/dist/upgrade/commands.context.js +29 -0
- package/dist/upgrade/commands.context.js.map +1 -0
- package/dist/upgrade/commands.d.ts +29 -2
- package/dist/upgrade/commands.js +87 -134
- package/dist/upgrade/commands.js.map +1 -1
- package/dist/upgrade/detection.context.d.ts +6 -0
- package/dist/upgrade/detection.context.js +24 -0
- package/dist/upgrade/detection.context.js.map +1 -0
- package/dist/upgrade/detection.d.ts +140 -10
- package/dist/upgrade/detection.js +153 -66
- package/dist/upgrade/detection.js.map +1 -1
- package/dist/upgrade/index.d.ts +2 -2
- package/dist/utils/chromeFetch.d.ts +1 -1
- package/dist/utils/chromeFetch.js +2 -2
- package/dist/utils/clock.d.ts +14 -0
- package/dist/utils/clock.js +28 -0
- package/dist/utils/clock.js.map +1 -0
- package/dist/utils/debugFilter.d.ts +1 -1
- package/dist/utils/debugFilter.js +8 -6
- package/dist/utils/debugFilter.js.map +1 -1
- package/dist/utils/delay.d.ts +2 -3
- package/dist/utils/delay.js +10 -16
- package/dist/utils/delay.js.map +1 -1
- package/dist/utils/evaluate.js +12 -14
- package/dist/utils/evaluate.js.map +1 -1
- package/dist/utils/ffmpeg.context.d.ts +6 -0
- package/dist/utils/ffmpeg.context.js +31 -0
- package/dist/utils/ffmpeg.context.js.map +1 -0
- package/dist/utils/ffmpeg.d.ts +85 -29
- package/dist/utils/ffmpeg.js +126 -129
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/fileLogger.d.ts +18 -2
- package/dist/utils/fileLogger.js +60 -31
- package/dist/utils/fileLogger.js.map +1 -1
- package/dist/utils/format.d.ts +7 -1
- package/dist/utils/format.js +22 -2
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/html.js +1 -3
- package/dist/utils/html.js.map +1 -1
- package/dist/utils/index.d.ts +21 -20
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.js +31 -45
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/m3u.d.ts +18 -3
- package/dist/utils/m3u.js +28 -14
- package/dist/utils/m3u.js.map +1 -1
- package/dist/utils/memo.d.ts +19 -0
- package/dist/utils/memo.js +53 -0
- package/dist/utils/memo.js.map +1 -0
- package/dist/utils/morganStream.js +4 -4
- package/dist/utils/morganStream.js.map +1 -1
- package/dist/utils/pid.d.ts +2 -2
- package/dist/utils/pid.js +2 -2
- package/dist/utils/platform.d.ts +1 -1
- package/dist/utils/platform.js +3 -2
- package/dist/utils/platform.js.map +1 -1
- package/dist/utils/retry.d.ts +2 -0
- package/dist/utils/retry.js +9 -7
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/sanitize.d.ts +2 -2
- package/dist/utils/sanitize.js +4 -4
- package/dist/utils/streamContext.d.ts +7 -0
- package/dist/utils/streamContext.js +10 -1
- package/dist/utils/streamContext.js.map +1 -1
- package/dist/utils/timing.d.ts +6 -3
- package/dist/utils/timing.js +10 -6
- package/dist/utils/timing.js.map +1 -1
- package/dist/utils/version.d.ts +3 -2
- package/dist/utils/version.js +15 -28
- package/dist/utils/version.js.map +1 -1
- package/package.json +14 -10
- package/dist/config/providerPacks.d.ts +0 -46
- package/dist/config/providerPacks.js.map +0 -1
- package/dist/config/providers.d.ts +0 -175
- package/dist/config/providers.js +0 -691
- package/dist/config/providers.js.map +0 -1
- package/dist/native/intercept.d.ts +0 -35
- package/dist/native/intercept.js +0 -184
- package/dist/native/intercept.js.map +0 -1
- package/dist/routes/config/channels/routes.js +0 -1477
- package/dist/routes/config/channels/routes.js.map +0 -1
- package/dist/routes/config/providers.js.map +0 -1
- package/dist/routes/providers.d.ts +0 -6
- package/dist/routes/providers.js.map +0 -1
- /package/dist/routes/config/{providers.d.ts → services.d.ts} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LOG, delay, evaluateWithAbort, formatError } from "../../utils/index.js";
|
|
2
2
|
import { logAvailableChannels, normalizeChannelName, scrollAndClick } from "./shared.js";
|
|
3
3
|
import { CONFIG } from "../../config/index.js";
|
|
4
|
-
// Unified channel cache for Hulu. Maps normalized channel names to their combined entry. Aliases (e.g., "abc"
|
|
4
|
+
// Unified channel cache for Hulu. Maps normalized channel names to their combined entry. Aliases (e.g., "abc" -> same entry as "wls") share object references for
|
|
5
5
|
// automatic propagation of fresh programs and EAB data. Cleared on browser disconnect via clearHuluCache().
|
|
6
6
|
const huluChannelCache = new Map();
|
|
7
7
|
// Transient staging map for listing API response data. Maps channel UUIDs to their program schedules. Populated from listing API responses (which arrive before
|
|
@@ -48,7 +48,7 @@ function resolveHuluCacheKey(normalizedName) {
|
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
* Looks up a channel in the unified cache by normalized name, with fuzzy fallback via resolveHuluCacheKey. Only used for lookups keyed by channelSelector
|
|
51
|
+
* Looks up a channel in the unified cache by normalized name, with fuzzy fallback via resolveHuluCacheKey. Only used for lookups keyed by channelSelector -
|
|
52
52
|
* internal cache operations that use API-derived names should use huluChannelCache.get() directly.
|
|
53
53
|
* @param normalizedName - The normalized (lowercased, whitespace-collapsed) channel name to look up.
|
|
54
54
|
* @returns The matching cache entry, or null if no match found.
|
|
@@ -152,11 +152,11 @@ async function readRenderedChannels(page) {
|
|
|
152
152
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
153
153
|
const text = srOnly.textContent ?? "";
|
|
154
154
|
const nameMatch = /^(.+?) Details, row/.exec(text);
|
|
155
|
-
if (nameMatch) {
|
|
155
|
+
if (nameMatch?.[1]) {
|
|
156
156
|
displayName = nameMatch[1].trim();
|
|
157
157
|
}
|
|
158
158
|
const rowMatch = /row (\d+) of/.exec(text);
|
|
159
|
-
if (rowMatch) {
|
|
159
|
+
if (rowMatch?.[1]) {
|
|
160
160
|
// Row numbers in sr-only text are 1-based. Convert to 0-based for scroll offset calculation.
|
|
161
161
|
rowNumber = parseInt(rowMatch[1], 10) - 1;
|
|
162
162
|
}
|
|
@@ -171,9 +171,8 @@ async function readRenderedChannels(page) {
|
|
|
171
171
|
}
|
|
172
172
|
// Assign DOM indices and populate the unified channel cache with row numbers and display names.
|
|
173
173
|
const rendered = [];
|
|
174
|
-
for (
|
|
175
|
-
|
|
176
|
-
rendered.push({ displayName: ch.displayName, domIndex: i, name: ch.name, rowNumber: ch.rowNumber });
|
|
174
|
+
for (const [domIndex, ch] of channels.entries()) {
|
|
175
|
+
rendered.push({ displayName: ch.displayName, domIndex, name: ch.name, rowNumber: ch.rowNumber });
|
|
177
176
|
// Cache the row number and display name for future direct-scroll lookups.
|
|
178
177
|
if (ch.rowNumber >= 0) {
|
|
179
178
|
const existing = huluChannelCache.get(ch.name);
|
|
@@ -242,7 +241,7 @@ async function scrollToGuideRow(page, gridDocTop, rowHeight, rowIndex) {
|
|
|
242
241
|
/**
|
|
243
242
|
* Locates the on-now program cell for the channel at the given data-testid name (lowercased, trimmed), scrolls it into view, and returns its center coordinates
|
|
244
243
|
* for a subsequent page.mouse.click(). We return coordinates rather than clicking inside the evaluate because page.mouse.click() generates the full pointer event
|
|
245
|
-
* chain (pointerdown
|
|
244
|
+
* chain (pointerdown -> mousedown -> pointerup -> mouseup -> click) that React's event delegation requires, whereas a bare DOM .click() dispatches only a synthetic
|
|
246
245
|
* click event that may not be processed reliably in a Puppeteer automation context.
|
|
247
246
|
* @param page - The Puppeteer page object.
|
|
248
247
|
* @param targetName - The lowercased, trimmed channel name to match against data-testid.
|
|
@@ -279,9 +278,9 @@ async function locateOnNowCell(page, targetName) {
|
|
|
279
278
|
// pattern is used to identify call signs in the guide grid so position-based inference can find local affiliates that sort by their hidden network name.
|
|
280
279
|
const CALL_SIGN_PATTERN = /^[WK][A-Z]{2,3}$/i;
|
|
281
280
|
// Network names that map to local affiliate call signs in Hulu's guide. The details API returns channel_info.name as the local call sign rather than the
|
|
282
|
-
// network name, so these names won't match any details response entry. Cold direct tuning is skipped for these
|
|
281
|
+
// network name, so these names won't match any details response entry. Cold direct tuning is skipped for these - the guide grid handles them via
|
|
283
282
|
// position-based inference. On the first guide grid tune, the in-page interceptor captures the affiliate's channel_id from the playlist request and caches
|
|
284
|
-
// it under the channelSelector key (e.g., "abc"
|
|
283
|
+
// it under the channelSelector key (e.g., "abc" -> uuid). Subsequent tunes resolve via warm direct tuning.
|
|
285
284
|
const NETWORK_NAMES_WITH_AFFILIATES = new Set(["abc", "cbs", "cw", "fox", "nbc", "pbs"]);
|
|
286
285
|
/**
|
|
287
286
|
* Position-based inference for local affiliates. When binary search returns "missing" (target name sorts between rendered channels but no exact match), this
|
|
@@ -312,30 +311,25 @@ function inferLocalAffiliate(rendered, targetName) {
|
|
|
312
311
|
return null;
|
|
313
312
|
}
|
|
314
313
|
// Find the insertion point: the first anchor whose name sorts after the target.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
insertBeforeIndex = i;
|
|
319
|
-
break;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
// Determine the DOM index range between the two surrounding anchor channels.
|
|
314
|
+
const insertBeforeIndex = anchors.findIndex((anchor) => targetName.localeCompare(anchor.name) < 0);
|
|
315
|
+
// Determine the DOM index range between the two surrounding anchor channels. The anchor indexing is guaranteed in-range by construction of
|
|
316
|
+
// insertBeforeIndex above, but noUncheckedIndexedAccess needs explicit narrowing.
|
|
323
317
|
let lowerDomIndex;
|
|
324
318
|
let upperDomIndex;
|
|
325
319
|
if (insertBeforeIndex === 0) {
|
|
326
320
|
// Target sorts before all anchors. Look for call signs before the first anchor.
|
|
327
321
|
lowerDomIndex = -1;
|
|
328
|
-
upperDomIndex = anchors[0].
|
|
322
|
+
upperDomIndex = anchors[0]?.domIndex ?? rendered.length;
|
|
329
323
|
}
|
|
330
324
|
else if (insertBeforeIndex === -1) {
|
|
331
325
|
// Target sorts after all anchors. Look for call signs after the last anchor.
|
|
332
|
-
lowerDomIndex = anchors
|
|
326
|
+
lowerDomIndex = anchors.at(-1)?.domIndex ?? (rendered.length - 1);
|
|
333
327
|
upperDomIndex = rendered.length;
|
|
334
328
|
}
|
|
335
329
|
else {
|
|
336
330
|
// Target sorts between two anchors.
|
|
337
|
-
lowerDomIndex = anchors[insertBeforeIndex - 1]
|
|
338
|
-
upperDomIndex = anchors[insertBeforeIndex].
|
|
331
|
+
lowerDomIndex = anchors[insertBeforeIndex - 1]?.domIndex ?? -1;
|
|
332
|
+
upperDomIndex = anchors[insertBeforeIndex]?.domIndex ?? rendered.length;
|
|
339
333
|
}
|
|
340
334
|
// Find call sign channels in the DOM range between the two anchors.
|
|
341
335
|
for (const ch of rendered) {
|
|
@@ -388,8 +382,8 @@ async function waitForPlayButton(page, playSelector, timeout) {
|
|
|
388
382
|
}
|
|
389
383
|
/**
|
|
390
384
|
* Clicks the on-now program cell and waits for the play button, retrying on failure. Handles two distinct failure modes: (1) the on-now cell click doesn't
|
|
391
|
-
* register because React hasn't fully hydrated event handlers
|
|
392
|
-
* silently swallowed
|
|
385
|
+
* register because React hasn't fully hydrated event handlers - the play button never appears; (2) the play button appears and is clicked but the click is
|
|
386
|
+
* silently swallowed - playback doesn't start. Both failures are detected by waitForPlayButton and trigger a retry of the full on-now cell and play button
|
|
393
387
|
* sequence after a brief delay.
|
|
394
388
|
* @param page - The Puppeteer page object.
|
|
395
389
|
* @param clickTarget - The lowercased, trimmed channel name to locate in the guide grid.
|
|
@@ -400,7 +394,7 @@ async function waitForPlayButton(page, playSelector, timeout) {
|
|
|
400
394
|
async function clickOnNowCellAndPlay(page, clickTarget, playSelector, channelName) {
|
|
401
395
|
// Maximum number of on-now cell click attempts. The first click may not register if React hasn't finished hydrating the guide's event handlers.
|
|
402
396
|
const MAX_CLICK_ATTEMPTS = 3;
|
|
403
|
-
// Shorter timeout for the play button on non-final attempts. When a click registers, the play button appears in under 10ms
|
|
397
|
+
// Shorter timeout for the play button on non-final attempts. When a click registers, the play button appears in under 10ms - this timeout only determines how
|
|
404
398
|
// quickly we detect a missed click and retry. Keeping it low saves ~2s per failed attempt compared to the previous 3000ms value.
|
|
405
399
|
const RETRY_PLAY_TIMEOUT = 1000;
|
|
406
400
|
// Delay between click retries. Gives React additional time to finish hydrating event handlers.
|
|
@@ -483,7 +477,7 @@ async function clickOnNowCellAndPlay(page, clickTarget, playSelector, channelNam
|
|
|
483
477
|
* self-resolved from in-page API data. Two resolution mechanisms handle different channel types:
|
|
484
478
|
*
|
|
485
479
|
* 1. Server-side injection: finds the currently-airing EAB from the entry's program schedule and calls __prismcastResolveDirectTune to inject both values into the
|
|
486
|
-
* held playlist request. Primary mechanism for local affiliates whose call signs don't match the channelSelector network name
|
|
480
|
+
* held playlist request. Primary mechanism for local affiliates whose call signs don't match the channelSelector network name - the interceptor can't
|
|
487
481
|
* self-resolve by name for these, so external injection is the only path.
|
|
488
482
|
* 2. Self-resolution detection: queries __prismcastIsDirectTuneResolved to check if the interceptor already captured UUID+EAB from the expanded
|
|
489
483
|
* Details+Listing API responses and resolved the playlist autonomously. Primary mechanism for non-affiliate channels on cold cache, where the interceptor's
|
|
@@ -504,7 +498,7 @@ async function tryFastPathTune(page, entry, channelName) {
|
|
|
504
498
|
const currentEab = findCurrentEabFromPrograms(entry.programs);
|
|
505
499
|
if (currentEab) {
|
|
506
500
|
const injected = await page.evaluate((u, e) => {
|
|
507
|
-
const resolver = window
|
|
501
|
+
const resolver = window["__prismcastResolveDirectTune"];
|
|
508
502
|
if (typeof resolver === "function") {
|
|
509
503
|
return resolver(u, e);
|
|
510
504
|
}
|
|
@@ -521,7 +515,7 @@ async function tryFastPathTune(page, entry, channelName) {
|
|
|
521
515
|
// responses triggered by the Channels tab click and resolves the held playlist autonomously, making the guide grid click redundant.
|
|
522
516
|
if (!resolved) {
|
|
523
517
|
const selfResolved = await page.evaluate(() => {
|
|
524
|
-
const checker = window
|
|
518
|
+
const checker = window["__prismcastIsDirectTuneResolved"];
|
|
525
519
|
if (typeof checker === "function") {
|
|
526
520
|
return checker();
|
|
527
521
|
}
|
|
@@ -554,7 +548,7 @@ async function tryFastPathTune(page, entry, channelName) {
|
|
|
554
548
|
*/
|
|
555
549
|
async function releaseHeldPlaylist(page) {
|
|
556
550
|
await page.evaluate(() => {
|
|
557
|
-
const release = window
|
|
551
|
+
const release = window["__prismcastReleasePlaylist"];
|
|
558
552
|
if (typeof release === "function") {
|
|
559
553
|
release();
|
|
560
554
|
}
|
|
@@ -563,13 +557,13 @@ async function releaseHeldPlaylist(page) {
|
|
|
563
557
|
/**
|
|
564
558
|
* Guide grid strategy: finds a channel in a virtualized, alphabetically sorted channel grid by scrolling the page to the target row using binary search, then
|
|
565
559
|
* clicking the on-now program cell to open the playback overlay. This strategy works for sites like Hulu Live TV where the channel guide is rendered as a
|
|
566
|
-
* virtualized list
|
|
560
|
+
* virtualized list - only ~13 of ~124 rows exist in the DOM at any time, positioned absolutely within a tall spacer div. The virtualizer renders rows based on
|
|
567
561
|
* the page scroll position (`document.documentElement.scrollTop`), so we scroll to bring the target channel into the DOM, then interact with it directly.
|
|
568
562
|
*
|
|
569
563
|
* Three mechanisms handle different channel types:
|
|
570
|
-
* 1. Binary search with passive row number caching
|
|
571
|
-
* 2. Position-based inference
|
|
572
|
-
* 3. Linear scan fallback
|
|
564
|
+
* 1. Binary search with passive row number caching - primary mechanism for most channels (~800ms first time, ~200ms on cache hit)
|
|
565
|
+
* 2. Position-based inference - handles local affiliates when searching by network name (e.g., "ABC" finds the local call sign at the right sort position)
|
|
566
|
+
* 3. Linear scan fallback - safety net for raw call sign searches or any channel the binary search cannot find (~2.4 seconds)
|
|
573
567
|
*
|
|
574
568
|
* The selection process:
|
|
575
569
|
* 1. If listSelector is provided, click the tab/button to reveal the channel list (e.g., a "Channels" tab)
|
|
@@ -587,7 +581,7 @@ async function guideGridStrategy(page, profile) {
|
|
|
587
581
|
const { channelSelection, channelSelector: channelName } = profile;
|
|
588
582
|
const { listSelector, playSelector } = channelSelection;
|
|
589
583
|
// Ensure the guide is open and on the correct tab. We wait for the tab button to become VISIBLE (not just present in the DOM) because the guide overlay may exist
|
|
590
|
-
// in the DOM structure while still hidden during page initialization or animation. Clicking a hidden button dispatches a DOM event but has no visual effect
|
|
584
|
+
// in the DOM structure while still hidden during page initialization or animation. Clicking a hidden button dispatches a DOM event but has no visual effect - the
|
|
591
585
|
// guide remains hidden and the virtualizer never populates rows. We use $eval for the click because overlapping elements (spinners, overlays) can intercept
|
|
592
586
|
// Puppeteer's coordinate-based mouse events.
|
|
593
587
|
if (listSelector) {
|
|
@@ -601,7 +595,7 @@ async function guideGridStrategy(page, profile) {
|
|
|
601
595
|
LOG.warn("Could not click channel list selector %s: %s.", listSelector, formatError(error));
|
|
602
596
|
}
|
|
603
597
|
}
|
|
604
|
-
// Wait for channel grid rows to become visible. If rows don't appear within a short initial window, retry the tab click once
|
|
598
|
+
// Wait for channel grid rows to become visible. If rows don't appear within a short initial window, retry the tab click once - the first click may have fired
|
|
605
599
|
// during a transitional state before the guide was fully interactive, or the guide may have been animating open.
|
|
606
600
|
let rowsVisible = false;
|
|
607
601
|
for (let guideAttempt = 0; guideAttempt < 2; guideAttempt++) {
|
|
@@ -685,7 +679,7 @@ async function guideGridStrategy(page, profile) {
|
|
|
685
679
|
break;
|
|
686
680
|
}
|
|
687
681
|
// Determine binary search direction by comparing the target against the first and last rendered non-call-sign channel names. Call sign channels (W*/K*
|
|
688
|
-
// local affiliates) are excluded from direction comparison because they sort by hidden network name, not by their displayed call sign
|
|
682
|
+
// local affiliates) are excluded from direction comparison because they sort by hidden network name, not by their displayed call sign - using them for
|
|
689
683
|
// localeCompare would send the search the wrong way.
|
|
690
684
|
const nonCallSigns = rendered.filter((ch) => !CALL_SIGN_PATTERN.test(ch.name));
|
|
691
685
|
if (nonCallSigns.length === 0) {
|
|
@@ -693,8 +687,8 @@ async function guideGridStrategy(page, profile) {
|
|
|
693
687
|
low = mid + 1;
|
|
694
688
|
continue;
|
|
695
689
|
}
|
|
696
|
-
const first = nonCallSigns[0]
|
|
697
|
-
const last = nonCallSigns
|
|
690
|
+
const first = nonCallSigns[0]?.name ?? "";
|
|
691
|
+
const last = nonCallSigns.at(-1)?.name ?? "";
|
|
698
692
|
if (normalizedName.localeCompare(first) < 0) {
|
|
699
693
|
// Target sorts before the first visible non-call-sign channel. Scroll up (toward lower row indices).
|
|
700
694
|
high = mid - 1;
|
|
@@ -705,7 +699,7 @@ async function guideGridStrategy(page, profile) {
|
|
|
705
699
|
low = mid + 1;
|
|
706
700
|
continue;
|
|
707
701
|
}
|
|
708
|
-
// The target is alphabetically between the first and last rendered channels but was not found by exact data-testid match. This is the "missing" case
|
|
702
|
+
// The target is alphabetically between the first and last rendered channels but was not found by exact data-testid match. This is the "missing" case - the
|
|
709
703
|
// channel may be a local affiliate whose call sign doesn't match the network name we're searching for. Try position-based inference.
|
|
710
704
|
const inferred = inferLocalAffiliate(rendered, normalizedName);
|
|
711
705
|
if (inferred) {
|
|
@@ -751,7 +745,7 @@ async function guideGridStrategy(page, profile) {
|
|
|
751
745
|
if (!found) {
|
|
752
746
|
// Log available channels from the unified cache to help users identify the correct channelSelector value. The cache accumulates all channel names
|
|
753
747
|
// encountered during binary search and linear scan, so it contains most or all channels even though the virtualized grid only renders ~13 at a time.
|
|
754
|
-
const availableChannels = Array.from(huluChannelCache.keys()).
|
|
748
|
+
const availableChannels = Array.from(huluChannelCache.keys()).toSorted();
|
|
755
749
|
if (availableChannels.length > 0) {
|
|
756
750
|
logAvailableChannels({
|
|
757
751
|
availableChannels,
|
|
@@ -767,12 +761,12 @@ async function guideGridStrategy(page, profile) {
|
|
|
767
761
|
// triggers full Details+Listing API responses. For non-affiliates, the in-page interceptor may have already self-resolved from those responses (capturing
|
|
768
762
|
// UUID+EAB and swapping the playlist autonomously). For channels where the interceptor hasn't self-resolved, the unified cache provides UUID+EAB for
|
|
769
763
|
// injection. Either path avoids the redundant on-now cell click. Note: for affiliates, the inference block above may have already called tryFastPathTune with
|
|
770
|
-
// the same entry
|
|
764
|
+
// the same entry - that call is redundant here but harmless (~10ms), and avoiding it with a flag would create a fragile coupling to the inference block.
|
|
771
765
|
const fastPathSuccess = await tryFastPathTune(page, findHuluChannelEntry(normalizedName), channelName);
|
|
772
766
|
if (fastPathSuccess) {
|
|
773
767
|
return { success: true };
|
|
774
768
|
}
|
|
775
|
-
// Release the held playlist before falling through to the click path. Neither tryFastPathTune nor the inference block calls releaseHeldPlaylist
|
|
769
|
+
// Release the held playlist before falling through to the click path. Neither tryFastPathTune nor the inference block calls releaseHeldPlaylist - they either
|
|
776
770
|
// inject successfully (resolving the held playlist directly) or return false without side effects. The release is a no-op if the hold was already resolved.
|
|
777
771
|
await releaseHeldPlaylist(page);
|
|
778
772
|
// Click the on-now program cell and wait for the play button, with click retries to handle React hydration timing.
|
|
@@ -804,7 +798,7 @@ async function guideGridWithRetry(page, profile) {
|
|
|
804
798
|
/**
|
|
805
799
|
* Sets up server-side response interception on the page to capture channel data from Hulu's guide APIs. As the live page loads, Hulu fetches program details from
|
|
806
800
|
* guide.hulu.com/guide/details in batches and program schedules from guide.hulu.com/guide/listing. We intercept both responses to populate the unified channel
|
|
807
|
-
* cache: listing data goes into the staging map (UUID
|
|
801
|
+
* cache: listing data goes into the staging map (UUID -> programs), then details data triggers populateHuluChannelCache which joins name -> UUID with staged
|
|
808
802
|
* programs. Also bridges in-page console signals (HULU-DIAG, HULU-CACHE, HULU-FAIL) to the Node.js LOG. Uses a WeakSet to prevent duplicate listener
|
|
809
803
|
* registration.
|
|
810
804
|
* @param page - The Puppeteer page object.
|
|
@@ -848,7 +842,7 @@ function setupDetailsResponseInterception(page) {
|
|
|
848
842
|
}
|
|
849
843
|
// Direct tune failure: the interceptor's hold period expired without resolving UUID+EAB for the target channel. The 503 response prevents Hulu from playing
|
|
850
844
|
// the wrong channel. The guide grid binary search runs as the fallback. Since all cold tunes go through the guide grid (triggering full API expansion), this
|
|
851
|
-
// path should rarely be reached
|
|
845
|
+
// path should rarely be reached - it provides defense-in-depth against silent false positives.
|
|
852
846
|
if (text.startsWith("[HULU-FAIL]")) {
|
|
853
847
|
LOG.warn(text);
|
|
854
848
|
}
|
|
@@ -858,7 +852,7 @@ function setupDetailsResponseInterception(page) {
|
|
|
858
852
|
if (response.status() !== 200) {
|
|
859
853
|
return;
|
|
860
854
|
}
|
|
861
|
-
// Details API: populate the unified channel cache by joining name
|
|
855
|
+
// Details API: populate the unified channel cache by joining name -> UUID from this response with programs from the listing staging map.
|
|
862
856
|
if (url.includes("guide.hulu.com/guide/details")) {
|
|
863
857
|
void response.json().then((data) => {
|
|
864
858
|
if (!Array.isArray(data.items)) {
|
|
@@ -904,7 +898,7 @@ function setupDetailsResponseInterception(page) {
|
|
|
904
898
|
/**
|
|
905
899
|
* Resolves a direct URL for Hulu channel tuning and installs a fetch interceptor that handles both warm and cold tunes. On warm cache (UUID and EAB known from
|
|
906
900
|
* previous API responses), the interceptor has both values at install time and swaps the first playlist request immediately. On cold cache (no UUID), returns
|
|
907
|
-
* null so the guide grid runs
|
|
901
|
+
* null so the guide grid runs - the Channels tab click triggers full Details+Listing API expansion for all ~123 channels, and the interceptor captures UUID+EAB
|
|
908
902
|
* from those expanded responses to resolve the held playlist. Without the Channels tab click, the initial page load only provides data for ~10 visible channels.
|
|
909
903
|
* On all tunes, the interceptor also expands listing and details API requests to populate the full cache for future warm tunes.
|
|
910
904
|
* @param channelSelector - The channel selector string (e.g., "Fox", "CNN", "ESPN").
|
|
@@ -919,7 +913,7 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
919
913
|
// during both the guide grid flow (cold cache) and the intercepted page load (warm cache).
|
|
920
914
|
setupDetailsResponseInterception(page);
|
|
921
915
|
// Look up the currently-airing EAB for the target channel (if UUID and programs are known). On warm cache (both UUID and EAB available), the interceptor has
|
|
922
|
-
// both at install time and swaps immediately. On cold cache (no UUID), we return null below so the guide grid runs
|
|
916
|
+
// both at install time and swaps immediately. On cold cache (no UUID), we return null below so the guide grid runs - the Channels tab click triggers full API
|
|
923
917
|
// expansion.
|
|
924
918
|
const cachedEab = (cachedEntry?.programs) ? findCurrentEabFromPrograms(cachedEntry.programs) : null;
|
|
925
919
|
const isWarmCache = Boolean(cachedUuid && cachedEab);
|
|
@@ -927,7 +921,7 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
927
921
|
LOG.debug("tuning:hulu", "resolveHuluDirectUrl: warm cache for %s (uuid=%s, eab=%s).", channelSelector, cachedUuid, cachedEab);
|
|
928
922
|
}
|
|
929
923
|
else if (!cachedUuid) {
|
|
930
|
-
// On cold cache, all channels fall through to the guide grid
|
|
924
|
+
// On cold cache, all channels fall through to the guide grid - the Channels tab click triggers full Details+Listing API expansion for all ~123 channels.
|
|
931
925
|
// The affiliate/non-affiliate distinction is diagnostic only (both return null). Affiliates use position-based inference; non-affiliates use exact match.
|
|
932
926
|
if (NETWORK_NAMES_WITH_AFFILIATES.has(normalizedName)) {
|
|
933
927
|
LOG.debug("tuning:hulu", "resolveHuluDirectUrl: cold cache for %s (local affiliate). Falling through to guide grid.", channelSelector);
|
|
@@ -960,7 +954,7 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
960
954
|
/* Install the fetch interceptor before navigation on both warm and cold tunes. On warm tunes, it swaps channel_id and content_eab_id in playlist requests
|
|
961
955
|
* immediately. On cold tunes, the guide grid's Channels tab click triggers full API expansion, and the interceptor holds the playlist request until both
|
|
962
956
|
* UUID and EAB are captured from those responses. On all tunes, it captures listing API responses to build an in-page EAB map and expands subsequent
|
|
963
|
-
* details API requests, populating the UUID cache to ~123 channels on a single page load. The script runs via evaluateOnNewDocument
|
|
957
|
+
* details API requests, populating the UUID cache to ~123 channels on a single page load. The script runs via evaluateOnNewDocument - it executes before
|
|
964
958
|
* any page JavaScript, patching window.fetch so Hulu's module-scoped fetch reference captures the interceptor. Each stream gets its own page via
|
|
965
959
|
* createPageWithCapture(), so there's no persistence concern.
|
|
966
960
|
*/
|
|
@@ -969,7 +963,7 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
969
963
|
// inject UUID+EAB via the fast path (resolving the held playlist) or release the hold before falling through to the click path.
|
|
970
964
|
const attemptDirectTune = true;
|
|
971
965
|
await page.evaluateOnNewDocument((initialUuid, initialEab, cachedUuids, cachedEabs, targetName, holdPlaylist) => {
|
|
972
|
-
// evaluateOnNewDocument runs in every frame, including ad iframes. We only want to intercept fetches in the main frame
|
|
966
|
+
// evaluateOnNewDocument runs in every frame, including ad iframes. We only want to intercept fetches in the main frame - iframes don't make Hulu API
|
|
973
967
|
// calls. The try/catch handles cross-origin iframes where accessing window.top throws a SecurityError.
|
|
974
968
|
try {
|
|
975
969
|
if (window.self !== window.top) {
|
|
@@ -991,24 +985,32 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
991
985
|
// the click-based flow where the playlist should pass through immediately and capture the affiliate UUID via [HULU-CACHE].
|
|
992
986
|
let holdActive = holdPlaylist;
|
|
993
987
|
// Tracks whether the initial page-load playlist request has been seen. When holdActive is false (cold affiliate guide grid click fallback), the first live
|
|
994
|
-
// playlist request carries the previously-playing channel's UUID
|
|
988
|
+
// playlist request carries the previously-playing channel's UUID - not the target's. We skip that one and only capture from subsequent requests, which
|
|
995
989
|
// are triggered by the guide grid's play button click and carry the correct affiliate UUID.
|
|
996
990
|
let initialPlaylistSeen = false;
|
|
997
991
|
// eslint-disable-next-line no-console
|
|
998
992
|
console.log("[HULU-DIAG] Fetch interceptor installed (" + ((uuid && eab) ? "warm" : "cold") + ")." + (uuid ? " uuid=" + uuid + " eab=" + (eab || "pending") : ""));
|
|
999
993
|
// Promise that resolves when both UUID and EAB are available for the playlist swap. On warm cache, resolves immediately (both injected at install time).
|
|
1000
994
|
// On cold cache, resolves when the in-page listing and details API response parsers have captured both values. The playlist handler awaits this Promise
|
|
1001
|
-
// to hold the request until the target channel's data is ready.
|
|
995
|
+
// to hold the request until the target channel's data is ready. We use Promise.withResolvers() (Chrome 119+) for the deferred case so the resolver-extraction
|
|
996
|
+
// pattern matches inPageTimeout below rather than the older "new Promise(executor) + outer-scope variable" pattern.
|
|
1002
997
|
let directTuneResolve = null;
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
998
|
+
let directTunePromise;
|
|
999
|
+
if (uuid && eab) {
|
|
1000
|
+
directTunePromise = Promise.resolve();
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- Standard pattern for signal promises.
|
|
1004
|
+
const deferred = Promise.withResolvers();
|
|
1005
|
+
directTunePromise = deferred.promise;
|
|
1006
|
+
directTuneResolve = deferred.resolve;
|
|
1007
|
+
}
|
|
1006
1008
|
// Tracks whether the direct tune has been successfully resolved (UUID+EAB available and playlist swap in progress). Set by tryResolveDirectTune when both
|
|
1007
1009
|
// values are captured from API responses or injected externally. Warm cache starts resolved. Used by __prismcastIsDirectTuneResolved to let the guide grid
|
|
1008
1010
|
// strategy detect that the interceptor already handled the tune autonomously, avoiding a redundant on-now cell click.
|
|
1009
1011
|
let directTuneResolved = Boolean(uuid && eab);
|
|
1010
1012
|
// Checks whether both UUID and EAB are now known and resolves directTunePromise if so. Called after each successful capture from listing or details API
|
|
1011
|
-
// responses. Order-independent
|
|
1013
|
+
// responses. Order-independent - handles both "listing first, details second" and "details first, listing second" sequences.
|
|
1012
1014
|
function tryResolveDirectTune() {
|
|
1013
1015
|
if (uuid && eab && directTuneResolve) {
|
|
1014
1016
|
directTuneResolved = true;
|
|
@@ -1016,11 +1018,19 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
1016
1018
|
directTuneResolve = null;
|
|
1017
1019
|
}
|
|
1018
1020
|
}
|
|
1021
|
+
// Returns a Promise that resolves after the given delay. Used as the timeout side of Promise.race against a deferred Promise. Defined in-page because
|
|
1022
|
+
// node:timers/promises is not reachable from browser context, but Promise.withResolvers (Chrome 119+) is.
|
|
1023
|
+
async function inPageTimeout(ms) {
|
|
1024
|
+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- Standard pattern for signal promises.
|
|
1025
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
1026
|
+
setTimeout(resolve, ms);
|
|
1027
|
+
return promise;
|
|
1028
|
+
}
|
|
1019
1029
|
// Injection endpoint for the guide grid strategy's fast-path tune. After binary search identifies the target channel and the unified cache provides the
|
|
1020
1030
|
// UUID and EAB, the strategy calls this function via page.evaluate to feed both values into the interceptor. The held playlist request then resumes with the
|
|
1021
1031
|
// swapped channel_id and content_eab_id. Returns true if the injection was accepted (directTunePromise not yet resolved), false if the Promise already
|
|
1022
1032
|
// resolved (self-resolution from API data, 8s timeout, or a previous injection).
|
|
1023
|
-
window
|
|
1033
|
+
window["__prismcastResolveDirectTune"] = (u, e) => {
|
|
1024
1034
|
if (!directTuneResolve) {
|
|
1025
1035
|
return false;
|
|
1026
1036
|
}
|
|
@@ -1032,7 +1042,7 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
1032
1042
|
// Release endpoint for the guide grid strategy's click-path fallback. When the fast-path injection can't proceed (UUID or EAB not in unified cache),
|
|
1033
1043
|
// the strategy calls this to unblock the held playlist request and revert to the click-based flow. Sets holdActive to false so the playlist handler follows
|
|
1034
1044
|
// the affiliate capture path ([HULU-CACHE]) on subsequent requests from the play button click.
|
|
1035
|
-
window
|
|
1045
|
+
window["__prismcastReleasePlaylist"] = () => {
|
|
1036
1046
|
holdActive = false;
|
|
1037
1047
|
if (directTuneResolve) {
|
|
1038
1048
|
directTuneResolve();
|
|
@@ -1040,39 +1050,38 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
1040
1050
|
}
|
|
1041
1051
|
};
|
|
1042
1052
|
// Query endpoint for the guide grid strategy to check if the interceptor has already resolved the direct tune. Returns true if the playlist was swapped
|
|
1043
|
-
// (self-resolution from API data, external injection, or warm cache). The guide grid checks this after finding the target channel
|
|
1053
|
+
// (self-resolution from API data, external injection, or warm cache). The guide grid checks this after finding the target channel - if the interceptor
|
|
1044
1054
|
// already handled the tune, the on-now cell click is redundant and can be skipped.
|
|
1045
|
-
window
|
|
1055
|
+
window["__prismcastIsDirectTuneResolved"] = () => directTuneResolved;
|
|
1046
1056
|
// In-page EAB map built dynamically from listing API responses. On the first cold tune, cachedEabs is empty (no pre-computed data from Node.js), so this
|
|
1047
1057
|
// map is the sole source of expansion data for details API requests. Populated asynchronously when the first listing response arrives.
|
|
1048
1058
|
const capturedCurrentEabs = new Map();
|
|
1049
1059
|
// Deferred Promise that resolves when captureListingData finishes parsing the first listing response. Details API requests await this Promise (with a 2s
|
|
1050
1060
|
// timeout) before expanding, so even the very first details request gets expanded with listing-derived EABs. The listing response typically arrives
|
|
1051
|
-
// ~200-600ms after the request fires, adding minimal latency to the details response
|
|
1061
|
+
// ~200-600ms after the request fires, adding minimal latency to the details response - and the details API is not on the critical path for the channel
|
|
1052
1062
|
// grid that binary search needs (it only provides program info for the mini-guide overlay).
|
|
1053
|
-
|
|
1054
|
-
const listingCapturedPromise =
|
|
1055
|
-
|
|
1056
|
-
});
|
|
1063
|
+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- Standard pattern for signal promises.
|
|
1064
|
+
const { promise: listingCapturedPromise, resolve: signalListingCaptured } = Promise.withResolvers();
|
|
1065
|
+
let listingCaptureSignaled = false;
|
|
1057
1066
|
// Fire-and-forget: parses a listing API response to build the in-page EAB map. For each channel, finds the currently-airing program by comparing airing
|
|
1058
1067
|
// times against the current time, mirroring the server-side findCurrentEabFromPrograms() logic. Called on all listing return paths (expanded and passthrough).
|
|
1059
1068
|
function captureListingData(response) {
|
|
1060
1069
|
try {
|
|
1061
1070
|
void response.clone().json().then((data) => {
|
|
1062
|
-
const channels = data
|
|
1071
|
+
const channels = data["channels"];
|
|
1063
1072
|
if (!Array.isArray(channels)) {
|
|
1064
1073
|
return;
|
|
1065
1074
|
}
|
|
1066
1075
|
const now = Date.now();
|
|
1067
1076
|
let captured = 0;
|
|
1068
1077
|
for (const channel of channels) {
|
|
1069
|
-
if ((typeof channel
|
|
1078
|
+
if ((typeof channel["id"] !== "string") || !Array.isArray(channel["programs"])) {
|
|
1070
1079
|
continue;
|
|
1071
1080
|
}
|
|
1072
|
-
for (const program of channel
|
|
1073
|
-
if ((typeof program
|
|
1074
|
-
if ((now >= new Date(program
|
|
1075
|
-
capturedCurrentEabs.set(channel
|
|
1081
|
+
for (const program of channel["programs"]) {
|
|
1082
|
+
if ((typeof program["eab"] === "string") && (typeof program["airingStart"] === "string") && (typeof program["airingEnd"] === "string")) {
|
|
1083
|
+
if ((now >= new Date(program["airingStart"]).getTime()) && (now < new Date(program["airingEnd"]).getTime())) {
|
|
1084
|
+
capturedCurrentEabs.set(channel["id"], program["eab"]);
|
|
1076
1085
|
captured++;
|
|
1077
1086
|
break;
|
|
1078
1087
|
}
|
|
@@ -1084,7 +1093,7 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
1084
1093
|
console.log("[HULU-DIAG] Listing response captured: " + String(captured) + " current EABs for details expansion.");
|
|
1085
1094
|
}
|
|
1086
1095
|
// If the target UUID is already known (from a details response that arrived first), look up its EAB in the freshly captured listing data. This
|
|
1087
|
-
// handles the "details first, listing second" ordering
|
|
1096
|
+
// handles the "details first, listing second" ordering - the details parser set uuid but couldn't find the EAB yet.
|
|
1088
1097
|
if (uuid && !eab) {
|
|
1089
1098
|
const capturedEab = capturedCurrentEabs.get(uuid);
|
|
1090
1099
|
if (capturedEab) {
|
|
@@ -1093,18 +1102,18 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
1093
1102
|
}
|
|
1094
1103
|
}
|
|
1095
1104
|
// Signal that listing data is available. Any details request awaiting listingCapturedPromise will now proceed with expansion.
|
|
1096
|
-
if (
|
|
1097
|
-
|
|
1098
|
-
|
|
1105
|
+
if (!listingCaptureSignaled) {
|
|
1106
|
+
signalListingCaptured();
|
|
1107
|
+
listingCaptureSignaled = true;
|
|
1099
1108
|
}
|
|
1100
1109
|
}).catch(() => { });
|
|
1101
1110
|
}
|
|
1102
1111
|
catch {
|
|
1103
|
-
// Response doesn't support clone() or json()
|
|
1112
|
+
// Response doesn't support clone() or json() - silently skip capture.
|
|
1104
1113
|
}
|
|
1105
1114
|
}
|
|
1106
1115
|
// Fire-and-forget: parses a details API response to extract the target channel's UUID for cold direct tune. On warm cache (uuid already set at install
|
|
1107
|
-
// time), this is a no-op
|
|
1116
|
+
// time), this is a no-op - the UUID is already available. On cold cache, this is the primary mechanism for discovering the target's UUID: the expanded
|
|
1108
1117
|
// details response contains channel_info for all ~123 channels, and we match by normalized name. After finding the UUID, looks up the EAB in the captured
|
|
1109
1118
|
// listing data and calls tryResolveDirectTune to release the held playlist request.
|
|
1110
1119
|
function captureDetailsData(response) {
|
|
@@ -1113,16 +1122,16 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
1113
1122
|
}
|
|
1114
1123
|
try {
|
|
1115
1124
|
void response.clone().json().then((data) => {
|
|
1116
|
-
const items = data
|
|
1125
|
+
const items = data["items"];
|
|
1117
1126
|
if (!Array.isArray(items)) {
|
|
1118
1127
|
return;
|
|
1119
1128
|
}
|
|
1120
1129
|
for (const item of items) {
|
|
1121
|
-
const info = item
|
|
1122
|
-
if (info && (typeof info
|
|
1123
|
-
const name = info
|
|
1130
|
+
const info = item["channel_info"];
|
|
1131
|
+
if (info && (typeof info["name"] === "string") && (typeof info["id"] === "string")) {
|
|
1132
|
+
const name = info["name"].trim().toLowerCase().replace(/\s+/g, " ");
|
|
1124
1133
|
if (name === targetName) {
|
|
1125
|
-
uuid = info
|
|
1134
|
+
uuid = info["id"];
|
|
1126
1135
|
// eslint-disable-next-line no-console
|
|
1127
1136
|
console.log("[HULU-DIAG] Details response: found UUID " + uuid + " for " + targetName + ".");
|
|
1128
1137
|
// Look up the EAB for this UUID in already-captured listing data. If the listing response arrived before this details response, the EAB is
|
|
@@ -1139,7 +1148,7 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
1139
1148
|
}).catch(() => { });
|
|
1140
1149
|
}
|
|
1141
1150
|
catch {
|
|
1142
|
-
// Response doesn't support clone() or json()
|
|
1151
|
+
// Response doesn't support clone() or json() - silently skip capture.
|
|
1143
1152
|
}
|
|
1144
1153
|
}
|
|
1145
1154
|
// Extracts the request body from either the init options or a cloned Request object. Returns null if the body is not a string and the input is not a
|
|
@@ -1154,7 +1163,7 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
1154
1163
|
return null;
|
|
1155
1164
|
}
|
|
1156
1165
|
// Sends a fetch request with a modified body, reconstructing the request from a URL string when the input is a Request object. Using input.url as a
|
|
1157
|
-
// string sidesteps the Fetch spec's body-lock check
|
|
1166
|
+
// string sidesteps the Fetch spec's body-lock check - getBodyText() locked the ReadableStream via input.clone().text(), and the spec checks lock status
|
|
1158
1167
|
// on the first argument BEFORE applying init overrides, causing a TypeError even though we provide a replacement body.
|
|
1159
1168
|
async function fetchWithBody(input, init, body) {
|
|
1160
1169
|
if (input instanceof Request) {
|
|
@@ -1179,19 +1188,19 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
1179
1188
|
if (listingBody) {
|
|
1180
1189
|
try {
|
|
1181
1190
|
const listingObj = JSON.parse(listingBody);
|
|
1182
|
-
if (Array.isArray(listingObj
|
|
1183
|
-
const existing = new Set(listingObj
|
|
1191
|
+
if (Array.isArray(listingObj["channels"])) {
|
|
1192
|
+
const existing = new Set(listingObj["channels"]);
|
|
1184
1193
|
for (const id of cachedUuids) {
|
|
1185
1194
|
existing.add(id);
|
|
1186
1195
|
}
|
|
1187
|
-
listingObj
|
|
1196
|
+
listingObj["channels"] = [...existing];
|
|
1188
1197
|
const listingResponse = await fetchWithBody(input, init, JSON.stringify(listingObj));
|
|
1189
1198
|
captureListingData(listingResponse);
|
|
1190
1199
|
return listingResponse;
|
|
1191
1200
|
}
|
|
1192
1201
|
}
|
|
1193
1202
|
catch {
|
|
1194
|
-
// Body parse error
|
|
1203
|
+
// Body parse error - pass through unmodified.
|
|
1195
1204
|
}
|
|
1196
1205
|
}
|
|
1197
1206
|
const listingPassthrough = await originalFetch(input, init);
|
|
@@ -1201,20 +1210,20 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
1201
1210
|
// Expand details API requests to include EABs for all known channels. The mini-guide details request only covers ~7 visible channels (~19 EABs),
|
|
1202
1211
|
// but we have EABs from two sources: pre-injected cachedEabs from Node.js (available on warm tunes and subsequent cold tunes), and dynamically
|
|
1203
1212
|
// captured capturedCurrentEabs from the listing API response (the primary source on the first cold tune). Merging both ensures the details response
|
|
1204
|
-
// returns channel_info (name
|
|
1213
|
+
// returns channel_info (name->UUID mappings) for all ~123 channels on any tune, even the very first cold tune after a restart.
|
|
1205
1214
|
if (url.includes("guide.hulu.com") && url.includes("/guide/details")) {
|
|
1206
1215
|
// If the in-page EAB map is empty and no pre-injected EABs are available, wait for the listing response to be parsed before proceeding. This holds
|
|
1207
|
-
// the first details request for ~200-600ms until captureListingData resolves the Promise. The 2s timeout is a safety net
|
|
1216
|
+
// the first details request for ~200-600ms until captureListingData resolves the Promise. The 2s timeout is a safety net - if the listing response
|
|
1208
1217
|
// never arrives, the details request proceeds without expansion (same behavior as before).
|
|
1209
1218
|
if ((capturedCurrentEabs.size === 0) && (cachedEabs.length === 0)) {
|
|
1210
|
-
await Promise.race([listingCapturedPromise,
|
|
1219
|
+
await Promise.race([listingCapturedPromise, inPageTimeout(2000)]);
|
|
1211
1220
|
}
|
|
1212
1221
|
const detailsBody = await getBodyText(input, init);
|
|
1213
1222
|
if (detailsBody) {
|
|
1214
1223
|
try {
|
|
1215
1224
|
const detailsObj = JSON.parse(detailsBody);
|
|
1216
|
-
if (Array.isArray(detailsObj
|
|
1217
|
-
const existing = new Set(detailsObj
|
|
1225
|
+
if (Array.isArray(detailsObj["eabs"])) {
|
|
1226
|
+
const existing = new Set(detailsObj["eabs"]);
|
|
1218
1227
|
// Add pre-injected EABs from Node.js cache (available on warm tunes and subsequent cold tunes with partial cache data).
|
|
1219
1228
|
for (const id of cachedEabs) {
|
|
1220
1229
|
existing.add(id);
|
|
@@ -1223,14 +1232,14 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
1223
1232
|
for (const capturedEab of capturedCurrentEabs.values()) {
|
|
1224
1233
|
existing.add(capturedEab);
|
|
1225
1234
|
}
|
|
1226
|
-
detailsObj
|
|
1235
|
+
detailsObj["eabs"] = [...existing];
|
|
1227
1236
|
const expandedResponse = await fetchWithBody(input, init, JSON.stringify(detailsObj));
|
|
1228
1237
|
captureDetailsData(expandedResponse);
|
|
1229
1238
|
return expandedResponse;
|
|
1230
1239
|
}
|
|
1231
1240
|
}
|
|
1232
1241
|
catch {
|
|
1233
|
-
// Body parse error
|
|
1242
|
+
// Body parse error - pass through unmodified.
|
|
1234
1243
|
}
|
|
1235
1244
|
}
|
|
1236
1245
|
const detailsPassthrough = await originalFetch(input, init);
|
|
@@ -1246,7 +1255,7 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
1246
1255
|
// interceptor returns a 503 (safety net). When holdActive is false (set by __prismcastReleasePlaylist after fast-path failure), the playlist passes
|
|
1247
1256
|
// through with affiliate UUID capture for the click fallback.
|
|
1248
1257
|
if (holdActive) {
|
|
1249
|
-
await Promise.race([directTunePromise,
|
|
1258
|
+
await Promise.race([directTunePromise, inPageTimeout(8000)]);
|
|
1250
1259
|
}
|
|
1251
1260
|
if (!uuid || !eab) {
|
|
1252
1261
|
// Affiliate guide grid click fallback: holdActive was set to false by __prismcastReleasePlaylist, or was never enabled. The guide grid handles channel
|
|
@@ -1257,18 +1266,18 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
1257
1266
|
if (affiliateBody) {
|
|
1258
1267
|
try {
|
|
1259
1268
|
const bodyObj = JSON.parse(affiliateBody);
|
|
1260
|
-
if ((bodyObj
|
|
1269
|
+
if ((bodyObj["play_intent"] === "live") && (typeof bodyObj["channel_id"] === "string")) {
|
|
1261
1270
|
if (initialPlaylistSeen) {
|
|
1262
1271
|
// eslint-disable-next-line no-console
|
|
1263
|
-
console.log("[HULU-CACHE] " + targetName + "=" + bodyObj
|
|
1272
|
+
console.log("[HULU-CACHE] " + targetName + "=" + bodyObj["channel_id"]);
|
|
1264
1273
|
}
|
|
1265
1274
|
initialPlaylistSeen = true;
|
|
1266
1275
|
}
|
|
1267
1276
|
}
|
|
1268
1277
|
catch {
|
|
1269
|
-
// Body parse error
|
|
1278
|
+
// Body parse error - skip capture.
|
|
1270
1279
|
}
|
|
1271
|
-
// Must use fetchWithBody
|
|
1280
|
+
// Must use fetchWithBody - getBodyText may have locked the Request's ReadableStream via clone().
|
|
1272
1281
|
return fetchWithBody(input, init, affiliateBody);
|
|
1273
1282
|
}
|
|
1274
1283
|
return originalFetch(input, init);
|
|
@@ -1281,19 +1290,17 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
1281
1290
|
if (bodyText) {
|
|
1282
1291
|
try {
|
|
1283
1292
|
const bodyObj = JSON.parse(bodyText);
|
|
1284
|
-
if (bodyObj
|
|
1285
|
-
const originalChannelId = String(bodyObj
|
|
1286
|
-
|
|
1287
|
-
bodyObj
|
|
1288
|
-
// eslint-disable-next-line camelcase
|
|
1289
|
-
bodyObj.content_eab_id = eab;
|
|
1293
|
+
if (bodyObj["play_intent"] === "live") {
|
|
1294
|
+
const originalChannelId = String(bodyObj["channel_id"]);
|
|
1295
|
+
bodyObj["channel_id"] = uuid;
|
|
1296
|
+
bodyObj["content_eab_id"] = eab;
|
|
1290
1297
|
// eslint-disable-next-line no-console
|
|
1291
1298
|
console.log("[HULU-DIAG] Playlist swapped: channel_id " + originalChannelId + " -> " + uuid + ", eab -> " + eab);
|
|
1292
1299
|
return await fetchWithBody(input, init, JSON.stringify(bodyObj));
|
|
1293
1300
|
}
|
|
1294
1301
|
}
|
|
1295
1302
|
catch {
|
|
1296
|
-
// Body parse error
|
|
1303
|
+
// Body parse error - pass through unmodified.
|
|
1297
1304
|
}
|
|
1298
1305
|
}
|
|
1299
1306
|
}
|
|
@@ -1305,7 +1312,7 @@ async function resolveHuluDirectUrl(channelSelector, page) {
|
|
|
1305
1312
|
LOG.debug("tuning:hulu", "Failed to install Hulu fetch interceptor: %s.", formatError(error));
|
|
1306
1313
|
return null;
|
|
1307
1314
|
}
|
|
1308
|
-
// On cold cache (no UUID), fall through to the guide grid. The initial page load only provides Details API data for ~10 visible channels
|
|
1315
|
+
// On cold cache (no UUID), fall through to the guide grid. The initial page load only provides Details API data for ~10 visible channels - the guide grid's
|
|
1309
1316
|
// Channels tab click triggers full expansion for all ~123 channels, and the interceptor captures UUID+EAB from those expanded responses to resolve the held
|
|
1310
1317
|
// playlist. Affiliates additionally need position-based inference because their channel_info.name uses call signs rather than network names.
|
|
1311
1318
|
if (!cachedUuid) {
|
|
@@ -1341,7 +1348,7 @@ async function discoverHuluChannels(page) {
|
|
|
1341
1348
|
return buildHuluDiscoveredChannels();
|
|
1342
1349
|
}
|
|
1343
1350
|
// Set up response interception BEFORE navigation so we capture the initial details and listing API responses during page load. These responses populate the
|
|
1344
|
-
// unified channel cache with UUID, programs, and display names for all ~130 channels
|
|
1351
|
+
// unified channel cache with UUID, programs, and display names for all ~130 channels - warming the tuning cache as a side effect of discovery. The same
|
|
1345
1352
|
// setupDetailsResponseInterception function used by the tuning path ensures a single code path for all API response processing.
|
|
1346
1353
|
setupDetailsResponseInterception(page);
|
|
1347
1354
|
try {
|
|
@@ -1350,7 +1357,7 @@ async function discoverHuluChannels(page) {
|
|
|
1350
1357
|
catch {
|
|
1351
1358
|
return [];
|
|
1352
1359
|
}
|
|
1353
|
-
// Click the Channels tab to reveal the channel list and trigger full API expansion. Matches the tuning path's retry logic
|
|
1360
|
+
// Click the Channels tab to reveal the channel list and trigger full API expansion. Matches the tuning path's retry logic - if guide rows don't appear after
|
|
1354
1361
|
// the first tab click, retry once with a longer delay in case the first click fired during a transitional state before the guide was fully interactive.
|
|
1355
1362
|
const listSelector = "#CHANNELS";
|
|
1356
1363
|
try {
|
|
@@ -1415,8 +1422,8 @@ async function discoverHuluChannels(page) {
|
|
|
1415
1422
|
// Reassign sequential domIndex values across the full accumulated list so that inferLocalAffiliate's position-based logic works correctly. The original
|
|
1416
1423
|
// domIndex values are from individual scroll windows (0-12) and are not meaningful across the full channel list.
|
|
1417
1424
|
const indexedChannels = allChannels.map((ch, i) => ({ ...ch, domIndex: i }));
|
|
1418
|
-
// Build a callSign
|
|
1419
|
-
// uses during binary search
|
|
1425
|
+
// Build a callSign -> networkName map by reusing inferLocalAffiliate for each broadcast network. This is the same position-based inference the tuning strategy
|
|
1426
|
+
// uses during binary search - a call sign channel occupies the alphabetical position where its network name would sort.
|
|
1420
1427
|
const affiliateMap = new Map();
|
|
1421
1428
|
for (const network of NETWORK_NAMES_WITH_AFFILIATES) {
|
|
1422
1429
|
const callSign = inferLocalAffiliate(indexedChannels, network);
|
|
@@ -1447,7 +1454,7 @@ async function discoverHuluChannels(page) {
|
|
|
1447
1454
|
}
|
|
1448
1455
|
return { channelSelector: ch.displayName, name: ch.displayName };
|
|
1449
1456
|
});
|
|
1450
|
-
// Do not cache empty results
|
|
1457
|
+
// Do not cache empty results - leave the flag false so subsequent calls retry the full walk. Empty results can indicate no Hulu + Live TV subscription.
|
|
1451
1458
|
if (discovered.length > 0) {
|
|
1452
1459
|
discovered.sort((a, b) => a.name.localeCompare(b.name));
|
|
1453
1460
|
huluFullyDiscovered = true;
|