prismcast 1.10.1 → 1.10.3
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/dist/app.d.ts +5 -3
- package/dist/app.js +48 -43
- package/dist/app.js.map +1 -1
- package/dist/browser/hlsPlaylistObserver.d.ts +38 -0
- package/dist/browser/hlsPlaylistObserver.js +113 -0
- package/dist/browser/hlsPlaylistObserver.js.map +1 -0
- package/dist/browser/index.d.ts +26 -16
- package/dist/browser/index.js +97 -72
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/manifestInterceptor.d.ts +29 -29
- package/dist/browser/manifestInterceptor.js +106 -212
- package/dist/browser/manifestInterceptor.js.map +1 -1
- package/dist/browser/tabNetworkObserver.d.ts +48 -0
- package/dist/browser/tabNetworkObserver.js +140 -0
- package/dist/browser/tabNetworkObserver.js.map +1 -0
- package/dist/browser/tuning/hbo.js +31 -123
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/tuning/hulu.js +70 -7
- package/dist/browser/tuning/hulu.js.map +1 -1
- package/dist/browser/video.js +1 -1
- package/dist/browser/video.js.map +1 -1
- package/dist/config/consistencyProbe.js +13 -11
- package/dist/config/consistencyProbe.js.map +1 -1
- package/dist/config/index.d.ts +6 -2
- package/dist/config/index.js +35 -21
- package/dist/config/index.js.map +1 -1
- package/dist/config/paths.d.ts +29 -8
- package/dist/config/paths.js +54 -10
- package/dist/config/paths.js.map +1 -1
- package/dist/config/presets.js +2 -2
- package/dist/config/presets.js.map +1 -1
- package/dist/config/profiles.d.ts +1 -1
- package/dist/config/profiles.js +12 -12
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/services.d.ts +5 -5
- package/dist/config/services.js +8 -8
- package/dist/config/services.js.map +1 -1
- package/dist/config/sites.d.ts +2 -2
- package/dist/config/sites.js +6 -6
- package/dist/config/sites.js.map +1 -1
- package/dist/config/userChannels.d.ts +108 -14
- package/dist/config/userChannels.js +292 -38
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.js +2 -2
- package/dist/config/userConfig.js.map +1 -1
- package/dist/config/userProfiles.d.ts +3 -3
- package/dist/config/userProfiles.js +19 -19
- package/dist/config/userProfiles.js.map +1 -1
- package/dist/hdhr/discover.js +3 -4
- package/dist/hdhr/discover.js.map +1 -1
- package/dist/identity.d.ts +2 -0
- package/dist/identity.js +19 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.js +7 -9
- package/dist/index.js.map +1 -1
- package/dist/native/index.d.ts +1 -1
- package/dist/native/index.js +9 -18
- package/dist/native/index.js.map +1 -1
- package/dist/native/proxy.d.ts +1 -4
- package/dist/native/proxy.js +0 -9
- package/dist/native/proxy.js.map +1 -1
- package/dist/routes/cdp.d.ts +171 -0
- package/dist/routes/cdp.js +702 -0
- package/dist/routes/cdp.js.map +1 -0
- package/dist/routes/clientActions.d.ts +97 -0
- package/dist/routes/clientActions.js +113 -0
- package/dist/routes/clientActions.js.map +1 -0
- package/dist/routes/components.d.ts +4 -3
- package/dist/routes/components.js +36 -39
- package/dist/routes/components.js.map +1 -1
- package/dist/routes/config/channels/endpoints/browse.js.map +1 -1
- package/dist/routes/config/channels/endpoints/crud.js +37 -72
- package/dist/routes/config/channels/endpoints/crud.js.map +1 -1
- package/dist/routes/config/channels/endpoints/importExport.js.map +1 -1
- package/dist/routes/config/channels/healthBridge.d.ts +23 -0
- package/dist/routes/config/channels/healthBridge.js +71 -0
- package/dist/routes/config/channels/healthBridge.js.map +1 -0
- package/dist/routes/config/channels/table.js +97 -84
- package/dist/routes/config/channels/table.js.map +1 -1
- package/dist/routes/config/services.js +26 -23
- package/dist/routes/config/services.js.map +1 -1
- package/dist/routes/config/settings.js +9 -6
- package/dist/routes/config/settings.js.map +1 -1
- package/dist/routes/debug.js +277 -139
- package/dist/routes/debug.js.map +1 -1
- package/dist/routes/index.js +2 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/logs.js +1 -1
- package/dist/routes/logs.js.map +1 -1
- package/dist/routes/root/content.js +18 -14
- package/dist/routes/root/content.js.map +1 -1
- package/dist/routes/root/index.js +25 -28
- package/dist/routes/root/index.js.map +1 -1
- package/dist/routes/root/scripts/channels.js +50 -21
- package/dist/routes/root/scripts/channels.js.map +1 -1
- package/dist/routes/root/scripts/config.js +72 -3
- package/dist/routes/root/scripts/config.js.map +1 -1
- package/dist/routes/root/scripts/shared.js +60 -0
- package/dist/routes/root/scripts/shared.js.map +1 -1
- package/dist/routes/root/scripts/status.handlers.d.ts +5 -27
- package/dist/routes/root/scripts/status.handlers.js +8 -60
- package/dist/routes/root/scripts/status.handlers.js.map +1 -1
- package/dist/routes/root/scripts/status.js +6 -1
- package/dist/routes/root/scripts/status.js.map +1 -1
- package/dist/routes/streams.js +5 -3
- package/dist/routes/streams.js.map +1 -1
- package/dist/service/commands.context.js +2 -2
- package/dist/service/commands.context.js.map +1 -1
- package/dist/service/commands.d.ts +1 -1
- package/dist/service/commands.js +1 -10
- package/dist/service/commands.js.map +1 -1
- package/dist/service/generators.context.js +2 -1
- package/dist/service/generators.context.js.map +1 -1
- package/dist/service/generators.d.ts +1 -1
- package/dist/service/generators.js +6 -19
- package/dist/service/generators.js.map +1 -1
- package/dist/streaming/clients.js +3 -11
- package/dist/streaming/clients.js.map +1 -1
- package/dist/streaming/fmp4Segmenter.js +1 -1
- package/dist/streaming/fmp4Segmenter.js.map +1 -1
- package/dist/streaming/hls.js +2 -6
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsSegments.d.ts +1 -1
- package/dist/streaming/hlsSegments.js.map +1 -1
- package/dist/streaming/mpegts.js +1 -1
- package/dist/streaming/mpegts.js.map +1 -1
- package/dist/streaming/preroll.d.ts +1 -1
- package/dist/streaming/preroll.js +1 -5
- package/dist/streaming/preroll.js.map +1 -1
- package/dist/streaming/setup.js +6 -6
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.js +2 -4
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +5 -8
- package/dist/streaming/statusEmitter.js +0 -5
- package/dist/streaming/statusEmitter.js.map +1 -1
- package/dist/types/channels.d.ts +9 -0
- package/dist/types/channels.js.map +1 -1
- package/dist/types/profiles.d.ts +2 -2
- package/dist/upgrade/commands.context.js +14 -10
- package/dist/upgrade/commands.context.js.map +1 -1
- package/dist/upgrade/commands.d.ts +7 -15
- package/dist/upgrade/commands.js +27 -28
- package/dist/upgrade/commands.js.map +1 -1
- package/dist/upgrade/detection.context.js +2 -1
- package/dist/upgrade/detection.context.js.map +1 -1
- package/dist/upgrade/detection.d.ts +2 -1
- package/dist/upgrade/detection.js +20 -51
- package/dist/upgrade/detection.js.map +1 -1
- package/dist/upgrade/index.d.ts +1 -0
- package/dist/upgrade/index.js +1 -0
- package/dist/upgrade/index.js.map +1 -1
- package/dist/upgrade/lifecycle.context.d.ts +19 -0
- package/dist/upgrade/lifecycle.context.js +47 -0
- package/dist/upgrade/lifecycle.context.js.map +1 -0
- package/dist/upgrade/lifecycle.d.ts +72 -0
- package/dist/upgrade/lifecycle.js +176 -0
- package/dist/upgrade/lifecycle.js.map +1 -0
- package/dist/upgrade/pathHandle.d.ts +28 -0
- package/dist/upgrade/pathHandle.js +101 -0
- package/dist/upgrade/pathHandle.js.map +1 -0
- package/dist/utils/bootSession.context.d.ts +15 -0
- package/dist/utils/bootSession.context.js +59 -0
- package/dist/utils/bootSession.context.js.map +1 -0
- package/dist/utils/bootSession.d.ts +16 -0
- package/dist/utils/bootSession.js +20 -0
- package/dist/utils/bootSession.js.map +1 -0
- package/dist/utils/clock.js +1 -1
- package/dist/utils/debugFilter.js +1 -0
- package/dist/utils/debugFilter.js.map +1 -1
- package/dist/utils/ffmpeg.js +1 -1
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/fileLogger.js +3 -3
- package/dist/utils/fileLogger.js.map +1 -1
- package/dist/utils/format.js +3 -3
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/index.d.ts +5 -1
- package/dist/utils/index.js +5 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.js +53 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/markup.d.ts +22 -0
- package/dist/utils/markup.js +77 -0
- package/dist/utils/markup.js.map +1 -0
- package/dist/utils/morganStream.js +1 -1
- package/dist/utils/morganStream.js.map +1 -1
- package/dist/utils/network.d.ts +8 -0
- package/dist/utils/network.js +22 -0
- package/dist/utils/network.js.map +1 -0
- package/dist/utils/pid.d.ts +2 -25
- package/dist/utils/pid.js +10 -49
- package/dist/utils/pid.js.map +1 -1
- package/dist/utils/platform.d.ts +0 -27
- package/dist/utils/platform.js +0 -57
- package/dist/utils/platform.js.map +1 -1
- package/dist/utils/processInspector.context.d.ts +7 -0
- package/dist/utils/processInspector.context.js +104 -0
- package/dist/utils/processInspector.context.js.map +1 -0
- package/dist/utils/processInspector.d.ts +54 -0
- package/dist/utils/processInspector.js +125 -0
- package/dist/utils/processInspector.js.map +1 -0
- package/dist/utils/retry.d.ts +1 -1
- package/dist/utils/retry.js +1 -1
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/runtimeIdentity.context.d.ts +6 -0
- package/dist/utils/runtimeIdentity.context.js +13 -0
- package/dist/utils/runtimeIdentity.context.js.map +1 -0
- package/dist/utils/runtimeIdentity.d.ts +96 -0
- package/dist/utils/runtimeIdentity.js +171 -0
- package/dist/utils/runtimeIdentity.js.map +1 -0
- package/dist/utils/timing.d.ts +1 -1
- package/dist/utils/timing.js +0 -5
- package/dist/utils/timing.js.map +1 -1
- package/dist/utils/version.js +40 -10
- package/dist/utils/version.js.map +1 -1
- package/package.json +7 -5
- package/dist/utils/html.d.ts +0 -6
- package/dist/utils/html.js +0 -22
- package/dist/utils/html.js.map +0 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { LOG } from "../utils/index.js";
|
|
2
|
+
// Default target filter: attach to every target except the browser-level one. The browser target emits no Network events relevant to a tab consumer, and
|
|
3
|
+
// attaching to it pollutes the session set. Defining the default at module scope makes it shareable across calls and trivially testable.
|
|
4
|
+
const defaultTargetFilter = (info) => info.type !== "browser";
|
|
5
|
+
/**
|
|
6
|
+
* Installs a tab-wide network observer on the given page. The returned handle delivers every response observed across every target accepted by the targetFilter
|
|
7
|
+
* predicate (top page, iframes including OOPIFs, workers, service workers by default) to the supplied callback. Disposal is the caller's responsibility; the
|
|
8
|
+
* returned handle supports both an explicit dispose() call and the "using" keyword via Symbol.dispose.
|
|
9
|
+
*
|
|
10
|
+
* Returns null when the page is already closed, the root CDP session could not be created, or the puppeteer Connection backing the session is not available
|
|
11
|
+
* (e.g., the session detached during construction).
|
|
12
|
+
*
|
|
13
|
+
* @param page - The puppeteer page to observe.
|
|
14
|
+
* @param options - Observer options including the response callback and optional target-type predicate.
|
|
15
|
+
* @returns The observer handle, or null if installation failed.
|
|
16
|
+
*/
|
|
17
|
+
export async function observeTabResponses(page, options) {
|
|
18
|
+
if (page.isClosed()) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
let rootSession;
|
|
22
|
+
try {
|
|
23
|
+
rootSession = await page.createCDPSession();
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
LOG.debug("browser:tabObserver", "Failed to create root CDP session: %s.", String(error));
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
// Connection lookup. With flatten=true, child sessions multiplex onto the parent connection and are reachable by sessionId via connection.session(). Puppeteer
|
|
30
|
+
// types CDPSession.connection() as `Connection | undefined`; the undefined case indicates the session has been detached or is otherwise unusable. We bail out
|
|
31
|
+
// cleanly rather than risking a TypeError on first child attach.
|
|
32
|
+
const connection = rootSession.connection();
|
|
33
|
+
if (!connection) {
|
|
34
|
+
LOG.debug("browser:tabObserver", "Root CDP session has no connection - aborting observer install.");
|
|
35
|
+
try {
|
|
36
|
+
await rootSession.detach();
|
|
37
|
+
}
|
|
38
|
+
catch (_error) {
|
|
39
|
+
// Cleanup best-effort.
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const targetFilter = options.targetFilter ?? defaultTargetFilter;
|
|
44
|
+
// Track every session we attach to so disposal can tear them all down deterministically. The root session is included so it is cleaned up alongside children
|
|
45
|
+
// by one code path. A Set is used because the same session is never registered twice (sessionIds are unique per connection) but membership tests are needed
|
|
46
|
+
// during detach handling.
|
|
47
|
+
const sessions = new Set([rootSession]);
|
|
48
|
+
let disposed = false;
|
|
49
|
+
// Installs Network domain + response listener + recursive auto-attach on a freshly-attached session. Called for the root session synchronously below and for
|
|
50
|
+
// every child target as it attaches. Order matters: the Target.attachedToTarget listener has to be registered before setAutoAttach is sent, because
|
|
51
|
+
// setAutoAttach retroactively fires attachedToTarget for currently-attached children and we cannot miss those events. The Network.responseReceived and
|
|
52
|
+
// Target.detachedFromTarget listeners are insensitive to ordering relative to setAutoAttach; only the attach listener carries the retroactive-fire constraint.
|
|
53
|
+
const attachToSession = async (session, sessionId) => {
|
|
54
|
+
if (disposed) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
sessions.add(session);
|
|
58
|
+
// Listen for responses on this session. The handler captures sessionId by closure so the consumer can identify the originating target.
|
|
59
|
+
session.on("Network.responseReceived", (params) => {
|
|
60
|
+
if (disposed) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
options.onResponse({ headers: params.response.headers ?? {}, sessionId, url: params.response.url });
|
|
64
|
+
});
|
|
65
|
+
// Propagate the attach policy down this subtree. Without this, grandchildren of the root (iframes inside iframes, workers spawned by an OOPIF) would attach
|
|
66
|
+
// but their Target.attachedToTarget events would fire on the intermediate session, not the root - and we would not be listening there. Recursive setAutoAttach
|
|
67
|
+
// ensures every descendant session reaches our handler.
|
|
68
|
+
session.on("Target.attachedToTarget", (params) => { void onChildAttached(params); });
|
|
69
|
+
session.on("Target.detachedFromTarget", (params) => { onChildDetached(params); });
|
|
70
|
+
try {
|
|
71
|
+
await session.send("Target.setAutoAttach", { autoAttach: true, flatten: true, waitForDebuggerOnStart: false });
|
|
72
|
+
await session.send("Network.enable");
|
|
73
|
+
LOG.debug("browser:tabObserver", "Attached to session %s.", sessionId || "<root>");
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
// Session may have detached between attach and our init - benign during page navigations and shutdown. Logging at debug rather than warn because the
|
|
77
|
+
// observer's job is to be resilient to lifecycle churn, not to alert on every churn event.
|
|
78
|
+
LOG.debug("browser:tabObserver", "Setup failed for session %s: %s.", sessionId || "<root>", String(error));
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
// Handler for Target.attachedToTarget. Resolves the freshly-attached sessionId to a CDPSession via the parent connection, applies the target-type predicate,
|
|
82
|
+
// and forwards to attachToSession. The filter is the consumer's policy lever; the default skips only the browser-level target.
|
|
83
|
+
const onChildAttached = async (params) => {
|
|
84
|
+
if (disposed) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!targetFilter({ type: params.targetInfo.type })) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const childSession = connection.session(params.sessionId);
|
|
91
|
+
if (!childSession) {
|
|
92
|
+
// Race: the child detached between the attach event and our lookup. Not actionable.
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
await attachToSession(childSession, params.sessionId);
|
|
96
|
+
};
|
|
97
|
+
// Handler for Target.detachedFromTarget. Removes the detached session from tracking and clears its listeners. We do not call detach() on the session here -
|
|
98
|
+
// Chrome has already torn down the underlying target, and an explicit detach would race with the framework's internal cleanup. removeAllListeners is sufficient
|
|
99
|
+
// because we no longer want events from this session and the session object will be garbage-collected once the framework releases it.
|
|
100
|
+
const onChildDetached = (params) => {
|
|
101
|
+
if (disposed) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const childSession = connection.session(params.sessionId);
|
|
105
|
+
if (!childSession) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
sessions.delete(childSession);
|
|
109
|
+
childSession.removeAllListeners("Network.responseReceived");
|
|
110
|
+
childSession.removeAllListeners("Target.attachedToTarget");
|
|
111
|
+
childSession.removeAllListeners("Target.detachedFromTarget");
|
|
112
|
+
LOG.debug("browser:tabObserver", "Detached from session %s.", params.sessionId);
|
|
113
|
+
};
|
|
114
|
+
// Install on the root session. The empty sessionId is the CDP convention for "root" - flattened child sessions get real sessionIds. Doing this synchronously
|
|
115
|
+
// before returning the handle means by the time the caller awaits the returned promise, all currently-attached children have already fired their retroactive
|
|
116
|
+
// Target.attachedToTarget events and been onboarded.
|
|
117
|
+
await attachToSession(rootSession, "");
|
|
118
|
+
const dispose = () => {
|
|
119
|
+
if (disposed) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
disposed = true;
|
|
123
|
+
for (const session of sessions) {
|
|
124
|
+
try {
|
|
125
|
+
session.removeAllListeners("Network.responseReceived");
|
|
126
|
+
session.removeAllListeners("Target.attachedToTarget");
|
|
127
|
+
session.removeAllListeners("Target.detachedFromTarget");
|
|
128
|
+
void session.send("Network.disable").catch(() => { });
|
|
129
|
+
void session.detach().catch(() => { });
|
|
130
|
+
}
|
|
131
|
+
catch (_error) {
|
|
132
|
+
// Disposal is best-effort; a failed session teardown does not block teardown of the others. The Set iteration continues regardless.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
sessions.clear();
|
|
136
|
+
LOG.debug("browser:tabObserver", "Tab network observer disposed.");
|
|
137
|
+
};
|
|
138
|
+
return { dispose, [Symbol.dispose]: dispose };
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=tabNetworkObserver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tabNetworkObserver.js","sourceRoot":"","sources":["../../src/browser/tabNetworkObserver.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { LOG, evaluateWithAbort, formatError } from "../../utils/index.js";
|
|
2
2
|
import { CONFIG } from "../../config/index.js";
|
|
3
3
|
import { logAvailableChannels } from "./shared.js";
|
|
4
|
-
// Base URL for HBO Max watch page navigation
|
|
4
|
+
// Base URL for HBO Max watch page navigation. Used to build full watch URLs by concatenating with the relative /channel/watch/<uuid>/<uuid> path read from the
|
|
5
|
+
// rail. Kept as a separate constant from HBO_CHANNELS_URL so the two roles (path-concatenation base vs. landing-URL constant) stay distinct.
|
|
5
6
|
const HBO_MAX_BASE_URL = "https://play.hbomax.com";
|
|
7
|
+
// The /channels hub URL where HBO Max surfaces all live linear channels. PrismCast lands here directly via provider.guideUrl rather than chasing brand-page links
|
|
8
|
+
// out of the homepage menu - HBO's site restructure (late 2025/early 2026) consolidated brand tabs into the /channels hub, deprecating the previous menu-bar
|
|
9
|
+
// driven navigation entirely. This is now the single landing URL for both tuning and discovery.
|
|
10
|
+
const HBO_CHANNELS_URL = "https://play.hbomax.com/channels";
|
|
6
11
|
// Unified channel cache for HBO Max. Maps lowercased channel names (e.g., "hbo", "hbo hits") to their combined discovery and tuning data. Populated during the
|
|
7
12
|
// first tune (when the strategy reads all channels from the channel rail) or the first discovery call. Both tuning (via resolveHboDirectUrl) and discovery (via
|
|
8
13
|
// getCachedChannels / discoverHboChannels) read from this single cache. Cleared on browser disconnect via clearHboCache().
|
|
9
14
|
const hboChannelCache = new Map();
|
|
10
|
-
// Module-level cache for the HBO tab page URL discovered from the homepage menu bar. Cleared on browser disconnect (via clearHboCache) and inline when the
|
|
11
|
-
// cached URL turns out to be stale (the channel rail is not found at the cached URL). Separate from the channel cache because it's a navigation target, not
|
|
12
|
-
// channel data.
|
|
13
|
-
let hboTabUrl = null;
|
|
14
15
|
/**
|
|
15
16
|
* Returns a cached HBO Max watch URL for the given channel selector, or null if no cached URL exists.
|
|
16
17
|
* @param channelSelector - The channel selector string (e.g., "HBO", "HBO Hits").
|
|
@@ -32,12 +33,11 @@ function invalidateHboDirectUrl(channelSelector) {
|
|
|
32
33
|
hboChannelCache.delete(channelSelector.toLowerCase());
|
|
33
34
|
}
|
|
34
35
|
/**
|
|
35
|
-
* Clears
|
|
36
|
-
*
|
|
36
|
+
* Clears the unified HBO channel cache. Called by clearChannelSelectionCaches() in the coordinator when the browser restarts, since cached watch URLs may be
|
|
37
|
+
* stale in a new browser session.
|
|
37
38
|
*/
|
|
38
39
|
function clearHboCache() {
|
|
39
40
|
hboChannelCache.clear();
|
|
40
|
-
hboTabUrl = null;
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
43
|
* Derives a DiscoveredChannel array from the unified channel cache. HBO does not create alias entries (resolveHboDirectUrl does exact-match only, no
|
|
@@ -60,43 +60,15 @@ function populateHboChannelCache(rawChannels) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
|
-
* Reads
|
|
64
|
-
*
|
|
65
|
-
* @param page - The Puppeteer page object, expected to be on the HBO Max homepage.
|
|
66
|
-
* @returns The full HBO tab page URL, or null if the tab link was not found.
|
|
67
|
-
*/
|
|
68
|
-
async function readHboTabUrl(page) {
|
|
69
|
-
// Wait for the HBO tab link to appear in the menu bar. The homepage is a single-page application that renders the navigation dynamically after the initial HTML
|
|
70
|
-
// shell loads. Without this wait, the evaluate call below would run against an incomplete DOM and fail to find the tab link.
|
|
71
|
-
const HBO_TAB_SELECTOR = "a[aria-label=\"H B O\"]";
|
|
72
|
-
try {
|
|
73
|
-
await page.waitForSelector(HBO_TAB_SELECTOR, { timeout: 5000 });
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
const href = await evaluateWithAbort(page, (selector) => {
|
|
79
|
-
const tab = document.querySelector(selector);
|
|
80
|
-
if (!tab) {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
return tab.getAttribute("href");
|
|
84
|
-
}, [HBO_TAB_SELECTOR]);
|
|
85
|
-
if (!href) {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
return HBO_MAX_BASE_URL + href;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Reads all channels from the HBO Channels tile rail on the HBO tab page. The rail section contains tiles for each live channel, each with a backup text
|
|
92
|
-
* `<p aria-hidden="true">` element containing the channel name and an `<a>` with href pointing to the watch page. Returns all discovered channels so the caller
|
|
63
|
+
* Reads all channels from the "Everything You Love From HBO" rail on the /channels hub. The rail section contains tiles for each live linear channel, each with
|
|
64
|
+
* a `<p aria-hidden="true">` element containing the channel name and an `<a>` whose href points to the watch page. Returns all discovered channels so the caller
|
|
93
65
|
* can populate the cache in bulk.
|
|
94
|
-
* @param page - The Puppeteer page object, expected to be on
|
|
66
|
+
* @param page - The Puppeteer page object, expected to be on https://play.hbomax.com/channels.
|
|
95
67
|
* @returns Object with `railFound` indicating whether the rail section was present, and `channels` containing all discovered channel names and watch paths.
|
|
96
68
|
*/
|
|
97
69
|
async function readHboChannelRail(page) {
|
|
98
|
-
const HBO_RAIL_SELECTOR = "section[data-testid=\"
|
|
99
|
-
// Wait for the
|
|
70
|
+
const HBO_RAIL_SELECTOR = "section[data-testid=\"channels-hub-page-everything-you-love-hbo-rail-us_rail\"]";
|
|
71
|
+
// Wait for the HBO channel rail section to appear. If it doesn't appear, the page structure may have changed or the user's HBO Max subscription may not be active.
|
|
100
72
|
try {
|
|
101
73
|
await page.waitForSelector(HBO_RAIL_SELECTOR, { timeout: CONFIG.streaming.videoTimeout });
|
|
102
74
|
}
|
|
@@ -142,74 +114,23 @@ async function readHboChannelRail(page) {
|
|
|
142
114
|
return { channels, railFound: true };
|
|
143
115
|
}
|
|
144
116
|
/**
|
|
145
|
-
* HBO grid strategy:
|
|
146
|
-
*
|
|
147
|
-
* without loading the tab page.
|
|
117
|
+
* HBO grid strategy: reads the live channel rail on the /channels hub for all channel watch URLs, then navigates to the target channel's URL. All discovered
|
|
118
|
+
* channels are cached so subsequent tunes resolve via resolveDirectUrl without re-reading the rail.
|
|
148
119
|
*
|
|
149
|
-
* The strategy handles
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* 3. Watch page -> video playback begins
|
|
120
|
+
* The strategy handles two phases per tune:
|
|
121
|
+
* 1. Channel rail (page is already on /channels, navigated by the coordinator via provider.guideUrl) -> read all watch URLs.
|
|
122
|
+
* 2. Watch page -> video playback begins.
|
|
153
123
|
*
|
|
154
|
-
*
|
|
155
|
-
* This fallback triggers at most once per tune attempt.
|
|
156
|
-
* @param page - The Puppeteer page object, expected to be on the HBO Max homepage.
|
|
124
|
+
* @param page - The Puppeteer page object, expected to be on https://play.hbomax.com/channels.
|
|
157
125
|
* @param profile - The resolved site profile with a non-null channelSelector (channel name, e.g., "HBO", "HBO Hits").
|
|
158
126
|
* @returns Result object with success status and optional failure reason.
|
|
159
127
|
*/
|
|
160
128
|
async function hboGridStrategy(page, profile) {
|
|
161
129
|
const channelName = profile.channelSelector;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (hboTabUrl) {
|
|
165
|
-
usedCache = true;
|
|
166
|
-
LOG.debug("tuning:hbo", "Using cached HBO tab URL: %s.", hboTabUrl);
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
const discovered = await readHboTabUrl(page);
|
|
170
|
-
if (!discovered) {
|
|
171
|
-
return { reason: "HBO tab not found in homepage menu bar. HBO Max subscription may not be active.", success: false };
|
|
172
|
-
}
|
|
173
|
-
hboTabUrl = discovered;
|
|
174
|
-
LOG.debug("tuning:hbo", "Discovered HBO tab URL: %s.", hboTabUrl);
|
|
175
|
-
}
|
|
176
|
-
try {
|
|
177
|
-
await page.goto(hboTabUrl, { timeout: CONFIG.streaming.navigationTimeout, waitUntil: "load" });
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
return { reason: "Failed to navigate to HBO tab page: " + formatError(error) + ".", success: false };
|
|
181
|
-
}
|
|
182
|
-
// Phase 2: Read the channel rail for all channel watch URLs.
|
|
183
|
-
let railResult = await readHboChannelRail(page);
|
|
184
|
-
// Fallback: if the rail was not found and we used a cached URL, the cache may be stale. Clear it, navigate back to the homepage, rediscover the tab URL, and retry.
|
|
185
|
-
if (!railResult.railFound && usedCache) {
|
|
186
|
-
LOG.debug("tuning:hbo", "HBO channel rail not found at cached URL. Rediscovering tab URL from homepage.");
|
|
187
|
-
hboTabUrl = null;
|
|
188
|
-
try {
|
|
189
|
-
await page.goto(HBO_MAX_BASE_URL, { timeout: CONFIG.streaming.navigationTimeout, waitUntil: "load" });
|
|
190
|
-
}
|
|
191
|
-
catch (error) {
|
|
192
|
-
return { reason: "Failed to navigate back to HBO Max homepage: " + formatError(error) + ".", success: false };
|
|
193
|
-
}
|
|
194
|
-
const rediscovered = await readHboTabUrl(page);
|
|
195
|
-
if (!rediscovered) {
|
|
196
|
-
return { reason: "HBO tab not found in homepage menu bar after cache invalidation.", success: false };
|
|
197
|
-
}
|
|
198
|
-
hboTabUrl = rediscovered;
|
|
199
|
-
LOG.debug("tuning:hbo", "Rediscovered HBO tab URL: %s.", hboTabUrl);
|
|
200
|
-
try {
|
|
201
|
-
await page.goto(hboTabUrl, { timeout: CONFIG.streaming.navigationTimeout, waitUntil: "load" });
|
|
202
|
-
}
|
|
203
|
-
catch (error) {
|
|
204
|
-
return { reason: "Failed to navigate to rediscovered HBO tab page: " + formatError(error) + ".", success: false };
|
|
205
|
-
}
|
|
206
|
-
railResult = await readHboChannelRail(page);
|
|
207
|
-
if (!railResult.railFound) {
|
|
208
|
-
return { reason: "HBO channel rail not found at rediscovered URL. Site structure may have changed.", success: false };
|
|
209
|
-
}
|
|
210
|
-
}
|
|
130
|
+
// Phase 1: Read the channel rail. The coordinator has already navigated the page to provider.guideUrl (the /channels hub), so we read the rail in place.
|
|
131
|
+
const railResult = await readHboChannelRail(page);
|
|
211
132
|
if (!railResult.railFound) {
|
|
212
|
-
return { reason: "HBO channel rail not found on
|
|
133
|
+
return { reason: "HBO channel rail not found on /channels. HBO Max may have restructured the page, or the subscription may not be active.", success: false };
|
|
213
134
|
}
|
|
214
135
|
// Populate the unified channel cache with all discovered channels. Always repopulate rather than skipping when the cache has entries, because invalidated
|
|
215
136
|
// entries (deleted by invalidateHboDirectUrl) need to be restored with fresh watch URLs from the rail.
|
|
@@ -221,12 +142,12 @@ async function hboGridStrategy(page, profile) {
|
|
|
221
142
|
logAvailableChannels({
|
|
222
143
|
availableChannels: railResult.channels.map((ch) => ch.name).sort(),
|
|
223
144
|
channelName,
|
|
224
|
-
guideUrl:
|
|
145
|
+
guideUrl: HBO_CHANNELS_URL,
|
|
225
146
|
providerName: "HBO Max"
|
|
226
147
|
});
|
|
227
148
|
return { reason: "Channel " + channelName + " not found in HBO channel rail.", success: false };
|
|
228
149
|
}
|
|
229
|
-
// Phase
|
|
150
|
+
// Phase 2: Navigate to the watch URL to start playback.
|
|
230
151
|
LOG.debug("tuning:hbo", "Navigating to HBO Max watch URL for %s.", channelName);
|
|
231
152
|
try {
|
|
232
153
|
await page.goto(watchUrl, { timeout: CONFIG.streaming.navigationTimeout, waitUntil: "load" });
|
|
@@ -249,9 +170,8 @@ async function resolveHboDirectUrlAsync(channelSelector, _page) {
|
|
|
249
170
|
}
|
|
250
171
|
/**
|
|
251
172
|
* Discovers all channels from the HBO Max channel rail. Returns cached results if the unified channel cache is populated from a prior tune or discovery call.
|
|
252
|
-
* Otherwise
|
|
253
|
-
*
|
|
254
|
-
* @param page - The Puppeteer page object, already on the HBO Max homepage (navigated by the route handler).
|
|
173
|
+
* Otherwise reads the rail in place - the route handler has already navigated the page to provider.guideUrl (the /channels hub).
|
|
174
|
+
* @param page - The Puppeteer page object, already on https://play.hbomax.com/channels (navigated by the route handler).
|
|
255
175
|
* @returns Array of discovered channels.
|
|
256
176
|
*/
|
|
257
177
|
async function discoverHboChannels(page) {
|
|
@@ -259,19 +179,7 @@ async function discoverHboChannels(page) {
|
|
|
259
179
|
if (hboChannelCache.size > 0) {
|
|
260
180
|
return buildHboDiscoveredChannels();
|
|
261
181
|
}
|
|
262
|
-
//
|
|
263
|
-
const tabUrl = await readHboTabUrl(page);
|
|
264
|
-
if (!tabUrl) {
|
|
265
|
-
return [];
|
|
266
|
-
}
|
|
267
|
-
// Navigate to the HBO tab page.
|
|
268
|
-
try {
|
|
269
|
-
await page.goto(tabUrl, { timeout: CONFIG.streaming.navigationTimeout, waitUntil: "load" });
|
|
270
|
-
}
|
|
271
|
-
catch {
|
|
272
|
-
return [];
|
|
273
|
-
}
|
|
274
|
-
// Read the channel rail.
|
|
182
|
+
// Read the channel rail directly. The coordinator has already landed the page on /channels.
|
|
275
183
|
const railResult = await readHboChannelRail(page);
|
|
276
184
|
if (!railResult.railFound || (railResult.channels.length === 0)) {
|
|
277
185
|
return [];
|
|
@@ -292,11 +200,11 @@ function getHboCachedChannels() {
|
|
|
292
200
|
export const hboProvider = {
|
|
293
201
|
discoverChannels: discoverHboChannels,
|
|
294
202
|
getCachedChannels: getHboCachedChannels,
|
|
295
|
-
guideUrl:
|
|
203
|
+
guideUrl: HBO_CHANNELS_URL,
|
|
296
204
|
label: "HBO Max",
|
|
297
|
-
// Profile for HBO Max live channels (play.hbomax.com). The
|
|
298
|
-
// Hits, HBO Drama, HBO Comedy, HBO Movies) as tiles. The hboGrid strategy
|
|
299
|
-
//
|
|
205
|
+
// Profile for HBO Max live channels (play.hbomax.com/channels). The /channels hub contains an "Everything You Love From HBO" rail showing all 5 live linear
|
|
206
|
+
// channels (HBO, HBO Hits, HBO Drama, HBO Comedy, HBO Movies) as tiles. The hboGrid strategy reads the rail for the watch URL matching the channelSelector name
|
|
207
|
+
// and navigates to it. Extends fullscreenApi for requestFullscreen() behavior inherited by the watch page.
|
|
300
208
|
profile: {
|
|
301
209
|
category: "multiChannel",
|
|
302
210
|
channelSelection: { strategy: "hboGrid" },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hbo.js","sourceRoot":"","sources":["../../../src/browser/tuning/hbo.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,GAAG,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC3E,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAE/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,
|
|
1
|
+
{"version":3,"file":"hbo.js","sourceRoot":"","sources":["../../../src/browser/tuning/hbo.ts"],"names":[],"mappings":"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"}
|
|
@@ -554,6 +554,63 @@ async function releaseHeldPlaylist(page) {
|
|
|
554
554
|
}
|
|
555
555
|
});
|
|
556
556
|
}
|
|
557
|
+
/**
|
|
558
|
+
* Hulu surfaces a "Who's Watching?" profile-selector modal when an account has more than one profile, and re-prompts intermittently across sessions. The modal
|
|
559
|
+
* blocks all guide and channel UI until a profile is chosen, so PrismCast's tuning logic (#CHANNELS click, guide-grid binary search) never gets a chance to run.
|
|
560
|
+
* This helper detects the modal and clicks the first profile, restoring the normal /live page flow. Both the tuning path (guideGridStrategy) and the discovery
|
|
561
|
+
* path (discoverHuluChannels) call this immediately after navigation so they share one source of truth for the profile-selector workaround.
|
|
562
|
+
*
|
|
563
|
+
* Selection policy: always the first profile. We do not expose a configurable name because the typical PrismCast deployment has a single primary profile, and an
|
|
564
|
+
* operator who wants a different one can reorder profiles in Hulu's account settings. The selected profile name is captured in the audit log so the operator
|
|
565
|
+
* always knows which one was picked.
|
|
566
|
+
*
|
|
567
|
+
* The probe is cheap when no modal is present (one DOM evaluate) and best-effort when it is: any failure between detection and post-click navigation is logged
|
|
568
|
+
* and returns silently. The downstream #CHANNELS-wait timeout becomes the failure signal of last resort, so the existing error path stays intact.
|
|
569
|
+
* @param page - The Puppeteer page object.
|
|
570
|
+
*/
|
|
571
|
+
async function handleProfileSelectorIfPresent(page) {
|
|
572
|
+
const probe = await page.evaluate(() => {
|
|
573
|
+
const modal = document.querySelector("[data-testid=\"ProfileSelectorModal\"]");
|
|
574
|
+
if (!modal) {
|
|
575
|
+
return { present: false };
|
|
576
|
+
}
|
|
577
|
+
const tile = modal.querySelector("a.ProfileList__profile-item--modal");
|
|
578
|
+
if (!tile) {
|
|
579
|
+
return { present: true };
|
|
580
|
+
}
|
|
581
|
+
const rect = tile.getBoundingClientRect();
|
|
582
|
+
const ariaLabel = tile.getAttribute("aria-label") ?? "";
|
|
583
|
+
// "Switch profile to <name>" is the format Hulu emits today; we strip the prefix to surface the name in the log. A future relabel falls through to the
|
|
584
|
+
// literal "(unnamed)" rather than failing, so a Hulu UX rename never blocks tuning - it only loses the friendly name in the audit line.
|
|
585
|
+
const match = (/^Switch profile to (.+)$/).exec(ariaLabel);
|
|
586
|
+
return {
|
|
587
|
+
name: match ? match[1] : "(unnamed)",
|
|
588
|
+
present: true,
|
|
589
|
+
tile: { x: rect.left + (rect.width / 2), y: rect.top + (rect.height / 2) }
|
|
590
|
+
};
|
|
591
|
+
});
|
|
592
|
+
if (!probe.present) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (!probe.tile) {
|
|
596
|
+
LOG.warn("Hulu prompted for profile selection but no profile tile was found. Tuning may fail; sign into Hulu in this browser to investigate.");
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
LOG.info("Hulu prompted for profile selection. Defaulting to the first profile, \"%s\".", probe.name ?? "(unnamed)");
|
|
600
|
+
try {
|
|
601
|
+
// Coordinate-based click. Synthetic element.click() inside page.evaluate() does not reliably reach React's handlers on Hulu's SPA; the project's tuning code
|
|
602
|
+
// standardizes on page.mouse.click() with computed centers for this reason.
|
|
603
|
+
await page.mouse.click(probe.tile.x, probe.tile.y);
|
|
604
|
+
// Wait for the URL to leave /profiles, indicating the navigation triggered by the profile click has begun. The caller's downstream wait for #CHANNELS picks
|
|
605
|
+
// up the rest of the page settling; we only need to confirm the picker is out of the way before yielding back. The 5s timeout is a "post-click settle"
|
|
606
|
+
// budget - longer than a synchronous URL update needs, shorter than CONFIG.streaming.videoTimeout (which is sized for page-render waits); a real failure
|
|
607
|
+
// bubbles up via the catch and the downstream #CHANNELS-wait becomes the final timeout.
|
|
608
|
+
await page.waitForFunction(() => !window.location.pathname.startsWith("/profiles"), { timeout: 5000 });
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
LOG.warn("Hulu profile selection click did not complete cleanly: %s.", formatError(error));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
557
614
|
/**
|
|
558
615
|
* 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
|
|
559
616
|
* 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,13 +623,14 @@ async function releaseHeldPlaylist(page) {
|
|
|
566
623
|
* 3. Linear scan fallback - safety net for raw call sign searches or any channel the binary search cannot find (~2.4 seconds)
|
|
567
624
|
*
|
|
568
625
|
* The selection process:
|
|
569
|
-
* 1.
|
|
570
|
-
* 2.
|
|
571
|
-
* 3.
|
|
572
|
-
* 4.
|
|
573
|
-
* 5.
|
|
574
|
-
* 6.
|
|
575
|
-
* 7.
|
|
626
|
+
* 1. Clear any "Who's Watching?" profile-selector modal that intercepts before guide interaction (see handleProfileSelectorIfPresent)
|
|
627
|
+
* 2. If listSelector is provided, click the tab/button to reveal the channel list (e.g., a "Channels" tab)
|
|
628
|
+
* 3. Wait for the channel grid rows to render in the DOM
|
|
629
|
+
* 4. Check the unified cache for a row number direct-scroll shortcut
|
|
630
|
+
* 5. Binary search: scroll to the midpoint row, read rendered channels (caching row numbers), check for exact match or infer local affiliate
|
|
631
|
+
* 6. If binary search fails, linear scan from top to bottom as a universal fallback
|
|
632
|
+
* 7. Click the on-now program cell (`.LiveGuideProgram--first`) in the target channel's row to open the playback overlay
|
|
633
|
+
* 8. If playSelector is provided, wait for and click the play button to start live playback
|
|
576
634
|
* @param page - The Puppeteer page object.
|
|
577
635
|
* @param profile - The resolved site profile with a non-null channelSelector (channel name) and channelSelection config.
|
|
578
636
|
* @returns Result object with success status and optional failure reason.
|
|
@@ -580,6 +638,9 @@ async function releaseHeldPlaylist(page) {
|
|
|
580
638
|
async function guideGridStrategy(page, profile) {
|
|
581
639
|
const { channelSelection, channelSelector: channelName } = profile;
|
|
582
640
|
const { listSelector, playSelector } = channelSelection;
|
|
641
|
+
// Clear the "Who's Watching?" profile-selector modal before any guide interaction. Hulu prompts for profile selection intermittently across sessions; without
|
|
642
|
+
// this step, all downstream selectors (#CHANNELS, live-guide-row) fail because the picker overlay blocks them.
|
|
643
|
+
await handleProfileSelectorIfPresent(page);
|
|
583
644
|
// 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
|
|
584
645
|
// 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
|
|
585
646
|
// guide remains hidden and the virtualizer never populates rows. We use $eval for the click because overlapping elements (spinners, overlays) can intercept
|
|
@@ -1357,6 +1418,8 @@ async function discoverHuluChannels(page) {
|
|
|
1357
1418
|
catch {
|
|
1358
1419
|
return [];
|
|
1359
1420
|
}
|
|
1421
|
+
// Clear the "Who's Watching?" profile-selector modal before any guide interaction (same logic guideGridStrategy uses on the tuning path).
|
|
1422
|
+
await handleProfileSelectorIfPresent(page);
|
|
1360
1423
|
// 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
|
|
1361
1424
|
// 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.
|
|
1362
1425
|
const listSelector = "#CHANNELS";
|