Skip to content

Commit bb027f1

Browse files
shakyShanegithub-actions[bot]
authored andcommitted
Release build 4.59.0 [ci release]
1 parent 414f24b commit bb027f1

File tree

11 files changed

+808
-23
lines changed

11 files changed

+808
-23
lines changed

Sources/ContentScopeScripts/dist/pages/duckplayer/index.html

Lines changed: 148 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1424,6 +1424,110 @@
14241424
});
14251425
}
14261426

1427+
// ../../src/features/duckplayer/util.js
1428+
var VideoParams = class _VideoParams {
1429+
/**
1430+
* @param {string} id - the YouTube video ID
1431+
* @param {string|null|undefined} time - an optional time
1432+
*/
1433+
constructor(id, time) {
1434+
this.id = id;
1435+
this.time = time;
1436+
}
1437+
static validVideoId = /^[a-zA-Z0-9-_]+$/;
1438+
static validTimestamp = /^[0-9hms]+$/;
1439+
/**
1440+
* @returns {string}
1441+
*/
1442+
toPrivatePlayerUrl() {
1443+
const duckUrl = new URL(`duck://player/${this.id}`);
1444+
if (this.time) {
1445+
duckUrl.searchParams.set("t", this.time);
1446+
}
1447+
return duckUrl.href;
1448+
}
1449+
/**
1450+
* Create a VideoParams instance from a href, only if it's on the watch page
1451+
*
1452+
* @param {string} href
1453+
* @returns {VideoParams|null}
1454+
*/
1455+
static forWatchPage(href) {
1456+
let url;
1457+
try {
1458+
url = new URL(href);
1459+
} catch (e) {
1460+
return null;
1461+
}
1462+
if (!url.pathname.startsWith("/watch")) {
1463+
return null;
1464+
}
1465+
return _VideoParams.fromHref(url.href);
1466+
}
1467+
/**
1468+
* Convert a relative pathname into VideoParams
1469+
*
1470+
* @param pathname
1471+
* @returns {VideoParams|null}
1472+
*/
1473+
static fromPathname(pathname) {
1474+
let url;
1475+
try {
1476+
url = new URL(pathname, window.location.origin);
1477+
} catch (e) {
1478+
return null;
1479+
}
1480+
return _VideoParams.fromHref(url.href);
1481+
}
1482+
/**
1483+
* Convert a href into valid video params. Those can then be converted into a private player
1484+
* link when needed
1485+
*
1486+
* @param href
1487+
* @returns {VideoParams|null}
1488+
*/
1489+
static fromHref(href) {
1490+
let url;
1491+
try {
1492+
url = new URL(href);
1493+
} catch (e) {
1494+
return null;
1495+
}
1496+
let id = null;
1497+
const vParam = url.searchParams.get("v");
1498+
const tParam = url.searchParams.get("t");
1499+
if (url.searchParams.has("list") && !url.searchParams.has("index")) {
1500+
return null;
1501+
}
1502+
let time = null;
1503+
if (vParam && _VideoParams.validVideoId.test(vParam)) {
1504+
id = vParam;
1505+
} else {
1506+
return null;
1507+
}
1508+
if (tParam && _VideoParams.validTimestamp.test(tParam)) {
1509+
time = tParam;
1510+
}
1511+
return new _VideoParams(id, time);
1512+
}
1513+
};
1514+
1515+
// pages/duckplayer/src/js/utils.js
1516+
function createYoutubeURLForError(href, urlBase) {
1517+
const valid = VideoParams.forWatchPage(href);
1518+
if (!valid)
1519+
return null;
1520+
const original = new URL(href);
1521+
if (original.searchParams.get("feature") !== "emb_err_woyt")
1522+
return null;
1523+
const url = new URL(urlBase);
1524+
url.searchParams.set("v", valid.id);
1525+
if (typeof valid.time === "string") {
1526+
url.searchParams.set("t", valid.time);
1527+
}
1528+
return url.toString();
1529+
}
1530+
14271531
// pages/duckplayer/src/js/index.js
14281532
var VideoPlayer = {
14291533
/**
@@ -1461,10 +1565,41 @@
14611565
* Sets up the video player:
14621566
* 1. Fetches the video id
14631567
* 2. If the video id is correctly formatted, it loads the YouTube video in the iframe, otherwise displays an error message
1568+
* @param {object} opts
1569+
* @param {string} opts.base
14641570
*/
1465-
init: () => {
1571+
init: (opts) => {
14661572
VideoPlayer.loadVideoById();
14671573
VideoPlayer.setTabTitle();
1574+
VideoPlayer.setClickListener(opts.base);
1575+
},
1576+
/**
1577+
* In certain circumstances, we may want to intercept
1578+
* clicks within the iframe - for example when showing a video
1579+
* that cannot be played in the embed
1580+
*
1581+
* @param {string} urlBase - macos/windows current use a different base URL
1582+
*/
1583+
setClickListener: (urlBase) => {
1584+
VideoPlayer.onIframeLoaded(() => {
1585+
const iframe = VideoPlayer.iframe();
1586+
iframe.contentDocument?.addEventListener("click", (e) => {
1587+
if (!e.target)
1588+
return;
1589+
const target = (
1590+
/** @type {Element} */
1591+
e.target
1592+
);
1593+
if (!("href" in target) || typeof target.href !== "string")
1594+
return;
1595+
const next = createYoutubeURLForError(target.href, urlBase);
1596+
if (!next)
1597+
return;
1598+
e.preventDefault();
1599+
e.stopImmediatePropagation();
1600+
window.location.href = next;
1601+
});
1602+
});
14681603
},
14691604
/**
14701605
* Tries loading the video if there's a valid video id, otherwise shows error message.
@@ -1491,8 +1626,15 @@
14911626
*/
14921627
onIframeLoaded: (callback) => {
14931628
const iframe = VideoPlayer.iframe();
1494-
if (iframe) {
1495-
iframe.addEventListener("load", callback);
1629+
if (VideoPlayer.loaded) {
1630+
callback();
1631+
} else {
1632+
if (iframe) {
1633+
iframe.addEventListener("load", () => {
1634+
VideoPlayer.loaded = true;
1635+
callback();
1636+
});
1637+
}
14961638
}
14971639
},
14981640
/**
@@ -1980,7 +2122,9 @@
19802122
console.warn("cannot continue as messaging was not resolved");
19812123
return;
19822124
}
1983-
VideoPlayer.init();
2125+
VideoPlayer.init({
2126+
base: baseUrl("apple")
2127+
});
19842128
Tooltip.init();
19852129
PlayOnYouTube.init({
19862130
base: baseUrl("apple")

Sources/ContentScopeScripts/dist/pages/duckplayer/js/index.js

Lines changed: 148 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,110 @@
11101110
});
11111111
}
11121112

1113+
// ../../src/features/duckplayer/util.js
1114+
var VideoParams = class _VideoParams {
1115+
/**
1116+
* @param {string} id - the YouTube video ID
1117+
* @param {string|null|undefined} time - an optional time
1118+
*/
1119+
constructor(id, time) {
1120+
this.id = id;
1121+
this.time = time;
1122+
}
1123+
static validVideoId = /^[a-zA-Z0-9-_]+$/;
1124+
static validTimestamp = /^[0-9hms]+$/;
1125+
/**
1126+
* @returns {string}
1127+
*/
1128+
toPrivatePlayerUrl() {
1129+
const duckUrl = new URL(`duck://player/${this.id}`);
1130+
if (this.time) {
1131+
duckUrl.searchParams.set("t", this.time);
1132+
}
1133+
return duckUrl.href;
1134+
}
1135+
/**
1136+
* Create a VideoParams instance from a href, only if it's on the watch page
1137+
*
1138+
* @param {string} href
1139+
* @returns {VideoParams|null}
1140+
*/
1141+
static forWatchPage(href) {
1142+
let url;
1143+
try {
1144+
url = new URL(href);
1145+
} catch (e) {
1146+
return null;
1147+
}
1148+
if (!url.pathname.startsWith("/watch")) {
1149+
return null;
1150+
}
1151+
return _VideoParams.fromHref(url.href);
1152+
}
1153+
/**
1154+
* Convert a relative pathname into VideoParams
1155+
*
1156+
* @param pathname
1157+
* @returns {VideoParams|null}
1158+
*/
1159+
static fromPathname(pathname) {
1160+
let url;
1161+
try {
1162+
url = new URL(pathname, window.location.origin);
1163+
} catch (e) {
1164+
return null;
1165+
}
1166+
return _VideoParams.fromHref(url.href);
1167+
}
1168+
/**
1169+
* Convert a href into valid video params. Those can then be converted into a private player
1170+
* link when needed
1171+
*
1172+
* @param href
1173+
* @returns {VideoParams|null}
1174+
*/
1175+
static fromHref(href) {
1176+
let url;
1177+
try {
1178+
url = new URL(href);
1179+
} catch (e) {
1180+
return null;
1181+
}
1182+
let id = null;
1183+
const vParam = url.searchParams.get("v");
1184+
const tParam = url.searchParams.get("t");
1185+
if (url.searchParams.has("list") && !url.searchParams.has("index")) {
1186+
return null;
1187+
}
1188+
let time = null;
1189+
if (vParam && _VideoParams.validVideoId.test(vParam)) {
1190+
id = vParam;
1191+
} else {
1192+
return null;
1193+
}
1194+
if (tParam && _VideoParams.validTimestamp.test(tParam)) {
1195+
time = tParam;
1196+
}
1197+
return new _VideoParams(id, time);
1198+
}
1199+
};
1200+
1201+
// pages/duckplayer/src/js/utils.js
1202+
function createYoutubeURLForError(href, urlBase) {
1203+
const valid = VideoParams.forWatchPage(href);
1204+
if (!valid)
1205+
return null;
1206+
const original = new URL(href);
1207+
if (original.searchParams.get("feature") !== "emb_err_woyt")
1208+
return null;
1209+
const url = new URL(urlBase);
1210+
url.searchParams.set("v", valid.id);
1211+
if (typeof valid.time === "string") {
1212+
url.searchParams.set("t", valid.time);
1213+
}
1214+
return url.toString();
1215+
}
1216+
11131217
// pages/duckplayer/src/js/index.js
11141218
var VideoPlayer = {
11151219
/**
@@ -1147,10 +1251,41 @@
11471251
* Sets up the video player:
11481252
* 1. Fetches the video id
11491253
* 2. If the video id is correctly formatted, it loads the YouTube video in the iframe, otherwise displays an error message
1254+
* @param {object} opts
1255+
* @param {string} opts.base
11501256
*/
1151-
init: () => {
1257+
init: (opts) => {
11521258
VideoPlayer.loadVideoById();
11531259
VideoPlayer.setTabTitle();
1260+
VideoPlayer.setClickListener(opts.base);
1261+
},
1262+
/**
1263+
* In certain circumstances, we may want to intercept
1264+
* clicks within the iframe - for example when showing a video
1265+
* that cannot be played in the embed
1266+
*
1267+
* @param {string} urlBase - macos/windows current use a different base URL
1268+
*/
1269+
setClickListener: (urlBase) => {
1270+
VideoPlayer.onIframeLoaded(() => {
1271+
const iframe = VideoPlayer.iframe();
1272+
iframe.contentDocument?.addEventListener("click", (e) => {
1273+
if (!e.target)
1274+
return;
1275+
const target = (
1276+
/** @type {Element} */
1277+
e.target
1278+
);
1279+
if (!("href" in target) || typeof target.href !== "string")
1280+
return;
1281+
const next = createYoutubeURLForError(target.href, urlBase);
1282+
if (!next)
1283+
return;
1284+
e.preventDefault();
1285+
e.stopImmediatePropagation();
1286+
window.location.href = next;
1287+
});
1288+
});
11541289
},
11551290
/**
11561291
* Tries loading the video if there's a valid video id, otherwise shows error message.
@@ -1177,8 +1312,15 @@
11771312
*/
11781313
onIframeLoaded: (callback) => {
11791314
const iframe = VideoPlayer.iframe();
1180-
if (iframe) {
1181-
iframe.addEventListener("load", callback);
1315+
if (VideoPlayer.loaded) {
1316+
callback();
1317+
} else {
1318+
if (iframe) {
1319+
iframe.addEventListener("load", () => {
1320+
VideoPlayer.loaded = true;
1321+
callback();
1322+
});
1323+
}
11821324
}
11831325
},
11841326
/**
@@ -1666,7 +1808,9 @@
16661808
console.warn("cannot continue as messaging was not resolved");
16671809
return;
16681810
}
1669-
VideoPlayer.init();
1811+
VideoPlayer.init({
1812+
base: baseUrl("apple")
1813+
});
16701814
Tooltip.init();
16711815
PlayOnYouTube.init({
16721816
base: baseUrl("apple")
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { VideoParams } from '../../../../../../src/features/duckplayer/util'
2+
3+
/**
4+
* @param {string} href
5+
* @param {string} urlBase
6+
* @return {null | string}
7+
*/
8+
export function createYoutubeURLForError (href, urlBase) {
9+
const valid = VideoParams.forWatchPage(href)
10+
if (!valid) return null
11+
12+
// this will not throw, since it was guarded above
13+
const original = new URL(href)
14+
15+
// for now, we're only intercepting clicks when `emb_err_woyt` is present
16+
// this may not be enough to cover all situations, but it solves our immediate
17+
// problems whilst keeping the blast radius low
18+
if (original.searchParams.get('feature') !== 'emb_err_woyt') return null
19+
20+
// if we get this far, we think a click is occurring that would cause a navigation loop
21+
// construct the 'next' url
22+
const url = new URL(urlBase)
23+
url.searchParams.set('v', valid.id)
24+
25+
if (typeof valid.time === 'string') {
26+
url.searchParams.set('t', valid.time)
27+
}
28+
29+
return url.toString()
30+
}

0 commit comments

Comments
 (0)