prismcast 1.2.0 → 1.3.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 +26 -1
- package/dist/app.js +29 -2
- package/dist/app.js.map +1 -1
- package/dist/browser/cdp.js +1 -1
- package/dist/browser/cdp.js.map +1 -1
- package/dist/browser/channelSelection.d.ts +14 -4
- package/dist/browser/channelSelection.js +48 -704
- package/dist/browser/channelSelection.js.map +1 -1
- package/dist/browser/index.d.ts +18 -4
- package/dist/browser/index.js +205 -39
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/tuning/fox.d.ts +19 -0
- package/dist/browser/tuning/fox.js +61 -0
- package/dist/browser/tuning/fox.js.map +1 -0
- package/dist/browser/tuning/hbo.d.ts +22 -0
- package/dist/browser/tuning/hbo.js +182 -0
- package/dist/browser/tuning/hbo.js.map +1 -0
- package/dist/browser/tuning/hulu.d.ts +30 -0
- package/dist/browser/tuning/hulu.js +514 -0
- package/dist/browser/tuning/hulu.js.map +1 -0
- package/dist/browser/tuning/sling.d.ts +22 -0
- package/dist/browser/tuning/sling.js +287 -0
- package/dist/browser/tuning/sling.js.map +1 -0
- package/dist/browser/tuning/thumbnailRow.d.ts +18 -0
- package/dist/browser/tuning/thumbnailRow.js +107 -0
- package/dist/browser/tuning/thumbnailRow.js.map +1 -0
- package/dist/browser/tuning/tileClick.d.ts +18 -0
- package/dist/browser/tuning/tileClick.js +102 -0
- package/dist/browser/tuning/tileClick.js.map +1 -0
- package/dist/browser/tuning/youtubeTv.d.ts +17 -0
- package/dist/browser/tuning/youtubeTv.js +99 -0
- package/dist/browser/tuning/youtubeTv.js.map +1 -0
- package/dist/browser/video.d.ts +2 -2
- package/dist/browser/video.js +18 -8
- package/dist/browser/video.js.map +1 -1
- package/dist/channels/index.js +131 -12
- package/dist/channels/index.js.map +1 -1
- package/dist/config/profiles.d.ts +5 -5
- package/dist/config/profiles.js +35 -11
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/providers.d.ts +48 -4
- package/dist/config/providers.js +182 -22
- package/dist/config/providers.js.map +1 -1
- package/dist/config/sites.d.ts +10 -0
- package/dist/config/sites.js +96 -17
- package/dist/config/sites.js.map +1 -1
- package/dist/config/userChannels.js +21 -3
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +2 -1
- package/dist/config/userConfig.js +12 -4
- package/dist/config/userConfig.js.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/routes/auth.js +6 -0
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/config.d.ts +5 -0
- package/dist/routes/config.js +273 -35
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/debug.d.ts +6 -0
- package/dist/routes/debug.js +274 -0
- package/dist/routes/debug.js.map +1 -0
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.js +3 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/logs.js +17 -5
- package/dist/routes/logs.js.map +1 -1
- package/dist/routes/root.js +507 -128
- package/dist/routes/root.js.map +1 -1
- package/dist/routes/theme.js +7 -0
- package/dist/routes/theme.js.map +1 -1
- package/dist/service/commands.js +8 -8
- package/dist/service/commands.js.map +1 -1
- package/dist/service/generators.js +22 -16
- package/dist/service/generators.js.map +1 -1
- package/dist/streaming/fmp4Segmenter.d.ts +7 -0
- package/dist/streaming/fmp4Segmenter.js +124 -15
- package/dist/streaming/fmp4Segmenter.js.map +1 -1
- package/dist/streaming/hls.d.ts +74 -6
- package/dist/streaming/hls.js +212 -130
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsSegments.js +11 -4
- package/dist/streaming/hlsSegments.js.map +1 -1
- package/dist/streaming/lifecycle.js +1 -1
- package/dist/streaming/lifecycle.js.map +1 -1
- package/dist/streaming/monitor.d.ts +1 -1
- package/dist/streaming/monitor.js +202 -53
- package/dist/streaming/monitor.js.map +1 -1
- package/dist/streaming/mp4Parser.d.ts +29 -1
- package/dist/streaming/mp4Parser.js +243 -18
- package/dist/streaming/mp4Parser.js.map +1 -1
- package/dist/streaming/mpegts.d.ts +5 -2
- package/dist/streaming/mpegts.js +122 -29
- package/dist/streaming/mpegts.js.map +1 -1
- package/dist/streaming/registry.d.ts +1 -1
- package/dist/streaming/registry.js.map +1 -1
- package/dist/streaming/setup.d.ts +13 -1
- package/dist/streaming/setup.js +136 -36
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.js +9 -9
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.js +5 -6
- package/dist/streaming/statusEmitter.js.map +1 -1
- package/dist/types/index.d.ts +16 -3
- package/dist/types/index.js +5 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/debugFilter.d.ts +38 -0
- package/dist/utils/debugFilter.js +157 -0
- package/dist/utils/debugFilter.js.map +1 -0
- package/dist/utils/evaluate.js.map +1 -1
- package/dist/utils/ffmpeg.d.ts +5 -0
- package/dist/utils/ffmpeg.js +13 -2
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/fileLogger.d.ts +2 -1
- package/dist/utils/fileLogger.js +4 -2
- package/dist/utils/fileLogger.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logEmitter.d.ts +1 -0
- package/dist/utils/logEmitter.js.map +1 -1
- package/dist/utils/logger.d.ts +9 -8
- package/dist/utils/logger.js +52 -28
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/retry.d.ts +1 -1
- package/dist/utils/retry.js +3 -3
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/version.js +4 -9
- package/dist/utils/version.js.map +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { LOG, delay, formatError } from "../../utils/index.js";
|
|
2
|
+
import { CONFIG } from "../../config/index.js";
|
|
3
|
+
import { normalizeChannelName } from "../channelSelection.js";
|
|
4
|
+
// Sling TV guide grid row index cache. Maps normalized channel names (from data-testid="channel-{NAME}" attributes) to their row indices extracted from the
|
|
5
|
+
// parent .guide-row-container CSS class (gridGuideRow-{N}). Separate from the Hulu guideRowCache because Sling uses a different row index system (CSS class-based)
|
|
6
|
+
// and different scroll mechanics (.guide-cell scrollTop vs document.documentElement.scrollTop).
|
|
7
|
+
const slingRowCache = new Map();
|
|
8
|
+
/**
|
|
9
|
+
* Clears the Sling TV row cache. Called by clearChannelSelectionCaches() in the coordinator when the browser restarts.
|
|
10
|
+
*/
|
|
11
|
+
export function clearSlingCache() {
|
|
12
|
+
slingRowCache.clear();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Reads all rendered channel entries from the Sling TV guide grid in a single browser evaluate call. Extracts names from data-testid="channel-{NAME}" attributes
|
|
16
|
+
* and row indices from the parent .guide-row-container CSS class containing gridGuideRow-{N}. When the target channel is found, also locates the on-now program
|
|
17
|
+
* cell, scrolls it into view, and returns its center coordinates — eliminating a second evaluate round-trip. Populates the slingRowCache as a side effect. For
|
|
18
|
+
* local affiliates (ABC, FOX, NBC), also matches channels whose name starts with the target followed by " (" to handle the "network (callsign)" format.
|
|
19
|
+
* @param page - The Puppeteer page object.
|
|
20
|
+
* @param targetName - The normalized (lowercased, trimmed) channel name to match, or null to skip click target resolution.
|
|
21
|
+
* @returns Object with all rendered channels, optional click coordinates for the target's on-now cell, and the actual matched name (which may differ from
|
|
22
|
+
* targetName for local affiliates).
|
|
23
|
+
*/
|
|
24
|
+
async function readSlingChannelsAndLocate(page, targetName) {
|
|
25
|
+
const raw = await page.evaluate((target) => {
|
|
26
|
+
const containers = document.querySelectorAll("[data-testid^=\"channel-\"]");
|
|
27
|
+
if (containers.length === 0) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const prefix = "channel-";
|
|
31
|
+
const channels = [];
|
|
32
|
+
let clickTarget = null;
|
|
33
|
+
let matchedName = null;
|
|
34
|
+
for (const el of Array.from(containers)) {
|
|
35
|
+
const testid = el.getAttribute("data-testid") ?? "";
|
|
36
|
+
const name = testid.slice(prefix.length).trim().replace(/\s+/g, " ").toLowerCase();
|
|
37
|
+
// Extract row index from the parent .guide-row-container CSS class. The class name follows the pattern "gridGuideRow-{N}".
|
|
38
|
+
let rowIndex = -1;
|
|
39
|
+
const rowContainer = el.closest(".guide-row-container");
|
|
40
|
+
if (rowContainer) {
|
|
41
|
+
const classMatch = /gridGuideRow-(\d+)/.exec(rowContainer.className);
|
|
42
|
+
if (classMatch) {
|
|
43
|
+
rowIndex = parseInt(classMatch[1], 10);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
channels.push({ name, rowIndex });
|
|
47
|
+
// When the target is found, locate the on-now program cell in the same pass. This avoids a second querySelectorAll + normalize loop in a separate evaluate.
|
|
48
|
+
// Sling local affiliates use the format "network (callsign)" so we also check for a prefix match where the channel name starts with the target followed by
|
|
49
|
+
// " (". This handles market-specific call signs without hardcoding them.
|
|
50
|
+
if (target && !clickTarget && ((name === target) || name.startsWith(target + " ("))) {
|
|
51
|
+
matchedName = name;
|
|
52
|
+
if (rowContainer) {
|
|
53
|
+
const onNow = rowContainer.querySelector(".grid-program-cell-container.active");
|
|
54
|
+
if (onNow) {
|
|
55
|
+
onNow.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
|
|
56
|
+
const rect = onNow.getBoundingClientRect();
|
|
57
|
+
if ((rect.width > 0) && (rect.height > 0)) {
|
|
58
|
+
clickTarget = { x: rect.x + (rect.width / 2), y: rect.y + (rect.height / 2) };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { channels, clickTarget, matchedName };
|
|
65
|
+
}, targetName);
|
|
66
|
+
if (!raw) {
|
|
67
|
+
return { channels: null, clickTarget: null, matchedName: null };
|
|
68
|
+
}
|
|
69
|
+
// Populate the row index cache with discovered mappings.
|
|
70
|
+
const rendered = [];
|
|
71
|
+
for (const ch of raw.channels) {
|
|
72
|
+
rendered.push({ name: ch.name, rowIndex: ch.rowIndex });
|
|
73
|
+
if (ch.rowIndex >= 0) {
|
|
74
|
+
slingRowCache.set(ch.name, ch.rowIndex);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { channels: rendered, clickTarget: raw.clickTarget, matchedName: raw.matchedName };
|
|
78
|
+
}
|
|
79
|
+
// Shorter timeout for non-final click attempts. Successful navigations typically complete in 1-2 seconds, so 5 seconds is generous while still failing fast enough
|
|
80
|
+
// to allow meaningful retries within the overall time budget.
|
|
81
|
+
const CLICK_RETRY_TIMEOUT = 5000;
|
|
82
|
+
// The guide page URL contains this path segment. Used to detect whether the page has navigated away from the guide after a click attempt.
|
|
83
|
+
const GUIDE_URL_MARKER = "grid_guide";
|
|
84
|
+
// Maximum number of click attempts before giving up. Three attempts with 5-second timeouts on the first two gives a worst-case wall-clock time of ~15 seconds —
|
|
85
|
+
// comparable to a single 10-second timeout plus the overhead of the full retry cycle, but with a much higher success rate for transient failures.
|
|
86
|
+
const MAX_CLICK_ATTEMPTS = 3;
|
|
87
|
+
/**
|
|
88
|
+
* Clicks the on-now program cell and waits for Sling to navigate to the player page. The click triggers a full page navigation to a /1/asset/{assetId}/watch URL,
|
|
89
|
+
* so we use Promise.all with page.waitForNavigation() to ensure the player page's DOM is ready before returning. Without this wait, initializePlayback() could run
|
|
90
|
+
* against a page that is mid-transition — either finding nothing or grabbing a stale element from the guide page. Uses domcontentloaded rather than load because
|
|
91
|
+
* the player page only needs to render a <video> element — waiting for all subresources (images, fonts) would add unnecessary latency since startVideoPlayback()
|
|
92
|
+
* independently waits for the video element. No settle delay before the click because readSlingChannelsAndLocate() already called scrollIntoView and read
|
|
93
|
+
* getBoundingClientRect, confirming the element is positioned, and any mispositioned click is caught by the navigation timeout.
|
|
94
|
+
* @param page - The Puppeteer page object.
|
|
95
|
+
* @param target - The x/y coordinates of the on-now cell to click.
|
|
96
|
+
* @param timeout - Navigation timeout in milliseconds. Defaults to CONFIG.streaming.navigationTimeout.
|
|
97
|
+
* @returns Result object with success status and optional failure reason.
|
|
98
|
+
*/
|
|
99
|
+
async function clickSlingOnNowAndWaitForNavigation(page, target, timeout = CONFIG.streaming.navigationTimeout) {
|
|
100
|
+
try {
|
|
101
|
+
// Register the navigation wait before the click fires to avoid a race where the navigation completes before waitForNavigation starts listening.
|
|
102
|
+
await Promise.all([
|
|
103
|
+
page.waitForNavigation({ timeout, waitUntil: "domcontentloaded" }),
|
|
104
|
+
page.mouse.click(target.x, target.y)
|
|
105
|
+
]);
|
|
106
|
+
return { success: true };
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
return { reason: "Navigation to Sling TV player page failed: " + formatError(error) + ".", success: false };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Attempts to click the on-now program cell up to MAX_CLICK_ATTEMPTS times with in-place retry. On each non-final attempt, uses a shorter timeout (CLICK_RETRY_TIMEOUT)
|
|
114
|
+
* to fail fast and allow a retry within the same time budget as a single full-timeout attempt plus the expensive full-retry cycle. Between retries, checks whether the
|
|
115
|
+
* page has already navigated away from the guide (indicating the click triggered navigation but the player page was slow to load) and re-reads on-now cell coordinates
|
|
116
|
+
* from the guide page to handle any virtualizer layout shifts that occurred during the timeout.
|
|
117
|
+
* @param page - The Puppeteer page object.
|
|
118
|
+
* @param initialTarget - The x/y coordinates from the initial readSlingChannelsAndLocate() call.
|
|
119
|
+
* @param normalizedName - The normalized channel name for re-reading coordinates on retry.
|
|
120
|
+
* @param channelName - The original channel name for log messages.
|
|
121
|
+
* @returns Result object with success status and optional failure reason from the last attempt.
|
|
122
|
+
*/
|
|
123
|
+
async function clickWithRetry(page, initialTarget, normalizedName, channelName) {
|
|
124
|
+
let target = initialTarget;
|
|
125
|
+
let lastResult = { reason: "No click attempts made.", success: false };
|
|
126
|
+
for (let attempt = 0; attempt < MAX_CLICK_ATTEMPTS; attempt++) {
|
|
127
|
+
// On retry attempts, re-read on-now cell coordinates. The virtualizer may have shifted layout during the timeout, making the original coordinates stale.
|
|
128
|
+
if (attempt > 0) {
|
|
129
|
+
// If the page has navigated away from the guide, the click did trigger navigation — it was just slow. Return success and let initializePlayback's
|
|
130
|
+
// waitForVideoReady handle the rest rather than re-clicking a page that is already mid-navigation.
|
|
131
|
+
if (!page.url().includes(GUIDE_URL_MARKER)) {
|
|
132
|
+
LOG.debug("tuning:sling", "Sling page navigated away from guide after click attempt %s for %s. Treating as success.", attempt, channelName);
|
|
133
|
+
return { success: true };
|
|
134
|
+
}
|
|
135
|
+
LOG.debug("tuning:sling", "Sling click attempt %s of %s for %s.", attempt + 1, MAX_CLICK_ATTEMPTS, channelName);
|
|
136
|
+
// Re-read coordinates from the still-loaded guide page. Wrapped in try/catch because the page might commit a pending navigation between the URL check above
|
|
137
|
+
// and this evaluate call, destroying the execution context. In that case, fall through with the previous coordinates — the next URL check will detect the
|
|
138
|
+
// navigation and return success.
|
|
139
|
+
try {
|
|
140
|
+
// eslint-disable-next-line no-await-in-loop
|
|
141
|
+
const retryResult = await readSlingChannelsAndLocate(page, normalizedName);
|
|
142
|
+
if (retryResult.clickTarget) {
|
|
143
|
+
target = retryResult.clickTarget;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Page navigated away during re-read. The next iteration's URL check will catch this.
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Use a shorter timeout on non-final attempts to fail fast. The final attempt gets the full navigationTimeout as a last-ditch effort.
|
|
151
|
+
const timeout = (attempt < (MAX_CLICK_ATTEMPTS - 1)) ? CLICK_RETRY_TIMEOUT : CONFIG.streaming.navigationTimeout;
|
|
152
|
+
// eslint-disable-next-line no-await-in-loop
|
|
153
|
+
lastResult = await clickSlingOnNowAndWaitForNavigation(page, target, timeout);
|
|
154
|
+
if (lastResult.success) {
|
|
155
|
+
return lastResult;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return lastResult;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Sling TV grid strategy: finds a channel in the virtualized, alphabetically sorted guide grid at watch.sling.com/dashboard/grid_guide/grid_guide_a_z using binary
|
|
162
|
+
* search on the .guide-cell scroll container, then clicks the on-now program cell to navigate to the player page. The guide renders ~8-10 of ~638 rows at a time,
|
|
163
|
+
* each 120px tall, sorted A-Z by channel name. Channel identification uses data-testid="channel-{NAME}" attributes.
|
|
164
|
+
*
|
|
165
|
+
* The selection process:
|
|
166
|
+
* 1. Wait for channel entries to appear in the DOM (confirms guide grid has loaded)
|
|
167
|
+
* 2. Read grid metadata: locate the .guide-cell scroll host and compute total rows from scrollHeight / 120
|
|
168
|
+
* 3. Check the slingRowCache for a direct-scroll shortcut from a previous tune
|
|
169
|
+
* 4. Binary search: scroll .guide-cell to the midpoint row, read rendered channels, compare alphabetically to adjust bounds
|
|
170
|
+
* 5. Click the on-now program cell via clickWithRetry() — retries up to 3 times on navigation timeout before giving up
|
|
171
|
+
* @param page - The Puppeteer page object.
|
|
172
|
+
* @param profile - The resolved site profile with a non-null channelSelector (channel name).
|
|
173
|
+
* @returns Result object with success status and optional failure reason.
|
|
174
|
+
*/
|
|
175
|
+
export async function slingGridStrategy(page, profile) {
|
|
176
|
+
const channelName = profile.channelSelector;
|
|
177
|
+
// Phase 1: Wait for the guide grid to render. Channel entries appear as data-testid="channel-{NAME}" elements within the virtualized list.
|
|
178
|
+
try {
|
|
179
|
+
await page.waitForSelector("[data-testid^=\"channel-\"]", { timeout: 5000, visible: true });
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return { reason: "Sling TV guide grid did not load.", success: false };
|
|
183
|
+
}
|
|
184
|
+
// Phase 2: Read grid metadata. The .guide-cell element is the scroll host for the virtualized channel list. Each row is 120px tall with a 30px offset for the
|
|
185
|
+
// time header row at the top of the grid.
|
|
186
|
+
const ROW_HEIGHT = 120;
|
|
187
|
+
const ROW_OFFSET = 30;
|
|
188
|
+
const gridMeta = await page.evaluate((rowHeight) => {
|
|
189
|
+
const guideCell = document.querySelector(".guide-cell");
|
|
190
|
+
if (!guideCell) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const totalRows = Math.round(guideCell.scrollHeight / rowHeight);
|
|
194
|
+
if (totalRows <= 0) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
return { totalRows };
|
|
198
|
+
}, ROW_HEIGHT);
|
|
199
|
+
if (!gridMeta) {
|
|
200
|
+
return { reason: "Could not locate Sling TV guide grid scroll container.", success: false };
|
|
201
|
+
}
|
|
202
|
+
const { totalRows } = gridMeta;
|
|
203
|
+
const normalizedName = normalizeChannelName(channelName);
|
|
204
|
+
// Helper: scroll the .guide-cell container to a specific row index and wait for the virtualizer to render.
|
|
205
|
+
const scrollToRow = async (rowIndex) => {
|
|
206
|
+
await page.evaluate((scrollTo) => {
|
|
207
|
+
const guideCell = document.querySelector(".guide-cell");
|
|
208
|
+
if (guideCell) {
|
|
209
|
+
guideCell.scrollTop = scrollTo;
|
|
210
|
+
}
|
|
211
|
+
}, ROW_OFFSET + (rowIndex * ROW_HEIGHT));
|
|
212
|
+
await delay(200);
|
|
213
|
+
};
|
|
214
|
+
// Phase 3: Check the cache for a direct-scroll shortcut. If we've tuned to this channel before, skip binary search and scroll directly.
|
|
215
|
+
const cachedRow = slingRowCache.get(normalizedName);
|
|
216
|
+
if (cachedRow !== undefined) {
|
|
217
|
+
LOG.debug("tuning:sling", "Sling cache hit for %s at row %s.", channelName, cachedRow);
|
|
218
|
+
await scrollToRow(cachedRow);
|
|
219
|
+
const { channels, clickTarget } = await readSlingChannelsAndLocate(page, normalizedName);
|
|
220
|
+
if (channels && clickTarget) {
|
|
221
|
+
return await clickWithRetry(page, clickTarget, normalizedName, channelName);
|
|
222
|
+
}
|
|
223
|
+
// Cache hit but channel not found at expected position. Clear this entry and fall through to binary search.
|
|
224
|
+
LOG.debug("tuning:sling", "Sling cache miss for %s. Falling back to binary search.", channelName);
|
|
225
|
+
slingRowCache.delete(normalizedName);
|
|
226
|
+
}
|
|
227
|
+
// Phase 4: Binary search through the virtualized channel list. On each iteration we scroll to the midpoint, read rendered channels (with click target resolution
|
|
228
|
+
// for the target name), and either click immediately on match or compare alphabetically to adjust bounds. The combined readSlingChannelsAndLocate call returns
|
|
229
|
+
// both the channel list (for direction) and the on-now cell coordinates (for clicking) in a single browser round-trip.
|
|
230
|
+
let low = 0;
|
|
231
|
+
let high = totalRows - 1;
|
|
232
|
+
const maxIterations = 12;
|
|
233
|
+
let foundClickTarget = null;
|
|
234
|
+
let foundMatchedName = null;
|
|
235
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
236
|
+
if (low > high) {
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
const mid = Math.floor((low + high) / 2);
|
|
240
|
+
// eslint-disable-next-line no-await-in-loop
|
|
241
|
+
await scrollToRow(mid);
|
|
242
|
+
// eslint-disable-next-line no-await-in-loop
|
|
243
|
+
const { channels, clickTarget, matchedName } = await readSlingChannelsAndLocate(page, normalizedName);
|
|
244
|
+
if (!channels || (channels.length === 0)) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
// If the target was found, the click coordinates are already resolved. No second evaluate needed.
|
|
248
|
+
if (clickTarget) {
|
|
249
|
+
foundClickTarget = clickTarget;
|
|
250
|
+
foundMatchedName = matchedName;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
// Sort by name so first/last reflect alphabetical extremes. querySelectorAll returns DOM insertion order, which may not match visual order in a virtualizer
|
|
254
|
+
// that recycles elements by appending new rows rather than inserting in visual position.
|
|
255
|
+
channels.sort((a, b) => a.name.localeCompare(b.name));
|
|
256
|
+
// Determine binary search direction by comparing against the first and last rendered channel names.
|
|
257
|
+
const first = channels[0].name;
|
|
258
|
+
const last = channels[channels.length - 1].name;
|
|
259
|
+
if (normalizedName.localeCompare(first) < 0) {
|
|
260
|
+
// Target sorts before the first rendered channel. Scroll up.
|
|
261
|
+
high = mid - 1;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (normalizedName.localeCompare(last) > 0) {
|
|
265
|
+
// Target sorts after the last rendered channel. Scroll down.
|
|
266
|
+
low = mid + 1;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
// Target is between the first and last rendered channels but no exact match. The channel may not exist in the guide.
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
if (!foundClickTarget) {
|
|
273
|
+
return { reason: "Could not find channel " + channelName + " in Sling TV guide grid.", success: false };
|
|
274
|
+
}
|
|
275
|
+
// When a local affiliate was matched via prefix, cache the network name as an alias so subsequent tunes skip binary search and scroll directly to the
|
|
276
|
+
// affiliate's row.
|
|
277
|
+
if (foundMatchedName && (foundMatchedName !== normalizedName)) {
|
|
278
|
+
const affiliateRow = slingRowCache.get(foundMatchedName);
|
|
279
|
+
if (affiliateRow !== undefined) {
|
|
280
|
+
slingRowCache.set(normalizedName, affiliateRow);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Phase 5: Click the on-now program cell and wait for Sling to navigate to the player page. Uses the retry loop to handle transient click or navigation failures
|
|
284
|
+
// without tearing down the entire attempt and reloading the guide page.
|
|
285
|
+
return await clickWithRetry(page, foundClickTarget, normalizedName, channelName);
|
|
286
|
+
}
|
|
287
|
+
//# sourceMappingURL=sling.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sling.js","sourceRoot":"","sources":["../../../src/browser/tuning/sling.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ChannelSelectionProfile, ChannelSelectorResult } from "../../types/index.js";
|
|
2
|
+
import type { Page } from "puppeteer-core";
|
|
3
|
+
/**
|
|
4
|
+
* Thumbnail row strategy: finds a channel by matching the slug in thumbnail image URLs, then clicks an adjacent clickable element on the same row. This strategy
|
|
5
|
+
* works for sites like USA Network where channels are displayed as rows with a thumbnail on the left and program entries to the right.
|
|
6
|
+
*
|
|
7
|
+
* The selection process:
|
|
8
|
+
* 1. Search all images on the page for one whose src URL contains the channel slug
|
|
9
|
+
* 2. Verify the image has dimensions (is rendered and visible)
|
|
10
|
+
* 3. Walk up the DOM to find a container wide enough to hold both thumbnail and guide entries
|
|
11
|
+
* 4. Search for clickable elements (links, buttons, cards) to the right of the thumbnail on the same row
|
|
12
|
+
* 5. Fall back to divs with cursor:pointer if no semantic clickables found
|
|
13
|
+
* 6. Click the found element to switch to the channel
|
|
14
|
+
* @param page - The Puppeteer page object.
|
|
15
|
+
* @param profile - The resolved site profile with a non-null channelSelector (image URL slug).
|
|
16
|
+
* @returns Result object with success status and optional failure reason.
|
|
17
|
+
*/
|
|
18
|
+
export declare function thumbnailRowStrategy(page: Page, profile: ChannelSelectionProfile): Promise<ChannelSelectorResult>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { CONFIG } from "../../config/index.js";
|
|
2
|
+
import { evaluateWithAbort } from "../../utils/index.js";
|
|
3
|
+
import { scrollAndClick } from "../channelSelection.js";
|
|
4
|
+
/**
|
|
5
|
+
* Thumbnail row strategy: finds a channel by matching the slug in thumbnail image URLs, then clicks an adjacent clickable element on the same row. This strategy
|
|
6
|
+
* works for sites like USA Network where channels are displayed as rows with a thumbnail on the left and program entries to the right.
|
|
7
|
+
*
|
|
8
|
+
* The selection process:
|
|
9
|
+
* 1. Search all images on the page for one whose src URL contains the channel slug
|
|
10
|
+
* 2. Verify the image has dimensions (is rendered and visible)
|
|
11
|
+
* 3. Walk up the DOM to find a container wide enough to hold both thumbnail and guide entries
|
|
12
|
+
* 4. Search for clickable elements (links, buttons, cards) to the right of the thumbnail on the same row
|
|
13
|
+
* 5. Fall back to divs with cursor:pointer if no semantic clickables found
|
|
14
|
+
* 6. Click the found element to switch to the channel
|
|
15
|
+
* @param page - The Puppeteer page object.
|
|
16
|
+
* @param profile - The resolved site profile with a non-null channelSelector (image URL slug).
|
|
17
|
+
* @returns Result object with success status and optional failure reason.
|
|
18
|
+
*/
|
|
19
|
+
export async function thumbnailRowStrategy(page, profile) {
|
|
20
|
+
const channelSlug = profile.channelSelector;
|
|
21
|
+
// Find clickable element by evaluating DOM. The logic walks through the page looking for channel thumbnail images, then finds clickable show entries on the
|
|
22
|
+
// same row.
|
|
23
|
+
const clickTarget = await evaluateWithAbort(page, (slug) => {
|
|
24
|
+
const images = document.querySelectorAll("img");
|
|
25
|
+
for (const img of Array.from(images)) {
|
|
26
|
+
// Channel thumbnails have URLs containing the channel slug pattern. Match against the src URL.
|
|
27
|
+
if (img.src.includes(slug)) {
|
|
28
|
+
const imgRect = img.getBoundingClientRect();
|
|
29
|
+
// Verify the image has dimensions (is actually rendered and visible).
|
|
30
|
+
if ((imgRect.width > 0) && (imgRect.height > 0)) {
|
|
31
|
+
// Found the channel thumbnail. Now walk up the DOM tree to find a container that holds both the thumbnail and the guide entries for this row.
|
|
32
|
+
let rowContainer = img.parentElement;
|
|
33
|
+
while (rowContainer && (rowContainer !== document.body)) {
|
|
34
|
+
const containerRect = rowContainer.getBoundingClientRect();
|
|
35
|
+
// Look for a container significantly wider than the thumbnail (indicating it contains more than just the image). The factor of 2 is a heuristic
|
|
36
|
+
// that works for typical channel guide layouts.
|
|
37
|
+
if (containerRect.width > (imgRect.width * 2)) {
|
|
38
|
+
// This container is wide enough to contain guide entries. Search for clickable elements (show cards) to the right of the thumbnail.
|
|
39
|
+
const clickables = rowContainer.querySelectorAll("a, button, [role=\"button\"], [onclick], [class*=\"card\"], [class*=\"program\"], [class*=\"show\"], [class*=\"episode\"]");
|
|
40
|
+
const imgCenterY = imgRect.y + (imgRect.height / 2);
|
|
41
|
+
for (const clickable of Array.from(clickables)) {
|
|
42
|
+
const clickRect = clickable.getBoundingClientRect();
|
|
43
|
+
const clickCenterY = clickRect.y + (clickRect.height / 2);
|
|
44
|
+
// The guide entry must meet these criteria:
|
|
45
|
+
// - To the right of the thumbnail (with small tolerance for overlapping borders)
|
|
46
|
+
// - Has dimensions (is visible)
|
|
47
|
+
// - On the same row (vertical center within thumbnail height)
|
|
48
|
+
const isRightOfThumbnail = clickRect.x > (imgRect.x + imgRect.width - 10);
|
|
49
|
+
const hasDimensions = (clickRect.width > 0) && (clickRect.height > 0);
|
|
50
|
+
const isSameRow = Math.abs(clickCenterY - imgCenterY) < imgRect.height;
|
|
51
|
+
if (isRightOfThumbnail && hasDimensions && isSameRow) {
|
|
52
|
+
// Found a suitable click target. Scroll it into view and return its center coordinates.
|
|
53
|
+
clickable.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
|
|
54
|
+
const newRect = clickable.getBoundingClientRect();
|
|
55
|
+
return { x: newRect.x + (newRect.width / 2), y: newRect.y + (newRect.height / 2) };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Fallback: if no semantically clickable elements found, look for divs with cursor: pointer styling. These are often custom-styled click
|
|
59
|
+
// handlers.
|
|
60
|
+
const allDivs = rowContainer.querySelectorAll("div");
|
|
61
|
+
for (const div of Array.from(allDivs)) {
|
|
62
|
+
const divRect = div.getBoundingClientRect();
|
|
63
|
+
const divCenterY = divRect.y + (divRect.height / 2);
|
|
64
|
+
const style = window.getComputedStyle(div);
|
|
65
|
+
const isRightOfThumbnail = divRect.x > (imgRect.x + imgRect.width - 10);
|
|
66
|
+
const hasDimensions = (divRect.width > 20) && (divRect.height > 20);
|
|
67
|
+
const isClickable = style.cursor === "pointer";
|
|
68
|
+
const isSameRow = Math.abs(divCenterY - imgCenterY) < imgRect.height;
|
|
69
|
+
if (isRightOfThumbnail && hasDimensions && isClickable && isSameRow) {
|
|
70
|
+
div.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
|
|
71
|
+
const newRect = div.getBoundingClientRect();
|
|
72
|
+
return { x: newRect.x + (newRect.width / 2), y: newRect.y + (newRect.height / 2) };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
rowContainer = rowContainer.parentElement;
|
|
77
|
+
}
|
|
78
|
+
// Ultimate fallback: click a fixed offset to the right of the thumbnail. This is a last resort if the guide structure doesn't match our
|
|
79
|
+
// expectations.
|
|
80
|
+
img.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
|
|
81
|
+
const newImgRect = img.getBoundingClientRect();
|
|
82
|
+
return { x: newImgRect.x + newImgRect.width + 50, y: newImgRect.y + (newImgRect.height / 2) };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Channel thumbnail not found in any images.
|
|
87
|
+
return null;
|
|
88
|
+
}, [channelSlug]);
|
|
89
|
+
if (clickTarget) {
|
|
90
|
+
await scrollAndClick(page, clickTarget);
|
|
91
|
+
// Poll for the video readyState to drop below 3, indicating the channel switch has started loading new content. This replaces a fixed post-click delay with
|
|
92
|
+
// early exit. If no video exists yet or readyState never drops (channel already selected), the timeout expires harmlessly and waitForVideoReady() handles the
|
|
93
|
+
// rest.
|
|
94
|
+
try {
|
|
95
|
+
await page.waitForFunction(() => {
|
|
96
|
+
const v = document.querySelector("video");
|
|
97
|
+
return !v || (v.readyState < 3);
|
|
98
|
+
}, { timeout: CONFIG.playback.channelSwitchDelay });
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Timeout — readyState never dropped. Proceed normally.
|
|
102
|
+
}
|
|
103
|
+
return { success: true };
|
|
104
|
+
}
|
|
105
|
+
return { reason: "Channel thumbnail not found in page images.", success: false };
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=thumbnailRow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thumbnailRow.js","sourceRoot":"","sources":["../../../src/browser/tuning/thumbnailRow.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAE/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,IAAU,EAAE,OAAgC;IAErF,MAAM,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC;IAE5C,4JAA4J;IAC5J,YAAY;IACZ,MAAM,WAAW,GAAG,MAAM,iBAAiB,CAAC,IAAI,EAAE,CAAC,IAAY,EAAyB,EAAE;QAExF,MAAM,MAAM,GAAG,QAAQ,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAEhD,KAAI,MAAM,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YAEpC,+FAA+F;YAC/F,IAAG,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;gBAE1B,MAAM,OAAO,GAAG,GAAG,CAAC,qBAAqB,EAAE,CAAC;gBAE5C,sEAAsE;gBACtE,IAAG,CAAC,OAAO,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;oBAE/C,8IAA8I;oBAC9I,IAAI,YAAY,GAA0B,GAAG,CAAC,aAAa,CAAC;oBAE5D,OAAM,YAAY,IAAI,CAAC,YAAY,KAAK,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;wBAEvD,MAAM,aAAa,GAAG,YAAY,CAAC,qBAAqB,EAAE,CAAC;wBAE3D,gJAAgJ;wBAChJ,gDAAgD;wBAChD,IAAG,aAAa,CAAC,KAAK,GAAG,CAAC,OAAO,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC;4BAE7C,oIAAoI;4BACpI,MAAM,UAAU,GAAG,YAAY,CAAC,gBAAgB,CAC9C,2HAA2H,CAC5H,CAAC;4BAEF,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;4BAEpD,KAAI,MAAM,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;gCAE9C,MAAM,SAAS,GAAG,SAAS,CAAC,qBAAqB,EAAE,CAAC;gCACpD,MAAM,YAAY,GAAG,SAAS,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gCAE1D,4CAA4C;gCAC5C,iFAAiF;gCACjF,gCAAgC;gCAChC,8DAA8D;gCAC9D,MAAM,kBAAkB,GAAG,SAAS,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;gCAC1E,MAAM,aAAa,GAAG,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gCACtE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,GAAG,UAAU,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;gCAEvE,IAAG,kBAAkB,IAAI,aAAa,IAAI,SAAS,EAAE,CAAC;oCAEpD,wFAAwF;oCACvF,SAAyB,CAAC,cAAc,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;oCAEtG,MAAM,OAAO,GAAG,SAAS,CAAC,qBAAqB,EAAE,CAAC;oCAElD,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;gCACrF,CAAC;4BACH,CAAC;4BAED,yIAAyI;4BACzI,YAAY;4BACZ,MAAM,OAAO,GAAG,YAAY,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;4BAErD,KAAI,MAAM,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;gCAErC,MAAM,OAAO,GAAG,GAAG,CAAC,qBAAqB,EAAE,CAAC;gCAC5C,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gCACpD,MAAM,KAAK,GAAG,MAAM,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;gCAE3C,MAAM,kBAAkB,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;gCACxE,MAAM,aAAa,GAAG,CAAC,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;gCACpE,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC;gCAC/C,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,GAAG,UAAU,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;gCAErE,IAAG,kBAAkB,IAAI,aAAa,IAAI,WAAW,IAAI,SAAS,EAAE,CAAC;oCAEnE,GAAG,CAAC,cAAc,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;oCAE/E,MAAM,OAAO,GAAG,GAAG,CAAC,qBAAqB,EAAE,CAAC;oCAE5C,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;gCACrF,CAAC;4BACH,CAAC;wBACH,CAAC;wBAED,YAAY,GAAG,YAAY,CAAC,aAAa,CAAC;oBAC5C,CAAC;oBAED,wIAAwI;oBACxI,gBAAgB;oBAChB,GAAG,CAAC,cAAc,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;oBAE/E,MAAM,UAAU,GAAG,GAAG,CAAC,qBAAqB,EAAE,CAAC;oBAE/C,OAAO,EAAE,CAAC,EAAE,UAAU,CAAC,CAAC,GAAG,UAAU,CAAC,KAAK,GAAG,EAAE,EAAE,CAAC,EAAE,UAAU,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;gBAChG,CAAC;YACH,CAAC;QACH,CAAC;QAED,6CAA6C;QAC7C,OAAO,IAAI,CAAC;IACd,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;IAElB,IAAG,WAAW,EAAE,CAAC;QAEf,MAAM,cAAc,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QAExC,4JAA4J;QAC5J,8JAA8J;QAC9J,QAAQ;QACR,IAAI,CAAC;YAEH,MAAM,IAAI,CAAC,eAAe,CACxB,GAAY,EAAE;gBAEZ,MAAM,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;gBAE1C,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC;YAClC,CAAC,EACD,EAAE,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,kBAAkB,EAAE,CAChD,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YAEP,wDAAwD;QAC1D,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,6CAA6C,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AACnF,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ChannelSelectionProfile, ChannelSelectorResult } from "../../types/index.js";
|
|
2
|
+
import type { Page } from "puppeteer-core";
|
|
3
|
+
/**
|
|
4
|
+
* Tile click strategy: finds a channel by matching the slug in tile image URLs, clicks the tile to open an entity modal, then clicks a "watch live" play button on
|
|
5
|
+
* the modal. This strategy works for sites like Disney+ where live channels are displayed as tiles in a horizontal shelf, and selecting one opens a modal with a
|
|
6
|
+
* play button to start the live stream.
|
|
7
|
+
*
|
|
8
|
+
* The selection process:
|
|
9
|
+
* 1. Search all images on the page for one whose src URL contains the channel slug
|
|
10
|
+
* 2. Walk up the DOM to find the nearest clickable ancestor (the tile container)
|
|
11
|
+
* 3. Scroll the tile into view and click it
|
|
12
|
+
* 4. Wait for the play button to appear on the resulting modal
|
|
13
|
+
* 5. Click the play button to start live playback
|
|
14
|
+
* @param page - The Puppeteer page object.
|
|
15
|
+
* @param profile - The resolved site profile with a non-null channelSelector (image URL slug).
|
|
16
|
+
* @returns Result object with success status and optional failure reason.
|
|
17
|
+
*/
|
|
18
|
+
export declare function tileClickStrategy(page: Page, profile: ChannelSelectionProfile): Promise<ChannelSelectorResult>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { CONFIG } from "../../config/index.js";
|
|
2
|
+
import { evaluateWithAbort } from "../../utils/index.js";
|
|
3
|
+
import { scrollAndClick } from "../channelSelection.js";
|
|
4
|
+
/**
|
|
5
|
+
* Tile click strategy: finds a channel by matching the slug in tile image URLs, clicks the tile to open an entity modal, then clicks a "watch live" play button on
|
|
6
|
+
* the modal. This strategy works for sites like Disney+ where live channels are displayed as tiles in a horizontal shelf, and selecting one opens a modal with a
|
|
7
|
+
* play button to start the live stream.
|
|
8
|
+
*
|
|
9
|
+
* The selection process:
|
|
10
|
+
* 1. Search all images on the page for one whose src URL contains the channel slug
|
|
11
|
+
* 2. Walk up the DOM to find the nearest clickable ancestor (the tile container)
|
|
12
|
+
* 3. Scroll the tile into view and click it
|
|
13
|
+
* 4. Wait for the play button to appear on the resulting modal
|
|
14
|
+
* 5. Click the play button to start live playback
|
|
15
|
+
* @param page - The Puppeteer page object.
|
|
16
|
+
* @param profile - The resolved site profile with a non-null channelSelector (image URL slug).
|
|
17
|
+
* @returns Result object with success status and optional failure reason.
|
|
18
|
+
*/
|
|
19
|
+
export async function tileClickStrategy(page, profile) {
|
|
20
|
+
const channelSlug = profile.channelSelector;
|
|
21
|
+
// Step 1: Find the channel tile by matching the slug in a descendant image's src URL. Live channels are displayed as tiles in a horizontal shelf, each containing
|
|
22
|
+
// an image with the network name in the URL label parameter (e.g., "poster_linear_espn_none"). We match the image, then walk up the DOM to find the nearest
|
|
23
|
+
// clickable ancestor that represents the entire tile.
|
|
24
|
+
const tileTarget = await evaluateWithAbort(page, (slug) => {
|
|
25
|
+
const images = document.querySelectorAll("img");
|
|
26
|
+
for (const img of Array.from(images)) {
|
|
27
|
+
if (img.src.includes(slug)) {
|
|
28
|
+
const imgRect = img.getBoundingClientRect();
|
|
29
|
+
// Verify the image has dimensions (is actually rendered and visible). This matches the pattern in thumbnailRowStrategy and provides defense-in-depth if the
|
|
30
|
+
// wait phase timed out before the image fully loaded.
|
|
31
|
+
if ((imgRect.width > 0) && (imgRect.height > 0)) {
|
|
32
|
+
// Walk up the DOM to find the nearest clickable ancestor wrapping the tile. Check for semantic clickable elements (<a>, <button>, role="button") and
|
|
33
|
+
// elements with explicit click handlers first. Track cursor:pointer elements as a fallback for sites using custom click handlers without semantic markup.
|
|
34
|
+
let ancestor = img.parentElement;
|
|
35
|
+
let pointerFallback = null;
|
|
36
|
+
while (ancestor && (ancestor !== document.body)) {
|
|
37
|
+
const tag = ancestor.tagName;
|
|
38
|
+
// Semantic clickable elements are the most reliable indicators of an interactive tile container.
|
|
39
|
+
if ((tag === "A") || (tag === "BUTTON") || (ancestor.getAttribute("role") === "button") || ancestor.hasAttribute("onclick")) {
|
|
40
|
+
ancestor.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
|
|
41
|
+
const rect = ancestor.getBoundingClientRect();
|
|
42
|
+
if ((rect.width > 0) && (rect.height > 0)) {
|
|
43
|
+
return { x: rect.x + (rect.width / 2), y: rect.y + (rect.height / 2) };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Track the nearest cursor:pointer ancestor with reasonable dimensions as a fallback.
|
|
47
|
+
if (!pointerFallback) {
|
|
48
|
+
const rect = ancestor.getBoundingClientRect();
|
|
49
|
+
if ((rect.width > 20) && (rect.height > 20) && (window.getComputedStyle(ancestor).cursor === "pointer")) {
|
|
50
|
+
pointerFallback = ancestor;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
ancestor = ancestor.parentElement;
|
|
54
|
+
}
|
|
55
|
+
// Fallback: use cursor:pointer ancestor if no semantic clickable was found above.
|
|
56
|
+
if (pointerFallback) {
|
|
57
|
+
pointerFallback.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
|
|
58
|
+
const rect = pointerFallback.getBoundingClientRect();
|
|
59
|
+
if ((rect.width > 0) && (rect.height > 0)) {
|
|
60
|
+
return { x: rect.x + (rect.width / 2), y: rect.y + (rect.height / 2) };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}, [channelSlug]);
|
|
68
|
+
if (!tileTarget) {
|
|
69
|
+
return { reason: "Channel tile not found in page images.", success: false };
|
|
70
|
+
}
|
|
71
|
+
// Click the channel tile to open the entity modal.
|
|
72
|
+
await scrollAndClick(page, tileTarget);
|
|
73
|
+
// Step 2: Wait for the "WATCH LIVE" button to appear on the entity modal. The button is an <a> element with a specific data-testid attribute. After clicking the
|
|
74
|
+
// tile, the site performs a SPA navigation that renders a modal with playback options.
|
|
75
|
+
const playButtonSelector = "[data-testid=\"live-modal-watch-live-action-button\"]";
|
|
76
|
+
try {
|
|
77
|
+
await page.waitForSelector(playButtonSelector, { timeout: CONFIG.streaming.videoTimeout });
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return { reason: "Play button did not appear after clicking channel tile.", success: false };
|
|
81
|
+
}
|
|
82
|
+
// Get the play button coordinates for clicking.
|
|
83
|
+
const playTarget = await evaluateWithAbort(page, (selector) => {
|
|
84
|
+
const button = document.querySelector(selector);
|
|
85
|
+
if (!button) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
button.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
|
|
89
|
+
const rect = button.getBoundingClientRect();
|
|
90
|
+
if ((rect.width > 0) && (rect.height > 0)) {
|
|
91
|
+
return { x: rect.x + (rect.width / 2), y: rect.y + (rect.height / 2) };
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}, [playButtonSelector]);
|
|
95
|
+
if (!playTarget) {
|
|
96
|
+
return { reason: "Play button found but has no dimensions.", success: false };
|
|
97
|
+
}
|
|
98
|
+
// Click the play button to start live playback.
|
|
99
|
+
await scrollAndClick(page, playTarget);
|
|
100
|
+
return { success: true };
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=tileClick.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tileClick.js","sourceRoot":"","sources":["../../../src/browser/tuning/tileClick.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ChannelSelectionProfile, ChannelSelectorResult } from "../../types/index.js";
|
|
2
|
+
import type { Page } from "puppeteer-core";
|
|
3
|
+
/**
|
|
4
|
+
* YouTube TV grid strategy: finds a channel in the non-virtualized EPG grid at tv.youtube.com/live by querying the aria-label attribute on thumbnail endpoint
|
|
5
|
+
* elements. All ~256 channel rows are present in the DOM simultaneously, so a single querySelector locates the target channel. The strategy extracts the watch
|
|
6
|
+
* URL from the matching anchor element and navigates directly — no scrolling, clicking, or timing workarounds needed.
|
|
7
|
+
*
|
|
8
|
+
* The selection process:
|
|
9
|
+
* 1. Wait for ytu-epg-row elements to confirm the guide grid has loaded.
|
|
10
|
+
* 2. Find the target channel using a case-insensitive aria-label CSS selector.
|
|
11
|
+
* 3. Extract the href attribute and validate it starts with "watch/" (not "live" or "browse/").
|
|
12
|
+
* 4. Navigate to the full watch URL via page.goto().
|
|
13
|
+
* @param page - The Puppeteer page object.
|
|
14
|
+
* @param profile - The resolved site profile with a non-null channelSelector (channel name, e.g., "CNN", "ESPN", "NBC").
|
|
15
|
+
* @returns Result object with success status and optional failure reason.
|
|
16
|
+
*/
|
|
17
|
+
export declare function youtubeGridStrategy(page: Page, profile: ChannelSelectionProfile): Promise<ChannelSelectorResult>;
|