@bnhf/prismcast 1.3.4-2026.2.19
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/LICENSE.md +7 -0
- package/README.md +347 -0
- package/bin/prismcast +6 -0
- package/dist/app.d.ts +6 -0
- package/dist/app.js +315 -0
- package/dist/app.js.map +1 -0
- package/dist/browser/cdp.d.ts +38 -0
- package/dist/browser/cdp.js +155 -0
- package/dist/browser/cdp.js.map +1 -0
- package/dist/browser/channelSelection.d.ts +65 -0
- package/dist/browser/channelSelection.js +202 -0
- package/dist/browser/channelSelection.js.map +1 -0
- package/dist/browser/display.d.ts +34 -0
- package/dist/browser/display.js +54 -0
- package/dist/browser/display.js.map +1 -0
- package/dist/browser/index.d.ts +205 -0
- package/dist/browser/index.js +1205 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/tuning/fox.d.ts +2 -0
- package/dist/browser/tuning/fox.js +83 -0
- package/dist/browser/tuning/fox.js.map +1 -0
- package/dist/browser/tuning/hbo.d.ts +2 -0
- package/dist/browser/tuning/hbo.js +237 -0
- package/dist/browser/tuning/hbo.js.map +1 -0
- package/dist/browser/tuning/hulu.d.ts +2 -0
- package/dist/browser/tuning/hulu.js +550 -0
- package/dist/browser/tuning/hulu.js.map +1 -0
- package/dist/browser/tuning/sling.d.ts +2 -0
- package/dist/browser/tuning/sling.js +518 -0
- package/dist/browser/tuning/sling.js.map +1 -0
- package/dist/browser/tuning/thumbnailRow.d.ts +2 -0
- package/dist/browser/tuning/thumbnailRow.js +108 -0
- package/dist/browser/tuning/thumbnailRow.js.map +1 -0
- package/dist/browser/tuning/tileClick.d.ts +2 -0
- package/dist/browser/tuning/tileClick.js +103 -0
- package/dist/browser/tuning/tileClick.js.map +1 -0
- package/dist/browser/tuning/youtubeTv.d.ts +2 -0
- package/dist/browser/tuning/youtubeTv.js +182 -0
- package/dist/browser/tuning/youtubeTv.js.map +1 -0
- package/dist/browser/video.d.ts +289 -0
- package/dist/browser/video.js +996 -0
- package/dist/browser/video.js.map +1 -0
- package/dist/channels/index.d.ts +3 -0
- package/dist/channels/index.js +392 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/config/index.d.ts +53 -0
- package/dist/config/index.js +233 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/presets.d.ts +98 -0
- package/dist/config/presets.js +241 -0
- package/dist/config/presets.js.map +1 -0
- package/dist/config/profiles.d.ts +79 -0
- package/dist/config/profiles.js +245 -0
- package/dist/config/profiles.js.map +1 -0
- package/dist/config/providers.d.ts +120 -0
- package/dist/config/providers.js +450 -0
- package/dist/config/providers.js.map +1 -0
- package/dist/config/sites.d.ts +22 -0
- package/dist/config/sites.js +377 -0
- package/dist/config/sites.js.map +1 -0
- package/dist/config/userChannels.d.ts +178 -0
- package/dist/config/userChannels.js +543 -0
- package/dist/config/userChannels.js.map +1 -0
- package/dist/config/userConfig.d.ts +235 -0
- package/dist/config/userConfig.js +913 -0
- package/dist/config/userConfig.js.map +1 -0
- package/dist/hdhr/channelMap.d.ts +21 -0
- package/dist/hdhr/channelMap.js +82 -0
- package/dist/hdhr/channelMap.js.map +1 -0
- package/dist/hdhr/deviceId.d.ts +11 -0
- package/dist/hdhr/deviceId.js +84 -0
- package/dist/hdhr/deviceId.js.map +1 -0
- package/dist/hdhr/discover.d.ts +6 -0
- package/dist/hdhr/discover.js +155 -0
- package/dist/hdhr/discover.js.map +1 -0
- package/dist/hdhr/index.d.ts +9 -0
- package/dist/hdhr/index.js +87 -0
- package/dist/hdhr/index.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +144 -0
- package/dist/index.js.map +1 -0
- package/dist/routes/assets.d.ts +6 -0
- package/dist/routes/assets.js +79 -0
- package/dist/routes/assets.js.map +1 -0
- package/dist/routes/auth.d.ts +6 -0
- package/dist/routes/auth.js +77 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/channels.d.ts +6 -0
- package/dist/routes/channels.js +40 -0
- package/dist/routes/channels.js.map +1 -0
- package/dist/routes/components.d.ts +138 -0
- package/dist/routes/components.js +210 -0
- package/dist/routes/components.js.map +1 -0
- package/dist/routes/config.d.ts +72 -0
- package/dist/routes/config.js +1977 -0
- package/dist/routes/config.js.map +1 -0
- 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/health.d.ts +6 -0
- package/dist/routes/health.js +85 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/hls.d.ts +6 -0
- package/dist/routes/hls.js +25 -0
- package/dist/routes/hls.js.map +1 -0
- package/dist/routes/index.d.ts +19 -0
- package/dist/routes/index.js +49 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/logs.d.ts +6 -0
- package/dist/routes/logs.js +164 -0
- package/dist/routes/logs.js.map +1 -0
- package/dist/routes/mpegts.d.ts +6 -0
- package/dist/routes/mpegts.js +19 -0
- package/dist/routes/mpegts.js.map +1 -0
- package/dist/routes/play.d.ts +6 -0
- package/dist/routes/play.js +18 -0
- package/dist/routes/play.js.map +1 -0
- package/dist/routes/playlist.d.ts +36 -0
- package/dist/routes/playlist.js +134 -0
- package/dist/routes/playlist.js.map +1 -0
- package/dist/routes/root.d.ts +6 -0
- package/dist/routes/root.js +2920 -0
- package/dist/routes/root.js.map +1 -0
- package/dist/routes/streams.d.ts +6 -0
- package/dist/routes/streams.js +88 -0
- package/dist/routes/streams.js.map +1 -0
- package/dist/routes/theme.d.ts +15 -0
- package/dist/routes/theme.js +275 -0
- package/dist/routes/theme.js.map +1 -0
- package/dist/routes/ui.d.ts +56 -0
- package/dist/routes/ui.js +354 -0
- package/dist/routes/ui.js.map +1 -0
- package/dist/service/commands.d.ts +41 -0
- package/dist/service/commands.js +391 -0
- package/dist/service/commands.js.map +1 -0
- package/dist/service/generators.d.ts +33 -0
- package/dist/service/generators.js +432 -0
- package/dist/service/generators.js.map +1 -0
- package/dist/service/index.d.ts +2 -0
- package/dist/service/index.js +7 -0
- package/dist/service/index.js.map +1 -0
- package/dist/streaming/clients.d.ts +48 -0
- package/dist/streaming/clients.js +114 -0
- package/dist/streaming/clients.js.map +1 -0
- package/dist/streaming/fmp4Segmenter.d.ts +61 -0
- package/dist/streaming/fmp4Segmenter.js +461 -0
- package/dist/streaming/fmp4Segmenter.js.map +1 -0
- package/dist/streaming/hls.d.ts +120 -0
- package/dist/streaming/hls.js +722 -0
- package/dist/streaming/hls.js.map +1 -0
- package/dist/streaming/hlsSegments.d.ts +54 -0
- package/dist/streaming/hlsSegments.js +162 -0
- package/dist/streaming/hlsSegments.js.map +1 -0
- package/dist/streaming/lifecycle.d.ts +33 -0
- package/dist/streaming/lifecycle.js +185 -0
- package/dist/streaming/lifecycle.js.map +1 -0
- package/dist/streaming/monitor.d.ts +74 -0
- package/dist/streaming/monitor.js +1310 -0
- package/dist/streaming/monitor.js.map +1 -0
- package/dist/streaming/mp4Parser.d.ts +74 -0
- package/dist/streaming/mp4Parser.js +566 -0
- package/dist/streaming/mp4Parser.js.map +1 -0
- package/dist/streaming/mpegts.d.ts +14 -0
- package/dist/streaming/mpegts.js +248 -0
- package/dist/streaming/mpegts.js.map +1 -0
- package/dist/streaming/registry.d.ts +119 -0
- package/dist/streaming/registry.js +127 -0
- package/dist/streaming/registry.js.map +1 -0
- package/dist/streaming/setup.d.ts +135 -0
- package/dist/streaming/setup.js +670 -0
- package/dist/streaming/setup.js.map +1 -0
- package/dist/streaming/showInfo.d.ts +30 -0
- package/dist/streaming/showInfo.js +362 -0
- package/dist/streaming/showInfo.js.map +1 -0
- package/dist/streaming/statusEmitter.d.ts +125 -0
- package/dist/streaming/statusEmitter.js +139 -0
- package/dist/streaming/statusEmitter.js.map +1 -0
- package/dist/types/index.d.ts +403 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- 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/delay.d.ts +6 -0
- package/dist/utils/delay.js +15 -0
- package/dist/utils/delay.js.map +1 -0
- package/dist/utils/errors.d.ts +15 -0
- package/dist/utils/errors.js +40 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/evaluate.d.ts +51 -0
- package/dist/utils/evaluate.js +124 -0
- package/dist/utils/evaluate.js.map +1 -0
- package/dist/utils/ffmpeg.d.ts +65 -0
- package/dist/utils/ffmpeg.js +317 -0
- package/dist/utils/ffmpeg.js.map +1 -0
- package/dist/utils/fileLogger.d.ts +25 -0
- package/dist/utils/fileLogger.js +248 -0
- package/dist/utils/fileLogger.js.map +1 -0
- package/dist/utils/format.d.ts +16 -0
- package/dist/utils/format.js +46 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/html.d.ts +6 -0
- package/dist/utils/html.js +24 -0
- package/dist/utils/html.js.map +1 -0
- package/dist/utils/index.d.ts +15 -0
- package/dist/utils/index.js +20 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logEmitter.d.ts +17 -0
- package/dist/utils/logEmitter.js +30 -0
- package/dist/utils/logEmitter.js.map +1 -0
- package/dist/utils/logger.d.ts +82 -0
- package/dist/utils/logger.js +219 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/m3u.d.ts +32 -0
- package/dist/utils/m3u.js +148 -0
- package/dist/utils/m3u.js.map +1 -0
- package/dist/utils/morganStream.d.ts +7 -0
- package/dist/utils/morganStream.js +33 -0
- package/dist/utils/morganStream.js.map +1 -0
- package/dist/utils/platform.d.ts +64 -0
- package/dist/utils/platform.js +157 -0
- package/dist/utils/platform.js.map +1 -0
- package/dist/utils/retry.d.ts +15 -0
- package/dist/utils/retry.js +82 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/streamContext.d.ts +28 -0
- package/dist/utils/streamContext.js +33 -0
- package/dist/utils/streamContext.js.map +1 -0
- package/dist/utils/version.d.ts +37 -0
- package/dist/utils/version.js +228 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +92 -0
- package/prismcast.png +0 -0
- package/prismcast.svg +74 -0
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
async function thumbnailRowStrategyFn(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
|
+
export const thumbnailRowStrategy = { execute: thumbnailRowStrategyFn, usesImageSlug: true };
|
|
108
|
+
//# sourceMappingURL=thumbnailRow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thumbnailRow.js","sourceRoot":"","sources":["../../../src/browser/tuning/thumbnailRow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
async function tileClickStrategyFn(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
|
+
export const tileClickStrategy = { execute: tileClickStrategyFn, usesImageSlug: true };
|
|
103
|
+
//# 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,182 @@
|
|
|
1
|
+
import { LOG, evaluateWithAbort, formatError } from "../../utils/index.js";
|
|
2
|
+
import { CONFIG } from "../../config/index.js";
|
|
3
|
+
import { logAvailableChannels } from "../channelSelection.js";
|
|
4
|
+
// Base URL for YouTube TV watch page navigation.
|
|
5
|
+
const YOUTUBE_TV_BASE_URL = "https://tv.youtube.com";
|
|
6
|
+
// Module-level cache for watch URLs discovered during channel selection. On the first tune to any YTTV channel, the strategy performs a bulk scrape of all ~256
|
|
7
|
+
// channels in the non-virtualized EPG grid, populating this cache with every channel's watch URL keyed by its lowercased guide name (e.g., "cnn", "nbc 5", "wgn").
|
|
8
|
+
// Subsequent tunes to any YTTV channel resolve via findWatchUrl() in resolveDirectUrl, skipping guide navigation entirely. Cleared on browser disconnect via
|
|
9
|
+
// clearYttvCache().
|
|
10
|
+
const yttvWatchUrlCache = new Map();
|
|
11
|
+
// Known alternate channel names for affiliates that vary by market. CW appears as "WGN" in some markets. PBS affiliates appear under local call letters (e.g.,
|
|
12
|
+
// WTTW, KQED) or branded names (e.g., "Cascade PBS", "Lakeshore PBS") rather than "PBS", so we list the major market call letters and branded names to cover most
|
|
13
|
+
// users. Each alternate is tried after the primary name fails both exact and prefix+digit matching. Users in smaller markets override via custom channel entries with
|
|
14
|
+
// their local call letters as the channelSelector.
|
|
15
|
+
const CHANNEL_ALTERNATES = {
|
|
16
|
+
"cw": ["WGN"],
|
|
17
|
+
"pbs": [
|
|
18
|
+
"Cascade PBS", "GBH", "KAET", "KBTC", "KCET", "KCTS", "KERA", "KLCS", "KOCE", "KPBS", "KQED", "KRMA", "KUHT", "KVIE", "Lakeshore PBS", "MPT", "NJ PBS",
|
|
19
|
+
"THIRTEEN", "TPT", "WETA", "WGBH", "WHYY", "WLIW", "WNED", "WNET", "WNIT", "WPBA", "WPBT", "WTTW", "WTVS", "WXEL"
|
|
20
|
+
]
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Looks up a watch URL in the cache using the same three-tier matching logic as the guide grid DOM query. The tiers are tried in order for each name in the
|
|
24
|
+
* candidate list (primary channelSelector first, then any CHANNEL_ALTERNATES):
|
|
25
|
+
*
|
|
26
|
+
* 1. Exact match: cache key equals the lowercased name (e.g., "cnn" matches "cnn").
|
|
27
|
+
* 2. Prefix+digit: cache key starts with the name followed by a space and a digit. Catches local affiliates displayed as "{Network} {Number}" (e.g., "nbc 5",
|
|
28
|
+
* "abc 7") while excluding unrelated channels (e.g., "nbc sports chicago").
|
|
29
|
+
* 3. Parenthetical suffix: cache key starts with the name followed by " (". Catches timezone/region variants like "magnolia network (pacific)".
|
|
30
|
+
*
|
|
31
|
+
* When a non-exact match succeeds, the result is also cached under the primary channelSelector key for O(1) lookup on subsequent calls. This function doubles as
|
|
32
|
+
* the resolveDirectUrl hook — after the first tune populates the cache via bulk scrape, every subsequent YTTV tune resolves here without loading the guide page.
|
|
33
|
+
* @param channelName - The channelSelector value (e.g., "CNN", "NBC", "CW").
|
|
34
|
+
* @returns The full watch URL or null if no match is found.
|
|
35
|
+
*/
|
|
36
|
+
function findWatchUrl(channelName) {
|
|
37
|
+
const lower = channelName.toLowerCase();
|
|
38
|
+
// Build the candidate list: primary name first, then any known alternates for markets where the affiliate uses a different name. The eslint disable is needed
|
|
39
|
+
// because TypeScript's Record indexing doesn't capture that the key may not exist at runtime.
|
|
40
|
+
const alternates = CHANNEL_ALTERNATES[lower];
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
42
|
+
const namesToTry = alternates ? [lower, ...alternates.map((a) => a.toLowerCase())] : [lower];
|
|
43
|
+
for (const name of namesToTry) {
|
|
44
|
+
// Tier 1: Exact match.
|
|
45
|
+
const exact = yttvWatchUrlCache.get(name);
|
|
46
|
+
if (exact) {
|
|
47
|
+
// Cache under the primary channelSelector key if we matched via an alternate name, so subsequent lookups are O(1).
|
|
48
|
+
if (name !== lower) {
|
|
49
|
+
yttvWatchUrlCache.set(lower, exact);
|
|
50
|
+
}
|
|
51
|
+
return exact;
|
|
52
|
+
}
|
|
53
|
+
// Tier 2: Prefix+digit match for local affiliates. Iterate all cache entries to find one whose key starts with "{name} " followed by a digit, matching the
|
|
54
|
+
// "{Network} {Number}" pattern (e.g., "nbc 5", "abc 7") while excluding unrelated channels (e.g., "nbc sports chicago").
|
|
55
|
+
for (const [key, url] of yttvWatchUrlCache) {
|
|
56
|
+
if (key.startsWith(name + " ") && (key.length > name.length + 1) && (key.charCodeAt(name.length + 1) >= 48) && (key.charCodeAt(name.length + 1) <= 57)) {
|
|
57
|
+
yttvWatchUrlCache.set(lower, url);
|
|
58
|
+
return url;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Tier 3: Parenthetical suffix match for timezone/region variants. Find a cache entry whose key starts with "{name} (" to catch channels like
|
|
62
|
+
// "magnolia network (pacific)" or "the filipino channel (pacific)".
|
|
63
|
+
for (const [key, url] of yttvWatchUrlCache) {
|
|
64
|
+
if (key.startsWith(name + " (")) {
|
|
65
|
+
yttvWatchUrlCache.set(lower, url);
|
|
66
|
+
return url;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Invalidates the cached YouTube TV watch URL for the given channel selector. Called when a cached URL fails to produce a working stream. Deletes the
|
|
74
|
+
* channelSelector key — the bulk-scraped guide-name entries are left intact and will be refreshed on the next strategy run when the guide page is reloaded.
|
|
75
|
+
* @param channelSelector - The channel selector string to invalidate.
|
|
76
|
+
*/
|
|
77
|
+
function invalidateYttvDirectUrl(channelSelector) {
|
|
78
|
+
yttvWatchUrlCache.delete(channelSelector.toLowerCase());
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Clears all cached YouTube TV watch URLs. Called by clearChannelSelectionCaches() in the coordinator when the browser restarts.
|
|
82
|
+
*/
|
|
83
|
+
function clearYttvCache() {
|
|
84
|
+
yttvWatchUrlCache.clear();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* YouTube TV grid strategy: scrapes all watch URLs from the non-virtualized EPG grid at tv.youtube.com/live in a single pass, populating the module-level cache so
|
|
88
|
+
* that subsequent tunes to any YTTV channel resolve via findWatchUrl() without loading the guide page. All ~256 channel rows are present in the DOM simultaneously,
|
|
89
|
+
* so one querySelectorAll captures every channel's name and watch URL.
|
|
90
|
+
*
|
|
91
|
+
* The selection process:
|
|
92
|
+
* 1. Wait for ytu-epg-row elements to confirm the guide grid has loaded.
|
|
93
|
+
* 2. Bulk scrape all channels: extract aria-label names and watch/ hrefs from every thumbnail endpoint.
|
|
94
|
+
* 3. Populate the watch URL cache with all discovered channels.
|
|
95
|
+
* 4. Look up the target channel using tiered matching (exact, prefix+digit, parenthetical, alternates) against the cache.
|
|
96
|
+
* 5. Navigate to the matched watch URL via page.goto().
|
|
97
|
+
* @param page - The Puppeteer page object.
|
|
98
|
+
* @param profile - The resolved site profile with a non-null channelSelector (channel name, e.g., "CNN", "ESPN", "NBC").
|
|
99
|
+
* @returns Result object with success status and optional failure reason.
|
|
100
|
+
*/
|
|
101
|
+
async function youtubeGridStrategy(page, profile) {
|
|
102
|
+
const channelName = profile.channelSelector;
|
|
103
|
+
// Wait for the EPG grid to render. All ~256 rows load simultaneously (no virtualization), so once any row exists, all channels are queryable.
|
|
104
|
+
try {
|
|
105
|
+
await page.waitForSelector("ytu-epg-row", { timeout: CONFIG.streaming.videoTimeout });
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return { reason: "YouTube TV guide grid did not load.", success: false };
|
|
109
|
+
}
|
|
110
|
+
// Bulk scrape all channels from the grid in a single evaluate round-trip. For each thumbnail endpoint with a valid watch/ href, extract the channel name (from
|
|
111
|
+
// the aria-label, stripping the "watch " prefix) and the watch path. Channels with "live" or "browse/" hrefs are premium add-ons or info pages and are excluded.
|
|
112
|
+
const allChannels = await evaluateWithAbort(page, () => {
|
|
113
|
+
const results = [];
|
|
114
|
+
for (const thumb of Array.from(document.querySelectorAll("ytu-endpoint.tenx-thumb[aria-label]"))) {
|
|
115
|
+
const label = thumb.getAttribute("aria-label") ?? "";
|
|
116
|
+
if (!label.startsWith("watch ")) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const anchor = thumb.querySelector("a");
|
|
120
|
+
const href = anchor?.getAttribute("href") ?? "";
|
|
121
|
+
// Only include channels with streamable watch URLs. Channels with "live" or "browse/" hrefs are premium add-ons or info pages.
|
|
122
|
+
if (href.startsWith("watch/")) {
|
|
123
|
+
results.push({ name: label.slice(6), watchPath: href });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return results;
|
|
127
|
+
}, []);
|
|
128
|
+
// Populate the watch URL cache with all discovered channels. This makes every subsequent YTTV tune a cache hit via resolveDirectUrl, skipping guide navigation
|
|
129
|
+
// entirely. Cache keys are lowercased guide names (e.g., "cnn", "nbc 5", "wgn"). The tiered matching in findWatchUrl() handles channelSelector-to-guide-name
|
|
130
|
+
// resolution (e.g., "NBC" finds "nbc 5" via prefix+digit matching, "CW" finds "wgn" via CHANNEL_ALTERNATES).
|
|
131
|
+
for (const ch of allChannels) {
|
|
132
|
+
yttvWatchUrlCache.set(ch.name.toLowerCase(), YOUTUBE_TV_BASE_URL + "/" + ch.watchPath);
|
|
133
|
+
}
|
|
134
|
+
LOG.debug("tuning:yttv", "Cached %s YouTube TV watch URLs.", yttvWatchUrlCache.size);
|
|
135
|
+
// Look up the target channel using tiered matching against the populated cache.
|
|
136
|
+
const watchUrl = findWatchUrl(channelName);
|
|
137
|
+
if (!watchUrl) {
|
|
138
|
+
// Channel not found. Log available channels as a diagnostic to help users identify their market's channel names and create user-defined channels with the
|
|
139
|
+
// correct channelSelector value. Build additional known names from CHANNEL_ALTERNATES values so they are also filtered out of the diagnostic list.
|
|
140
|
+
const additionalKnownNames = [];
|
|
141
|
+
for (const alts of Object.values(CHANNEL_ALTERNATES)) {
|
|
142
|
+
for (const alt of alts) {
|
|
143
|
+
additionalKnownNames.push(alt);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
logAvailableChannels({
|
|
147
|
+
additionalKnownNames,
|
|
148
|
+
availableChannels: allChannels.map((ch) => ch.name).sort(),
|
|
149
|
+
channelName,
|
|
150
|
+
guideUrl: "https://tv.youtube.com/live",
|
|
151
|
+
presetSuffix: "-yttv",
|
|
152
|
+
providerName: "YouTube TV"
|
|
153
|
+
});
|
|
154
|
+
return { reason: "Channel \"" + channelName + "\" not found in YouTube TV guide.", success: false };
|
|
155
|
+
}
|
|
156
|
+
LOG.debug("tuning:yttv", "Navigating to YouTube TV watch URL for %s.", channelName);
|
|
157
|
+
try {
|
|
158
|
+
await page.goto(watchUrl, { timeout: CONFIG.streaming.navigationTimeout, waitUntil: "load" });
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
return { reason: "Failed to navigate to YouTube TV watch page: " + formatError(error) + ".", success: false };
|
|
162
|
+
}
|
|
163
|
+
return { success: true };
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Async wrapper around findWatchUrl for the ChannelStrategyEntry.resolveDirectUrl contract. The page parameter is unused because YTTV watch URLs are resolved
|
|
167
|
+
* purely from the in-memory cache populated during the first guide page scrape.
|
|
168
|
+
* @param channelSelector - The channel selector string (e.g., "CNN", "ESPN", "NBC").
|
|
169
|
+
* @param _page - Unused. Present to satisfy the async resolveDirectUrl signature.
|
|
170
|
+
* @returns The cached watch URL or null.
|
|
171
|
+
*/
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/require-await
|
|
173
|
+
async function resolveYttvDirectUrl(channelSelector, _page) {
|
|
174
|
+
return findWatchUrl(channelSelector);
|
|
175
|
+
}
|
|
176
|
+
export const yttvStrategy = {
|
|
177
|
+
clearCache: clearYttvCache,
|
|
178
|
+
execute: youtubeGridStrategy,
|
|
179
|
+
invalidateDirectUrl: invalidateYttvDirectUrl,
|
|
180
|
+
resolveDirectUrl: resolveYttvDirectUrl
|
|
181
|
+
};
|
|
182
|
+
//# sourceMappingURL=youtubeTv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"youtubeTv.js","sourceRoot":"","sources":["../../../src/browser/tuning/youtubeTv.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,wBAAwB,CAAC;AAE9D,iDAAiD;AACjD,MAAM,mBAAmB,GAAG,wBAAwB,CAAC;AAErD,gKAAgK;AAChK,mKAAmK;AACnK,6JAA6J;AAC7J,oBAAoB;AACpB,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAAkB,CAAC;AAEpD,+JAA+J;AAC/J,kKAAkK;AAClK,sKAAsK;AACtK,mDAAmD;AACnD,MAAM,kBAAkB,GAA6B;IAEnD,IAAI,EAAE,CAAC,KAAK,CAAC;IACb,KAAK,EAAE;QACL,aAAa,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,QAAQ;QACtJ,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;KAClH;CACF,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,SAAS,YAAY,CAAC,WAAmB;IAEvC,MAAM,KAAK,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC;IAExC,8JAA8J;IAC9J,8FAA8F;IAC9F,MAAM,UAAU,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAE7C,uEAAuE;IACvE,MAAM,UAAU,GAAG,UAAU,CAAC,CAAC,CAAC,CAAE,KAAK,EAAE,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAE/F,KAAI,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAE7B,uBAAuB;QACvB,MAAM,KAAK,GAAG,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAE1C,IAAG,KAAK,EAAE,CAAC;YAET,mHAAmH;YACnH,IAAG,IAAI,KAAK,KAAK,EAAE,CAAC;gBAElB,iBAAiB,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YACtC,CAAC;YAED,OAAO,KAAK,CAAC;QACf,CAAC;QAED,2JAA2J;QAC3J,yHAAyH;QACzH,KAAI,MAAM,CAAE,GAAG,EAAE,GAAG,CAAE,IAAI,iBAAiB,EAAE,CAAC;YAE5C,IAAG,GAAG,CAAC,UAAU,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;gBAEtJ,iBAAiB,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;gBAElC,OAAO,GAAG,CAAC;YACb,CAAC;QACH,CAAC;QAED,8IAA8I;QAC9I,oEAAoE;QACpE,KAAI,MAAM,CAAE,GAAG,EAAE,GAAG,CAAE,IAAI,iBAAiB,EAAE,CAAC;YAE5C,IAAG,GAAG,CAAC,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;gBAE/B,iBAAiB,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;gBAElC,OAAO,GAAG,CAAC;YACb,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,SAAS,uBAAuB,CAAC,eAAuB;IAEtD,iBAAiB,CAAC,MAAM,CAAC,eAAe,CAAC,WAAW,EAAE,CAAC,CAAC;AAC1D,CAAC;AAED;;GAEG;AACH,SAAS,cAAc;IAErB,iBAAiB,CAAC,KAAK,EAAE,CAAC;AAC5B,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,KAAK,UAAU,mBAAmB,CAAC,IAAU,EAAE,OAAgC;IAE7E,MAAM,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC;IAE5C,8IAA8I;IAC9I,IAAI,CAAC;QAEH,MAAM,IAAI,CAAC,eAAe,CAAC,aAAa,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC;IACxF,CAAC;IAAC,MAAM,CAAC;QAEP,OAAO,EAAE,MAAM,EAAE,qCAAqC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC3E,CAAC;IAED,+JAA+J;IAC/J,iKAAiK;IACjK,MAAM,WAAW,GAAG,MAAM,iBAAiB,CAAC,IAAI,EAAE,GAA0C,EAAE;QAE5F,MAAM,OAAO,GAA0C,EAAE,CAAC;QAE1D,KAAI,MAAM,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,qCAAqC,CAAC,CAAC,EAAE,CAAC;YAEhG,MAAM,KAAK,GAAG,KAAK,CAAC,YAAY,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;YAErD,IAAG,CAAC,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAE/B,SAAS;YACX,CAAC;YAED,MAAM,MAAM,GAAG,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;YACxC,MAAM,IAAI,GAAG,MAAM,EAAE,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YAEhD,+HAA+H;YAC/H,IAAG,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAE7B,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC1D,CAAC;QACH,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,+JAA+J;IAC/J,6JAA6J;IAC7J,6GAA6G;IAC7G,KAAI,MAAM,EAAE,IAAI,WAAW,EAAE,CAAC;QAE5B,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,mBAAmB,GAAG,GAAG,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IACzF,CAAC;IAED,GAAG,CAAC,KAAK,CAAC,aAAa,EAAE,kCAAkC,EAAE,iBAAiB,CAAC,IAAI,CAAC,CAAC;IAErF,gFAAgF;IAChF,MAAM,QAAQ,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC;IAE3C,IAAG,CAAC,QAAQ,EAAE,CAAC;QAEb,0JAA0J;QAC1J,mJAAmJ;QACnJ,MAAM,oBAAoB,GAAa,EAAE,CAAC;QAE1C,KAAI,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,EAAE,CAAC;YAEpD,KAAI,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;gBAEtB,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;QAED,oBAAoB,CAAC;YAEnB,oBAAoB;YACpB,iBAAiB,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE;YAC1D,WAAW;YACX,QAAQ,EAAE,6BAA6B;YACvC,YAAY,EAAE,OAAO;YACrB,YAAY,EAAE,YAAY;SAC3B,CAAC,CAAC;QAEH,OAAO,EAAE,MAAM,EAAE,YAAY,GAAG,WAAW,GAAG,mCAAmC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IACtG,CAAC;IAED,GAAG,CAAC,KAAK,CAAC,aAAa,EAAE,4CAA4C,EAAE,WAAW,CAAC,CAAC;IAEpF,IAAI,CAAC;QAEH,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,SAAS,CAAC,iBAAiB,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;IAChG,CAAC;IAAC,OAAM,KAAK,EAAE,CAAC;QAEd,OAAO,EAAE,MAAM,EAAE,+CAA+C,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAChH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED;;;;;;GAMG;AACH,+FAA+F;AAC/F,KAAK,UAAU,oBAAoB,CAAC,eAAuB,EAAE,KAAW;IAEtE,OAAO,YAAY,CAAC,eAAe,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,CAAC,MAAM,YAAY,GAAyB;IAEhD,UAAU,EAAE,cAAc;IAC1B,OAAO,EAAE,mBAAmB;IAC5B,mBAAmB,EAAE,uBAAuB;IAC5C,gBAAgB,EAAE,oBAAoB;CACvC,CAAC"}
|