@okostream/mcp 0.21.0 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.js +246 -163
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -119137,7 +119137,7 @@ function sanitizeScopes(scopes) {
|
|
|
119137
119137
|
}
|
|
119138
119138
|
|
|
119139
119139
|
// build/version.js
|
|
119140
|
-
var OKO_MCP_VERSION = "0.
|
|
119140
|
+
var OKO_MCP_VERSION = "0.22.1";
|
|
119141
119141
|
|
|
119142
119142
|
// build/auth.js
|
|
119143
119143
|
var state = null;
|
|
@@ -119484,175 +119484,245 @@ function pinnedLookup(address, family) {
|
|
|
119484
119484
|
// build/videoUrl.js
|
|
119485
119485
|
var TWITCH_PARENT = "okostream.app";
|
|
119486
119486
|
function extractIframeSrc(input) {
|
|
119487
|
-
const
|
|
119488
|
-
if (!
|
|
119487
|
+
const tag = /<iframe\b[^>]*>/i.exec(input);
|
|
119488
|
+
if (!tag)
|
|
119489
119489
|
return null;
|
|
119490
|
-
const match =
|
|
119491
|
-
|
|
119492
|
-
|
|
119493
|
-
var YT_ID_RE = /^[\w-]{11}$/;
|
|
119494
|
-
function extractYouTubeId(url) {
|
|
119495
|
-
let parsed;
|
|
119496
|
-
try {
|
|
119497
|
-
parsed = new URL(url);
|
|
119498
|
-
} catch {
|
|
119490
|
+
const match = /\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/i.exec(tag[0]);
|
|
119491
|
+
const src = (match?.[1] ?? match?.[2] ?? match?.[3] ?? "").trim().replace(/&/gi, "&");
|
|
119492
|
+
if (!src)
|
|
119499
119493
|
return null;
|
|
119500
|
-
}
|
|
119501
|
-
const host = parsed.hostname.replace(/^www\./, "");
|
|
119502
|
-
if (host === "youtu.be") {
|
|
119503
|
-
const id = parsed.pathname.slice(1).split("/")[0] ?? "";
|
|
119504
|
-
return YT_ID_RE.test(id) ? id : null;
|
|
119505
|
-
}
|
|
119506
|
-
if (host !== "youtube.com" && host !== "m.youtube.com")
|
|
119507
|
-
return null;
|
|
119508
|
-
const path = parsed.pathname;
|
|
119509
|
-
if (path.startsWith("/embed/")) {
|
|
119510
|
-
const id = path.slice(7).split("/")[0] ?? "";
|
|
119511
|
-
return YT_ID_RE.test(id) ? id : null;
|
|
119512
|
-
}
|
|
119513
|
-
if (path.startsWith("/shorts/")) {
|
|
119514
|
-
const id = path.slice(8).split("/")[0] ?? "";
|
|
119515
|
-
return YT_ID_RE.test(id) ? id : null;
|
|
119516
|
-
}
|
|
119517
|
-
if (path.startsWith("/live/")) {
|
|
119518
|
-
const id = path.slice(6).split("/")[0] ?? "";
|
|
119519
|
-
return YT_ID_RE.test(id) ? id : null;
|
|
119520
|
-
}
|
|
119521
|
-
if (path === "/watch") {
|
|
119522
|
-
const id = parsed.searchParams.get("v") ?? "";
|
|
119523
|
-
return YT_ID_RE.test(id) ? id : null;
|
|
119524
|
-
}
|
|
119525
|
-
return null;
|
|
119494
|
+
return src.startsWith("//") ? `https:${src}` : src;
|
|
119526
119495
|
}
|
|
119527
|
-
function
|
|
119496
|
+
function parseUrl(url) {
|
|
119528
119497
|
try {
|
|
119529
|
-
return new URL(url)
|
|
119498
|
+
return new URL(url);
|
|
119530
119499
|
} catch {
|
|
119531
119500
|
return null;
|
|
119532
119501
|
}
|
|
119533
119502
|
}
|
|
119534
|
-
|
|
119535
|
-
|
|
119536
|
-
|
|
119537
|
-
|
|
119538
|
-
|
|
119503
|
+
var WRAP_START_RE = /^[<(\[{«„“"'`]+/;
|
|
119504
|
+
var WRAP_END_RE = /[>)\]}»“”"'`.,;:!?]+$/;
|
|
119505
|
+
var BARE_HOST_RE = /^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}(?::\d+)?(?:[/?#]|$)/i;
|
|
119506
|
+
function wordAsAddress(word, bare) {
|
|
119507
|
+
let link = word.replace(WRAP_START_RE, "").replace(WRAP_END_RE, "").replace(/&/gi, "&");
|
|
119508
|
+
if (/^https?:\/\//i.test(link))
|
|
119509
|
+
link = link.replace(/^http:/i, "https:");
|
|
119510
|
+
else if (link.startsWith("//"))
|
|
119511
|
+
link = `https:${link}`;
|
|
119512
|
+
else if (bare && BARE_HOST_RE.test(link))
|
|
119513
|
+
link = `https://${link}`;
|
|
119514
|
+
else
|
|
119539
119515
|
return null;
|
|
119540
|
-
|
|
119541
|
-
|
|
119542
|
-
const oid = parsed.searchParams.get("oid");
|
|
119543
|
-
const id = parsed.searchParams.get("id");
|
|
119544
|
-
return oid && id ? { oid, id } : null;
|
|
119545
|
-
}
|
|
119546
|
-
const match = parsed.pathname.match(/\/video(-?\d+)_(\d+)/);
|
|
119547
|
-
return match ? { oid: match[1] ?? "", id: match[2] ?? "" } : null;
|
|
119516
|
+
const parsed = parseUrl(link);
|
|
119517
|
+
return parsed && parsed.hostname.includes(".") ? link : null;
|
|
119548
119518
|
}
|
|
119549
|
-
function
|
|
119550
|
-
|
|
119551
|
-
|
|
119552
|
-
|
|
119553
|
-
|
|
119554
|
-
|
|
119555
|
-
|
|
119556
|
-
|
|
119557
|
-
|
|
119558
|
-
for (const pattern of patterns) {
|
|
119559
|
-
const match = parsed.pathname.match(pattern);
|
|
119560
|
-
if (match?.[1])
|
|
119561
|
-
return match[1];
|
|
119519
|
+
function readVideoAddress(input) {
|
|
119520
|
+
const fromCode = extractIframeSrc(input);
|
|
119521
|
+
const words = fromCode !== null ? [fromCode] : input.split(/\s+/).filter(Boolean);
|
|
119522
|
+
for (const bare of [false, true]) {
|
|
119523
|
+
for (const word of words) {
|
|
119524
|
+
const address = wordAsAddress(word, bare);
|
|
119525
|
+
if (address !== null)
|
|
119526
|
+
return address;
|
|
119527
|
+
}
|
|
119562
119528
|
}
|
|
119563
119529
|
return null;
|
|
119564
119530
|
}
|
|
119565
|
-
|
|
119566
|
-
|
|
119567
|
-
|
|
119568
|
-
|
|
119569
|
-
|
|
119570
|
-
} catch {
|
|
119571
|
-
return null;
|
|
119572
|
-
}
|
|
119573
|
-
if (parsed.hostname.replace(/^www\./, "") !== "vimeo.com")
|
|
119574
|
-
return null;
|
|
119575
|
-
const event = parsed.pathname.match(VIMEO_EVENT_PATH_RE);
|
|
119576
|
-
return event ? `https://vimeo.com/event/${event[1]}/embed${event[2] ? `/${event[2]}` : ""}` : null;
|
|
119531
|
+
function withTwitchParent(parsed) {
|
|
119532
|
+
const params = new URLSearchParams(parsed.search);
|
|
119533
|
+
params.delete("parent");
|
|
119534
|
+
params.append("parent", TWITCH_PARENT);
|
|
119535
|
+
return `${parsed.origin}${parsed.pathname}?${params.toString()}`;
|
|
119577
119536
|
}
|
|
119578
|
-
|
|
119579
|
-
|
|
119580
|
-
|
|
119581
|
-
|
|
119582
|
-
|
|
119583
|
-
|
|
119584
|
-
|
|
119585
|
-
|
|
119586
|
-
|
|
119587
|
-
|
|
119588
|
-
|
|
119589
|
-
|
|
119590
|
-
|
|
119591
|
-
|
|
119592
|
-
|
|
119593
|
-
|
|
119594
|
-
|
|
119595
|
-
|
|
119596
|
-
|
|
119597
|
-
|
|
119598
|
-
|
|
119599
|
-
|
|
119600
|
-
|
|
119537
|
+
var YT_ID_RE = /^[\w-]{11}$/;
|
|
119538
|
+
var VK_HOSTS = /* @__PURE__ */ new Set(["vk.com", "vk.ru", "vkvideo.ru"]);
|
|
119539
|
+
var VK_VIDEO_RE = /(?:video|clip)(-?\d+)_(\d+)/;
|
|
119540
|
+
var VIMEO_EVENT_PATH_RE = /^\/event\/(\d+)(?:\/embed(?:\/([A-Za-z0-9]+))?)?\/?$/;
|
|
119541
|
+
var VIMEO_VIDEO_PATH_RE = /^\/(?:(?:channels\/[^/]+|groups\/[^/]+\/videos|showcase\/[^/]+\/video|album\/[^/]+\/video)\/)?(\d+)(?:\/([0-9a-z]+))?\/?$/i;
|
|
119542
|
+
var KINESCOPE_NOT_A_VIDEO = /* @__PURE__ */ new Set(["embed", "pl", "playlist", "chat"]);
|
|
119543
|
+
var bareHost = (host) => host.replace(/^(?:www|m)\./, "");
|
|
119544
|
+
var SERVICES = [
|
|
119545
|
+
{
|
|
119546
|
+
provider: "youtube",
|
|
119547
|
+
owns: (host) => ["youtube.com", "music.youtube.com", "youtu.be", "youtube-nocookie.com"].includes(bareHost(host)),
|
|
119548
|
+
player: (parsed, address, host) => {
|
|
119549
|
+
const site = bareHost(host);
|
|
119550
|
+
if (site !== "youtube.com" && site !== "youtube-nocookie.com")
|
|
119551
|
+
return null;
|
|
119552
|
+
const match = /^\/embed(?:\/([^/]*))?\/?$/.exec(parsed.pathname);
|
|
119553
|
+
if (!match)
|
|
119554
|
+
return null;
|
|
119555
|
+
const part = match[1] ?? "";
|
|
119556
|
+
if (part === "" || part === "videoseries")
|
|
119557
|
+
return parsed.searchParams.get("list") ? address : null;
|
|
119558
|
+
if (part === "live_stream")
|
|
119559
|
+
return parsed.searchParams.get("channel") ? address : null;
|
|
119560
|
+
return YT_ID_RE.test(part) ? address : null;
|
|
119561
|
+
},
|
|
119562
|
+
page: (parsed, host) => {
|
|
119563
|
+
const site = bareHost(host);
|
|
119564
|
+
const path = parsed.pathname;
|
|
119565
|
+
let id = null;
|
|
119566
|
+
if (site === "youtu.be")
|
|
119567
|
+
id = path.slice(1).split("/")[0] ?? null;
|
|
119568
|
+
else if (site === "youtube.com" || site === "music.youtube.com") {
|
|
119569
|
+
if (path === "/watch")
|
|
119570
|
+
id = parsed.searchParams.get("v");
|
|
119571
|
+
else if (path === "/playlist") {
|
|
119572
|
+
const list = parsed.searchParams.get("list");
|
|
119573
|
+
return list ? `https://www.youtube.com/embed?listType=playlist&list=${encodeURIComponent(list)}` : null;
|
|
119574
|
+
} else {
|
|
119575
|
+
const match = /^\/(?:shorts|live|v)\/([^/]+)/.exec(path);
|
|
119576
|
+
id = match?.[1] ?? null;
|
|
119577
|
+
}
|
|
119578
|
+
}
|
|
119579
|
+
return id && YT_ID_RE.test(id) ? `https://www.youtube.com/embed/${id}` : null;
|
|
119580
|
+
}
|
|
119581
|
+
},
|
|
119582
|
+
{
|
|
119583
|
+
provider: "vk",
|
|
119584
|
+
owns: (host) => VK_HOSTS.has(bareHost(host)),
|
|
119585
|
+
// The player's own address (`video_ext.php?oid=…&id=…`, the `src` of VK's embed code) — its host, its `hash` and the rest: `hash` is
|
|
119586
|
+
// the access key of a video shared by link (met live 2026-11-11: only the pasted embed code played, because nothing rewrote it).
|
|
119587
|
+
player: (parsed, address) => parsed.pathname.endsWith("/video_ext.php") && parsed.searchParams.get("oid") && parsed.searchParams.get("id") ? address : null,
|
|
119588
|
+
// A page names the video in its path (`/video-OWNER_ID`, `/clip-OWNER_ID`, also behind `/playlist/…/` or `/@club/…`) or in the `z`
|
|
119589
|
+
// parameter VK adds when a video is opened over a wall, a community or a catalogue; `z` wins — it is the video the person looks at.
|
|
119590
|
+
page: (parsed) => {
|
|
119591
|
+
if (parsed.pathname.endsWith("/video_ext.php"))
|
|
119592
|
+
return null;
|
|
119593
|
+
const match = VK_VIDEO_RE.exec(parsed.searchParams.get("z") ?? "") ?? VK_VIDEO_RE.exec(parsed.pathname);
|
|
119594
|
+
return match ? `https://vk.com/video_ext.php?oid=${match[1]}&id=${match[2]}&hd=2` : null;
|
|
119595
|
+
}
|
|
119596
|
+
},
|
|
119597
|
+
{
|
|
119598
|
+
provider: "rutube",
|
|
119599
|
+
owns: (host) => bareHost(host) === "rutube.ru",
|
|
119600
|
+
player: (parsed, address) => /^\/play\/embed\/[a-f0-9]+\/?$/i.test(parsed.pathname) ? address : null,
|
|
119601
|
+
// `/video/<id>/`, a private video (`/video/private/<id>/?p=<key>` — the key rides along: without it the player shows a stub),
|
|
119602
|
+
// `/shorts/<id>/` and `/live/video/<id>/` (Rutube's own FAQ: a short is embedded as a video).
|
|
119603
|
+
page: (parsed) => {
|
|
119604
|
+
const match = /^\/(?:video(?:\/private)?|shorts|live\/video)\/([a-f0-9]+)(?:\/|$)/i.exec(parsed.pathname);
|
|
119605
|
+
if (!match)
|
|
119606
|
+
return null;
|
|
119607
|
+
const key = parsed.searchParams.get("p");
|
|
119608
|
+
return `https://rutube.ru/play/embed/${match[1]}${key ? `/?p=${encodeURIComponent(key)}` : ""}`;
|
|
119609
|
+
}
|
|
119610
|
+
},
|
|
119611
|
+
{
|
|
119612
|
+
provider: "vimeo",
|
|
119613
|
+
owns: (host) => ["vimeo.com", "player.vimeo.com"].includes(bareHost(host)),
|
|
119614
|
+
player: (parsed, address, host) => bareHost(host) === "player.vimeo.com" && /^\/video\/\d+\/?$/.test(parsed.pathname) ? address : null,
|
|
119615
|
+
page: (parsed, host) => {
|
|
119616
|
+
if (bareHost(host) !== "vimeo.com")
|
|
119617
|
+
return null;
|
|
119618
|
+
const event = VIMEO_EVENT_PATH_RE.exec(parsed.pathname);
|
|
119619
|
+
if (event)
|
|
119620
|
+
return `https://vimeo.com/event/${event[1]}/embed${event[2] ? `/${event[2]}` : ""}`;
|
|
119621
|
+
const video = VIMEO_VIDEO_PATH_RE.exec(parsed.pathname);
|
|
119622
|
+
if (!video)
|
|
119623
|
+
return null;
|
|
119624
|
+
const key = video[2] ?? parsed.searchParams.get("h");
|
|
119625
|
+
return `https://player.vimeo.com/video/${video[1]}${key ? `?h=${encodeURIComponent(key)}` : ""}`;
|
|
119626
|
+
}
|
|
119627
|
+
},
|
|
119628
|
+
{
|
|
119629
|
+
provider: "kinescope",
|
|
119630
|
+
owns: (host) => bareHost(host) === "kinescope.io",
|
|
119631
|
+
player: (parsed, address) => /^\/embed\/[A-Za-z0-9-]+\/?$/.test(parsed.pathname) ? address : null,
|
|
119632
|
+
// `/<id>`: digits in Kinescope's own examples, also letters and digits or a dashed id.
|
|
119633
|
+
page: (parsed) => {
|
|
119634
|
+
const id = /^\/([A-Za-z0-9-]+)\/?$/.exec(parsed.pathname)?.[1];
|
|
119635
|
+
return id && !KINESCOPE_NOT_A_VIDEO.has(id.toLowerCase()) ? `https://kinescope.io/embed/${id}` : null;
|
|
119636
|
+
}
|
|
119637
|
+
},
|
|
119638
|
+
{
|
|
119639
|
+
provider: "dzen",
|
|
119640
|
+
owns: (host) => bareHost(host) === "dzen.ru",
|
|
119641
|
+
player: (parsed, address) => /^\/embed\/[A-Za-z0-9_-]+\/?$/.test(parsed.pathname) ? address : null,
|
|
119642
|
+
page: (parsed) => {
|
|
119643
|
+
const match = /^\/video\/watch\/([A-Za-z0-9_-]+)/.exec(parsed.pathname);
|
|
119644
|
+
return match ? `https://dzen.ru/embed/${match[1]}` : null;
|
|
119645
|
+
}
|
|
119646
|
+
},
|
|
119647
|
+
{
|
|
119648
|
+
provider: "twitch",
|
|
119649
|
+
owns: (host) => ["twitch.tv", "player.twitch.tv", "clips.twitch.tv"].includes(bareHost(host)),
|
|
119650
|
+
player: (parsed, _address, host) => {
|
|
119651
|
+
const site = bareHost(host);
|
|
119652
|
+
const has = (name7) => Boolean(parsed.searchParams.get(name7));
|
|
119653
|
+
if (site === "player.twitch.tv")
|
|
119654
|
+
return has("channel") || has("video") || has("collection") ? withTwitchParent(parsed) : null;
|
|
119655
|
+
if (site === "clips.twitch.tv" && /^\/embed\/?$/.test(parsed.pathname))
|
|
119656
|
+
return has("clip") ? withTwitchParent(parsed) : null;
|
|
119657
|
+
return null;
|
|
119658
|
+
},
|
|
119659
|
+
page: (parsed, host) => {
|
|
119660
|
+
const site = bareHost(host);
|
|
119661
|
+
const path = parsed.pathname;
|
|
119662
|
+
const clip = site === "clips.twitch.tv" ? /^\/([A-Za-z0-9_-]+)\/?$/.exec(path) : /^\/[A-Za-z0-9_]+\/clip\/([A-Za-z0-9_-]+)/.exec(path);
|
|
119663
|
+
if (clip && clip[1] !== "embed")
|
|
119664
|
+
return `https://clips.twitch.tv/embed?clip=${clip[1]}&parent=${TWITCH_PARENT}`;
|
|
119665
|
+
if (site !== "twitch.tv")
|
|
119666
|
+
return null;
|
|
119667
|
+
const recording = /^\/videos\/(\d+)/.exec(path);
|
|
119668
|
+
if (recording)
|
|
119669
|
+
return `https://player.twitch.tv/?video=v${recording[1]}&parent=${TWITCH_PARENT}`;
|
|
119670
|
+
const channel = /^\/([A-Za-z0-9_]+)\/?$/.exec(path);
|
|
119671
|
+
return channel ? `https://player.twitch.tv/?channel=${channel[1]}&parent=${TWITCH_PARENT}` : null;
|
|
119672
|
+
}
|
|
119673
|
+
},
|
|
119674
|
+
{
|
|
119675
|
+
provider: "dailymotion",
|
|
119676
|
+
owns: (host) => ["dailymotion.com", "geo.dailymotion.com", "dai.ly"].includes(bareHost(host)),
|
|
119677
|
+
// Today's player (`geo.dailymotion.com/player/<player>.html?video=<id>`, and `…/player.html?video=<id>` from its oEmbed) and the
|
|
119678
|
+
// older one (`www.dailymotion.com/embed/video/<id>`).
|
|
119679
|
+
player: (parsed, address, host) => {
|
|
119680
|
+
const site = bareHost(host);
|
|
119681
|
+
if (site === "geo.dailymotion.com" && /^\/player(?:\/[A-Za-z0-9]+)?\.html$/.test(parsed.pathname)) {
|
|
119682
|
+
return parsed.searchParams.get("video") || parsed.searchParams.get("playlist") ? address : null;
|
|
119683
|
+
}
|
|
119684
|
+
return site === "dailymotion.com" && /^\/embed\/video\/[A-Za-z0-9]+\/?$/.test(parsed.pathname) ? address : null;
|
|
119685
|
+
},
|
|
119686
|
+
page: (parsed, host) => {
|
|
119687
|
+
const site = bareHost(host);
|
|
119688
|
+
const match = site === "dai.ly" ? /^\/([A-Za-z0-9]+)\/?$/.exec(parsed.pathname) : site === "dailymotion.com" ? /^\/video\/([A-Za-z0-9]+)/.exec(parsed.pathname) : null;
|
|
119689
|
+
return match ? `https://www.dailymotion.com/embed/video/${match[1]}` : null;
|
|
119690
|
+
}
|
|
119691
|
+
},
|
|
119692
|
+
{
|
|
119693
|
+
provider: "wistia",
|
|
119694
|
+
owns: (host) => host === "wistia.com" || host.endsWith(".wistia.com") || host === "fast.wistia.net",
|
|
119695
|
+
player: (parsed, address, host) => (host === "fast.wistia.net" || host === "fast.wistia.com") && /^\/embed\/iframe\/[A-Za-z0-9]+\/?$/.test(parsed.pathname) ? address : null,
|
|
119696
|
+
page: (parsed, host) => {
|
|
119697
|
+
if (host === "fast.wistia.net")
|
|
119698
|
+
return null;
|
|
119699
|
+
const match = /\/medias\/([A-Za-z0-9]+)/.exec(parsed.pathname);
|
|
119700
|
+
return match ? `https://fast.wistia.net/embed/iframe/${match[1]}` : null;
|
|
119701
|
+
}
|
|
119601
119702
|
}
|
|
119602
|
-
|
|
119603
|
-
}
|
|
119703
|
+
];
|
|
119604
119704
|
function detectAndNormalize(url) {
|
|
119605
|
-
const
|
|
119606
|
-
|
|
119705
|
+
const address = url.trim();
|
|
119706
|
+
const parsed = address ? parseUrl(address) : null;
|
|
119707
|
+
if (!parsed)
|
|
119607
119708
|
return { provider: "unknown", normalizedUrl: null };
|
|
119608
|
-
const host =
|
|
119609
|
-
|
|
119610
|
-
|
|
119611
|
-
return { provider: "
|
|
119612
|
-
}
|
|
119613
|
-
|
|
119614
|
-
|
|
119615
|
-
|
|
119616
|
-
|
|
119617
|
-
|
|
119618
|
-
|
|
119619
|
-
|
|
119620
|
-
|
|
119621
|
-
|
|
119622
|
-
|
|
119623
|
-
|
|
119624
|
-
|
|
119625
|
-
const vimeoId = extractPathId(trimmed, "vimeo.com", [/\/video\/(\d+)/, /^\/(\d+)/]);
|
|
119626
|
-
if (vimeoId)
|
|
119627
|
-
return { provider: "vimeo", normalizedUrl: `https://player.vimeo.com/video/${vimeoId}` };
|
|
119628
|
-
if (trimmed.includes("vimeo.com"))
|
|
119629
|
-
return { provider: "vimeo", normalizedUrl: null };
|
|
119630
|
-
const kinescopeId = extractPathId(trimmed, "kinescope.io", [/\/embed\/([a-zA-Z0-9]+)/, /^\/([a-zA-Z0-9]+)$/]);
|
|
119631
|
-
if (kinescopeId)
|
|
119632
|
-
return { provider: "kinescope", normalizedUrl: `https://kinescope.io/embed/${kinescopeId}` };
|
|
119633
|
-
if (trimmed.includes("kinescope.io"))
|
|
119634
|
-
return { provider: "kinescope", normalizedUrl: null };
|
|
119635
|
-
const dzenId = extractPathId(trimmed, "dzen.ru", [/\/embed\/([a-zA-Z0-9_-]+)/, /\/video\/watch\/([a-zA-Z0-9_-]+)/]);
|
|
119636
|
-
if (dzenId)
|
|
119637
|
-
return { provider: "dzen", normalizedUrl: `https://dzen.ru/embed/${dzenId}` };
|
|
119638
|
-
if (trimmed.includes("dzen.ru"))
|
|
119639
|
-
return { provider: "dzen", normalizedUrl: null };
|
|
119640
|
-
const twitchEmbed = extractTwitchEmbed(trimmed);
|
|
119641
|
-
if (twitchEmbed)
|
|
119642
|
-
return { provider: "twitch", normalizedUrl: twitchEmbed };
|
|
119643
|
-
if (trimmed.includes("twitch.tv"))
|
|
119644
|
-
return { provider: "twitch", normalizedUrl: null };
|
|
119645
|
-
const dailymotionId = extractPathId(trimmed, "dailymotion.com", [/\/embed\/video\/([a-zA-Z0-9]+)/, /\/video\/([a-zA-Z0-9]+)/]) ?? extractPathId(trimmed, "dai.ly", [/^\/([a-zA-Z0-9]+)/]);
|
|
119646
|
-
if (dailymotionId)
|
|
119647
|
-
return { provider: "dailymotion", normalizedUrl: `https://www.dailymotion.com/embed/video/${dailymotionId}` };
|
|
119648
|
-
if (trimmed.includes("dailymotion.com") || trimmed.includes("dai.ly"))
|
|
119649
|
-
return { provider: "dailymotion", normalizedUrl: null };
|
|
119650
|
-
const wistiaId = extractPathId(trimmed, "wistia.com", [/\/medias\/([a-zA-Z0-9]+)/]) ?? extractPathId(trimmed, "fast.wistia.net", [/\/embed\/iframe\/([a-zA-Z0-9]+)/]);
|
|
119651
|
-
if (wistiaId)
|
|
119652
|
-
return { provider: "wistia", normalizedUrl: `https://fast.wistia.net/embed/iframe/${wistiaId}` };
|
|
119653
|
-
if (trimmed.includes("wistia.com") || trimmed.includes("wistia.net"))
|
|
119654
|
-
return { provider: "wistia", normalizedUrl: null };
|
|
119655
|
-
return { provider: "unknown", normalizedUrl: null };
|
|
119709
|
+
const host = parsed.hostname.toLowerCase();
|
|
119710
|
+
const service = SERVICES.find((candidate) => candidate.owns(host));
|
|
119711
|
+
if (!service)
|
|
119712
|
+
return { provider: "unknown", normalizedUrl: null };
|
|
119713
|
+
return { provider: service.provider, normalizedUrl: service.player(parsed, address, host) ?? service.page(parsed, host) };
|
|
119714
|
+
}
|
|
119715
|
+
function readVideoInput(input) {
|
|
119716
|
+
const address = readVideoAddress(input);
|
|
119717
|
+
if (address === null)
|
|
119718
|
+
return { address: null, provider: "unknown", embedUrl: null, how: "none" };
|
|
119719
|
+
const detected = detectAndNormalize(address);
|
|
119720
|
+
if (detected.provider === "unknown")
|
|
119721
|
+
return { address, provider: "unknown", embedUrl: address, how: "unknown" };
|
|
119722
|
+
if (detected.normalizedUrl === null) {
|
|
119723
|
+
return { address, provider: detected.provider, embedUrl: address, how: extractIframeSrc(input) !== null ? "player" : "unread" };
|
|
119724
|
+
}
|
|
119725
|
+
return { address, provider: detected.provider, embedUrl: detected.normalizedUrl, how: detected.normalizedUrl === address ? "player" : "page" };
|
|
119656
119726
|
}
|
|
119657
119727
|
|
|
119658
119728
|
// build/tools/pollModel.js
|
|
@@ -119895,12 +119965,20 @@ function normalizeVideo(videoUrl) {
|
|
|
119895
119965
|
const raw = videoUrl?.trim();
|
|
119896
119966
|
if (!raw)
|
|
119897
119967
|
return { originalUrl: null, embedUrl: null };
|
|
119898
|
-
const
|
|
119899
|
-
|
|
119900
|
-
|
|
119901
|
-
|
|
119902
|
-
|
|
119903
|
-
|
|
119968
|
+
const reading = readVideoInput(raw);
|
|
119969
|
+
return { originalUrl: raw, embedUrl: assertEmbeddableUrl(reading.embedUrl ?? raw) };
|
|
119970
|
+
}
|
|
119971
|
+
function videoLinkWarning(videoUrl) {
|
|
119972
|
+
const raw = videoUrl?.trim();
|
|
119973
|
+
if (!raw)
|
|
119974
|
+
return null;
|
|
119975
|
+
const reading = readVideoInput(raw);
|
|
119976
|
+
if (reading.how !== "unread")
|
|
119977
|
+
return null;
|
|
119978
|
+
return {
|
|
119979
|
+
code: "video_link_unread",
|
|
119980
|
+
message: `The link was recognized as ${reading.provider}, but no video was found in it. It is stored as it stands and the player may stay empty: open the page to check, or pass the embed code (<iframe \u2026>) from the service's "Share \u2192 Embed".`
|
|
119981
|
+
};
|
|
119904
119982
|
}
|
|
119905
119983
|
function csvRows(headers, rows) {
|
|
119906
119984
|
return [headers, ...rows].map((row) => row.map(csvCell).join(",")).join("\n");
|
|
@@ -123005,6 +123083,9 @@ function resolve3(enabled, setupVisibility, override, scheduledForFuture = false
|
|
|
123005
123083
|
return { enabled: true, visibleNow: false, decidedBy: "setup schedule" };
|
|
123006
123084
|
return { enabled: true, visibleNow: setupVisibility !== "hidden", decidedBy: "setup default" };
|
|
123007
123085
|
}
|
|
123086
|
+
function placeholderLive(enabled, override) {
|
|
123087
|
+
return resolve3(enabled, "hidden", override);
|
|
123088
|
+
}
|
|
123008
123089
|
async function readLiveLayer(eventId, data, nowMs) {
|
|
123009
123090
|
const blocks = record2(data.blocks);
|
|
123010
123091
|
let ctrl;
|
|
@@ -123033,7 +123114,7 @@ async function readLiveLayer(eventId, data, nowMs) {
|
|
|
123033
123114
|
banner: resolve3(banner.enabled === true, banner.visibility, ctrl.bannerVisible, showAt !== null && showAt > nowMs),
|
|
123034
123115
|
cta: resolve3(cta.enabled === true, cta.visibility, ctrl.ctaVisible),
|
|
123035
123116
|
cards: resolve3(cards.enabled === true, cards.visibility, ctrl.cardsVisible),
|
|
123036
|
-
placeholder:
|
|
123117
|
+
placeholder: placeholderLive(placeholder.enabled === true, ctrl.placeholderVisible),
|
|
123037
123118
|
chat: { panel: chat.enabled === true, accepting }
|
|
123038
123119
|
};
|
|
123039
123120
|
const hints = [LIVE_FIELDS_HINT];
|
|
@@ -123699,7 +123780,7 @@ function registerEventTools(server2) {
|
|
|
123699
123780
|
args: [
|
|
123700
123781
|
"title: webinar title.",
|
|
123701
123782
|
"workspaceId: optional workspace id; omitted means personal workspace.",
|
|
123702
|
-
"videoUrl: optional iframe
|
|
123783
|
+
"videoUrl: optional link to the video or the service's embed code (<iframe \u2026>); OKO reads the player address out of it \u2014 an embed code or a player address is stored as it stands, a link it cannot read is stored too and named in warnings (video_link_unread).",
|
|
123703
123784
|
"eventDate: optional ISO datetime.",
|
|
123704
123785
|
"chatEnabled: whether the chat block is on the page at creation (default true \u2014 the chat exists practically always). It does NOT open message sending: a new event starts closed, so the chat is visible and silent until the organizer opens it (oko_set_chat_mode, or the analytics switch), sets blocks.chat.intake to open, or enables the session triggers in oko_configure_blocks."
|
|
123705
123786
|
],
|
|
@@ -123746,7 +123827,7 @@ function registerEventTools(server2) {
|
|
|
123746
123827
|
...eventLinks(docRef.id),
|
|
123747
123828
|
workspaceId,
|
|
123748
123829
|
runtime: created.runtime,
|
|
123749
|
-
warnings: sessionWarnings,
|
|
123830
|
+
warnings: [...videoLinkWarning(args.videoUrl) ? [videoLinkWarning(args.videoUrl)] : [], ...sessionWarnings],
|
|
123750
123831
|
hints: [
|
|
123751
123832
|
"Analytics sessions: the page starts and ends its session by itself at the event time (sessionMode schedule; a session costs 1 lumen per minute, taken when it ends). Change the time of the session or turn the auto start off with oko_update_event (sessionMode, sessionStartAt, sessionEndAt).",
|
|
123752
123833
|
...eventRuntimeHint(created.runtime),
|
|
@@ -123763,7 +123844,7 @@ function registerEventTools(server2) {
|
|
|
123763
123844
|
exampleCall: { "eventId": "Kx9pQ2mR7tL4vB8nW3cY", "title": "Autumn product webinar (rescheduled)", "eventDate": "2026-09-12T07:00:00.000Z", "speakerName": "Jane Doe", "description": "", "durationMinutes": 60 },
|
|
123764
123845
|
args: ["eventId plus any subset of title, videoUrl, eventDate, speakerName, description, pinned, sessionMode, durationMinutes, sessionStartAt, sessionEndAt, pageTheme, pageAccent."],
|
|
123765
123846
|
returns: "event id, the list of Firestore fields updated, and session warnings (plan versus event time, lumens versus plan, a plan with no sessions).",
|
|
123766
|
-
examples: ["Use for title/date/speaker/video edits; use oko_configure_blocks for blocks and forms."],
|
|
123847
|
+
examples: ["Use for title/date/speaker/video edits; use oko_configure_blocks for blocks and forms.", "videoUrl takes a link to the video or the service's embed code (<iframe \u2026>); a link OKO cannot read is stored as it stands and named in warnings (video_link_unread)."],
|
|
123767
123848
|
notes: [
|
|
123768
123849
|
"An ANALYTICS SESSION measures the show and costs 1 lumen per minute, taken when it ends (no debt; it stops by itself when the lumens run out). It always has a start and an end: the page's session plan \u2014 sessionStartAt and sessionEndAt, both or neither \u2014 and without them the plan is the event time (eventDate to eventDate + durationMinutes).",
|
|
123769
123850
|
"sessionMode now means ONLY the automatic start: 'schedule' (the default of every new page) starts the session by itself at the plan's start; 'manual' turns that off \u2014 the organizer starts it with oko_start_session and a Telegram reminder comes 10 minutes before eventDate. Either way the session ends at the end of its plan, and oko_extend_session moves that end while it runs. This is still an analytics session, not an event status.",
|
|
@@ -123846,7 +123927,9 @@ function registerEventTools(server2) {
|
|
|
123846
123927
|
await writeEventPatchWithCards(args.eventId, patch, null, validate);
|
|
123847
123928
|
const touchesSession = ["meta.eventDate", "meta.durationMinutes", "meta.sessionStartAt", "meta.sessionEndAt", "analyticsSessionMode"].some((key) => key in patch);
|
|
123848
123929
|
const sessionWarnings = touchesSession ? await collectSessionWarnings(args.eventId, await readEvent(args.eventId, uid, "read")) : [];
|
|
123930
|
+
const videoWarning = videoLinkWarning(args.videoUrl);
|
|
123849
123931
|
const warnings = [
|
|
123932
|
+
...videoWarning ? [videoWarning] : [],
|
|
123850
123933
|
...dateAdvice.map((message) => ({ code: "event_date_advice", message })),
|
|
123851
123934
|
...sessionWarnings
|
|
123852
123935
|
];
|