Загрузка данных
// ==UserScript==
// @name [VOT] - Voice Over Translation
// @name:de [VOT] - Voice-Over-Video-Übersetzung
// @name:es [VOT] - Traducción de vídeo en off
// @name:fr [VOT] - Traduction vidéo voix-off
// @name:it [VOT] - Traduzione Video fuori campo
// @name:ru [VOT] - Закадровый перевод видео
// @name:zh [VOT] - 画外音视频翻译
// @namespace vot
// @version 1.11.5.7
// @author Toil, SashaXser, MrSoczekXD, mynovelhost, sodapng
// @description A small extension that adds a Yandex Browser video translation to other browsers
// @description:de Eine kleine Erweiterung, die eine Voice-over-Übersetzung von Videos aus dem Yandex-Browser zu anderen Browsern hinzufügt
// @description:es Una pequeña extensión que agrega una traducción de voz en off de un video de Yandex Browser a otros navegadores
// @description:fr Une petite extension qui ajoute la traduction vocale de la vidéo du Navigateur Yandex à d'autres navigateurs
// @description:it Una piccola estensione che aggiunge la traduzione vocale del video dal browser Yandex ad altri browser
// @description:ru Небольшое расширение, которое добавляет закадровый перевод видео из Яндекс Браузера в другие браузеры
// @description:zh 一个小扩展,它增加了视频从Yandex浏览器到其他浏览器的画外音翻译
// @license MIT
// @icon https://translate.yandex.ru/icons/favicon.ico
// @homepageURL https://github.com/ilyhalight/voice-over-translation
// @source https://github.com/ilyhalight/voice-over-translation.git
// @supportURL https://github.com/ilyhalight/voice-over-translation/issues
// @downloadURL https://raw.githubusercontent.com/ilyhalight/voice-over-translation/master/dist/vot.user.js
// @updateURL https://raw.githubusercontent.com/ilyhalight/voice-over-translation/master/dist/vot.user.js
// @match *://*.youtube.com/*
// @match *://*.youtube-nocookie.com/*
// @match *://*.youtubekids.com/*
// @match *://*.twitch.tv/*
// @match *://*.xvideos.com/*
// @match *://*.xvideos-ar.com/*
// @match *://*.xvideos005.com/*
// @match *://*.xv-ru.com/*
// @match *://*.xhamster.com/*
// @match *://*.xhamster.desi/*
// @match *://*.xhvid.com/*
// @match *://*.spankbang.com/*
// @match *://*.rule34video.com/*
// @match *://*.picarto.tv/*
// @match *://*.olympics.com/*
// @match *://*.pornhub.com/*
// @match *://*.pornhub.org/*
// @match *://*.vk.com/*
// @match *://*.vkvideo.ru/*
// @match *://*.vk.ru/*
// @match *://*.vimeo.com/*
// @match *://*.imdb.com/*
// @match *://*.9gag.com/*
// @match *://*.twitter.com/*
// @match *://*.x.com/*
// @match *://*.facebook.com/*
// @match *://*.rutube.ru/*
// @match *://*.bilibili.com/*
// @match *://*.bilibili.tv/*
// @match *://my.mail.ru/*
// @match *://*.bitchute.com/*
// @match *://*.coursera.org/*
// @match *://*.udemy.com/course/*
// @match *://*.tiktok.com/*
// @match *://*.douyin.com/*
// @match *://rumble.com/*
// @match *://*.eporner.com/*
// @match *://*.dailymotion.com/*
// @match *://*.ok.ru/*
// @match *://trovo.live/*
// @match *://disk.yandex.ru/*
// @match *://disk.yandex.kz/*
// @match *://disk.yandex.com/*
// @match *://disk.yandex.com.am/*
// @match *://disk.yandex.com.ge/*
// @match *://disk.yandex.com.tr/*
// @match *://disk.yandex.by/*
// @match *://disk.yandex.az/*
// @match *://disk.yandex.co.il/*
// @match *://disk.yandex.ee/*
// @match *://disk.yandex.lt/*
// @match *://disk.yandex.lv/*
// @match *://disk.yandex.md/*
// @match *://disk.yandex.net/*
// @match *://disk.yandex.tj/*
// @match *://disk.yandex.tm/*
// @match *://disk.yandex.uz/*
// @match *://disk.360.yandex.ru/*
// @match *://youtube.googleapis.com/embed/*
// @match *://*.banned.video/*
// @match *://*.madmaxworld.tv/*
// @match *://*.weverse.io/*
// @match *://*.newgrounds.com/*
// @match *://*.egghead.io/*
// @match *://*.youku.com/*
// @match *://*.archive.org/*
// @match *://*.patreon.com/*
// @match *://*.reddit.com/*
// @match *://*.kodikplayer.com/*
// @match *://*.kick.com/*
// @match *://developer.apple.com/*
// @match *://dev.epicgames.com/*
// @match *://*.rapid-cloud.co/*
// @match *://odysee.com/*
// @match *://learning.sap.com/*
// @match *://*.watchporn.to/*
// @match *://*.linkedin.com/*
// @match *://*.incestflix.net/*
// @match *://*.incestflix.to/*
// @match *://*.porntn.com/*
// @match *://*.dzen.ru/*
// @match *://*.cloudflarestream.com/*
// @match *://*.loom.com/*
// @match *://*.artstation.com/learning/*
// @match *://*.rt.com/*
// @match *://*.bitview.net/*
// @match *://*.kickstarter.com/*
// @match *://*.thisvid.com/*
// @match *://*.ign.com/*
// @match *://*.bunkr.site/*
// @match *://*.bunkr.black/*
// @match *://*.bunkr.cat/*
// @match *://*.bunkr.media/*
// @match *://*.bunkr.red/*
// @match *://*.bunkr.ws/*
// @match *://*.bunkr.org/*
// @match *://*.bunkr.sk/*
// @match *://*.bunkr.si/*
// @match *://*.bunkr.su/*
// @match *://*.bunkr.ci/*
// @match *://*.bunkr.cr/*
// @match *://*.bunkr.fi/*
// @match *://*.bunkr.ph/*
// @match *://*.bunkr.pk/*
// @match *://*.bunkr.ps/*
// @match *://*.bunkr.ru/*
// @match *://*.bunkr.la/*
// @match *://*.bunkr.is/*
// @match *://*.bunkr.to/*
// @match *://*.bunkr.ac/*
// @match *://*.bunkr.ax/*
// @match *://web.telegram.org/k/*
// @match *://rust-server-531j.onrender.com/*
// @match *://mylearn.oracle.com/*
// @match *://learn.deeplearning.ai/*
// @match *://learn-staging.deeplearning.ai/*
// @match *://learn-dev.deeplearning.ai/*
// @match *://*.netacad.com/content/i2cs/*
// @match *://*.nicovideo.jp/*
// @match *://*.zdf.de/*
// @match *://iframe.mediadelivery.net/*
// @match *://video.bunnycdn.com/*
// @match *://*.weibo.com/*
// @match *://*.jove.com/*
// @match *://*.preservetube.com/*
// @match *://*.mediafile.cc/*
// @match *://*.skilljar.com/*
// @match *://projector.datacamp.com/*
// @match *://*/*.mp4*
// @match *://*/*.webm*
// @match *://*.yewtu.be/*
// @match *://inv.nadeko.net/*
// @match *://invidious.nerdvpn.de/*
// @match *://invidious.protokolla.fi/*
// @match *://invidious.materialio.us/*
// @match *://iv.melmac.space/*
// @match *://*.piped.video/*
// @match *://piped.kavin.rocks/*
// @match *://piped.private.coffee/*
// @match *://proxitok.pabloferreiro.es/*
// @match *://proxitok.pussthecat.org/*
// @match *://tok.habedieeh.re/*
// @match *://proxitok.esmailelbob.xyz/*
// @match *://proxitok.privacydev.net/*
// @match *://tok.artemislena.eu/*
// @match *://tok.adminforge.de/*
// @match *://tt.vern.cc/*
// @match *://cringe.whatever.social/*
// @match *://proxitok.lunar.icu/*
// @match *://proxitok.privacy.com.de/*
// @match *://peertube.tmp.rcp.tf/*
// @match *://*.dalek.zone/*
// @match *://video.sadmin.io/*
// @match *://videos.viorsan.com/*
// @match *://peertube.1312.media/*
// @match *://tube.shanti.cafe/*
// @match *://*.bee-tube.fr/*
// @match *://video.blender.org/*
// @match *://*.beetoons.tv/*
// @match *://*.makertube.net/*
// @match *://*.peertube.tv/*
// @match *://*.framatube.org/*
// @match *://*.tilvids.com/*
// @match *://*.diode.zone/*
// @match *://*.fedimovie.com/*
// @match *://video.hardlimit.com/*
// @match *://*.share.tube/*
// @match *://*.peervideo.club/*
// @match *://*.coursehunter.net/*
// @match *://*.coursetrain.net/*
// @exclude file://*/*.mp4*
// @exclude file://*/*.webm*
// @exclude *://accounts.youtube.com/*
// @require https://gist.githubusercontent.com/ilyhalight/6eb5bb4dffc7ca9e3c57d6933e2452f3/raw/7ab38af2228d0bed13912e503bc8a9ee4b11828d/gm-addstyle-polyfill.js
// @connect yandex.ru
// @connect disk.yandex.kz
// @connect disk.yandex.com
// @connect disk.yandex.com.am
// @connect disk.yandex.com.ge
// @connect disk.yandex.com.tr
// @connect disk.yandex.by
// @connect disk.yandex.az
// @connect disk.yandex.co.il
// @connect disk.yandex.ee
// @connect disk.yandex.lt
// @connect disk.yandex.lv
// @connect disk.yandex.md
// @connect disk.yandex.net
// @connect disk.yandex.tj
// @connect disk.yandex.tm
// @connect disk.yandex.uz
// @connect disk.360.yandex.ru
// @connect yandex.net
// @connect timeweb.cloud
// @connect raw.githubusercontent.com
// @connect vimeo.com
// @connect toil.cc
// @connect deno.dev
// @connect onrender.com
// @connect workers.dev
// @connect eu.cc
// @connect cloudflare-dns.com
// @connect porntn.com
// @connect youtube.com
// @connect googlevideo.com
// @grant GM_addStyle
// @grant GM_deleteValue
// @grant GM_getValue
// @grant GM_info
// @grant GM_listValues
// @grant GM_notification
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant GM.deleteValue
// @grant GM.getValue
// @grant GM.getValues
// @grant GM.listValues
// @grant GM.notification
// @grant GM.setValue
// @grant GM.xmlHttpRequest
// @grant window.focus
// ==/UserScript==
var vot = (function(exports) {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
//#region \0rolldown/runtime.js
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
var __exportAll = (all, no_symbols) => {
let target = {};
for (var name in all) __defProp(target, name, {
get: all[name],
enumerable: true
});
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
return target;
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
//#region node_modules/@vot.js/shared/dist/data/config.js
var config_default$1 = {
"host": "api.browser.yandex.ru",
"hostVOT": "vot.toil.cc/v1",
"hostWorker": "vot-worker.toil.cc",
"mediaProxy": "media-proxy.transly.eu.cc",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 YaBrowser/26.3.3.869 Yowser/2.5 Safari/537.36",
"componentVersion": "26.3.3.869",
"hmac": "bt8xH3VOlb4mqf0nqAibnDOoiPlXsisf",
"defaultDuration": 310,
"minChunkSize": 5295308,
"loggerLevel": 1,
"version": "2.4.17"
};
//#endregion
//#region node_modules/@bufbuild/protobuf/dist/esm/wire/varint.js
/**
* Read a 64 bit varint as two JS numbers.
*
* Returns tuple:
* [0]: low bits
* [1]: high bits
*
* Copyright 2008 Google Inc. All rights reserved.
*
* See https://github.com/protocolbuffers/protobuf/blob/8a71927d74a4ce34efe2d8769fda198f52d20d12/js/experimental/runtime/kernel/buffer_decoder.js#L175
*/
function varint64read() {
let lowBits = 0;
let highBits = 0;
for (let shift = 0; shift < 28; shift += 7) {
let b = this.buf[this.pos++];
lowBits |= (b & 127) << shift;
if ((b & 128) == 0) {
this.assertBounds();
return [lowBits, highBits];
}
}
let middleByte = this.buf[this.pos++];
lowBits |= (middleByte & 15) << 28;
highBits = (middleByte & 112) >> 4;
if ((middleByte & 128) == 0) {
this.assertBounds();
return [lowBits, highBits];
}
for (let shift = 3; shift <= 31; shift += 7) {
let b = this.buf[this.pos++];
highBits |= (b & 127) << shift;
if ((b & 128) == 0) {
this.assertBounds();
return [lowBits, highBits];
}
}
throw new Error("invalid varint");
}
/**
* Write a 64 bit varint, given as two JS numbers, to the given bytes array.
*
* Copyright 2008 Google Inc. All rights reserved.
*
* See https://github.com/protocolbuffers/protobuf/blob/8a71927d74a4ce34efe2d8769fda198f52d20d12/js/experimental/runtime/kernel/writer.js#L344
*/
function varint64write(lo, hi, bytes) {
for (let i = 0; i < 28; i = i + 7) {
const shift = lo >>> i;
const hasNext = !(shift >>> 7 == 0 && hi == 0);
const byte = (hasNext ? shift | 128 : shift) & 255;
bytes.push(byte);
if (!hasNext) return;
}
const splitBits = lo >>> 28 & 15 | (hi & 7) << 4;
const hasMoreBits = !(hi >> 3 == 0);
bytes.push((hasMoreBits ? splitBits | 128 : splitBits) & 255);
if (!hasMoreBits) return;
for (let i = 3; i < 31; i = i + 7) {
const shift = hi >>> i;
const hasNext = !(shift >>> 7 == 0);
const byte = (hasNext ? shift | 128 : shift) & 255;
bytes.push(byte);
if (!hasNext) return;
}
bytes.push(hi >>> 31 & 1);
}
var TWO_PWR_32_DBL = 4294967296;
/**
* Parse decimal string of 64 bit integer value as two JS numbers.
*
* Copyright 2008 Google Inc. All rights reserved.
*
* See https://github.com/protocolbuffers/protobuf-javascript/blob/a428c58273abad07c66071d9753bc4d1289de426/experimental/runtime/int64.js#L10
*/
function int64FromString(dec) {
const minus = dec[0] === "-";
if (minus) dec = dec.slice(1);
const base = 1e6;
let lowBits = 0;
let highBits = 0;
function add1e6digit(begin, end) {
const digit1e6 = Number(dec.slice(begin, end));
highBits *= base;
lowBits = lowBits * base + digit1e6;
if (lowBits >= TWO_PWR_32_DBL) {
highBits = highBits + (lowBits / TWO_PWR_32_DBL | 0);
lowBits = lowBits % TWO_PWR_32_DBL;
}
}
add1e6digit(-24, -18);
add1e6digit(-18, -12);
add1e6digit(-12, -6);
add1e6digit(-6);
return minus ? negate(lowBits, highBits) : newBits(lowBits, highBits);
}
/**
* Losslessly converts a 64-bit signed integer in 32:32 split representation
* into a decimal string.
*
* Copyright 2008 Google Inc. All rights reserved.
*
* See https://github.com/protocolbuffers/protobuf-javascript/blob/a428c58273abad07c66071d9753bc4d1289de426/experimental/runtime/int64.js#L10
*/
function int64ToString(lo, hi) {
let bits = newBits(lo, hi);
const negative = bits.hi & 2147483648;
if (negative) bits = negate(bits.lo, bits.hi);
const result = uInt64ToString(bits.lo, bits.hi);
return negative ? "-" + result : result;
}
/**
* Losslessly converts a 64-bit unsigned integer in 32:32 split representation
* into a decimal string.
*
* Copyright 2008 Google Inc. All rights reserved.
*
* See https://github.com/protocolbuffers/protobuf-javascript/blob/a428c58273abad07c66071d9753bc4d1289de426/experimental/runtime/int64.js#L10
*/
function uInt64ToString(lo, hi) {
({lo, hi} = toUnsigned(lo, hi));
if (hi <= 2097151) return String(TWO_PWR_32_DBL * hi + lo);
const low = lo & 16777215;
const mid = (lo >>> 24 | hi << 8) & 16777215;
const high = hi >> 16 & 65535;
let digitA = low + mid * 6777216 + high * 6710656;
let digitB = mid + high * 8147497;
let digitC = high * 2;
const base = 1e7;
if (digitA >= base) {
digitB += Math.floor(digitA / base);
digitA %= base;
}
if (digitB >= base) {
digitC += Math.floor(digitB / base);
digitB %= base;
}
return digitC.toString() + decimalFrom1e7WithLeadingZeros(digitB) + decimalFrom1e7WithLeadingZeros(digitA);
}
function toUnsigned(lo, hi) {
return {
lo: lo >>> 0,
hi: hi >>> 0
};
}
function newBits(lo, hi) {
return {
lo: lo | 0,
hi: hi | 0
};
}
/**
* Returns two's compliment negation of input.
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#Signed_32-bit_integers
*/
function negate(lowBits, highBits) {
highBits = ~highBits;
if (lowBits) lowBits = ~lowBits + 1;
else highBits += 1;
return newBits(lowBits, highBits);
}
/**
* Returns decimal representation of digit1e7 with leading zeros.
*/
var decimalFrom1e7WithLeadingZeros = (digit1e7) => {
const partial = String(digit1e7);
return "0000000".slice(partial.length) + partial;
};
/**
* Write a 32 bit varint, signed or unsigned. Same as `varint64write(0, value, bytes)`
*
* Copyright 2008 Google Inc. All rights reserved.
*
* See https://github.com/protocolbuffers/protobuf/blob/1b18833f4f2a2f681f4e4a25cdf3b0a43115ec26/js/binary/encoder.js#L144
*/
function varint32write(value, bytes) {
if (value >= 0) {
while (value > 127) {
bytes.push(value & 127 | 128);
value = value >>> 7;
}
bytes.push(value);
} else {
for (let i = 0; i < 9; i++) {
bytes.push(value & 127 | 128);
value = value >> 7;
}
bytes.push(1);
}
}
/**
* Read an unsigned 32 bit varint.
*
* See https://github.com/protocolbuffers/protobuf/blob/8a71927d74a4ce34efe2d8769fda198f52d20d12/js/experimental/runtime/kernel/buffer_decoder.js#L220
*/
function varint32read() {
let b = this.buf[this.pos++];
let result = b & 127;
if ((b & 128) == 0) {
this.assertBounds();
return result;
}
b = this.buf[this.pos++];
result |= (b & 127) << 7;
if ((b & 128) == 0) {
this.assertBounds();
return result;
}
b = this.buf[this.pos++];
result |= (b & 127) << 14;
if ((b & 128) == 0) {
this.assertBounds();
return result;
}
b = this.buf[this.pos++];
result |= (b & 127) << 21;
if ((b & 128) == 0) {
this.assertBounds();
return result;
}
b = this.buf[this.pos++];
result |= (b & 15) << 28;
for (let readBytes = 5; (b & 128) !== 0 && readBytes < 10; readBytes++) b = this.buf[this.pos++];
if ((b & 128) != 0) throw new Error("invalid varint");
this.assertBounds();
return result >>> 0;
}
//#endregion
//#region node_modules/@bufbuild/protobuf/dist/esm/proto-int64.js
/**
* Int64Support for the current environment.
*/
var protoInt64 = /* @__PURE__ */ makeInt64Support();
function makeInt64Support() {
const dv = /* @__PURE__ */ new DataView(/* @__PURE__ */ new ArrayBuffer(8));
if (typeof BigInt === "function" && typeof dv.getBigInt64 === "function" && typeof dv.getBigUint64 === "function" && typeof dv.setBigInt64 === "function" && typeof dv.setBigUint64 === "function" && (typeof process != "object" || typeof process.env != "object" || process.env.BUF_BIGINT_DISABLE !== "1")) {
const MIN = BigInt("-9223372036854775808"), MAX = BigInt("9223372036854775807"), UMIN = BigInt("0"), UMAX = BigInt("18446744073709551615");
return {
zero: BigInt(0),
supported: true,
parse(value) {
const bi = typeof value == "bigint" ? value : BigInt(value);
if (bi > MAX || bi < MIN) throw new Error(`invalid int64: ${value}`);
return bi;
},
uParse(value) {
const bi = typeof value == "bigint" ? value : BigInt(value);
if (bi > UMAX || bi < UMIN) throw new Error(`invalid uint64: ${value}`);
return bi;
},
enc(value) {
dv.setBigInt64(0, this.parse(value), true);
return {
lo: dv.getInt32(0, true),
hi: dv.getInt32(4, true)
};
},
uEnc(value) {
dv.setBigInt64(0, this.uParse(value), true);
return {
lo: dv.getInt32(0, true),
hi: dv.getInt32(4, true)
};
},
dec(lo, hi) {
dv.setInt32(0, lo, true);
dv.setInt32(4, hi, true);
return dv.getBigInt64(0, true);
},
uDec(lo, hi) {
dv.setInt32(0, lo, true);
dv.setInt32(4, hi, true);
return dv.getBigUint64(0, true);
}
};
}
return {
zero: "0",
supported: false,
parse(value) {
if (typeof value != "string") value = value.toString();
assertInt64String(value);
return value;
},
uParse(value) {
if (typeof value != "string") value = value.toString();
assertUInt64String(value);
return value;
},
enc(value) {
if (typeof value != "string") value = value.toString();
assertInt64String(value);
return int64FromString(value);
},
uEnc(value) {
if (typeof value != "string") value = value.toString();
assertUInt64String(value);
return int64FromString(value);
},
dec(lo, hi) {
return int64ToString(lo, hi);
},
uDec(lo, hi) {
return uInt64ToString(lo, hi);
}
};
}
function assertInt64String(value) {
if (!/^-?[0-9]+$/.test(value)) throw new Error("invalid int64: " + value);
}
function assertUInt64String(value) {
if (!/^[0-9]+$/.test(value)) throw new Error("invalid uint64: " + value);
}
//#endregion
//#region node_modules/@bufbuild/protobuf/dist/esm/wire/text-encoding.js
var symbol = Symbol.for("@bufbuild/protobuf/text-encoding");
function getTextEncoding() {
if (globalThis[symbol] == void 0) {
const te = new globalThis.TextEncoder();
const td = new globalThis.TextDecoder();
globalThis[symbol] = {
encodeUtf8(text) {
return te.encode(text);
},
decodeUtf8(bytes) {
return td.decode(bytes);
},
checkUtf8(text) {
try {
return true;
} catch (e) {
return false;
}
}
};
}
return globalThis[symbol];
}
//#endregion
//#region node_modules/@bufbuild/protobuf/dist/esm/wire/binary-encoding.js
/**
* Protobuf binary format wire types.
*
* A wire type provides just enough information to find the length of the
* following value.
*
* See https://developers.google.com/protocol-buffers/docs/encoding#structure
*/
var WireType;
(function(WireType) {
/**
* Used for int32, int64, uint32, uint64, sint32, sint64, bool, enum
*/
WireType[WireType["Varint"] = 0] = "Varint";
/**
* Used for fixed64, sfixed64, double.
* Always 8 bytes with little-endian byte order.
*/
WireType[WireType["Bit64"] = 1] = "Bit64";
/**
* Used for string, bytes, embedded messages, packed repeated fields
*
* Only repeated numeric types (types which use the varint, 32-bit,
* or 64-bit wire types) can be packed. In proto3, such fields are
* packed by default.
*/
WireType[WireType["LengthDelimited"] = 2] = "LengthDelimited";
/**
* Start of a tag-delimited aggregate, such as a proto2 group, or a message
* in editions with message_encoding = DELIMITED.
*/
WireType[WireType["StartGroup"] = 3] = "StartGroup";
/**
* End of a tag-delimited aggregate.
*/
WireType[WireType["EndGroup"] = 4] = "EndGroup";
/**
* Used for fixed32, sfixed32, float.
* Always 4 bytes with little-endian byte order.
*/
WireType[WireType["Bit32"] = 5] = "Bit32";
})(WireType || (WireType = {}));
var BinaryWriter = class {
constructor(encodeUtf8 = getTextEncoding().encodeUtf8) {
this.encodeUtf8 = encodeUtf8;
/**
* Previous fork states.
*/
this.stack = [];
this.chunks = [];
this.buf = [];
}
/**
* Return all bytes written and reset this writer.
*/
finish() {
if (this.buf.length) {
this.chunks.push(new Uint8Array(this.buf));
this.buf = [];
}
let len = 0;
for (let i = 0; i < this.chunks.length; i++) len += this.chunks[i].length;
let bytes = new Uint8Array(len);
let offset = 0;
for (let i = 0; i < this.chunks.length; i++) {
bytes.set(this.chunks[i], offset);
offset += this.chunks[i].length;
}
this.chunks = [];
return bytes;
}
/**
* Start a new fork for length-delimited data like a message
* or a packed repeated field.
*
* Must be joined later with `join()`.
*/
fork() {
this.stack.push({
chunks: this.chunks,
buf: this.buf
});
this.chunks = [];
this.buf = [];
return this;
}
/**
* Join the last fork. Write its length and bytes, then
* return to the previous state.
*/
join() {
let chunk = this.finish();
let prev = this.stack.pop();
if (!prev) throw new Error("invalid state, fork stack empty");
this.chunks = prev.chunks;
this.buf = prev.buf;
this.uint32(chunk.byteLength);
return this.raw(chunk);
}
/**
* Writes a tag (field number and wire type).
*
* Equivalent to `uint32( (fieldNo << 3 | type) >>> 0 )`.
*
* Generated code should compute the tag ahead of time and call `uint32()`.
*/
tag(fieldNo, type) {
return this.uint32((fieldNo << 3 | type) >>> 0);
}
/**
* Write a chunk of raw bytes.
*/
raw(chunk) {
if (this.buf.length) {
this.chunks.push(new Uint8Array(this.buf));
this.buf = [];
}
this.chunks.push(chunk);
return this;
}
/**
* Write a `uint32` value, an unsigned 32 bit varint.
*/
uint32(value) {
assertUInt32(value);
while (value > 127) {
this.buf.push(value & 127 | 128);
value = value >>> 7;
}
this.buf.push(value);
return this;
}
/**
* Write a `int32` value, a signed 32 bit varint.
*/
int32(value) {
assertInt32(value);
varint32write(value, this.buf);
return this;
}
/**
* Write a `bool` value, a variant.
*/
bool(value) {
this.buf.push(value ? 1 : 0);
return this;
}
/**
* Write a `bytes` value, length-delimited arbitrary data.
*/
bytes(value) {
this.uint32(value.byteLength);
return this.raw(value);
}
/**
* Write a `string` value, length-delimited data converted to UTF-8 text.
*/
string(value) {
let chunk = this.encodeUtf8(value);
this.uint32(chunk.byteLength);
return this.raw(chunk);
}
/**
* Write a `float` value, 32-bit floating point number.
*/
float(value) {
assertFloat32(value);
let chunk = new Uint8Array(4);
new DataView(chunk.buffer).setFloat32(0, value, true);
return this.raw(chunk);
}
/**
* Write a `double` value, a 64-bit floating point number.
*/
double(value) {
let chunk = new Uint8Array(8);
new DataView(chunk.buffer).setFloat64(0, value, true);
return this.raw(chunk);
}
/**
* Write a `fixed32` value, an unsigned, fixed-length 32-bit integer.
*/
fixed32(value) {
assertUInt32(value);
let chunk = new Uint8Array(4);
new DataView(chunk.buffer).setUint32(0, value, true);
return this.raw(chunk);
}
/**
* Write a `sfixed32` value, a signed, fixed-length 32-bit integer.
*/
sfixed32(value) {
assertInt32(value);
let chunk = new Uint8Array(4);
new DataView(chunk.buffer).setInt32(0, value, true);
return this.raw(chunk);
}
/**
* Write a `sint32` value, a signed, zigzag-encoded 32-bit varint.
*/
sint32(value) {
assertInt32(value);
value = (value << 1 ^ value >> 31) >>> 0;
varint32write(value, this.buf);
return this;
}
/**
* Write a `fixed64` value, a signed, fixed-length 64-bit integer.
*/
sfixed64(value) {
let chunk = new Uint8Array(8), view = new DataView(chunk.buffer), tc = protoInt64.enc(value);
view.setInt32(0, tc.lo, true);
view.setInt32(4, tc.hi, true);
return this.raw(chunk);
}
/**
* Write a `fixed64` value, an unsigned, fixed-length 64 bit integer.
*/
fixed64(value) {
let chunk = new Uint8Array(8), view = new DataView(chunk.buffer), tc = protoInt64.uEnc(value);
view.setInt32(0, tc.lo, true);
view.setInt32(4, tc.hi, true);
return this.raw(chunk);
}
/**
* Write a `int64` value, a signed 64-bit varint.
*/
int64(value) {
let tc = protoInt64.enc(value);
varint64write(tc.lo, tc.hi, this.buf);
return this;
}
/**
* Write a `sint64` value, a signed, zig-zag-encoded 64-bit varint.
*/
sint64(value) {
let tc = protoInt64.enc(value), sign = tc.hi >> 31;
varint64write(tc.lo << 1 ^ sign, (tc.hi << 1 | tc.lo >>> 31) ^ sign, this.buf);
return this;
}
/**
* Write a `uint64` value, an unsigned 64-bit varint.
*/
uint64(value) {
let tc = protoInt64.uEnc(value);
varint64write(tc.lo, tc.hi, this.buf);
return this;
}
};
var BinaryReader = class {
constructor(buf, decodeUtf8 = getTextEncoding().decodeUtf8) {
this.decodeUtf8 = decodeUtf8;
this.varint64 = varint64read;
/**
* Read a `uint32` field, an unsigned 32 bit varint.
*/
this.uint32 = varint32read;
this.buf = buf;
this.len = buf.length;
this.pos = 0;
this.view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
}
/**
* Reads a tag - field number and wire type.
*/
tag() {
let tag = this.uint32(), fieldNo = tag >>> 3, wireType = tag & 7;
if (fieldNo <= 0 || wireType < 0 || wireType > 5) throw new Error("illegal tag: field no " + fieldNo + " wire type " + wireType);
return [fieldNo, wireType];
}
/**
* Skip one element and return the skipped data.
*
* When skipping StartGroup, provide the tags field number to check for
* matching field number in the EndGroup tag.
*/
skip(wireType, fieldNo) {
let start = this.pos;
switch (wireType) {
case WireType.Varint:
while (this.buf[this.pos++] & 128);
break;
case WireType.Bit64: this.pos += 4;
case WireType.Bit32:
this.pos += 4;
break;
case WireType.LengthDelimited:
let len = this.uint32();
this.pos += len;
break;
case WireType.StartGroup:
for (;;) {
const [fn, wt] = this.tag();
if (wt === WireType.EndGroup) {
if (fieldNo !== void 0 && fn !== fieldNo) throw new Error("invalid end group tag");
break;
}
this.skip(wt, fn);
}
break;
default: throw new Error("cant skip wire type " + wireType);
}
this.assertBounds();
return this.buf.subarray(start, this.pos);
}
/**
* Throws error if position in byte array is out of range.
*/
assertBounds() {
if (this.pos > this.len) throw new RangeError("premature EOF");
}
/**
* Read a `int32` field, a signed 32 bit varint.
*/
int32() {
return this.uint32() | 0;
}
/**
* Read a `sint32` field, a signed, zigzag-encoded 32-bit varint.
*/
sint32() {
let zze = this.uint32();
return zze >>> 1 ^ -(zze & 1);
}
/**
* Read a `int64` field, a signed 64-bit varint.
*/
int64() {
return protoInt64.dec(...this.varint64());
}
/**
* Read a `uint64` field, an unsigned 64-bit varint.
*/
uint64() {
return protoInt64.uDec(...this.varint64());
}
/**
* Read a `sint64` field, a signed, zig-zag-encoded 64-bit varint.
*/
sint64() {
let [lo, hi] = this.varint64();
let s = -(lo & 1);
lo = (lo >>> 1 | (hi & 1) << 31) ^ s;
hi = hi >>> 1 ^ s;
return protoInt64.dec(lo, hi);
}
/**
* Read a `bool` field, a variant.
*/
bool() {
let [lo, hi] = this.varint64();
return lo !== 0 || hi !== 0;
}
/**
* Read a `fixed32` field, an unsigned, fixed-length 32-bit integer.
*/
fixed32() {
return this.view.getUint32((this.pos += 4) - 4, true);
}
/**
* Read a `sfixed32` field, a signed, fixed-length 32-bit integer.
*/
sfixed32() {
return this.view.getInt32((this.pos += 4) - 4, true);
}
/**
* Read a `fixed64` field, an unsigned, fixed-length 64 bit integer.
*/
fixed64() {
return protoInt64.uDec(this.sfixed32(), this.sfixed32());
}
/**
* Read a `fixed64` field, a signed, fixed-length 64-bit integer.
*/
sfixed64() {
return protoInt64.dec(this.sfixed32(), this.sfixed32());
}
/**
* Read a `float` field, 32-bit floating point number.
*/
float() {
return this.view.getFloat32((this.pos += 4) - 4, true);
}
/**
* Read a `double` field, a 64-bit floating point number.
*/
double() {
return this.view.getFloat64((this.pos += 8) - 8, true);
}
/**
* Read a `bytes` field, length-delimited arbitrary data.
*/
bytes() {
let len = this.uint32(), start = this.pos;
this.pos += len;
this.assertBounds();
return this.buf.subarray(start, start + len);
}
/**
* Read a `string` field, length-delimited data converted to UTF-8 text.
*/
string() {
return this.decodeUtf8(this.bytes());
}
};
/**
* Assert a valid signed protobuf 32-bit integer as a number or string.
*/
function assertInt32(arg) {
if (typeof arg == "string") arg = Number(arg);
else if (typeof arg != "number") throw new Error("invalid int32: " + typeof arg);
if (!Number.isInteger(arg) || arg > 2147483647 || arg < -2147483648) throw new Error("invalid int32: " + arg);
}
/**
* Assert a valid unsigned protobuf 32-bit integer as a number or string.
*/
function assertUInt32(arg) {
if (typeof arg == "string") arg = Number(arg);
else if (typeof arg != "number") throw new Error("invalid uint32: " + typeof arg);
if (!Number.isInteger(arg) || arg > 4294967295 || arg < 0) throw new Error("invalid uint32: " + arg);
}
/**
* Assert a valid protobuf float value as a number or string.
*/
function assertFloat32(arg) {
if (typeof arg == "string") {
const o = arg;
arg = Number(arg);
if (isNaN(arg) && o !== "NaN") throw new Error("invalid float32: " + o);
} else if (typeof arg != "number") throw new Error("invalid float32: " + typeof arg);
if (Number.isFinite(arg) && (arg > 34028234663852886e22 || arg < -34028234663852886e22)) throw new Error("invalid float32: " + arg);
}
//#endregion
//#region node_modules/@vot.js/shared/dist/protos/yandex.js
var StreamInterval;
(function(StreamInterval) {
StreamInterval[StreamInterval["NO_CONNECTION"] = 0] = "NO_CONNECTION";
StreamInterval[StreamInterval["TRANSLATING"] = 10] = "TRANSLATING";
StreamInterval[StreamInterval["STREAMING"] = 20] = "STREAMING";
StreamInterval[StreamInterval["UNRECOGNIZED"] = -1] = "UNRECOGNIZED";
})(StreamInterval || (StreamInterval = {}));
function streamIntervalFromJSON(object) {
switch (object) {
case 0:
case "NO_CONNECTION": return StreamInterval.NO_CONNECTION;
case 10:
case "TRANSLATING": return StreamInterval.TRANSLATING;
case 20:
case "STREAMING": return StreamInterval.STREAMING;
default: return StreamInterval.UNRECOGNIZED;
}
}
function streamIntervalToJSON(object) {
switch (object) {
case StreamInterval.NO_CONNECTION: return "NO_CONNECTION";
case StreamInterval.TRANSLATING: return "TRANSLATING";
case StreamInterval.STREAMING: return "STREAMING";
case StreamInterval.UNRECOGNIZED:
default: return "UNRECOGNIZED";
}
}
function createBaseVideoTranslationHelpObject() {
return {
target: "",
targetUrl: ""
};
}
var VideoTranslationHelpObject = {
encode(message, writer = new BinaryWriter()) {
if (message.target !== "") writer.uint32(10).string(message.target);
if (message.targetUrl !== "") writer.uint32(18).string(message.targetUrl);
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseVideoTranslationHelpObject();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) break;
message.target = reader.string();
continue;
case 2:
if (tag !== 18) break;
message.targetUrl = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
target: isSet(object.target) ? globalThis.String(object.target) : "",
targetUrl: isSet(object.targetUrl) ? globalThis.String(object.targetUrl) : ""
};
},
toJSON(message) {
const obj = {};
if (message.target !== "") obj.target = message.target;
if (message.targetUrl !== "") obj.targetUrl = message.targetUrl;
return obj;
},
create(base) {
return VideoTranslationHelpObject.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseVideoTranslationHelpObject();
message.target = object.target ?? "";
message.targetUrl = object.targetUrl ?? "";
return message;
}
};
function createBaseVideoTranslationRequest() {
return {
url: "",
deviceId: void 0,
firstRequest: false,
duration: 0,
unknown0: 0,
language: "",
forceSourceLang: false,
unknown1: 0,
translationHelp: [],
wasStream: false,
responseLanguage: "",
unknown2: 0,
unknown3: 0,
bypassCache: false,
useLivelyVoice: false,
videoTitle: ""
};
}
var VideoTranslationRequest = {
encode(message, writer = new BinaryWriter()) {
if (message.url !== "") writer.uint32(26).string(message.url);
if (message.deviceId !== void 0) writer.uint32(34).string(message.deviceId);
if (message.firstRequest !== false) writer.uint32(40).bool(message.firstRequest);
if (message.duration !== 0) writer.uint32(49).double(message.duration);
if (message.unknown0 !== 0) writer.uint32(56).int32(message.unknown0);
if (message.language !== "") writer.uint32(66).string(message.language);
if (message.forceSourceLang !== false) writer.uint32(72).bool(message.forceSourceLang);
if (message.unknown1 !== 0) writer.uint32(80).int32(message.unknown1);
for (const v of message.translationHelp) VideoTranslationHelpObject.encode(v, writer.uint32(90).fork()).join();
if (message.wasStream !== false) writer.uint32(104).bool(message.wasStream);
if (message.responseLanguage !== "") writer.uint32(114).string(message.responseLanguage);
if (message.unknown2 !== 0) writer.uint32(120).int32(message.unknown2);
if (message.unknown3 !== 0) writer.uint32(128).int32(message.unknown3);
if (message.bypassCache !== false) writer.uint32(136).bool(message.bypassCache);
if (message.useLivelyVoice !== false) writer.uint32(144).bool(message.useLivelyVoice);
if (message.videoTitle !== "") writer.uint32(154).string(message.videoTitle);
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseVideoTranslationRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 3:
if (tag !== 26) break;
message.url = reader.string();
continue;
case 4:
if (tag !== 34) break;
message.deviceId = reader.string();
continue;
case 5:
if (tag !== 40) break;
message.firstRequest = reader.bool();
continue;
case 6:
if (tag !== 49) break;
message.duration = reader.double();
continue;
case 7:
if (tag !== 56) break;
message.unknown0 = reader.int32();
continue;
case 8:
if (tag !== 66) break;
message.language = reader.string();
continue;
case 9:
if (tag !== 72) break;
message.forceSourceLang = reader.bool();
continue;
case 10:
if (tag !== 80) break;
message.unknown1 = reader.int32();
continue;
case 11:
if (tag !== 90) break;
message.translationHelp.push(VideoTranslationHelpObject.decode(reader, reader.uint32()));
continue;
case 13:
if (tag !== 104) break;
message.wasStream = reader.bool();
continue;
case 14:
if (tag !== 114) break;
message.responseLanguage = reader.string();
continue;
case 15:
if (tag !== 120) break;
message.unknown2 = reader.int32();
continue;
case 16:
if (tag !== 128) break;
message.unknown3 = reader.int32();
continue;
case 17:
if (tag !== 136) break;
message.bypassCache = reader.bool();
continue;
case 18:
if (tag !== 144) break;
message.useLivelyVoice = reader.bool();
continue;
case 19:
if (tag !== 154) break;
message.videoTitle = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
url: isSet(object.url) ? globalThis.String(object.url) : "",
deviceId: isSet(object.deviceId) ? globalThis.String(object.deviceId) : void 0,
firstRequest: isSet(object.firstRequest) ? globalThis.Boolean(object.firstRequest) : false,
duration: isSet(object.duration) ? globalThis.Number(object.duration) : 0,
unknown0: isSet(object.unknown0) ? globalThis.Number(object.unknown0) : 0,
language: isSet(object.language) ? globalThis.String(object.language) : "",
forceSourceLang: isSet(object.forceSourceLang) ? globalThis.Boolean(object.forceSourceLang) : false,
unknown1: isSet(object.unknown1) ? globalThis.Number(object.unknown1) : 0,
translationHelp: globalThis.Array.isArray(object?.translationHelp) ? object.translationHelp.map((e) => VideoTranslationHelpObject.fromJSON(e)) : [],
wasStream: isSet(object.wasStream) ? globalThis.Boolean(object.wasStream) : false,
responseLanguage: isSet(object.responseLanguage) ? globalThis.String(object.responseLanguage) : "",
unknown2: isSet(object.unknown2) ? globalThis.Number(object.unknown2) : 0,
unknown3: isSet(object.unknown3) ? globalThis.Number(object.unknown3) : 0,
bypassCache: isSet(object.bypassCache) ? globalThis.Boolean(object.bypassCache) : false,
useLivelyVoice: isSet(object.useLivelyVoice) ? globalThis.Boolean(object.useLivelyVoice) : false,
videoTitle: isSet(object.videoTitle) ? globalThis.String(object.videoTitle) : ""
};
},
toJSON(message) {
const obj = {};
if (message.url !== "") obj.url = message.url;
if (message.deviceId !== void 0) obj.deviceId = message.deviceId;
if (message.firstRequest !== false) obj.firstRequest = message.firstRequest;
if (message.duration !== 0) obj.duration = message.duration;
if (message.unknown0 !== 0) obj.unknown0 = Math.round(message.unknown0);
if (message.language !== "") obj.language = message.language;
if (message.forceSourceLang !== false) obj.forceSourceLang = message.forceSourceLang;
if (message.unknown1 !== 0) obj.unknown1 = Math.round(message.unknown1);
if (message.translationHelp?.length) obj.translationHelp = message.translationHelp.map((e) => VideoTranslationHelpObject.toJSON(e));
if (message.wasStream !== false) obj.wasStream = message.wasStream;
if (message.responseLanguage !== "") obj.responseLanguage = message.responseLanguage;
if (message.unknown2 !== 0) obj.unknown2 = Math.round(message.unknown2);
if (message.unknown3 !== 0) obj.unknown3 = Math.round(message.unknown3);
if (message.bypassCache !== false) obj.bypassCache = message.bypassCache;
if (message.useLivelyVoice !== false) obj.useLivelyVoice = message.useLivelyVoice;
if (message.videoTitle !== "") obj.videoTitle = message.videoTitle;
return obj;
},
create(base) {
return VideoTranslationRequest.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseVideoTranslationRequest();
message.url = object.url ?? "";
message.deviceId = object.deviceId ?? void 0;
message.firstRequest = object.firstRequest ?? false;
message.duration = object.duration ?? 0;
message.unknown0 = object.unknown0 ?? 0;
message.language = object.language ?? "";
message.forceSourceLang = object.forceSourceLang ?? false;
message.unknown1 = object.unknown1 ?? 0;
message.translationHelp = object.translationHelp?.map((e) => VideoTranslationHelpObject.fromPartial(e)) || [];
message.wasStream = object.wasStream ?? false;
message.responseLanguage = object.responseLanguage ?? "";
message.unknown2 = object.unknown2 ?? 0;
message.unknown3 = object.unknown3 ?? 0;
message.bypassCache = object.bypassCache ?? false;
message.useLivelyVoice = object.useLivelyVoice ?? false;
message.videoTitle = object.videoTitle ?? "";
return message;
}
};
function createBaseVideoTranslationResponse() {
return {
url: void 0,
duration: void 0,
status: 0,
remainingTime: void 0,
unknown0: void 0,
translationId: "",
language: void 0,
message: void 0,
isLivelyVoice: false,
unknown2: void 0,
shouldRetry: void 0,
unknown3: void 0
};
}
var VideoTranslationResponse = {
encode(message, writer = new BinaryWriter()) {
if (message.url !== void 0) writer.uint32(10).string(message.url);
if (message.duration !== void 0) writer.uint32(17).double(message.duration);
if (message.status !== 0) writer.uint32(32).int32(message.status);
if (message.remainingTime !== void 0) writer.uint32(40).int32(message.remainingTime);
if (message.unknown0 !== void 0) writer.uint32(48).int32(message.unknown0);
if (message.translationId !== "") writer.uint32(58).string(message.translationId);
if (message.language !== void 0) writer.uint32(66).string(message.language);
if (message.message !== void 0) writer.uint32(74).string(message.message);
if (message.isLivelyVoice !== false) writer.uint32(80).bool(message.isLivelyVoice);
if (message.unknown2 !== void 0) writer.uint32(88).int32(message.unknown2);
if (message.shouldRetry !== void 0) writer.uint32(96).int32(message.shouldRetry);
if (message.unknown3 !== void 0) writer.uint32(104).int32(message.unknown3);
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseVideoTranslationResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) break;
message.url = reader.string();
continue;
case 2:
if (tag !== 17) break;
message.duration = reader.double();
continue;
case 4:
if (tag !== 32) break;
message.status = reader.int32();
continue;
case 5:
if (tag !== 40) break;
message.remainingTime = reader.int32();
continue;
case 6:
if (tag !== 48) break;
message.unknown0 = reader.int32();
continue;
case 7:
if (tag !== 58) break;
message.translationId = reader.string();
continue;
case 8:
if (tag !== 66) break;
message.language = reader.string();
continue;
case 9:
if (tag !== 74) break;
message.message = reader.string();
continue;
case 10:
if (tag !== 80) break;
message.isLivelyVoice = reader.bool();
continue;
case 11:
if (tag !== 88) break;
message.unknown2 = reader.int32();
continue;
case 12:
if (tag !== 96) break;
message.shouldRetry = reader.int32();
continue;
case 13:
if (tag !== 104) break;
message.unknown3 = reader.int32();
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
url: isSet(object.url) ? globalThis.String(object.url) : void 0,
duration: isSet(object.duration) ? globalThis.Number(object.duration) : void 0,
status: isSet(object.status) ? globalThis.Number(object.status) : 0,
remainingTime: isSet(object.remainingTime) ? globalThis.Number(object.remainingTime) : void 0,
unknown0: isSet(object.unknown0) ? globalThis.Number(object.unknown0) : void 0,
translationId: isSet(object.translationId) ? globalThis.String(object.translationId) : "",
language: isSet(object.language) ? globalThis.String(object.language) : void 0,
message: isSet(object.message) ? globalThis.String(object.message) : void 0,
isLivelyVoice: isSet(object.isLivelyVoice) ? globalThis.Boolean(object.isLivelyVoice) : false,
unknown2: isSet(object.unknown2) ? globalThis.Number(object.unknown2) : void 0,
shouldRetry: isSet(object.shouldRetry) ? globalThis.Number(object.shouldRetry) : void 0,
unknown3: isSet(object.unknown3) ? globalThis.Number(object.unknown3) : void 0
};
},
toJSON(message) {
const obj = {};
if (message.url !== void 0) obj.url = message.url;
if (message.duration !== void 0) obj.duration = message.duration;
if (message.status !== 0) obj.status = Math.round(message.status);
if (message.remainingTime !== void 0) obj.remainingTime = Math.round(message.remainingTime);
if (message.unknown0 !== void 0) obj.unknown0 = Math.round(message.unknown0);
if (message.translationId !== "") obj.translationId = message.translationId;
if (message.language !== void 0) obj.language = message.language;
if (message.message !== void 0) obj.message = message.message;
if (message.isLivelyVoice !== false) obj.isLivelyVoice = message.isLivelyVoice;
if (message.unknown2 !== void 0) obj.unknown2 = Math.round(message.unknown2);
if (message.shouldRetry !== void 0) obj.shouldRetry = Math.round(message.shouldRetry);
if (message.unknown3 !== void 0) obj.unknown3 = Math.round(message.unknown3);
return obj;
},
create(base) {
return VideoTranslationResponse.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseVideoTranslationResponse();
message.url = object.url ?? void 0;
message.duration = object.duration ?? void 0;
message.status = object.status ?? 0;
message.remainingTime = object.remainingTime ?? void 0;
message.unknown0 = object.unknown0 ?? void 0;
message.translationId = object.translationId ?? "";
message.language = object.language ?? void 0;
message.message = object.message ?? void 0;
message.isLivelyVoice = object.isLivelyVoice ?? false;
message.unknown2 = object.unknown2 ?? void 0;
message.shouldRetry = object.shouldRetry ?? void 0;
message.unknown3 = object.unknown3 ?? void 0;
return message;
}
};
function createBaseVideoTranslationCacheItem() {
return {
status: 0,
remainingTime: void 0,
message: void 0,
unknown0: void 0
};
}
var VideoTranslationCacheItem = {
encode(message, writer = new BinaryWriter()) {
if (message.status !== 0) writer.uint32(8).int32(message.status);
if (message.remainingTime !== void 0) writer.uint32(16).int32(message.remainingTime);
if (message.message !== void 0) writer.uint32(26).string(message.message);
if (message.unknown0 !== void 0) writer.uint32(32).int32(message.unknown0);
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseVideoTranslationCacheItem();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 8) break;
message.status = reader.int32();
continue;
case 2:
if (tag !== 16) break;
message.remainingTime = reader.int32();
continue;
case 3:
if (tag !== 26) break;
message.message = reader.string();
continue;
case 4:
if (tag !== 32) break;
message.unknown0 = reader.int32();
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
status: isSet(object.status) ? globalThis.Number(object.status) : 0,
remainingTime: isSet(object.remainingTime) ? globalThis.Number(object.remainingTime) : void 0,
message: isSet(object.message) ? globalThis.String(object.message) : void 0,
unknown0: isSet(object.unknown0) ? globalThis.Number(object.unknown0) : void 0
};
},
toJSON(message) {
const obj = {};
if (message.status !== 0) obj.status = Math.round(message.status);
if (message.remainingTime !== void 0) obj.remainingTime = Math.round(message.remainingTime);
if (message.message !== void 0) obj.message = message.message;
if (message.unknown0 !== void 0) obj.unknown0 = Math.round(message.unknown0);
return obj;
},
create(base) {
return VideoTranslationCacheItem.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseVideoTranslationCacheItem();
message.status = object.status ?? 0;
message.remainingTime = object.remainingTime ?? void 0;
message.message = object.message ?? void 0;
message.unknown0 = object.unknown0 ?? void 0;
return message;
}
};
function createBaseVideoTranslationCacheRequest() {
return {
url: "",
duration: 0,
language: "",
responseLanguage: ""
};
}
var VideoTranslationCacheRequest = {
encode(message, writer = new BinaryWriter()) {
if (message.url !== "") writer.uint32(10).string(message.url);
if (message.duration !== 0) writer.uint32(17).double(message.duration);
if (message.language !== "") writer.uint32(26).string(message.language);
if (message.responseLanguage !== "") writer.uint32(34).string(message.responseLanguage);
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseVideoTranslationCacheRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) break;
message.url = reader.string();
continue;
case 2:
if (tag !== 17) break;
message.duration = reader.double();
continue;
case 3:
if (tag !== 26) break;
message.language = reader.string();
continue;
case 4:
if (tag !== 34) break;
message.responseLanguage = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
url: isSet(object.url) ? globalThis.String(object.url) : "",
duration: isSet(object.duration) ? globalThis.Number(object.duration) : 0,
language: isSet(object.language) ? globalThis.String(object.language) : "",
responseLanguage: isSet(object.responseLanguage) ? globalThis.String(object.responseLanguage) : ""
};
},
toJSON(message) {
const obj = {};
if (message.url !== "") obj.url = message.url;
if (message.duration !== 0) obj.duration = message.duration;
if (message.language !== "") obj.language = message.language;
if (message.responseLanguage !== "") obj.responseLanguage = message.responseLanguage;
return obj;
},
create(base) {
return VideoTranslationCacheRequest.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseVideoTranslationCacheRequest();
message.url = object.url ?? "";
message.duration = object.duration ?? 0;
message.language = object.language ?? "";
message.responseLanguage = object.responseLanguage ?? "";
return message;
}
};
function createBaseVideoTranslationCacheResponse() {
return {
default: void 0,
cloning: void 0
};
}
var VideoTranslationCacheResponse = {
encode(message, writer = new BinaryWriter()) {
if (message.default !== void 0) VideoTranslationCacheItem.encode(message.default, writer.uint32(10).fork()).join();
if (message.cloning !== void 0) VideoTranslationCacheItem.encode(message.cloning, writer.uint32(18).fork()).join();
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseVideoTranslationCacheResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) break;
message.default = VideoTranslationCacheItem.decode(reader, reader.uint32());
continue;
case 2:
if (tag !== 18) break;
message.cloning = VideoTranslationCacheItem.decode(reader, reader.uint32());
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
default: isSet(object.default) ? VideoTranslationCacheItem.fromJSON(object.default) : void 0,
cloning: isSet(object.cloning) ? VideoTranslationCacheItem.fromJSON(object.cloning) : void 0
};
},
toJSON(message) {
const obj = {};
if (message.default !== void 0) obj.default = VideoTranslationCacheItem.toJSON(message.default);
if (message.cloning !== void 0) obj.cloning = VideoTranslationCacheItem.toJSON(message.cloning);
return obj;
},
create(base) {
return VideoTranslationCacheResponse.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseVideoTranslationCacheResponse();
message.default = object.default !== void 0 && object.default !== null ? VideoTranslationCacheItem.fromPartial(object.default) : void 0;
message.cloning = object.cloning !== void 0 && object.cloning !== null ? VideoTranslationCacheItem.fromPartial(object.cloning) : void 0;
return message;
}
};
function createBaseAudioBufferObject() {
return {
audioFile: new Uint8Array(0),
fileId: ""
};
}
var AudioBufferObject = {
encode(message, writer = new BinaryWriter()) {
if (message.audioFile.length !== 0) writer.uint32(18).bytes(message.audioFile);
if (message.fileId !== "") writer.uint32(10).string(message.fileId);
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseAudioBufferObject();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 2:
if (tag !== 18) break;
message.audioFile = reader.bytes();
continue;
case 1:
if (tag !== 10) break;
message.fileId = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
audioFile: isSet(object.audioFile) ? bytesFromBase64(object.audioFile) : new Uint8Array(0),
fileId: isSet(object.fileId) ? globalThis.String(object.fileId) : ""
};
},
toJSON(message) {
const obj = {};
if (message.audioFile.length !== 0) obj.audioFile = base64FromBytes(message.audioFile);
if (message.fileId !== "") obj.fileId = message.fileId;
return obj;
},
create(base) {
return AudioBufferObject.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseAudioBufferObject();
message.audioFile = object.audioFile ?? new Uint8Array(0);
message.fileId = object.fileId ?? "";
return message;
}
};
function createBasePartialAudioBufferObject() {
return {
audioFile: new Uint8Array(0),
chunkId: 0
};
}
var PartialAudioBufferObject = {
encode(message, writer = new BinaryWriter()) {
if (message.audioFile.length !== 0) writer.uint32(18).bytes(message.audioFile);
if (message.chunkId !== 0) writer.uint32(8).int32(message.chunkId);
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBasePartialAudioBufferObject();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 2:
if (tag !== 18) break;
message.audioFile = reader.bytes();
continue;
case 1:
if (tag !== 8) break;
message.chunkId = reader.int32();
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
audioFile: isSet(object.audioFile) ? bytesFromBase64(object.audioFile) : new Uint8Array(0),
chunkId: isSet(object.chunkId) ? globalThis.Number(object.chunkId) : 0
};
},
toJSON(message) {
const obj = {};
if (message.audioFile.length !== 0) obj.audioFile = base64FromBytes(message.audioFile);
if (message.chunkId !== 0) obj.chunkId = Math.round(message.chunkId);
return obj;
},
create(base) {
return PartialAudioBufferObject.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBasePartialAudioBufferObject();
message.audioFile = object.audioFile ?? new Uint8Array(0);
message.chunkId = object.chunkId ?? 0;
return message;
}
};
function createBaseChunkAudioObject() {
return {
audioBuffer: void 0,
audioPartsLength: 0,
fileId: "",
version: 0
};
}
var ChunkAudioObject = {
encode(message, writer = new BinaryWriter()) {
if (message.audioBuffer !== void 0) PartialAudioBufferObject.encode(message.audioBuffer, writer.uint32(10).fork()).join();
if (message.audioPartsLength !== 0) writer.uint32(16).int32(message.audioPartsLength);
if (message.fileId !== "") writer.uint32(26).string(message.fileId);
if (message.version !== 0) writer.uint32(32).int32(message.version);
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseChunkAudioObject();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) break;
message.audioBuffer = PartialAudioBufferObject.decode(reader, reader.uint32());
continue;
case 2:
if (tag !== 16) break;
message.audioPartsLength = reader.int32();
continue;
case 3:
if (tag !== 26) break;
message.fileId = reader.string();
continue;
case 4:
if (tag !== 32) break;
message.version = reader.int32();
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
audioBuffer: isSet(object.audioBuffer) ? PartialAudioBufferObject.fromJSON(object.audioBuffer) : void 0,
audioPartsLength: isSet(object.audioPartsLength) ? globalThis.Number(object.audioPartsLength) : 0,
fileId: isSet(object.fileId) ? globalThis.String(object.fileId) : "",
version: isSet(object.version) ? globalThis.Number(object.version) : 0
};
},
toJSON(message) {
const obj = {};
if (message.audioBuffer !== void 0) obj.audioBuffer = PartialAudioBufferObject.toJSON(message.audioBuffer);
if (message.audioPartsLength !== 0) obj.audioPartsLength = Math.round(message.audioPartsLength);
if (message.fileId !== "") obj.fileId = message.fileId;
if (message.version !== 0) obj.version = Math.round(message.version);
return obj;
},
create(base) {
return ChunkAudioObject.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseChunkAudioObject();
message.audioBuffer = object.audioBuffer !== void 0 && object.audioBuffer !== null ? PartialAudioBufferObject.fromPartial(object.audioBuffer) : void 0;
message.audioPartsLength = object.audioPartsLength ?? 0;
message.fileId = object.fileId ?? "";
message.version = object.version ?? 0;
return message;
}
};
function createBaseVideoTranslationAudioRequest() {
return {
translationId: "",
url: "",
partialAudioInfo: void 0,
audioInfo: void 0
};
}
var VideoTranslationAudioRequest = {
encode(message, writer = new BinaryWriter()) {
if (message.translationId !== "") writer.uint32(10).string(message.translationId);
if (message.url !== "") writer.uint32(18).string(message.url);
if (message.partialAudioInfo !== void 0) ChunkAudioObject.encode(message.partialAudioInfo, writer.uint32(34).fork()).join();
if (message.audioInfo !== void 0) AudioBufferObject.encode(message.audioInfo, writer.uint32(50).fork()).join();
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseVideoTranslationAudioRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) break;
message.translationId = reader.string();
continue;
case 2:
if (tag !== 18) break;
message.url = reader.string();
continue;
case 4:
if (tag !== 34) break;
message.partialAudioInfo = ChunkAudioObject.decode(reader, reader.uint32());
continue;
case 6:
if (tag !== 50) break;
message.audioInfo = AudioBufferObject.decode(reader, reader.uint32());
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
translationId: isSet(object.translationId) ? globalThis.String(object.translationId) : "",
url: isSet(object.url) ? globalThis.String(object.url) : "",
partialAudioInfo: isSet(object.partialAudioInfo) ? ChunkAudioObject.fromJSON(object.partialAudioInfo) : void 0,
audioInfo: isSet(object.audioInfo) ? AudioBufferObject.fromJSON(object.audioInfo) : void 0
};
},
toJSON(message) {
const obj = {};
if (message.translationId !== "") obj.translationId = message.translationId;
if (message.url !== "") obj.url = message.url;
if (message.partialAudioInfo !== void 0) obj.partialAudioInfo = ChunkAudioObject.toJSON(message.partialAudioInfo);
if (message.audioInfo !== void 0) obj.audioInfo = AudioBufferObject.toJSON(message.audioInfo);
return obj;
},
create(base) {
return VideoTranslationAudioRequest.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseVideoTranslationAudioRequest();
message.translationId = object.translationId ?? "";
message.url = object.url ?? "";
message.partialAudioInfo = object.partialAudioInfo !== void 0 && object.partialAudioInfo !== null ? ChunkAudioObject.fromPartial(object.partialAudioInfo) : void 0;
message.audioInfo = object.audioInfo !== void 0 && object.audioInfo !== null ? AudioBufferObject.fromPartial(object.audioInfo) : void 0;
return message;
}
};
function createBaseVideoTranslationAudioResponse() {
return {
status: 0,
remainingChunks: []
};
}
var VideoTranslationAudioResponse = {
encode(message, writer = new BinaryWriter()) {
if (message.status !== 0) writer.uint32(8).int32(message.status);
for (const v of message.remainingChunks) writer.uint32(18).string(v);
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseVideoTranslationAudioResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 8) break;
message.status = reader.int32();
continue;
case 2:
if (tag !== 18) break;
message.remainingChunks.push(reader.string());
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
status: isSet(object.status) ? globalThis.Number(object.status) : 0,
remainingChunks: globalThis.Array.isArray(object?.remainingChunks) ? object.remainingChunks.map((e) => globalThis.String(e)) : []
};
},
toJSON(message) {
const obj = {};
if (message.status !== 0) obj.status = Math.round(message.status);
if (message.remainingChunks?.length) obj.remainingChunks = message.remainingChunks;
return obj;
},
create(base) {
return VideoTranslationAudioResponse.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseVideoTranslationAudioResponse();
message.status = object.status ?? 0;
message.remainingChunks = object.remainingChunks?.map((e) => e) || [];
return message;
}
};
function createBaseSubtitlesObject() {
return {
language: "",
url: "",
unknown0: 0,
translatedLanguage: "",
translatedUrl: "",
unknown1: 0,
unknown2: 0
};
}
var SubtitlesObject = {
encode(message, writer = new BinaryWriter()) {
if (message.language !== "") writer.uint32(10).string(message.language);
if (message.url !== "") writer.uint32(18).string(message.url);
if (message.unknown0 !== 0) writer.uint32(24).int32(message.unknown0);
if (message.translatedLanguage !== "") writer.uint32(34).string(message.translatedLanguage);
if (message.translatedUrl !== "") writer.uint32(42).string(message.translatedUrl);
if (message.unknown1 !== 0) writer.uint32(48).int32(message.unknown1);
if (message.unknown2 !== 0) writer.uint32(56).int32(message.unknown2);
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseSubtitlesObject();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) break;
message.language = reader.string();
continue;
case 2:
if (tag !== 18) break;
message.url = reader.string();
continue;
case 3:
if (tag !== 24) break;
message.unknown0 = reader.int32();
continue;
case 4:
if (tag !== 34) break;
message.translatedLanguage = reader.string();
continue;
case 5:
if (tag !== 42) break;
message.translatedUrl = reader.string();
continue;
case 6:
if (tag !== 48) break;
message.unknown1 = reader.int32();
continue;
case 7:
if (tag !== 56) break;
message.unknown2 = reader.int32();
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
language: isSet(object.language) ? globalThis.String(object.language) : "",
url: isSet(object.url) ? globalThis.String(object.url) : "",
unknown0: isSet(object.unknown0) ? globalThis.Number(object.unknown0) : 0,
translatedLanguage: isSet(object.translatedLanguage) ? globalThis.String(object.translatedLanguage) : "",
translatedUrl: isSet(object.translatedUrl) ? globalThis.String(object.translatedUrl) : "",
unknown1: isSet(object.unknown1) ? globalThis.Number(object.unknown1) : 0,
unknown2: isSet(object.unknown2) ? globalThis.Number(object.unknown2) : 0
};
},
toJSON(message) {
const obj = {};
if (message.language !== "") obj.language = message.language;
if (message.url !== "") obj.url = message.url;
if (message.unknown0 !== 0) obj.unknown0 = Math.round(message.unknown0);
if (message.translatedLanguage !== "") obj.translatedLanguage = message.translatedLanguage;
if (message.translatedUrl !== "") obj.translatedUrl = message.translatedUrl;
if (message.unknown1 !== 0) obj.unknown1 = Math.round(message.unknown1);
if (message.unknown2 !== 0) obj.unknown2 = Math.round(message.unknown2);
return obj;
},
create(base) {
return SubtitlesObject.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseSubtitlesObject();
message.language = object.language ?? "";
message.url = object.url ?? "";
message.unknown0 = object.unknown0 ?? 0;
message.translatedLanguage = object.translatedLanguage ?? "";
message.translatedUrl = object.translatedUrl ?? "";
message.unknown1 = object.unknown1 ?? 0;
message.unknown2 = object.unknown2 ?? 0;
return message;
}
};
function createBaseSubtitlesRequest() {
return {
url: "",
language: ""
};
}
var SubtitlesRequest = {
encode(message, writer = new BinaryWriter()) {
if (message.url !== "") writer.uint32(10).string(message.url);
if (message.language !== "") writer.uint32(18).string(message.language);
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseSubtitlesRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) break;
message.url = reader.string();
continue;
case 2:
if (tag !== 18) break;
message.language = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
url: isSet(object.url) ? globalThis.String(object.url) : "",
language: isSet(object.language) ? globalThis.String(object.language) : ""
};
},
toJSON(message) {
const obj = {};
if (message.url !== "") obj.url = message.url;
if (message.language !== "") obj.language = message.language;
return obj;
},
create(base) {
return SubtitlesRequest.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseSubtitlesRequest();
message.url = object.url ?? "";
message.language = object.language ?? "";
return message;
}
};
function createBaseSubtitlesResponse() {
return {
waiting: false,
subtitles: []
};
}
var SubtitlesResponse = {
encode(message, writer = new BinaryWriter()) {
if (message.waiting !== false) writer.uint32(8).bool(message.waiting);
for (const v of message.subtitles) SubtitlesObject.encode(v, writer.uint32(18).fork()).join();
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseSubtitlesResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 8) break;
message.waiting = reader.bool();
continue;
case 2:
if (tag !== 18) break;
message.subtitles.push(SubtitlesObject.decode(reader, reader.uint32()));
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
waiting: isSet(object.waiting) ? globalThis.Boolean(object.waiting) : false,
subtitles: globalThis.Array.isArray(object?.subtitles) ? object.subtitles.map((e) => SubtitlesObject.fromJSON(e)) : []
};
},
toJSON(message) {
const obj = {};
if (message.waiting !== false) obj.waiting = message.waiting;
if (message.subtitles?.length) obj.subtitles = message.subtitles.map((e) => SubtitlesObject.toJSON(e));
return obj;
},
create(base) {
return SubtitlesResponse.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseSubtitlesResponse();
message.waiting = object.waiting ?? false;
message.subtitles = object.subtitles?.map((e) => SubtitlesObject.fromPartial(e)) || [];
return message;
}
};
function createBaseStreamTranslationObject() {
return {
url: "",
timestamp: ""
};
}
var StreamTranslationObject = {
encode(message, writer = new BinaryWriter()) {
if (message.url !== "") writer.uint32(10).string(message.url);
if (message.timestamp !== "") writer.uint32(18).string(message.timestamp);
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseStreamTranslationObject();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) break;
message.url = reader.string();
continue;
case 2:
if (tag !== 18) break;
message.timestamp = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
url: isSet(object.url) ? globalThis.String(object.url) : "",
timestamp: isSet(object.timestamp) ? globalThis.String(object.timestamp) : ""
};
},
toJSON(message) {
const obj = {};
if (message.url !== "") obj.url = message.url;
if (message.timestamp !== "") obj.timestamp = message.timestamp;
return obj;
},
create(base) {
return StreamTranslationObject.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseStreamTranslationObject();
message.url = object.url ?? "";
message.timestamp = object.timestamp ?? "";
return message;
}
};
function createBaseStreamTranslationRequest() {
return {
url: "",
language: "",
responseLanguage: "",
unknown0: 0,
unknown1: 0
};
}
var StreamTranslationRequest = {
encode(message, writer = new BinaryWriter()) {
if (message.url !== "") writer.uint32(10).string(message.url);
if (message.language !== "") writer.uint32(18).string(message.language);
if (message.responseLanguage !== "") writer.uint32(26).string(message.responseLanguage);
if (message.unknown0 !== 0) writer.uint32(40).int32(message.unknown0);
if (message.unknown1 !== 0) writer.uint32(48).int32(message.unknown1);
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseStreamTranslationRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) break;
message.url = reader.string();
continue;
case 2:
if (tag !== 18) break;
message.language = reader.string();
continue;
case 3:
if (tag !== 26) break;
message.responseLanguage = reader.string();
continue;
case 5:
if (tag !== 40) break;
message.unknown0 = reader.int32();
continue;
case 6:
if (tag !== 48) break;
message.unknown1 = reader.int32();
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
url: isSet(object.url) ? globalThis.String(object.url) : "",
language: isSet(object.language) ? globalThis.String(object.language) : "",
responseLanguage: isSet(object.responseLanguage) ? globalThis.String(object.responseLanguage) : "",
unknown0: isSet(object.unknown0) ? globalThis.Number(object.unknown0) : 0,
unknown1: isSet(object.unknown1) ? globalThis.Number(object.unknown1) : 0
};
},
toJSON(message) {
const obj = {};
if (message.url !== "") obj.url = message.url;
if (message.language !== "") obj.language = message.language;
if (message.responseLanguage !== "") obj.responseLanguage = message.responseLanguage;
if (message.unknown0 !== 0) obj.unknown0 = Math.round(message.unknown0);
if (message.unknown1 !== 0) obj.unknown1 = Math.round(message.unknown1);
return obj;
},
create(base) {
return StreamTranslationRequest.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseStreamTranslationRequest();
message.url = object.url ?? "";
message.language = object.language ?? "";
message.responseLanguage = object.responseLanguage ?? "";
message.unknown0 = object.unknown0 ?? 0;
message.unknown1 = object.unknown1 ?? 0;
return message;
}
};
function createBaseStreamTranslationResponse() {
return {
interval: 0,
translatedInfo: void 0,
pingId: void 0
};
}
var StreamTranslationResponse = {
encode(message, writer = new BinaryWriter()) {
if (message.interval !== 0) writer.uint32(8).int32(message.interval);
if (message.translatedInfo !== void 0) StreamTranslationObject.encode(message.translatedInfo, writer.uint32(18).fork()).join();
if (message.pingId !== void 0) writer.uint32(24).int32(message.pingId);
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseStreamTranslationResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 8) break;
message.interval = reader.int32();
continue;
case 2:
if (tag !== 18) break;
message.translatedInfo = StreamTranslationObject.decode(reader, reader.uint32());
continue;
case 3:
if (tag !== 24) break;
message.pingId = reader.int32();
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
interval: isSet(object.interval) ? streamIntervalFromJSON(object.interval) : 0,
translatedInfo: isSet(object.translatedInfo) ? StreamTranslationObject.fromJSON(object.translatedInfo) : void 0,
pingId: isSet(object.pingId) ? globalThis.Number(object.pingId) : void 0
};
},
toJSON(message) {
const obj = {};
if (message.interval !== 0) obj.interval = streamIntervalToJSON(message.interval);
if (message.translatedInfo !== void 0) obj.translatedInfo = StreamTranslationObject.toJSON(message.translatedInfo);
if (message.pingId !== void 0) obj.pingId = Math.round(message.pingId);
return obj;
},
create(base) {
return StreamTranslationResponse.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseStreamTranslationResponse();
message.interval = object.interval ?? 0;
message.translatedInfo = object.translatedInfo !== void 0 && object.translatedInfo !== null ? StreamTranslationObject.fromPartial(object.translatedInfo) : void 0;
message.pingId = object.pingId ?? void 0;
return message;
}
};
function createBaseStreamPingRequest() {
return { pingId: 0 };
}
var StreamPingRequest = {
encode(message, writer = new BinaryWriter()) {
if (message.pingId !== 0) writer.uint32(8).int32(message.pingId);
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseStreamPingRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 8) break;
message.pingId = reader.int32();
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return { pingId: isSet(object.pingId) ? globalThis.Number(object.pingId) : 0 };
},
toJSON(message) {
const obj = {};
if (message.pingId !== 0) obj.pingId = Math.round(message.pingId);
return obj;
},
create(base) {
return StreamPingRequest.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseStreamPingRequest();
message.pingId = object.pingId ?? 0;
return message;
}
};
function createBaseYandexSessionRequest() {
return {
uuid: "",
module: ""
};
}
var YandexSessionRequest = {
encode(message, writer = new BinaryWriter()) {
if (message.uuid !== "") writer.uint32(10).string(message.uuid);
if (message.module !== "") writer.uint32(18).string(message.module);
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseYandexSessionRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) break;
message.uuid = reader.string();
continue;
case 2:
if (tag !== 18) break;
message.module = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
uuid: isSet(object.uuid) ? globalThis.String(object.uuid) : "",
module: isSet(object.module) ? globalThis.String(object.module) : ""
};
},
toJSON(message) {
const obj = {};
if (message.uuid !== "") obj.uuid = message.uuid;
if (message.module !== "") obj.module = message.module;
return obj;
},
create(base) {
return YandexSessionRequest.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseYandexSessionRequest();
message.uuid = object.uuid ?? "";
message.module = object.module ?? "";
return message;
}
};
function createBaseYandexSessionResponse() {
return {
secretKey: "",
expires: 0
};
}
var YandexSessionResponse = {
encode(message, writer = new BinaryWriter()) {
if (message.secretKey !== "") writer.uint32(10).string(message.secretKey);
if (message.expires !== 0) writer.uint32(16).int32(message.expires);
return writer;
},
decode(input, length) {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === void 0 ? reader.len : reader.pos + length;
const message = createBaseYandexSessionResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) break;
message.secretKey = reader.string();
continue;
case 2:
if (tag !== 16) break;
message.expires = reader.int32();
continue;
}
if ((tag & 7) === 4 || tag === 0) break;
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
secretKey: isSet(object.secretKey) ? globalThis.String(object.secretKey) : "",
expires: isSet(object.expires) ? globalThis.Number(object.expires) : 0
};
},
toJSON(message) {
const obj = {};
if (message.secretKey !== "") obj.secretKey = message.secretKey;
if (message.expires !== 0) obj.expires = Math.round(message.expires);
return obj;
},
create(base) {
return YandexSessionResponse.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseYandexSessionResponse();
message.secretKey = object.secretKey ?? "";
message.expires = object.expires ?? 0;
return message;
}
};
function bytesFromBase64(b64) {
if (globalThis.Buffer) return Uint8Array.from(globalThis.Buffer.from(b64, "base64"));
else {
const bin = globalThis.atob(b64);
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; ++i) arr[i] = bin.charCodeAt(i);
return arr;
}
}
function base64FromBytes(arr) {
if (globalThis.Buffer) return globalThis.Buffer.from(arr).toString("base64");
else {
const bin = [];
arr.forEach((byte) => {
bin.push(globalThis.String.fromCharCode(byte));
});
return globalThis.btoa(bin.join(""));
}
}
function isSet(value) {
return value !== null && value !== void 0;
}
//#endregion
//#region node_modules/@vot.js/shared/dist/types/logger.js
var LoggerLevel;
(function(LoggerLevel) {
LoggerLevel[LoggerLevel["DEBUG"] = 0] = "DEBUG";
LoggerLevel[LoggerLevel["INFO"] = 1] = "INFO";
LoggerLevel[LoggerLevel["WARN"] = 2] = "WARN";
LoggerLevel[LoggerLevel["ERROR"] = 3] = "ERROR";
LoggerLevel[LoggerLevel["SILENCE"] = 4] = "SILENCE";
})(LoggerLevel || (LoggerLevel = {}));
//#endregion
//#region node_modules/@vot.js/shared/dist/utils/logger.js
var prefix = `[vot.js v${config_default$1.version}]`;
function canLog(level) {
return config_default$1.loggerLevel <= level;
}
function log(...messages) {
if (!canLog(LoggerLevel.DEBUG)) return;
console.log(prefix, ...messages);
}
function info(...messages) {
if (!canLog(LoggerLevel.INFO)) return;
console.info(prefix, ...messages);
}
function warn(...messages) {
if (!canLog(LoggerLevel.WARN)) return;
console.warn(prefix, ...messages);
}
function error(...messages) {
if (!canLog(LoggerLevel.ERROR)) return;
console.error(prefix, ...messages);
}
var Logger = {
canLog,
log,
info,
warn,
error
};
//#endregion
//#region src/shims/nodeCrypto.ts
var nodeCrypto_exports = /* @__PURE__ */ __exportAll({
default: () => webCrypto,
getRandomValues: () => getRandomValues,
randomUUID: () => randomUUID,
subtle: () => subtle
});
var webCrypto, subtle, getRandomValues, randomUUID;
var init_nodeCrypto = __esmMin((() => {
webCrypto = globalThis.crypto;
if (!webCrypto?.subtle) throw new TypeError("Web Crypto API is not available in this environment.");
subtle = webCrypto.subtle;
getRandomValues = webCrypto.getRandomValues.bind(webCrypto);
randomUUID = typeof webCrypto.randomUUID === "function" ? webCrypto.randomUUID.bind(webCrypto) : void 0;
}));
//#endregion
//#region node_modules/@vot.js/shared/dist/secure.js
var { componentVersion } = config_default$1;
async function getCrypto() {
if (typeof window !== "undefined" && window.crypto) return window.crypto;
return await Promise.resolve().then(() => (init_nodeCrypto(), nodeCrypto_exports));
}
var utf8Encoder = new TextEncoder();
async function signHMAC(hashName, hmac, data) {
const crypto = await getCrypto();
const key = await crypto.subtle.importKey("raw", utf8Encoder.encode(hmac), {
name: "HMAC",
hash: { name: hashName }
}, false, ["sign", "verify"]);
return await crypto.subtle.sign("HMAC", key, data);
}
async function getSignature(body) {
const signature = await signHMAC("SHA-256", config_default$1.hmac, body);
return new Uint8Array(signature).reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
}
async function getSecYaHeaders(secType, session, body, path) {
const { secretKey, uuid } = session;
const token = `${uuid}:${path}:${componentVersion}`;
const tokenSign = await getSignature(utf8Encoder.encode(token));
if (secType === "Ya-Summary") return {
[`X-${secType}-Sk`]: secretKey,
[`X-${secType}-Token`]: `${tokenSign}:${token}`
};
if (!body) throw new TypeError(`Body is required for sec type ${secType}`);
const sign = await getSignature(body);
return {
[`${secType}-Signature`]: sign,
[`Sec-${secType}-Sk`]: secretKey,
[`Sec-${secType}-Token`]: `${tokenSign}:${token}`
};
}
function getUUID() {
const hexDigits = "0123456789ABCDEF";
let uuid = "";
for (let i = 0; i < 32; i++) {
const randomDigit = Math.floor(Math.random() * 16);
uuid += hexDigits[randomDigit];
}
return uuid;
}
async function getHmacSha1(hmacKey, salt) {
try {
const signature = await signHMAC("SHA-1", hmacKey, utf8Encoder.encode(salt));
return btoa(String.fromCharCode(...new Uint8Array(signature)));
} catch (err) {
Logger.error(err);
return false;
}
}
var browserSecHeaders = {
"sec-ch-ua": `"Chromium";v="146", "YaBrowser";v="${componentVersion.slice(0, 5)}", "Not?A_Brand";v="26", "Yowser";v="2.5"`,
"sec-ch-ua-full-version-list": `"Chromium";v="146.0.7680.154", "YaBrowser";v="${componentVersion}", "Not?A_Brand";v="26.0.0.0", "Yowser";v="2.5"`,
"Sec-Fetch-Mode": "no-cors"
};
//#endregion
//#region node_modules/@vot.js/shared/dist/utils/utils.js
var iso6392to6391 = {
afr: "af",
aka: "ak",
alb: "sq",
amh: "am",
ara: "ar",
arm: "hy",
asm: "as",
aym: "ay",
aze: "az",
baq: "eu",
bel: "be",
ben: "bn",
bos: "bs",
bul: "bg",
bur: "my",
cat: "ca",
chi: "zh",
cos: "co",
cze: "cs",
dan: "da",
div: "dv",
dut: "nl",
eng: "en",
epo: "eo",
est: "et",
ewe: "ee",
fin: "fi",
fre: "fr",
fry: "fy",
geo: "ka",
ger: "de",
gla: "gd",
gle: "ga",
glg: "gl",
gre: "el",
grn: "gn",
guj: "gu",
hat: "ht",
hau: "ha",
hin: "hi",
hrv: "hr",
hun: "hu",
ibo: "ig",
ice: "is",
ind: "id",
ita: "it",
jav: "jv",
jpn: "ja",
kan: "kn",
kaz: "kk",
khm: "km",
kin: "rw",
kir: "ky",
kor: "ko",
kur: "ku",
lao: "lo",
lat: "la",
lav: "lv",
lin: "ln",
lit: "lt",
ltz: "lb",
lug: "lg",
mac: "mk",
mal: "ml",
mao: "mi",
mar: "mr",
may: "ms",
mlg: "mg",
mlt: "mt",
mon: "mn",
nep: "ne",
nor: "no",
nya: "ny",
ori: "or",
orm: "om",
pan: "pa",
per: "fa",
pol: "pl",
por: "pt",
pus: "ps",
que: "qu",
rum: "ro",
rus: "ru",
san: "sa",
sin: "si",
slo: "sk",
slv: "sl",
smo: "sm",
sna: "sn",
snd: "sd",
som: "so",
sot: "st",
spa: "es",
srp: "sr",
sun: "su",
swa: "sw",
swe: "sv",
tam: "ta",
tat: "tt",
tel: "te",
tgk: "tg",
tha: "th",
tir: "ti",
tso: "ts",
tuk: "tk",
tur: "tr",
uig: "ug",
ukr: "uk",
urd: "ur",
uzb: "uz",
vie: "vi",
wel: "cy",
xho: "xh",
yid: "yi",
yor: "yo",
zul: "zu"
};
async function fetchWithTimeout(url, options = { headers: { "User-Agent": config_default$1.userAgent } }) {
const { timeout = 3e3, signal, ...fetchOptions } = options;
if (!signal && (!timeout || timeout <= 0)) return await fetch(url, fetchOptions);
const controller = new AbortController();
const abort = (reason) => {
if (!controller.signal.aborted) controller.abort(reason);
};
if (signal) if (signal.aborted) abort(signal.reason);
else signal.addEventListener("abort", () => abort(signal.reason), { once: true });
let timeoutId;
if (timeout && timeout > 0) timeoutId = setTimeout(() => abort(/* @__PURE__ */ new Error("Fetch timeout")), timeout);
try {
return await fetch(url, {
...fetchOptions,
signal: controller.signal
});
} finally {
if (timeoutId) clearTimeout(timeoutId);
}
}
function getTimestamp$1() {
return Math.floor(Date.now() / 1e3);
}
function normalizeLang$1(lang) {
if (lang.length === 3) return iso6392to6391[lang];
return lang.toLowerCase().split(/[_;-]/)[0].trim();
}
function proxyMedia(url, format = "mp4") {
const generalUrl = `https://${config_default$1.mediaProxy}/v1/proxy/video.${format}?format=base64&force=true`;
if (!(url instanceof URL)) return `${generalUrl}&url=${btoa(url)}`;
return `${generalUrl}&url=${btoa(url.href)}&origin=${url.origin}&referer=${url.origin}`;
}
//#endregion
//#region node_modules/@vot.js/core/dist/protobuf.js
function encodeTranslationRequest(url, duration, requestLang, responseLang, translationHelp, { forceSourceLang = false, wasStream = false, videoTitle = "", bypassCache = false, useLivelyVoice = false, firstRequest = true } = {}) {
return VideoTranslationRequest.encode({
url,
firstRequest,
duration,
unknown0: 1,
language: requestLang,
forceSourceLang,
unknown1: 0,
translationHelp: translationHelp ?? [],
responseLanguage: responseLang,
wasStream,
unknown2: 1,
unknown3: 2,
bypassCache,
useLivelyVoice,
videoTitle
}).finish();
}
function decodeTranslationResponse(response) {
return VideoTranslationResponse.decode(new Uint8Array(response));
}
function encodeTranslationCacheRequest(url, duration, requestLang, responseLang) {
return VideoTranslationCacheRequest.encode({
url,
duration,
language: requestLang,
responseLanguage: responseLang
}).finish();
}
function decodeTranslationCacheResponse(response) {
return VideoTranslationCacheResponse.decode(new Uint8Array(response));
}
function isPartialAudioBuffer(audioBuffer) {
return "chunkId" in audioBuffer;
}
function encodeTranslationAudioRequest(url, translationId, audioBuffer, partialAudio) {
if (partialAudio && isPartialAudioBuffer(audioBuffer)) return VideoTranslationAudioRequest.encode({
url,
translationId,
partialAudioInfo: {
...partialAudio,
audioBuffer
}
}).finish();
return VideoTranslationAudioRequest.encode({
url,
translationId,
audioInfo: audioBuffer
}).finish();
}
function decodeTranslationAudioResponse(response) {
return VideoTranslationAudioResponse.decode(new Uint8Array(response));
}
function encodeSubtitlesRequest(url, requestLang) {
return SubtitlesRequest.encode({
url,
language: requestLang
}).finish();
}
function decodeSubtitlesResponse(response) {
return SubtitlesResponse.decode(new Uint8Array(response));
}
function encodeStreamPingRequest(pingId) {
return StreamPingRequest.encode({ pingId }).finish();
}
function encodeStreamRequest(url, requestLang, responseLang) {
return StreamTranslationRequest.encode({
url,
language: requestLang,
responseLanguage: responseLang,
unknown0: 1,
unknown1: 0
}).finish();
}
function decodeStreamResponse(response) {
return StreamTranslationResponse.decode(new Uint8Array(response));
}
var YandexVOTProtobuf = {
encodeTranslationRequest,
decodeTranslationResponse,
encodeTranslationCacheRequest,
decodeTranslationCacheResponse,
isPartialAudioBuffer,
encodeTranslationAudioRequest,
decodeTranslationAudioResponse,
encodeSubtitlesRequest,
decodeSubtitlesResponse,
encodeStreamPingRequest,
encodeStreamRequest,
decodeStreamResponse
};
function encodeSessionRequest(uuid, module) {
return YandexSessionRequest.encode({
uuid,
module
}).finish();
}
function decodeSessionResponse(response) {
return YandexSessionResponse.decode(new Uint8Array(response));
}
var YandexSessionProtobuf = {
encodeSessionRequest,
decodeSessionResponse
};
//#endregion
//#region node_modules/@vot.js/core/dist/types/yandex.js
var VideoTranslationStatus;
(function(VideoTranslationStatus) {
VideoTranslationStatus[VideoTranslationStatus["FAILED"] = 0] = "FAILED";
VideoTranslationStatus[VideoTranslationStatus["FINISHED"] = 1] = "FINISHED";
VideoTranslationStatus[VideoTranslationStatus["WAITING"] = 2] = "WAITING";
VideoTranslationStatus[VideoTranslationStatus["LONG_WAITING"] = 3] = "LONG_WAITING";
VideoTranslationStatus[VideoTranslationStatus["PART_CONTENT"] = 5] = "PART_CONTENT";
VideoTranslationStatus[VideoTranslationStatus["AUDIO_REQUESTED"] = 6] = "AUDIO_REQUESTED";
VideoTranslationStatus[VideoTranslationStatus["SESSION_REQUIRED"] = 7] = "SESSION_REQUIRED";
})(VideoTranslationStatus || (VideoTranslationStatus = {}));
var AudioDownloadType;
(function(AudioDownloadType) {
AudioDownloadType["WEB_API_VIDEO_SRC_FROM_IFRAME"] = "web_api_video_src_from_iframe";
AudioDownloadType["WEB_API_VIDEO_SRC"] = "web_api_video_src";
AudioDownloadType["WEB_API_GET_ALL_GENERATING_URLS_DATA_FROM_IFRAME"] = "web_api_get_all_generating_urls_data_from_iframe";
AudioDownloadType["WEB_API_GET_ALL_GENERATING_URLS_DATA_FROM_IFRAME_TMP_EXP"] = "web_api_get_all_generating_urls_data_from_iframe_tmp_exp";
AudioDownloadType["WEB_API_REPLACED_FETCH_INSIDE_IFRAME"] = "web_api_replaced_fetch_inside_iframe";
AudioDownloadType["ANDROID_API"] = "android_api";
AudioDownloadType["WEB_API_SLOW"] = "web_api_slow";
AudioDownloadType["WEB_API_STEAL_SIG_AND_N"] = "web_api_steal_sig_and_n";
AudioDownloadType["WEB_API_COMBINED"] = "web_api_get_all_generating_urls_data_from_iframe,web_api_steal_sig_and_n";
})(AudioDownloadType || (AudioDownloadType = {}));
//#endregion
//#region node_modules/@vot.js/core/dist/types/service.js
var VideoService$1;
(function(VideoService) {
VideoService["custom"] = "custom";
VideoService["directlink"] = "custom";
VideoService["youtube"] = "youtube";
VideoService["preservetube"] = "preservetube";
VideoService["piped"] = "piped";
VideoService["invidious"] = "invidious";
VideoService["niconico"] = "niconico";
VideoService["vk"] = "vk";
VideoService["nine_gag"] = "nine_gag";
VideoService["gag"] = "nine_gag";
VideoService["twitch"] = "twitch";
VideoService["proxitok"] = "proxitok";
VideoService["tiktok"] = "tiktok";
VideoService["vimeo"] = "vimeo";
VideoService["xvideos"] = "xvideos";
VideoService["xhamster"] = "xhamster";
VideoService["spankbang"] = "spankbang";
VideoService["rule34video"] = "rule34video";
VideoService["picarto"] = "picarto";
VideoService["olympicsreplay"] = "olympics_replay";
VideoService["pornhub"] = "pornhub";
VideoService["twitter"] = "twitter";
VideoService["x"] = "twitter";
VideoService["rumble"] = "rumble";
VideoService["facebook"] = "facebook";
VideoService["rutube"] = "rutube";
VideoService["coub"] = "coub";
VideoService["bilibili"] = "bilibili";
VideoService["mail_ru"] = "mailru";
VideoService["mailru"] = "mailru";
VideoService["bitchute"] = "bitchute";
VideoService["eporner"] = "eporner";
VideoService["peertube"] = "peertube";
VideoService["dailymotion"] = "dailymotion";
VideoService["trovo"] = "trovo";
VideoService["yandexdisk"] = "yandexdisk";
VideoService["ok_ru"] = "okru";
VideoService["okru"] = "okru";
VideoService["googledrive"] = "googledrive";
VideoService["bannedvideo"] = "bannedvideo";
VideoService["weverse"] = "weverse";
VideoService["weibo"] = "weibo";
VideoService["newgrounds"] = "newgrounds";
VideoService["egghead"] = "egghead";
VideoService["youku"] = "youku";
VideoService["archive"] = "archive";
VideoService["kodik"] = "kodik";
VideoService["patreon"] = "patreon";
VideoService["reddit"] = "reddit";
VideoService["kick"] = "kick";
VideoService["apple_developer"] = "apple_developer";
VideoService["appledeveloper"] = "apple_developer";
VideoService["epicgames"] = "epicgames";
VideoService["odysee"] = "odysee";
VideoService["coursehunterLike"] = "coursehunterLike";
VideoService["sap"] = "sap";
VideoService["watchpornto"] = "watchpornto";
VideoService["jove"] = "jove";
VideoService["linkedin"] = "linkedin";
VideoService["incestflix"] = "incestflix";
VideoService["porntn"] = "porntn";
VideoService["dzen"] = "dzen";
VideoService["bunnystream"] = "bunnystream";
VideoService["cloudflarestream"] = "cloudflarestream";
VideoService["loom"] = "loom";
VideoService["rtnews"] = "rtnews";
VideoService["bitview"] = "bitview";
VideoService["thisvid"] = "thisvid";
VideoService["ign"] = "ign";
VideoService["zdf"] = "zdf";
VideoService["bunkr"] = "bunkr";
VideoService["imdb"] = "imdb";
VideoService["telegram"] = "telegram";
})(VideoService$1 || (VideoService$1 = {}));
//#endregion
//#region node_modules/@vot.js/core/dist/utils/vot.js
function convertVOT(service, videoId, url) {
if (service === VideoService$1.patreon) return {
service: "mux",
videoId: new URL(url).pathname.slice(1)
};
return {
service,
videoId
};
}
//#endregion
//#region node_modules/@vot.js/core/dist/client.js
var VOTJSError = class extends Error {
data;
constructor(message, data = void 0) {
super(message);
this.data = data;
this.name = "VOTJSError";
}
};
var MinimalClient = class {
host;
schema;
fetch;
fetchOpts;
sessions = {};
userAgent = config_default$1.userAgent;
headers = {
"User-Agent": this.userAgent,
Accept: "application/x-protobuf",
"Accept-Language": "en",
"Content-Type": "application/x-protobuf",
Pragma: "no-cache",
"Cache-Control": "no-cache"
};
hostSchemaRe = /(http(s)?):\/\//;
constructor({ host = config_default$1.host, fetchFn = fetchWithTimeout, fetchOpts = {}, headers = {} } = {}) {
const schema = this.hostSchemaRe.exec(host)?.[1];
this.host = schema ? host.replace(`${schema}://`, "") : host;
this.schema = schema ?? "https";
this.fetch = fetchFn;
this.fetchOpts = fetchOpts;
this.headers = {
...this.headers,
...headers
};
}
async request(path, body, headers = {}, method = "POST") {
const options = this.getOpts(new Blob([body]), headers, method);
try {
const res = await this.fetch(`${this.schema}://${this.host}${path}`, options);
const data = await res.arrayBuffer();
return {
success: res.status === 200,
data
};
} catch (err) {
return {
success: false,
data: err?.message
};
}
}
async requestJSON(path, body = null, headers = {}, method = "POST") {
const options = this.getOpts(body, {
"Content-Type": "application/json",
...headers
}, method);
try {
const res = await this.fetch(`${this.schema}://${this.host}${path}`, options);
const data = await res.json();
return {
success: res.status === 200,
data
};
} catch (err) {
return {
success: false,
data: err?.message
};
}
}
getOpts(body, headers = {}, method = "POST") {
return {
method,
headers: {
...this.headers,
...headers
},
body,
...this.fetchOpts
};
}
async getSession(module) {
const timestamp = getTimestamp$1();
const session = this.sessions[module];
if (session && session.timestamp + session.expires > timestamp) return session;
const { secretKey, expires, uuid } = await this.createSession(module);
this.sessions[module] = {
secretKey,
expires,
timestamp,
uuid
};
return this.sessions[module];
}
async createSession(module) {
const uuid = getUUID();
const body = YandexSessionProtobuf.encodeSessionRequest(uuid, module);
const res = await this.request("/session/create", body, { "Vtrans-Signature": await getSignature(body) });
if (!res.success) throw new VOTJSError("Failed to request create session", res);
return {
...YandexSessionProtobuf.decodeSessionResponse(res.data),
uuid
};
}
};
var VOTClient$1 = class extends MinimalClient {
hostVOT;
schemaVOT;
apiToken;
requestLang;
responseLang;
paths = {
videoTranslation: "/video-translation/translate",
videoTranslationFailAudio: "/video-translation/fail-audio-js",
videoTranslationAudio: "/video-translation/audio",
videoTranslationCache: "/video-translation/cache",
videoSubtitles: "/video-subtitles/get-subtitles",
streamPing: "/stream-translation/ping-stream",
streamTranslation: "/stream-translation/translate-stream"
};
isCustomLink(url) {
return !!(/\.(m3u8|m4(a|v)|mpd)/.exec(url) ?? /^https:\/\/cdn\.qstv\.on\.epicgames\.com/.exec(url));
}
headersVOT = {
"User-Agent": `vot.js/${config_default$1.version}`,
"Content-Type": "application/json",
Pragma: "no-cache",
"Cache-Control": "no-cache"
};
constructor({ host, hostVOT = config_default$1.hostVOT, fetchFn, fetchOpts, requestLang = "en", responseLang = "ru", apiToken, headers } = {}) {
super({
host,
fetchFn,
fetchOpts,
headers
});
const schemaVOT = this.hostSchemaRe.exec(hostVOT)?.[1];
this.hostVOT = schemaVOT ? hostVOT.replace(`${schemaVOT}://`, "") : hostVOT;
this.schemaVOT = schemaVOT ?? "https";
this.requestLang = requestLang;
this.responseLang = responseLang;
this.apiToken = apiToken;
}
get apiTokenHeader() {
if (!this.apiToken) return {};
return { Authorization: `OAuth ${this.apiToken}` };
}
async requestVOT(path, body, headers = {}) {
const options = this.getOpts(JSON.stringify(body), {
...this.headersVOT,
...headers
});
try {
const res = await this.fetch(`${this.schemaVOT}://${this.hostVOT}${path}`, options);
const data = await res.json();
return {
success: res.status === 200,
data
};
} catch (err) {
return {
success: false,
data: err?.message
};
}
}
async translateVideoYAImpl({ videoData, requestLang = this.requestLang, responseLang = this.responseLang, translationHelp = null, headers = {}, extraOpts = {}, shouldSendFailedAudio = true }) {
const { url, duration = config_default$1.defaultDuration } = videoData;
const session = await this.getSession("video-translation");
const body = YandexVOTProtobuf.encodeTranslationRequest(url, duration, requestLang, responseLang, translationHelp, extraOpts);
const path = this.paths.videoTranslation;
const vtransHeaders = await getSecYaHeaders("Vtrans", session, body, path);
const apiTokenHeader = extraOpts.useLivelyVoice ? this.apiTokenHeader : {};
const res = await this.request(path, body, {
...vtransHeaders,
...apiTokenHeader,
...headers
});
if (!res.success) throw new VOTJSError("Failed to request video translation", res);
const translationData = YandexVOTProtobuf.decodeTranslationResponse(res.data);
Logger.log("translateVideo", translationData);
const { status, translationId } = translationData;
switch (status) {
case VideoTranslationStatus.FAILED: throw new VOTJSError("Yandex couldn't translate video", translationData);
case VideoTranslationStatus.FINISHED:
case VideoTranslationStatus.PART_CONTENT:
if (!translationData.url) throw new VOTJSError("Audio link wasn't received from Yandex response", translationData);
return {
translationId,
translated: true,
url: translationData.url,
status,
remainingTime: translationData.remainingTime ?? -1
};
case VideoTranslationStatus.WAITING:
case VideoTranslationStatus.LONG_WAITING: return {
translationId,
translated: false,
status,
remainingTime: translationData.remainingTime ?? -1
};
case VideoTranslationStatus.AUDIO_REQUESTED:
if (url.startsWith("https://youtu.be/") && shouldSendFailedAudio) {
await this.requestVtransFailAudio(url);
await this.requestVtransAudio(url, translationData.translationId, {
audioFile: new Uint8Array(),
fileId: AudioDownloadType.WEB_API_GET_ALL_GENERATING_URLS_DATA_FROM_IFRAME
});
return await this.translateVideoYAImpl({
videoData,
requestLang,
responseLang,
translationHelp,
headers,
shouldSendFailedAudio: false
});
}
return {
translationId,
translated: false,
status,
remainingTime: translationData.remainingTime ?? -1
};
case VideoTranslationStatus.SESSION_REQUIRED: throw new VOTJSError("Yandex auth required to translate video. See docs for more info", translationData);
default:
Logger.error("Unknown response", translationData);
throw new VOTJSError("Unknown response from Yandex", translationData);
}
}
async translateVideoVOTImpl({ url, videoId, service, requestLang = this.requestLang, responseLang = this.responseLang, headers = {}, provider = "yandex" }) {
const votData = convertVOT(service, videoId, url);
const res = await this.requestVOT(this.paths.videoTranslation, {
provider,
service: votData.service,
video_id: votData.videoId,
from_lang: requestLang,
to_lang: responseLang,
raw_video: url
}, { ...headers });
if (!res.success) throw new VOTJSError("Failed to request video translation", res);
const translationData = res.data;
switch (translationData.status) {
case "failed": throw new VOTJSError("Yandex couldn't translate video", translationData);
case "success":
if (!translationData.translated_url) throw new VOTJSError("Audio link wasn't received from VOT response", translationData);
return {
translationId: String(translationData.id),
translated: true,
url: translationData.translated_url,
status: 1,
remainingTime: -1
};
case "waiting": return {
translationId: "",
translated: false,
remainingTime: translationData.remaining_time,
status: 2,
message: translationData.message
};
}
}
async requestVtransFailAudio(url) {
const res = await this.requestJSON(this.paths.videoTranslationFailAudio, JSON.stringify({ video_url: url }), void 0, "PUT");
if (!res.data || typeof res.data === "string" || res.data.status !== 1) throw new VOTJSError("Failed to request to fake video translation fail audio js", res);
return res;
}
async requestVtransAudio(url, translationId, audioBuffer, partialAudio, headers = {}) {
const session = await this.getSession("video-translation");
let body;
if (YandexVOTProtobuf.isPartialAudioBuffer(audioBuffer)) {
if (!partialAudio) throw new VOTJSError("Partial audio metadata is required for partial audio buffer", audioBuffer);
body = YandexVOTProtobuf.encodeTranslationAudioRequest(url, translationId, audioBuffer, partialAudio);
} else body = YandexVOTProtobuf.encodeTranslationAudioRequest(url, translationId, audioBuffer, void 0);
const path = this.paths.videoTranslationAudio;
const vtransHeaders = await getSecYaHeaders("Vtrans", session, body, path);
const res = await this.request(path, body, {
...vtransHeaders,
...headers
}, "PUT");
if (!res.success) throw new VOTJSError("Failed to request video translation audio", res);
return YandexVOTProtobuf.decodeTranslationAudioResponse(res.data);
}
async translateVideoCache({ videoData, requestLang = this.requestLang, responseLang = this.responseLang, headers = {} }) {
const { url, duration = config_default$1.defaultDuration } = videoData;
const session = await this.getSession("video-translation");
const body = YandexVOTProtobuf.encodeTranslationCacheRequest(url, duration, requestLang, responseLang);
const path = this.paths.videoTranslationCache;
const vtransHeaders = await getSecYaHeaders("Vtrans", session, body, path);
const res = await this.request(path, body, {
...vtransHeaders,
...headers
}, "POST");
if (!res.success) throw new VOTJSError("Failed to request video translation cache", res);
return YandexVOTProtobuf.decodeTranslationCacheResponse(res.data);
}
async translateVideo({ videoData, requestLang = this.requestLang, responseLang = this.responseLang, translationHelp = null, headers = {}, extraOpts = {}, shouldSendFailedAudio = true }) {
const { url, videoId, host } = videoData;
return this.isCustomLink(url) ? await this.translateVideoVOTImpl({
url,
videoId,
service: host,
requestLang,
responseLang,
headers,
provider: extraOpts.useLivelyVoice ? "yandex_lively" : "yandex"
}) : await this.translateVideoYAImpl({
videoData,
requestLang,
responseLang,
translationHelp,
headers,
extraOpts,
shouldSendFailedAudio
});
}
async getSubtitlesYAImpl({ videoData, requestLang = this.requestLang, headers = {} }) {
const { url } = videoData;
const session = await this.getSession("video-translation");
const body = YandexVOTProtobuf.encodeSubtitlesRequest(url, requestLang);
const path = this.paths.videoSubtitles;
const vsubsHeaders = await getSecYaHeaders("Vsubs", session, body, path);
const res = await this.request(path, body, {
...vsubsHeaders,
...headers
});
if (!res.success) throw new VOTJSError("Failed to request video subtitles", res);
const subtitlesData = YandexVOTProtobuf.decodeSubtitlesResponse(res.data);
const subtitles = subtitlesData.subtitles.map((subtitle) => {
const { language, url, translatedLanguage, translatedUrl } = subtitle;
return {
language,
url,
translatedLanguage,
translatedUrl
};
});
return {
waiting: subtitlesData.waiting,
subtitles
};
}
async getSubtitlesVOTImpl({ url, videoId, service, headers = {} }) {
const votData = convertVOT(service, videoId, url);
const res = await this.requestVOT(this.paths.videoSubtitles, {
provider: "yandex",
service: votData.service,
video_id: votData.videoId
}, headers);
if (!res.success) throw new VOTJSError("Failed to request video subtitles", res);
const subtitlesData = res.data;
return {
waiting: false,
subtitles: subtitlesData.reduce((result, subtitle) => {
if (!subtitle.lang_from) return result;
const originalSubtitle = subtitlesData.find((sub) => sub.lang === subtitle.lang_from);
if (!originalSubtitle) return result;
result.push({
language: originalSubtitle.lang,
url: originalSubtitle.subtitle_url,
translatedLanguage: subtitle.lang,
translatedUrl: subtitle.subtitle_url
});
return result;
}, [])
};
}
async getSubtitles({ videoData, requestLang = this.requestLang, headers = {} }) {
const { url, videoId, host } = videoData;
return this.isCustomLink(url) ? await this.getSubtitlesVOTImpl({
url,
videoId,
service: host,
headers
}) : await this.getSubtitlesYAImpl({
videoData,
requestLang,
headers
});
}
async pingStream({ pingId, headers = {} }) {
const session = await this.getSession("video-translation");
const body = YandexVOTProtobuf.encodeStreamPingRequest(pingId);
const path = this.paths.streamPing;
const vtransHeaders = await getSecYaHeaders("Vtrans", session, body, path);
const res = await this.request(path, body, {
...vtransHeaders,
...headers
});
if (!res.success) throw new VOTJSError("Failed to request stream ping", res);
return true;
}
async translateStream({ videoData, requestLang = this.requestLang, responseLang = this.responseLang, headers = {} }) {
const { url } = videoData;
if (this.isCustomLink(url)) throw new VOTJSError("Unsupported video URL for getting stream translation");
const session = await this.getSession("video-translation");
const body = YandexVOTProtobuf.encodeStreamRequest(url, requestLang, responseLang);
const path = this.paths.streamTranslation;
const vtransHeaders = await getSecYaHeaders("Vtrans", session, body, path);
const res = await this.request(path, body, {
...vtransHeaders,
...headers
});
if (!res.success) throw new VOTJSError("Failed to request stream translation", res);
const translateResponse = YandexVOTProtobuf.decodeStreamResponse(res.data);
const interval = translateResponse.interval;
switch (interval) {
case StreamInterval.NO_CONNECTION:
case StreamInterval.TRANSLATING: return {
translated: false,
interval,
message: interval === StreamInterval.NO_CONNECTION ? "streamNoConnectionToServer" : "translationTakeFewMinutes"
};
case StreamInterval.STREAMING:
if (translateResponse.pingId === void 0) throw new VOTJSError("Stream ping id wasn't received from Yandex response", translateResponse);
return {
translated: true,
interval,
pingId: translateResponse.pingId,
result: translateResponse.translatedInfo
};
default:
Logger.error("Unknown response", translateResponse);
throw new VOTJSError("Unknown response from Yandex", translateResponse);
}
}
};
var VOTWorkerClient$1 = class extends VOTClient$1 {
constructor(opts = {}) {
opts.host = opts.host ?? config_default$1.hostWorker;
super(opts);
}
async request(path, body, headers = {}, method = "POST") {
const options = this.getOpts(JSON.stringify({
headers: {
...this.headers,
...headers
},
body: Array.from(body)
}), { "Content-Type": "application/json" }, method);
try {
const res = await this.fetch(`${this.schema}://${this.host}${path}`, options);
const data = await res.arrayBuffer();
return {
success: res.status === 200,
data
};
} catch (err) {
return {
success: false,
data: err?.message
};
}
}
async requestJSON(path, body = null, headers = {}, method = "POST") {
const options = this.getOpts(JSON.stringify({
headers: {
...this.headers,
"Content-Type": "application/json",
Accept: "application/json",
...headers
},
body
}), {
Accept: "application/json",
"Content-Type": "application/json"
}, method);
try {
const res = await this.fetch(`${this.schema}://${this.host}${path}`, options);
const data = await res.json();
return {
success: res.status === 200,
data
};
} catch (err) {
return {
success: false,
data: err?.message
};
}
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/client.js
var VOTClient = class extends VOTClient$1 {
constructor(opts) {
super(opts);
this.headers = {
...browserSecHeaders,
...this.headers
};
}
};
var VOTWorkerClient = class extends VOTWorkerClient$1 {
constructor(opts) {
super(opts);
this.headers = {
...browserSecHeaders,
...this.headers
};
}
};
//#endregion
//#region node_modules/@vot.js/core/dist/utils/videoData.js
var VideoDataError = class extends Error {
constructor(message) {
super(message);
this.name = "VideoDataError";
}
};
var localLinkRe = /(file:\/\/(\/)?|(http(s)?:\/\/)(127\.0\.0\.1|localhost|192\.168\.(\d){1,3}\.(\d){1,3}))/;
//#endregion
//#region node_modules/@vot.js/shared/dist/data/alternativeUrls.js
var sitesInvidious = [
"yewtu.be",
"inv.nadeko.net",
"invidious.nerdvpn.de",
"invidious.protokolla.fi",
"invidious.materialio.us",
"iv.melmac.space"
];
var sitesPiped = [
"piped.video",
"piped.kavin.rocks",
"piped.private.coffee"
];
var sitesProxiTok = [
"proxitok.pabloferreiro.es",
"proxitok.pussthecat.org",
"tok.habedieeh.re",
"proxitok.esmailelbob.xyz",
"proxitok.privacydev.net",
"tok.artemislena.eu",
"tok.adminforge.de",
"tt.vern.cc",
"cringe.whatever.social",
"proxitok.lunar.icu",
"proxitok.privacy.com.de"
];
var sitesPeertube = [
"peertube.tmp.rcp.tf",
"dalek.zone",
"video.sadmin.io",
"videos.viorsan.com",
"peertube.1312.media",
"tube.shanti.cafe",
"bee-tube.fr",
"video.blender.org",
"beetoons.tv",
"makertube.net",
"peertube.tv",
"framatube.org",
"tilvids.com",
"diode.zone",
"fedimovie.com",
"video.hardlimit.com",
"share.tube",
"peervideo.club"
];
var sitesCoursehunterLike = ["coursehunter.net", "coursetrain.net"];
//#endregion
//#region node_modules/@vot.js/ext/dist/types/service.js
var ExtVideoService;
(function(ExtVideoService) {
ExtVideoService["udemy"] = "udemy";
ExtVideoService["coursera"] = "coursera";
ExtVideoService["douyin"] = "douyin";
ExtVideoService["artstation"] = "artstation";
ExtVideoService["kickstarter"] = "kickstarter";
ExtVideoService["datacamp"] = "datacamp";
ExtVideoService["oraclelearn"] = "oraclelearn";
ExtVideoService["deeplearningai"] = "deeplearningai";
ExtVideoService["netacad"] = "netacad";
ExtVideoService["mediafile"] = "mediafile";
ExtVideoService["skilljar"] = "skilljar";
})(ExtVideoService || (ExtVideoService = {}));
({
...VideoService$1,
...ExtVideoService
});
//#endregion
//#region node_modules/@vot.js/ext/dist/data/sites.js
var sharedSelectors = {
bilibiliPlayer: ".bpx-player-video-wrap, div.player-mobile-box.player-mobile-autoplay",
flowplayer: ".fp-player, div.flowplayer",
idPlayer: "#player",
jwPlayer: ".jwplayer, .jw-media",
player: ".player",
shakaPlayer: ".shaka-video-container, [id^=\"shaka-video-container-\"]",
videoJsUniversal: "[id^='vjs_video_']:not([id*='_html5_api']):not(video), video-js:not([id*='_html5_api']), .video-js:not(video):not([id*='_html5_api']), .vjs-player:not([id*='_html5_api']), [data-vjs-player]:not([id*='_html5_api'])",
vkVideoPlayer: ".videoplayer_media, vk-video-player"
};
var sites_default = [
{
additionalData: "mobile",
host: VideoService$1.youtube,
url: "https://youtu.be/",
match: /^m.youtube.com$/,
selector: ".player-container",
needExtraData: true
},
{
host: VideoService$1.youtube,
url: "https://youtu.be/",
match: (enteredUrl) => /^(www.)?youtube(-nocookie|kids)?.com$/.test(enteredUrl.hostname) && enteredUrl.pathname.startsWith("/tv"),
selector: "#container",
needExtraData: true
},
{
host: VideoService$1.youtube,
url: "https://youtu.be/",
match: (url) => /^(www.)?youtube(-nocookie|kids)?.com$/.test(url.host) && !url.pathname.startsWith("/embed/"),
selector: ".html5-video-container:not(#inline-player *)",
needExtraData: true
},
{
host: VideoService$1.youtube,
url: "https://youtu.be/",
additionalData: "embed",
match: (url) => /^(www.)?youtube(-nocookie|kids)?.com$/.test(url.host) && url.pathname.startsWith("/embed/"),
selector: "html",
needExtraData: true
},
{
host: VideoService$1.invidious,
url: "https://youtu.be/",
match: sitesInvidious,
selector: sharedSelectors.idPlayer,
needBypassCSP: true
},
{
host: VideoService$1.piped,
url: "https://youtu.be/",
match: sitesPiped,
selector: sharedSelectors.shakaPlayer,
needBypassCSP: true
},
{
host: VideoService$1.preservetube,
url: "https://preservetube.com/",
match: /^preservetube\.com$/,
selector: "div.video-wrapper",
needExtraData: true
},
{
host: VideoService$1.zdf,
url: "https://www.zdf.de/play/",
match: [/^zdf.de$/, /^(www.)?zdf.de$/],
selector: "div.zdfplayer-app.zdfplayer-desktop, div.zdfplayer-app"
},
{
host: VideoService$1.niconico,
url: "https://www.nicovideo.jp/watch/",
match: [/^(www\.|sp\.)?nicovideo\.jp$/, /^nico\.ms$/],
selector: `[class*="grid-area_[player]"] > div`
},
{
additionalData: "mobile",
host: VideoService$1.vk,
url: "https://vk.com/",
match: [/^m.vk.(com|ru)$/, /^m.vkvideo.ru$/],
selector: sharedSelectors.vkVideoPlayer,
shadowRoot: true,
needExtraData: true
},
{
additionalData: "clips",
host: VideoService$1.vk,
url: "https://vk.com/",
match: /^(www.|m.)?vk.(com|ru)$/,
selector: "div[data-testid=\"clipcontainer-video\"]",
needExtraData: true
},
{
host: VideoService$1.vk,
url: "https://vk.com/",
match: [/^(www\.|m\.)?vk\.(com|ru)$/, /^(.*\.)?vkvideo\.ru$/],
selector: sharedSelectors.vkVideoPlayer,
needExtraData: true
},
{
host: VideoService$1.nine_gag,
url: "https://9gag.com/gag/",
match: /^9gag.com$/,
selector: ".video-post",
needExtraData: true
},
{
host: VideoService$1.twitch,
url: "https://twitch.tv/",
match: [
/^m.twitch.tv$/,
/^(www.)?twitch.tv$/,
/^clips.twitch.tv$/,
/^player.twitch.tv$/
],
needExtraData: true,
selector: ".video-ref, main > div > section > div > div > div"
},
{
host: VideoService$1.proxitok,
url: "https://www.tiktok.com/",
match: sitesProxiTok,
selector: ".column.has-text-centered"
},
{
host: VideoService$1.tiktok,
url: "https://www.tiktok.com/",
match: /^(www.)?tiktok.com$/,
selector: null
},
{
host: ExtVideoService.douyin,
url: "https://www.douyin.com/",
match: /^(www.)?douyin.com/,
selector: ".xg-video-container",
needExtraData: true,
needBypassCSP: true
},
{
host: VideoService$1.vimeo,
url: "https://vimeo.com/",
match: /^(www\.|m\.)?vimeo.com$/,
needExtraData: true,
selector: sharedSelectors.player
},
{
host: VideoService$1.vimeo,
url: "https://player.vimeo.com/",
match: /^player.vimeo.com$/,
additionalData: "embed",
needExtraData: true,
needBypassCSP: true,
selector: sharedSelectors.player
},
{
host: VideoService$1.xvideos,
url: "https://www.xvideos.com/",
match: [
/^(www.)?xvideos(-ar)?.com$/,
/^(www.)?xvideos(\d\d\d).com$/,
/^(www.)?xv-ru.com$/
],
selector: "#hlsplayer",
needBypassCSP: true
},
{
host: VideoService$1.xhamster,
url: "https://xhamster.com/",
match: (url) => /^(?:[^.]+\.)?(?:xhamster\.(?:com|desi)|xhamster\d+\.(?:com|desi)|xhvid\.com)$/.test(url.host) && /\/(?:videos\/[^/]+-[\dA-Za-z]+)\/?$/.test(url.pathname),
selector: "#player-container"
},
{
host: VideoService$1.spankbang,
url: "https://spankbang.com/",
match: (url) => /^(?:[^.]+\.)?spankbang\.com$/.test(url.host) && /\/(?:[\da-z]+\/(?:video|play|embed)(?:\/[^/]+)?|[\da-z]+-[\da-z]+\/playlist\/[^/?#&]+)\/?$/i.test(url.pathname),
selector: "#main_video_player"
},
{
host: VideoService$1.rule34video,
url: "https://rule34video.com/video/",
match: (url) => /^(www\.)?rule34video\.com$/.test(url.host) && /\/videos?\/\d+/.test(url.pathname),
selector: sharedSelectors.flowplayer
},
{
host: VideoService$1.picarto,
url: "https://picarto.tv/",
match: (url) => /^(www\.)?picarto\.tv$/.test(url.host) && /^(?:\/[^/]+\/(?:profile\/)?videos\/[^/?#&]+|\/videopopout\/[^/?#&]+|\/[^/#?]+\/?)$/.test(url.pathname),
selector: `[class*="VideosTab__PlayerWrapper"]`
},
{
host: VideoService$1.olympicsreplay,
url: "https://olympics.com/",
match: (url) => /^(www\.)?olympics\.com$/.test(url.host) && /^\/[a-z]{2}\/(?:[a-z0-9-]+\/)?(?:replay|videos?|original-series\/episode)\/[\w-]+\/?$/i.test(url.pathname),
selector: sharedSelectors.videoJsUniversal
},
{
host: VideoService$1.pornhub,
url: "https://rt.pornhub.com/view_video.php?viewkey=",
match: /^[a-z]+.pornhub.(com|org)$/,
selector: ".mainPlayerDiv > .video-element-wrapper-js > div",
eventSelector: ".mgp_eventCatcher"
},
{
additionalData: "embed",
host: VideoService$1.pornhub,
url: "https://rt.pornhub.com/view_video.php?viewkey=",
match: (url) => /^[a-z]+.pornhub.(com|org)$/.exec(url.host) && url.pathname.startsWith("/embed/"),
selector: sharedSelectors.idPlayer
},
{
host: VideoService$1.twitter,
url: "https://twitter.com/i/status/",
match: /^(twitter|x).com$/,
selector: "div[data-testid=\"videoComponent\"]",
needBypassCSP: true
},
{
host: VideoService$1.rumble,
url: "https://rumble.com/",
match: /^rumble.com$/,
selector: `[id^="vid_"] > div`
},
{
host: VideoService$1.facebook,
url: "https://facebook.com/",
match: (url) => url.host.includes("facebook.com") && url.pathname.includes("/videos/"),
selector: "div[role=\"main\"] div[data-pagelet$=\"video\" i]",
needBypassCSP: true
},
{
additionalData: "reels",
host: VideoService$1.facebook,
url: "https://facebook.com/",
match: (url) => url.host.includes("facebook.com") && url.pathname.includes("/reel/"),
selector: "div[role=\"main\"]",
needBypassCSP: true
},
{
host: VideoService$1.rutube,
url: "https://rutube.ru/video/",
match: /^rutube.ru$/,
selector: `div[class*="videoWrapper"]`
},
{
additionalData: "embed",
host: VideoService$1.rutube,
url: "https://rutube.ru/video/",
match: /^rutube.ru$/,
selector: "#app > div > div"
},
{
host: VideoService$1.bilibili,
url: "https://www.bilibili.com/",
match: /^(www|m|player).bilibili.com$/,
selector: sharedSelectors.bilibiliPlayer
},
{
host: VideoService$1.bilibili,
url: "https://www.bilibili.tv/",
match: /^(?:www\.|m\.)?bilibili\.tv$/,
selector: sharedSelectors.bilibiliPlayer
},
{
additionalData: "old",
host: VideoService$1.bilibili,
url: "https://www.bilibili.com/",
match: /^(www|m).bilibili.com$/,
selector: null
},
{
host: VideoService$1.mailru,
url: "https://my.mail.ru/",
match: /^my.mail.ru$/,
selector: "#b-video-wrapper"
},
{
host: VideoService$1.bitchute,
url: "https://www.bitchute.com/video/",
match: /^(www.)?bitchute.com$/,
selector: sharedSelectors.videoJsUniversal
},
{
host: VideoService$1.eporner,
url: "https://www.eporner.com/",
match: /^(www.)?eporner.com$/,
selector: sharedSelectors.videoJsUniversal
},
{
host: VideoService$1.peertube,
url: "stub",
match: sitesPeertube,
selector: sharedSelectors.videoJsUniversal
},
{
host: VideoService$1.dailymotion,
url: "https://www.dailymotion.com/video/",
match: /^((www\.|player\.)?dailymotion\.com|geo(\d+)?\.dailymotion\.com|dai\.ly)$/,
selector: sharedSelectors.player
},
{
host: VideoService$1.trovo,
url: "https://trovo.live/s/",
match: /^trovo.live$/,
selector: ".player-video"
},
{
host: VideoService$1.yandexdisk,
url: "https://yadi.sk/",
match: /^disk.yandex.(ru|kz|com(\.(am|ge|tr))?|by|az|co\.il|ee|lt|lv|md|net|tj|tm|uz)$/,
selector: ".video-player__player > div:nth-child(1)",
needBypassCSP: true,
needExtraData: true
},
{
host: VideoService$1.okru,
url: "https://ok.ru/video/",
match: /^ok.ru$/,
selector: sharedSelectors.vkVideoPlayer,
shadowRoot: true
},
{
host: VideoService$1.googledrive,
url: "https://drive.google.com/file/d/",
match: /^youtube.googleapis.com$/,
selector: "html"
},
{
host: VideoService$1.bannedvideo,
url: "https://madmaxworld.tv/watch?id=",
match: /^(www.)?banned.video|madmaxworld.tv$/,
selector: sharedSelectors.videoJsUniversal,
needExtraData: true
},
{
host: VideoService$1.weverse,
url: "https://weverse.io/",
match: /^weverse.io$/,
selector: ".webplayer-internal-source-wrapper",
needExtraData: true
},
{
host: VideoService$1.weibo,
url: "https://weibo.com/",
match: (url) => /^(?:www\.)?weibo\.com$/.test(url.host) && /^\/(?:\d+\/[A-Za-z0-9]+|0\/[A-Za-z0-9]+|tv\/show\/\d+:(?:[\da-f]{32}|\d{16,}))\/?$/.test(url.pathname) || /^video\.weibo\.com$/.test(url.host) && /^\/show\/?$/.test(url.pathname) && /^\d+:(?:[\da-f]{32}|\d{16,})$/i.test(url.searchParams.get("fid") ?? "") || /^(?:www\.)?weibo\.com$/.test(url.host) && /^\/newlogin\/?$/.test(url.pathname) && (url.searchParams.has("url") || /^[A-Za-z0-9]+$/.test(url.searchParams.get("layerid") ?? "")),
selector: sharedSelectors.videoJsUniversal || "#playVideo"
},
{
host: VideoService$1.newgrounds,
url: "https://www.newgrounds.com/",
match: /^(www.)?newgrounds.com$/,
selector: ".ng-video-player"
},
{
host: VideoService$1.egghead,
url: "https://egghead.io/",
match: /^egghead.io$/,
selector: ".cueplayer-react-video-holder"
},
{
host: VideoService$1.youku,
url: "https://v.youku.com/",
match: /^v.youku.com$/,
selector: "#ykPlayer"
},
{
host: VideoService$1.archive,
url: "https://archive.org/details/",
match: /^archive.org$/,
selector: sharedSelectors.jwPlayer,
needExtraData: true
},
{
host: VideoService$1.kodik,
url: "stub",
match: /^kodikplayer.com$/,
selector: sharedSelectors.flowplayer,
needExtraData: true
},
{
host: VideoService$1.patreon,
url: "stub",
match: /^(www.)?patreon.com$/,
selector: "div[data-tag=\"post-card\"] div[elevation=\"subtle\"] > div > div > div > div",
needExtraData: true
},
{
additionalData: "old",
host: VideoService$1.reddit,
url: "stub",
match: /^old.reddit.com$/,
selector: ".reddit-video-player-root",
needExtraData: true,
needBypassCSP: true
},
{
host: VideoService$1.reddit,
url: "stub",
match: /^(www.|new.)?reddit.com$/,
selector: "div[slot=post-media-container]",
shadowRoot: true,
needExtraData: true,
needBypassCSP: true
},
{
host: VideoService$1.kick,
url: "https://kick.com/",
match: /^kick.com$/,
selector: "#injected-embedded-channel-player-video > div",
needExtraData: true
},
{
host: VideoService$1.appledeveloper,
url: "https://developer.apple.com/",
match: /^developer.apple.com$/,
selector: ".developer-video-player",
needExtraData: true,
needBypassCSP: true
},
{
host: VideoService$1.epicgames,
url: "https://dev.epicgames.com/community/learning/",
match: /^dev.epicgames.com$/,
selector: sharedSelectors.videoJsUniversal,
needExtraData: true
},
{
host: VideoService$1.odysee,
url: "stub",
match: /^odysee.com$/,
selector: ".video-js-parent",
needExtraData: true
},
{
host: VideoService$1.coursehunterLike,
url: "stub",
match: sitesCoursehunterLike,
selector: null,
needExtraData: true
},
{
host: VideoService$1.sap,
url: "https://learning.sap.com/courses/",
match: /^learning.sap.com$/,
selector: ".playkit-container",
eventSelector: ".playkit-player",
needExtraData: true,
needBypassCSP: true
},
{
host: ExtVideoService.udemy,
url: "https://www.udemy.com/",
match: /udemy.com$/,
selector: sharedSelectors.shakaPlayer,
needExtraData: true
},
{
host: ExtVideoService.datacamp,
url: "https://www.datacamp.com/courses/",
match: (url) => /^(?:campus\.|projector\.)?datacamp\.com$/.test(url.hostname),
selector: sharedSelectors.videoJsUniversal,
needExtraData: true
},
{
host: ExtVideoService.coursera,
url: "https://www.coursera.org/",
match: /coursera.org$/,
selector: sharedSelectors.videoJsUniversal,
needExtraData: true
},
{
host: VideoService$1.watchpornto,
url: "https://watchporn.to/",
match: /^watchporn.to$/,
selector: sharedSelectors.flowplayer
},
{
host: VideoService$1.jove,
url: "https://jove.com/",
match: /^(?:app|www)\.jove\.com$/,
selector: sharedSelectors.flowplayer
},
{
host: VideoService$1.linkedin,
url: "https://www.linkedin.com/learning/",
match: /^(www.)?linkedin.com$/,
selector: sharedSelectors.videoJsUniversal,
needExtraData: true,
needBypassCSP: true
},
{
host: VideoService$1.incestflix,
url: "https://www.incestflix.net/watch/",
match: /^(www.)?incestflix.(net|to|com)$/,
selector: "#incflix-stream",
needExtraData: true
},
{
host: VideoService$1.porntn,
url: "https://porntn.com/videos/",
match: /^porntn.com$/,
selector: sharedSelectors.flowplayer,
needExtraData: true
},
{
host: VideoService$1.dzen,
url: "https://dzen.ru/video/watch/",
match: /^dzen.ru$/,
selector: `[class*="player__playerWrap"] > div`
},
{
host: VideoService$1.bunnystream,
url: "stub",
match: [
/^video\.bunnycdn\.com$/,
/^iframe\.mediadelivery\.net$/,
/^(?:[^.]+\.)*b-cdn\.net$/
],
selector: null
},
{
host: VideoService$1.cloudflarestream,
url: "stub",
match: /^(watch|embed|iframe|customer-[^.]+).cloudflarestream.com$/,
selector: null
},
{
host: VideoService$1.loom,
url: "https://www.loom.com/share/",
match: /^(www.)?loom.com$/,
selector: ".VideoLayersContainer",
needExtraData: true,
needBypassCSP: true
},
{
host: ExtVideoService.artstation,
url: "https://www.artstation.com/learning/",
match: /^(www.)?artstation.com$/,
selector: sharedSelectors.videoJsUniversal,
needExtraData: true
},
{
host: VideoService$1.rtnews,
url: "https://www.rt.com/",
match: /^(www.)?rt.com$/,
selector: sharedSelectors.jwPlayer,
needExtraData: true
},
{
host: VideoService$1.bitview,
url: "https://www.bitview.net/watch?v=",
match: /^(www.)?bitview.net$/,
selector: ".vlScreen",
needExtraData: true
},
{
host: ExtVideoService.kickstarter,
url: "https://www.kickstarter.com/",
match: /^(www.)?kickstarter.com/,
selector: ".ksr-video-player",
needExtraData: true
},
{
host: VideoService$1.thisvid,
url: "https://thisvid.com/",
match: /^(www.)?thisvid.com$/,
selector: sharedSelectors.flowplayer
},
{
additionalData: "regional",
host: VideoService$1.ign,
url: "https://de.ign.com/",
match: /^(\w{2}.)?ign.com$/,
needExtraData: true,
selector: ".video-container"
},
{
host: VideoService$1.ign,
url: "https://www.ign.com/",
match: /^(www.)?ign.com$/,
selector: sharedSelectors.player,
needExtraData: true
},
{
host: VideoService$1.bunkr,
url: "https://bunkr.site/",
match: /^bunkr\.(site|black|cat|media|red|site|ws|org|s[kiu]|c[ir]|fi|p[hks]|ru|la|is|to|a[cx])$/,
needExtraData: true,
selector: ".plyr__video-wrapper"
},
{
host: VideoService$1.imdb,
url: "https://www.imdb.com/video/",
match: /^(www\.)?imdb\.com$/,
selector: sharedSelectors.jwPlayer
},
{
host: VideoService$1.telegram,
url: "https://t.me/",
match: (url) => /^web\.telegram\.org$/.test(url.hostname) && url.pathname.startsWith("/k"),
selector: ".ckin__player"
},
{
host: ExtVideoService.oraclelearn,
url: "https://mylearn.oracle.com/ou/course/",
match: /^mylearn\.oracle\.com/,
selector: sharedSelectors.videoJsUniversal,
needExtraData: true,
needBypassCSP: true
},
{
host: ExtVideoService.deeplearningai,
url: "https://learn.deeplearning.ai/courses/",
match: /^learn(-dev|-staging)?\.deeplearning\.ai/,
selector: ".lesson-video-player",
needExtraData: true
},
{
host: ExtVideoService.netacad,
url: "https://www.netacad.com/",
match: /^(www\.)?netacad\.com/,
selector: sharedSelectors.videoJsUniversal,
needExtraData: true
},
{
host: ExtVideoService.mediafile,
url: "https://mediafile.cc/",
match: /^(www\.)?mediafile\.cc$/,
selector: "div#playerContainer",
needExtraData: true
},
{
host: ExtVideoService.skilljar,
url: "https://anthropic.skilljar.com/",
match: /skilljar\.com$/,
selector: sharedSelectors.jwPlayer,
needExtraData: true
},
{
host: VideoService$1.custom,
url: "stub",
match: (url) => /([^.]+)\.(mp4|webm)/.test(url.pathname),
rawResult: true
}
];
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/base.js
var VideoHelperError = class extends Error {
constructor(message) {
super(message);
this.name = "VideoHelperError";
}
};
var BaseHelper = class {
API_ORIGIN = window.location.origin;
fetch;
extraInfo;
referer;
origin;
service;
video;
language;
constructor({ fetchFn = fetchWithTimeout, extraInfo = true, referer = document.referrer ?? `${window.location.origin}/`, origin = window.location.origin, service, video, language = "en" } = {}) {
this.fetch = fetchFn;
this.extraInfo = extraInfo;
this.referer = referer;
this.origin = /^(http(s)?):\/\//.test(String(origin)) ? origin : window.location.origin;
this.service = service;
this.video = video;
this.language = language;
}
getVideoData(_videoId) {
return Promise.resolve(void 0);
}
getVideoId(_url) {
return Promise.resolve(void 0);
}
returnBaseData(videoId) {
if (!this.service) return;
return {
url: this.service.url + videoId,
videoId,
host: this.service.host,
duration: void 0
};
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/appledeveloper.js
var AppleDeveloperHelper = class extends BaseHelper {
API_ORIGIN = "https://developer.apple.com";
async getVideoData(videoId) {
try {
const contentUrl = document.querySelector("meta[property='og:video']")?.content;
if (!contentUrl) throw new VideoHelperError("Failed to find content url");
return { url: contentUrl };
} catch (err) {
Logger.error(`Failed to get apple developer video data by video ID: ${videoId}`, err.message);
return;
}
}
async getVideoId(url) {
return /videos\/play\/([^/]+)\/([\d]+)/.exec(url.pathname)?.[0];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/archive.js
var ArchiveHelper = class extends BaseHelper {
async getVideoId(url) {
const videoId = /(details|embed)\/(.+)/.exec(url.pathname)?.[2];
if (!videoId) return void 0;
return videoId.replace(/\/$/, "") || void 0;
}
async getVideoData(videoId) {
if (!videoId) return void 0;
const downloadUrl = `https://archive.org/download/${videoId.replace(/\+/g, "%20").replace(/\.[^.]+$/, ".mp4")}`;
return {
url: videoId,
video_url: downloadUrl,
translationHelp: [{
target: "video_file_url",
targetUrl: downloadUrl
}]
};
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/artstation.js
var ArtstationHelper = class extends BaseHelper {
API_ORIGIN = "https://www.artstation.com/api/v2/learning";
getCSRFToken() {
return document.querySelector("meta[name=\"public-csrf-token\"]")?.content;
}
async getCourseInfo(courseId) {
try {
const csrfToken = this.getCSRFToken();
return await (await this.fetch(`${this.API_ORIGIN}/courses/${courseId}/autoplay.json`, {
method: "POST",
headers: csrfToken ? { "PUBLIC-CSRF-TOKEN": csrfToken } : {}
})).json();
} catch (err) {
Logger.error(`Failed to get artstation course info by courseId: ${courseId}.`, err.message);
return false;
}
}
async getVideoUrl(chapterId) {
try {
return (await (await this.fetch(`${this.API_ORIGIN}/quicksilver/video_url.json?chapter_id=${chapterId}`)).json()).url.replace("qsep://", "https://");
} catch (err) {
Logger.error(`Failed to get artstation video url by chapterId: ${chapterId}.`, err.message);
return false;
}
}
async getVideoData(videoId) {
const [, courseId, , , chapterId] = videoId.split("/");
const courseInfo = await this.getCourseInfo(courseId);
if (!courseInfo) return;
const chapter = courseInfo.chapters.find((chapter) => chapter.hash_id === chapterId);
if (!chapter) return;
const videoUrl = await this.getVideoUrl(chapter.id);
if (!videoUrl) return;
const { title, duration, subtitles: videoSubtitles } = chapter;
return {
url: videoUrl,
title,
duration,
subtitles: videoSubtitles.filter((subtitle) => subtitle.format === "vtt").map((subtitle) => ({
language: normalizeLang$1(subtitle.locale),
source: "artstation",
format: "vtt",
url: subtitle.file_url
}))
};
}
async getVideoId(url) {
return /courses\/(\w{3,5})\/([^/]+)\/chapters\/(\w{3,5})/.exec(url.pathname)?.[0];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/bannedvideo.js
var BannedVideoHelper = class extends BaseHelper {
API_ORIGIN = "https://api.banned.video";
async getVideoInfo(videoId) {
try {
return await (await this.fetch(`${this.API_ORIGIN}/graphql`, {
method: "POST",
body: JSON.stringify({
operationName: "GetVideo",
query: `query GetVideo($id: String!) {
getVideo(id: $id) {
title
description: summary
duration: videoDuration
videoUrl: directUrl
isStream: live
}
}`,
variables: { id: videoId }
}),
headers: {
"User-Agent": "bannedVideoFrontEnd",
"apollographql-client-name": "banned-web",
"apollographql-client-version": "1.3",
"content-type": "application/json"
}
})).json();
} catch (err) {
console.error(`Failed to get bannedvideo video info by videoId: ${videoId}.`, err.message);
return false;
}
}
async getVideoData(videoId) {
const videoInfo = await this.getVideoInfo(videoId);
if (!videoInfo) return;
const { videoUrl, duration, isStream, description, title } = videoInfo.data.getVideo;
return {
url: videoUrl,
duration,
isStream,
title,
description
};
}
async getVideoId(url) {
return url.searchParams.get("id") ?? void 0;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/bilibili.js
var BilibiliHelper = class extends BaseHelper {
async getVideoId(url) {
const bangumiId = /bangumi\/play\/([^/]+)/.exec(url.pathname)?.[0];
if (bangumiId) return bangumiId;
const bvid = url.searchParams.get("bvid");
if (bvid) return `video/${bvid}`;
const intlId = /^\/(?:[a-z]{2}\/)?((?:play\/\d+(?:\/\d+)?|video\/\d+))\/?$/i.exec(url.pathname)?.[1];
if (intlId) return intlId;
let vid = /video\/([^/]+)/.exec(url.pathname)?.[0];
if (vid && url.searchParams.get("p") !== null) vid += `/?p=${url.searchParams.get("p")}`;
return vid;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/bitchute.js
var BitchuteHelper = class extends BaseHelper {
async getVideoId(url) {
return /(video|embed)\/([^/]+)/.exec(url.pathname)?.[2];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/bitview.js
var BitviewHelper = class extends BaseHelper {
async getVideoData(videoId) {
try {
const videoUrl = document.querySelector(".vlScreen > video")?.src;
if (!videoUrl) throw new VideoHelperError("Failed to find video URL");
return { url: videoUrl };
} catch (err) {
Logger.error(`Failed to get Bitview data by videoId: ${videoId}`, err.message);
return;
}
}
async getVideoId(url) {
return url.searchParams.get("v");
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/bunkr.js
var BunkrHelper = class extends BaseHelper {
async getVideoData(_videoId) {
const url = document.querySelector("#player > source[type=\"video/mp4\"]")?.src;
if (!url) return;
return { url };
}
async getVideoId(url) {
return /\/f\/([^/]+)/.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/bunnystream.js
var BunnyStreamHelper = class extends BaseHelper {
async getVideoId(url) {
return url.pathname + url.search;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/cloudflarestream.js
var CloudflareStreamHelper = class extends BaseHelper {
async getVideoId(url) {
return url.pathname + url.search;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/coursehunterLike.js
var CoursehunterLikeHelper = class extends BaseHelper {
API_ORIGIN = this.origin ?? "https://coursehunter.net";
async getCourseId() {
const courseId = window.course_id;
if (courseId !== void 0) return String(courseId);
return document.querySelector("input[name=\"course_id\"]")?.value;
}
async getLessonsData(courseId) {
const lessons = window.lessons;
if (lessons?.length) return lessons;
try {
return await (await this.fetch(`${this.API_ORIGIN}/api/v1/course/${courseId}/lessons`)).json();
} catch (err) {
Logger.error(`Failed to get CoursehunterLike lessons data by courseId: ${courseId}, because ${err.message}`);
return;
}
}
getLessondId(videoId) {
let lessondId = videoId.split("?lesson=")?.[1];
if (lessondId) return +lessondId;
lessondId = document.querySelector(".lessons-item_active")?.dataset?.index;
if (lessondId) return +lessondId;
return 1;
}
async getVideoData(videoId) {
const courseId = await this.getCourseId();
if (!courseId) return;
const lessonsData = await this.getLessonsData(courseId);
if (!lessonsData) return;
const lessonId = this.getLessondId(videoId);
const { file: videoUrl, duration, title } = lessonsData?.[lessonId - 1];
if (!videoUrl) return;
return {
url: videoId,
video_url: videoUrl,
translationHelp: [{
target: "video_file_url",
targetUrl: proxyMedia(videoUrl)
}],
duration,
title
};
}
async getVideoId(url) {
return url.href;
}
};
//#endregion
//#region node_modules/@vot.js/shared/dist/data/consts.js
var availableLangs = [
"auto",
"ru",
"en",
"zh",
"ko",
"lt",
"lv",
"ar",
"fr",
"it",
"es",
"de",
"ja"
];
var availableTTS = [
"ru",
"en",
"kk"
];
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/videojs.js
var VideoJSHelper = class VideoJSHelper extends BaseHelper {
SUBTITLE_SOURCE = "videojs";
SUBTITLE_FORMAT = "vtt";
static getPlayer() {
const vjs = window.videojs;
const techEl = document.querySelector("video.vjs-tech[id], video[id$='_html5_api']");
const derivedPlayerId = techEl?.id?.endsWith("_html5_api") ? techEl.id.slice(0, -10) : void 0;
if (vjs?.getPlayer) {
if (derivedPlayerId) {
const p = vjs.getPlayer(derivedPlayerId);
if (p) return p;
}
if (techEl) {
const p = vjs.getPlayer(techEl);
if (p) return p;
}
}
const players = (typeof vjs?.getPlayers === "function" ? vjs.getPlayers() : vjs?.players) ?? {};
for (const p of Object.values(players)) {
const player = p;
const innerVideo = (typeof player.el === "function" ? player.el() : null)?.querySelector?.("video.vjs-tech, video") ?? null;
if (innerVideo && techEl && innerVideo === techEl) return p;
if (derivedPlayerId && typeof player.id === "function" && player.id() === derivedPlayerId) return p;
}
}
getVideoDataByPlayer(videoId) {
try {
const player = VideoJSHelper.getPlayer();
const techEl = document.querySelector("video.vjs-tech, video[id$='_html5_api'], video[src]");
if (!player && !techEl) throw new Error(`Video player/video element not found, videoId ${videoId}`);
const duration = player?.duration?.() ?? techEl?.duration;
let url;
if (player) {
const sources = typeof player.currentSources === "function" ? player.currentSources() : player.getCache?.()?.sources;
url = (Array.isArray(sources) ? sources.find((source) => source?.type === "video/mp4" || source?.type === "video/webm" || source?.src) : void 0)?.src;
}
url ??= techEl?.currentSrc || techEl?.src || techEl?.getAttribute?.("src") || void 0;
if (!url) throw new Error(`Failed to find video url for videoID ${videoId}`);
const subtitles = (techEl ? Array.from(techEl.querySelectorAll("track[src]")) : []).filter((t) => t.kind !== "metadata").flatMap((t) => {
const src = t.getAttribute("src");
if (!src) return [];
const absUrl = new URL(src, window.location.href).toString();
return [{
language: normalizeLang$1(t.srclang || ""),
source: this.SUBTITLE_SOURCE,
format: this.SUBTITLE_FORMAT,
url: absUrl
}];
});
return {
url,
duration,
subtitles
};
} catch (err) {
Logger.error("Failed to get videojs video data", err.message);
return;
}
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/coursera.js
var CourseraHelper = class CourseraHelper extends VideoJSHelper {
API_ORIGIN = "https://www.coursera.org/api";
SUBTITLE_SOURCE = "coursera";
async getCourseData(courseId) {
try {
return (await (await this.fetch(`${this.API_ORIGIN}/onDemandCourses.v1/${courseId}`)).json())?.elements?.[0];
} catch (err) {
Logger.error(`Failed to get course data by courseId: ${courseId}`, err.message);
return;
}
}
static getPlayer() {
return VideoJSHelper.getPlayer();
}
async getVideoData(videoId) {
const data = this.getVideoDataByPlayer(videoId);
if (!data) return;
const { options_: options } = CourseraHelper.getPlayer() ?? {};
if (!data.subtitles?.length && options) data.subtitles = options.tracks.map((track) => ({
url: track.src,
language: normalizeLang$1(track.srclang),
source: this.SUBTITLE_SOURCE,
format: this.SUBTITLE_FORMAT
}));
const courseId = options?.courseId;
if (!courseId) return data;
let courseLang = "en";
const courseData = await this.getCourseData(courseId);
if (courseData) {
const { primaryLanguageCodes: [primaryLangauge] } = courseData;
courseLang = primaryLangauge ? normalizeLang$1(primaryLangauge) : "en";
}
if (!availableLangs.includes(courseLang)) courseLang = "en";
const subtitleUrl = (data.subtitles.find((subtitle) => subtitle.language === courseLang) ?? data.subtitles?.[0])?.url;
if (!subtitleUrl) Logger.warn("Failed to find any subtitle file");
const { url, duration } = data;
const translationHelp = subtitleUrl ? [{
target: "subtitles_file_url",
targetUrl: subtitleUrl
}, {
target: "video_file_url",
targetUrl: url
}] : null;
return {
...subtitleUrl ? {
url: this.service?.url + videoId,
translationHelp
} : {
url,
translationHelp
},
detectedLanguage: courseLang,
duration
};
}
async getVideoId(url) {
return (/learn\/([^/]+)\/lecture\/([^/]+)/.exec(url.pathname) ?? /lecture\/([^/]+)\/([^/]+)/.exec(url.pathname))?.[0];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/dailymotion.js
var DailymotionHelper = class extends BaseHelper {
getVideoIdFromUrl(url) {
const videoIdFromQuery = url.searchParams.get("video");
if (videoIdFromQuery) return videoIdFromQuery;
}
resolveVideoIdViaPostMessage() {
return new Promise((resolve) => {
const origin = "https://www.dailymotion.com";
const timeout = setTimeout(() => {
window.removeEventListener("message", onMessage);
resolve(void 0);
}, 3e3);
const onMessage = (e) => {
if (e.origin !== origin) return;
if (!(typeof e.data === "string" && e.data.startsWith("getVideoId:"))) return;
clearTimeout(timeout);
window.removeEventListener("message", onMessage);
resolve(e.data.replace("getVideoId:", ""));
};
window.addEventListener("message", onMessage);
window.top?.postMessage("getVideoId:", origin);
});
}
async getVideoId(url) {
const videoIdFromUrl = this.getVideoIdFromUrl(url);
if (videoIdFromUrl) return videoIdFromUrl;
if (window.self !== window.top) return await this.resolveVideoIdViaPostMessage();
return url.hostname === "dai.ly" ? url.pathname.slice(1) : /video\/([^/]+)/.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/datacamp.js
var DataCampHelper = class extends VideoJSHelper {
SUBTITLE_SOURCE = "datacamp";
getVideoDataFromInput() {
try {
const input = document.querySelector("#slideDeckData") || document.getElementById("videoData");
if (!input || !(input instanceof HTMLInputElement) && !(input instanceof HTMLTextAreaElement) || !input.value) return null;
return JSON.parse(input.value);
} catch (err) {
Logger.error("Failed to parse DataCamp videoData input", err instanceof Error ? err.message : String(err));
return null;
}
}
async getVideoData(videoId) {
if (!this.getVideoDataByPlayer(videoId)) return;
const meta = this.getVideoDataFromInput();
const videoUrl = meta?.video_url ?? meta?.plain_video_mp4_link ?? meta?.plain_video_hls_link ?? meta?.video_mp4_link ?? meta?.video_hls_link;
if (!videoUrl) return;
return {
url: videoId,
video_url: videoUrl,
translationHelp: [{
target: "video_file_url",
targetUrl: videoUrl
}]
};
}
async getVideoId(url) {
return url.href;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/deeplearningai.js
var DeeplearningAIHelper = class extends BaseHelper {
async getVideoData(_videoId) {
if (!this.video) return;
const sourceUrl = this.video.querySelector("source[type=\"application/x-mpegurl\"]")?.src.replace(/\.m3u8/, "_360p.mp4");
if (!sourceUrl) return;
return { url: sourceUrl };
}
async getVideoId(url) {
return /courses\/(([^/]+)\/lesson\/([^/]+)\/([^/]+))/.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/douyin.js
var DouyinHelper = class DouyinHelper extends BaseHelper {
static getPlayer() {
if (typeof player === "undefined") return;
return player;
}
async getVideoData(_videoId) {
const xgPlayer = DouyinHelper.getPlayer();
if (!xgPlayer) return;
const { config: { url: sources, duration, lang, isLive: isStream } } = xgPlayer;
if (!sources) return;
const source = sources.find((s) => s.src.includes("www.douyin.com/aweme/v1/play/"));
if (!source) return;
return {
url: proxyMedia(source.src),
duration,
isStream,
...availableLangs.includes(lang) ? { detectedLanguage: lang } : {}
};
}
async getVideoId(url) {
const pathId = /video\/([\d]+)/.exec(url.pathname)?.[0];
if (pathId) return pathId;
return DouyinHelper.getPlayer()?.config.vid;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/dzen.js
var DzenHelper = class extends BaseHelper {
async getVideoId(url) {
return /video\/watch\/([^/]+)/.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/egghead.js
var EggheadHelper = class extends BaseHelper {
async getVideoId(url) {
return url.pathname.slice(1);
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/epicgames.js
var EpicGamesHelper = class extends BaseHelper {
API_ORIGIN = "https://dev.epicgames.com/community/api/learning";
async getPostInfo(videoId) {
try {
return await (await this.fetch(`${this.API_ORIGIN}/post.json?hash_id=${videoId}`)).json();
} catch (err) {
Logger.error(`Failed to get epicgames post info by videoId: ${videoId}.`, err.message);
return false;
}
}
getVideoBlock() {
const videoUrlRe = /videoUrl\s?=\s"([^"]+)"?/;
const script = Array.from(document.body.querySelectorAll("script")).find((s) => videoUrlRe.exec(s.innerHTML));
if (!script) return;
const content = script.innerHTML.trim();
const playlistUrl = videoUrlRe.exec(content)?.[1]?.replace("qsep://", "https://");
if (!playlistUrl) return;
let subtitlesString = /sources\s?=\s(\[([^\]]+)\])?/.exec(content)?.[1];
if (!subtitlesString) return {
playlistUrl,
subtitles: []
};
try {
subtitlesString = `${subtitlesString.replace(/src:(\s)+?(videoUrl)/g, "src:\"removed\"").substring(0, subtitlesString.lastIndexOf("},"))}]`.split("\n").map((line) => line.replace(/([^\s]+):\s?(?!.*\1)/, "\"$1\":")).join("\n");
return {
playlistUrl,
subtitles: JSON.parse(subtitlesString).filter((sub) => sub.type === "captions")
};
} catch {
return {
playlistUrl,
subtitles: []
};
}
}
async getVideoData(videoId) {
const courseId = videoId.split(":")?.[1];
const postInfo = await this.getPostInfo(courseId);
if (!postInfo) return;
const videoBlock = this.getVideoBlock();
if (!videoBlock) return;
const { playlistUrl, subtitles: videoSubtitles } = videoBlock;
const { title, description } = postInfo;
return {
url: playlistUrl,
title,
description,
subtitles: videoSubtitles.map((caption) => ({
language: normalizeLang$1(caption.srclang),
source: "epicgames",
format: "vtt",
url: caption.src
}))
};
}
async getVideoId(_url) {
return new Promise((resolve) => {
const origin = "https://dev.epicgames.com";
const reqId = btoa(window.location.href);
window.addEventListener("message", (e) => {
if (e.origin !== origin) return;
if (!(typeof e.data === "string" && e.data.startsWith("getVideoId:"))) return;
return resolve(e.data.replace("getVideoId:", ""));
});
window.top?.postMessage(`getVideoId:${reqId}`, origin);
});
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/eporner.js
var EpornerHelper = class extends BaseHelper {
async getVideoId(url) {
return /video-([^/]+)\/([^/]+)/.exec(url.pathname)?.[0];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/facebook.js
var FacebookHelper = class extends BaseHelper {
async getVideoId(url) {
return url.pathname.slice(1);
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/googledrive.js
var GoogleDriveHelper = class extends BaseHelper {
getPlayerData() {
return document.querySelector("#movie_player")?.getVideoData?.() ?? void 0;
}
async getVideoId(_url) {
return this.getPlayerData()?.video_id;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/ign.js
var IgnHelper = class extends BaseHelper {
getVideoDataBySource(videoId) {
const url = document.querySelector(".icms.video > source[type=\"video/mp4\"][data-quality=\"360\"]")?.src;
if (!url) return this.returnBaseData(videoId);
return { url: proxyMedia(url) };
}
getVideoDataByNext(videoId) {
try {
const nextContent = document.getElementById("__NEXT_DATA__")?.textContent;
if (!nextContent) throw new VideoDataError("Not found __NEXT_DATA__ content");
const { props: { pageProps: { page: { description, title, video: { videoMetadata: { duration }, assets } } } } } = JSON.parse(nextContent);
const videoUrl = assets.find((asset) => asset.height === 360 && asset.url.includes(".mp4"))?.url;
if (!videoUrl) throw new VideoDataError("Not found video URL in assets");
return {
url: proxyMedia(videoUrl),
duration,
title,
description
};
} catch (err) {
Logger.warn(`Failed to get ign video data by video ID: ${videoId}, because ${err.message}. Using clear link instead...`);
return this.returnBaseData(videoId);
}
}
async getVideoData(videoId) {
if (document.getElementById("__NEXT_DATA__")) return this.getVideoDataByNext(videoId);
return this.getVideoDataBySource(videoId);
}
async getVideoId(url) {
return /([^/]+)\/([\d]+)\/video\/([^/]+)/.exec(url.pathname)?.[0] ?? /\/videos\/([^/]+)/.exec(url.pathname)?.[0];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/imdb.js
var IMDbHelper = class extends BaseHelper {
async getVideoId(url) {
return /video\/([^/]+)/.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/incestflix.js
var IncestflixHelper = class extends BaseHelper {
async getVideoData(videoId) {
try {
const sourceEl = document.querySelector("#incflix-stream source:first-of-type");
if (!sourceEl) throw new VideoHelperError("Failed to find source element");
const srcLink = sourceEl.getAttribute("src");
if (!srcLink) throw new VideoHelperError("Failed to find source link");
const source = new URL(srcLink.startsWith("//") ? `https:${srcLink}` : srcLink);
source.searchParams.append("media-proxy", "video.mp4");
return { url: proxyMedia(source) };
} catch (err) {
Logger.error(`Failed to get Incestflix data by videoId: ${videoId}`, err.message);
return;
}
}
async getVideoId(url) {
return /\/watch\/([^/]+)/.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/jove.js
var JoveHelper = class extends BaseHelper {
async getVideoId(url) {
const groups = /^\/(?:[a-z]{2}\/)?v\/(?<id>\d+)\/(?<slug>[^/?#]+)\/?$/i.exec(url.pathname)?.groups;
if (!groups) return;
const { id, slug } = groups;
return `v/${id}/${slug}`;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/kick.js
var KickHelper = class extends BaseHelper {
API_ORIGIN = "https://kick.com/api";
async getClipInfo(clipId) {
try {
const { clip_url: url, duration, title } = (await (await this.fetch(`${this.API_ORIGIN}/v2/clips/${clipId}`)).json()).clip;
return {
url,
duration,
title
};
} catch (err) {
Logger.error(`Failed to get kick clip info by clipId: ${clipId}.`, err.message);
return;
}
}
async getVideoInfo(videoId) {
try {
const { source: url, livestream } = await (await this.fetch(`${this.API_ORIGIN}/v1/video/${videoId}`)).json();
const { session_title: title, duration } = livestream;
return {
url,
duration: Math.round(duration / 1e3),
title
};
} catch (err) {
Logger.error(`Failed to get kick video info by videoId: ${videoId}.`, err.message);
return;
}
}
async getVideoData(videoId) {
return videoId.startsWith("videos") ? await this.getVideoInfo(videoId.replace("videos/", "")) : await this.getClipInfo(videoId.replace("clips/", ""));
}
async getVideoId(url) {
return /([^/]+)\/((videos|clips)\/([^/]+))/.exec(url.pathname)?.[2];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/kickstarter.js
var KickstarterHelper = class extends BaseHelper {
async getVideoData(videoId) {
try {
const videoEl = document.querySelector(".ksr-video-player > video");
const url = videoEl?.querySelector("source[type^='video/mp4']")?.src;
if (!url) throw new VideoHelperError("Failed to find video URL");
const subtitles = videoEl?.querySelectorAll("track") ?? [];
return {
url,
subtitles: Array.from(subtitles).reduce((result, sub) => {
const lang = sub.getAttribute("srclang");
const url = sub.getAttribute("src");
if (!lang || !url) return result;
result.push({
language: normalizeLang$1(lang),
url,
format: "vtt",
source: "kickstarter"
});
return result;
}, [])
};
} catch (err) {
Logger.error(`Failed to get Kickstarter data by videoId: ${videoId}`, err.message);
return;
}
}
async getVideoId(url) {
return url.pathname.slice(1);
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/kodik.js
var KodikHelper = class extends BaseHelper {
API_ORIGIN = window.location.origin;
getSecureData(videoPath) {
try {
const [videoType, videoId, hash] = videoPath.split("/").filter((a) => a);
const allScripts = Array.from(document.getElementsByTagName("script"));
const secureScript = allScripts.filter((s) => s.innerHTML.includes(`videoId = "${videoId}"`) || s.innerHTML.includes(`serialId = Number(${videoId})`));
if (!secureScript.length) throw new VideoHelperError("Failed to find secure script");
const secureScriptContent = secureScript[0]?.textContent?.trim();
if (!secureScriptContent) throw new VideoHelperError("Secure script content is empty");
const secureContent = /'{[^']+}'/.exec(secureScriptContent)?.[0];
if (!secureContent) throw new VideoHelperError("Secure json wasn't found in secure script");
const secureJSON = JSON.parse(secureContent.replaceAll("'", ""));
if (videoType !== "serial") return {
videoType,
videoId,
hash,
...secureJSON
};
const videoInfoContent = allScripts.find((s) => s.innerHTML.includes(`var videoInfo = {}`))?.textContent?.trim();
if (!videoInfoContent) throw new VideoHelperError("Failed to find videoInfo content");
const realVideoType = /videoInfo\.type\s+?=\s+?'([^']+)'/.exec(videoInfoContent)?.[1];
const realVideoId = /videoInfo\.id\s+?=\s+?'([^']+)'/.exec(videoInfoContent)?.[1];
const realHash = /videoInfo\.hash\s+?=\s+?'([^']+)'/.exec(videoInfoContent)?.[1];
if (!realVideoType || !realVideoId || !realHash) throw new VideoHelperError("Failed to parse videoInfo content");
return {
videoType: realVideoType,
videoId: realVideoId,
hash: realHash,
...secureJSON
};
} catch (err) {
Logger.error(`Failed to get kodik secure data by videoPath: ${videoPath}.`, err.message);
return false;
}
}
async getFtor(secureData) {
const { videoType, videoId: id, hash, d, d_sign, pd, pd_sign, ref, ref_sign } = secureData;
try {
return await (await this.fetch(`${this.API_ORIGIN}/ftor`, {
method: "POST",
headers: {
"User-Agent": config_default$1.userAgent,
Origin: this.API_ORIGIN,
Referer: `${this.API_ORIGIN}/${videoType}/${id}/${hash}/360p`
},
body: new URLSearchParams({
d,
d_sign,
pd,
pd_sign,
ref: decodeURIComponent(ref),
ref_sign,
bad_user: "false",
cdn_is_working: "true",
info: "{}",
type: videoType,
hash,
id
})
})).json();
} catch (err) {
Logger.error(`Failed to get kodik video data (type: ${videoType}, id: ${id}, hash: ${hash})`, err.message);
return false;
}
}
decryptUrl(encryptedUrl) {
return `${atob(encryptedUrl.replace(/[a-zA-Z]/g, (e) => {
const charCode = e.charCodeAt(0) + 18;
const pos = e <= "Z" ? 90 : 122;
return String.fromCharCode(pos >= charCode ? charCode : charCode - 26);
}))}`;
}
async getVideoData(videoId) {
const secureData = this.getSecureData(videoId);
if (!secureData) return;
const videoData = await this.getFtor(secureData);
if (!videoData) return;
const videoLink = Object.entries(videoData.links[videoData.default.toString()]).find(([, data]) => data.type === "application/x-mpegURL")?.[1];
if (!videoLink) return;
return { url: videoLink.src.startsWith("//") ? `${videoLink.src}` : this.decryptUrl(videoLink.src) };
}
async getVideoId(url) {
return /\/(uv|video|seria|episode|season|serial)\/([^/]+)\/([^/]+)\/([\d]+)p/.exec(url.pathname)?.[0];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/linkedin.js
var LinkedinHelper = class extends VideoJSHelper {
SUBTITLE_SOURCE = "linkedin";
async getVideoData(videoId) {
const data = this.getVideoDataByPlayer(videoId);
if (!data) return;
const { url, duration, subtitles } = data;
return {
url: proxyMedia(new URL(url)),
duration,
subtitles
};
}
async getVideoId(url) {
return /\/learning\/(([^/]+)\/([^/]+))/.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/shared/dist/types/helpers/bannedvideo.js
var TypeName;
(function(TypeName) {
TypeName["Channel"] = "Channel";
TypeName["Video"] = "Video";
})(TypeName || (TypeName = {}));
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/loom.js
var LoomHelper = class extends BaseHelper {
getClientVersion() {
if (typeof SENTRY_RELEASE === "undefined") return;
return SENTRY_RELEASE.id;
}
async getVideoData(videoId) {
try {
const clientVer = this.getClientVersion();
if (!clientVer) throw new VideoHelperError("Failed to get client version");
const res = await this.fetch("https://www.loom.com/graphql", {
headers: {
"User-Agent": config_default$1.userAgent,
"content-type": "application/json",
"x-loom-request-source": `loom_web_${clientVer}`,
"apollographql-client-name": "web",
"apollographql-client-version": clientVer,
"Alt-Used": "www.loom.com"
},
body: `{"operationName":"FetchCaptions","variables":{"videoId":"${videoId}"},"query":"query FetchCaptions($videoId: ID!, $password: String) {\\n fetchVideoTranscript(videoId: $videoId, password: $password) {\\n ... on VideoTranscriptDetails {\\n id\\n captions_source_url\\n language\\n __typename\\n }\\n ... on GenericError {\\n message\\n __typename\\n }\\n __typename\\n }\\n}"}`,
method: "POST"
});
if (res.status !== 200) throw new VideoHelperError("Failed to get data from graphql");
const data = (await res.json()).data.fetchVideoTranscript;
if (data.__typename === "GenericError") throw new VideoHelperError(data.message);
return {
url: this.service?.url + videoId,
subtitles: [{
format: "vtt",
language: normalizeLang$1(data.language),
source: "loom",
url: data.captions_source_url
}]
};
} catch (err) {
Logger.error(`Failed to get Loom video data, because: ${err.message}`);
return this.returnBaseData(videoId);
}
}
async getVideoId(url) {
return /(embed|share)\/([^/]+)?/.exec(url.pathname)?.[2];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/mailru.js
var MailRuHelper = class extends BaseHelper {
API_ORIGIN = "https://my.mail.ru";
async getVideoMeta(videoId) {
try {
return await (await this.fetch(`${this.API_ORIGIN}/+/video/meta/${videoId}?xemail=&ajax_call=1&func_name=&mna=&mnb=&ext=1&_=${Date.now()}`)).json();
} catch (err) {
Logger.error("Failed to get mail.ru video data", err.message);
return;
}
}
async getVideoId(url) {
const pathname = url.pathname;
if (/\/(v|mail|bk|inbox)\//.exec(pathname)) return pathname.slice(1);
const videoId = /video\/embed\/([^/]+)/.exec(pathname)?.[1];
if (!videoId) return;
const videoData = await this.getVideoMeta(videoId);
if (!videoData) return;
return videoData.meta.url.replace("//my.mail.ru/", "");
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/mediafile.js
var MediafileHelper = class extends BaseHelper {
DEFAULT_SITE_ORIGIN = "https://mediafile.cc";
SITE_ORIGIN = this.service?.url?.slice(0, -1) ?? this.DEFAULT_SITE_ORIGIN;
getVideoSrc() {
const video = this.video instanceof HTMLVideoElement ? this.video : document.querySelector("video");
return video?.src || video?.currentSrc || video?.querySelector("source[src]")?.src || void 0;
}
async getVideoData(videoId) {
const videoSource = this.getVideoSrc();
if (!videoSource) return;
return {
url: `${this.SITE_ORIGIN}/${videoId}`,
video_url: videoSource,
translationHelp: [{
target: "video_file_url",
targetUrl: videoSource
}]
};
}
async getVideoId(url) {
return url.pathname.replace(/^\/+/, "") || void 0;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/netacad.js
var NetacadHelper = class extends VideoJSHelper {
SUBTITLE_SOURCE = "netacad";
async getVideoData(videoId) {
const data = this.getVideoDataByPlayer(videoId);
if (!data) return;
const { url, duration, subtitles } = data;
return {
url: proxyMedia(new URL(url)),
duration,
subtitles
};
}
async getVideoId(url) {
return url.pathname + url.search;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/newgrounds.js
var NewgroundsHelper = class extends BaseHelper {
async getVideoId(url) {
return /([^/]+)\/(view)\/([^/]+)/.exec(url.pathname)?.[0];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/niconico.js
var NicoNicoHelper = class extends BaseHelper {
async getVideoId(url) {
if (url.hostname === "nico.ms") return url.pathname.replace(/^\//, "").split("/")[0] || void 0;
return /\/watch\/([^/?#]+)/.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/nine_gag.js
var NineGAGHelper = class extends BaseHelper {
async getVideoData(videoId) {
const data = this.returnBaseData(videoId);
if (!data) return data;
try {
if (!this.video) throw new Error("Video element not found");
const videoUrl = this.video.querySelector("source[type^=\"video/mp4\"], source[type^=\"video/webm\"]")?.src;
if (!videoUrl || !/^https?:\/\//.test(videoUrl)) throw new Error("Video source not found");
return {
...data,
translationHelp: [{
target: "video_file_url",
targetUrl: videoUrl
}]
};
} catch {
return data;
}
}
async getVideoId(url) {
return /gag\/([^/]+)/.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/odysee.js
var OdyseeHelper = class extends BaseHelper {
API_ORIGIN = "https://odysee.com";
async getVideoData(videoId) {
return { url: `${this.API_ORIGIN}/${videoId}`.replace(/:[a-zA-Z0-9]+$/, "") };
}
async getVideoId(url) {
return url.pathname.slice(1);
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/okru.js
var OKRuHelper = class extends BaseHelper {
async getVideoId(url) {
return /\/video\/(\d+)/.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/olympicsreplay.js
var OlympicsReplayHelper = class extends BaseHelper {
async getVideoId(url) {
return /\/([a-z]{2}\/(?:[a-z0-9-]+\/)?(?:replay|videos?|original-series\/episode)\/[\w-]+)\/?$/i.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/oraclelearn.js
var OracleLearnHelper = class extends VideoJSHelper {
SUBTITLE_SOURCE = "oraclelearn";
async getVideoData(videoId) {
const data = this.getVideoDataByPlayer(videoId);
if (!data) return;
const { url, duration, subtitles } = data;
const baseData = this.returnBaseData(videoId);
const videoUrl = proxyMedia(new URL(url));
if (!baseData) return {
url: videoUrl,
duration,
subtitles
};
return {
url: baseData.url,
duration,
subtitles,
translationHelp: [{
target: "video_file_url",
targetUrl: videoUrl
}]
};
}
async getVideoId(url) {
return /\/ou\/course\/(([^/]+)\/(\d+)\/(\d+))/.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/patreon.js
var PatreonHelper = class extends BaseHelper {
API_ORIGIN = "https://www.patreon.com/api";
async getPosts(postId) {
try {
return await (await this.fetch(`${this.API_ORIGIN}/posts/${postId}?json-api-use-default-includes=false`)).json();
} catch (err) {
Logger.error(`Failed to get patreon posts by postId: ${postId}.`, err.message);
return false;
}
}
async getVideoData(postId) {
const postData = await this.getPosts(postId);
if (!postData) return;
const postFileUrl = postData.data.attributes.post_file.url;
if (!postFileUrl) return;
return { url: postFileUrl };
}
async getVideoId(url) {
const fullPostId = /posts\/([^/]+)/.exec(url.pathname)?.[1];
if (!fullPostId) return;
return fullPostId.replace(/[^\d.]/g, "");
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/peertube.js
var PeertubeHelper = class extends BaseHelper {
async getVideoId(url) {
const normalizedPathname = url.pathname.replace(/\/+$/, "");
const watchVideoId = /\/videos\/watch\/([^/]+)/.exec(normalizedPathname)?.[1];
if (watchVideoId) return `/videos/watch/${watchVideoId}`;
const shortVideoId = /\/w\/([^/]+)/.exec(normalizedPathname)?.[1];
if (shortVideoId) return `/videos/watch/${shortVideoId}`;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/picarto.js
var PicartoHelper = class extends BaseHelper {
async getVideoId(url) {
return /\/((?:videopopout|[^/]+(?:\/profile)?\/videos)\/[^/?#&/]+)\/?$/.exec(url.pathname)?.[1] ?? /^\/([^/#?]+)\/?$/.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/pornhub.js
var PornhubHelper = class extends BaseHelper {
async getVideoId(url) {
return url.searchParams.get("viewkey") ?? /embed\/([^/]+)/.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/porntn.js
var PornTNHelper = class extends BaseHelper {
async getVideoData(videoId) {
try {
if (typeof flashvars === "undefined") return;
const { rnd, video_url: source, video_title: title } = flashvars;
if (!source || !rnd) throw new VideoHelperError("Failed to find video source or rnd");
const getFileUrl = new URL(source);
getFileUrl.searchParams.append("rnd", rnd);
Logger.log("PornTN get_file link", getFileUrl.href);
const cdnResponse = await this.fetch(getFileUrl.href, { method: "head" });
const cdnUrl = new URL(cdnResponse.url);
Logger.log("PornTN cdn link", cdnUrl.href);
return {
url: proxyMedia(cdnUrl),
title
};
} catch (err) {
Logger.error(`Failed to get PornTN data by videoId: ${videoId}`, err.message);
return;
}
}
async getVideoId(url) {
return /\/videos\/(([^/]+)\/([^/]+))/.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/preservetube.js
var PreserveTubeHelper = class extends BaseHelper {
async getVideoData(videoId) {
try {
if (!videoId) throw new VideoHelperError("Failed to find PreserveTube video ID");
return { url: `https://s3.archive.party/preservetube/${videoId}.mp4` };
} catch (err) {
Logger.error(`Failed to get PreserveTube data by videoId: ${videoId}`, err.message);
return;
}
}
async getVideoId(url) {
return url.searchParams.get("v") ?? void 0;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/reddit.js
var RedditHelper = class extends BaseHelper {
API_ORIGIN = "https://www.reddit.com";
async getDashAudioUrl(dashUrl) {
const res = await fetch(dashUrl);
if (!res.ok) return void 0;
const xml = await res.text();
const doc = new DOMParser().parseFromString(xml, "application/xml");
const audioBaseUrl = doc.querySelector("AdaptationSet[contentType=\"audio\"] BaseURL")?.textContent ?? doc.querySelector("Representation[id=\"AUDIO-1\"] BaseURL")?.textContent;
if (!audioBaseUrl) return void 0;
const base = new URL(dashUrl);
return new URL(audioBaseUrl, base).href;
}
async getVideoData(videoId) {
try {
const res = await fetch(`${this.API_ORIGIN}/r/${videoId}.json`, { headers: { Accept: "application/json" } });
if (!res.ok) throw new VideoHelperError(`Reddit API error: ${res.status}`);
const post = (await res.json())?.[0]?.data?.children?.[0]?.data;
const redditVideo = post?.secure_media?.reddit_video ?? post?.media?.reddit_video ?? post?.crosspost_parent_list?.[0]?.secure_media?.reddit_video ?? post?.crosspost_parent_list?.[0]?.media?.reddit_video;
if (!redditVideo) throw new VideoHelperError("No reddit_video found in post");
const dashUrl = redditVideo.dash_url?.replaceAll("&", "&");
if (!dashUrl) throw new VideoHelperError("No dash_url in reddit_video");
const audioUrl = await this.getDashAudioUrl(dashUrl);
if (!audioUrl) throw new VideoHelperError("Failed to extract audio URL from DASH MPD");
return { url: audioUrl };
} catch (err) {
Logger.error(`Failed to get reddit video data by video ID: ${videoId}`, err.message);
return;
}
}
async getVideoId(url) {
return /\/r\/(([^/]+)\/([^/]+)\/([^/]+)\/([^/]+))/.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/rtnews.js
var RtNewsHelper = class extends BaseHelper {
async getVideoData(videoId) {
const videoEl = document.querySelector(".jw-video, .media__video_noscript");
if (!videoEl) return;
let videoSrc = videoEl.getAttribute("src");
if (!videoSrc) return;
if (videoSrc.endsWith(".MP4")) videoSrc = proxyMedia(videoSrc);
return {
videoId,
url: videoSrc
};
}
async getVideoId(url) {
return url.pathname.slice(1);
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/rule34video.js
var Rule34VideoHelper = class extends BaseHelper {
async getVideoId(url) {
const parts = /\/videos?\/(\d+)(?:\/(.+))?\/?$/.exec(url.pathname);
if (!parts) return;
const [, id, tail] = parts;
return tail ? `${id}/${tail.replace(/\/+$/, "")}/` : id;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/rumble.js
var RumbleHelper = class extends BaseHelper {
async getVideoId(url) {
return url.pathname.slice(1);
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/rutube.js
var RutubeHelper = class extends BaseHelper {
async getVideoId(url) {
return /(?:video|embed)\/([^/]+)/.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/sap.js
var SapHelper = class extends BaseHelper {
API_ORIGIN = "https://learning.sap.com/";
async requestKaltura(kalturaDomain, partnerId, entryId) {
const clientTag = "html5:v3.17.22";
const apiVersion = "3.3.0";
try {
return await (await this.fetch(`https://${kalturaDomain}/api_v3/service/multirequest`, {
method: "POST",
body: JSON.stringify({
"1": {
service: "session",
action: "startWidgetSession",
widgetId: `_${partnerId}`
},
"2": {
service: "baseEntry",
action: "list",
ks: "{1:result:ks}",
filter: { redirectFromEntryId: entryId },
responseProfile: {
type: 1,
fields: "id,referenceId,name,description,dataUrl,duration,flavorParamsIds,type,dvrStatus,externalSourceType,createdAt,updatedAt,endDate,plays,views,downloadUrl,creatorId"
}
},
"3": {
service: "baseEntry",
action: "getPlaybackContext",
entryId: "{2:result:objects:0:id}",
ks: "{1:result:ks}",
contextDataParams: {
objectType: "KalturaContextDataParams",
flavorTags: "all"
}
},
apiVersion,
format: 1,
ks: "",
clientTag,
partnerId
}),
headers: { "Content-Type": "application/json" }
})).json();
} catch (err) {
Logger.error("Failed to request kaltura data", err.message);
return;
}
}
async getKalturaData(videoId) {
try {
const scriptEl = document.querySelector("script[data-nscript=\"beforeInteractive\"]");
if (!scriptEl) throw new VideoHelperError("Failed to find script element");
const sapData = /https:\/\/([^"]+)\/p\/([^"]+)\/embedPlaykitJs\/uiconf_id\/([^"]+)/.exec(scriptEl?.src);
if (!sapData) throw new VideoHelperError(`Failed to get sap data for videoId: ${videoId}`);
const [, kalturaDomain, partnerId] = sapData;
let entryId = document.querySelector("#shadow")?.firstChild?.getAttribute("id");
if (!entryId) {
const nextDataEl = document.querySelector("#__NEXT_DATA__");
if (!nextDataEl) throw new VideoHelperError("Failed to find next data element");
entryId = /"sourceId":\s?"([^"]+)"/.exec(nextDataEl.innerText)?.[1];
}
if (!kalturaDomain || Number.isNaN(+partnerId) || !entryId) throw new VideoHelperError(`One of the necessary parameters for getting a link to a sap video in wasn't found for ${videoId}. Params: kalturaDomain = ${kalturaDomain}, partnerId = ${partnerId}, entryId = ${entryId}`);
return await this.requestKaltura(kalturaDomain, partnerId, entryId);
} catch (err) {
Logger.error("Failed to get kaltura data", err.message);
return;
}
}
async getVideoData(videoId) {
const kalturaData = await this.getKalturaData(videoId);
if (!kalturaData) return;
const [, baseEntryList, playbackContext] = kalturaData;
const { duration } = baseEntryList.objects[0];
const videoUrl = playbackContext.sources.find((source) => source.format === "url" && source.protocols === "http,https" && source.url.includes(".mp4"))?.url;
if (!videoUrl) return;
return {
url: videoUrl,
subtitles: playbackContext.playbackCaptions.map((caption) => {
return {
language: normalizeLang$1(caption.languageCode),
source: "sap",
format: "vtt",
url: caption.webVttUrl,
isAutoGenerated: caption.label.includes("auto-generated")
};
}),
duration
};
}
async getVideoId(url) {
return /((courses|learning-journeys)\/([^/]+)(\/[^/]+)?)/.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/skilljar.js
var SkilljarHelper = class extends BaseHelper {
async getVideoId(url) {
return url.pathname.slice(1);
}
async getVideoData(videoId) {
try {
if (typeof jwplayer === "undefined") throw new Error("JW Player not found on page");
const player = jwplayer();
if (!player || typeof player.getDuration !== "function") throw new Error("JW Player instance not ready");
const item = player.getPlaylistItem ? player.getPlaylistItem() : null;
if (!item) throw new Error("No playlist item found");
const duration = player.getDuration ? player.getDuration() : item.duration ?? 0;
const validSources = (item.allSources ?? []).filter((s) => typeof s.height === "number" && s.height > 0);
if (validSources.length === 0) throw new Error("No sources with height > 0 found");
validSources.sort((a, b) => (a.height ?? 0) - (b.height ?? 0));
return {
url: videoId,
duration,
translationHelp: [{
target: "video_file_url",
targetUrl: validSources[0].file
}],
subtitles: []
};
} catch (err) {
console.error("[VOT] SkilljarHelper error:", err instanceof Error ? err.message : String(err));
return;
}
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/spankbang.js
var SpankBangHelper = class extends BaseHelper {
async getVideoId(url) {
return /\/([\da-z]+\/(?:video|play|embed)(?:\/[^/]+)?)\/?$/i.exec(url.pathname)?.[1] ?? /\/([\da-z]+-[\da-z]+\/playlist\/[^/]+)\/?$/i.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/telegram.js
var TelegramHelper = class TelegramHelper extends BaseHelper {
static getMediaViewer() {
if (typeof appMediaViewer === "undefined") return;
return appMediaViewer;
}
async getVideoId(_url) {
const mediaViewer = TelegramHelper.getMediaViewer();
if (!mediaViewer) return;
if (mediaViewer.live) return;
const message = mediaViewer.target.message;
if (message.peer_id._ !== "peerChannel") return;
const media = message.media;
if (media._ !== "messageMediaDocument") return;
if (media.document.type !== "video") return;
const postId = message.mid & 4294967295;
return `${await mediaViewer.managers.appPeersManager.getPeerUsername(message.peerId)}/${postId}`;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/thisvid.js
var ThisVidHelper = class extends BaseHelper {
async getVideoId(url) {
return /(videos|embed)\/[^/]+/.exec(url.pathname)?.[0];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/tiktok.js
var TikTokHelper = class extends BaseHelper {
async getVideoId(url) {
return /([^/]+)\/video\/([^/]+)/.exec(url.pathname)?.[0];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/trovo.js
var TrovoHelper = class extends BaseHelper {
async getVideoId(url) {
const vid = url.searchParams.get("vid");
const path = /([^/]+)\/([\d]+)/.exec(url.pathname)?.[0];
if (!vid || !path) return;
return `${path}?vid=${vid}`;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/twitch.js
var TwitchHelper = class extends BaseHelper {
API_ORIGIN = "https://clips.twitch.tv";
async getClipLink(pathname, clipId) {
const schema = document.querySelector("script[type='application/ld+json']");
const clearPathname = pathname.slice(1);
if (schema) {
const channelLink = JSON.parse(schema.innerText)["@graph"].find((obj) => obj["@type"] === "VideoObject")?.creator.url;
if (!channelLink) throw new VideoHelperError("Failed to find channel link");
return `${channelLink.replace("https://www.twitch.tv/", "")}/clip/${clearPathname}`;
}
const isEmbed = clearPathname === "embed";
const channelLink = document.querySelector(isEmbed ? ".tw-link[data-test-selector='stream-info-card-component__stream-avatar-link']" : ".clips-player a:not([class])");
if (!channelLink) return;
return `${channelLink.href.replace("https://www.twitch.tv/", "")}/clip/${isEmbed ? clipId : clearPathname}`;
}
async getVideoData(videoId) {
const title = document.querySelector("[data-a-target=\"stream-title\"], [data-test-selector=\"stream-info-card-component__subtitle\"]")?.innerText;
const isStream = !!document.querySelector("[data-a-target=\"animated-channel-viewers-count\"], .channel-status-info--live, .top-bar--pointer-enabled .tw-channel-status-text-indicator");
return {
url: this.service?.url + videoId,
isStream,
title
};
}
async getVideoId(url) {
const pathname = url.pathname;
if (/^m\.twitch\.tv$/.test(pathname)) return /videos\/([^/]+)/.exec(url.href)?.[0] ?? pathname.slice(1);
else if (/^player\.twitch\.tv$/.test(url.hostname)) return `videos/${url.searchParams.get("video")}`;
const clipPath = /([^/]+)\/(?:clip)\/([^/]+)/.exec(pathname);
if (clipPath) return clipPath[0];
if (/^clips\.twitch\.tv$/.test(url.hostname)) return await this.getClipLink(pathname, url.searchParams.get("clip"));
const videoPath = /(?:videos)\/([^/]+)/.exec(pathname);
if (videoPath) return videoPath[0];
const isUserOfflinePage = document.querySelector(".home-offline-hero .tw-link");
if (isUserOfflinePage?.href) {
const pageUrl = new URL(isUserOfflinePage.href);
return /(?:videos)\/([^/]+)/.exec(pageUrl.pathname)?.[0];
}
return document.querySelector(".persistent-player") ? pathname : void 0;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/twitter.js
var TwitterHelper = class extends BaseHelper {
async getVideoId(url) {
const videoId = /status\/([^/]+)/.exec(url.pathname)?.[1];
if (videoId) return videoId;
const newLink = (this.video?.closest("[data-testid=\"tweet\"]"))?.querySelector("a[role=\"link\"][aria-label]")?.href;
return newLink ? /status\/([^/]+)/.exec(newLink)?.[1] : void 0;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/udemy.js
function isObject(value) {
return typeof value === "object" && value !== null;
}
function isUrlCandidate(value) {
return typeof value === "object" && value !== null;
}
function getUrlCandidates(data) {
if (Array.isArray(data)) return data.filter(isUrlCandidate);
if (typeof data !== "object" || data === null) return [];
const source = data;
return (Array.isArray(source.Video) ? source.Video : Array.isArray(source.video) ? source.video : []).filter(isUrlCandidate);
}
function getOutputCandidates(data) {
if (!isObject(data)) return [];
const result = [];
for (const [fallbackLabel, value] of Object.entries(data)) {
if (!isObject(value) || typeof value.url !== "string") continue;
result.push({
src: value.url,
type: typeof value.type === "string" ? value.type : void 0,
label: typeof value.height === "number" || typeof value.height === "string" ? value.height : fallbackLabel
});
}
return result;
}
function getQualityValue(value) {
if (typeof value === "number" && Number.isFinite(value)) return value;
const match = String(value ?? "").match(/(\d{3,4})/);
return Number(match?.[1] ?? 0);
}
function getCandidateUrl(candidate) {
if (typeof candidate.file === "string") return candidate.file;
if (typeof candidate.src === "string") return candidate.src;
}
function isM3U8(type, url) {
return type.includes("mpegurl") || /\.m3u8(?:$|[?#])/i.test(url);
}
function isDash(type, url) {
return type.includes("dash") || /\.mpd(?:$|[?#])/i.test(url);
}
var UdemyHelper = class extends BaseHelper {
API_ORIGIN = `${window.location.origin}/api-2.0`;
getModuleData() {
const moduleData = (document.querySelector(".ud-app-loader[data-module-id='course-taking']") ?? document.querySelector("[data-module-id='course-taking']"))?.dataset?.moduleArgs;
if (!moduleData) return;
try {
return JSON.parse(moduleData);
} catch {
return;
}
}
getLectureId(videoId) {
const lectureIdRe = /(?:\/learn\/(?:v4\/t\/)?lecture\/|#\/?lecture\/)(\d+)/i;
const lectureViewIdRe = /\/lecture\/view\/\?/i;
const href = window.location.href;
const lectureViewSearchParams = lectureViewIdRe.test(href) ? new URL(href).searchParams : void 0;
const lectureViewId = lectureViewSearchParams ? (() => {
for (const [key, value] of lectureViewSearchParams) {
const normalizedKey = key.toLowerCase();
if (normalizedKey === "lectureid" || normalizedKey === "lecture_id") return value;
}
})() : void 0;
return lectureIdRe.exec(href)?.[1] ?? lectureViewId ?? (videoId ? lectureIdRe.exec(`/${videoId}`)?.[1] : void 0);
}
getCourseId(moduleData) {
const moduleDataWithExtra = moduleData;
const moduleCourseId = this.normalizeId(moduleDataWithExtra?.courseId ?? moduleDataWithExtra?.course_id ?? moduleDataWithExtra?.course?.id);
if (moduleCourseId) return moduleCourseId;
const attrCourseId = this.normalizeId(document.querySelector("[data-course-id]")?.getAttribute("data-course-id"));
if (attrCourseId) return attrCourseId;
const pageHtml = document.documentElement?.innerHTML ?? "";
return /data-course-id=["'](\d+)/i.exec(pageHtml)?.[1] ?? /"courseId"\s*:\s*(\d+)/i.exec(pageHtml)?.[1] ?? /"courseId"\s*:\s*(\d+)/i.exec(pageHtml)?.[1];
}
normalizeId(value) {
if (typeof value === "number" && Number.isFinite(value)) return String(value);
if (typeof value === "string") return /^\d+$/.test(value) ? value : void 0;
}
parseJson(value) {
try {
return JSON.parse(value);
} catch {
const normalized = value.replaceAll(""", "\"").replaceAll(""", "\"").replaceAll("'", "'").replaceAll("'", "'");
try {
return JSON.parse(normalized);
} catch {
return;
}
}
}
getViewHtmlCandidates(viewHtml) {
if (typeof viewHtml !== "string" || !viewHtml.trim()) return [];
const doc = new DOMParser().parseFromString(viewHtml, "text/html");
const candidates = [];
for (const sourceEl of Array.from(doc.querySelectorAll("source"))) {
const src = sourceEl.getAttribute("src");
if (!src) continue;
candidates.push({
src,
type: sourceEl.getAttribute("type") ?? void 0,
label: sourceEl.getAttribute("data-res") ?? void 0
});
}
for (const setupDataEl of Array.from(doc.querySelectorAll("[videojs-setup-data]"))) {
const setupDataRaw = setupDataEl.getAttribute("videojs-setup-data");
if (!setupDataRaw) continue;
const setupData = this.parseJson(setupDataRaw);
if (setupData) candidates.push(...getUrlCandidates(setupData.sources));
}
return candidates;
}
isErrorData(data) {
return Object.hasOwn(data, "error") || Object.hasOwn(data, "detail") && !Object.hasOwn(data, "_class");
}
async getLectureData(courseId, lectureId) {
try {
const data = await (await this.fetch(`${this.API_ORIGIN}/users/me/subscribed-courses/${courseId}/lectures/${lectureId}/?` + new URLSearchParams({
"fields[lecture]": "title,description,view_html,asset,download_url,is_free,last_watched_second",
"fields[asset]": "asset_type,length,stream_url,media_sources,stream_urls,download_urls,external_url,captions,data,thumbnail_sprite,slides,slide_urls,course_is_drmed,media_license_token"
}).toString())).json();
if (this.isErrorData(data)) throw new VideoHelperError(data.detail ?? "unknown error");
return data;
} catch (err) {
Logger.error(`Failed to get lecture data by courseId: ${courseId} and lectureId: ${lectureId}`, err.message);
return;
}
}
async getCourseLang(courseId) {
try {
const data = await (await this.fetch(`${this.API_ORIGIN}/users/me/subscribed-courses/${courseId}?` + new URLSearchParams({ "fields[course]": "locale" }).toString())).json();
if (!this.isErrorData(data)) return data;
const data2 = await (await this.fetch(`${this.API_ORIGIN}/courses/${courseId}/?` + new URLSearchParams({ "fields[course]": "locale" }).toString())).json();
if (this.isErrorData(data2)) throw new VideoHelperError(data2.detail ?? "unknown error");
return data2;
} catch (err) {
Logger.error(`Failed to get course lang by courseId: ${courseId}`, err.message);
return;
}
}
findVideoUrl(sources, streamUrls, downloadUrls, streamUrl, externalUrl, outputs, viewHtml) {
const allCandidates = [];
const mediaSources = Array.isArray(sources) ? sources : [];
for (const source of mediaSources) allCandidates.push({
src: source.src,
type: source.type,
label: source.label
});
allCandidates.push(...getUrlCandidates(streamUrls));
allCandidates.push(...getUrlCandidates(downloadUrls));
allCandidates.push(...getOutputCandidates(outputs));
if (typeof viewHtml === "string") allCandidates.push(...this.getViewHtmlCandidates(viewHtml));
if (typeof streamUrl === "string") allCandidates.push({ src: streamUrl });
if (typeof externalUrl === "string") allCandidates.push({ src: externalUrl });
const playerSrc = this.video?.currentSrc || this.video?.src;
if (typeof playerSrc === "string" && playerSrc) allCandidates.push({ src: playerSrc });
const dedupCandidates = /* @__PURE__ */ new Map();
for (const candidate of allCandidates) {
const url = getCandidateUrl(candidate);
if (!url || /^javascript:/i.test(url)) continue;
const quality = getQualityValue(candidate.label ?? candidate.quality ?? candidate.height);
const type = String(candidate.type ?? "").toLowerCase();
const prev = dedupCandidates.get(url);
if (!prev || quality > prev.quality) dedupCandidates.set(url, {
url,
type,
quality,
isYouTubeWatch: /:\/\/(?:www\.)?youtube\.com\/watch\?/i.test(url)
});
}
const candidates = Array.from(dedupCandidates.values());
if (!candidates.length) return;
const mp4Candidates = candidates.filter((item) => item.type.includes("mp4") || /\.mp4(?:$|[?#])/i.test(item.url));
if (mp4Candidates.length) {
mp4Candidates.sort((a, b) => b.quality - a.quality);
return mp4Candidates[0]?.url;
}
const hlsUrl = candidates.find((item) => isM3U8(item.type, item.url))?.url;
if (hlsUrl) return hlsUrl;
const dashUrl = candidates.find((item) => isDash(item.type, item.url))?.url;
if (dashUrl) return dashUrl;
const nonYoutube = candidates.find((item) => !item.isYouTubeWatch)?.url;
if (nonYoutube) return nonYoutube;
return candidates[0]?.url;
}
getCaptionLocale(caption) {
const localeId = typeof caption.locale_id === "string" ? caption.locale_id : typeof caption.locale?.locale === "string" ? caption.locale.locale : void 0;
return localeId ? normalizeLang$1(localeId) : void 0;
}
findSubtitleUrl(captions, detectedLanguage) {
if (!Array.isArray(captions)) return;
const captionsWithDownload = captions.filter((caption) => isObject(caption) && (typeof caption.url === "string" || typeof caption.download_url === "string"));
const subtitle = captionsWithDownload.find((caption) => this.getCaptionLocale(caption) === detectedLanguage) ?? captionsWithDownload.find((caption) => this.getCaptionLocale(caption) === "en") ?? captionsWithDownload[0];
return subtitle?.url ?? subtitle?.download_url;
}
async getVideoData(videoId) {
const moduleData = this.getModuleData();
const courseId = this.getCourseId(moduleData);
const lectureId = this.getLectureId(videoId);
Logger.log(`[Udemy] courseId: ${courseId}, lectureId: ${lectureId}`);
if (!lectureId || !courseId) return;
const lectureData = await this.getLectureData(courseId, lectureId);
if (!lectureData) return;
const { title, description, asset, view_html } = lectureData;
const { length: duration, media_sources, captions } = asset;
const assetWithExtraUrls = asset;
const streamUrls = assetWithExtraUrls.stream_urls;
const downloadUrls = assetWithExtraUrls.download_urls;
const videoUrl = this.findVideoUrl(media_sources, streamUrls, downloadUrls, assetWithExtraUrls.stream_url ?? assetWithExtraUrls.streamUrl, assetWithExtraUrls.external_url, assetWithExtraUrls.data?.outputs, view_html);
if (!videoUrl) {
Logger.log("Failed to find video file in asset sources", asset);
return;
}
let courseLang = "en";
const courseLocale = (await this.getCourseLang(courseId))?.locale?.locale;
if (typeof courseLocale === "string") courseLang = normalizeLang$1(courseLocale);
if (!availableLangs.includes(courseLang)) courseLang = "en";
const subtitleUrl = this.findSubtitleUrl(captions, courseLang);
if (!subtitleUrl) Logger.log("Failed to find subtitle file in captions", captions);
return {
...subtitleUrl ? {
url: this.service?.url + videoId,
translationHelp: [{
target: "subtitles_file_url",
targetUrl: subtitleUrl
}, {
target: "video_file_url",
targetUrl: videoUrl
}],
detectedLanguage: courseLang
} : {
url: videoUrl,
translationHelp: null
},
duration,
title,
description
};
}
async getVideoId(url) {
return url.pathname.slice(1);
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/vimeo.js
var VimeoHelper = class extends BaseHelper {
API_KEY = "";
DEFAULT_SITE_ORIGIN = "https://vimeo.com";
SITE_ORIGIN = this.service?.url?.slice(0, -1) ?? this.DEFAULT_SITE_ORIGIN;
isErrorData(data) {
return Object.hasOwn(data, "error");
}
isPrivatePlayer() {
return this.referer && !this.referer.includes("vimeo.com") && this.origin.endsWith("player.vimeo.com");
}
toPublicUrl(videoId) {
const [id, hash] = videoId.split(":", 2);
return hash ? `${this.DEFAULT_SITE_ORIGIN}/${id}/${hash}` : `${this.DEFAULT_SITE_ORIGIN}/${id}`;
}
returnPublicBaseData(videoId) {
const baseData = this.returnBaseData(videoId);
if (!baseData) return;
return {
...baseData,
url: this.toPublicUrl(videoId)
};
}
normalizePublicVideoUrl(url, videoId) {
try {
const parsed = new URL(url);
if (parsed.hostname === "player.vimeo.com") return this.toPublicUrl(videoId);
if (parsed.hostname.endsWith("vimeo.com")) {
const colonMatch = /^\/(\d+):([a-z0-9]+)$/i.exec(parsed.pathname);
if (colonMatch) return `${this.DEFAULT_SITE_ORIGIN}/${colonMatch[1]}/${colonMatch[2]}`;
}
} catch {}
return url;
}
async getViewerData() {
try {
const data = await (await this.fetch("https://vimeo.com/_next/viewer")).json();
const { apiUrl, jwt } = data;
this.API_ORIGIN = `https://${apiUrl}`;
this.API_KEY = `jwt ${jwt}`;
return data;
} catch (err) {
Logger.error(`Failed to get default viewer data.`, err.message);
return false;
}
}
async getVideoInfo(videoId) {
try {
const params = new URLSearchParams({ fields: "name,link,description,duration" }).toString();
const data = await (await this.fetch(`${this.API_ORIGIN}/videos/${videoId}?${params}`, { headers: { Authorization: this.API_KEY } })).json();
if (this.isErrorData(data)) throw new Error(data.developer_message ?? data.error);
return data;
} catch (err) {
Logger.error(`Failed to get video info by video ID: ${videoId}`, err.message);
return false;
}
}
async getPrivateVideoSource(files) {
try {
const { default_cdn, cdns } = files.dash;
const cdnUrl = cdns[default_cdn].url;
const res = await this.fetch(cdnUrl);
if (res.status !== 200) throw new VideoHelperError(await res.text());
const data = await res.json();
const baseUrl = new URL(data.base_url, cdnUrl);
const videoData = data.audio.find((v) => v.mime_type === "audio/mp4" && v.format === "dash");
if (!videoData) throw new VideoHelperError("Failed to find video data");
const segmentUrl = videoData.segments?.[0]?.url;
if (!segmentUrl) throw new VideoHelperError("Failed to find first segment url");
const [videoName, videoParams] = segmentUrl.split("?", 2);
const params = new URLSearchParams(videoParams);
params.delete("range");
return new URL(`${videoData.base_url}${videoName}?${params.toString()}`, baseUrl).href;
} catch (err) {
Logger.error(`Failed to get private video source`, err.message);
return false;
}
}
async getPrivateVideoInfo(videoId) {
try {
if (typeof playerConfig === "undefined") return;
const videoSource = await this.getPrivateVideoSource(playerConfig.request.files);
if (!videoSource) throw new VideoHelperError("Failed to get private video source");
const { video: { title, duration }, request: { text_tracks: subs } } = playerConfig;
return {
url: `${this.SITE_ORIGIN}/${videoId}`,
video_url: videoSource,
title,
duration,
subs
};
} catch (err) {
Logger.error(`Failed to get private video info by video ID: ${videoId}`, err.message);
return false;
}
}
async getSubsInfo(videoId) {
try {
const params = new URLSearchParams({
per_page: "100",
fields: "language,type,link"
}).toString();
const content = await (await this.fetch(`${this.API_ORIGIN}/videos/${videoId}/texttracks?${params}`, { headers: { Authorization: this.API_KEY } })).json();
if (this.isErrorData(content)) throw new Error(content.developer_message ?? content.error);
return content.data;
} catch (err) {
Logger.error(`Failed to get subtitles info by video ID: ${videoId}`, err.message);
return [];
}
}
async getVideoData(videoId) {
if (this.isPrivatePlayer()) {
const videoInfo = await this.getPrivateVideoInfo(videoId);
if (!videoInfo) return;
const { url, subs, video_url, title, duration } = videoInfo;
const subtitles = subs.map((sub) => ({
language: normalizeLang$1(sub.lang),
source: "vimeo",
format: "vtt",
url: new URL(sub.url, this.SITE_ORIGIN).href,
isAutoGenerated: sub.lang.includes("autogenerated")
}));
const translationHelp = subtitles.length ? [{
target: "video_file_url",
targetUrl: video_url
}, {
target: "subtitles_file_url",
targetUrl: subtitles[0].url
}] : null;
return {
...translationHelp ? {
url,
translationHelp
} : { url: video_url },
subtitles,
title,
duration
};
}
if (!this.extraInfo) return this.returnPublicBaseData(videoId);
if (videoId.includes("/")) videoId = videoId.replace("/", ":");
if (!await this.getViewerData()) return this.returnPublicBaseData(videoId);
const videoInfo = await this.getVideoInfo(videoId);
if (!videoInfo) return this.returnPublicBaseData(videoId);
const subtitles = (await this.getSubsInfo(videoId)).map((caption) => ({
language: normalizeLang$1(caption.language),
source: "vimeo",
format: "vtt",
url: caption.link,
isAutoGenerated: caption.language.includes("autogen")
}));
const { link, duration, name: title, description } = videoInfo;
return {
url: this.normalizePublicVideoUrl(link, videoId),
title,
description,
subtitles,
duration
};
}
async getVideoId(url) {
const normalizedPathname = url.pathname.replace(/\/+$/, "");
const embedId = /video\/[^/]+$/.exec(normalizedPathname)?.[0];
if (this.isPrivatePlayer()) return embedId;
if (embedId) {
const hash = url.searchParams.get("h");
const videoId = embedId.replace("video/", "");
return hash ? `${videoId}/${hash}` : videoId;
}
const categoriesVideoId = /channels\/[^/]+\/([^/]+)/.exec(normalizedPathname)?.[1] ?? /groups\/[^/]+\/videos\/([^/]+)/.exec(normalizedPathname)?.[1] ?? /(showcase|album)\/[^/]+\/video\/([^/]+)/.exec(normalizedPathname)?.[2];
if (categoriesVideoId) return categoriesVideoId;
return /([^/]+\/)?[^/]+$/.exec(normalizedPathname)?.[0];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/vk.js
var VKHelper = class VKHelper extends BaseHelper {
static getPlayer() {
if (typeof Videoview === "undefined") return;
return Videoview?.getPlayerObject?.call(void 0);
}
async getVideoData(videoId) {
const player = VKHelper.getPlayer();
if (!player) return this.returnBaseData(videoId);
try {
const { description: descriptionHTML, duration, md_title: title } = player.vars;
const doc = new DOMParser().parseFromString(descriptionHTML, "text/html");
const description = Array.from(doc.body.childNodes).filter((el) => el.nodeName !== "BR").map((el) => el.textContent).join("\n");
let subtitles;
if (Object.hasOwn(player.vars, "subs")) subtitles = player.vars.subs.map((sub) => ({
language: normalizeLang$1(sub.lang),
source: "vk",
format: "vtt",
url: sub.url,
isAutoGenerated: !!sub.is_auto
}));
return {
url: this.service?.url + videoId,
title,
description,
duration,
subtitles
};
} catch (err) {
Logger.error(`Failed to get VK video data, because: ${err.message}`);
return this.returnBaseData(videoId);
}
}
async getVideoId(url) {
const pathID = /^\/(video|clip)-?\d{8,9}_\d{9}$/.exec(url.pathname);
if (pathID) return pathID[0].slice(1);
const idInsidePlaylist = /\/playlist\/[^/]+\/(video-?\d{8,9}_\d{9})/.exec(url.pathname);
if (idInsidePlaylist) return idInsidePlaylist[1];
const paramZ = url.searchParams.get("z");
if (paramZ) return paramZ.split("/")[0];
const paramOID = url.searchParams.get("oid");
const paramID = url.searchParams.get("id");
if (paramOID && paramID) return `video-${Math.abs(parseInt(paramOID, 10))}_${paramID}`;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/watchpornto.js
var WatchPornToHelper = class extends BaseHelper {
async getVideoId(url) {
return /(video|embed)\/(\d+)(\/[^/]+\/)?/.exec(url.pathname)?.[0];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/weibo.js
var weiboVideoIdRe = /^\d+:(?:[\da-f]{32}|\d{16,})$/i;
var weiboLayerIdRe = /^[A-Za-z0-9]+$/;
var weiboHostRe = /^(?:www\.)?weibo\.com$/;
var weiboLoginPathRe = /^\/newlogin\/?$/;
var WeiboHelper = class extends BaseHelper {
async getVideoId(url) {
if (url.hostname === "video.weibo.com") {
const fid = url.searchParams.get("fid");
if (!fid || !weiboVideoIdRe.test(fid)) return;
return `tv/show/${fid}`;
}
if (weiboHostRe.test(url.host) && weiboLoginPathRe.test(url.pathname)) {
const nestedUrl = url.searchParams.get("url");
if (nestedUrl) try {
const parsedNestedUrl = new URL(nestedUrl, url.origin);
if (parsedNestedUrl.href !== url.href) {
const nestedVideoId = await this.getVideoId(parsedNestedUrl);
if (nestedVideoId) return nestedVideoId;
}
} catch {}
const layerId = url.searchParams.get("layerid");
if (layerId && weiboLayerIdRe.test(layerId)) return `0/${layerId}`;
}
const normalizedPath = url.pathname.replace(/\/+$/, "");
if (/^\/\d+\/[A-Za-z0-9]+$/.test(normalizedPath) || /^\/0\/[A-Za-z0-9]+$/.test(normalizedPath) || /^\/tv\/show\/\d+:(?:[\da-f]{32}|\d{16,})$/i.test(normalizedPath)) return normalizedPath.slice(1);
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/weverse.js
var WeverseHelper = class extends BaseHelper {
API_ORIGIN = "https://global.apis.naver.com/weverse/wevweb";
API_APP_ID = "be4d79eb8fc7bd008ee82c8ec4ff6fd4";
API_HMAC_KEY = "1b9cb6378d959b45714bec49971ade22e6e24e42";
HEADERS = {
Accept: "application/json, text/plain, */*",
Origin: "https://weverse.io",
Referer: "https://weverse.io/"
};
getURLData() {
return {
appId: this.API_APP_ID,
language: "en",
os: "WEB",
platform: "WEB",
wpf: "pc"
};
}
async createHash(pathname) {
const timestamp = Date.now();
const salt = pathname.substring(0, Math.min(255, pathname.length)) + timestamp;
const sign = await getHmacSha1(this.API_HMAC_KEY, salt);
if (!sign) throw new VideoHelperError("Failed to get weverse HMAC signature");
return {
wmsgpad: timestamp.toString(),
wmd: sign
};
}
async getHashURLParams(pathname) {
const hash = await this.createHash(pathname);
return new URLSearchParams(hash).toString();
}
async getPostPreview(postId) {
const pathname = `/post/v1.0/post-${postId}/preview?` + new URLSearchParams({
fieldSet: "postForPreview",
...this.getURLData()
}).toString();
try {
const urlParams = await this.getHashURLParams(pathname);
return await (await this.fetch(`${this.API_ORIGIN + pathname}&${urlParams}`, { headers: this.HEADERS })).json();
} catch (err) {
Logger.error(`Failed to get weverse post preview by postId: ${postId}`, err.message);
return false;
}
}
async getVideoInKey(videoId) {
const pathname = `/video/v1.1/vod/${videoId}/inKey?` + new URLSearchParams({
gcc: "RU",
...this.getURLData()
}).toString();
try {
const urlParams = await this.getHashURLParams(pathname);
return await (await this.fetch(`${this.API_ORIGIN + pathname}&${urlParams}`, {
method: "POST",
headers: this.HEADERS
})).json();
} catch (err) {
Logger.error(`Failed to get weverse InKey by videoId: ${videoId}`, err.message);
return false;
}
}
async getVideoInfo(infraVideoId, inkey, serviceId) {
const timestamp = Date.now();
try {
const urlParams = new URLSearchParams({
key: inkey,
sid: serviceId,
nonce: timestamp.toString(),
devt: "html5_pc",
prv: "N",
aup: "N",
stpb: "N",
cpl: "en",
env: "prod",
lc: "en",
adi: JSON.stringify([{ adSystem: null }]),
adu: "/"
}).toString();
return await (await this.fetch(`https://global.apis.naver.com/rmcnmv/rmcnmv/vod/play/v2.0/${infraVideoId}?` + urlParams, { headers: this.HEADERS })).json();
} catch (err) {
Logger.error(`Failed to get weverse video info (infraVideoId: ${infraVideoId}, inkey: ${inkey}, serviceId: ${serviceId}`, err.message);
return false;
}
}
extractVideoInfo(videoList) {
return videoList.find((video) => video.useP2P === false && video.source.includes(".mp4"));
}
async getVideoData(videoId) {
const videoPreview = await this.getPostPreview(videoId);
if (!videoPreview) return;
const { videoId: internalVideoId, serviceId, infraVideoId } = videoPreview.extension.video;
if (!(internalVideoId && serviceId && infraVideoId)) return;
const inkeyData = await this.getVideoInKey(internalVideoId);
if (!inkeyData) return;
const videoInfo = await this.getVideoInfo(infraVideoId, inkeyData.inKey, serviceId);
if (!videoInfo) return;
const videoItem = this.extractVideoInfo(videoInfo.videos.list);
if (!videoItem) return;
return {
url: videoItem.source,
duration: videoItem.duration
};
}
async getVideoId(url) {
return /([^/]+)\/(live|media)\/([^/]+)/.exec(url.pathname)?.[3];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/xhamster.js
var XHamsterHelper = class extends BaseHelper {
async getVideoId(url) {
return /\/(videos\/[^/]+-[\dA-Za-z]+)\/?$/.exec(url.pathname)?.[1];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/xvideos.js
var XVideosHelper = class extends BaseHelper {
async getVideoId(url) {
return /[^/]+\/[^/]+$/.exec(url.pathname)?.[0];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/yandexdisk.js
var YandexDiskHelper = class extends BaseHelper {
API_ORIGIN = window.location.origin;
CLIENT_PREFIX = "/client/disk";
INLINE_PREFIX = "/i/";
DISK_PREFIX = "/d/";
isErrorData(data) {
return Object.hasOwn(data, "error");
}
async getClientVideoData(videoId) {
const dialogId = new URL(window.location.href).searchParams.get("idDialog");
if (!dialogId) return;
const preloadedScript = document.querySelector("#preloaded-data");
if (!preloadedScript) return;
try {
const { idClient, sk } = JSON.parse(preloadedScript.innerText).config;
const data = await (await this.fetch(`${this.API_ORIGIN}/models-v2?m=mpfs/info`, {
method: "POST",
body: JSON.stringify({
apiMethod: "mpfs/info",
connection_id: idClient,
requestParams: { path: dialogId },
sk
}),
headers: { "Content-Type": "application/json" }
})).json();
if (this.isErrorData(data)) throw new VideoHelperError(data.error?.message ?? data.error?.code);
if (data?.type !== "file") throw new VideoHelperError("Failed to get resource info");
const { meta: { short_url, video_info }, name } = data;
if (!video_info) throw new VideoHelperError("There's no video open right now");
if (!short_url) throw new VideoHelperError("Access to the video is limited");
return {
url: short_url,
title: this.clearTitle(name),
duration: Math.round(video_info.duration / 1e3)
};
} catch (err) {
Logger.error(`Failed to get yandex disk video data by video ID: ${videoId}, because ${err.message}`);
return;
}
}
clearTitle(title) {
return title.replace(/(\.[^.]+)$/, "");
}
getBodyHash(fileHash, sk) {
const data = JSON.stringify({
hash: fileHash,
sk
});
return encodeURIComponent(data);
}
async fetchList(dirHash, sk) {
const body = this.getBodyHash(dirHash, sk);
const data = await (await this.fetch(`${this.API_ORIGIN}/public/api/fetch-list`, {
method: "POST",
body
})).json();
if (Object.hasOwn(data, "error")) throw new VideoHelperError("Failed to fetch folder list");
return data.resources;
}
async getDownloadUrl(fileHash, sk) {
const body = this.getBodyHash(fileHash, sk);
const data = await (await this.fetch(`${this.API_ORIGIN}/public/api/download-url`, {
method: "POST",
body
})).json();
if (data.error) throw new VideoHelperError("Failed to get download url");
return data.data.url;
}
async getDiskVideoData(videoId) {
try {
const prefetchEl = document.getElementById("store-prefetch");
if (!prefetchEl) throw new VideoHelperError("Failed to get prefetch data");
const resourcePaths = videoId.split("/").slice(3);
if (!resourcePaths.length) throw new VideoHelperError("Failed to find video file path");
const { resources, rootResourceId, environment: { sk } } = JSON.parse(prefetchEl.innerText);
const rootResource = resources[rootResourceId];
const resourcePathsLastIdx = resourcePaths.length - 1;
const resourcePath = resourcePaths.filter((_, idx) => idx !== resourcePathsLastIdx).join("/");
let resourcesList = Object.values(resources);
if (resourcePath.includes("/")) resourcesList = await this.fetchList(`${rootResource.hash}:/${resourcePath}`, sk);
const resource = resourcesList.find((resource) => resource.name === resourcePaths[resourcePathsLastIdx]);
if (!resource) throw new VideoHelperError("Failed to find resource");
if (resource && resource.type === "dir") throw new VideoHelperError("Path is dir, but expected file");
const { meta: { short_url, mediatype, videoDuration }, path, name } = resource;
if (mediatype !== "video") throw new VideoHelperError("Resource isn't a video");
const title = this.clearTitle(name);
const duration = Math.round(videoDuration / 1e3);
if (short_url) return {
url: short_url,
duration,
title
};
const downloadUrl = await this.getDownloadUrl(path, sk);
return {
url: proxyMedia(new URL(downloadUrl)),
duration,
title
};
} catch (err) {
Logger.error(`Failed to get yandex disk video data by disk video ID: ${videoId}`, err.message);
return;
}
}
async getVideoData(videoId) {
if (videoId.startsWith(this.INLINE_PREFIX) || /^\/d\/([^/]+)$/.exec(videoId)) return { url: this.service?.url + videoId.slice(1) };
videoId = decodeURIComponent(videoId);
if (videoId.startsWith(this.CLIENT_PREFIX)) return await this.getClientVideoData(videoId);
return await this.getDiskVideoData(videoId);
}
async getVideoId(url) {
if (url.pathname.startsWith(this.CLIENT_PREFIX)) return url.pathname + url.search;
const fileId = /\/i\/([^/]+)/.exec(url.pathname)?.[0];
if (fileId) return fileId;
return /\/d\/([^/]+)/.exec(url.pathname) ? url.pathname : void 0;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/youku.js
var YoukuHelper = class extends BaseHelper {
async getVideoId(url) {
return /v_show\/id_[\w=]+/.exec(url.pathname)?.[0];
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/youtube.js
var YoutubeHelper = class YoutubeHelper extends BaseHelper {
static isMobile() {
return /^m\.youtube\.com$/.test(window.location.hostname);
}
static extractVideoId(url) {
const rawHash = url.hash.replace(/^#/, "");
if (rawHash) {
const normalizedHash = rawHash.startsWith("!") ? rawHash.slice(1) : rawHash;
let decodedHash = normalizedHash;
try {
decodedHash = decodeURIComponent(normalizedHash);
} catch {}
try {
const hashUrl = decodedHash.startsWith("http") ? new URL(decodedHash) : new URL(decodedHash.startsWith("/") ? decodedHash : `/${decodedHash}`, url.origin);
const hashVideoId = YoutubeHelper.extractVideoId(hashUrl);
if (hashVideoId) return hashVideoId;
} catch {
const hashVideoId = /(?:^|[?&#])v=([^&#]+)/.exec(decodedHash)?.[1];
if (hashVideoId) return hashVideoId;
}
}
if (url.hostname === "youtu.be") return url.pathname.replace(/^\/+/, "").split("/")[0] || void 0;
const pathVideoId = /\/(?:watch|embed|shorts|live|v|e)\/([^/?#]+)/.exec(url.pathname)?.[1] ?? void 0;
if (pathVideoId) return pathVideoId;
return url.searchParams.get("v") ?? void 0;
}
static getPlayer() {
if (window.location.pathname.startsWith("/shorts/") && !YoutubeHelper.isMobile()) return document.querySelector("#shorts-player");
return document.querySelector("#movie_player");
}
static getPlayerResponse() {
return YoutubeHelper.getPlayer()?.getPlayerResponse?.call(void 0);
}
static getPlayerData() {
return YoutubeHelper.getPlayer()?.getVideoData?.call(void 0);
}
static getVolume() {
const player = YoutubeHelper.getPlayer();
if (player?.getVolume) return player.getVolume() / 100;
return 1;
}
static setVolume(volume) {
const player = YoutubeHelper.getPlayer();
if (player?.setVolume) {
player.setVolume(Math.round(volume * 100));
return true;
}
return false;
}
static isMuted() {
const player = YoutubeHelper.getPlayer();
if (player?.isMuted) return player.isMuted();
return false;
}
static videoSeek(video, time) {
Logger.log("videoSeek", time);
video.currentTime = (YoutubeHelper.getPlayer()?.getProgressState()?.seekableEnd ?? video.currentTime) - time;
}
static getPoToken() {
const player = YoutubeHelper.getPlayer();
if (!player) return;
const audioTrack = player.getAudioTrack?.call(void 0);
if (!audioTrack?.captionTracks?.length) return;
const audioTrackWithPoToken = audioTrack.captionTracks.find((captionTrack) => captionTrack.url.includes("&pot="));
if (!audioTrackWithPoToken) return;
return /&pot=([^&]+)/.exec(audioTrackWithPoToken.url)?.[1];
}
static getGlobalConfig() {
if (typeof yt !== "undefined") return yt?.config_;
return typeof ytcfg !== "undefined" ? ytcfg?.data_ : void 0;
}
static getDeviceParams() {
const ytconfig = YoutubeHelper.getGlobalConfig();
if (!ytconfig) return "c=WEB";
const innertubeClient = ytconfig.INNERTUBE_CONTEXT?.client;
const deviceParams = new URLSearchParams(ytconfig.DEVICE);
deviceParams.delete("ceng");
deviceParams.delete("cengver");
deviceParams.set("c", innertubeClient?.clientName ?? ytconfig.INNERTUBE_CLIENT_NAME);
deviceParams.set("cver", innertubeClient?.clientVersion ?? ytconfig.INNERTUBE_CLIENT_VERSION);
deviceParams.set("cplayer", "UNIPLAYER");
return deviceParams.toString();
}
static getSubtitles(userLang) {
const playerCaptions = YoutubeHelper.getPlayerResponse()?.captions?.playerCaptionsTracklistRenderer;
if (!playerCaptions) return [];
const captionTracks = playerCaptions.captionTracks ?? [];
const userLangSupported = (playerCaptions.translationLanguages ?? []).find((language) => language.languageCode === userLang);
const asrLang = captionTracks.find((captionTrack) => captionTrack?.kind === "asr")?.languageCode ?? "en";
const subtitles = captionTracks.reduce((result, captionTrack) => {
if (!("languageCode" in captionTrack)) return result;
const language = captionTrack.languageCode ? normalizeLang$1(captionTrack.languageCode) : void 0;
const url = captionTrack.baseUrl;
if (!language || !url) return result;
const captionUrl = `${url.startsWith("http") ? url : `${window.location.origin}/${url}`}&fmt=json3`;
result.push({
source: "youtube",
format: "json",
language,
isAutoGenerated: captionTrack?.kind === "asr",
url: captionUrl
});
if (userLangSupported && captionTrack.isTranslatable && captionTrack.languageCode === asrLang && userLang !== language) result.push({
source: "youtube",
format: "json",
language: userLang,
isAutoGenerated: captionTrack?.kind === "asr",
translatedFromLanguage: language,
url: `${captionUrl}&tlang=${userLang}`
});
return result;
}, []);
Logger.log("youtube subtitles:", subtitles);
return subtitles;
}
static getLanguage() {
if (!YoutubeHelper.isMobile()) {
const trackInfo = YoutubeHelper.getPlayer()?.getAudioTrack?.call(void 0)?.getLanguageInfo();
if (trackInfo && trackInfo.id !== "und") return normalizeLang$1(trackInfo.id.split(".")[0]);
}
const autoCaption = YoutubeHelper.getPlayerResponse()?.captions?.playerCaptionsTracklistRenderer.captionTracks.find((caption) => caption.kind === "asr" && caption.languageCode);
return autoCaption ? normalizeLang$1(autoCaption.languageCode) : void 0;
}
async getVideoData(videoId) {
const { title: localizedTitle } = YoutubeHelper.getPlayerData() ?? {};
const { shortDescription: description, isLive: isStream, title } = YoutubeHelper.getPlayerResponse()?.videoDetails ?? {};
const subtitles = YoutubeHelper.getSubtitles(this.language);
let detectedLanguage = YoutubeHelper.getLanguage();
if (detectedLanguage && !availableLangs.includes(detectedLanguage)) detectedLanguage = void 0;
const duration = YoutubeHelper.getPlayer()?.getDuration?.call(void 0) ?? void 0;
return {
url: this.service?.url + videoId,
isStream,
title,
localizedTitle,
detectedLanguage,
description,
subtitles,
duration
};
}
async getVideoId(url) {
if (url.searchParams.has("enablejsapi")) {
const videoUrl = YoutubeHelper.getPlayer()?.getVideoUrl();
url = videoUrl ? new URL(videoUrl) : url;
}
return YoutubeHelper.extractVideoId(url);
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/zdf.js
var zdfPlayPathRe = /^\/play\/([^/?#]+)\/([^/?#]+)\/([^/?#]+)\/?$/i;
var ZDFHelper = class extends BaseHelper {
async getVideoId(url) {
const match = zdfPlayPathRe.exec(url.pathname);
if (!match) return;
const [, publicationForm, collectionCanonical, videoCanonical] = match;
return `${publicationForm}/${collectionCanonical}/${videoCanonical}`;
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/helpers/index.js
var availableHelpers = {
[VideoService$1.mailru]: MailRuHelper,
[VideoService$1.weverse]: WeverseHelper,
[VideoService$1.weibo]: WeiboHelper,
[VideoService$1.kodik]: KodikHelper,
[VideoService$1.patreon]: PatreonHelper,
[VideoService$1.reddit]: RedditHelper,
[VideoService$1.bannedvideo]: BannedVideoHelper,
[VideoService$1.kick]: KickHelper,
[VideoService$1.appledeveloper]: AppleDeveloperHelper,
[VideoService$1.epicgames]: EpicGamesHelper,
[VideoService$1.odysee]: OdyseeHelper,
[VideoService$1.coursehunterLike]: CoursehunterLikeHelper,
[VideoService$1.twitch]: TwitchHelper,
[VideoService$1.sap]: SapHelper,
[VideoService$1.jove]: JoveHelper,
[VideoService$1.linkedin]: LinkedinHelper,
[VideoService$1.vimeo]: VimeoHelper,
[VideoService$1.yandexdisk]: YandexDiskHelper,
[VideoService$1.vk]: VKHelper,
[VideoService$1.trovo]: TrovoHelper,
[VideoService$1.incestflix]: IncestflixHelper,
[VideoService$1.porntn]: PornTNHelper,
[VideoService$1.googledrive]: GoogleDriveHelper,
[VideoService$1.bilibili]: BilibiliHelper,
[VideoService$1.xvideos]: XVideosHelper,
[VideoService$1.xhamster]: XHamsterHelper,
[VideoService$1.spankbang]: SpankBangHelper,
[VideoService$1.rule34video]: Rule34VideoHelper,
[VideoService$1.picarto]: PicartoHelper,
[VideoService$1.olympicsreplay]: OlympicsReplayHelper,
[VideoService$1.watchpornto]: WatchPornToHelper,
[VideoService$1.archive]: ArchiveHelper,
[VideoService$1.dailymotion]: DailymotionHelper,
[VideoService$1.youku]: YoukuHelper,
[VideoService$1.egghead]: EggheadHelper,
[VideoService$1.newgrounds]: NewgroundsHelper,
[VideoService$1.okru]: OKRuHelper,
[VideoService$1.peertube]: PeertubeHelper,
[VideoService$1.eporner]: EpornerHelper,
[VideoService$1.bitchute]: BitchuteHelper,
[VideoService$1.rutube]: RutubeHelper,
[VideoService$1.facebook]: FacebookHelper,
[VideoService$1.rumble]: RumbleHelper,
[VideoService$1.twitter]: TwitterHelper,
[VideoService$1.pornhub]: PornhubHelper,
[VideoService$1.tiktok]: TikTokHelper,
[VideoService$1.proxitok]: TikTokHelper,
[VideoService$1.nine_gag]: NineGAGHelper,
[VideoService$1.youtube]: YoutubeHelper,
[VideoService$1.preservetube]: PreserveTubeHelper,
[VideoService$1.invidious]: YoutubeHelper,
[VideoService$1.piped]: YoutubeHelper,
[VideoService$1.zdf]: ZDFHelper,
[VideoService$1.dzen]: DzenHelper,
[VideoService$1.bunnystream]: BunnyStreamHelper,
[VideoService$1.cloudflarestream]: CloudflareStreamHelper,
[VideoService$1.loom]: LoomHelper,
[VideoService$1.rtnews]: RtNewsHelper,
[VideoService$1.bitview]: BitviewHelper,
[VideoService$1.thisvid]: ThisVidHelper,
[VideoService$1.ign]: IgnHelper,
[VideoService$1.bunkr]: BunkrHelper,
[VideoService$1.imdb]: IMDbHelper,
[VideoService$1.telegram]: TelegramHelper,
[VideoService$1.niconico]: NicoNicoHelper,
[ExtVideoService.udemy]: UdemyHelper,
[ExtVideoService.coursera]: CourseraHelper,
[ExtVideoService.douyin]: DouyinHelper,
[ExtVideoService.artstation]: ArtstationHelper,
[ExtVideoService.kickstarter]: KickstarterHelper,
[ExtVideoService.datacamp]: DataCampHelper,
[ExtVideoService.oraclelearn]: OracleLearnHelper,
[ExtVideoService.deeplearningai]: DeeplearningAIHelper,
[ExtVideoService.netacad]: NetacadHelper,
[ExtVideoService.mediafile]: MediafileHelper,
[ExtVideoService.skilljar]: SkilljarHelper
};
var VideoHelper = class {
helpersData;
constructor(helpersData = {}) {
this.helpersData = helpersData;
}
getHelper(service) {
return new availableHelpers[service](this.helpersData);
}
};
//#endregion
//#region node_modules/@vot.js/ext/dist/utils/videoData.js
function hasHelper(host) {
return host in availableHelpers;
}
function getService() {
if (localLinkRe.exec(window.location.href)) return [];
const hostname = window.location.hostname;
const enteredURL = new URL(window.location.href);
const isMatches = (match) => {
if (match instanceof RegExp) return match.test(hostname);
else if (typeof match === "string") return hostname.includes(match);
else if (typeof match === "function") return match(enteredURL);
return false;
};
return sites_default.filter((e) => {
return !!e.match && (Array.isArray(e.match) ? e.match.some(isMatches) : isMatches(e.match)) && e.host && e.url;
});
}
async function getVideoID(service, opts = {}) {
const url = new URL(window.location.href);
const serviceHost = service.host;
if (hasHelper(serviceHost)) return await new VideoHelper(opts).getHelper(serviceHost).getVideoId(url);
return serviceHost === VideoService$1.custom ? url.href : void 0;
}
async function getVideoData(service, opts = {}) {
const currentUrl = new URL(window.location.href);
const videoId = await getVideoID(service, opts);
if (!videoId) throw new VideoDataError(`Entered unsupported link: "${service.host}"`);
const origin = currentUrl.origin;
if ([
VideoService$1.peertube,
VideoService$1.coursehunterLike,
VideoService$1.bunnystream,
VideoService$1.cloudflarestream
].includes(service.host)) service.url = origin;
if (service.rawResult) return {
url: videoId,
videoId,
host: service.host,
duration: void 0
};
if (!service.needExtraData) return {
url: service.url + videoId,
videoId,
host: service.host,
duration: void 0
};
if (!hasHelper(service.host)) throw new VideoDataError(`No helper is available for "${service.host}"`);
const result = await new VideoHelper({
...opts,
service,
origin
}).getHelper(service.host).getVideoData(videoId);
if (!result) throw new VideoDataError(`Failed to get video raw url for ${service.host}`);
return {
...result,
url: result.url,
videoId,
host: service.host
};
}
//#endregion
//#region node_modules/chaimu/dist/config.js
var fetchFn = (...args) => {
if (typeof globalThis.fetch !== "function") throw new Error("Fetch API is not available in this environment");
return globalThis.fetch(...args);
};
var config_default = {
version: "1.0.6",
debug: false,
fetchFn
};
//#endregion
//#region node_modules/chaimu/dist/debug.js
var debug_default = { log: (...text) => {
if (!config_default.debug) return;
return console.log(`%c✦ chaimu.js v${config_default.version} ✦`, "background: #000; color: #fff; padding: 0 8px", ...text);
} };
//#endregion
//#region node_modules/chaimu/dist/player.js
var videoLipSyncEvents = [
"playing",
"ratechange",
"play",
"waiting",
"stalled",
"seeking",
"pause",
"ended",
"seeked"
];
var BUFFERING_PAUSE_DELAY_MS = 250;
var SYNC_DRIFT_TOLERANCE_SEC = .15;
function initAudioContext() {
if (typeof window === "undefined") return;
const audioContext = window.AudioContext ?? window.webkitAudioContext;
return audioContext ? new audioContext() : void 0;
}
var IDLE_SUSPEND_DELAY_MS = 1e4;
var BasePlayer = class {
static name = "BasePlayer";
chaimu;
fetch;
isBuffering = false;
bufferingPauseTimer;
_src;
fetchOpts;
constructor(chaimu, src) {
this.chaimu = chaimu;
this._src = src;
this.fetch = this.chaimu.fetchFn;
this.fetchOpts = this.chaimu.fetchOpts;
}
async init() {
return this;
}
async clear() {
return this;
}
lipSync(_mode = false) {
return this;
}
handleVideoEvent = (event) => {
debug_default.log(`handle video ${event.type}`);
this.lipSync(event.type);
return this;
};
isPlaybackBlocked() {
const video = this.chaimu.video;
return this.isBuffering || !video || video.ended || video.seeking || video.readyState < HTMLMediaElement.HAVE_FUTURE_DATA;
}
handlePlaybackError(action, error) {
if (error instanceof DOMException && error.name === "NotAllowedError") {
debug_default.log(`[${this.name}] ${action} blocked by autoplay policy`);
return;
}
console.error(`[${this.name}] ${action} failed`, error);
}
cancelBufferingPause() {
if (this.bufferingPauseTimer !== void 0) {
clearTimeout(this.bufferingPauseTimer);
this.bufferingPauseTimer = void 0;
}
}
resetPlaybackState() {
this.cancelBufferingPause();
this.isBuffering = false;
}
shouldPauseForBuffering() {
const video = this.chaimu.video;
if (!video || video.paused || video.ended) return false;
return video.seeking || video.readyState < HTMLMediaElement.HAVE_FUTURE_DATA;
}
scheduleBufferingPause(onPause) {
this.cancelBufferingPause();
this.bufferingPauseTimer = setTimeout(() => {
this.bufferingPauseTimer = void 0;
if (!this.shouldPauseForBuffering()) return;
this.isBuffering = true;
onPause();
}, BUFFERING_PAUSE_DELAY_MS);
}
async resumeAudioContext() {
const audioContext = this.chaimu.audioContext;
if (!audioContext) return true;
if (audioContext.state === "running") return true;
if (audioContext.state === "closed") {
this.handlePlaybackError("resume AudioContext", /* @__PURE__ */ new Error("AudioContext is closed"));
return false;
}
try {
await audioContext.resume();
return true;
} catch (error) {
this.handlePlaybackError("resume AudioContext", error);
return false;
}
}
async suspendAudioContext() {
const audioContext = this.chaimu.audioContext;
if (!audioContext || audioContext.state !== "running") return;
try {
await audioContext.suspend();
} catch (error) {
this.handlePlaybackError("suspend AudioContext", error);
}
}
removeVideoEvents() {
for (const e of videoLipSyncEvents) this.chaimu.video?.removeEventListener(e, this.handleVideoEvent);
return this;
}
addVideoEvents() {
for (const e of videoLipSyncEvents) this.chaimu.video?.addEventListener(e, this.handleVideoEvent);
return this;
}
async play() {
return this;
}
async pause() {
return this;
}
get name() {
return this.constructor.name;
}
set src(url) {
this._src = url;
}
get src() {
return this._src;
}
get currentSrc() {
return this._src;
}
set volume(_value) {}
get volume() {
return 0;
}
get playbackRate() {
return 0;
}
set playbackRate(_value) {}
get currentTime() {
return 0;
}
};
var AudioPlayer = class extends BasePlayer {
static name = "AudioPlayer";
audio;
gainNode;
audioSource;
suspendTimer;
constructor(chaimu, src) {
super(chaimu, src);
this.updateAudio();
}
initAudioBooster() {
if (!this.chaimu.audioContext) return this;
this.disconnectAudioNodes();
const gainNode = this.chaimu.audioContext.createGain();
this.gainNode = gainNode;
gainNode.connect(this.chaimu.audioContext.destination);
this.audioSource = this.chaimu.audioContext.createMediaElementSource(this.audio);
this.audioSource.connect(gainNode);
return this;
}
disconnectAudioNodes() {
if (this.audioSource) {
this.audioSource.disconnect();
this.audioSource = void 0;
}
if (this.gainNode) {
this.gainNode.disconnect();
this.gainNode = void 0;
}
}
scheduleSuspend() {
this.cancelSuspend();
this.suspendTimer = setTimeout(async () => {
debug_default.log("[AudioPlayer] idle suspend");
await this.suspendAudioContext();
}, IDLE_SUSPEND_DELAY_MS);
}
cancelSuspend() {
if (this.suspendTimer !== void 0) {
clearTimeout(this.suspendTimer);
this.suspendTimer = void 0;
}
}
syncAudioToVideo(force = false) {
const video = this.chaimu.video;
if (!video) return this;
this.audio.playbackRate = video.playbackRate;
const drift = Math.abs(this.audio.currentTime - video.currentTime);
if (force || drift > SYNC_DRIFT_TOLERANCE_SEC) this.audio.currentTime = video.currentTime;
return this;
}
updateAudio() {
this.audio = new Audio(this.src);
this.audio.crossOrigin = "anonymous";
return this;
}
async init() {
this.updateAudio();
this.initAudioBooster();
return this;
}
async resumeAndPlayAudio() {
if (!this.audio || this.isPlaybackBlocked()) return;
this.cancelSuspend();
if (!await this.resumeAudioContext() || this.isPlaybackBlocked()) return;
try {
await this.audio.play();
} catch (error) {
this.handlePlaybackError("play audio element", error);
}
}
lipSync(mode = false) {
debug_default.log("[AudioPlayer] lipsync video", this.chaimu.video);
if (!this.chaimu.video) return this;
this.syncAudioToVideo(mode === "play" || mode === "seeked" || mode === "seeking");
if (!mode) return this;
switch (mode) {
case "ratechange":
this.cancelBufferingPause();
return this;
case "play":
case "playing":
case "seeked":
this.cancelBufferingPause();
this.isBuffering = false;
if (!this.chaimu.video.paused) this.syncPlay();
return this;
case "waiting":
case "stalled":
this.scheduleBufferingPause(() => {
this.audio.pause();
});
return this;
case "seeking":
this.cancelBufferingPause();
this.isBuffering = true;
this.audio.pause();
return this;
case "pause":
case "ended":
this.cancelBufferingPause();
this.isBuffering = false;
this.pause();
return this;
default: return this;
}
}
async clear() {
this.cancelSuspend();
this.resetPlaybackState();
this.audio.pause();
this.audio.src = "";
this.audio.removeAttribute("src");
this.audio.load();
this.disconnectAudioNodes();
await this.suspendAudioContext();
return this;
}
syncPlay() {
debug_default.log("[AudioPlayer] sync play called");
this.resumeAndPlayAudio();
return this;
}
async play() {
debug_default.log("[AudioPlayer] play called");
await this.resumeAndPlayAudio();
return this;
}
async pause() {
debug_default.log("[AudioPlayer] pause called");
this.resetPlaybackState();
if (this.audio) this.audio.pause();
this.scheduleSuspend();
return this;
}
set src(url) {
this._src = url;
if (!url) {
this.clear();
return;
}
this.audio.src = url;
}
get src() {
return this._src;
}
get currentSrc() {
return this.audio.currentSrc;
}
set volume(value) {
if (this.gainNode) {
this.gainNode.gain.value = value;
return;
}
this.audio.volume = value;
}
get volume() {
return this.gainNode ? this.gainNode.gain.value : this.audio.volume;
}
get playbackRate() {
return this.audio.playbackRate;
}
set playbackRate(value) {
this.audio.playbackRate = value;
}
get currentTime() {
return this.audio.currentTime;
}
};
var ChaimuPlayer = class extends BasePlayer {
static name = "ChaimuPlayer";
audioBuffer;
audioElement;
mediaElementSource;
gainNode;
blobUrl;
isClearing = false;
isInitializing = false;
clearingPromise;
suspendTimer;
async fetchAudio() {
if (!this._src) throw new Error("No audio source provided");
if (!this.chaimu.audioContext) throw new Error("No audio context available");
debug_default.log(`[ChaimuPlayer] Fetching audio from ${this._src}...`);
let tempBlobUrl;
try {
const res = await this.fetch(this._src, this.fetchOpts);
if (!res.ok) throw new Error(`Response status: ${res.status}`);
debug_default.log(`[ChaimuPlayer] Decoding fetched audio...`);
const data = await res.arrayBuffer();
const blob = new Blob([data]);
tempBlobUrl = URL.createObjectURL(blob);
this.audioBuffer = await this.chaimu.audioContext.decodeAudioData(data);
if (this.blobUrl) URL.revokeObjectURL(this.blobUrl);
this.blobUrl = tempBlobUrl;
tempBlobUrl = void 0;
} catch (err) {
if (tempBlobUrl) URL.revokeObjectURL(tempBlobUrl);
throw new Error(`Failed to fetch audio file, because ${err.message}`);
}
return this;
}
initAudioBooster() {
if (!this.chaimu.audioContext) return this;
this.disconnectAudioNodes();
this.gainNode = this.chaimu.audioContext.createGain();
return this;
}
disconnectAudioNodes() {
if (this.mediaElementSource) {
this.mediaElementSource.disconnect();
this.mediaElementSource = void 0;
}
if (this.gainNode) {
this.gainNode.disconnect();
this.gainNode = void 0;
}
}
scheduleSuspend() {
this.cancelSuspend();
this.suspendTimer = setTimeout(async () => {
debug_default.log("[ChaimuPlayer] idle suspend");
await this.suspendAudioContext();
}, IDLE_SUSPEND_DELAY_MS);
}
cancelSuspend() {
if (this.suspendTimer !== void 0) {
clearTimeout(this.suspendTimer);
this.suspendTimer = void 0;
}
}
async init() {
if (this.isInitializing) throw new Error("Initialization already in progress");
this.isInitializing = true;
try {
await this.fetchAudio();
this.initAudioBooster();
this.createAudioElement();
return this;
} finally {
this.isInitializing = false;
}
}
createAudioElement() {
if (!this.chaimu.audioContext) throw new Error("No audio context available");
if (!this.blobUrl) throw new Error("No blob URL available.");
const audio = new Audio(this.blobUrl);
audio.crossOrigin = "anonymous";
if ("preservesPitch" in audio) {
audio.preservesPitch = true;
if ("mozPreservesPitch" in audio) audio.mozPreservesPitch = true;
if ("webkitPreservesPitch" in audio) audio.webkitPreservesPitch = true;
}
this.audioElement = audio;
const gainNode = this.gainNode;
if (!gainNode) throw new Error("Audio gain node is missing");
this.mediaElementSource = this.chaimu.audioContext.createMediaElementSource(audio);
this.mediaElementSource.connect(gainNode);
gainNode.connect(this.chaimu.audioContext.destination);
}
lipSync(mode = false) {
debug_default.log("[ChaimuPlayer] lipsync video", this.chaimu.video, this);
if (!this.chaimu.video || !mode) return this;
switch (mode) {
case "play":
case "playing":
case "ratechange":
case "seeked":
this.cancelBufferingPause();
this.isBuffering = false;
if (!this.chaimu.video.paused) this.start();
return this;
case "waiting":
case "stalled":
this.scheduleBufferingPause(() => {
if (this.audioElement) this.audioElement.pause();
});
return this;
case "seeking":
this.cancelBufferingPause();
this.isBuffering = true;
if (this.audioElement) this.audioElement.pause();
return this;
case "pause":
case "ended":
this.cancelBufferingPause();
this.isBuffering = false;
this.pause();
return this;
default: return this;
}
}
async reopenCtx() {
if (!this.chaimu.audioContext) throw new Error("No audio context available");
try {
if (this.chaimu.audioContext.state !== "closed") await this.chaimu.audioContext.close();
} catch (err) {
debug_default.log("[ChaimuPlayer] Failed to close audio context:", err);
}
this.chaimu.audioContext = initAudioContext();
return this;
}
async clear() {
if (this.isClearing && this.clearingPromise) return this.clearingPromise;
if (!this.chaimu.audioContext) throw new Error("No audio context available");
debug_default.log("clear audio context");
this.cancelSuspend();
this.resetPlaybackState();
this.isClearing = true;
this.clearingPromise = (async () => {
try {
if (this.audioElement) {
this.audioElement.pause();
this.audioElement = void 0;
}
if (this.blobUrl) {
URL.revokeObjectURL(this.blobUrl);
this.blobUrl = void 0;
}
this.audioBuffer = void 0;
const oldVolume = this.gainNode ? this.gainNode.gain.value : 1;
this.disconnectAudioNodes();
await this.reopenCtx();
if (this.chaimu.audioContext) {
this.initAudioBooster();
this.volume = oldVolume;
await this.suspendAudioContext();
}
return this;
} finally {
this.isClearing = false;
this.clearingPromise = void 0;
}
})();
return this.clearingPromise;
}
async start() {
if (!this.chaimu.audioContext) throw new Error("No audio context available");
if (!this.audioElement) throw new Error("Audio element is missing");
if (this.isClearing && this.clearingPromise) {
debug_default.log("The other cleaner is still running, waiting...");
await this.clearingPromise;
}
if (!this.chaimu.audioContext || !this.audioElement) return this;
this.cancelSuspend();
if (this.isPlaybackBlocked()) return this;
debug_default.log("starting audio via HTMLAudioElement");
if (!await this.resumeAudioContext()) return this;
if (this.isPlaybackBlocked()) return this;
if (this.chaimu.video) {
this.audioElement.currentTime = this.chaimu.video.currentTime;
this.audioElement.playbackRate = this.chaimu.video.playbackRate;
}
try {
await this.audioElement.play();
} catch (error) {
this.handlePlaybackError("play audio element", error);
}
return this;
}
async pause() {
if (!this.chaimu.audioContext) throw new Error("No audio context available");
this.resetPlaybackState();
if (this.audioElement) this.audioElement.pause();
this.scheduleSuspend();
return this;
}
async play() {
if (!this.chaimu.audioContext) throw new Error("No audio context available");
this.cancelSuspend();
await this.resumeAudioContext();
return this;
}
set src(url) {
this._src = url;
}
get src() {
return this._src;
}
get currentSrc() {
return this._src;
}
set volume(value) {
if (this.gainNode) this.gainNode.gain.value = value;
}
get volume() {
return this.gainNode ? this.gainNode.gain.value : 0;
}
set playbackRate(value) {
if (this.audioElement) this.audioElement.playbackRate = value;
}
get playbackRate() {
return this.audioElement ? this.audioElement.playbackRate : this.chaimu.video?.playbackRate ?? 1;
}
get currentTime() {
return this.chaimu.video?.currentTime ?? 0;
}
};
//#endregion
//#region node_modules/chaimu/dist/client.js
var Chaimu = class {
_debug = false;
audioContext;
player;
video;
fetchFn;
fetchOpts;
constructor({ url, video, debug = false, fetchFn = config_default.fetchFn, fetchOpts = {}, preferAudio = false }) {
this._debug = config_default.debug = debug;
this.fetchFn = fetchFn;
this.fetchOpts = fetchOpts;
this.audioContext = preferAudio ? void 0 : initAudioContext();
this.player = this.audioContext && !preferAudio ? new ChaimuPlayer(this, url) : new AudioPlayer(this, url);
this.video = video;
}
async init() {
await this.player.init();
this.player.addVideoEvents();
if (this.video.paused || this.video.ended || this.video.seeking || this.video.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) await this.player.pause();
else this.player.lipSync("play");
}
set debug(value) {
this._debug = config_default.debug = value;
}
get debug() {
return this._debug;
}
};
//#endregion
//#region src/bootstrap/bootState.ts
var MAIN_BOOT_KEY = "__VOT_MAIN_BOOT_STATE__";
function isBootstrapStatus(value) {
return value === "idle" || value === "booting" || value === "booted" || value === "failed";
}
function isBootstrapState(value) {
if (!value || typeof value !== "object") return false;
return isBootstrapStatus(value.status);
}
function getOrCreateBootState(bootKey = MAIN_BOOT_KEY) {
const scope = globalThis;
const existing = scope[bootKey];
if (isBootstrapState(existing)) return existing;
const created = {
status: "idle",
promise: null,
error: null
};
scope[bootKey] = created;
return created;
}
//#endregion
//#region src/bootstrap/iframeInteractor.ts
var iframeInteractorInitialized = false;
function initIframeInteractor() {
if (iframeInteractorInitialized) return;
iframeInteractorInitialized = true;
const currentConfig = Object.entries({
"https://dev.epicgames.com": {
targetOrigin: "https://dev.epicgames.com",
dataFilter: (data) => typeof data === "string" && data.startsWith("getVideoId:"),
extractVideoId: (url) => url.pathname.split("/").at(-2) ?? null,
responseFormatter: (videoId, data) => `${typeof data === "string" ? data : ""}:${videoId}`
},
"https://www.dailymotion.com": {
targetOrigin: "https://geo.dailymotion.com",
dataFilter: (data) => typeof data === "string" && data.startsWith("getVideoId:"),
extractVideoId: (url) => {
return /(?:^|\/)video\/([^/]+)/.exec(url.pathname)?.[1];
},
responseFormatter: (videoId) => `getVideoId:${videoId}`
}
}).find(([origin]) => globalThis.location.origin === origin)?.[1];
if (!currentConfig) return;
globalThis.addEventListener("message", (event) => {
try {
if (event.origin !== currentConfig.targetOrigin) return;
if (!currentConfig.dataFilter(event.data)) return;
const videoId = currentConfig.extractVideoId(new URL(globalThis.location.href));
if (!videoId) return;
const response = currentConfig.responseFormatter(videoId, event.data);
if (event.source && "postMessage" in event.source) event.source.postMessage(response, currentConfig.targetOrigin);
} catch (error) {
console.error("Iframe communication error:", error);
}
});
}
//#endregion
//#region src/config/config.ts
var workerHost = "api.browser.yandex.ru";
/**
* used for streaming
*
* @see https://github.com/FOSWLY/media-proxy
*/
var m3u8ProxyHost = "media-proxy.toil.cc/v1/proxy/m3u8";
/**
* @see https://github.com/FOSWLY/vot-worker
*/
var proxyWorkerHostMode1 = "vot-worker.vtrans.eu.cc";
var proxyWorkerHost = "vot-worker.eu.cc";
var votBackendUrl = "https://vot.toil.cc/v1";
/**
* @see https://github.com/FOSWLY/translate-backend
*/
var foswlyTranslateUrl = "https://translate-backend.transly.eu.cc/v2";
var detectRustServerUrl = "https://rust-server-531j.onrender.com/detect";
var authServerUrl = "https://rust-server-531j.onrender.com";
var authLoginUrl = `${authServerUrl}/v1/auth/handle`;
var avatarServerUrl = "https://avatars.mds.yandex.net/get-yapic";
var repoPath = "ilyhalight/voice-over-translation";
var contentUrl = `https://raw.githubusercontent.com/${repoPath}`;
var repositoryUrl = `https://github.com/${repoPath}`;
var defaultTranslationService = "yandexbrowser";
var defaultDetectService = "yandexbrowser";
var proxyOnlyCountries = [
"UA",
"LV",
"LT"
];
/**
* 100 - 3000 ms - delay before hiding button
*/
var defaultAutoHideDelay = 1e3;
var actualCompatVersion = "2025-05-09";
//#endregion
//#region src/types/storage.ts
var subtitleResponseLanguageModes = ["auto", "original"];
var storageKeys = [
"autoTranslate",
"autoSubtitles",
"dontTranslateLanguages",
"enabledDontTranslateLanguages",
"enabledAutoVolume",
"enabledSmartDucking",
"autoVolume",
"buttonPos",
"showVideoSlider",
"syncVolume",
"downloadWithName",
"sendNotifyOnComplete",
"subtitlesMaxLength",
"subtitlesSmartLayout",
"highlightWords",
"subtitlesFontSize",
"subtitlesFontFamily",
"subtitlesOpacity",
"subtitlesDownloadFormat",
"responseLanguage",
"responseLanguageSubtitles",
"defaultVolume",
"onlyBypassMediaCSP",
"newAudioPlayer",
"showPiPButton",
"translateAPIErrors",
"translationService",
"detectService",
"translationHotkey",
"subtitlesHotkey",
"m3u8ProxyHost",
"proxyWorkerHost",
"translateProxyEnabled",
"translateProxyEnabledDefault",
"audioBooster",
"useLivelyVoice",
"autoHideButtonDelay",
"useAudioDownload",
"compatVersion",
"localePhrases",
"localeLang",
"localeHash",
"localeVersion",
"localeUpdatedAt",
"localeLangOverride",
"account"
];
//#endregion
//#region src/utils/debug.ts
var noop = () => {};
var debug = {
log: noop,
warn: noop,
error: noop
};
//#endregion
//#region src/utils/number.ts
function clampNumber$1(value, min, max) {
if (!Number.isFinite(value)) return min;
if (max < min) return min;
return Math.max(min, Math.min(max, value));
}
function clampNumberWithSortedBounds(value, min, max) {
return Math.min(Math.max(value, Math.min(min, max)), Math.max(min, max));
}
//#endregion
//#region src/utils/localization.ts
function getNavigatorLang() {
if (typeof navigator === "undefined") return "en";
return navigator.language?.substring(0, 2).toLowerCase() || "en";
}
var slavicLangs = new Set([
"uk",
"be",
"bg",
"mk",
"sr",
"bs",
"hr",
"sl",
"pl",
"sk",
"cs"
]);
function resolveCalculatedResLang(baseLang) {
if (availableTTS.includes(baseLang)) return baseLang;
if (slavicLangs.has(baseLang)) return "ru";
return "en";
}
var lang = getNavigatorLang();
var calculatedResLang = resolveCalculatedResLang(lang);
//#endregion
//#region src/utils/utils.ts
var DEFAULT_OBJECT_URL_REVOKE_DELAY_MS = 3e4;
var ASCII_CONTROL_CHARS_RE = /\p{Cc}/gu;
var INVALID_FILENAME_CHARS_RE = /[\\/:*?"'<>|]+/g;
var URL_PROTOCOL_RE = /^https?:\/\//i;
var MULTIPLE_DASHES_RE = /-{2,}/g;
var EDGE_FILE_CHARS_RE = /^[.\s-]+|[.\s-]+$/g;
function getDateFallbackFilename() {
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
}
function stripAsciiControlChars(value) {
return value.replace(ASCII_CONTROL_CHARS_RE, "");
}
/**
* Creates a stable JSON string representation for consistent hashing
* @param value The value to stringify
* @returns A stable JSON string
*/
function stableStringify(value) {
const seen = /* @__PURE__ */ new WeakSet();
return JSON.stringify(value, (_key, val) => {
if (val && typeof val === "object") {
if (seen.has(val)) return "[Circular]";
seen.add(val);
if (Array.isArray(val)) return val;
const sorted = {};
const keys = Object.keys(val).sort((a, b) => a.localeCompare(b));
for (const key of keys) sorted[key] = val[key];
return sorted;
}
return val;
});
}
/**
* Small, deterministic hash for cache keys. (Not crypto.)
* @param str The string to hash
* @returns A base36 string representation of the hash
*/
function fnv1a32ToKeyPart(str) {
let hash = 2166136261;
let i = 0;
while (i < str.length) {
const codePoint = str.codePointAt(i) ?? 0;
hash ^= codePoint;
hash = Math.imul(hash, 16777619);
i += codePoint > 65535 ? 2 : 1;
}
return (hash >>> 0).toString(36);
}
var isPiPAvailable = () => Boolean(document.pictureInPictureEnabled);
async function writeBlobToHandle(handle, blob) {
try {
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
return true;
} catch {
return false;
}
}
async function shareBlob(blob, filename) {
const nav = typeof navigator === "undefined" ? void 0 : navigator;
if (!nav?.share || typeof File === "undefined") return "unsupported";
let file;
try {
file = new File([blob], filename, { type: blob.type || "application/octet-stream" });
} catch {
return "unsupported";
}
if (typeof nav.canShare === "function" && !nav.canShare({ files: [file] })) return "unsupported";
try {
await nav.share({
files: [file],
title: filename
});
return "shared";
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") return "shared";
return "error";
}
}
function triggerBlobDownload(blob, filename) {
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
anchor.rel = "noopener noreferrer";
anchor.target = "_blank";
anchor.style.position = "fixed";
anchor.style.left = "-9999px";
anchor.style.top = "0";
(document.body ?? document.documentElement).append(anchor);
try {
anchor.click();
return true;
} catch {
return false;
} finally {
anchor.remove();
revokeObjectUrlLater(url);
}
}
/** Downloads binary file with entered filename */
async function downloadBlob(blob, filename, options = {}) {
if (options.fileHandle) {
if (await writeBlobToHandle(options.fileHandle, blob)) return true;
}
if (options.preferShare) {
if (await shareBlob(blob, filename) === "shared") return true;
}
return triggerBlobDownload(blob, filename);
}
function revokeObjectUrlLater(url, delayMs = DEFAULT_OBJECT_URL_REVOKE_DELAY_MS) {
const safeDelayMs = Number.isFinite(delayMs) && delayMs >= 0 ? delayMs : DEFAULT_OBJECT_URL_REVOKE_DELAY_MS;
globalThis.setTimeout(() => URL.revokeObjectURL(url), safeDelayMs);
}
function clearFileName(filename) {
const trimmed = filename.trim();
if (!trimmed) return getDateFallbackFilename();
return stripAsciiControlChars(trimmed).replace(URL_PROTOCOL_RE, "").replace(INVALID_FILENAME_CHARS_RE, "-").replace(MULTIPLE_DASHES_RE, "-").replace(EDGE_FILE_CHARS_RE, "") || getDateFallbackFilename();
}
var getTimestamp = () => Math.floor(Date.now() / 1e3);
var getHeaders = (headers) => headers ? Object.fromEntries(new Headers(headers)) : {};
function clamp(value, min = 0, max = 100) {
return clampNumberWithSortedBounds(value, min, max);
}
function toFlatObj(data) {
const out = {};
const stack = Object.entries(data);
while (stack.length) {
const entry = stack.pop();
if (!entry) continue;
const [key, val] = entry;
if (val === void 0) continue;
if (!(val !== null && typeof val === "object" && !Array.isArray(val))) {
out[key] = val;
continue;
}
for (const [k, v] of Object.entries(val)) stack.push([`${key}.${k}`, v]);
}
return out;
}
//#endregion
//#region src/core/cacheManager.ts
var YANDEX_TTL_MS = 7200 * 1e3;
var RESPONSE_CACHE_CREATED_AT_HEADER = "x-vot-cache-created-at";
var RESPONSE_CACHE_KEY_HEADER = "x-vot-cache-key";
var DEFAULT_RESPONSE_CACHE_NAME = "vot-http-cache-v1";
var MAX_MEMORY_CACHE_ENTRIES = 500;
var VOT_SESSION_STORAGE_KEY = "VOTSession";
var EXPIRY_FIELD_BY_FIELD = {
translation: "translationExpiresAt",
subtitles: "subtitlesExpiresAt"
};
function getCurrentUnixTimestampSeconds() {
return Math.floor(Date.now() / 1e3);
}
function isClientSession(value) {
if (!value || typeof value !== "object") return false;
const candidate = value;
return typeof candidate.expires === "number" && Number.isFinite(candidate.expires) && typeof candidate.timestamp === "number" && Number.isFinite(candidate.timestamp) && typeof candidate.uuid === "string" && candidate.uuid.length > 0 && typeof candidate.secretKey === "string" && candidate.secretKey.length > 0;
}
function sanitizeVOTSessions(value) {
if (!value || typeof value !== "object") return {};
const now = getCurrentUnixTimestampSeconds();
const entries = Object.entries(value).flatMap(([module, session]) => {
if (!isClientSession(session)) return [];
if (session.timestamp + session.expires <= now) return [];
return [[module, session]];
});
return Object.fromEntries(entries);
}
function hasSessions(sessions) {
return Object.keys(sessions).length > 0;
}
var VOTSessionStorageCache = class {
constructor(storage = votStorage) {
this.storage = storage;
}
getStorageKey() {
return VOT_SESSION_STORAGE_KEY;
}
async restore(_host, currentSessions = {}) {
const storageKey = this.getStorageKey();
const rawStoredSession = await this.storage.getRaw(storageKey);
const restoredSessions = sanitizeVOTSessions(rawStoredSession);
if (!hasSessions(restoredSessions)) {
if (rawStoredSession !== void 0) await this.storage.deleteRaw(storageKey);
return currentSessions;
}
return {
...currentSessions,
...restoredSessions
};
}
async persist(_host, sessions) {
const storageKey = this.getStorageKey();
const sanitizedSessions = sanitizeVOTSessions(sessions);
if (!hasSessions(sanitizedSessions)) {
await this.storage.deleteRaw(storageKey);
return;
}
await this.storage.setRaw(storageKey, sanitizedSessions);
}
};
/**
* Small in-memory cache with TTL for both translations and subtitles.
*
* The cache is keyed by a stable key built by VideoHandler.
*/
var InMemoryCacheManager = class {
cache = /* @__PURE__ */ new Map();
/**
* Clears all cached entries.
*
* Used when runtime settings change (e.g. proxy mode/host), because cached
* translation URLs and especially previous failures can become stale.
*/
clear() {
this.cache.clear();
}
getTranslation(key) {
return this.getValue(key, "translation");
}
setTranslation(key, translation) {
this.setValue(key, "translation", translation);
}
getSubtitles(key) {
return this.getValue(key, "subtitles");
}
setSubtitles(key, subtitles) {
this.setValue(key, "subtitles", subtitles);
}
deleteSubtitles(key) {
this.deleteValue(key, "subtitles");
}
getValue(key, field) {
const now = Date.now();
const entry = this.cache.get(key);
if (!entry) return void 0;
const expiryField = EXPIRY_FIELD_BY_FIELD[field];
const expiresAt = entry[expiryField];
if (expiresAt !== void 0 && expiresAt <= now) {
entry[field] = void 0;
entry[expiryField] = void 0;
this.evictIfEmpty(key, entry);
return;
}
return entry[field];
}
setValue(key, field, value) {
const now = Date.now();
const entry = this.getOrCreateEntry(key);
const expiresAt = now + YANDEX_TTL_MS;
const expiryField = EXPIRY_FIELD_BY_FIELD[field];
entry[field] = value;
entry[expiryField] = expiresAt;
}
deleteValue(key, field) {
const entry = this.cache.get(key);
if (!entry) return;
const expiryField = EXPIRY_FIELD_BY_FIELD[field];
entry[field] = void 0;
entry[expiryField] = void 0;
this.evictIfEmpty(key, entry);
}
evictIfEmpty(key, entry) {
if (entry.translation === void 0 && entry.subtitles === void 0) this.cache.delete(key);
}
getOrCreateEntry(key) {
const existing = this.cache.get(key);
if (existing) return existing;
const entry = {};
this.cache.set(key, entry);
return entry;
}
};
var ResponseCacheManager = class {
memoryCache = /* @__PURE__ */ new Map();
inFlightRequests = /* @__PURE__ */ new Map();
async execute(context, options, fetcher) {
if (!options || options.ttlMs <= 0) return fetcher();
const key = options.key ?? this.buildDefaultCacheKey(context);
if (!key) return fetcher();
const method = this.normalizeMethod(context.method);
const ttlMs = options.ttlMs;
const cacheName = options.cacheName || DEFAULT_RESPONSE_CACHE_NAME;
const useMemory = options.useMemory !== false;
const useCacheApi = options.useCacheApi !== false && method === "GET" && this.supportsCacheApi();
const cacheApiKey = useCacheApi ? fnv1a32ToKeyPart(key) : "";
const dedupe = options.dedupe !== false;
const allowStaleOnError = options.allowStaleOnError !== false;
const nowMs = Date.now();
const staleFallback = await this.readCachedResponse({
key,
nowMs,
useMemory,
useCacheApi,
cacheName,
url: context.url,
cacheApiKey,
ttlMs,
allowStaleOnError
});
if (staleFallback.fresh) return staleFallback.fresh;
if (!dedupe) return await this.runNetworkRequestWithFallback({
key,
cacheName,
url: context.url,
cacheApiKey,
ttlMs,
useMemory,
useCacheApi
}, fetcher, allowStaleOnError ? staleFallback.stale : void 0);
const inFlight = this.inFlightRequests.get(key);
if (inFlight !== void 0) return (await inFlight).clone();
const networkPromise = this.runNetworkRequestWithFallback({
key,
cacheName,
url: context.url,
cacheApiKey,
ttlMs,
useMemory,
useCacheApi
}, fetcher, allowStaleOnError ? staleFallback.stale?.clone() : void 0);
this.inFlightRequests.set(key, networkPromise);
try {
return (await networkPromise).clone();
} finally {
this.inFlightRequests.delete(key);
}
}
async readCachedResponse({ key, nowMs, useMemory, useCacheApi, cacheName, url, cacheApiKey, ttlMs, allowStaleOnError }) {
let staleFallback;
if (useMemory) {
const memoryHit = this.readMemoryCache(key, nowMs);
if (memoryHit.fresh) return { fresh: memoryHit.fresh };
staleFallback = memoryHit.stale;
}
if (!useCacheApi) return { stale: staleFallback };
const cacheApiHit = await this.readCacheApi(cacheName, url, cacheApiKey, ttlMs, nowMs, allowStaleOnError);
if (cacheApiHit.fresh) {
if (useMemory) this.writeMemoryCache(key, cacheApiHit.fresh.clone(), cacheApiHit.expiresAt ?? nowMs + ttlMs);
return { fresh: cacheApiHit.fresh };
}
return { stale: staleFallback ?? cacheApiHit.stale };
}
async runNetworkRequestWithFallback(cacheConfig, fetcher, staleFallback) {
try {
return await this.runNetworkRequest(cacheConfig, fetcher);
} catch (err) {
if (staleFallback) return staleFallback;
throw err;
}
}
async runNetworkRequest({ key, cacheName, url, cacheApiKey, ttlMs, useMemory, useCacheApi }, fetcher) {
const response = await fetcher();
if (!response.ok) return response;
const createdAtMs = Date.now();
const expiresAt = this.computeExpiresAt(createdAtMs, ttlMs);
if (useMemory) this.writeMemoryCache(key, response.clone(), expiresAt);
if (useCacheApi) {
const storable = this.toStorableResponse(response.clone(), createdAtMs);
await this.writeCacheApi(cacheName, url, cacheApiKey, storable);
}
return response;
}
computeExpiresAt(createdAtMs, ttlMs) {
if (!Number.isFinite(ttlMs) || ttlMs <= 0) return createdAtMs;
if (ttlMs >= Number.MAX_SAFE_INTEGER - createdAtMs) return Number.MAX_SAFE_INTEGER;
return createdAtMs + ttlMs;
}
normalizeMethod(method) {
return (method || "GET").toUpperCase();
}
resolveBodyKey(body) {
if (body == null) return "";
if (typeof body === "string") return body;
if (body instanceof URLSearchParams) return body.toString();
}
buildDefaultCacheKey(context) {
const method = this.normalizeMethod(context.method);
if (method === "GET") return `${method}:${context.url}`;
const bodyKey = this.resolveBodyKey(context.body);
if (bodyKey === void 0) return void 0;
return `${method}:${context.url}#${fnv1a32ToKeyPart(bodyKey)}`;
}
supportsCacheApi() {
return typeof caches !== "undefined" && typeof caches.open === "function";
}
readCreatedAtMs(response) {
const raw = response.headers.get(RESPONSE_CACHE_CREATED_AT_HEADER);
if (!raw) return null;
const value = Number(raw);
return Number.isFinite(value) ? value : null;
}
ensureVaryByCacheKey(headers) {
const varyRaw = headers.get("vary");
if (!varyRaw) {
headers.set("vary", RESPONSE_CACHE_KEY_HEADER);
return;
}
const varyParts = new Set(varyRaw.split(",").map((part) => part.trim().toLowerCase()));
if (!varyParts.has("*") && !varyParts.has(RESPONSE_CACHE_KEY_HEADER)) headers.set("vary", `${varyRaw}, ${RESPONSE_CACHE_KEY_HEADER}`);
}
toStorableResponse(response, createdAtMs) {
const headers = new Headers(response.headers);
headers.set(RESPONSE_CACHE_CREATED_AT_HEADER, String(createdAtMs));
this.ensureVaryByCacheKey(headers);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers
});
}
readMemoryCache(key, nowMs) {
const entry = this.memoryCache.get(key);
if (!entry) return {};
if (entry.expiresAt > nowMs) {
this.touchMemoryCache(key, entry);
return {
fresh: entry.response.clone(),
expiresAt: entry.expiresAt
};
}
this.memoryCache.delete(key);
return {
stale: entry.response.clone(),
expiresAt: entry.expiresAt
};
}
touchMemoryCache(key, entry) {
this.memoryCache.delete(key);
this.memoryCache.set(key, entry);
}
trimMemoryCache() {
while (this.memoryCache.size > MAX_MEMORY_CACHE_ENTRIES) {
const first = this.memoryCache.keys().next().value;
if (typeof first !== "string") break;
this.memoryCache.delete(first);
}
}
writeMemoryCache(key, response, expiresAt) {
if (this.memoryCache.has(key)) this.memoryCache.delete(key);
this.memoryCache.set(key, {
response,
expiresAt
});
this.trimMemoryCache();
}
async readCacheApi(cacheName, url, cacheKey, ttlMs, nowMs, allowStaleOnError) {
try {
const request = new Request(url, {
method: "GET",
headers: { [RESPONSE_CACHE_KEY_HEADER]: cacheKey }
});
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
if (!cached) return {};
const createdAtMs = this.readCreatedAtMs(cached);
if (createdAtMs === null) {
await cache.delete(request);
return {};
}
const expiresAt = this.computeExpiresAt(createdAtMs, ttlMs);
if (expiresAt > nowMs) return {
fresh: cached.clone(),
expiresAt
};
if (!allowStaleOnError) {
await cache.delete(request);
return {};
}
const stale = cached.clone();
await cache.delete(request);
return {
stale,
expiresAt
};
} catch {
return {};
}
}
async writeCacheApi(cacheName, url, cacheKey, response) {
try {
const request = new Request(url, {
method: "GET",
headers: { [RESPONSE_CACHE_KEY_HEADER]: cacheKey }
});
await (await caches.open(cacheName)).put(request, response);
} catch {}
}
};
var responseCacheManager = new ResponseCacheManager();
async function executeWithResponseCache(context, options, fetcher) {
return responseCacheManager.execute(context, options, fetcher);
}
//#endregion
//#region src/utils/errors.ts
/**
* Small error helpers used across the project.
*/
function stringifyUnknownObject(value) {
const seen = /* @__PURE__ */ new WeakSet();
try {
return JSON.stringify(value, (_key, currentValue) => {
if (typeof currentValue !== "object" || currentValue === null) return currentValue;
if (seen.has(currentValue)) return "[Circular]";
seen.add(currentValue);
return currentValue;
}) ?? null;
} catch {
return null;
}
}
function extractNestedMessage(error) {
const candidates = [
error?.data?.message,
error?.error?.message,
error?.message
];
for (const candidate of candidates) if (typeof candidate === "string" && candidate) return candidate;
return null;
}
function formatObjectError(error, fallback) {
const nestedMessage = extractNestedMessage(error);
if (nestedMessage) return nestedMessage;
const serialized = stringifyUnknownObject(error);
if (serialized && serialized !== "{}") return serialized;
const ctorName = error.constructor?.name;
return ctorName ? `[${ctorName}]` : fallback;
}
function formatPrimitiveError(error, fallback) {
if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") return `${error}`;
if (typeof error === "symbol") return error.description ? `Symbol(${error.description})` : "Symbol";
if (typeof error === "function") return error.name ? `[Function ${error.name}]` : "[Function]";
return fallback;
}
function toErrorMessage(error, fallback = "Unknown error") {
if (error instanceof Error) return error.message || fallback;
if (typeof error === "string") return error || fallback;
if (error === null || error === void 0) return fallback;
if (typeof error === "object") return formatObjectError(error, fallback);
return formatPrimitiveError(error, fallback);
}
/**
* Extracts a human-readable error message from various error shapes.
*/
function getErrorMessage(error) {
return toErrorMessage(error, "");
}
function isAbortError(err) {
const anyErr = err;
return typeof DOMException !== "undefined" && anyErr instanceof DOMException && anyErr.name === "AbortError" || anyErr instanceof Error && anyErr.name === "AbortError" || anyErr?.message === "AbortError";
}
/**
* Creates a canonical AbortError instance. Prefer DOMException when available.
*
* Note: This is intentionally not coupled to AbortSignal.reason to avoid
* surfacing string/opaque abort reasons as user-facing "errors".
*/
function makeAbortError(message = "Aborted") {
try {
return new DOMException(message, "AbortError");
} catch {
const err = new Error(message);
err.name = "AbortError";
return err;
}
}
//#endregion
//#region src/utils/abort.ts
var NEVER_ABORTED_SIGNAL = new AbortController().signal;
/**
* Throws a canonical AbortError if the provided signal is aborted.
*
* Runtimes that implement `AbortSignal.throwIfAborted()` throw `signal.reason`,
* which can be *any* value. We normalize cancellation to a standard
* `AbortError` so callers can reliably use `isAbortError()`.
*/
function throwIfAborted(signal) {
const maybeThrow = signal.throwIfAborted;
if (typeof maybeThrow === "function") try {
maybeThrow.call(signal);
return;
} catch (e) {
if (signal.aborted || isAbortError(e)) throw makeAbortError();
throw e instanceof Error ? e : new Error(String(e));
}
if (signal.aborted) throw makeAbortError();
}
/**
* Creates an AbortSignal that auto-aborts after `timeoutMs`.
*
* If an `external` signal is provided, the returned signal is aborted when
* *either* external aborts or the timeout elapses.
*/
function createTimeoutSignal(timeoutMs, external) {
if (!(Number.isFinite(timeoutMs) && timeoutMs > 0)) return {
signal: external ?? NEVER_ABORTED_SIGNAL,
cleanup: () => {}
};
const controller = new AbortController();
let timeoutId;
const onExternalAbort = () => {
if (timeoutId !== void 0) {
clearTimeout(timeoutId);
timeoutId = void 0;
}
controller.abort(external?.reason);
};
if (external) {
external.addEventListener("abort", onExternalAbort, { once: true });
if (external.aborted) onExternalAbort();
}
if (!controller.signal.aborted) timeoutId = setTimeout(() => {
controller.abort(makeAbortError("Timeout"));
timeoutId = void 0;
}, timeoutMs);
return {
signal: controller.signal,
cleanup: () => {
if (timeoutId !== void 0) {
clearTimeout(timeoutId);
timeoutId = void 0;
}
external?.removeEventListener("abort", onExternalAbort);
}
};
}
var browserInfo = (/* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
(function(e, t) {
"object" == typeof exports && "object" == typeof module ? module.exports = t() : "function" == typeof define && define.amd ? define([], t) : "object" == typeof exports ? exports.bowser = t() : e.bowser = t();
})(exports, (function() {
return function(e) {
var t = {};
function r(i) {
if (t[i]) return t[i].exports;
var n = t[i] = {
i,
l: !1,
exports: {}
};
return e[i].call(n.exports, n, n.exports, r), n.l = !0, n.exports;
}
return r.m = e, r.c = t, r.d = function(e, t, i) {
r.o(e, t) || Object.defineProperty(e, t, {
enumerable: !0,
get: i
});
}, r.r = function(e) {
"undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(e, "__esModule", { value: !0 });
}, r.t = function(e, t) {
if (1 & t && (e = r(e)), 8 & t) return e;
if (4 & t && "object" == typeof e && e && e.__esModule) return e;
var i = Object.create(null);
if (r.r(i), Object.defineProperty(i, "default", {
enumerable: !0,
value: e
}), 2 & t && "string" != typeof e) for (var n in e) r.d(i, n, function(t) {
return e[t];
}.bind(null, n));
return i;
}, r.n = function(e) {
var t = e && e.__esModule ? function() {
return e.default;
} : function() {
return e;
};
return r.d(t, "a", t), t;
}, r.o = function(e, t) {
return Object.prototype.hasOwnProperty.call(e, t);
}, r.p = "", r(r.s = 90);
}({
17: function(e, t, r) {
"use strict";
t.__esModule = !0, t.default = void 0;
var i = r(18);
t.default = function() {
function e() {}
return e.getFirstMatch = function(e, t) {
var r = t.match(e);
return r && r.length > 0 && r[1] || "";
}, e.getSecondMatch = function(e, t) {
var r = t.match(e);
return r && r.length > 1 && r[2] || "";
}, e.matchAndReturnConst = function(e, t, r) {
if (e.test(t)) return r;
}, e.getWindowsVersionName = function(e) {
switch (e) {
case "NT": return "NT";
case "XP": return "XP";
case "NT 5.0": return "2000";
case "NT 5.1": return "XP";
case "NT 5.2": return "2003";
case "NT 6.0": return "Vista";
case "NT 6.1": return "7";
case "NT 6.2": return "8";
case "NT 6.3": return "8.1";
case "NT 10.0": return "10";
default: return;
}
}, e.getMacOSVersionName = function(e) {
var t = e.split(".").splice(0, 2).map((function(e) {
return parseInt(e, 10) || 0;
}));
t.push(0);
var r = t[0], i = t[1];
if (10 === r) switch (i) {
case 5: return "Leopard";
case 6: return "Snow Leopard";
case 7: return "Lion";
case 8: return "Mountain Lion";
case 9: return "Mavericks";
case 10: return "Yosemite";
case 11: return "El Capitan";
case 12: return "Sierra";
case 13: return "High Sierra";
case 14: return "Mojave";
case 15: return "Catalina";
default: return;
}
switch (r) {
case 11: return "Big Sur";
case 12: return "Monterey";
case 13: return "Ventura";
case 14: return "Sonoma";
case 15: return "Sequoia";
default: return;
}
}, e.getAndroidVersionName = function(e) {
var t = e.split(".").splice(0, 2).map((function(e) {
return parseInt(e, 10) || 0;
}));
if (t.push(0), !(1 === t[0] && t[1] < 5)) return 1 === t[0] && t[1] < 6 ? "Cupcake" : 1 === t[0] && t[1] >= 6 ? "Donut" : 2 === t[0] && t[1] < 2 ? "Eclair" : 2 === t[0] && 2 === t[1] ? "Froyo" : 2 === t[0] && t[1] > 2 ? "Gingerbread" : 3 === t[0] ? "Honeycomb" : 4 === t[0] && t[1] < 1 ? "Ice Cream Sandwich" : 4 === t[0] && t[1] < 4 ? "Jelly Bean" : 4 === t[0] && t[1] >= 4 ? "KitKat" : 5 === t[0] ? "Lollipop" : 6 === t[0] ? "Marshmallow" : 7 === t[0] ? "Nougat" : 8 === t[0] ? "Oreo" : 9 === t[0] ? "Pie" : void 0;
}, e.getVersionPrecision = function(e) {
return e.split(".").length;
}, e.compareVersions = function(t, r, i) {
void 0 === i && (i = !1);
var n = e.getVersionPrecision(t), a = e.getVersionPrecision(r), o = Math.max(n, a), s = 0, u = e.map([t, r], (function(t) {
var r = o - e.getVersionPrecision(t), i = t + new Array(r + 1).join(".0");
return e.map(i.split("."), (function(e) {
return new Array(20 - e.length).join("0") + e;
})).reverse();
}));
for (i && (s = o - Math.min(n, a)), o -= 1; o >= s;) {
if (u[0][o] > u[1][o]) return 1;
if (u[0][o] === u[1][o]) {
if (o === s) return 0;
o -= 1;
} else if (u[0][o] < u[1][o]) return -1;
}
}, e.map = function(e, t) {
var r, i = [];
if (Array.prototype.map) return Array.prototype.map.call(e, t);
for (r = 0; r < e.length; r += 1) i.push(t(e[r]));
return i;
}, e.find = function(e, t) {
var r, i;
if (Array.prototype.find) return Array.prototype.find.call(e, t);
for (r = 0, i = e.length; r < i; r += 1) {
var n = e[r];
if (t(n, r)) return n;
}
}, e.assign = function(e) {
for (var t, r, i = e, n = arguments.length, a = new Array(n > 1 ? n - 1 : 0), o = 1; o < n; o++) a[o - 1] = arguments[o];
if (Object.assign) return Object.assign.apply(Object, [e].concat(a));
var s = function() {
var e = a[t];
"object" == typeof e && null !== e && Object.keys(e).forEach((function(t) {
i[t] = e[t];
}));
};
for (t = 0, r = a.length; t < r; t += 1) s();
return e;
}, e.getBrowserAlias = function(e) {
return i.BROWSER_ALIASES_MAP[e];
}, e.getBrowserTypeByAlias = function(e) {
return i.BROWSER_MAP[e] || "";
}, e;
}(), e.exports = t.default;
},
18: function(e, t, r) {
"use strict";
t.__esModule = !0, t.ENGINE_MAP = t.OS_MAP = t.PLATFORMS_MAP = t.BROWSER_MAP = t.BROWSER_ALIASES_MAP = void 0;
t.BROWSER_ALIASES_MAP = {
AmazonBot: "amazonbot",
"Amazon Silk": "amazon_silk",
"Android Browser": "android",
BaiduSpider: "baiduspider",
Bada: "bada",
BingCrawler: "bingcrawler",
Brave: "brave",
BlackBerry: "blackberry",
"ChatGPT-User": "chatgpt_user",
Chrome: "chrome",
ClaudeBot: "claudebot",
Chromium: "chromium",
Diffbot: "diffbot",
DuckDuckBot: "duckduckbot",
DuckDuckGo: "duckduckgo",
Electron: "electron",
Epiphany: "epiphany",
FacebookExternalHit: "facebookexternalhit",
Firefox: "firefox",
Focus: "focus",
Generic: "generic",
"Google Search": "google_search",
Googlebot: "googlebot",
GPTBot: "gptbot",
"Internet Explorer": "ie",
InternetArchiveCrawler: "internetarchivecrawler",
"K-Meleon": "k_meleon",
LibreWolf: "librewolf",
Linespider: "linespider",
Maxthon: "maxthon",
"Meta-ExternalAds": "meta_externalads",
"Meta-ExternalAgent": "meta_externalagent",
"Meta-ExternalFetcher": "meta_externalfetcher",
"Meta-WebIndexer": "meta_webindexer",
"Microsoft Edge": "edge",
"MZ Browser": "mz",
"NAVER Whale Browser": "naver",
"OAI-SearchBot": "oai_searchbot",
Omgilibot: "omgilibot",
Opera: "opera",
"Opera Coast": "opera_coast",
"Pale Moon": "pale_moon",
PerplexityBot: "perplexitybot",
"Perplexity-User": "perplexity_user",
PhantomJS: "phantomjs",
PingdomBot: "pingdombot",
Puffin: "puffin",
QQ: "qq",
QQLite: "qqlite",
QupZilla: "qupzilla",
Roku: "roku",
Safari: "safari",
Sailfish: "sailfish",
"Samsung Internet for Android": "samsung_internet",
SlackBot: "slackbot",
SeaMonkey: "seamonkey",
Sleipnir: "sleipnir",
"Sogou Browser": "sogou",
Swing: "swing",
Tizen: "tizen",
"UC Browser": "uc",
Vivaldi: "vivaldi",
"WebOS Browser": "webos",
WeChat: "wechat",
YahooSlurp: "yahooslurp",
"Yandex Browser": "yandex",
YandexBot: "yandexbot",
YouBot: "youbot"
};
t.BROWSER_MAP = {
amazonbot: "AmazonBot",
amazon_silk: "Amazon Silk",
android: "Android Browser",
baiduspider: "BaiduSpider",
bada: "Bada",
bingcrawler: "BingCrawler",
blackberry: "BlackBerry",
brave: "Brave",
chatgpt_user: "ChatGPT-User",
chrome: "Chrome",
claudebot: "ClaudeBot",
chromium: "Chromium",
diffbot: "Diffbot",
duckduckbot: "DuckDuckBot",
duckduckgo: "DuckDuckGo",
edge: "Microsoft Edge",
electron: "Electron",
epiphany: "Epiphany",
facebookexternalhit: "FacebookExternalHit",
firefox: "Firefox",
focus: "Focus",
generic: "Generic",
google_search: "Google Search",
googlebot: "Googlebot",
gptbot: "GPTBot",
ie: "Internet Explorer",
internetarchivecrawler: "InternetArchiveCrawler",
k_meleon: "K-Meleon",
librewolf: "LibreWolf",
linespider: "Linespider",
maxthon: "Maxthon",
meta_externalads: "Meta-ExternalAds",
meta_externalagent: "Meta-ExternalAgent",
meta_externalfetcher: "Meta-ExternalFetcher",
meta_webindexer: "Meta-WebIndexer",
mz: "MZ Browser",
naver: "NAVER Whale Browser",
oai_searchbot: "OAI-SearchBot",
omgilibot: "Omgilibot",
opera: "Opera",
opera_coast: "Opera Coast",
pale_moon: "Pale Moon",
perplexitybot: "PerplexityBot",
perplexity_user: "Perplexity-User",
phantomjs: "PhantomJS",
pingdombot: "PingdomBot",
puffin: "Puffin",
qq: "QQ Browser",
qqlite: "QQ Browser Lite",
qupzilla: "QupZilla",
roku: "Roku",
safari: "Safari",
sailfish: "Sailfish",
samsung_internet: "Samsung Internet for Android",
seamonkey: "SeaMonkey",
slackbot: "SlackBot",
sleipnir: "Sleipnir",
sogou: "Sogou Browser",
swing: "Swing",
tizen: "Tizen",
uc: "UC Browser",
vivaldi: "Vivaldi",
webos: "WebOS Browser",
wechat: "WeChat",
yahooslurp: "YahooSlurp",
yandex: "Yandex Browser",
yandexbot: "YandexBot",
youbot: "YouBot"
};
t.PLATFORMS_MAP = {
bot: "bot",
desktop: "desktop",
mobile: "mobile",
tablet: "tablet",
tv: "tv"
};
t.OS_MAP = {
Android: "Android",
Bada: "Bada",
BlackBerry: "BlackBerry",
ChromeOS: "Chrome OS",
HarmonyOS: "HarmonyOS",
iOS: "iOS",
Linux: "Linux",
MacOS: "macOS",
PlayStation4: "PlayStation 4",
Roku: "Roku",
Tizen: "Tizen",
WebOS: "WebOS",
Windows: "Windows",
WindowsPhone: "Windows Phone"
};
t.ENGINE_MAP = {
Blink: "Blink",
EdgeHTML: "EdgeHTML",
Gecko: "Gecko",
Presto: "Presto",
Trident: "Trident",
WebKit: "WebKit"
};
},
90: function(e, t, r) {
"use strict";
t.__esModule = !0, t.default = void 0;
var i, n = (i = r(91)) && i.__esModule ? i : { default: i }, a = r(18);
function o(e, t) {
for (var r = 0; r < t.length; r++) {
var i = t[r];
i.enumerable = i.enumerable || !1, i.configurable = !0, "value" in i && (i.writable = !0), Object.defineProperty(e, i.key, i);
}
}
t.default = function() {
function e() {}
var t, r, i;
return e.getParser = function(e, t, r) {
if (void 0 === t && (t = !1), void 0 === r && (r = null), "string" != typeof e) throw new Error("UserAgent should be a string");
return new n.default(e, t, r);
}, e.parse = function(e, t) {
return void 0 === t && (t = null), new n.default(e, t).getResult();
}, t = e, i = [
{
key: "BROWSER_MAP",
get: function() {
return a.BROWSER_MAP;
}
},
{
key: "ENGINE_MAP",
get: function() {
return a.ENGINE_MAP;
}
},
{
key: "OS_MAP",
get: function() {
return a.OS_MAP;
}
},
{
key: "PLATFORMS_MAP",
get: function() {
return a.PLATFORMS_MAP;
}
}
], (r = null) && o(t.prototype, r), i && o(t, i), e;
}(), e.exports = t.default;
},
91: function(e, t, r) {
"use strict";
t.__esModule = !0, t.default = void 0;
var i = u(r(92)), n = u(r(93)), a = u(r(94)), o = u(r(95)), s = u(r(17));
function u(e) {
return e && e.__esModule ? e : { default: e };
}
t.default = function() {
function e(e, t, r) {
if (void 0 === t && (t = !1), void 0 === r && (r = null), null == e || "" === e) throw new Error("UserAgent parameter can't be empty");
this._ua = e;
var i = !1;
"boolean" == typeof t ? (i = t, this._hints = r) : this._hints = null != t && "object" == typeof t ? t : null, this.parsedResult = {}, !0 !== i && this.parse();
}
var t = e.prototype;
return t.getHints = function() {
return this._hints;
}, t.hasBrand = function(e) {
if (!this._hints || !Array.isArray(this._hints.brands)) return !1;
var t = e.toLowerCase();
return this._hints.brands.some((function(e) {
return e.brand && e.brand.toLowerCase() === t;
}));
}, t.getBrandVersion = function(e) {
if (this._hints && Array.isArray(this._hints.brands)) {
var t = e.toLowerCase(), r = this._hints.brands.find((function(e) {
return e.brand && e.brand.toLowerCase() === t;
}));
return r ? r.version : void 0;
}
}, t.getUA = function() {
return this._ua;
}, t.test = function(e) {
return e.test(this._ua);
}, t.parseBrowser = function() {
var e = this;
this.parsedResult.browser = {};
var t = s.default.find(i.default, (function(t) {
if ("function" == typeof t.test) return t.test(e);
if (Array.isArray(t.test)) return t.test.some((function(t) {
return e.test(t);
}));
throw new Error("Browser's test function is not valid");
}));
return t && (this.parsedResult.browser = t.describe(this.getUA(), this)), this.parsedResult.browser;
}, t.getBrowser = function() {
return this.parsedResult.browser ? this.parsedResult.browser : this.parseBrowser();
}, t.getBrowserName = function(e) {
return e ? String(this.getBrowser().name).toLowerCase() || "" : this.getBrowser().name || "";
}, t.getBrowserVersion = function() {
return this.getBrowser().version;
}, t.getOS = function() {
return this.parsedResult.os ? this.parsedResult.os : this.parseOS();
}, t.parseOS = function() {
var e = this;
this.parsedResult.os = {};
var t = s.default.find(n.default, (function(t) {
if ("function" == typeof t.test) return t.test(e);
if (Array.isArray(t.test)) return t.test.some((function(t) {
return e.test(t);
}));
throw new Error("Browser's test function is not valid");
}));
return t && (this.parsedResult.os = t.describe(this.getUA())), this.parsedResult.os;
}, t.getOSName = function(e) {
var t = this.getOS().name;
return e ? String(t).toLowerCase() || "" : t || "";
}, t.getOSVersion = function() {
return this.getOS().version;
}, t.getPlatform = function() {
return this.parsedResult.platform ? this.parsedResult.platform : this.parsePlatform();
}, t.getPlatformType = function(e) {
void 0 === e && (e = !1);
var t = this.getPlatform().type;
return e ? String(t).toLowerCase() || "" : t || "";
}, t.parsePlatform = function() {
var e = this;
this.parsedResult.platform = {};
var t = s.default.find(a.default, (function(t) {
if ("function" == typeof t.test) return t.test(e);
if (Array.isArray(t.test)) return t.test.some((function(t) {
return e.test(t);
}));
throw new Error("Browser's test function is not valid");
}));
return t && (this.parsedResult.platform = t.describe(this.getUA())), this.parsedResult.platform;
}, t.getEngine = function() {
return this.parsedResult.engine ? this.parsedResult.engine : this.parseEngine();
}, t.getEngineName = function(e) {
return e ? String(this.getEngine().name).toLowerCase() || "" : this.getEngine().name || "";
}, t.parseEngine = function() {
var e = this;
this.parsedResult.engine = {};
var t = s.default.find(o.default, (function(t) {
if ("function" == typeof t.test) return t.test(e);
if (Array.isArray(t.test)) return t.test.some((function(t) {
return e.test(t);
}));
throw new Error("Browser's test function is not valid");
}));
return t && (this.parsedResult.engine = t.describe(this.getUA())), this.parsedResult.engine;
}, t.parse = function() {
return this.parseBrowser(), this.parseOS(), this.parsePlatform(), this.parseEngine(), this;
}, t.getResult = function() {
return s.default.assign({}, this.parsedResult);
}, t.satisfies = function(e) {
var t = this, r = {}, i = 0, n = {}, a = 0;
if (Object.keys(e).forEach((function(t) {
var o = e[t];
"string" == typeof o ? (n[t] = o, a += 1) : "object" == typeof o && (r[t] = o, i += 1);
})), i > 0) {
var o = Object.keys(r), u = s.default.find(o, (function(e) {
return t.isOS(e);
}));
if (u) {
var d = this.satisfies(r[u]);
if (void 0 !== d) return d;
}
var c = s.default.find(o, (function(e) {
return t.isPlatform(e);
}));
if (c) {
var f = this.satisfies(r[c]);
if (void 0 !== f) return f;
}
}
if (a > 0) {
var l = Object.keys(n), b = s.default.find(l, (function(e) {
return t.isBrowser(e, !0);
}));
if (void 0 !== b) return this.compareVersion(n[b]);
}
}, t.isBrowser = function(e, t) {
void 0 === t && (t = !1);
var r = this.getBrowserName().toLowerCase(), i = e.toLowerCase(), n = s.default.getBrowserTypeByAlias(i);
return t && n && (i = n.toLowerCase()), i === r;
}, t.compareVersion = function(e) {
var t = [0], r = e, i = !1, n = this.getBrowserVersion();
if ("string" == typeof n) return ">" === e[0] || "<" === e[0] ? (r = e.substr(1), "=" === e[1] ? (i = !0, r = e.substr(2)) : t = [], ">" === e[0] ? t.push(1) : t.push(-1)) : "=" === e[0] ? r = e.substr(1) : "~" === e[0] && (i = !0, r = e.substr(1)), t.indexOf(s.default.compareVersions(n, r, i)) > -1;
}, t.isOS = function(e) {
return this.getOSName(!0) === String(e).toLowerCase();
}, t.isPlatform = function(e) {
return this.getPlatformType(!0) === String(e).toLowerCase();
}, t.isEngine = function(e) {
return this.getEngineName(!0) === String(e).toLowerCase();
}, t.is = function(e, t) {
return void 0 === t && (t = !1), this.isBrowser(e, t) || this.isOS(e) || this.isPlatform(e);
}, t.some = function(e) {
var t = this;
return void 0 === e && (e = []), e.some((function(e) {
return t.is(e);
}));
}, e;
}(), e.exports = t.default;
},
92: function(e, t, r) {
"use strict";
t.__esModule = !0, t.default = void 0;
var i, n = (i = r(17)) && i.__esModule ? i : { default: i };
var a = /version\/(\d+(\.?_?\d+)+)/i;
t.default = [
{
test: [/gptbot/i],
describe: function(e) {
var t = { name: "GPTBot" }, r = n.default.getFirstMatch(/gptbot\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/chatgpt-user/i],
describe: function(e) {
var t = { name: "ChatGPT-User" }, r = n.default.getFirstMatch(/chatgpt-user\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/oai-searchbot/i],
describe: function(e) {
var t = { name: "OAI-SearchBot" }, r = n.default.getFirstMatch(/oai-searchbot\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [
/claudebot/i,
/claude-web/i,
/claude-user/i,
/claude-searchbot/i
],
describe: function(e) {
var t = { name: "ClaudeBot" }, r = n.default.getFirstMatch(/(?:claudebot|claude-web|claude-user|claude-searchbot)\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/omgilibot/i, /webzio-extended/i],
describe: function(e) {
var t = { name: "Omgilibot" }, r = n.default.getFirstMatch(/(?:omgilibot|webzio-extended)\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/diffbot/i],
describe: function(e) {
var t = { name: "Diffbot" }, r = n.default.getFirstMatch(/diffbot\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/perplexitybot/i],
describe: function(e) {
var t = { name: "PerplexityBot" }, r = n.default.getFirstMatch(/perplexitybot\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/perplexity-user/i],
describe: function(e) {
var t = { name: "Perplexity-User" }, r = n.default.getFirstMatch(/perplexity-user\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/youbot/i],
describe: function(e) {
var t = { name: "YouBot" }, r = n.default.getFirstMatch(/youbot\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/meta-webindexer/i],
describe: function(e) {
var t = { name: "Meta-WebIndexer" }, r = n.default.getFirstMatch(/meta-webindexer\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/meta-externalads/i],
describe: function(e) {
var t = { name: "Meta-ExternalAds" }, r = n.default.getFirstMatch(/meta-externalads\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/meta-externalagent/i],
describe: function(e) {
var t = { name: "Meta-ExternalAgent" }, r = n.default.getFirstMatch(/meta-externalagent\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/meta-externalfetcher/i],
describe: function(e) {
var t = { name: "Meta-ExternalFetcher" }, r = n.default.getFirstMatch(/meta-externalfetcher\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/googlebot/i],
describe: function(e) {
var t = { name: "Googlebot" }, r = n.default.getFirstMatch(/googlebot\/(\d+(\.\d+))/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/linespider/i],
describe: function(e) {
var t = { name: "Linespider" }, r = n.default.getFirstMatch(/(?:linespider)(?:-[-\w]+)?[\s/](\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/amazonbot/i],
describe: function(e) {
var t = { name: "AmazonBot" }, r = n.default.getFirstMatch(/amazonbot\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/bingbot/i],
describe: function(e) {
var t = { name: "BingCrawler" }, r = n.default.getFirstMatch(/bingbot\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/baiduspider/i],
describe: function(e) {
var t = { name: "BaiduSpider" }, r = n.default.getFirstMatch(/baiduspider\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/duckduckbot/i],
describe: function(e) {
var t = { name: "DuckDuckBot" }, r = n.default.getFirstMatch(/duckduckbot\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/ia_archiver/i],
describe: function(e) {
var t = { name: "InternetArchiveCrawler" }, r = n.default.getFirstMatch(/ia_archiver\/(\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/facebookexternalhit/i, /facebookcatalog/i],
describe: function() {
return { name: "FacebookExternalHit" };
}
},
{
test: [/slackbot/i, /slack-imgProxy/i],
describe: function(e) {
var t = { name: "SlackBot" }, r = n.default.getFirstMatch(/(?:slackbot|slack-imgproxy)(?:-[-\w]+)?[\s/](\d+(\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/yahoo!?[\s/]*slurp/i],
describe: function() {
return { name: "YahooSlurp" };
}
},
{
test: [/yandexbot/i, /yandexmobilebot/i],
describe: function() {
return { name: "YandexBot" };
}
},
{
test: [/pingdom/i],
describe: function() {
return { name: "PingdomBot" };
}
},
{
test: [/opera/i],
describe: function(e) {
var t = { name: "Opera" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:opera)[\s/](\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/opr\/|opios/i],
describe: function(e) {
var t = { name: "Opera" }, r = n.default.getFirstMatch(/(?:opr|opios)[\s/](\S+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/SamsungBrowser/i],
describe: function(e) {
var t = { name: "Samsung Internet for Android" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:SamsungBrowser)[\s/](\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/Whale/i],
describe: function(e) {
var t = { name: "NAVER Whale Browser" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:whale)[\s/](\d+(?:\.\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/PaleMoon/i],
describe: function(e) {
var t = { name: "Pale Moon" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:PaleMoon)[\s/](\d+(?:\.\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/MZBrowser/i],
describe: function(e) {
var t = { name: "MZ Browser" }, r = n.default.getFirstMatch(/(?:MZBrowser)[\s/](\d+(?:\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/focus/i],
describe: function(e) {
var t = { name: "Focus" }, r = n.default.getFirstMatch(/(?:focus)[\s/](\d+(?:\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/swing/i],
describe: function(e) {
var t = { name: "Swing" }, r = n.default.getFirstMatch(/(?:swing)[\s/](\d+(?:\.\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/coast/i],
describe: function(e) {
var t = { name: "Opera Coast" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:coast)[\s/](\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/opt\/\d+(?:.?_?\d+)+/i],
describe: function(e) {
var t = { name: "Opera Touch" }, r = n.default.getFirstMatch(/(?:opt)[\s/](\d+(\.?_?\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/yabrowser/i],
describe: function(e) {
var t = { name: "Yandex Browser" }, r = n.default.getFirstMatch(/(?:yabrowser)[\s/](\d+(\.?_?\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/ucbrowser/i],
describe: function(e) {
var t = { name: "UC Browser" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:ucbrowser)[\s/](\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/Maxthon|mxios/i],
describe: function(e) {
var t = { name: "Maxthon" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:Maxthon|mxios)[\s/](\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/epiphany/i],
describe: function(e) {
var t = { name: "Epiphany" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:epiphany)[\s/](\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/puffin/i],
describe: function(e) {
var t = { name: "Puffin" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:puffin)[\s/](\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/sleipnir/i],
describe: function(e) {
var t = { name: "Sleipnir" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:sleipnir)[\s/](\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/k-meleon/i],
describe: function(e) {
var t = { name: "K-Meleon" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/(?:k-meleon)[\s/](\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/micromessenger/i],
describe: function(e) {
var t = { name: "WeChat" }, r = n.default.getFirstMatch(/(?:micromessenger)[\s/](\d+(\.?_?\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/qqbrowser/i],
describe: function(e) {
var t = { name: /qqbrowserlite/i.test(e) ? "QQ Browser Lite" : "QQ Browser" }, r = n.default.getFirstMatch(/(?:qqbrowserlite|qqbrowser)[/](\d+(\.?_?\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/msie|trident/i],
describe: function(e) {
var t = { name: "Internet Explorer" }, r = n.default.getFirstMatch(/(?:msie |rv:)(\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/\sedg\//i],
describe: function(e) {
var t = { name: "Microsoft Edge" }, r = n.default.getFirstMatch(/\sedg\/(\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/edg([ea]|ios)/i],
describe: function(e) {
var t = { name: "Microsoft Edge" }, r = n.default.getSecondMatch(/edg([ea]|ios)\/(\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/vivaldi/i],
describe: function(e) {
var t = { name: "Vivaldi" }, r = n.default.getFirstMatch(/vivaldi\/(\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/seamonkey/i],
describe: function(e) {
var t = { name: "SeaMonkey" }, r = n.default.getFirstMatch(/seamonkey\/(\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/sailfish/i],
describe: function(e) {
var t = { name: "Sailfish" }, r = n.default.getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/silk/i],
describe: function(e) {
var t = { name: "Amazon Silk" }, r = n.default.getFirstMatch(/silk\/(\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/phantom/i],
describe: function(e) {
var t = { name: "PhantomJS" }, r = n.default.getFirstMatch(/phantomjs\/(\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/slimerjs/i],
describe: function(e) {
var t = { name: "SlimerJS" }, r = n.default.getFirstMatch(/slimerjs\/(\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/blackberry|\bbb\d+/i, /rim\stablet/i],
describe: function(e) {
var t = { name: "BlackBerry" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/blackberry[\d]+\/(\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/(web|hpw)[o0]s/i],
describe: function(e) {
var t = { name: "WebOS Browser" }, r = n.default.getFirstMatch(a, e) || n.default.getFirstMatch(/w(?:eb)?[o0]sbrowser\/(\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/bada/i],
describe: function(e) {
var t = { name: "Bada" }, r = n.default.getFirstMatch(/dolfin\/(\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/tizen/i],
describe: function(e) {
var t = { name: "Tizen" }, r = n.default.getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.?_?\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/qupzilla/i],
describe: function(e) {
var t = { name: "QupZilla" }, r = n.default.getFirstMatch(/(?:qupzilla)[\s/](\d+(\.?_?\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/librewolf/i],
describe: function(e) {
var t = { name: "LibreWolf" }, r = n.default.getFirstMatch(/(?:librewolf)[\s/](\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/firefox|iceweasel|fxios/i],
describe: function(e) {
var t = { name: "Firefox" }, r = n.default.getFirstMatch(/(?:firefox|iceweasel|fxios)[\s/](\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/electron/i],
describe: function(e) {
var t = { name: "Electron" }, r = n.default.getFirstMatch(/(?:electron)\/(\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [
/sogoumobilebrowser/i,
/metasr/i,
/se 2\.[x]/i
],
describe: function(e) {
var t = { name: "Sogou Browser" }, r = n.default.getFirstMatch(/(?:sogoumobilebrowser)[\s/](\d+(\.?_?\d+)+)/i, e), i = n.default.getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.?_?\d+)+)/i, e), a = n.default.getFirstMatch(/se ([\d.]+)x/i, e), o = r || i || a;
return o && (t.version = o), t;
}
},
{
test: [/MiuiBrowser/i],
describe: function(e) {
var t = { name: "Miui" }, r = n.default.getFirstMatch(/(?:MiuiBrowser)[\s/](\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: function(e) {
return !!e.hasBrand("DuckDuckGo") || e.test(/\sDdg\/[\d.]+$/i);
},
describe: function(e, t) {
var r = { name: "DuckDuckGo" };
if (t) {
var i = t.getBrandVersion("DuckDuckGo");
if (i) return r.version = i, r;
}
var a = n.default.getFirstMatch(/\sDdg\/([\d.]+)$/i, e);
return a && (r.version = a), r;
}
},
{
test: function(e) {
return e.hasBrand("Brave");
},
describe: function(e, t) {
var r = { name: "Brave" };
if (t) {
var i = t.getBrandVersion("Brave");
if (i) return r.version = i, r;
}
return r;
}
},
{
test: [/chromium/i],
describe: function(e) {
var t = { name: "Chromium" }, r = n.default.getFirstMatch(/(?:chromium)[\s/](\d+(\.?_?\d+)+)/i, e) || n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/chrome|crios|crmo/i],
describe: function(e) {
var t = { name: "Chrome" }, r = n.default.getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/GSA/i],
describe: function(e) {
var t = { name: "Google Search" }, r = n.default.getFirstMatch(/(?:GSA)\/(\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: function(e) {
var t = !e.test(/like android/i), r = e.test(/android/i);
return t && r;
},
describe: function(e) {
var t = { name: "Android Browser" }, r = n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/playstation 4/i],
describe: function(e) {
var t = { name: "PlayStation 4" }, r = n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/safari|applewebkit/i],
describe: function(e) {
var t = { name: "Safari" }, r = n.default.getFirstMatch(a, e);
return r && (t.version = r), t;
}
},
{
test: [/.*/i],
describe: function(e) {
var t = -1 !== e.search("\\(") ? /^(.*)\/(.*)[ \t]\((.*)/ : /^(.*)\/(.*) /;
return {
name: n.default.getFirstMatch(t, e),
version: n.default.getSecondMatch(t, e)
};
}
}
], e.exports = t.default;
},
93: function(e, t, r) {
"use strict";
t.__esModule = !0, t.default = void 0;
var i, n = (i = r(17)) && i.__esModule ? i : { default: i }, a = r(18);
t.default = [
{
test: [/Roku\/DVP/],
describe: function(e) {
var t = n.default.getFirstMatch(/Roku\/DVP-(\d+\.\d+)/i, e);
return {
name: a.OS_MAP.Roku,
version: t
};
}
},
{
test: [/windows phone/i],
describe: function(e) {
var t = n.default.getFirstMatch(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i, e);
return {
name: a.OS_MAP.WindowsPhone,
version: t
};
}
},
{
test: [/windows /i],
describe: function(e) {
var t = n.default.getFirstMatch(/Windows ((NT|XP)( \d\d?.\d)?)/i, e), r = n.default.getWindowsVersionName(t);
return {
name: a.OS_MAP.Windows,
version: t,
versionName: r
};
}
},
{
test: [/Macintosh(.*?) FxiOS(.*?)\//],
describe: function(e) {
var t = { name: a.OS_MAP.iOS }, r = n.default.getSecondMatch(/(Version\/)(\d[\d.]+)/, e);
return r && (t.version = r), t;
}
},
{
test: [/macintosh/i],
describe: function(e) {
var t = n.default.getFirstMatch(/mac os x (\d+(\.?_?\d+)+)/i, e).replace(/[_\s]/g, "."), r = n.default.getMacOSVersionName(t), i = {
name: a.OS_MAP.MacOS,
version: t
};
return r && (i.versionName = r), i;
}
},
{
test: [/(ipod|iphone|ipad)/i],
describe: function(e) {
var t = n.default.getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i, e).replace(/[_\s]/g, ".");
return {
name: a.OS_MAP.iOS,
version: t
};
}
},
{
test: [/OpenHarmony/i],
describe: function(e) {
var t = n.default.getFirstMatch(/OpenHarmony\s+(\d+(\.\d+)*)/i, e);
return {
name: a.OS_MAP.HarmonyOS,
version: t
};
}
},
{
test: function(e) {
var t = !e.test(/like android/i), r = e.test(/android/i);
return t && r;
},
describe: function(e) {
var t = n.default.getFirstMatch(/android[\s/-](\d+(\.\d+)*)/i, e), r = n.default.getAndroidVersionName(t), i = {
name: a.OS_MAP.Android,
version: t
};
return r && (i.versionName = r), i;
}
},
{
test: [/(web|hpw)[o0]s/i],
describe: function(e) {
var t = n.default.getFirstMatch(/(?:web|hpw)[o0]s\/(\d+(\.\d+)*)/i, e), r = { name: a.OS_MAP.WebOS };
return t && t.length && (r.version = t), r;
}
},
{
test: [/blackberry|\bbb\d+/i, /rim\stablet/i],
describe: function(e) {
var t = n.default.getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i, e) || n.default.getFirstMatch(/blackberry\d+\/(\d+([_\s]\d+)*)/i, e) || n.default.getFirstMatch(/\bbb(\d+)/i, e);
return {
name: a.OS_MAP.BlackBerry,
version: t
};
}
},
{
test: [/bada/i],
describe: function(e) {
var t = n.default.getFirstMatch(/bada\/(\d+(\.\d+)*)/i, e);
return {
name: a.OS_MAP.Bada,
version: t
};
}
},
{
test: [/tizen/i],
describe: function(e) {
var t = n.default.getFirstMatch(/tizen[/\s](\d+(\.\d+)*)/i, e);
return {
name: a.OS_MAP.Tizen,
version: t
};
}
},
{
test: [/linux/i],
describe: function() {
return { name: a.OS_MAP.Linux };
}
},
{
test: [/CrOS/],
describe: function() {
return { name: a.OS_MAP.ChromeOS };
}
},
{
test: [/PlayStation 4/],
describe: function(e) {
var t = n.default.getFirstMatch(/PlayStation 4[/\s](\d+(\.\d+)*)/i, e);
return {
name: a.OS_MAP.PlayStation4,
version: t
};
}
}
], e.exports = t.default;
},
94: function(e, t, r) {
"use strict";
t.__esModule = !0, t.default = void 0;
var i, n = (i = r(17)) && i.__esModule ? i : { default: i }, a = r(18);
t.default = [
{
test: [/googlebot/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Google"
};
}
},
{
test: [/linespider/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Line"
};
}
},
{
test: [/amazonbot/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Amazon"
};
}
},
{
test: [/gptbot/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "OpenAI"
};
}
},
{
test: [/chatgpt-user/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "OpenAI"
};
}
},
{
test: [/oai-searchbot/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "OpenAI"
};
}
},
{
test: [/baiduspider/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Baidu"
};
}
},
{
test: [/bingbot/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Bing"
};
}
},
{
test: [/duckduckbot/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "DuckDuckGo"
};
}
},
{
test: [
/claudebot/i,
/claude-web/i,
/claude-user/i,
/claude-searchbot/i
],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Anthropic"
};
}
},
{
test: [/omgilibot/i, /webzio-extended/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Webz.io"
};
}
},
{
test: [/diffbot/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Diffbot"
};
}
},
{
test: [/perplexitybot/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Perplexity AI"
};
}
},
{
test: [/perplexity-user/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Perplexity AI"
};
}
},
{
test: [/youbot/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "You.com"
};
}
},
{
test: [/ia_archiver/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Internet Archive"
};
}
},
{
test: [/meta-webindexer/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Meta"
};
}
},
{
test: [/meta-externalads/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Meta"
};
}
},
{
test: [/meta-externalagent/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Meta"
};
}
},
{
test: [/meta-externalfetcher/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Meta"
};
}
},
{
test: [/facebookexternalhit/i, /facebookcatalog/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Meta"
};
}
},
{
test: [/slackbot/i, /slack-imgProxy/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Slack"
};
}
},
{
test: [/yahoo/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Yahoo"
};
}
},
{
test: [/yandexbot/i, /yandexmobilebot/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Yandex"
};
}
},
{
test: [/pingdom/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.bot,
vendor: "Pingdom"
};
}
},
{
test: [/huawei/i],
describe: function(e) {
var t = n.default.getFirstMatch(/(can-l01)/i, e) && "Nova", r = {
type: a.PLATFORMS_MAP.mobile,
vendor: "Huawei"
};
return t && (r.model = t), r;
}
},
{
test: [/nexus\s*(?:7|8|9|10).*/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.tablet,
vendor: "Nexus"
};
}
},
{
test: [/ipad/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.tablet,
vendor: "Apple",
model: "iPad"
};
}
},
{
test: [/Macintosh(.*?) FxiOS(.*?)\//],
describe: function() {
return {
type: a.PLATFORMS_MAP.tablet,
vendor: "Apple",
model: "iPad"
};
}
},
{
test: [/kftt build/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.tablet,
vendor: "Amazon",
model: "Kindle Fire HD 7"
};
}
},
{
test: [/silk/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.tablet,
vendor: "Amazon"
};
}
},
{
test: [/tablet(?! pc)/i],
describe: function() {
return { type: a.PLATFORMS_MAP.tablet };
}
},
{
test: function(e) {
var t = e.test(/ipod|iphone/i), r = e.test(/like (ipod|iphone)/i);
return t && !r;
},
describe: function(e) {
var t = n.default.getFirstMatch(/(ipod|iphone)/i, e);
return {
type: a.PLATFORMS_MAP.mobile,
vendor: "Apple",
model: t
};
}
},
{
test: [/nexus\s*[0-6].*/i, /galaxy nexus/i],
describe: function() {
return {
type: a.PLATFORMS_MAP.mobile,
vendor: "Nexus"
};
}
},
{
test: [/Nokia/i],
describe: function(e) {
var t = n.default.getFirstMatch(/Nokia\s+([0-9]+(\.[0-9]+)?)/i, e), r = {
type: a.PLATFORMS_MAP.mobile,
vendor: "Nokia"
};
return t && (r.model = t), r;
}
},
{
test: [/[^-]mobi/i],
describe: function() {
return { type: a.PLATFORMS_MAP.mobile };
}
},
{
test: function(e) {
return "blackberry" === e.getBrowserName(!0);
},
describe: function() {
return {
type: a.PLATFORMS_MAP.mobile,
vendor: "BlackBerry"
};
}
},
{
test: function(e) {
return "bada" === e.getBrowserName(!0);
},
describe: function() {
return { type: a.PLATFORMS_MAP.mobile };
}
},
{
test: function(e) {
return "windows phone" === e.getBrowserName();
},
describe: function() {
return {
type: a.PLATFORMS_MAP.mobile,
vendor: "Microsoft"
};
}
},
{
test: function(e) {
var t = Number(String(e.getOSVersion()).split(".")[0]);
return "android" === e.getOSName(!0) && t >= 3;
},
describe: function() {
return { type: a.PLATFORMS_MAP.tablet };
}
},
{
test: function(e) {
return "android" === e.getOSName(!0);
},
describe: function() {
return { type: a.PLATFORMS_MAP.mobile };
}
},
{
test: [/smart-?tv|smarttv/i],
describe: function() {
return { type: a.PLATFORMS_MAP.tv };
}
},
{
test: [/netcast/i],
describe: function() {
return { type: a.PLATFORMS_MAP.tv };
}
},
{
test: function(e) {
return "macos" === e.getOSName(!0);
},
describe: function() {
return {
type: a.PLATFORMS_MAP.desktop,
vendor: "Apple"
};
}
},
{
test: function(e) {
return "windows" === e.getOSName(!0);
},
describe: function() {
return { type: a.PLATFORMS_MAP.desktop };
}
},
{
test: function(e) {
return "linux" === e.getOSName(!0);
},
describe: function() {
return { type: a.PLATFORMS_MAP.desktop };
}
},
{
test: function(e) {
return "playstation 4" === e.getOSName(!0);
},
describe: function() {
return { type: a.PLATFORMS_MAP.tv };
}
},
{
test: function(e) {
return "roku" === e.getOSName(!0);
},
describe: function() {
return { type: a.PLATFORMS_MAP.tv };
}
}
], e.exports = t.default;
},
95: function(e, t, r) {
"use strict";
t.__esModule = !0, t.default = void 0;
var i, n = (i = r(17)) && i.__esModule ? i : { default: i }, a = r(18);
t.default = [
{
test: function(e) {
return "microsoft edge" === e.getBrowserName(!0);
},
describe: function(e) {
if (/\sedg\//i.test(e)) return { name: a.ENGINE_MAP.Blink };
var t = n.default.getFirstMatch(/edge\/(\d+(\.?_?\d+)+)/i, e);
return {
name: a.ENGINE_MAP.EdgeHTML,
version: t
};
}
},
{
test: [/trident/i],
describe: function(e) {
var t = { name: a.ENGINE_MAP.Trident }, r = n.default.getFirstMatch(/trident\/(\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: function(e) {
return e.test(/presto/i);
},
describe: function(e) {
var t = { name: a.ENGINE_MAP.Presto }, r = n.default.getFirstMatch(/presto\/(\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: function(e) {
var t = e.test(/gecko/i), r = e.test(/like gecko/i);
return t && !r;
},
describe: function(e) {
var t = { name: a.ENGINE_MAP.Gecko }, r = n.default.getFirstMatch(/gecko\/(\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
},
{
test: [/(apple)?webkit\/537\.36/i],
describe: function() {
return { name: a.ENGINE_MAP.Blink };
}
},
{
test: [/(apple)?webkit/i],
describe: function(e) {
var t = { name: a.ENGINE_MAP.WebKit }, r = n.default.getFirstMatch(/webkit\/(\d+(\.?_?\d+)+)/i, e);
return r && (t.version = r), t;
}
}
], e.exports = t.default;
}
});
}));
})))(), 1)).default.getParser(globalThis.navigator.userAgent).getResult();
//#endregion
//#region src/utils/gm.ts
var YANDEX_API_HOST = "api.browser.yandex.ru";
var GOOGLEVIDEO_HOST_SUFFIX = "googlevideo.com";
var HEADER_LINE_RE = /^([\w-]+):\s*(.+)$/;
var URL_SCHEME_RE = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
var scriptHandler = typeof GM_info === "undefined" ? void 0 : GM_info?.scriptHandler;
function getCallbackGmXhr() {
const gmXhr = typeof GM_xmlhttpRequest === "undefined" ? globalThis.GM_xmlhttpRequest : GM_xmlhttpRequest;
return typeof gmXhr === "function" ? gmXhr : void 0;
}
function getPromiseGmXhr() {
const gm = typeof GM === "undefined" ? globalThis.GM : GM;
const gmXhr = gm?.xmlHttpRequest ?? gm?.xmlhttpRequest;
return typeof gmXhr === "function" ? gmXhr.bind(gm) : void 0;
}
function hasSupportedGmXhr() {
return !!(getCallbackGmXhr() || getPromiseGmXhr());
}
var isProxyOnlyExtension = !(typeof IS_EXTENSION !== "undefined" && IS_EXTENSION) && (browserInfo.browser?.name === "Safari" || !["Tampermonkey", "Violentmonkey"].includes(scriptHandler));
var isSupportGM4 = typeof GM !== "undefined" || globalThis.GM !== void 0;
var isSupportGMXhr = hasSupportedGmXhr();
function getRequestHost(url) {
const normalizedUrl = url.trim();
try {
return new URL(normalizedUrl).hostname.toLowerCase();
} catch {
if (!URL_SCHEME_RE.test(normalizedUrl)) try {
return new URL(`https://${normalizedUrl}`).hostname.toLowerCase();
} catch {}
return;
}
}
function isHostOrSubdomain(host, targetHost) {
return host === targetHost || host.endsWith(`.${targetHost}`);
}
function shouldUseGmXhr(host, url, forceGmXhr = false) {
if (forceGmXhr) return true;
if (!host) {
const lowerUrl = url.toLowerCase();
return lowerUrl.includes(YANDEX_API_HOST) || lowerUrl.includes(GOOGLEVIDEO_HOST_SUFFIX);
}
return isHostOrSubdomain(host, YANDEX_API_HOST) || isHostOrSubdomain(host, GOOGLEVIDEO_HOST_SUFFIX);
}
function toRequestUrl(url) {
if (typeof url === "string") return url;
if (url instanceof URL) return url.href;
return url.url;
}
function resolveRequestMethod(url, method) {
if (method) return method.toUpperCase();
if (url instanceof Request) return (url.method || "GET").toUpperCase();
return "GET";
}
function parseResponseHeaders(rawHeaders) {
if (typeof rawHeaders !== "string" || rawHeaders.length === 0) return {};
return rawHeaders.split(/\r?\n/).reduce((acc, line) => {
const headerMatch = HEADER_LINE_RE.exec(line);
if (!headerMatch) return acc;
const [, key, value] = headerMatch;
acc[key] = value;
return acc;
}, {});
}
function getGmXhrErrorMessage(error) {
const maybeError = error;
if (typeof maybeError?.error === "string" && maybeError.error.trim().length > 0) return maybeError.error;
if (typeof maybeError?.statusText === "string" && maybeError.statusText.trim().length > 0 && maybeError.statusText !== "\"\"" && maybeError.statusText !== "''") return maybeError.statusText;
return getErrorMessage(error) || "Unknown GM XHR error";
}
function buildResponse(resp, urlStr) {
const status = resp.status;
const statusText = typeof resp.statusText === "string" ? resp.statusText : "";
const body = resp.response instanceof Blob ? resp.response : null;
const responseHeaders = parseResponseHeaders(resp.responseHeaders);
const response = new Response(body, {
status,
statusText,
headers: responseHeaders
});
Object.defineProperty(response, "url", { value: resp.finalUrl ?? urlStr });
return response;
}
async function executeCallbackGmXhr(gmXhr, urlStr, timeout, fetchOptions, method, headers) {
return new Promise((resolve, reject) => {
let settled = false;
let onAbort;
const cleanupAbort = () => {
if (onAbort) fetchOptions.signal?.removeEventListener("abort", onAbort);
};
const failOnce = (error) => {
if (settled) return;
settled = true;
cleanupAbort();
reject(error);
};
const redirectMode = fetchOptions.redirect;
const request = gmXhr({
method,
url: urlStr,
responseType: "blob",
data: fetchOptions.body,
timeout,
headers,
...redirectMode && { redirect: redirectMode },
onload: (resp) => {
if (settled) return;
settled = true;
cleanupAbort();
try {
const response = buildResponse(resp, urlStr);
debug.log("[GM_fetch] GM_xmlhttpRequest completed", {
url: response.url,
method,
status: response.status,
statusText: response.statusText
});
resolve(response);
} catch (buildErr) {
debug.warn("[GM_fetch] GM_xmlhttpRequest response build failed", {
url: urlStr,
method,
error: getErrorMessage(buildErr),
rawStatus: resp.status,
rawStatusText: resp.statusText
});
reject(buildErr instanceof Error ? buildErr : new Error(getErrorMessage(buildErr)));
}
},
ontimeout: () => {
debug.warn("[GM_fetch] GM_xmlhttpRequest timed out", {
url: urlStr,
method,
timeout
});
failOnce(/* @__PURE__ */ new Error("Timeout"));
},
onerror: (error) => {
const message = getGmXhrErrorMessage(error);
debug.warn("[GM_fetch] GM_xmlhttpRequest failed", {
url: urlStr,
method,
error: message
});
failOnce(new Error(message));
},
onabort: () => {
debug.warn("[GM_fetch] GM_xmlhttpRequest aborted", {
url: urlStr,
method
});
failOnce(makeAbortError());
}
});
onAbort = () => {
try {
request?.abort?.();
} catch {}
failOnce(makeAbortError());
};
if (fetchOptions.signal) {
fetchOptions.signal.addEventListener("abort", onAbort, { once: true });
if (fetchOptions.signal.aborted) {
onAbort();
return;
}
}
});
}
async function executePromiseGmXhr(gmXhr, urlStr, timeout, fetchOptions, method, headers) {
const redirectMode = fetchOptions.redirect;
const request = gmXhr({
method,
url: urlStr,
responseType: "blob",
data: fetchOptions.body,
timeout,
headers,
...redirectMode && { redirect: redirectMode }
});
let abortHandler;
try {
const abortPromise = new Promise((_, reject) => {
if (!fetchOptions.signal) return;
abortHandler = () => {
try {
request.abort?.();
} catch {}
reject(makeAbortError());
};
fetchOptions.signal.addEventListener("abort", abortHandler, { once: true });
if (fetchOptions.signal.aborted) abortHandler();
});
const response = buildResponse(await Promise.race([request, abortPromise]), urlStr);
debug.log("[GM_fetch] GM.xmlHttpRequest completed", {
url: response.url,
method,
status: response.status,
statusText: response.statusText
});
return response;
} finally {
if (abortHandler) fetchOptions.signal?.removeEventListener("abort", abortHandler);
}
}
async function gmXhrFetch(urlStr, timeout, fetchOptions) {
const headers = getHeaders(fetchOptions.headers);
const method = (fetchOptions.method || "GET").toUpperCase();
debug.log("[GM_fetch] GM_xmlhttpRequest start", {
url: urlStr,
method,
timeout,
headerCount: Object.keys(headers).length
});
const callbackGmXhr = getCallbackGmXhr();
if (callbackGmXhr) {
debug.log("[GM_fetch] attempting callback-style GM_xmlhttpRequest");
try {
return await executeCallbackGmXhr(callbackGmXhr, urlStr, timeout, fetchOptions, method, headers);
} catch (error) {
if (isAbortError(error)) throw error;
debug.warn("[GM_fetch] callback-style GM_xmlhttpRequest failed", {
url: urlStr,
method,
error: getGmXhrErrorMessage(error)
});
}
}
const promiseGmXhr = getPromiseGmXhr();
if (promiseGmXhr) {
debug.log("[GM_fetch] attempting promise-style GM.xmlHttpRequest");
try {
return await executePromiseGmXhr(promiseGmXhr, urlStr, timeout, fetchOptions, method, headers);
} catch (error) {
if (isAbortError(error)) throw error;
debug.warn("[GM_fetch] promise-style GM.xmlHttpRequest failed", {
url: urlStr,
method,
error: getGmXhrErrorMessage(error)
});
}
}
debug.warn("[GM_fetch] none of the GM approaches worked");
throw new Error("All GM approaches failed");
}
async function GM_fetch(url, opts = {}) {
const { timeout = 15e3, forceGmXhr = false, responseCache, ...fetchOptions } = opts;
const urlStr = toRequestUrl(url);
const host = getRequestHost(urlStr);
const method = resolveRequestMethod(url, fetchOptions.method);
const useGmXhr = shouldUseGmXhr(host, urlStr, forceGmXhr);
debug.log("[GM_fetch] request", {
url: urlStr,
method,
host: host ?? "unknown",
timeout,
transport: useGmXhr ? "GM_xmlhttpRequest" : "fetch",
forced: forceGmXhr,
responseCache: responseCache ? {
ttlMs: responseCache.ttlMs,
key: responseCache.key ?? null,
useMemory: responseCache.useMemory ?? true,
useCacheApi: responseCache.useCacheApi ?? true,
dedupe: responseCache.dedupe ?? true
} : null
});
const performRequest = async () => {
if (useGmXhr) {
debug.log("[GM_fetch] using GM_xmlhttpRequest transport", {
url: urlStr,
method,
host: host ?? "unknown",
reason: forceGmXhr ? "forced" : "host-policy"
});
try {
return await gmXhrFetch(urlStr, timeout, fetchOptions);
} catch (err) {
if (isAbortError(err)) throw err;
if (forceGmXhr || shouldUseGmXhr(host, urlStr)) throw err;
debug.warn("[GM_fetch] all GM approaches failed, falling back to native fetch", {
url: urlStr,
method,
host: host ?? "unknown",
error: getErrorMessage(err) || "Unknown error"
});
const { signal, cleanup } = createTimeoutSignal(timeout, fetchOptions.signal);
try {
return await fetch(url, {
...fetchOptions,
signal
});
} finally {
cleanup();
}
}
}
const { signal, cleanup } = createTimeoutSignal(timeout, fetchOptions.signal);
try {
return await fetch(url, {
...fetchOptions,
signal
});
} catch (err) {
if (signal.aborted || isAbortError(err)) throw err;
debug.warn("[GM_fetch] fetch failed, retrying via GM_xmlhttpRequest", {
url: urlStr,
method,
host: host ?? "unknown",
error: getErrorMessage(err) || "Unknown error"
});
return await gmXhrFetch(urlStr, timeout, fetchOptions);
} finally {
cleanup();
}
};
if (!responseCache) return await performRequest();
return await executeWithResponseCache({
url: urlStr,
method,
body: fetchOptions.body
}, responseCache, performRequest);
}
//#endregion
//#region src/utils/storage.ts
var compatRules = Object.entries({
numToBool: [
["autoTranslate"],
["dontTranslateYourLang", "enabledDontTranslateLanguages"],
["autoSetVolumeYandexStyle", "enabledAutoVolume"],
["showVideoSlider"],
["syncVolume"],
["downloadWithName"],
["sendNotifyOnComplete"],
["highlightWords"],
["onlyBypassMediaCSP"],
["newAudioPlayer"],
["showPiPButton"],
["translateAPIErrors"],
["audioBooster"],
["useNewModel", "useLivelyVoice"]
],
number: [["autoVolume"]],
array: [["dontTranslateLanguage", "dontTranslateLanguages"]],
string: [
["hotkeyButton", "translationHotkey"],
["locale-lang-override", "localeLangOverride"],
["locale-lang", "localeLang"]
]
}).flatMap(([category, entries]) => entries.map(([oldKey, maybeNewKey]) => ({
category,
oldKey,
newKey: maybeNewKey ?? oldKey,
shouldDeleteOldKey: Boolean(maybeNewKey)
})));
var compatRuleByOldKey = new Map(compatRules.map((rule) => [rule.oldKey, rule]));
var compatKeysToRead = Array.from(new Set(compatRules.map((rule) => rule.oldKey)));
function createUndefinedDefaults(keys) {
const defaults = {};
for (const key of keys) defaults[key] = void 0;
return defaults;
}
function isCompatValue(category, value) {
switch (category) {
case "numToBool":
case "number": return typeof value === "number";
case "array": return Array.isArray(value);
case "string": return typeof value === "string" || value === null;
default: return false;
}
}
function convertByCompatCategory(category, value) {
switch (category) {
case "string":
case "array":
case "number": return value;
default: return !!value;
}
}
function normalizeCompatValue(rule, value) {
let convertedValue = convertByCompatCategory(rule.category, value);
if (rule.oldKey === "autoVolume" && typeof value === "number" && value < 1) convertedValue = Math.round(value * 100);
return convertedValue;
}
function areStorageValuesEqual(a, b) {
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, index) => Object.is(item, b[index]));
return Object.is(a, b);
}
function parseStoredValue(rawValue) {
if (rawValue === null) return;
try {
return JSON.parse(rawValue);
} catch {
return;
}
}
async function updateConfig(data) {
if (data.compatVersion === "2025-05-09") return data;
const keysToRead = new Set([...Object.keys(data), ...compatKeysToRead]);
const persistedValues = await votStorage.getValues(createUndefinedDefaults(keysToRead));
const newData = { ...data };
const writeOperations = [];
const deleteOperations = [];
for (const [key, storedValue] of Object.entries(persistedValues)) {
if (storedValue === void 0) continue;
const compatRule = compatRuleByOldKey.get(key);
if (!compatRule || !isCompatValue(compatRule.category, storedValue)) continue;
const convertedValue = normalizeCompatValue(compatRule, storedValue);
newData[compatRule.newKey] = convertedValue;
const existingNewValue = persistedValues[compatRule.newKey];
if (compatRule.shouldDeleteOldKey || !areStorageValuesEqual(existingNewValue, convertedValue)) writeOperations.push(votStorage.set(compatRule.newKey, convertedValue));
if (compatRule.shouldDeleteOldKey) deleteOperations.push(votStorage.delete(compatRule.oldKey));
}
await Promise.all([...writeOperations, ...deleteOperations]);
return {
...newData,
compatVersion: actualCompatVersion
};
}
var VOTStorage = class {
support = null;
localStorageListeners = /* @__PURE__ */ new Map();
shouldUseSyntheticListeners(support) {
return !support.promiseAddValueChangeListener && !support.legacyAddValueChangeListener;
}
getGMRuntime() {
if (typeof GM !== "undefined") return GM;
return globalThis.GM;
}
resolveSupport() {
if (this.support) return this.support;
const gm = this.getGMRuntime();
const support = {
legacyGet: typeof GM_getValue === "function",
legacySet: typeof GM_setValue === "function",
legacyDelete: typeof GM_deleteValue === "function",
legacyList: typeof GM_listValues === "function",
legacyAddValueChangeListener: typeof globalThis.GM_addValueChangeListener === "function",
legacyRemoveValueChangeListener: typeof globalThis.GM_removeValueChangeListener === "function",
promiseGet: isSupportGM4 && typeof gm?.getValue === "function",
promiseGetValues: isSupportGM4 && typeof gm?.getValues === "function",
promiseSet: isSupportGM4 && typeof gm?.setValue === "function",
promiseDelete: isSupportGM4 && typeof gm?.deleteValue === "function",
promiseList: isSupportGM4 && typeof gm?.listValues === "function",
promiseAddValueChangeListener: isSupportGM4 && typeof gm?.addValueChangeListener === "function",
promiseRemoveValueChangeListener: isSupportGM4 && typeof gm?.removeValueChangeListener === "function"
};
this.support = support;
debug.log(`[VOT Storage] GM Promises: ${support.promiseGet} | GM legacy: ${support.legacyGet}`);
return support;
}
/**
* Check if storage type is LocalStorage
*/
get isSupportOnlyLS() {
const support = this.resolveSupport();
return !support.legacyGet && !support.legacySet && !support.legacyDelete && !support.legacyList && !support.promiseGet && !support.promiseGetValues && !support.promiseSet && !support.promiseDelete && !support.promiseList;
}
syncGetByName(name, def, support) {
if (support.legacyGet) return GM_getValue(name, def);
const val = globalThis.localStorage.getItem(name);
if (val === null) return def;
try {
return JSON.parse(val);
} catch {
return def;
}
}
async getRaw(name, def) {
const support = this.resolveSupport();
if (support.promiseGet && GM.getValue) return await GM.getValue(name, def);
return this.syncGetByName(name, def, support);
}
async get(name, def) {
return this.getRaw(name, def);
}
async getValues(data) {
const support = this.resolveSupport();
if (support.promiseGetValues && GM.getValues) return await GM.getValues(data);
const entries = Object.entries(data);
if (support.promiseGet && GM.getValue) {
const values = await Promise.all(entries.map(async ([key, value]) => {
return [key, await GM.getValue(key, value)];
}));
return Object.fromEntries(values);
}
return Object.fromEntries(entries.map(([key, value]) => [key, this.syncGetByName(key, value, support)]));
}
syncSetByName(name, value, support) {
if (support.legacySet) return GM_setValue(name, value);
return globalThis.localStorage.setItem(name, JSON.stringify(value));
}
async setRaw(name, value) {
const support = this.resolveSupport();
const storageKey = name;
const shouldNotify = this.shouldUseSyntheticListeners(support);
const oldValue = shouldNotify ? await this.getRaw(name) : void 0;
if (support.promiseSet && GM.setValue) {
await GM.setValue(name, value);
if (shouldNotify) this.notifyLocalStorageListeners(storageKey, oldValue, value, false);
return;
}
const setResult = this.syncSetByName(name, value, support);
this.notifyLocalStorageListeners(storageKey, oldValue, value, false);
return setResult;
}
async set(name, value) {
return this.setRaw(name, value);
}
syncDeleteByName(name, support) {
if (support.legacyDelete) return GM_deleteValue(name);
return globalThis.localStorage.removeItem(name);
}
async deleteRaw(name) {
const support = this.resolveSupport();
const storageKey = name;
const shouldNotify = this.shouldUseSyntheticListeners(support);
const oldValue = shouldNotify ? await this.getRaw(name) : void 0;
if (support.promiseDelete && GM.deleteValue) {
await GM.deleteValue(name);
if (shouldNotify) this.notifyLocalStorageListeners(storageKey, oldValue, void 0, false);
return;
}
const deleteResult = this.syncDeleteByName(name, support);
this.notifyLocalStorageListeners(storageKey, oldValue, void 0, false);
return deleteResult;
}
async delete(name) {
return this.deleteRaw(name);
}
addValueChangeListener(name, listener) {
const support = this.resolveSupport();
const gm = this.getGMRuntime();
if (support.promiseAddValueChangeListener) {
const addListener = gm?.addValueChangeListener;
const removeListener = support.promiseRemoveValueChangeListener ? gm?.removeValueChangeListener : void 0;
if (typeof addListener === "function") {
const listenerId = addListener(name, this.createTypedListener(listener));
return () => {
if (typeof removeListener === "function") removeListener(listenerId);
};
}
}
if (support.legacyAddValueChangeListener) {
const addListener = globalThis.GM_addValueChangeListener;
const removeListener = support.legacyRemoveValueChangeListener ? globalThis.GM_removeValueChangeListener : void 0;
if (typeof addListener === "function") {
const listenerId = addListener(name, this.createTypedListener(listener));
return () => {
if (typeof removeListener === "function") removeListener(listenerId);
};
}
}
const listeners = this.getLocalStorageListeners(name);
const typedListener = listener;
listeners.add(typedListener);
const onStorage = (event) => {
if (event.storageArea !== globalThis.localStorage || event.key !== name) return;
typedListener(name, parseStoredValue(event.oldValue), parseStoredValue(event.newValue), true);
};
globalThis.addEventListener("storage", onStorage);
return () => {
listeners.delete(typedListener);
if (listeners.size === 0) this.localStorageListeners.delete(name);
globalThis.removeEventListener("storage", onStorage);
};
}
createTypedListener(listener) {
return (key, oldValue, newValue, remote) => {
listener(key, oldValue, newValue, remote);
};
}
getLocalStorageListeners(name) {
const existing = this.localStorageListeners.get(name);
if (existing) return existing;
const created = /* @__PURE__ */ new Set();
this.localStorageListeners.set(name, created);
return created;
}
notifyLocalStorageListeners(name, oldValue, newValue, remote) {
const listeners = this.localStorageListeners.get(name);
if (!listeners || listeners.size === 0) return;
for (const listener of listeners) listener(name, oldValue, newValue, remote);
}
syncList(support) {
if (support.legacyList) return GM_listValues();
return storageKeys;
}
async list() {
const support = this.resolveSupport();
if (support.promiseList && GM.listValues) return await GM.listValues();
return this.syncList(support);
}
};
var VOT_STORAGE_GLOBAL_KEY = "__VOT_STORAGE_SINGLETON__";
var votStorage = (() => {
const scope = globalThis;
const existing = scope[VOT_STORAGE_GLOBAL_KEY];
if (existing instanceof VOTStorage) return existing;
const created = new VOTStorage();
scope[VOT_STORAGE_GLOBAL_KEY] = created;
return created;
})();
//#endregion
//#region src/core/authRefreshMessage.ts
var AUTH_REFRESH_MESSAGE_SOURCE = "vot-auth";
var AUTH_REFRESH_MESSAGE_TYPE = "account-updated";
function createAuthRefreshMessage() {
return {
source: AUTH_REFRESH_MESSAGE_SOURCE,
type: AUTH_REFRESH_MESSAGE_TYPE
};
}
function isAuthRefreshMessage(value) {
if (!value || typeof value !== "object") return false;
const candidate = value;
return candidate.source === "vot-auth" && candidate.type === "account-updated";
}
function notifyAuthOpener(target = globalThis.opener) {
if (!target || typeof target.postMessage !== "function") return;
target.postMessage(createAuthRefreshMessage(), "*");
}
//#endregion
//#region src/core/auth.ts
function getProfilePayload() {
const payload = globalThis._userData;
if (!payload || typeof payload !== "object") return null;
const candidate = payload;
if (typeof candidate.avatar_id !== "string" || typeof candidate.username !== "string" || candidate.avatar_id.length === 0 || candidate.username.length === 0) return null;
return {
avatar_id: candidate.avatar_id,
username: candidate.username
};
}
async function handleAuthCallbackPage() {
const { access_token: token, expires_in: expiresIn } = Object.fromEntries(new URLSearchParams(globalThis.location.hash.slice(1)));
if (!token || !expiresIn) throw new Error("[VOT] Invalid token response");
const numExpiresIn = Number.parseInt(expiresIn, 10);
if (Number.isNaN(numExpiresIn)) throw new TypeError("[VOT] Invalid expires_in value");
await votStorage.set("account", {
token,
expires: Date.now() + numExpiresIn * 1e3,
username: void 0,
avatarId: void 0
});
notifyAuthOpener();
}
async function handleProfilePage() {
const payload = getProfilePayload();
if (!payload) throw new Error("[VOT] Invalid user data");
const { avatar_id: avatarId, username } = payload;
const data = await votStorage.get("account");
if (!data) throw new Error("[VOT] No account data found");
await votStorage.set("account", {
...data,
username,
avatarId
});
notifyAuthOpener();
}
async function initAuth() {
if (globalThis.location.pathname === "/auth/callback") return handleAuthCallbackPage();
if (globalThis.location.pathname === "/my/profile") return handleProfilePage();
}
var en_default = {
recommended: "recommended",
translateVideo: "Translate video",
disableTranslate: "Turn off",
translationSettings: "Translation settings",
subtitlesSettings: "Subtitles settings",
subtitlesSmartLayout: "Smart subtitle layout",
resetSettings: "Reset settings",
videoBeingTranslated: "The video is being translated",
videoLanguage: "Video language",
translationLanguage: "Translation language",
translationTake: "The translation will take",
translationTakeMoreThanHour: "The translation will take more than an hour",
translationTakeAboutMinute: "The translation will take about a minute",
translationTakeFewMinutes: "The translation will take a few minutes",
translationTakeApproximatelyMinutes: "The translation will take approximately {0} minutes",
translationTakeApproximatelyMinute: "The translation will take approximately {0} minutes",
requestTranslationFailed: "Failed to request video translation",
audioNotReceived: "Audio link not received",
VOTFailedDownloadAudio: "Failed to download audio",
audioFormatNotSupported: "The audio format is not supported",
VOTAutoTranslate: "Translate on open",
VOTAutoSubtitles: "Subtitles on open",
VOTDontTranslateYourLang: "Don't translate from my language",
VOTVolume: "Video volume:",
VOTVolumeTranslation: "Translation volume:",
VOTAutoSetVolume: "Reduce video volume to",
VOTShowVideoSlider: "Video volume slider",
VOTSyncVolume: "Link translation and video volume",
VOTDisableFromYourLang: "You have disabled the translation of the video in your language",
VOTVideoIsTooLong: "Video is too long",
VOTNoVideoIDFound: "No video ID found",
VOTSubtitles: "Subtitles",
VOTSubtitlesDisabled: "Disabled",
VOTDefaultSubtitlesLanguage: "Default subtitle language",
VOTOriginalVideoLanguage: "Original video language",
VOTSubtitlesMaxLength: "Subtitles max length",
VOTHighlightWords: "Highlight words",
VOTTranslatedFrom: "translated from",
VOTAutogenerated: "autogenerated",
VOTSettings: "VOT Settings",
VOTMenuLanguage: "Menu language",
VOTAuthors: "Authors",
VOTVersion: "Version",
VOTLoader: "Loader",
VOTBrowser: "Browser",
VOTShowPiPButton: "Show PiP button",
langs: {
"auto": "Auto",
"af": "Afrikaans",
"ak": "Akan",
"sq": "Albanian",
"am": "Amharic",
"ar": "Arabic",
"hy": "Armenian",
"as": "Assamese",
"ay": "Aymara",
"az": "Azerbaijani",
"bn": "Bangla",
"eu": "Basque",
"be": "Belarusian",
"bho": "Bhojpuri",
"bs": "Bosnian",
"bg": "Bulgarian",
"my": "Burmese",
"ca": "Catalan",
"ceb": "Cebuano",
"zh": "Chinese",
"zh-Hans": "Chinese (Simplified)",
"zh-Hant": "Chinese (Traditional)",
"co": "Corsican",
"hr": "Croatian",
"cs": "Czech",
"da": "Danish",
"dv": "Divehi",
"nl": "Dutch",
"en": "English",
"eo": "Esperanto",
"et": "Estonian",
"ee": "Ewe",
"fil": "Filipino",
"fi": "Finnish",
"fr": "French",
"gl": "Galician",
"lg": "Ganda",
"ka": "Georgian",
"de": "German",
"el": "Greek",
"gn": "Guarani",
"gu": "Gujarati",
"ht": "Haitian Creole",
"ha": "Hausa",
"haw": "Hawaiian",
"iw": "Hebrew",
"hi": "Hindi",
"hmn": "Hmong",
"hu": "Hungarian",
"is": "Icelandic",
"ig": "Igbo",
"id": "Indonesian",
"ga": "Irish",
"it": "Italian",
"ja": "Japanese",
"jv": "Javanese",
"kn": "Kannada",
"kk": "Kazakh",
"km": "Khmer",
"rw": "Kinyarwanda",
"ko": "Korean",
"kri": "Krio",
"ku": "Kurdish",
"ky": "Kyrgyz",
"lo": "Lao",
"la": "Latin",
"lv": "Latvian",
"ln": "Lingala",
"lt": "Lithuanian",
"lb": "Luxembourgish",
"mk": "Macedonian",
"mg": "Malagasy",
"ms": "Malay",
"ml": "Malayalam",
"mt": "Maltese",
"mi": "Māori",
"mr": "Marathi",
"mn": "Mongolian",
"ne": "Nepali",
"nso": "Northern Sotho",
"no": "Norwegian",
"ny": "Nyanja",
"or": "Odia",
"om": "Oromo",
"ps": "Pashto",
"fa": "Persian",
"pl": "Polish",
"pt": "Portuguese",
"pa": "Punjabi",
"qu": "Quechua",
"ro": "Romanian",
"ru": "Russian",
"sm": "Samoan",
"sa": "Sanskrit",
"gd": "Scottish Gaelic",
"sr": "Serbian",
"sn": "Shona",
"sd": "Sindhi",
"si": "Sinhala",
"sk": "Slovak",
"sl": "Slovenian",
"so": "Somali",
"st": "Southern Sotho",
"es": "Spanish",
"su": "Sundanese",
"sw": "Swahili",
"sv": "Swedish",
"tg": "Tajik",
"ta": "Tamil",
"tt": "Tatar",
"te": "Telugu",
"th": "Thai",
"ti": "Tigrinya",
"ts": "Tsonga",
"tr": "Turkish",
"tk": "Turkmen",
"uk": "Ukrainian",
"ur": "Urdu",
"ug": "Uyghur",
"uz": "Uzbek",
"vi": "Vietnamese",
"cy": "Welsh",
"fy": "Western Frisian",
"xh": "Xhosa",
"yi": "Yiddish",
"yo": "Yoruba",
"zu": "Zulu"
},
streamNoConnectionToServer: "There is no connection to the server",
searchField: "Search...",
VOTTranslateAPIErrors: "Translate errors from the API",
VOTDetectService: "Language detection service",
VOTProxyWorkerHost: "Enter the proxy worker address",
VOTM3u8ProxyHost: "Enter the address of the m3u8 proxy worker",
proxySettings: "Proxy Settings",
translationTakeApproximatelyMinute2: "The translation will take approximately {0} minutes",
VOTAudioBooster: "Extended translation volume increase",
VOTSubtitlesDesign: "Subtitles design",
VOTSubtitlesFont: "Subtitle font",
VOTSubtitlesFontSize: "Font size of subtitles",
VOTSubtitlesOpacity: "Transparency of the subtitle background",
VOTSubtitlesDownloadFormat: "The format for downloading subtitles",
VOTDownloadWithName: "Download files with the video name",
VOTUpdateLocaleFiles: "Update localization files",
VOTLocaleHash: "Locale hash",
VOTUpdatedAt: "Updated at",
VOTNeedWebAudioAPI: "To enable this, you must have a Web Audio API",
VOTMediaCSPEnabledOnSite: "Media CSP is enabled on this site",
VOTOnlyBypassMediaCSP: "Use it only for bypassing Media CSP",
VOTNewAudioPlayer: "Use the new audio player",
VOTUseNewModel: "Use an experimental variation of Yandex voices for some videos",
TranslationDelayed: "The translation is slightly delayed",
VOTTranslationCompletedNotify: "The translation on the {0} has been completed!",
VOTSendNotifyOnComplete: "Send a notification that the video has been translated",
VOTBugReport: "Report a bug",
VOTTranslateProxyDisabled: "Disabled",
VOTTranslateProxyEnabled: "Enabled",
VOTTranslateProxyEverything: "Proxy everything",
VOTTranslateProxyStatus: "Proxying mode",
VOTTranslatedBy: "Translated by {0}",
VOTStreamNotAvailable: "Translate stream isn't available",
VOTTranslationTextService: "Text translation service",
VOTNotAffectToVoice: "Doesn't affect the translation of text in voice over",
DontTranslateSelectedLanguages: "Don't translate from selected languages",
showVideoVolumeSlider: "Display the video volume slider",
hotkeysSettings: "Hotkeys settings",
None: "None",
VOTUseLivelyVoice: "Use lively voices. Speakers sound like native Russians.",
miscSettings: "Misc settings",
services: {
"yandexbrowser": "Yandex Browser",
"msedge": "Microsoft Edge",
"rust-server": "Rust Server"
},
aboutExtension: "About extension",
appearance: "Appearance",
buttonPosition: "Button position in the player",
position: {
"left": "Left",
"right": "Right",
"top": "Top",
"default": "Default"
},
secs: "secs",
autoHideButtonDelay: "Delay before hiding the translate button",
notFound: "not found",
minButtonPositionContainer: "The button position only changes in players larger than 600 pixels.",
VOTTranslateProxyStatusDefault: "Completely disabling proxying in your country may break the extension",
PressTheKeyCombination: "Press the key combination...",
VOTUseAudioDownload: "Use audio download",
VOTUseAudioDownloadWarning: "Disabling audio downloads may affect the functionality of the extension",
VOTAccountRequired: "You need to log in to use this feature",
VOTMyAccount: "My account",
VOTLogin: "Login",
VOTLogout: "Logout",
VOTRefresh: "Refresh",
VOTYandexToken: "Enter the Yandex OAuth Token",
VOTYandexTokenInfo: "You can manually set the account token in this field. Please note that we don't check its validity before sending a translate request",
VOTLoginViaToken: "Login via token",
smartDucking: "Adaptive volume"
};
//#endregion
//#region src/localization/localizationProvider.ts
var LOCALE_STORAGE_KEYS = [
"localePhrases",
"localeLang",
"localeHash",
"localeVersion",
"localeUpdatedAt",
"localeLangOverride"
];
var DEFAULT_LOCALE = toFlatObj(en_default);
var repoBranch = "master";
var availableLocales = (() => {
const locales = Array.isArray([
"auto",
"en",
"ru",
"af",
"am",
"ar",
"az",
"bg",
"bn",
"bs",
"ca",
"cs",
"cy",
"da",
"de",
"el",
"es",
"et",
"eu",
"fa",
"fi",
"fr",
"gl",
"hi",
"hr",
"hu",
"hy",
"id",
"it",
"ja",
"jv",
"kk",
"km",
"kn",
"ko",
"lo",
"mk",
"ml",
"mn",
"ms",
"mt",
"my",
"ne",
"nl",
"pa",
"pl",
"pt",
"ro",
"si",
"sk",
"sl",
"sq",
"sr",
"su",
"sv",
"sw",
"tr",
"uk",
"ur",
"uz",
"vi",
"zh",
"zu"
]) ? [
"auto",
"en",
"ru",
"af",
"am",
"ar",
"az",
"bg",
"bn",
"bs",
"ca",
"cs",
"cy",
"da",
"de",
"el",
"es",
"et",
"eu",
"fa",
"fi",
"fr",
"gl",
"hi",
"hr",
"hu",
"hy",
"id",
"it",
"ja",
"jv",
"kk",
"km",
"kn",
"ko",
"lo",
"mk",
"ml",
"mn",
"ms",
"mt",
"my",
"ne",
"nl",
"pa",
"pl",
"pt",
"ro",
"si",
"sk",
"sl",
"sq",
"sr",
"su",
"sv",
"sw",
"tr",
"uk",
"ur",
"uz",
"vi",
"zh",
"zu"
] : ["en"];
return locales.includes("auto") ? locales : ["auto", ...locales];
})();
function resolveRuntimeLocaleVersion(buildVersion, scriptVersion) {
return buildVersion || scriptVersion || "unknown";
}
function getRuntimeLocaleVersion() {
return resolveRuntimeLocaleVersion(String("1.11.5.7"), typeof GM_info !== "undefined" ? String(GM_info?.script?.version || "") : "");
}
var LocalizationProvider = class {
/**
* Language used before page was reloaded
*/
lang;
/**
* Locale phrases with current language
*/
locale;
defaultLocale = DEFAULT_LOCALE;
localesUrl = `${contentUrl}/${repoBranch}/src/localization/locales`;
hashesUrl = `${contentUrl}/${repoBranch}/src/localization/hashes.json`;
warnedMissingKeys = /* @__PURE__ */ new Set();
_langOverride = "auto";
constructor() {
this.lang = this.getLang();
this.locale = {};
}
async init() {
const [langOverride, phrases] = await Promise.all([votStorage.get("localeLangOverride", "auto"), votStorage.get("localePhrases", "")]);
this._langOverride = langOverride;
this.lang = this.getLang();
this.setLocaleFromJsonString(phrases);
return this;
}
get langOverride() {
return this._langOverride;
}
getLang() {
return this.langOverride !== "auto" ? this.langOverride : lang;
}
getAvailableLangs() {
return [...availableLocales];
}
async reset() {
await Promise.all(LOCALE_STORAGE_KEYS.map((key) => votStorage.delete(key)));
return this;
}
buildUrl(baseUrl, path = "", force = false) {
return `${baseUrl}${path}${force ? `?timestamp=${getTimestamp()}` : ""}`;
}
async changeLang(newLang) {
if (this.langOverride === newLang) return false;
await votStorage.set("localeLangOverride", newLang);
this._langOverride = newLang;
this.lang = this.getLang();
await this.update(true);
return true;
}
async checkUpdates(force = false) {
debug.log("Check locale updates...");
try {
const runtimeLocaleVersion = getRuntimeLocaleVersion();
if (!force) {
const storedLocaleVersion = await votStorage.get("localeVersion", "");
if (runtimeLocaleVersion !== "unknown" && storedLocaleVersion === runtimeLocaleVersion) return false;
}
const res = await GM_fetch(this.buildUrl(this.hashesUrl, "", force));
if (!res.ok) throw res.status;
const hashes = await res.json();
if (!hashes || typeof hashes !== "object") throw new Error("Invalid locale hashes payload");
const nextHash = hashes[this.lang];
if (typeof nextHash !== "string" || !nextHash) return false;
return await votStorage.get("localeHash", "") === nextHash ? false : nextHash;
} catch (err) {
console.error("[VOT] [localizationProvider] Failed to get locales hash:", err);
return null;
}
}
async update(force = false) {
const runtimeLocaleVersion = getRuntimeLocaleVersion();
const storedLocaleVersion = await votStorage.get("localeVersion", "");
const hash = await this.checkUpdates(force);
if (hash === null) return this;
if (!hash) {
if (storedLocaleVersion !== runtimeLocaleVersion) await votStorage.set("localeVersion", runtimeLocaleVersion);
return this;
}
const timestamp = getTimestamp();
debug.log("Updating locale...");
try {
const res = await GM_fetch(this.buildUrl(this.localesUrl, `/${this.lang}.json`, force));
if (!res.ok) throw res.status;
const text = await res.text();
this.setLocaleFromJsonString(text);
await Promise.all([
votStorage.set("localePhrases", text),
votStorage.set("localeHash", hash),
votStorage.set("localeLang", this.lang),
votStorage.set("localeVersion", runtimeLocaleVersion),
votStorage.set("localeUpdatedAt", timestamp)
]);
} catch (err) {
console.error("[VOT] [localizationProvider] Failed to get locale:", err);
this.setLocaleFromJsonString(await votStorage.get("localePhrases", ""));
}
return this;
}
setLocaleFromJsonString(json) {
const trimmed = json.trim();
if (!trimmed) {
this.locale = {};
this.warnedMissingKeys.clear();
return this;
}
try {
const locale = JSON.parse(trimmed);
if (!locale || typeof locale !== "object" || Array.isArray(locale)) throw new Error("Locale payload should be a JSON object");
this.locale = toFlatObj(locale);
} catch (err) {
console.error("[VOT] [localizationProvider]", err);
this.locale = {};
}
this.warnedMissingKeys.clear();
return this;
}
getFromLocale(locale, key, source = "locale") {
return locale[key] ?? this.warnMissingKey(locale, key, source);
}
warnMissingKey(locale, key, source) {
const warningKey = `${source}:${key}`;
if (this.warnedMissingKeys.has(warningKey)) return;
this.warnedMissingKeys.add(warningKey);
console.warn("[VOT] [localizationProvider] locale", locale, "doesn't contain key", key);
}
getDefault(key) {
return this.getFromLocale(this.defaultLocale, key, "default") ?? key;
}
get(key) {
return this.getFromLocale(this.locale, key) ?? this.getDefault(key);
}
getLangLabel(lang) {
const key = `langs.${lang}`;
if (key in this.defaultLocale) {
const label = this.get(key);
if (label) return label;
}
return lang.toUpperCase();
}
};
var localizationProvider = new LocalizationProvider();
/**
* In the userscript build, SystemJS wrapping allowed a top-level await.
* For the extension build we bootstrap through loader scripts and keep the
* runtime initialization explicit, so avoid top-level await and expose a lazy
* ready Promise instead.
*/
var localizationProviderReadyPromise = null;
function ensureLocalizationProviderReady() {
localizationProviderReadyPromise ??= localizationProvider.init();
return localizationProviderReadyPromise;
}
//#endregion
//#region src/utils/iframeConnector.ts
/**
* Runtime frame detection helper.
*
* Audio download no longer relies on service iframes or postMessage bridges.
* We keep only the minimal utility used by bootstrap policy.
*/
var isIframe = () => globalThis.self !== globalThis.top;
//#endregion
//#region src/bootstrap/runtimeActivation.ts
var runtimeActivated = false;
var runtimeActivationPromise = null;
async function ensureRuntimeActivated(reason, logBootstrap) {
if (runtimeActivated) return;
if (runtimeActivationPromise !== null) {
await runtimeActivationPromise;
return;
}
runtimeActivationPromise = (async () => {
logBootstrap("Activating runtime", { reason });
if (globalThis.location.origin === "https://rust-server-531j.onrender.com") {
await initAuth();
runtimeActivated = true;
return;
}
await ensureLocalizationProviderReady();
if (!isIframe()) await localizationProvider.update();
debug.log(`Selected menu language: ${localizationProvider.lang}`);
runtimeActivated = true;
})();
try {
await runtimeActivationPromise;
} finally {
runtimeActivationPromise = null;
}
}
//#endregion
//#region src/bootstrap/videoObserverBinding.ts
var boundObservers = /* @__PURE__ */ new WeakSet();
function bindObserverListeners(options) {
const { videoObserver, videosWrappers, ensureRuntimeActivated, getServicesCached, findContainer, createVideoHandler } = options;
if (boundObservers.has(videoObserver)) return;
boundObservers.add(videoObserver);
const initializingVideos = /* @__PURE__ */ new WeakSet();
const containerOwners = /* @__PURE__ */ new WeakMap();
const videoContainers = /* @__PURE__ */ new WeakMap();
const pendingVideoByContainer = /* @__PURE__ */ new WeakMap();
const clearContainerOwner = (video) => {
const container = videoContainers.get(video);
if (container && containerOwners.get(container) === video) containerOwners.delete(container);
videoContainers.delete(video);
return container ?? void 0;
};
const clearPendingVideo = (container) => {
if (!container) return;
pendingVideoByContainer.delete(container);
};
const releaseVideoHandler = async (video, reason) => {
const videoHandler = videosWrappers.get(video);
if (!videoHandler) return;
try {
await videoHandler.release();
} catch (error) {
console.error(`[VOT] Failed to release videoHandler (${reason})`, error);
} finally {
videosWrappers.delete(video);
}
};
const getMatchedSiteAndContainer = (video) => {
for (const candidate of getServicesCached()) {
const container = findContainer(candidate, video);
if (container) return {
site: candidate,
container
};
}
return null;
};
const withRuntimeSiteUrl = (site) => {
const host = String(site.host);
return host === "peertube" || host === "directlink" ? {
...site,
url: globalThis.location.origin
} : site;
};
const promotePendingVideo = async (container) => {
if (!container) return;
const pendingVideo = pendingVideoByContainer.get(container);
if (!pendingVideo) return;
pendingVideoByContainer.delete(container);
if (!pendingVideo.isConnected || videosWrappers.has(pendingVideo) || initializingVideos.has(pendingVideo)) return;
await handleVideoAdded(pendingVideo);
};
const handleVideoAdded = async (video) => {
if (videosWrappers.has(video) || initializingVideos.has(video)) return;
initializingVideos.add(video);
try {
try {
await ensureRuntimeActivated("video-detected");
} catch (err) {
console.error("[VOT] Failed to activate runtime", err);
return;
}
const match = getMatchedSiteAndContainer(video);
if (!match) return;
const { site, container } = match;
const activeVideoForContainer = containerOwners.get(container);
if (activeVideoForContainer && activeVideoForContainer !== video) {
if (activeVideoForContainer.isConnected) {
pendingVideoByContainer.set(container, video);
return;
}
await releaseVideoHandler(activeVideoForContainer, "stale container");
clearContainerOwner(activeVideoForContainer);
}
const videoHandler = createVideoHandler(video, container, withRuntimeSiteUrl(site));
videosWrappers.set(video, videoHandler);
videoContainers.set(video, container);
containerOwners.set(container, video);
try {
await videoHandler.init();
if (videosWrappers.get(video) !== videoHandler) return;
try {
await videoHandler.setCanPlay();
} catch (err) {
console.error("[VOT] Failed to get video data", err);
}
} catch (err) {
if (videosWrappers.get(video) === videoHandler) {
await releaseVideoHandler(video, "init failed");
const container = clearContainerOwner(video);
clearPendingVideo(container);
await promotePendingVideo(container);
}
console.error("[VOT] Failed to initialize videoHandler", err);
}
} finally {
initializingVideos.delete(video);
}
};
videoObserver.onVideoAdded.addListener(handleVideoAdded);
videoObserver.onVideoRemoved.addListener(async (video) => {
const container = clearContainerOwner(video);
await releaseVideoHandler(video, "video removed");
initializingVideos.delete(video);
if (container && pendingVideoByContainer.get(container) === video) clearPendingVideo(container);
await promotePendingVideo(container);
});
}
//#endregion
//#region src/core/bootstrapPolicy.ts
function shouldSkipIframeBootstrap(input) {
if (!input.isIframe) return false;
return input.href === "about:blank" || input.href.startsWith("about:srcdoc") || input.origin === "null";
}
function resolveBootstrapMode(input) {
if (shouldSkipIframeBootstrap(input)) return "skip";
if (!input.isIframe && input.origin === input.authOrigin) return "auth-eager";
return "lazy";
}
//#endregion
//#region src/utils/dom.ts
function getComposableParent(node) {
if (!node) return null;
if (typeof ShadowRoot !== "undefined" && node instanceof ShadowRoot) return node.host;
return node.parentNode ?? null;
}
function getDeepActiveElement(root = document) {
let activeElement = root.activeElement;
while (activeElement instanceof HTMLElement && activeElement.shadowRoot) {
const nestedActiveElement = activeElement.shadowRoot.activeElement;
if (!nestedActiveElement) break;
activeElement = nestedActiveElement;
}
return activeElement;
}
/**
* Checks whether `target` is a descendant of `container` in the composed tree
* (crossing ShadowRoot boundaries via hosts).
*/
function containsCrossShadow(container, target) {
let node = target;
while (node) {
if (node === container) return true;
node = getComposableParent(node);
}
return false;
}
function closestCrossShadow(element, selector) {
if (!element || !selector) return null;
return walkCrossShadow(element, selector, element instanceof Document ? null : element);
}
function findMatchingDocumentElement(current, selector, origin) {
if (!origin) return current.querySelector(selector);
const matches = current.querySelectorAll(selector);
for (const match of matches) if (containsCrossShadow(match, origin)) return match;
return null;
}
function getNextCrossShadowTarget(current) {
const root = current.getRootNode();
if (root instanceof ShadowRoot) return root.host;
if (root instanceof Document) return root;
if (root !== current) {
const parent = getComposableParent(root);
if (parent && parent !== current && parent instanceof Element) return parent;
}
return null;
}
function walkCrossShadow(current, selector, origin) {
if (!current) return null;
if (current instanceof Document) return findMatchingDocumentElement(current, selector, origin);
const closest = current.closest(selector);
if (closest) return closest;
return walkCrossShadow(getNextCrossShadowTarget(current), selector, origin);
}
//#endregion
//#region src/core/containerResolution.ts
function findConnectedContainerBySelector(video, selector) {
if (!selector) return null;
const matched = closestCrossShadow(video, selector);
if (matched instanceof HTMLElement && matched.isConnected && containsCrossShadow(matched, video)) return matched;
return null;
}
//#endregion
//#region src/core/overlayMountTargets.ts
function resolveOverlayBaseContainer(container, site) {
return site.host === "youtube" && site.additionalData !== "mobile" ? container.parentElement ?? container : container;
}
function resolveOverlayMountTargets(input) {
const base = resolveOverlayBaseContainer(input.container, input.site);
const root = input.fullscreenRoot ?? base;
return {
base,
root,
portalContainer: base,
subtitlesMountContainer: root
};
}
//#endregion
//#region src/core/eventImpl.ts
/**
* Tiny, dependency-free event emitter.
*
* Notes:
* - Uses generics so emitted arguments stay strongly typed.
* - Adding the same listener twice is a no-op (idempotent).
* - Listener errors are isolated so one bad subscriber doesn't break the emitter.
*/
var EventImpl = class {
listeners = /* @__PURE__ */ new Set();
get size() {
return this.listeners.size;
}
addListener(handler) {
this.listeners.add(handler);
return this;
}
removeListener(handler) {
this.listeners.delete(handler);
return this;
}
dispatch(...args) {
for (const handler of this.listeners) try {
handler(...args);
} catch (exception) {
console.error("[VOT]", exception);
}
}
async dispatchAsync(...args) {
const pending = [];
for (const handler of this.listeners) try {
const result = handler(...args);
if (result && typeof result.then === "function") pending.push(Promise.resolve(result));
} catch (exception) {
console.error("[VOT]", exception);
}
if (!pending.length) return;
const settled = await Promise.allSettled(pending);
for (const item of settled) if (item.status === "rejected") console.error("[VOT]", item.reason);
}
clear() {
this.listeners.clear();
}
};
//#endregion
//#region src/audioDownloader/strategies/fileId.ts
function makeFileId(downloadType, itag, fileSize, minChunkSize) {
return JSON.stringify({
downloadType,
itag,
minChunkSize,
fileSize
});
}
//#endregion
//#region src/audioDownloader/ytAudio/src/internal/format-selection.ts
function normalizeMimeType(mimeType) {
return mimeType?.toLowerCase() ?? "";
}
function isAudioOnlyMimeType(mimeType) {
const normalizedMimeType = normalizeMimeType(mimeType);
return normalizedMimeType.includes("audio/") && !normalizedMimeType.includes("video/");
}
function isMp4aAdaptiveAudioMimeType(mimeType) {
const normalizedMimeType = normalizeMimeType(mimeType);
return normalizedMimeType.includes("audio/mp4") && normalizedMimeType.includes("mp4a.");
}
function isOpusAdaptiveAudioMimeType(mimeType) {
const normalizedMimeType = normalizeMimeType(mimeType);
return normalizedMimeType.includes("audio/webm") && normalizedMimeType.includes("opus");
}
function extractAudioCodecFromMimeType(mimeType) {
if (!mimeType) return "mp4a.40.2";
const codecsMatch = /codecs="([^"]+)"/i.exec(mimeType);
if (!codecsMatch?.[1]) return "mp4a.40.2";
const codecs = codecsMatch[1].split(",").map((value) => value.trim());
return codecs.find((value) => value.toLowerCase().startsWith("mp4a.")) ?? codecs[0] ?? "mp4a.40.2";
}
function pickByBitrate(formats, direction) {
let selected = null;
let selectedBitrate = direction === "max" ? -Infinity : Infinity;
for (const format of formats) {
const bitrate = format.bitrate ?? 0;
if (direction === "max" && bitrate > selectedBitrate || direction === "min" && bitrate < selectedBitrate) {
selected = format;
selectedBitrate = bitrate;
}
}
return selected;
}
function pickAdaptiveAudioFormat(formats, quality) {
const audioFormats = formats.filter((format) => isAudioOnlyMimeType(format.mimeType));
if (!audioFormats.length) throw new Error("No adaptive audio formats were found in player response");
const pickDirection = quality === "bestefficiency" ? "min" : "max";
const candidateGroups = quality === "bestefficiency" ? [audioFormats.filter((format) => isOpusAdaptiveAudioMimeType(format.mimeType)), audioFormats] : [
audioFormats.filter((format) => isMp4aAdaptiveAudioMimeType(format.mimeType)),
audioFormats.filter((format) => isOpusAdaptiveAudioMimeType(format.mimeType)),
audioFormats
];
for (const candidates of candidateGroups) {
if (!candidates.length) continue;
const selected = pickByBitrate(candidates, pickDirection);
if (selected) return selected;
}
throw new Error("No adaptive audio formats were found in player response");
}
//#endregion
//#region src/audioDownloader/ytAudio/src/AudioDownloader.ts
var VIDEO_ID_PATTERN = /^[a-zA-Z0-9_-]{11}$/;
var CLIENT_CONFIG = { ANDROID_VR: {
baseUrl: "https://m.youtube.com",
clientName: "ANDROID_VR",
clientVersion: "1.65.10",
clientNameId: "28",
deviceMake: "Oculus",
deviceModel: "Quest 3",
androidSdkVersion: 32,
osName: "Android",
osVersion: "12L",
userAgent: "com.google.android.apps.youtube.vr.oculus/1.65.10 (Linux; U; Android 12L; eureka-user Build/SQ3A.220605.009.A1) gzip"
} };
var CLIENTS = ["ANDROID_VR"];
function getClientHeaders(client) {
const cfg = CLIENT_CONFIG[client];
return {
accept: "*/*",
origin: cfg.baseUrl,
referer: `${cfg.baseUrl}/`,
"user-agent": cfg.userAgent
};
}
function withSignal(signal) {
return signal ? { signal } : {};
}
function resolveInnertubeClient(requestedClient) {
const cfg = CLIENT_CONFIG[requestedClient ?? "ANDROID_VR"];
if (!cfg) throw new Error(`Unsupported Innertube client: ${requestedClient}`);
return {
clientName: cfg.clientName,
clientVersion: cfg.clientVersion,
hl: "en",
gl: "US",
androidSdkVersion: cfg.androidSdkVersion,
osName: cfg.osName,
osVersion: cfg.osVersion,
platform: "MOBILE"
};
}
function extractVideoId(input) {
const value = input.trim();
if (VIDEO_ID_PATTERN.test(value)) return value;
let url;
try {
url = new URL(value);
} catch {
throw new Error(`Cannot extract YouTube video id from: ${input}`);
}
const hostname = url.hostname.toLowerCase();
if (hostname === "youtu.be" || hostname.endsWith(".youtu.be")) return getValidatedVideoId(url.pathname.split("/").find(Boolean), input);
const searchId = url.searchParams.get("v");
if (searchId && VIDEO_ID_PATTERN.test(searchId)) return searchId;
const pathId = getVideoIdFromPathSegments(url.pathname.split("/").filter(Boolean));
if (pathId) return pathId;
throw new Error(`Cannot extract YouTube video id from: ${input}`);
}
function getValidatedVideoId(id, input) {
if (id && VIDEO_ID_PATTERN.test(id)) return id;
throw new Error(`Cannot extract YouTube video id from: ${input}`);
}
function getVideoIdFromPathSegments(pathSegments) {
for (const marker of ["shorts", "embed"]) {
const markerIndex = pathSegments.indexOf(marker);
if (markerIndex === -1) continue;
const id = pathSegments[markerIndex + 1];
if (id && VIDEO_ID_PATTERN.test(id)) return id;
}
return null;
}
function decodeEscapedJsonString(input) {
return input.replaceAll("\\u0026", "&").replaceAll("\\/", "/");
}
function getRequiredVideoId(request) {
const source = request.videoId ?? request.videoUrl;
if (!source) throw new Error("Either videoId or videoUrl is required");
return extractVideoId(source);
}
function matchFirst(source, patterns) {
for (const pattern of patterns) {
const matched = pattern.exec(source)?.[1];
if (matched) return matched;
}
}
async function readResponseBytes(response) {
return new Uint8Array(await response.arrayBuffer());
}
function makeCPN(length = 16) {
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
let output = "";
if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
const bytes = new Uint8Array(length);
crypto.getRandomValues(bytes);
for (const byte of bytes) output += alphabet[byte % 64] ?? "a";
return output;
}
for (let i = 0; i < length; i++) output += alphabet[Math.floor(Math.random() * 64)] ?? "a";
return output;
}
function parsePositiveInteger(value) {
if (!value) return null;
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed <= 0) return null;
return parsed;
}
function parseContentRangeHeader(contentRange) {
if (!contentRange) return null;
const matched = /^bytes\s+(\d+)-(\d+)\/(?:\d+|\*)$/i.exec(contentRange.trim());
if (!matched) return null;
const start = Number.parseInt(matched[1] ?? "", 10);
const end = Number.parseInt(matched[2] ?? "", 10);
if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end < start) return null;
return {
start,
end
};
}
function getExpectedRangeLength(start, end) {
return end - start + 1;
}
function isValidRangeChunkResponse(response, bytes, start, end) {
const expectedLength = getExpectedRangeLength(start, end);
if (expectedLength <= 0) return false;
if (bytes.byteLength <= 0 || bytes.byteLength > expectedLength) return false;
const contentRange = parseContentRangeHeader(response.headers.get("content-range"));
if (contentRange) return contentRange.start === start && contentRange.end === start + bytes.byteLength - 1;
if (response.status === 206) return bytes.byteLength === expectedLength;
if (response.status === 200) return start === 0 && bytes.byteLength === expectedLength;
return false;
}
function describeRangeChunkResponse(response, bytes) {
const contentRange = response.headers.get("content-range") ?? "none";
const contentLength = response.headers.get("content-length") ?? "none";
return `status=${response.status}; bytes=${bytes.byteLength}; content-range=${contentRange}; content-length=${contentLength}`;
}
function getAudioMimeType(mimeType) {
const normalizedMimeType = mimeType?.toLowerCase() ?? "";
if (normalizedMimeType.includes("audio/webm")) return "audio/webm";
if (normalizedMimeType.includes("audio/mp4")) return "audio/mp4";
return "audio/aac";
}
function buildClientAttemptOrder(requestedClient) {
const ordered = requestedClient ? [requestedClient, ...CLIENTS] : [...CLIENTS];
const seen = /* @__PURE__ */ new Set();
return ordered.filter((client) => {
if (seen.has(client)) return false;
seen.add(client);
return true;
});
}
var AudioDownloader$1 = class {
fetchFn;
constructor(options = {}) {
this.fetchFn = options.fetchImplementation ?? fetch;
}
async fetchRangeChunk(streamUrl, start, end, signal) {
const rangeHeader = `bytes=${start}-${end}`;
const response = await this.fetchFn(streamUrl, {
headers: {
...getClientHeaders("ANDROID_VR"),
range: rangeHeader
},
...withSignal(signal)
});
if (!response.ok) throw new Error(`Failed to download stream chunk: ${response.status}`);
const bytes = await readResponseBytes(response);
if (!isValidRangeChunkResponse(response, bytes, start, end)) throw new Error(`Received unexpected stream chunk payload: ${describeRangeChunkResponse(response, bytes)}`);
return {
bytes,
resolvedUrl: response.status === 206 ? response.url : null
};
}
async downloadStreamByRanges(streamUrl, contentLengthHint, signal) {
const fileSize = this.resolveStreamContentLength(contentLengthHint);
const { bytes } = await this.fetchRangeChunk(streamUrl, 0, fileSize - 1, signal);
return bytes;
}
async downloadAudioToChunkStream(request, options) {
if (options.chunkSize <= 0) throw new RangeError("Audio downloader. ytAudio. chunkSize must be > 0");
return this.withResolvedPlayableAudioFormat(request, request.audioQuality ?? "best", "Chunk mode requires an adaptive audio stream format", "Unable to resolve streamable format for chunk mode", async ({ resolved, signal }) => {
const fileSize = this.resolveStreamContentLength(resolved.chosenFormat.contentLength);
const mediaPartsLength = Math.max(1, Math.ceil(fileSize / options.chunkSize));
return {
videoId: resolved.videoId,
fileSize,
itag: resolved.chosenFormat.itag ?? 0,
mediaPartsLength,
getMediaBuffers: async function* () {
let actualStreamUrl = resolved.streamUrl;
for (let index = 0; index < mediaPartsLength; index++) {
const start = index * options.chunkSize;
const end = Math.min(fileSize - 1, start + options.chunkSize - 1);
const { bytes, resolvedUrl } = await this.fetchRangeChunk(actualStreamUrl, start, end, signal);
if (resolvedUrl) actualStreamUrl = resolvedUrl;
yield bytes;
}
}.bind(this)
};
});
}
async downloadAudioToUint8Array(request) {
const chunks = [];
let total = 0;
const streamResult = await this.extractAndWriteAudio(request, { async write(chunk) {
chunks.push(chunk);
total += chunk.byteLength;
} });
const bytes = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return {
...streamResult,
bytes
};
}
async extractAndWriteAudio(request, sink) {
return this.withResolvedPlayableAudioFormat(request, request.audioQuality ?? "bestefficiency", "Selected stream is not audio-only", "Unable to download playable stream format", async ({ resolved, signal }) => {
const streamBytes = await this.downloadStreamByRanges(resolved.streamUrl, resolved.chosenFormat.contentLength, signal);
const hints = this.getExtractionHints(resolved.chosenFormat);
await sink.write(streamBytes);
return {
videoId: resolved.videoId,
bytesWritten: streamBytes.byteLength,
mimeType: getAudioMimeType(resolved.chosenFormat.mimeType),
codec: hints.codec,
sampleRate: hints.sampleRate,
channels: hints.channels
};
});
}
async withResolvedPlayableAudioFormat(request, quality, audioOnlyErrorMessage, failurePrefix, onResolved) {
const videoId = getRequiredVideoId(request);
const { signal } = request;
const watchContext = await this.fetchWatchContext(videoId, signal);
const clientAttempts = buildClientAttemptOrder(request.client);
const attemptErrors = [];
for (const client of clientAttempts) try {
const resolved = await this.resolvePlayableFormatForClient({
videoId,
watchContext,
client,
quality,
signal
});
if (!isAudioOnlyMimeType(resolved.chosenFormat.mimeType)) throw new Error(audioOnlyErrorMessage);
return await onResolved({
resolved,
signal
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
attemptErrors.push(`${client}: ${message}`);
}
throw new Error(`${failurePrefix}. Attempts: ${attemptErrors.join(" | ")}`);
}
async resolvePlayableFormatForClient({ videoId, watchContext, client, quality, signal }) {
const directAdaptiveFormats = ((await this.fetchPlayerResponse(videoId, watchContext, client, signal)).streamingData?.adaptiveFormats ?? []).filter((format) => Boolean(format.url));
if (!directAdaptiveFormats.length) throw new Error("Player response did not contain direct adaptive audio stream URLs");
const chosenFormat = pickAdaptiveAudioFormat(directAdaptiveFormats, quality);
return {
videoId,
chosenFormat,
streamUrl: this.resolveFormatUrl(chosenFormat)
};
}
resolveStreamContentLength(contentLengthHint) {
const contentLength = parsePositiveInteger(contentLengthHint);
if (contentLength !== null) return contentLength;
throw new Error("Failed to resolve stream content length: contentLength not found in format");
}
getExtractionHints(format) {
const codec = extractAudioCodecFromMimeType(format.mimeType);
const sampleRate = Number.parseInt(format.audioSampleRate ?? "", 10);
return {
codec,
sampleRate: Number.isFinite(sampleRate) && sampleRate > 0 ? sampleRate : 44100,
channels: format.audioChannels && format.audioChannels > 0 ? format.audioChannels : 2
};
}
resolveFormatUrl(format) {
if (!format.url) throw new Error("Selected format does not contain a direct stream URL");
const streamUrl = new URL(format.url);
streamUrl.searchParams.set("cpn", makeCPN());
return streamUrl.toString();
}
async fetchWatchContext(videoId, signal) {
const watchUrl = `${CLIENT_CONFIG.ANDROID_VR.baseUrl}/watch?v=${encodeURIComponent(videoId)}&hl=en`;
const response = await this.fetchFn(watchUrl, {
headers: getClientHeaders("ANDROID_VR"),
...withSignal(signal)
});
if (!response.ok) throw new Error(`Failed to load watch page: ${response.status}`);
const html = await response.text();
const apiKey = matchFirst(html, [/"INNERTUBE_API_KEY":"([^"]+)"/, /["']INNERTUBE_API_KEY["']\s*:\s*"([^"]+)"/]);
const clientVersion = matchFirst(html, [/"INNERTUBE_CLIENT_VERSION":"([^"]+)"/, /["']INNERTUBE_CLIENT_VERSION["']\s*:\s*"([^"]+)"/]);
const stsRaw = matchFirst(html, [/"STS":(\d+)/, /["']STS["']\s*:\s*(\d+)/]);
const visitorData = matchFirst(html, [
/"VISITOR_DATA":"([^"]+)"/,
/"visitorData":"([^"]+)"/,
/["'](?:VISITOR_DATA|visitorData)["']\s*:\s*"([^"]+)"/
]);
if (!apiKey || !clientVersion) throw new Error("Failed to extract required player context from watch page");
const signatureTimestamp = stsRaw ? Number.parseInt(stsRaw, 10) : void 0;
const context = {
apiKey,
clientVersion
};
if (typeof signatureTimestamp === "number" && Number.isFinite(signatureTimestamp)) context.signatureTimestamp = signatureTimestamp;
if (visitorData) context.visitorData = decodeEscapedJsonString(visitorData);
return context;
}
async fetchPlayerResponse(videoId, watchContext, requestedClient, signal) {
const client = resolveInnertubeClient(requestedClient);
if (watchContext.visitorData) client.visitorData = watchContext.visitorData;
const body = {
context: { client },
videoId,
contentCheckOk: true,
racyCheckOk: true
};
if (watchContext.signatureTimestamp) body.playbackContext = { contentPlaybackContext: { signatureTimestamp: watchContext.signatureTimestamp } };
const endpoint = `${CLIENT_CONFIG.ANDROID_VR.baseUrl}/youtubei/v1/player?key=${encodeURIComponent(watchContext.apiKey)}`;
const response = await this.fetchFn(endpoint, {
method: "POST",
headers: {
...getClientHeaders("ANDROID_VR"),
"content-type": "application/json",
...watchContext.visitorData ? { "x-goog-visitor-id": watchContext.visitorData } : {}
},
body: JSON.stringify(body),
...withSignal(signal)
});
if (!response.ok) throw new Error(`Player API request failed with status ${response.status}`);
const json = await response.json();
const hasFormats = Boolean(json.streamingData?.formats?.length);
const hasAdaptiveFormats = Boolean(json.streamingData?.adaptiveFormats?.length);
if (!hasFormats && !hasAdaptiveFormats) throw new Error("Player response did not contain streaming formats");
return json;
}
};
//#endregion
//#region src/audioDownloader/ytAudio/strategy.ts
var DEFAULT_YT_AUDIO_QUALITY = "bestefficiency";
var DEFAULT_FETCH_TIMEOUT_MS = 3e4;
function assertValidChunkSize(chunkSize) {
if (chunkSize <= 0) throw new RangeError("Audio downloader. ytAudio. chunkSize must be > 0");
}
function createYtAudioFetch({ signal, timeoutMs }) {
return async (input, init = {}) => await GM_fetch(input, {
...init,
signal: init.signal ?? signal,
forceGmXhr: true,
timeout: timeoutMs
});
}
async function getAudioFromYtAudio({ videoId, signal }, deps = {}) {
const chunkSize = deps.chunkSize ?? config_default$1.minChunkSize;
assertValidChunkSize(chunkSize);
const fetchImplementation = createYtAudioFetch({
signal,
timeoutMs: deps.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS
});
const downloader = deps.createDownloader?.(fetchImplementation) ?? new AudioDownloader$1({ fetchImplementation });
try {
const streamResult = await downloader.downloadAudioToChunkStream({
videoId,
audioQuality: DEFAULT_YT_AUDIO_QUALITY,
signal
}, { chunkSize });
return {
fileId: makeFileId(AudioDownloadType.WEB_API_STEAL_SIG_AND_N, streamResult.itag, String(streamResult.fileSize), chunkSize),
mediaPartsLength: streamResult.mediaPartsLength,
getMediaBuffers: streamResult.getMediaBuffers
};
} catch (error) {
console.warn("[VOT] ytAudio streaming mode failed, falling back to buffered mode", error);
}
const bytes = (await downloader.downloadAudioToUint8Array({
videoId,
audioQuality: DEFAULT_YT_AUDIO_QUALITY,
signal
})).bytes;
if (!bytes || bytes.byteLength === 0) throw new Error("Audio downloader. ytAudio. Empty audio");
const mediaPartsLength = Math.max(1, Math.ceil(bytes.byteLength / chunkSize));
return {
fileId: makeFileId(AudioDownloadType.WEB_API_STEAL_SIG_AND_N, 0, String(bytes.byteLength), chunkSize),
mediaPartsLength,
async *getMediaBuffers() {
for (let start = 0; start < bytes.byteLength; start += chunkSize) {
const end = Math.min(start + chunkSize, bytes.byteLength);
yield bytes.subarray(start, end);
}
}
};
}
//#endregion
//#region src/audioDownloader/strategies/index.ts
var YT_AUDIO_STRATEGY = "ytAudio";
var strategies = { [YT_AUDIO_STRATEGY]: getAudioFromYtAudio };
//#endregion
//#region src/audioDownloader/index.ts
function assertValidMediaPartsLength(mediaPartsLength) {
if (!Number.isInteger(mediaPartsLength) || mediaPartsLength < 1) throw new Error("Audio downloader. Invalid media parts length");
}
function assertHasAudioChunk(chunk) {
if (!chunk || chunk.byteLength === 0) throw new Error("Audio downloader. Empty audio");
return chunk;
}
async function handleCommonAudioDownloadRequest({ audioDownloader, translationId, videoId, signal }) {
const audioData = await strategies[audioDownloader.strategy]({
videoId,
signal
});
if (!audioData) throw new Error("Audio downloader. Can not get audio data");
debug.log("Audio downloader. Url found", { audioDownloadType: audioDownloader.strategy });
const { getMediaBuffers, mediaPartsLength, fileId } = audioData;
assertValidMediaPartsLength(mediaPartsLength);
if (mediaPartsLength < 2) {
const { value } = await getMediaBuffers().next();
const singleChunk = assertHasAudioChunk(value);
await audioDownloader.onDownloadedAudio.dispatchAsync(translationId, {
videoId,
fileId,
audioData: singleChunk
});
return;
}
let index = 0;
for await (const audioChunk of getMediaBuffers()) {
const chunk = assertHasAudioChunk(audioChunk);
await audioDownloader.onDownloadedPartialAudio.dispatchAsync(translationId, {
videoId,
fileId,
audioData: chunk,
version: 1,
index,
amount: mediaPartsLength
});
index++;
}
if (index !== mediaPartsLength) throw new Error(`Audio downloader. Expected ${mediaPartsLength} chunks, got ${index}`);
}
var AudioDownloader = class {
onDownloadedAudio = new EventImpl();
onDownloadedPartialAudio = new EventImpl();
onDownloadAudioError = new EventImpl();
strategy;
constructor(strategy = YT_AUDIO_STRATEGY) {
this.strategy = strategy;
debug.log("Audio downloader created", { strategy });
}
async runAudioDownload(videoId, translationId, signal) {
try {
await handleCommonAudioDownloadRequest({
audioDownloader: this,
translationId,
videoId,
signal
});
debug.log("Audio downloader. Audio download finished", { videoId });
} catch (err) {
debug.error("Audio downloader. Failed to download audio", {
videoId,
error: err instanceof Error ? err.message : String(err)
});
this.onDownloadAudioError.dispatch(videoId);
}
}
addEventListener(type, listener) {
switch (type) {
case "downloadedAudio":
this.onDownloadedAudio.addListener(listener);
break;
case "downloadedPartialAudio":
this.onDownloadedPartialAudio.addListener(listener);
break;
case "downloadAudioError":
this.onDownloadAudioError.addListener(listener);
break;
}
return this;
}
removeEventListener(type, listener) {
switch (type) {
case "downloadedAudio":
this.onDownloadedAudio.removeListener(listener);
break;
case "downloadedPartialAudio":
this.onDownloadedPartialAudio.removeListener(listener);
break;
case "downloadAudioError":
this.onDownloadAudioError.removeListener(listener);
break;
}
return this;
}
};
//#endregion
//#region src/utils/timeFormatting.ts
var MAX_SECS_FRACTION = .66;
function formatTranslationEta(secs, getMessage) {
let minutes = Math.floor(secs / 60);
if (Math.floor(secs % 60) / 60 >= MAX_SECS_FRACTION) minutes += 1;
if (minutes >= 60) return getMessage("translationTakeMoreThanHour");
if (minutes <= 1) return getMessage("translationTakeAboutMinute");
const minutesStr = String(minutes);
if (minutes !== 11 && minutes % 10 === 1) return getMessage("translationTakeApproximatelyMinute2").replace("{0}", minutesStr);
if (![
12,
13,
14
].includes(minutes) && [
2,
3,
4
].includes(minutes % 10)) return getMessage("translationTakeApproximatelyMinute").replace("{0}", minutesStr);
return getMessage("translationTakeApproximatelyMinutes").replace("{0}", minutesStr);
}
//#endregion
//#region src/utils/VOTLocalizedError.ts
var VOTLocalizedError = class extends Error {
name = "VOTLocalizedError";
/** Original (non-localized) message key. */
unlocalizedMessage;
/** Resolved localized message. */
localizedMessage;
constructor(message) {
super(localizationProvider.getDefault(message));
this.unlocalizedMessage = message;
this.localizedMessage = localizationProvider.get(message);
}
};
//#endregion
//#region src/videoHandler/modules/translationShared.ts
function normalizeTranslationHelp(translationHelp) {
return translationHelp ?? null;
}
async function requestTranslationAudio(requester, options) {
const response = await requester.translateVideoImpl(options.videoData, options.requestLang, options.responseLang, normalizeTranslationHelp(options.translationHelp), !options.useAudioDownload, options.signal);
if (!response?.url) return null;
return {
url: response.url,
usedLivelyVoice: Boolean(response.usedLivelyVoice)
};
}
function buildTranslationCacheValue(options) {
return {
videoId: options.videoId,
from: options.requestLang,
to: options.responseLang,
url: options.downloadTranslationUrl ?? options.fallbackUrl,
useLivelyVoice: options.usedLivelyVoice
};
}
async function updateTranslationIfFresh(options) {
if (options.isActionStale(options.actionContext)) return false;
await options.updateTranslation(options.url, options.actionContext);
if (options.isActionStale(options.actionContext)) return false;
return true;
}
async function requestAndApplyTranslation(options) {
const translateRes = await requestTranslationAudio(options.requester, {
videoData: options.request.videoData,
requestLang: options.request.requestLang,
responseLang: options.request.responseLang,
translationHelp: options.request.translationHelp,
useAudioDownload: options.request.useAudioDownload,
signal: options.request.signal
});
if (!translateRes) return null;
if (!await updateTranslationIfFresh({
url: translateRes.url,
actionContext: options.actionContext,
isActionStale: options.isActionStale,
updateTranslation: options.updateTranslation
}) || options.isActionStale(options.actionContext)) return null;
return translateRes;
}
function setTranslationCacheValue(options) {
options.setTranslation(options.cacheKey, buildTranslationCacheValue({
videoId: options.videoId,
requestLang: options.requestLang,
responseLang: options.responseLang,
fallbackUrl: options.fallbackUrl,
downloadTranslationUrl: options.downloadTranslationUrl,
usedLivelyVoice: options.usedLivelyVoice
}));
}
function notifyTranslationFailureIfNeeded(options) {
if (options.aborted || !options.translateApiErrorsEnabled || !options.hadAsyncWait) return options.hadAsyncWait;
options.notify({
videoId: options.videoId,
message: options.error
});
return false;
}
//#endregion
//#region src/core/translationHandler.ts
function asVotClientErrorShape(value) {
if (!value || typeof value !== "object") return null;
const candidate = value;
const data = candidate.data && typeof candidate.data === "object" ? candidate.data : void 0;
return {
name: candidate.name,
message: candidate.message,
data
};
}
function getServerErrorMessage(value) {
const message = asVotClientErrorShape(value)?.data?.message;
return typeof message === "string" && message.length > 0 ? message : void 0;
}
/**
* Historically we used `patch-package` to make `@vot.js/core` throw
* `VOTLocalizedError` for a few common failure cases.
*
* We now keep the dependency unpatched and instead map known error messages
* coming from the VOT client to the corresponding localized UI errors.
*/
function mapVotClientErrorForUi(error) {
const err = asVotClientErrorShape(error);
if (!err) return error;
if (err.name !== "VOTJSError") return error;
const message = typeof err.message === "string" ? err.message : "";
const hasServerMessage = typeof err.data?.message === "string" && err.data.message.length > 0;
if (message === "Yandex couldn't translate video" && !hasServerMessage) return new VOTLocalizedError("requestTranslationFailed");
if (message === "Failed to request video translation") return new VOTLocalizedError("requestTranslationFailed");
if (message === "Audio link wasn't received" || message === "Audio link wasn't received from VOT response") return new VOTLocalizedError("audioNotReceived");
return error;
}
function summarizeTranslationResponse(response) {
return {
status: response.status,
translated: response.translated,
remainingTime: response.remainingTime,
translationId: response.translationId
};
}
var VOTTranslationHandler = class VOTTranslationHandler {
videoHandler;
audioDownloader;
downloading;
downloadWaiters = /* @__PURE__ */ new Set();
requestedFailAudio = /* @__PURE__ */ new Set();
constructor(videoHandler) {
this.videoHandler = videoHandler;
this.audioDownloader = new AudioDownloader();
this.downloading = false;
this.audioDownloader.addEventListener("downloadedAudio", this.onDownloadedAudio).addEventListener("downloadedPartialAudio", this.onDownloadedPartialAudio).addEventListener("downloadAudioError", this.onDownloadAudioError);
}
onDownloadedAudio = async (translationId, data) => {
debug.log("downloadedAudio", data);
if (!this.downloading) {
debug.log("skip downloadedAudio");
return;
}
const { videoId, fileId, audioData } = data;
const videoUrl = this.getCanonicalUrl(videoId);
try {
await this.videoHandler.votClient.requestVtransAudio(videoUrl, translationId, {
audioFile: audioData,
fileId
});
} catch (error) {
debug.error("Failed to upload downloaded audio", error);
this.finishDownloadFailure(/* @__PURE__ */ new Error("Audio downloader failed while uploading full audio"));
return;
}
this.finishDownloadSuccess();
};
onDownloadedPartialAudio = async (translationId, data) => {
debug.log("downloadedPartialAudio", data);
if (!this.downloading) {
debug.log("skip downloadedPartialAudio");
return;
}
const { audioData, fileId, videoId, amount, version, index } = data;
const videoUrl = this.getCanonicalUrl(videoId);
try {
await this.videoHandler.votClient.requestVtransAudio(videoUrl, translationId, {
audioFile: audioData,
chunkId: index
}, {
audioPartsLength: amount,
fileId,
version
});
} catch (error) {
debug.error("Failed to upload downloaded audio chunk", error);
this.finishDownloadFailure(/* @__PURE__ */ new Error("Audio downloader failed while uploading chunk"));
return;
}
if (index === amount - 1) this.finishDownloadSuccess();
};
onDownloadAudioError = async (videoId) => {
if (!this.downloading) {
debug.log("skip downloadAudioError");
return;
}
debug.log(`Failed to download audio ${videoId}`);
const videoUrl = this.getCanonicalUrl(videoId);
if (!(this.videoHandler.site.host === "youtube" && Boolean(this.videoHandler.data?.useAudioDownload))) {
this.finishDownloadFailure(new VOTLocalizedError("VOTFailedDownloadAudio"));
return;
}
try {
if (this.requestedFailAudio.has(videoUrl)) debug.log("fail-audio-js request already sent for this video");
else {
debug.log("Sending fail-audio-js request");
await this.videoHandler.votClient.requestVtransFailAudio(videoUrl);
this.requestedFailAudio.add(videoUrl);
}
this.finishDownloadSuccess();
} catch (error) {
debug.error("fail-audio-js request failed", error);
this.finishDownloadFailure(new VOTLocalizedError("VOTFailedDownloadAudio"));
}
};
finishDownloadSuccess() {
this.downloading = false;
this.resolveDownloadWaiters();
}
finishDownloadFailure(error) {
this.downloading = false;
this.rejectDownloadWaiters(error);
}
getCanonicalUrl(videoId) {
return `https://youtu.be/${videoId}`;
}
/**
* Detector for cases when server rejects the request because
* "Lively/Live voices" are unavailable (unsupported language pair).
*/
isLivelyVoiceUnavailableError(value) {
const msg = getErrorMessage(value);
return !!msg && msg.toLowerCase().includes("обычная озвучка");
}
scheduleRetry(fn, delayMs, signal) {
return new Promise((resolve, reject) => {
let timeoutId = null;
const cleanup = () => {
if (timeoutId !== null) clearTimeout(timeoutId);
signal.removeEventListener("abort", onAbort);
};
const onAbort = () => {
cleanup();
reject(makeAbortError());
};
signal.addEventListener("abort", onAbort, { once: true });
if (signal.aborted) {
onAbort();
return;
}
timeoutId = setTimeout(async () => {
if (signal.aborted) {
onAbort();
return;
}
cleanup();
try {
resolve(await fn());
} catch (error) {
reject(error);
}
}, delayMs);
if (timeoutId !== null) this.videoHandler.autoRetry = timeoutId;
});
}
static MAX_INITIAL_WAIT_SEC = 180;
static LONG_WAIT_MS = 12e4;
static RETRY_INTERVAL_MS = 3e4;
getRetryDelayMs(retryAttempt, remainingTimeSeconds) {
if (retryAttempt > 0) return VOTTranslationHandler.RETRY_INTERVAL_MS;
const eta = remainingTimeSeconds ?? 0;
if (eta <= 0) return VOTTranslationHandler.RETRY_INTERVAL_MS;
if (eta <= VOTTranslationHandler.MAX_INITIAL_WAIT_SEC) return eta * 1e3;
return VOTTranslationHandler.LONG_WAIT_MS;
}
async translateVideoImpl(videoData, requestLang, responseLang, translationHelp = null, shouldSendFailedAudio = false, signal = NEVER_ABORTED_SIGNAL, options = {}) {
const { disableLivelyVoice = false, retryAttempt = 0 } = options;
clearTimeout(this.videoHandler.autoRetry);
this.finishDownloadSuccess();
const requestLangForApi = this.videoHandler.getRequestLangForTranslation(requestLang, responseLang);
debug.log("[Translation] translateVideoImpl start", {
videoId: videoData.videoId,
duration: videoData.duration,
requestLang,
requestLangForApi,
responseLang,
retryAttempt,
disableLivelyVoice,
shouldSendFailedAudio,
translationHelpCount: translationHelp?.length ?? 0
});
debug.log(videoData, `Translate video (requestLang: ${requestLang}, requestLangForApi: ${requestLangForApi}, responseLang: ${responseLang})`);
let livelyDisabled = disableLivelyVoice;
let translationResponse;
try {
throwIfAborted(signal);
const livelyVoiceAllowed = this.videoHandler.isLivelyVoiceAllowed(requestLangForApi, responseLang);
const translationAttempt = await this.requestTranslationWithLivelyFallback({
videoData,
requestLangForApi,
responseLang,
translationHelp,
shouldSendFailedAudio,
livelyDisabled,
livelyVoiceAllowed
});
livelyDisabled = translationAttempt.livelyDisabled;
const useLivelyVoice = translationAttempt.useLivelyVoice;
const res = translationAttempt.response;
translationResponse = res;
if (!res) throw new Error("Failed to get translation response");
debug.log("[Translation] translateVideoImpl response", {
videoId: videoData.videoId,
useLivelyVoice,
...summarizeTranslationResponse(res)
});
throwIfAborted(signal);
if (res.translated && res.remainingTime < 1) {
debug.log("[Translation] translation finished", {
videoId: videoData.videoId,
useLivelyVoice,
...summarizeTranslationResponse(res)
});
return {
...res,
usedLivelyVoice: useLivelyVoice
};
}
const message = res.message ?? localizationProvider.get("translationTakeFewMinutes");
debug.log("[Translation] translation still processing", {
videoId: videoData.videoId,
useLivelyVoice,
...summarizeTranslationResponse(res),
message
});
await this.videoHandler.updateTranslationErrorMsg(res.remainingTime > 0 ? formatTranslationEta(res.remainingTime, (key) => localizationProvider.get(key)) : message, signal);
if (res.status === VideoTranslationStatus.AUDIO_REQUESTED && this.videoHandler.isYouTubeHosts()) {
this.videoHandler.hadAsyncWait = true;
debug.log("[Translation] audio download started", {
videoId: videoData.videoId,
translationId: res.translationId
});
this.downloading = true;
await this.audioDownloader.runAudioDownload(videoData.videoId, res.translationId, signal);
debug.log("[Translation] waiting for audio download completion", {
videoId: videoData.videoId,
translationId: res.translationId,
timeoutMs: 15e3
});
await this.waitForAudioDownloadCompletion(signal, 15e3);
return await this.translateVideoImpl(videoData, requestLang, responseLang, translationHelp, true, signal, {
disableLivelyVoice: livelyDisabled,
retryAttempt
});
}
} catch (err) {
if (isAbortError(err)) {
debug.log("[Translation] translation aborted", {
videoId: videoData.videoId,
retryAttempt
});
return null;
}
const uiError = mapVotClientErrorForUi(err);
debug.error("[Translation] translation failed", {
videoId: videoData.videoId,
retryAttempt,
error: err,
mappedError: uiError
});
await this.videoHandler.updateTranslationErrorMsg(getServerErrorMessage(uiError) ?? uiError, signal);
this.videoHandler.hadAsyncWait = notifyTranslationFailureIfNeeded({
aborted: Boolean(this.videoHandler.actionsAbortController?.signal?.aborted),
translateApiErrorsEnabled: Boolean(this.videoHandler.data?.translateAPIErrors),
hadAsyncWait: this.videoHandler.hadAsyncWait,
videoId: videoData.videoId,
error: err,
notify: (params) => this.videoHandler.notifier.translationFailed(params)
});
return null;
}
this.videoHandler.hadAsyncWait = true;
const retryDelayMs = this.getRetryDelayMs(retryAttempt, translationResponse?.remainingTime ?? null);
debug.log("[Translation] scheduling translation retry", {
videoId: videoData.videoId,
retryAttempt,
retryDelayMs,
remainingTime: translationResponse?.remainingTime
});
return this.scheduleRetry(() => this.translateVideoImpl(videoData, requestLang, responseLang, translationHelp, shouldSendFailedAudio, signal, {
disableLivelyVoice: livelyDisabled,
retryAttempt: retryAttempt + 1
}), retryDelayMs, signal);
}
async requestTranslationWithLivelyFallback({ videoData, requestLangForApi, responseLang, translationHelp, shouldSendFailedAudio, livelyDisabled, livelyVoiceAllowed }) {
let useLivelyVoice = !livelyDisabled && livelyVoiceAllowed && Boolean(this.videoHandler.data?.useLivelyVoice);
debug.log("[Translation] requesting translation from VOT client", {
videoId: videoData.videoId,
requestLangForApi,
responseLang,
shouldSendFailedAudio,
livelyDisabled,
livelyVoiceAllowed,
useLivelyVoice,
translationHelpCount: translationHelp?.length ?? 0
});
while (true) {
try {
debug.log("[Translation] votClient.translateVideo call", {
videoId: videoData.videoId,
requestLangForApi,
responseLang,
useLivelyVoice,
shouldSendFailedAudio,
translationHelpCount: translationHelp?.length ?? 0
});
const response = await this.videoHandler.votClient.translateVideo({
videoData,
requestLang: requestLangForApi,
responseLang,
translationHelp,
extraOpts: {
useLivelyVoice,
videoTitle: this.videoHandler.videoData?.title
},
shouldSendFailedAudio
});
if (!useLivelyVoice || !this.isLivelyVoiceUnavailableError(response)) {
debug.log("[Translation] votClient.translateVideo resolved", {
videoId: videoData.videoId,
useLivelyVoice,
...summarizeTranslationResponse(response)
});
return {
response,
useLivelyVoice,
livelyDisabled
};
}
debug.warn("[Translation] lively voice unavailable in response", {
videoId: videoData.videoId,
useLivelyVoice,
...summarizeTranslationResponse(response)
});
} catch (err) {
if (!useLivelyVoice || !this.isLivelyVoiceUnavailableError(err)) throw err;
debug.warn("[Translation] lively voice unavailable in error", {
videoId: videoData.videoId,
useLivelyVoice,
error: err
});
}
livelyDisabled = true;
useLivelyVoice = false;
debug.log("[Translation] retrying translation without lively voice", {
videoId: videoData.videoId,
requestLangForApi,
responseLang
});
}
}
waitForAudioDownloadCompletion(signal, timeoutMs) {
if (!this.downloading) return Promise.resolve();
return new Promise((resolve, reject) => {
let entry;
const onAbort = () => {
cleanup();
reject(makeAbortError());
};
const timeoutId = setTimeout(() => {
cleanup();
resolve();
}, timeoutMs);
const cleanup = () => {
clearTimeout(timeoutId);
signal.removeEventListener("abort", onAbort);
this.downloadWaiters.delete(entry);
};
entry = {
resolve: () => {
cleanup();
resolve();
},
reject: (error) => {
cleanup();
reject(error);
}
};
this.downloadWaiters.add(entry);
signal.addEventListener("abort", onAbort, { once: true });
if (signal.aborted) onAbort();
});
}
resolveDownloadWaiters() {
this.forEachDownloadWaiter((waiter) => waiter.resolve());
}
rejectDownloadWaiters(error) {
this.forEachDownloadWaiter((waiter) => waiter.reject(error));
}
forEachDownloadWaiter(handler) {
if (!this.downloadWaiters.size) return;
const waiters = Array.from(this.downloadWaiters);
this.downloadWaiters.clear();
for (const waiter of waiters) handler(waiter);
}
};
//#endregion
//#region src/core/translationOrchestrator.ts
var TranslationOrchestrator = class {
state = { status: "idle" };
deps;
constructor(deps) {
this.deps = deps;
}
get currentState() {
return this.state;
}
setState(next) {
this.state = next;
debug.log("[TranslationOrchestrator] state", next);
}
reset() {
this.setState({ status: "idle" });
}
async runAutoTranslationIfEligible() {
if (this.state.status !== "idle") return;
if (!(this.deps.isFirstPlay() && this.deps.isAutoTranslateEnabled() && this.deps.getVideoId())) return;
if (this.deps.isMobileYouTubeMuted?.()) {
debug.log("[TranslationOrchestrator] Mobile YouTube video is muted, deferring auto-translate");
this.setState({
status: "deferred",
reason: "muted"
});
this.deps.setMuteWatcher?.(() => {
debug.log("[TranslationOrchestrator] Video unmuted, running deferred auto-translate");
this.setState({ status: "idle" });
this.runAutoTranslationIfEligible();
});
return;
}
this.setState({
status: "pending",
reason: "auto"
});
try {
await this.deps.scheduleAutoTranslate();
this.deps.setFirstPlay(false);
this.reset();
} catch (err) {
this.setState({
status: "error",
message: err
});
throw err;
}
}
};
//#endregion
//#region src/core/lifecycleShared.ts
function resetLifecycleTranslation(host, options = {}) {
const { requireVideoData = false, clearVideoData = false } = options;
if (requireVideoData && !host.videoData) return;
if (clearVideoData) host.videoData = void 0;
host.stopTranslation();
host.resetSubtitlesWidget();
}
function hideLifecycleOverlay(overlayView, options = {}) {
const { hideMenu = false } = options;
if (overlayView?.votButton?.container) overlayView.votButton.container.hidden = true;
if (hideMenu && overlayView?.votMenu) overlayView.votMenu.hidden = true;
}
function resetAndHideLifecycle(host, overlayView, options = {}) {
const { requireVideoData, clearVideoData, hideMenu } = options;
resetLifecycleTranslation(host, {
requireVideoData,
clearVideoData
});
hideLifecycleOverlay(overlayView, { hideMenu });
}
//#endregion
//#region src/core/videoLifecycleController.ts
var VideoLifecycleController = class {
host;
lifecycleGeneration = 0;
lastSetCanPlaySourceKey = "";
activeSetCanPlaySourceKey = "";
setCanPlayRequested = false;
setCanPlayLoopPromise;
constructor(host) {
this.host = host;
}
isStale(generation) {
return generation !== this.lifecycleGeneration;
}
resetActions(reason) {
if (typeof this.host.resetActionsAbortController === "function") {
this.host.resetActionsAbortController(reason);
return;
}
this.host.actionsAbortController?.abort(reason);
}
invalidateActiveSession(reason) {
if (this.lifecycleGeneration === 0) return;
this.lifecycleGeneration += 1;
this.resetActions(`[VideoLifecycle] ${reason}`);
debug.log(`[VideoLifecycle] cancelled active session (active: ${this.lifecycleGeneration})`, { reason });
}
startSession(reason) {
this.lifecycleGeneration += 1;
const sessionId = this.lifecycleGeneration;
this.resetActions(`[VideoLifecycle][session:${sessionId}] ${reason}`);
debug.log(`[VideoLifecycle][session:${sessionId}] started`, { reason });
return sessionId;
}
shouldAbortHandleSrcChanged(callId, stage) {
if (!this.isStale(callId)) return false;
debug.log(`[VideoLifecycle][session:${callId}] handleSrcChanged aborted at ${stage} (active: ${this.lifecycleGeneration})`);
return true;
}
showOverlayButton(overlayView) {
overlayView.votButton.container.hidden = false;
overlayView.votButton.opacity = 1;
this.host.queueOverlayAutoHide?.();
}
teardown() {
this.setCanPlayRequested = false;
this.invalidateActiveSession("teardown");
}
getCurrentSourceKey() {
const hasSrcObject = this.host.video.srcObject ? "1" : "0";
if (this.host.site.host === "youtube") {
const path = globalThis.location.pathname;
return `${`${globalThis.location.origin}${path}${globalThis.location.search}`}||${hasSrcObject}`;
}
const src = this.host.video.currentSrc || this.host.video.src || "";
return `${globalThis.location.href}||${src}||${hasSrcObject}`;
}
resolveContainer() {
const { site, video, container } = this.host;
if (!site.selector) return video.parentElement ?? container;
const matched = findConnectedContainerBySelector(video, site.selector);
if (matched) return matched;
if (container.isConnected && containsCrossShadow(container, video)) return container;
return video.parentElement ?? container;
}
async setCanPlay() {
this.setCanPlayRequested = true;
if (this.setCanPlayLoopPromise !== void 0) {
const incomingSourceKey = this.getCurrentSourceKey();
if (this.activeSetCanPlaySourceKey && incomingSourceKey !== this.activeSetCanPlaySourceKey) this.invalidateActiveSession("setCanPlay source changed while previous trigger is running");
else debug.log("[VideoLifecycle] setCanPlay deduplicated for same source", { sourceKey: incomingSourceKey });
return await this.setCanPlayLoopPromise;
}
const loopPromise = (async () => {
while (this.setCanPlayRequested) {
this.setCanPlayRequested = false;
await this.runSetCanPlayOnce();
}
})();
this.setCanPlayLoopPromise = loopPromise;
try {
await loopPromise;
} finally {
if (this.setCanPlayLoopPromise === loopPromise) this.setCanPlayLoopPromise = void 0;
}
}
async runSetCanPlayOnce() {
const sourceKey = this.getCurrentSourceKey();
if (this.host.videoData?.videoId && sourceKey === this.lastSetCanPlaySourceKey) {
debug.log("[VideoLifecycle] setCanPlay deduplicated for same source", { sourceKey });
return;
}
let nextVideoData;
try {
nextVideoData = await this.host.getVideoData();
} catch (err) {
debug.log(`[VideoLifecycle] getVideoData failed for source ${sourceKey}`, err);
this.host.videoData = void 0;
hideLifecycleOverlay(this.host.uiManager.votOverlayView, { hideMenu: true });
return;
}
if (this.getCurrentSourceKey() !== sourceKey) {
debug.log("[VideoLifecycle] discarded stale getVideoData result after source change", { sourceKey });
return;
}
this.host.videoData = nextVideoData;
this.activeSetCanPlaySourceKey = sourceKey;
const currentId = this.startSession(`setCanPlay (source: ${sourceKey})`);
debug.log(`[VideoLifecycle][session:${currentId}] setCanPlay started`, { sourceKey });
try {
await this.handleSrcChanged(currentId, sourceKey);
if (this.isStale(currentId)) {
debug.log(`[VideoLifecycle][session:${currentId}] setCanPlay aborted after src change (active: ${this.lifecycleGeneration})`);
return;
}
const autoSubtitlesPromise = this.runAutoSubtitlesIfEnabled(currentId);
await this.host.translationOrchestrator.runAutoTranslationIfEligible();
if (this.isStale(currentId)) {
debug.log(`[VideoLifecycle][session:${currentId}] auto-translation result ignored (stale session)`);
return;
}
await autoSubtitlesPromise;
if (this.isStale(currentId)) {
debug.log(`[VideoLifecycle][session:${currentId}] auto-subtitles result ignored (stale session)`);
return;
}
debug.log(`[VideoLifecycle][session:${currentId}] setCanPlay finished`);
} finally {
if (this.activeSetCanPlaySourceKey === sourceKey) this.activeSetCanPlaySourceKey = "";
}
}
async runAutoSubtitlesIfEnabled(sessionId) {
if (!this.host.data.autoSubtitles || !this.host.videoData?.videoId) return;
try {
await this.host.enableSubtitlesForCurrentLangPair();
} catch (err) {
debug.log(`[VideoLifecycle][session:${sessionId}] auto-subtitles failed`, err);
}
}
async handleSrcChanged(callId, expectedSourceKey) {
const sessionId = typeof callId === "number" ? callId : this.startSession("manual handleSrcChanged");
const sourceKey = typeof expectedSourceKey === "string" && expectedSourceKey.length > 0 ? expectedSourceKey : this.getCurrentSourceKey();
if (this.shouldAbortHandleSrcChanged(sessionId, "before start")) return;
debug.log(`[VideoLifecycle][session:${sessionId}] src changed`, { sourceKey });
this.host.translationOrchestrator.reset();
this.host.firstPlay = true;
const overlayView = this.host.uiManager.votOverlayView;
resetAndHideLifecycle(this.host, overlayView, { requireVideoData: true });
if (!this.host.video.src && !this.host.video.currentSrc && !this.host.video.srcObject) hideLifecycleOverlay(overlayView, { hideMenu: true });
const nextContainer = this.resolveContainer();
if (nextContainer !== this.host.container) this.host.container = nextContainer;
if (this.shouldAbortHandleSrcChanged(sessionId, "before getVideoData")) return;
this.showOverlayButton(overlayView);
if (this.shouldAbortHandleSrcChanged(sessionId, "after getVideoData")) return;
if (!this.host.videoData?.videoId) {
debug.log(`[VideoLifecycle][session:${sessionId}] No videoId resolved, hiding overlay`);
hideLifecycleOverlay(overlayView, { hideMenu: true });
return;
}
const subtitleLanguage = this.host.getPreferredSubtitlesLanguage(this.host.videoData.detectedLanguage, this.host.videoData.responseLanguage);
if (subtitleLanguage) {
const cacheKey = this.host.getSubtitlesCacheKey(this.host.videoData.videoId, this.host.videoData.detectedLanguage, subtitleLanguage);
const cachedSubtitles = this.host.cacheManager.getSubtitles(cacheKey);
this.host.subtitles = cachedSubtitles ?? [];
this.host.subtitlesCacheKey = cachedSubtitles !== void 0 ? cacheKey : null;
} else {
this.host.subtitles = [];
this.host.subtitlesCacheKey = null;
}
await this.host.updateSubtitlesLangSelect();
if (this.shouldAbortHandleSrcChanged(sessionId, "after subtitles update")) return;
this.host.translateToLang = this.host.data.responseLanguage ?? "ru";
this.host.setSelectMenuValues(this.host.videoData.detectedLanguage, this.host.videoData.responseLanguage);
this.showOverlayButton(overlayView);
this.lastSetCanPlaySourceKey = sourceKey;
debug.log(`[VideoLifecycle][session:${sessionId}] src handling finished`);
}
};
//#endregion
//#region src/core/videoLifecycleHost.ts
function createVideoLifecycleHost(handler, resolveOverlayMount) {
const self = () => handler;
return {
get video() {
return self().video;
},
get site() {
return self().site;
},
get container() {
return self().container;
},
set container(value) {
if (self().container === value) return;
self().container = value;
self().uiManager.updateMount(resolveOverlayMount(value));
},
get firstPlay() {
return self().firstPlay;
},
set firstPlay(value) {
self().firstPlay = value;
},
stopTranslation: () => handler.stopTranslation(),
get uiManager() {
return self().uiManager;
},
getVideoData: () => handler.getVideoData(),
cacheManager: { getSubtitles: (key) => self().cacheManager.getSubtitles(key) },
getSubtitlesCacheKey: (videoId, detectedLanguage, subtitleLanguage) => handler.getSubtitlesCacheKey(videoId, detectedLanguage, subtitleLanguage),
getPreferredSubtitlesLanguage: (detectedLanguage, responseLanguage) => handler.getPreferredSubtitlesLanguage(detectedLanguage, responseLanguage),
updateSubtitlesLangSelect: () => handler.updateSubtitlesLangSelect(),
enableSubtitlesForCurrentLangPair: () => handler.enableSubtitlesForCurrentLangPair(),
setSelectMenuValues: (from, to) => handler.setSelectMenuValues(from, to),
get translateToLang() {
return self().translateToLang;
},
set translateToLang(value) {
self().translateToLang = value;
},
get data() {
return self().data ?? {};
},
get subtitles() {
return self().subtitles;
},
set subtitles(value) {
self().subtitles = value;
},
get subtitlesCacheKey() {
return self().subtitlesCacheKey;
},
set subtitlesCacheKey(value) {
self().subtitlesCacheKey = value;
},
get videoData() {
return self().videoData;
},
set videoData(value) {
const handler = self();
if (handler.videoData?.videoId !== value?.videoId) handler.downloadTranslation = null;
handler.videoData = value;
},
get actionsAbortController() {
return self().actionsAbortController;
},
set actionsAbortController(value) {
self().actionsAbortController = value;
},
resetActionsAbortController: (reason) => handler.resetActionsAbortController(reason),
translationOrchestrator: handler.translationOrchestrator,
resetSubtitlesWidget: () => handler.resetSubtitlesWidget(),
queueOverlayAutoHide: () => handler.overlayVisibility?.queueAutoHide()
};
}
//#endregion
//#region src/utils/text.ts
var MAX_TEXT_LENGTH = 450;
var REMOVABLE_TOKEN_FILTER = new RegExp([
String.raw`(?:https?:\/\/|www\.)\S+`,
String.raw`#[^\s#]+`,
String.raw`auto-generated\s+by\s+youtube`,
String.raw`provided\s+to\s+youtube\s+by`,
String.raw`released\s+on`,
String.raw`\bpaypal\b`,
String.raw`\b0x[a-f0-9]{40}\b`,
String.raw`\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b`,
String.raw`\b(?:bc1|tb1|bcrt1)[ac-hj-np-z02-9]{11,71}\b`,
String.raw`\b(?:-1|0):[a-f0-9]{64}\b`
].join("|"), "giu");
var NOISE_CHARACTER_FILTER = /[\p{N}\p{P}\p{S}]+/gu;
var WHITESPACE_FILTER = /\s+/g;
var LETTER_FILTER = /\p{L}/u;
function trimToMaxLength(text, maxLength) {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength).trimEnd();
}
function cleanText(title, description) {
const raw = `${title ?? ""} ${description ?? ""}`.trim();
if (!raw) return "";
const cleaned = raw.normalize("NFKC").replace(REMOVABLE_TOKEN_FILTER, " ").replace(NOISE_CHARACTER_FILTER, " ").replace(WHITESPACE_FILTER, " ").trim();
if (!LETTER_FILTER.test(cleaned)) return "";
return trimToMaxLength(cleaned, MAX_TEXT_LENGTH);
}
//#endregion
//#region src/utils/translateApis.ts
var SETTINGS_CACHE_TTL_MS = 5e3;
var IMMUTABLE_API_CACHE_TTL_MS = Number.MAX_SAFE_INTEGER;
var cachedTranslationService = null;
var cachedTranslationServiceAt = 0;
var cachedDetectService = null;
var cachedDetectServiceAt = 0;
async function getTranslationServiceCached() {
const now = Date.now();
if (cachedTranslationService && now - cachedTranslationServiceAt < SETTINGS_CACHE_TTL_MS) return cachedTranslationService;
const service = await votStorage.get("translationService", defaultTranslationService);
cachedTranslationService = String(service);
cachedTranslationServiceAt = now;
return cachedTranslationService;
}
async function getDetectServiceCached() {
const now = Date.now();
if (cachedDetectService && now - cachedDetectServiceAt < SETTINGS_CACHE_TTL_MS) return cachedDetectService;
const service = await votStorage.get("detectService", defaultDetectService);
cachedDetectService = String(service);
cachedDetectServiceAt = now;
return cachedDetectService;
}
var foswlyServices = ["yandexbrowser", "msedge"];
/**
* Limit: 10k symbols for yandex, 50k for msedge
*/
var FOSWLYTranslateAPI = new class {
isFOSWLYError(data) {
return Object.hasOwn(data, "error");
}
async request(path, opts = {}) {
try {
const data = await (await GM_fetch(`${foswlyTranslateUrl}${path}`, {
timeout: 3e3,
responseCache: {
ttlMs: IMMUTABLE_API_CACHE_TTL_MS,
cacheName: "vot-foswly-api-v1",
allowStaleOnError: true
},
...opts
})).json();
if (this.isFOSWLYError(data)) throw new Error(data.error);
return data;
} catch (err) {
console.error(`[VOT] Failed to get data from FOSWLY Translate API, because ${err instanceof Error ? err.message : String(err)}`);
return;
}
}
async translateMultiple(text, lang, service) {
const result = await this.request("/translate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text,
lang,
service
})
});
return result ? result.translations : text;
}
async translate(text, lang, service) {
const result = await this.request(`/translate?${new URLSearchParams({
text,
lang,
service
})}`);
return result ? result.translations[0] : text;
}
async detect(text, service) {
const result = await this.request(`/detect?${new URLSearchParams({
text,
service
})}`);
return result ? result.lang : "en";
}
}();
var RustServerAPI = { async detect(text) {
try {
return await (await GM_fetch(detectRustServerUrl, {
method: "POST",
body: text,
timeout: 3e3,
responseCache: {
ttlMs: IMMUTABLE_API_CACHE_TTL_MS,
cacheName: "vot-rust-detect-v1",
allowStaleOnError: true
}
})).text();
} catch (error) {
console.error(`[VOT] Error getting lang from text, because ${error.message}`);
return "en";
}
} };
async function translate(text, fromLang = "", toLang = "ru") {
if (fromLang && toLang && fromLang === toLang) return text;
const service = await getTranslationServiceCached();
switch (service) {
case "yandexbrowser":
case "msedge": {
const langPair = fromLang && toLang ? `${fromLang}-${toLang}` : toLang;
return Array.isArray(text) ? await FOSWLYTranslateAPI.translateMultiple(text, langPair, service) : await FOSWLYTranslateAPI.translate(text, langPair, service);
}
default: return text;
}
}
async function detect(text) {
const service = await getDetectServiceCached();
switch (service) {
case "yandexbrowser":
case "msedge": return await FOSWLYTranslateAPI.detect(text, service);
case "rust-server": return await RustServerAPI.detect(text);
default: return "en";
}
}
var detectServices = [...foswlyServices, "rust-server"];
//#endregion
//#region src/utils/volume.ts
var VIDEO_VOLUME_MIN_PERCENT = 0;
var VIDEO_VOLUME_MAX_PERCENT = 100;
var VIDEO_VOLUME_STEP_01 = .01;
var EPS = 1e-6;
function clampInt(value, min, max) {
return Math.trunc(clampNumber$1(value, min, max));
}
function clampPercentInt(value, min = VIDEO_VOLUME_MIN_PERCENT, max = VIDEO_VOLUME_MAX_PERCENT) {
if (!Number.isFinite(value)) return min;
return clampInt(Math.round(value), min, max);
}
function volume01ToPercent(volume01) {
return clampPercentInt(clampNumber$1(volume01, 0, 1) * 100);
}
function percentToVolume01(percent) {
return clampPercentInt(percent) / 100;
}
function quantizeToStep(value, step, direction) {
if (!Number.isFinite(value)) return value;
if (!Number.isFinite(step) || step <= 0) return value;
const inv = 1 / step;
const scaled = value * inv;
switch (direction) {
case "down": return Math.floor(scaled + EPS) / inv;
case "up": return Math.ceil(scaled - EPS) / inv;
default: return Math.round(scaled) / inv;
}
}
function snapVolume01(volume01, direction = "nearest", step = VIDEO_VOLUME_STEP_01) {
return clampNumber$1(quantizeToStep(clampNumber$1(volume01, 0, 1), step, direction), 0, 1);
}
function snapVolume01Towards(next, current, desired, step = VIDEO_VOLUME_STEP_01) {
const cur = clampNumber$1(current, 0, 1);
const des = clampNumber$1(desired, 0, 1);
if (des < cur) {
const q = snapVolume01(next, "down", step);
return Math.max(des, q);
}
if (des > cur) {
const q = snapVolume01(next, "up", step);
return Math.min(des, q);
}
return snapVolume01(next, "nearest", step);
}
//#endregion
//#region src/core/hostPolicies.ts
var EXTERNAL_VOLUME_HOSTS = new Set(["youtube", "googledrive"]);
var YOUTUBE_LIKE_HOSTS = EXTERNAL_VOLUME_HOSTS;
var MUTE_SYNC_DISABLED_HOSTS = new Set(["rutube", "ok"]);
var TRANSLATION_DOWNLOAD_HOSTS = new Set([
"youtube",
"invidious",
"piped"
]);
function isExternalVolumeHost(host) {
return EXTERNAL_VOLUME_HOSTS.has(host);
}
function isYouTubeLikeHost(host) {
return YOUTUBE_LIKE_HOSTS.has(host);
}
function isMuteSyncDisabledHost(host) {
return MUTE_SYNC_DISABLED_HOSTS.has(host);
}
function isDesktopYouTubeLikeSite(site) {
return isYouTubeLikeHost(site.host) && site.additionalData !== "mobile";
}
function isTranslationDownloadHost(host) {
return TRANSLATION_DOWNLOAD_HOSTS.has(host);
}
//#endregion
//#region src/core/videoManager.ts
var FORCED_DETECTED_LANGUAGE_BY_HOST = {
rutube: "ru",
"ok.ru": "ru",
mail_ru: "ru",
weverse: "ko",
niconico: "ja",
youku: "zh",
bilibili: "zh",
weibo: "zh",
zdf: "de"
};
var YT_VOLUME_NOW_SELECTOR = ".ytp-volume-panel [aria-valuenow]";
var YT_PLAYER_VOLUME_STORAGE_KEY = "yt-player-volume";
var MIN_DETECT_TEXT_LENGTH = 35;
var MAX_SHARED_LANGUAGE_STATES = 500;
var REQUEST_LANG_SET = new Set(availableLangs);
/**
* Shared language caches across VideoManager instances within one frame.
*
* YouTube Shorts can transiently create multiple video handlers while the URL
* (and therefore resolved `videoId`) still points to the same active short.
* Per-instance caches are insufficient in that case and can trigger duplicate
* language detection requests.
*/
var sharedLanguageStateByVideoId = /* @__PURE__ */ new Map();
function getSharedLanguageState(videoId) {
const cachedState = sharedLanguageStateByVideoId.get(videoId);
if (cachedState) return cachedState;
const createdState = {};
sharedLanguageStateByVideoId.set(videoId, createdState);
while (sharedLanguageStateByVideoId.size > MAX_SHARED_LANGUAGE_STATES) {
const oldestVideoId = sharedLanguageStateByVideoId.keys().next().value;
if (typeof oldestVideoId !== "string") break;
sharedLanguageStateByVideoId.delete(oldestVideoId);
}
return createdState;
}
function normalizeToRequestLang(value) {
if (typeof value !== "string") return void 0;
const normalized = value.toLowerCase().split(/[-_]/)[0];
return REQUEST_LANG_SET.has(normalized) ? normalized : void 0;
}
function isResolvedLanguage(value) {
return Boolean(value && value !== "auto");
}
function buildDetectText(title, description) {
return cleanText(typeof title === "string" ? title : "", typeof description === "string" ? description : void 0);
}
function resolveHostDetectedLanguage(host) {
const forcedDetectedLanguage = FORCED_DETECTED_LANGUAGE_BY_HOST[host];
if (forcedDetectedLanguage) return forcedDetectedLanguage;
if (host === "vk") {
const trackLang = document.getElementsByTagName("track")?.[0]?.srclang;
return normalizeToRequestLang(trackLang);
}
}
function resolveYoutubeDetectedLanguageFromSubtitles(subtitles) {
if (!Array.isArray(subtitles) || subtitles.length === 0) return;
const pickLanguage = (preferManual) => {
for (const subtitle of subtitles) {
if (!subtitle || typeof subtitle !== "object") continue;
const candidate = subtitle;
if (candidate.source !== "youtube") continue;
if (typeof candidate.translatedFromLanguage === "string") continue;
if (preferManual && candidate.isAutoGenerated === true) continue;
const language = normalizeToRequestLang(candidate.language);
if (isResolvedLanguage(language)) return language;
}
};
return pickLanguage(true) ?? pickLanguage(false);
}
function getYoutubeVolumeStorageSnapshot() {
try {
const storage = globalThis.localStorage;
return {
storage,
value: storage.getItem(YT_PLAYER_VOLUME_STORAGE_KEY)
};
} catch {
return null;
}
}
function restoreYoutubeVolumeStorageSnapshot(snapshot) {
if (!snapshot) return;
try {
if (snapshot.value === null) snapshot.storage.removeItem(YT_PLAYER_VOLUME_STORAGE_KEY);
else snapshot.storage.setItem(YT_PLAYER_VOLUME_STORAGE_KEY, snapshot.value);
} catch {}
}
function preserveYoutubeVolumeStorage(action) {
const snapshot = getYoutubeVolumeStorageSnapshot();
const restoreSnapshot = () => restoreYoutubeVolumeStorageSnapshot(snapshot);
try {
return action();
} finally {
restoreSnapshot();
if (snapshot && typeof globalThis.setTimeout === "function") globalThis.setTimeout(restoreSnapshot, 0);
}
}
async function resolveDetectedLanguageForVideo(options) {
if (options.isStream) return { detectedLanguage: "auto" };
if (options.userOverrideLanguage) return { detectedLanguage: options.userOverrideLanguage };
const hostDetectedLanguage = resolveHostDetectedLanguage(options.host);
if (isResolvedLanguage(hostDetectedLanguage)) return {
detectedLanguage: hostDetectedLanguage,
cacheLanguage: hostDetectedLanguage
};
const normalizedPossibleLanguage = normalizeToRequestLang(options.possibleLanguage);
if (isResolvedLanguage(normalizedPossibleLanguage)) return {
detectedLanguage: normalizedPossibleLanguage,
cacheLanguage: normalizedPossibleLanguage
};
const youtubeSubtitleDetectedLanguage = options.host === "youtube" ? resolveYoutubeDetectedLanguageFromSubtitles(options.subtitles) : void 0;
if (isResolvedLanguage(youtubeSubtitleDetectedLanguage)) return {
detectedLanguage: youtubeSubtitleDetectedLanguage,
cacheLanguage: youtubeSubtitleDetectedLanguage
};
if (options.cachedDetectedLanguage) return { detectedLanguage: options.cachedDetectedLanguage };
if (!options.allowTextLanguageDetection) return { detectedLanguage: "auto" };
const text = buildDetectText(options.title, options.description);
if (!text || text.length < MIN_DETECT_TEXT_LENGTH) return { detectedLanguage: "auto" };
const detectedLanguage = await options.detectLanguage(text);
if (!detectedLanguage) return { detectedLanguage: "auto" };
return {
detectedLanguage,
cacheLanguage: detectedLanguage
};
}
function getAriaValueNowPercent(selector) {
const el = document.querySelector(selector);
const rawNow = el?.getAttribute("aria-valuenow");
const rawMax = el?.getAttribute("aria-valuemax");
const now = rawNow == null ? NaN : Number.parseFloat(rawNow);
const max = rawMax == null ? NaN : Number.parseFloat(rawMax);
if (!Number.isFinite(now)) return null;
if (Number.isFinite(max) && max > 0) return clampPercentInt(now / max * 100);
return clampPercentInt(now);
}
var VOTVideoManager = class {
videoHandler;
constructor(videoHandler) {
this.videoHandler = videoHandler;
}
setDetectedLanguageCache(videoId, language) {
getSharedLanguageState(videoId).detectedLanguage = language;
}
rememberUserLanguageSelection(videoId, language) {
const normalizedLanguage = normalizeToRequestLang(language);
if (!isResolvedLanguage(normalizedLanguage)) {
const sharedLanguageState = sharedLanguageStateByVideoId.get(videoId);
if (sharedLanguageState) delete sharedLanguageState.userLanguageOverride;
return;
}
const sharedLanguageState = getSharedLanguageState(videoId);
sharedLanguageState.userLanguageOverride = normalizedLanguage;
sharedLanguageState.detectedLanguage = normalizedLanguage;
}
rememberDetectedLanguage(videoId, language) {
const normalizedLanguage = normalizeToRequestLang(language);
if (!isResolvedLanguage(normalizedLanguage)) return;
this.setDetectedLanguageCache(videoId, normalizedLanguage);
if (this.videoHandler.videoData?.videoId === videoId) this.videoHandler.videoData.detectedLanguage = normalizedLanguage;
}
async detectLanguageSingleFlight(videoId, text) {
const sharedLanguageState = getSharedLanguageState(videoId);
const inFlightDetect = sharedLanguageState.detectInFlight;
if (inFlightDetect !== void 0) return inFlightDetect;
const task = (async () => {
debug.log(`Detecting language text: ${text}`);
const language = normalizeToRequestLang(await detect(text));
return isResolvedLanguage(language) ? language : void 0;
})();
sharedLanguageState.detectInFlight = task;
try {
return await task;
} finally {
if (sharedLanguageState.detectInFlight === task) delete sharedLanguageState.detectInFlight;
}
}
async ensureDetectedLanguageForTranslation(videoData) {
if (!videoData?.videoId || videoData.detectedLanguage !== "auto") return;
const sharedLanguageState = getSharedLanguageState(videoData.videoId);
const { detectedLanguage, cacheLanguage } = await resolveDetectedLanguageForVideo({
isStream: videoData.isStream,
host: this.videoHandler.site.host,
possibleLanguage: videoData.detectedLanguage,
subtitles: videoData.subtitles,
userOverrideLanguage: sharedLanguageState.userLanguageOverride,
cachedDetectedLanguage: sharedLanguageState.detectedLanguage,
title: videoData.title,
description: videoData.description,
allowTextLanguageDetection: true,
detectLanguage: async (text) => await this.detectLanguageSingleFlight(videoData.videoId, text)
});
if (cacheLanguage) this.setDetectedLanguageCache(videoData.videoId, cacheLanguage);
if (!detectedLanguage || detectedLanguage === "auto") return;
this.videoHandler.setSelectMenuValues(detectedLanguage, this.videoHandler.translateToLang);
}
async getVideoData() {
const { duration, url, videoId, host, title, translationHelp = null, localizedTitle, description, detectedLanguage: possibleLanguage, subtitles, isStream = false } = await getVideoData(this.videoHandler.site, {
fetchFn: GM_fetch,
video: this.videoHandler.video,
language: localizationProvider.lang
});
const sharedLanguageState = getSharedLanguageState(videoId);
const { detectedLanguage, cacheLanguage } = await resolveDetectedLanguageForVideo({
isStream,
host: this.videoHandler.site.host,
possibleLanguage,
subtitles,
userOverrideLanguage: sharedLanguageState.userLanguageOverride,
cachedDetectedLanguage: sharedLanguageState.detectedLanguage,
title,
description,
allowTextLanguageDetection: false,
detectLanguage: async (text) => await this.detectLanguageSingleFlight(videoId, text)
});
if (cacheLanguage) this.setDetectedLanguageCache(videoId, cacheLanguage);
const videoData = {
translationHelp,
isStream,
duration: duration || this.videoHandler.video?.duration || config_default$1.defaultDuration,
videoId,
url,
host,
detectedLanguage,
responseLanguage: this.videoHandler.translateToLang,
subtitles,
title,
localizedTitle,
description,
downloadTitle: localizedTitle ?? title ?? document.title ?? videoId
};
if (sharedLanguageState.lastLoggedDetectedLanguage !== detectedLanguage) {
debug.log("[VOT] Detected language:", detectedLanguage);
sharedLanguageState.lastLoggedDetectedLanguage = detectedLanguage;
}
return videoData;
}
async videoValidator() {
const videoData = this.videoHandler.videoData;
const data = this.videoHandler.data;
if (!videoData || !data) throw new VOTLocalizedError("VOTNoVideoIDFound");
debug.log("VideoValidator videoData: ", this.videoHandler.videoData);
if (this.videoHandler.data.enabledDontTranslateLanguages && this.videoHandler.data.dontTranslateLanguages?.includes(this.videoHandler.videoData.detectedLanguage)) throw new VOTLocalizedError("VOTDisableFromYourLang");
if (this.videoHandler.videoData.isStream) throw new VOTLocalizedError("VOTStreamNotAvailable");
if (this.videoHandler.videoData.duration > 14400) throw new VOTLocalizedError("VOTVideoIsTooLong");
return true;
}
/**
* Gets current video volume (0.0 - 1.0)
*/
getVideoVolume() {
const video = this.videoHandler.video;
if (!video) return void 0;
if (isExternalVolumeHost(this.videoHandler.site.host)) {
const ariaPercent = getAriaValueNowPercent(YT_VOLUME_NOW_SELECTOR);
if (ariaPercent != null) return percentToVolume01(ariaPercent);
const extVolume = YoutubeHelper.getVolume();
if (typeof extVolume === "number" && Number.isFinite(extVolume)) return snapVolume01(extVolume);
}
return snapVolume01(video.volume);
}
/**
* Sets the video volume
*/
setVideoVolume(volume, options = {}) {
const snapped = snapVolume01(volume);
if (!isExternalVolumeHost(this.videoHandler.site.host)) {
this.videoHandler.video.volume = snapped;
return this;
}
try {
const setExternalVolume = () => YoutubeHelper.setVolume(snapped);
const result = options.preserveYoutubeVolumeStorage ? preserveYoutubeVolumeStorage(setExternalVolume) : setExternalVolume();
if (typeof result === "boolean" && result || typeof result === "number" && Number.isFinite(result)) return this;
} catch {}
this.videoHandler.video.volume = snapped;
return this;
}
setVideoMuted(muted, options = {}) {
if (isExternalVolumeHost(this.videoHandler.site.host)) {
const player = YoutubeHelper.getPlayer();
const method = muted ? player?.mute : player?.unMute;
if (typeof method === "function") try {
const setExternalMuted = () => method.call(player);
if (options.preserveYoutubeVolumeStorage) preserveYoutubeVolumeStorage(setExternalMuted);
else setExternalMuted();
} catch {}
}
if (this.videoHandler.video) this.videoHandler.video.muted = muted;
return this;
}
/**
* Checks if the video is muted
*/
isMuted() {
if (!isExternalVolumeHost(this.videoHandler.site.host)) return this.videoHandler.video?.muted;
return YoutubeHelper.isMuted() || Boolean(this.videoHandler.video?.muted);
}
/**
* Syncs the video volume slider with the actual video volume.
*/
syncVideoVolumeSlider() {
const overlayView = this.videoHandler.uiManager.votOverlayView;
if (!overlayView?.isInitialized()) return this;
const ariaPercent = isExternalVolumeHost(this.videoHandler.site.host) ? getAriaValueNowPercent(YT_VOLUME_NOW_SELECTOR) : null;
const volumePercent = this.isMuted() ? 0 : ariaPercent ?? volume01ToPercent(this.getVideoVolume() ?? 0);
overlayView.videoVolumeSlider.value = volumePercent;
this.videoHandler.onVideoVolumeSliderSynced?.(volumePercent);
return this;
}
setSelectMenuValues(from, to) {
const videoData = this.videoHandler.videoData;
if (!videoData) return this;
const normalizedFrom = normalizeToRequestLang(from) ?? "auto";
const langPairLogKey = `${normalizedFrom}->${to}`;
const sharedLanguageState = getSharedLanguageState(videoData.videoId);
if (sharedLanguageState.lastLoggedLangPair !== langPairLogKey) {
debug.log(`[VOT] Set translation from ${normalizedFrom} to ${to}`);
sharedLanguageState.lastLoggedLangPair = langPairLogKey;
}
videoData.detectedLanguage = normalizedFrom;
videoData.responseLanguage = to;
this.videoHandler.translateFromLang = normalizedFrom;
this.videoHandler.translateToLang = to;
const overlayView = this.videoHandler.uiManager.votOverlayView;
if (!overlayView?.isInitialized()) return this;
overlayView.languagePairSelect.fromSelect.selectTitle = localizationProvider.getLangLabel(normalizedFrom);
overlayView.languagePairSelect.toSelect.selectTitle = localizationProvider.getLangLabel(to);
overlayView.languagePairSelect.fromSelect.setSelectedValue(normalizedFrom);
overlayView.languagePairSelect.toSelect.setSelectedValue(to);
return this;
}
}, t = globalThis, i = (t) => t, s = t.trustedTypes, e = s ? s.createPolicy("lit-html", { createHTML: (t) => t }) : void 0, h = "$lit$", o = `lit$${Math.random().toFixed(9).slice(2)}$`, n = "?" + o, r = `<${n}>`, l = document, c = () => l.createComment(""), a = (t) => null === t || "object" != typeof t && "function" != typeof t, u = Array.isArray, d = (t) => u(t) || "function" == typeof t?.[Symbol.iterator], f = "[ \n\f\r]", v = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, _ = /-->/g, m = />/g, p = RegExp(`>|${f}(?:([^\\s"'>=/]+)(${f}*=${f}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`, "g"), g = /'/g, $ = /"/g, y = /^(?:script|style|textarea|title)$/i, x = (t) => (i, ...s) => ({
_$litType$: t,
strings: i,
values: s
}), b = x(1), w = x(2);
x(3);
//#endregion
//#region node_modules/lit-html/lit-html.js
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
var E = Symbol.for("lit-noChange"), A = Symbol.for("lit-nothing"), C = /* @__PURE__ */ new WeakMap(), P = l.createTreeWalker(l, 129);
function V(t, i) {
if (!u(t) || !t.hasOwnProperty("raw")) throw Error("invalid template strings array");
return void 0 !== e ? e.createHTML(i) : i;
}
var N = (t, i) => {
const s = t.length - 1, e = [];
let n, l = 2 === i ? "<svg>" : 3 === i ? "<math>" : "", c = v;
for (let i = 0; i < s; i++) {
const s = t[i];
let a, u, d = -1, f = 0;
for (; f < s.length && (c.lastIndex = f, u = c.exec(s), null !== u);) f = c.lastIndex, c === v ? "!--" === u[1] ? c = _ : void 0 !== u[1] ? c = m : void 0 !== u[2] ? (y.test(u[2]) && (n = RegExp("</" + u[2], "g")), c = p) : void 0 !== u[3] && (c = p) : c === p ? ">" === u[0] ? (c = n ?? v, d = -1) : void 0 === u[1] ? d = -2 : (d = c.lastIndex - u[2].length, a = u[1], c = void 0 === u[3] ? p : "\"" === u[3] ? $ : g) : c === $ || c === g ? c = p : c === _ || c === m ? c = v : (c = p, n = void 0);
const x = c === p && t[i + 1].startsWith("/>") ? " " : "";
l += c === v ? s + r : d >= 0 ? (e.push(a), s.slice(0, d) + h + s.slice(d) + o + x) : s + o + (-2 === d ? i : x);
}
return [V(t, l + (t[s] || "<?>") + (2 === i ? "</svg>" : 3 === i ? "</math>" : "")), e];
};
var S = class S {
constructor({ strings: t, _$litType$: i }, e) {
let r;
this.parts = [];
let l = 0, a = 0;
const u = t.length - 1, d = this.parts, [f, v] = N(t, i);
if (this.el = S.createElement(f, e), P.currentNode = this.el.content, 2 === i || 3 === i) {
const t = this.el.content.firstChild;
t.replaceWith(...t.childNodes);
}
for (; null !== (r = P.nextNode()) && d.length < u;) {
if (1 === r.nodeType) {
if (r.hasAttributes()) for (const t of r.getAttributeNames()) if (t.endsWith(h)) {
const i = v[a++], s = r.getAttribute(t).split(o), e = /([.?@])?(.*)/.exec(i);
d.push({
type: 1,
index: l,
name: e[2],
strings: s,
ctor: "." === e[1] ? I : "?" === e[1] ? L : "@" === e[1] ? z : H
}), r.removeAttribute(t);
} else t.startsWith(o) && (d.push({
type: 6,
index: l
}), r.removeAttribute(t));
if (y.test(r.tagName)) {
const t = r.textContent.split(o), i = t.length - 1;
if (i > 0) {
r.textContent = s ? s.emptyScript : "";
for (let s = 0; s < i; s++) r.append(t[s], c()), P.nextNode(), d.push({
type: 2,
index: ++l
});
r.append(t[i], c());
}
}
} else if (8 === r.nodeType) if (r.data === n) d.push({
type: 2,
index: l
});
else {
let t = -1;
for (; -1 !== (t = r.data.indexOf(o, t + 1));) d.push({
type: 7,
index: l
}), t += o.length - 1;
}
l++;
}
}
static createElement(t, i) {
const s = l.createElement("template");
return s.innerHTML = t, s;
}
};
function M(t, i, s = t, e) {
if (i === E) return i;
let h = void 0 !== e ? s._$Co?.[e] : s._$Cl;
const o = a(i) ? void 0 : i._$litDirective$;
return h?.constructor !== o && (h?._$AO?.(!1), void 0 === o ? h = void 0 : (h = new o(t), h._$AT(t, s, e)), void 0 !== e ? (s._$Co ??= [])[e] = h : s._$Cl = h), void 0 !== h && (i = M(t, h._$AS(t, i.values), h, e)), i;
}
var R = class {
constructor(t, i) {
this._$AV = [], this._$AN = void 0, this._$AD = t, this._$AM = i;
}
get parentNode() {
return this._$AM.parentNode;
}
get _$AU() {
return this._$AM._$AU;
}
u(t) {
const { el: { content: i }, parts: s } = this._$AD, e = (t?.creationScope ?? l).importNode(i, !0);
P.currentNode = e;
let h = P.nextNode(), o = 0, n = 0, r = s[0];
for (; void 0 !== r;) {
if (o === r.index) {
let i;
2 === r.type ? i = new k(h, h.nextSibling, this, t) : 1 === r.type ? i = new r.ctor(h, r.name, r.strings, this, t) : 6 === r.type && (i = new Z(h, this, t)), this._$AV.push(i), r = s[++n];
}
o !== r?.index && (h = P.nextNode(), o++);
}
return P.currentNode = l, e;
}
p(t) {
let i = 0;
for (const s of this._$AV) void 0 !== s && (void 0 !== s.strings ? (s._$AI(t, s, i), i += s.strings.length - 2) : s._$AI(t[i])), i++;
}
};
var k = class k {
get _$AU() {
return this._$AM?._$AU ?? this._$Cv;
}
constructor(t, i, s, e) {
this.type = 2, this._$AH = A, this._$AN = void 0, this._$AA = t, this._$AB = i, this._$AM = s, this.options = e, this._$Cv = e?.isConnected ?? !0;
}
get parentNode() {
let t = this._$AA.parentNode;
const i = this._$AM;
return void 0 !== i && 11 === t?.nodeType && (t = i.parentNode), t;
}
get startNode() {
return this._$AA;
}
get endNode() {
return this._$AB;
}
_$AI(t, i = this) {
t = M(this, t, i), a(t) ? t === A || null == t || "" === t ? (this._$AH !== A && this._$AR(), this._$AH = A) : t !== this._$AH && t !== E && this._(t) : void 0 !== t._$litType$ ? this.$(t) : void 0 !== t.nodeType ? this.T(t) : d(t) ? this.k(t) : this._(t);
}
O(t) {
return this._$AA.parentNode.insertBefore(t, this._$AB);
}
T(t) {
this._$AH !== t && (this._$AR(), this._$AH = this.O(t));
}
_(t) {
this._$AH !== A && a(this._$AH) ? this._$AA.nextSibling.data = t : this.T(l.createTextNode(t)), this._$AH = t;
}
$(t) {
const { values: i, _$litType$: s } = t, e = "number" == typeof s ? this._$AC(t) : (void 0 === s.el && (s.el = S.createElement(V(s.h, s.h[0]), this.options)), s);
if (this._$AH?._$AD === e) this._$AH.p(i);
else {
const t = new R(e, this), s = t.u(this.options);
t.p(i), this.T(s), this._$AH = t;
}
}
_$AC(t) {
let i = C.get(t.strings);
return void 0 === i && C.set(t.strings, i = new S(t)), i;
}
k(t) {
u(this._$AH) || (this._$AH = [], this._$AR());
const i = this._$AH;
let s, e = 0;
for (const h of t) e === i.length ? i.push(s = new k(this.O(c()), this.O(c()), this, this.options)) : s = i[e], s._$AI(h), e++;
e < i.length && (this._$AR(s && s._$AB.nextSibling, e), i.length = e);
}
_$AR(t = this._$AA.nextSibling, s) {
for (this._$AP?.(!1, !0, s); t !== this._$AB;) {
const s = i(t).nextSibling;
i(t).remove(), t = s;
}
}
setConnected(t) {
void 0 === this._$AM && (this._$Cv = t, this._$AP?.(t));
}
};
var H = class {
get tagName() {
return this.element.tagName;
}
get _$AU() {
return this._$AM._$AU;
}
constructor(t, i, s, e, h) {
this.type = 1, this._$AH = A, this._$AN = void 0, this.element = t, this.name = i, this._$AM = e, this.options = h, s.length > 2 || "" !== s[0] || "" !== s[1] ? (this._$AH = Array(s.length - 1).fill(/* @__PURE__ */ new String()), this.strings = s) : this._$AH = A;
}
_$AI(t, i = this, s, e) {
const h = this.strings;
let o = !1;
if (void 0 === h) t = M(this, t, i, 0), o = !a(t) || t !== this._$AH && t !== E, o && (this._$AH = t);
else {
const e = t;
let n, r;
for (t = h[0], n = 0; n < h.length - 1; n++) r = M(this, e[s + n], i, n), r === E && (r = this._$AH[n]), o ||= !a(r) || r !== this._$AH[n], r === A ? t = A : t !== A && (t += (r ?? "") + h[n + 1]), this._$AH[n] = r;
}
o && !e && this.j(t);
}
j(t) {
t === A ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t ?? "");
}
};
var I = class extends H {
constructor() {
super(...arguments), this.type = 3;
}
j(t) {
this.element[this.name] = t === A ? void 0 : t;
}
};
var L = class extends H {
constructor() {
super(...arguments), this.type = 4;
}
j(t) {
this.element.toggleAttribute(this.name, !!t && t !== A);
}
};
var z = class extends H {
constructor(t, i, s, e, h) {
super(t, i, s, e, h), this.type = 5;
}
_$AI(t, i = this) {
if ((t = M(this, t, i, 0) ?? A) === E) return;
const s = this._$AH, e = t === A && s !== A || t.capture !== s.capture || t.once !== s.once || t.passive !== s.passive, h = t !== A && (s === A || e);
e && this.element.removeEventListener(this.name, this, s), h && this.element.addEventListener(this.name, this, t), this._$AH = t;
}
handleEvent(t) {
"function" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t) : this._$AH.handleEvent(t);
}
};
var Z = class {
constructor(t, i, s) {
this.element = t, this.type = 6, this._$AN = void 0, this._$AM = i, this.options = s;
}
get _$AU() {
return this._$AM._$AU;
}
_$AI(t) {
M(this, t);
}
}, B = t.litHtmlPolyfillSupport;
B?.(S, k), (t.litHtmlVersions ??= []).push("3.3.2");
var D = (t, i, s) => {
const e = s?.renderBefore ?? i;
let h = e._$litPart$;
if (void 0 === h) {
const t = s?.renderBefore ?? null;
e._$litPart$ = h = new k(i.insertBefore(c(), t), t, void 0, s ?? {});
}
return h._$AI(t), h;
};
//#endregion
//#region src/ui/components/componentShared.ts
function setInteractiveHiddenState(element, isHidden) {
element.hidden = isHidden;
element.setAttribute("aria-hidden", isHidden ? "true" : "false");
element.toggleAttribute("inert", isHidden);
}
function createDomId(prefix) {
return `${prefix}-${typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : Math.random().toString(36).slice(2)}`;
}
function isEventInside(event, element) {
const target = event.target;
if (target instanceof Node && element.contains(target)) return true;
return typeof event.composedPath === "function" && event.composedPath().includes(element);
}
function isPrimaryPointerAction(event) {
return event.isPrimary && event.button === 0;
}
function isKeyboardActivation(event) {
return event.key === "Enter" || event.key === " ";
}
function addKeyboardActivationListener(element, handler, options) {
element.addEventListener("keydown", (event) => {
if (!isKeyboardActivation(event)) return;
event.preventDefault();
handler();
}, options);
}
//#endregion
//#region src/ui.ts
function initKeyboardNavigationMode() {
if (globalThis.__votKeyboardNavInitialized) return;
globalThis.__votKeyboardNavInitialized = true;
const root = document.documentElement;
const CLASS = "vot-keyboard-nav";
const enable = () => root.classList.add(CLASS);
const disable = () => root.classList.remove(CLASS);
globalThis.addEventListener("keydown", (e) => {
if (e.key === "Tab") enable();
}, true);
for (const evt of [
"pointerdown",
"mousedown",
"touchstart"
]) globalThis.addEventListener(evt, disable, {
capture: true,
passive: true
});
}
initKeyboardNavigationMode();
var UI = {
makeButtonLike(el, { ariaLabel } = {}) {
el.setAttribute("role", "button");
if (!el.hasAttribute("tabindex")) el.tabIndex = 0;
const enabledTabIndex = el.tabIndex;
const syncDisabledState = () => {
if (el.getAttribute("disabled") === "true") {
el.setAttribute("aria-disabled", "true");
el.tabIndex = -1;
} else {
el.removeAttribute("aria-disabled");
el.tabIndex = enabledTabIndex;
}
};
syncDisabledState();
new MutationObserver(() => syncDisabledState()).observe(el, {
attributes: true,
attributeFilter: ["disabled"]
});
if (ariaLabel) el.setAttribute("aria-label", ariaLabel);
addKeyboardActivationListener(el, () => {
if (el.getAttribute("disabled") === "true" || el.getAttribute("aria-disabled") === "true") return;
el.click();
});
return el;
},
createEl(tag, classes = [], content = null) {
const el = document.createElement(tag);
if (classes.length) el.classList.add(...classes);
if (content !== null) el.append(content);
return el;
},
createHeader(html, level = 4) {
return UI.createEl("vot-block", ["vot-header", `vot-header-level-${level}`], html);
},
createInformation(labelHtml, valueHtml) {
const container = UI.createEl("vot-block", ["vot-info"]);
const header = UI.createEl("vot-block");
D(labelHtml, header);
const value = UI.createEl("vot-block");
D(valueHtml, value);
container.append(header, value);
return {
container,
header,
value
};
},
createButton(html) {
const el = UI.createEl("vot-block", ["vot-button"], html);
return UI.makeButtonLike(el);
},
createTextButton(html) {
const el = UI.createEl("vot-block", ["vot-text-button"], html);
return UI.makeButtonLike(el);
},
createOutlinedButton(html) {
const el = UI.createEl("vot-block", ["vot-outlined-button"], html);
return UI.makeButtonLike(el);
},
createIconButton(templateHtml, options = {}) {
const button = UI.createEl("vot-block", ["vot-icon-button"]);
D(templateHtml, button);
return UI.makeButtonLike(button, options);
},
createInlineLoader() {
return UI.createEl("vot-block", ["vot-inline-loader"]);
},
createPortal(local = false) {
return UI.createEl("vot-block", [`vot-portal${local ? "-local" : ""}`]);
},
createSubtitleInfo(word, desc, translationService) {
const container = UI.createEl("vot-block", ["vot-subtitles-info"]);
container.id = "vot-subtitles-info";
const translatedWith = UI.createEl("vot-block", ["vot-subtitles-info-service"], localizationProvider.get("VOTTranslatedBy").replace("{0}", translationService));
const header = UI.createEl("vot-block", ["vot-subtitles-info-header"], word);
const context = UI.createEl("vot-block", ["vot-subtitles-info-context"], desc);
container.append(translatedWith, header, context);
return {
container,
translatedWith,
header,
context
};
}
};
//#endregion
//#region src/types/components/tooltip.ts
var positions$1 = [
"left",
"top",
"right",
"bottom"
];
var triggers = ["hover", "click"];
//#endregion
//#region src/ui/components/tooltip.ts
var Tooltip = class Tooltip {
/** Whether tooltip element is currently mounted. */
showed = false;
target;
anchor;
content;
position;
preferredPosition;
trigger;
parentElement;
layoutRoot;
offsetX;
offsetY;
_hidden;
autoLayout;
pageWidth;
pageHeight;
globalOffsetX;
globalOffsetY;
renderOffsetX;
renderOffsetY;
maxWidth;
backgroundColor;
borderRadius;
_bordered;
container;
onResizeObserver;
intersectionObserver;
scrollListening = false;
positionRafId = null;
destroyFallbackTimerId;
static DESTROY_FALLBACK_MS = 700;
tooltipId = createDomId("vot-tooltip");
prevAriaDescribedBy = null;
constructor({ target, anchor = void 0, content = "", position = "top", trigger = "hover", offset = 4, maxWidth = void 0, hidden = false, autoLayout = true, backgroundColor = void 0, borderRadius = void 0, bordered = true, parentElement = document.body, layoutRoot = document.documentElement }) {
if (!(target instanceof HTMLElement)) throw new TypeError("target must be a valid HTMLElement");
this.target = target;
this.anchor = anchor instanceof HTMLElement ? anchor : target;
this.content = content;
if (typeof offset === "number") this.offsetY = this.offsetX = offset;
else {
this.offsetX = offset.x;
this.offsetY = offset.y;
}
this._hidden = hidden;
this.autoLayout = autoLayout;
this.trigger = Tooltip.validateTrigger(trigger) ? trigger : "hover";
this.position = Tooltip.validatePos(position) ? position : "top";
this.preferredPosition = this.position;
this.parentElement = parentElement;
this.layoutRoot = layoutRoot;
this.borderRadius = borderRadius;
this._bordered = bordered;
this.maxWidth = maxWidth;
this.backgroundColor = backgroundColor;
this.updatePageSize();
this.init();
}
static validatePos(position) {
return positions$1.includes(position);
}
static validateTrigger(trigger) {
return triggers.includes(trigger);
}
setPosition(position) {
this.preferredPosition = Tooltip.validatePos(position) ? position : "top";
this.position = this.preferredPosition;
this.schedulePositionUpdate();
return this;
}
setContent(content) {
this.content = content;
if (this.container) {
this.container.replaceChildren();
if (typeof content === "string") this.container.textContent = content;
else this.container.append(content);
this.schedulePositionUpdate();
return this;
}
return this;
}
/**
* Update tooltip mount dependencies.
* If the tooltip is currently rendered, it will be moved to the new parent.
*/
updateMount({ parentElement, layoutRoot }) {
if (parentElement && this.parentElement !== parentElement) {
this.parentElement = parentElement;
if (this.container?.isConnected) parentElement.appendChild(this.container);
}
if (layoutRoot && this.layoutRoot !== layoutRoot) this.layoutRoot = layoutRoot;
this.schedulePositionUpdate();
return this;
}
onResize = () => {
this.schedulePositionUpdate();
};
onClick = () => {
this.showed ? this.destroy() : this.create();
};
onDocumentPointerDown = (event) => {
if (!this.showed) return;
if (isEventInside(event, this.target) || this.container && isEventInside(event, this.container)) return;
this.destroy();
};
onTargetKeyDown = (event) => {
if (event.key !== "Escape" || !this.showed) return;
this.destroy();
};
onScroll = () => {
this.schedulePositionUpdate();
};
onHoverPointerDown = (e) => {
if (e.pointerType === "mouse") return;
this.create();
};
onHoverPointerUp = (e) => {
if (e.pointerType === "mouse") return;
this.destroy();
};
onMouseEnter = () => {
this.create();
};
onMouseLeave = (event) => {
if (this.isInTooltipContext(event.relatedTarget)) return;
this.destroy();
};
onTooltipMouseLeave = (event) => {
if (this.isInTooltipContext(event.relatedTarget)) return;
this.destroy();
};
isInTooltipContext(nextTarget) {
if (!(nextTarget instanceof Node)) return false;
return this.target.contains(nextTarget) || this.container?.contains(nextTarget);
}
updatePageSize() {
if (this.layoutRoot === document.documentElement) {
this.globalOffsetX = 0;
this.globalOffsetY = 0;
} else {
const { left, top } = this.layoutRoot.getBoundingClientRect();
this.globalOffsetX = left;
this.globalOffsetY = top;
}
const { left: parentLeft, top: parentTop } = (this.parentElement instanceof ShadowRoot ? this.parentElement.host : this.parentElement).getBoundingClientRect();
this.renderOffsetX = parentLeft;
this.renderOffsetY = parentTop;
this.pageWidth = this.layoutRoot.clientWidth || document.documentElement.clientWidth;
this.pageHeight = this.layoutRoot.clientHeight || document.documentElement.clientHeight;
return this;
}
onIntersect = ([entry]) => {
if (!entry.isIntersecting) return this.destroy(true);
};
init() {
this.onResizeObserver = new ResizeObserver(this.onResize);
this.intersectionObserver = new IntersectionObserver(this.onIntersect);
this.target.addEventListener("keydown", this.onTargetKeyDown);
if (this.trigger === "click") {
this.target.addEventListener("pointerdown", this.onClick);
return this;
}
this.target.addEventListener("mouseenter", this.onMouseEnter);
this.target.addEventListener("mouseleave", this.onMouseLeave);
this.target.addEventListener("focusin", this.onMouseEnter);
this.target.addEventListener("focusout", this.onMouseLeave);
this.target.addEventListener("pointerdown", this.onHoverPointerDown);
this.target.addEventListener("pointerup", this.onHoverPointerUp);
return this;
}
release() {
this.destroy(true);
this.detachScrollListener();
this.target.removeEventListener("keydown", this.onTargetKeyDown);
if (this.trigger === "click") {
this.target.removeEventListener("pointerdown", this.onClick);
return this;
}
this.target.removeEventListener("mouseenter", this.onMouseEnter);
this.target.removeEventListener("mouseleave", this.onMouseLeave);
this.target.removeEventListener("focusin", this.onMouseEnter);
this.target.removeEventListener("focusout", this.onMouseLeave);
this.target.removeEventListener("pointerdown", this.onHoverPointerDown);
this.target.removeEventListener("pointerup", this.onHoverPointerUp);
return this;
}
schedulePositionUpdate() {
if (!this.container) return;
if (this.positionRafId !== null) return;
this.positionRafId = requestAnimationFrame(() => {
this.positionRafId = null;
this.updatePageSize();
this.updatePos();
});
}
cancelPositionUpdate() {
if (this.positionRafId === null) return;
cancelAnimationFrame(this.positionRafId);
this.positionRafId = null;
}
clearDestroyFallbackTimer() {
if (this.destroyFallbackTimerId === void 0) return;
globalThis.clearTimeout(this.destroyFallbackTimerId);
this.destroyFallbackTimerId = void 0;
}
create() {
this.destroy(true);
this.showed = true;
this.container = UI.createEl("vot-block", ["vot-tooltip"], this.content);
if (this.bordered) this.container.classList.add("vot-tooltip-bordered");
this.container.setAttribute("role", "tooltip");
this.container.id = this.tooltipId;
this.container.dataset.trigger = this.trigger;
this.container.dataset.position = this.position;
this.parentElement.appendChild(this.container);
this.schedulePositionUpdate();
if (this.backgroundColor !== void 0) this.container.style.backgroundColor = this.backgroundColor;
if (this.borderRadius !== void 0) this.container.style.borderRadius = `${this.borderRadius}px`;
if (this.hidden) this.container.hidden = true;
else this.syncAriaDescribedBy(true);
this.container.style.opacity = "1";
if (this.trigger === "hover") this.container.addEventListener("mouseleave", this.onTooltipMouseLeave);
else document.addEventListener("pointerdown", this.onDocumentPointerDown, {
capture: true,
passive: true
});
this.attachScrollListener();
this.onResizeObserver?.observe(this.layoutRoot);
if (this.anchor !== this.layoutRoot) this.onResizeObserver?.observe(this.anchor);
this.intersectionObserver?.observe(this.target);
return this;
}
updatePos() {
if (!this.container) return this;
const { top, left } = this.calcPos(this.autoLayout, this.preferredPosition);
const availableWidth = Math.max(0, this.pageWidth - this.offsetX * 2);
const maxWidth = clamp(this.maxWidth ?? availableWidth, 0, availableWidth);
this.container.style.transform = `translate(${left}px, ${top}px)`;
this.container.dataset.position = this.position;
this.container.style.maxWidth = `${maxWidth}px`;
return this;
}
calcPos(autoLayout = true, position = this.preferredPosition) {
if (!this.container) return {
top: 0,
left: 0
};
const { left: anchorLeft, right: anchorRight, top: anchorTop, bottom: anchorBottom, width: anchorWidth, height: anchorHeight } = this.anchor.getBoundingClientRect();
const { width: containerWidth, height: containerHeight } = this.container.getBoundingClientRect();
const width = clamp(containerWidth, 0, this.pageWidth);
const height = clamp(containerHeight, 0, this.pageHeight);
const anchorBox = {
left: anchorLeft - this.globalOffsetX,
right: anchorRight - this.globalOffsetX,
top: anchorTop - this.globalOffsetY,
bottom: anchorBottom - this.globalOffsetY,
anchorWidth,
anchorHeight
};
const size = {
width,
height
};
const resolvedPosition = this.resolveTooltipPosition(anchorBox, size, position, autoLayout);
const coords = this.getTooltipCoordinates(anchorBox, size, resolvedPosition);
this.position = resolvedPosition;
return {
top: coords.top + this.globalOffsetY - this.renderOffsetY,
left: coords.left + this.globalOffsetX - this.renderOffsetX
};
}
resolveTooltipPosition(anchorBox, size, position, autoLayout) {
if (!autoLayout) return position;
switch (position) {
case "top": return clamp(anchorBox.top - size.height - this.offsetY, 0, this.pageHeight) + this.offsetY < size.height ? "bottom" : "top";
case "right": return clamp(anchorBox.right + this.offsetX, 0, this.pageWidth - size.width) + size.width > this.pageWidth - this.offsetX ? "left" : "right";
case "bottom": return clamp(anchorBox.bottom + this.offsetY, 0, this.pageHeight - size.height) + size.height > this.pageHeight - this.offsetY ? "top" : "bottom";
case "left": return Math.max(0, anchorBox.left - size.width - this.offsetX) + size.width > anchorBox.left - this.offsetX ? "right" : "left";
default: return position;
}
}
getTooltipCoordinates(anchorBox, size, position) {
switch (position) {
case "top": return {
top: clamp(anchorBox.top - size.height - this.offsetY, 0, this.pageHeight),
left: clamp(anchorBox.left - size.width / 2 + anchorBox.anchorWidth / 2, this.offsetX, this.pageWidth - size.width - this.offsetX)
};
case "right": return {
top: clamp(anchorBox.top + (anchorBox.anchorHeight - size.height) / 2, this.offsetY, this.pageHeight - size.height - this.offsetY),
left: clamp(anchorBox.right + this.offsetX, 0, this.pageWidth - size.width)
};
case "bottom": return {
top: clamp(anchorBox.bottom + this.offsetY, 0, this.pageHeight - size.height),
left: clamp(anchorBox.left - size.width / 2 + anchorBox.anchorWidth / 2, this.offsetX, this.pageWidth - size.width - this.offsetX)
};
case "left": return {
top: clamp(anchorBox.top + (anchorBox.anchorHeight - size.height) / 2, this.offsetY, this.pageHeight - size.height - this.offsetY),
left: Math.max(0, anchorBox.left - size.width - this.offsetX)
};
default: return {
top: 0,
left: 0
};
}
}
destroy(instant = false) {
if (!this.container) return this;
const container = this.container;
this.cancelPositionUpdate();
this.clearDestroyFallbackTimer();
this.showed = false;
this.syncAriaDescribedBy(false);
this.onResizeObserver?.disconnect();
this.intersectionObserver?.disconnect();
this.detachScrollListener();
this.detachOutsidePointerListener();
if (instant) {
container.remove();
this.container = void 0;
return this;
}
container.removeEventListener("mouseleave", this.onTooltipMouseLeave);
container.style.pointerEvents = "none";
container.style.opacity = "0";
const handleTransitionDone = () => {
this.clearDestroyFallbackTimer();
container?.remove();
if (this.container === container) this.container = void 0;
};
container.addEventListener("transitionend", handleTransitionDone, { once: true });
container.addEventListener("transitioncancel", handleTransitionDone, { once: true });
this.destroyFallbackTimerId = globalThis.setTimeout(handleTransitionDone, Tooltip.DESTROY_FALLBACK_MS);
return this;
}
detachOutsidePointerListener() {
document.removeEventListener("pointerdown", this.onDocumentPointerDown, { capture: true });
}
syncAriaDescribedBy(isShowing) {
const existing = this.target.getAttribute("aria-describedby");
this.prevAriaDescribedBy ??= existing;
if (!isShowing) {
if (this.prevAriaDescribedBy === null) this.target.removeAttribute("aria-describedby");
else this.target.setAttribute("aria-describedby", this.prevAriaDescribedBy);
this.prevAriaDescribedBy = null;
return;
}
const tokens = new Set((existing ?? "").split(/\s+/).filter(Boolean));
tokens.add(this.tooltipId);
this.target.setAttribute("aria-describedby", Array.from(tokens).join(" "));
}
set bordered(isBordered) {
this._bordered = isBordered;
this.container?.classList.toggle("vot-tooltip-bordered", isBordered);
}
get bordered() {
return this._bordered;
}
set hidden(isHidden) {
this._hidden = isHidden;
if (this.container) this.container.hidden = isHidden;
if (this.showed) this.syncAriaDescribedBy(!isHidden);
}
get hidden() {
return this._hidden;
}
attachScrollListener() {
if (this.scrollListening) return;
this.scrollListening = true;
document.addEventListener("scroll", this.onScroll, {
passive: true,
capture: true
});
}
detachScrollListener() {
if (!this.scrollListening) return;
this.scrollListening = false;
document.removeEventListener("scroll", this.onScroll, { capture: true });
}
};
//#endregion
//#region src/ui/shadowMount.ts
var shadowScopedCssText = scopeCssForShadowRoots(".vot-button{--vot-helper-theme:var(--vot-theme-rgb,var(--vot-primary-rgb,33, 150, 243));--vot-helper-ontheme:var(--vot-ontheme-rgb,var(--vot-onprimary-rgb,255, 255, 255));box-sizing:border-box;vertical-align:middle;text-align:center;text-overflow:ellipsis;cursor:pointer;min-width:64px;height:36px;color:rgb(var(--vot-helper-ontheme));background-color:rgb(var(--vot-helper-theme));box-shadow:var(--vot-shadow-1);transition:box-shadow var(--vot-duration-medium) var(--vot-easing-standard);outline:none;font-size:14px;line-height:36px;display:inline-block;position:relative;border-radius:var(--vot-radius-s)!important;padding:0 var(--vot-space-4)!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;border:none!important;font-weight:500!important}.vot-button:before,.vot-button:after{content:\"\";opacity:0;position:absolute;inset:0;border-radius:inherit!important}.vot-button:before{background-color:rgb(var(--vot-helper-ontheme));transition:opacity var(--vot-duration-medium) var(--vot-easing-standard)}.vot-button:after{transition:opacity var(--vot-duration-slow) var(--vot-easing-standard), background-size var(--vot-duration-slow) var(--vot-easing-standard);background:radial-gradient(circle,currentColor 1%,#0000 1%) 50%/10000% 10000% no-repeat}.vot-button:hover:before{opacity:.08}.vot-button:active:after{opacity:.32;background-size:100% 100%;transition:background-size}.vot-button:hover,.vot-button:active{box-shadow:var(--vot-shadow-2)}.vot-button[disabled=true]{background-color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .12);color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38);box-shadow:none;cursor:initial}.vot-button[disabled=true]:before,.vot-button[disabled=true]:after{opacity:0}.vot-outlined-button{--vot-helper-theme:var(--vot-theme-rgb,var(--vot-primary-rgb,33, 150, 243));box-sizing:border-box;vertical-align:middle;text-align:center;text-overflow:ellipsis;cursor:pointer;min-width:64px;height:36px;color:rgb(var(--vot-helper-theme));background-color:#0000;outline:none;font-size:14px;line-height:34px;display:inline-block;position:relative;border-radius:var(--vot-radius-s)!important;padding:0 var(--vot-space-4)!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;border:solid 1px var(--vot-border-color)!important;margin:0!important;font-weight:500!important}.vot-outlined-button:before,.vot-outlined-button:after{content:\"\";opacity:0;position:absolute;inset:0;border-radius:inherit!important}.vot-outlined-button:before{background-color:rgb(var(--vot-helper-theme));transition:opacity var(--vot-duration-medium) var(--vot-easing-standard)}.vot-outlined-button:after{transition:opacity var(--vot-duration-slow) var(--vot-easing-standard), background-size var(--vot-duration-slow) var(--vot-easing-standard);background:radial-gradient(circle,currentColor 1%,#0000 1%) 50%/10000% 10000% no-repeat}.vot-outlined-button:hover:before{opacity:.04}.vot-outlined-button:active:after{opacity:.16;background-size:100% 100%;transition:background-size}.vot-outlined-button[disabled=true]{color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38);cursor:initial;background-color:#0000}.vot-outlined-button[disabled=true]:before,.vot-outlined-button[disabled=true]:after{opacity:0}.vot-text-button{--vot-helper-theme:var(--vot-theme-rgb,var(--vot-primary-rgb,33, 150, 243));box-sizing:border-box;vertical-align:middle;text-align:center;text-overflow:ellipsis;cursor:pointer;min-width:64px;height:36px;color:rgb(var(--vot-helper-theme));background-color:#0000;outline:none;font-size:14px;line-height:36px;display:inline-block;position:relative;border-radius:var(--vot-radius-s)!important;padding:0 var(--vot-space-2)!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;border:none!important;margin:0!important;font-weight:500!important}.vot-text-button:before,.vot-text-button:after{content:\"\";opacity:0;position:absolute;inset:0;border-radius:inherit!important}.vot-text-button:before{background-color:rgb(var(--vot-helper-theme));transition:opacity var(--vot-duration-medium) var(--vot-easing-standard)}.vot-text-button:after{transition:opacity var(--vot-duration-slow) var(--vot-easing-standard), background-size var(--vot-duration-slow) var(--vot-easing-standard);background:radial-gradient(circle,currentColor 1%,#0000 1%) 50%/10000% 10000% no-repeat}.vot-text-button:hover:before{opacity:.04}.vot-text-button:active:after{opacity:.16;background-size:100% 100%;transition:background-size}.vot-text-button[disabled=true]{color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38);cursor:initial;background-color:#0000}.vot-text-button[disabled=true]:before,.vot-text-button[disabled=true]:after{opacity:0}.vot-icon-button{--vot-helper-onsurface:rgba(var(--vot-onsurface-rgb,0, 0, 0), .87);box-sizing:border-box;vertical-align:middle;text-align:center;text-overflow:ellipsis;cursor:pointer;width:36px;min-width:36px;height:36px;fill:var(--vot-helper-onsurface);color:var(--vot-helper-onsurface);background-color:#0000;outline:none;font-size:14px;line-height:36px;display:inline-block;position:relative;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;border:none!important;border-radius:50%!important;margin:0!important;padding:0!important;font-weight:500!important}.vot-icon-button:before,.vot-icon-button:after{content:\"\";opacity:0;position:absolute;inset:0;border-radius:inherit!important}.vot-icon-button:before{background-color:var(--vot-helper-onsurface);transition:opacity var(--vot-duration-medium) var(--vot-easing-standard)}.vot-icon-button:after{transition:opacity var(--vot-duration-slow) var(--vot-easing-standard), background-size var(--vot-duration-slow) var(--vot-easing-standard);background:radial-gradient(circle,currentColor 1%,#0000 1%) 50%/10000% 10000% no-repeat}.vot-icon-button:hover:before{opacity:.04}.vot-icon-button:active:after{opacity:.32;background-size:100% 100%;transition:background-size}.vot-icon-button[disabled=true]{color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38);fill:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38);cursor:initial;background-color:#0000}.vot-icon-button[disabled=true]:before,.vot-icon-button[disabled=true]:after{opacity:0}.vot-icon-button svg{fill:inherit;stroke:inherit;width:24px;height:36px}.vot-hotkey{justify-content:flex-start;align-items:center;gap:var(--vot-space-3,12px);flex-wrap:wrap;display:flex}.vot-hotkey-label{word-break:break-word;max-width:80%}.vot-hotkey-button{--vot-helper-theme:var(--vot-theme-rgb,var(--vot-primary-rgb,33, 150, 243));box-sizing:border-box;vertical-align:middle;text-align:center;text-overflow:ellipsis;cursor:pointer;background-color:#0000;outline:none;width:fit-content;min-width:32px;height:fit-content;font-size:15px;line-height:1.5;display:inline-block;position:relative;border-radius:var(--vot-radius-s)!important;padding:0 var(--vot-space-2)!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;border:solid 1px var(--vot-border-color)!important;margin:0!important;font-weight:400!important}.vot-hotkey-button:before,.vot-hotkey-button:after{content:\"\";opacity:0;position:absolute;inset:0;border-radius:inherit!important}.vot-hotkey-button:before{background-color:rgb(var(--vot-helper-theme));transition:opacity var(--vot-duration-medium) var(--vot-easing-standard)}.vot-hotkey-button:after{transition:opacity var(--vot-duration-slow) var(--vot-easing-standard), background-size var(--vot-duration-slow) var(--vot-easing-standard);background:radial-gradient(circle,currentColor 1%,#0000 1%) 50%/10000% 10000% no-repeat}.vot-hotkey-button:hover:before{opacity:.04}.vot-hotkey-button:active:after{opacity:.16;background-size:100% 100%;transition:background-size}.vot-hotkey-button[data-status=active]{color:rgb(var(--vot-helper-theme))}.vot-hotkey-button[data-status=active]:before{opacity:.04}.vot-hotkey-button[disabled=true]{color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38);cursor:initial;background-color:#0000}.vot-hotkey-button[disabled=true]:before,.vot-hotkey-button[disabled=true]:after{opacity:0}.vot-textfield{display:inline-block;--vot-helper-theme:rgb(var(--vot-theme-rgb,var(--vot-primary-rgb,33, 150, 243)))!important;--vot-helper-safari1:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38)!important;--vot-helper-safari2:rgba(var(--vot-onsurface-rgb,0, 0, 0), .6)!important;--vot-helper-safari3:rgba(var(--vot-onsurface-rgb,0, 0, 0), .87)!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;text-align:start!important;padding-top:6px!important;font-size:16px!important;line-height:1.5!important;position:relative!important}.vot-textfield>:is(input,textarea){box-sizing:border-box!important;border-style:solid!important;border-width:1px!important;border-color:transparent var(--vot-helper-safari2) var(--vot-helper-safari2)!important;width:100%!important;height:inherit!important;color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .87)!important;-webkit-text-fill-color:currentColor!important;font-family:inherit!important;font-size:inherit!important;line-height:inherit!important;caret-color:var(--vot-helper-theme)!important;background-color:#0000!important;border-radius:4px!important;margin:0!important;padding:15px 13px!important;transition:border .2s,box-shadow .2s!important;box-shadow:inset 1px 0 #0000,inset -1px 0 #0000,inset 0 -1px #0000!important}.vot-textfield>:is(input,textarea):not(:focus):not(:is(.vot-show-placeholder,.vot-show-placeholer))::placeholder{color:#0000!important}.vot-textfield>:is(input,textarea):not(:focus):placeholder-shown{border-top-color:var(--vot-helper-safari2)!important}.vot-textfield>:is(input,textarea)+span{font-family:inherit;width:100%!important;max-height:100%!important;color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .6)!important;cursor:text!important;pointer-events:none!important;font-size:75%!important;line-height:15px!important;transition:color .2s,font-size .2s,line-height .2s!important;display:flex!important;position:absolute!important;top:0!important;left:0!important}.vot-textfield>:is(input,textarea):not(:focus):placeholder-shown+span{font-size:inherit!important;line-height:68px!important}.vot-textfield>input+span:before,.vot-textfield>input+span:after,.vot-textfield>textarea+span:before,.vot-textfield>textarea+span:after{content:\"\"!important;box-sizing:border-box!important;border-top:solid 1px var(--vot-helper-safari2)!important;pointer-events:none!important;min-width:10px!important;height:8px!important;margin-top:6px!important;transition:border .2s,box-shadow .2s!important;display:block!important;box-shadow:inset 0 1px #0000!important}.vot-textfield>input+span:before,.vot-textfield>textarea+span:before{border-left:1px solid #0000!important;border-radius:4px 0!important;margin-right:4px!important}.vot-textfield>input+span:after,.vot-textfield>textarea+span:after{border-right:1px solid #0000!important;border-radius:0 4px!important;flex-grow:1!important;margin-left:4px!important}.vot-textfield>input:is(.vot-show-placeholder,.vot-show-placeholer)+span:before,.vot-textfield>textarea:is(.vot-show-placeholder,.vot-show-placeholer)+span:before{margin-right:0!important}.vot-textfield>input:is(.vot-show-placeholder,.vot-show-placeholer)+span:after,.vot-textfield>textarea:is(.vot-show-placeholder,.vot-show-placeholer)+span:after{margin-left:0!important}.vot-textfield>input:not(:focus):placeholder-shown+span:before,.vot-textfield>input:not(:focus):placeholder-shown+span:after,.vot-textfield>textarea:not(:focus):placeholder-shown+span:before,.vot-textfield>textarea:not(:focus):placeholder-shown+span:after{border-top-color:#0000!important}.vot-textfield:hover>input:not(:disabled),.vot-textfield:hover>textarea:not(:disabled){border-color:transparent var(--vot-helper-safari3) var(--vot-helper-safari3)!important}.vot-textfield:hover>input:not(:disabled)+span:before,.vot-textfield:hover>input:not(:disabled)+span:after,.vot-textfield:hover>textarea:not(:disabled)+span:before,.vot-textfield:hover>textarea:not(:disabled)+span:after{border-top-color:var(--vot-helper-safari3)!important}.vot-textfield:hover>input:not(:disabled):not(:focus):placeholder-shown,.vot-textfield:hover>textarea:not(:disabled):not(:focus):placeholder-shown{border-color:var(--vot-helper-safari3)!important}.vot-textfield>input:focus,.vot-textfield>textarea:focus{border-color:transparent var(--vot-helper-theme) var(--vot-helper-theme)!important;box-shadow:inset 1px 0 var(--vot-helper-theme), inset -1px 0 var(--vot-helper-theme), inset 0 -1px var(--vot-helper-theme)!important;outline:none!important}.vot-textfield>input:focus+span,.vot-textfield>textarea:focus+span{color:var(--vot-helper-theme)!important}.vot-textfield>input:focus+span:before,.vot-textfield>input:focus+span:after,.vot-textfield>textarea:focus+span:before,.vot-textfield>textarea:focus+span:after{border-top-color:var(--vot-helper-theme)!important;box-shadow:inset 0 1px var(--vot-helper-theme)!important}.vot-textfield>input:disabled,.vot-textfield>input:disabled+span,.vot-textfield>textarea:disabled,.vot-textfield>textarea:disabled+span{border-color:transparent var(--vot-helper-safari1) var(--vot-helper-safari1)!important;color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38)!important;pointer-events:none!important}.vot-textfield>input:disabled+span:before,.vot-textfield>input:disabled+span:after,.vot-textfield>textarea:disabled+span:before,.vot-textfield>textarea:disabled+span:after,.vot-textfield>input:disabled:placeholder-shown,.vot-textfield>input:disabled:placeholder-shown+span,.vot-textfield>textarea:disabled:placeholder-shown,.vot-textfield>textarea:disabled:placeholder-shown+span{border-top-color:var(--vot-helper-safari1)!important}.vot-textfield>input:disabled:placeholder-shown+span:before,.vot-textfield>input:disabled:placeholder-shown+span:after,.vot-textfield>textarea:disabled:placeholder-shown+span:before,.vot-textfield>textarea:disabled:placeholder-shown+span:after{border-top-color:#0000!important}@media not all and (resolution>=.001dpcm){@supports ((-webkit-appearance:none)){.vot-textfield>input,.vot-textfield>input+span,.vot-textfield>textarea,.vot-textfield>textarea+span,.vot-textfield>input+span:before,.vot-textfield>input+span:after,.vot-textfield>textarea+span:before,.vot-textfield>textarea+span:after{transition-duration:.1s!important}}}.vot-checkbox{--vot-checkbox-label-offset:30px;--vot-helper-theme:var(--vot-theme-rgb,var(--vot-primary-rgb,33, 150, 243));--vot-helper-ontheme:var(--vot-ontheme-rgb,var(--vot-onprimary-rgb,255, 255, 255));z-index:0;color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .87);text-align:start;font-size:16px;line-height:1.5;display:inline-block;position:relative;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;text-transform:none!important}.vot-checkbox-sub{padding-left:var(--vot-checkbox-label-offset)!important}.vot-checkbox>input{appearance:none;z-index:10000;box-sizing:border-box;opacity:1;cursor:pointer;background:0 0;outline:none;width:18px;height:18px;transition:border-color .2s,background-color .2s;display:block;position:absolute;border:2px solid!important;border-color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .6)!important;border-radius:2px!important;margin:3px 1px!important;padding:0!important}.vot-checkbox>input+span{box-sizing:border-box;width:inherit;cursor:pointer;font-family:inherit;display:inline-block;position:relative;padding-left:var(--vot-checkbox-label-offset)!important;font-weight:400!important}.vot-checkbox>input+span:before{content:\"\";background-color:rgb(var(--vot-onsurface-rgb,0, 0, 0));opacity:0;pointer-events:none;width:40px;height:40px;transition:opacity .3s,transform .2s;display:block;position:absolute;top:-8px;left:-10px;transform:scale(1);border-radius:50%!important}.vot-checkbox>input+span:after{content:\"\";z-index:10000;pointer-events:none;width:10px;height:5px;transition:border-color .2s;display:block;position:absolute;top:3px;left:1px;transform:translate(3px,4px)rotate(-45deg);box-sizing:content-box!important;border:0 solid #0000!important;border-width:0 0 2px 2px!important}.vot-checkbox>input:checked,.vot-checkbox>input:indeterminate{background-color:rgb(var(--vot-helper-theme));border-color:rgb(var(--vot-helper-theme))!important}.vot-checkbox>input:checked+span:before,.vot-checkbox>input:indeterminate+span:before{background-color:rgb(var(--vot-helper-theme))}.vot-checkbox>input:checked+span:after,.vot-checkbox>input:indeterminate+span:after{border-color:rgb(var(--vot-helper-ontheme,255, 255, 255))!important}.vot-checkbox>input:hover{box-shadow:none!important}.vot-checkbox>input:indeterminate+span:after{transform:translate(4px,3px);border-left-width:0!important}.vot-checkbox:hover>input+span:before{opacity:.04}.vot-checkbox:active>input,.vot-checkbox:active:hover>input:not(:disabled){border-color:rgb(var(--vot-helper-theme))!important}.vot-checkbox:active>input:checked{background-color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .6);border-color:#0000!important}.vot-checkbox:active>input+span:before{opacity:1;transition:transform,opacity;transform:scale(0)}.vot-checkbox>input:disabled{cursor:initial;border-color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38)!important}.vot-checkbox>input:disabled:checked,.vot-checkbox>input:disabled:indeterminate{background-color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38);border-color:#0000!important}.vot-checkbox>input:disabled+span{color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38);cursor:initial}.vot-checkbox>input:disabled+span:before{opacity:0;transform:scale(0)}html.vot-keyboard-nav .vot-checkbox>input:focus-visible{box-shadow:var(--vot-focus-ring), var(--vot-focus-ring-offset)!important}@supports not selector(:focus-visible){html.vot-keyboard-nav .vot-checkbox>input:focus{box-shadow:var(--vot-focus-ring), var(--vot-focus-ring-offset)!important}}.vot-slider{flex-direction:column;gap:6px;display:flex;width:100%!important;color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .87)!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", BlinkMacSystemFont, system-ui, -apple-system)!important;text-align:start!important;font-size:16px!important;line-height:1.5!important}.vot-slider>span{order:1;margin:0!important;display:block!important}.vot-slider .vot-slider-label{flex-wrap:wrap;align-items:baseline;gap:6px;width:100%;display:inline-flex}.vot-slider-label-value{font-variant-numeric:tabular-nums;margin-left:0!important;font-weight:500!important}.vot-slider .vot-slider-label-text{min-width:0}.vot-slider>input{order:2;appearance:none!important;cursor:pointer!important;background-color:#0000!important;border:none!important;width:100%!important;height:32px!important;margin:0!important;padding:0!important;display:block!important;position:relative!important;top:0!important}.vot-slider>input:hover{box-shadow:none!important}.vot-slider>input:before{content:\"\"!important;width:calc(100% * var(--vot-progress,0))!important;background:rgb(var(--vot-primary-rgb,33, 150, 243))!important;height:2px!important;display:block!important;position:absolute!important;top:calc(50% - 1px)!important}.vot-slider>input:disabled{cursor:default!important;opacity:.38!important}.vot-slider>input:disabled+span{color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38)!important}.vot-slider>input:disabled::-webkit-slider-runnable-track{background-color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38)!important}.vot-slider>input:disabled::-moz-range-track{background-color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38)!important}.vot-slider>input:disabled::-webkit-slider-thumb{background-color:rgb(var(--vot-onsurface-rgb,0, 0, 0))!important;box-shadow:0 0 0 1px rgb(var(--vot-surface-rgb,255, 255, 255))!important;transform:scale(4)!important}.vot-slider>input:disabled::-moz-range-thumb{background-color:rgb(var(--vot-onsurface-rgb,0, 0, 0))!important;box-shadow:0 0 0 1px rgb(var(--vot-surface-rgb,255, 255, 255))!important;transform:scale(4)!important}.vot-slider>input:disabled::-moz-range-progress{background-color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .87)!important}.vot-slider>input:focus{outline:none!important}.vot-slider>input::-webkit-slider-runnable-track{background-color:rgba(var(--vot-primary-rgb,33, 150, 243), .24)!important;border-radius:1px!important;width:100%!important;height:2px!important;margin:15px 0!important}.vot-slider>input::-moz-range-track{background-color:rgba(var(--vot-primary-rgb,33, 150, 243), .24)!important;border-radius:1px!important;width:100%!important;height:2px!important;margin:15px 0!important}.vot-slider>input::-webkit-slider-thumb{appearance:none!important;background-color:rgb(var(--vot-primary-rgb,33, 150, 243))!important;width:2px!important;height:2px!important;box-shadow:none!important;border:none!important;border-radius:50%!important;transition:box-shadow .2s!important;transform:scale(6)!important}.vot-slider>input::-moz-range-thumb{appearance:none!important;background-color:rgb(var(--vot-primary-rgb,33, 150, 243))!important;width:2px!important;height:2px!important;box-shadow:none!important;border:none!important;border-radius:50%!important;transition:box-shadow .2s!important;transform:scale(6)!important}.vot-slider>input::-webkit-slider-thumb{-webkit-appearance:none!important;margin:0!important}.vot-slider>input::-moz-range-progress{background-color:rgb(var(--vot-primary-rgb,33, 150, 243))!important;border-radius:1px!important;height:2px!important}.vot-slider>input:focus:not(:focus-visible)::-webkit-slider-thumb{box-shadow:none!important}.vot-slider>input:focus:not(:focus-visible)::-moz-range-thumb{box-shadow:none!important}html.vot-keyboard-nav .vot-slider>input:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 2px rgba(var(--vot-primary-rgb,33, 150, 243), .24)!important}html.vot-keyboard-nav .vot-slider>input:focus-visible::-moz-range-thumb{box-shadow:0 0 0 2px rgba(var(--vot-primary-rgb,33, 150, 243), .24)!important}@supports not selector(:focus-visible){html.vot-keyboard-nav .vot-slider>input:focus::-webkit-slider-thumb{box-shadow:0 0 0 2px rgba(var(--vot-primary-rgb,33, 150, 243), .24)!important}html.vot-keyboard-nav .vot-slider>input:focus::-moz-range-thumb{box-shadow:0 0 0 2px rgba(var(--vot-primary-rgb,33, 150, 243), .24)!important}}.vot-select{--vot-helper-theme-rgb:var(--vot-onsurface-rgb,0, 0, 0);--vot-helper-theme:rgba(var(--vot-helper-theme-rgb), .87);--vot-helper-safari1:rgba(var(--vot-onsurface-rgb,0, 0, 0), .6);--vot-helper-safari2:rgba(var(--vot-onsurface-rgb,0, 0, 0), .87);font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif);text-align:start;color:var(--vot-helper-theme);fill:var(--vot-helper-theme);justify-content:space-between;align-items:center;font-size:14px;line-height:1.5;display:flex;font-weight:400!important}.vot-select-outer{cursor:pointer;justify-content:space-between;align-items:center;width:120px;max-width:120px;display:flex;border:1px solid var(--vot-helper-safari1)!important;border-radius:4px!important;padding:0 5px!important;transition:border .2s!important}.vot-select-outer:hover{border-color:var(--vot-helper-safari2)!important}.vot-select-outer[disabled=true]{opacity:.5;cursor:default}.vot-select-outer[disabled=true]:hover{border-color:var(--vot-helper-safari1)!important}.vot-select-title{text-overflow:ellipsis;white-space:nowrap;font-family:inherit;overflow:hidden}.vot-select-arrow-icon{justify-content:center;align-items:center;width:20px;height:32px;display:flex}.vot-select-arrow-icon svg{fill:inherit;stroke:inherit}.vot-select-content-list{flex-direction:column;display:flex}.vot-select-content-list .vot-select-content-item{cursor:pointer;border-radius:8px!important;padding:5px 10px!important}.vot-select-content-list .vot-select-content-item:not([inert]):hover{background-color:#2a2c31}.vot-select-content-list .vot-select-content-item[data-vot-selected=true]{color:rgb(var(--vot-primary-rgb,33, 150, 243));background-color:rgba(var(--vot-primary-rgb,33, 150, 243), .2)}.vot-select-content-list .vot-select-content-item[data-vot-selected=true]:hover{background-color:rgba(var(--vot-primary-rgb,33, 150, 243), .1)!important}.vot-select-content-list .vot-select-content-item[inert]{cursor:default;color:rgba(var(--vot-onsurface-rgb,0, 0, 0), .38)}.vot-header{color:rgba(var(--vot-helper-onsurface-rgb), .87);font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif);text-align:start;line-height:1.5;font-weight:700!important}.vot-header:not(:first-child){padding-top:8px}.vot-header-level-1{font-size:2em}.vot-header-level-2{font-size:1.5em}.vot-header-level-3{font-size:1.17em}.vot-header-level-4{font-size:1em}.vot-header-level-5{font-size:.83em}.vot-header-level-6{font-size:.67em}.vot-info{color:rgba(var(--vot-helper-onsurface-rgb), .87);font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif);text-align:start;-webkit-user-select:text;user-select:text;font-size:16px;line-height:1.5;display:flex}.vot-info>:not(:first-child){color:rgba(var(--vot-helper-onsurface-rgb), .5);flex:1;margin-left:8px!important}.vot-details{color:rgba(var(--vot-helper-onsurface-rgb), .87);font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif);text-align:start;cursor:pointer;transition:background var(--vot-duration-medium) var(--vot-easing-standard);justify-content:space-between;align-items:center;font-size:16px;line-height:1.5;display:flex;border-radius:.5em!important;margin:-.5em!important;padding:.5em!important}.vot-details-arrow-icon{width:20px;height:32px;fill:rgba(var(--vot-helper-onsurface-rgb), .87);justify-content:center;align-items:center;display:flex;transform:scale(1.25)rotate(-90deg)}.vot-details:hover{background:rgba(var(--vot-onsurface-rgb,0, 0, 0), .06)}.vot-settings-section{border:1px solid var(--vot-border-color);border-radius:var(--vot-radius-l);padding:var(--vot-space-2);background:rgba(var(--vot-helper-onsurface-rgb), .03);flex-direction:column;display:flex}.vot-settings-section>*{margin:0!important}.vot-settings-section>*+*{margin-top:var(--vot-space-2)!important}.vot-settings-section-header{border-radius:var(--vot-radius-m);margin:0!important;padding:.45em .5em!important}.vot-settings-section-header .vot-details-arrow-icon{transition:transform var(--vot-duration-medium) var(--vot-easing-standard)}.vot-settings-section-header[data-open=true] .vot-details-arrow-icon{transform:scale(1.25)rotate(0)}.vot-settings-section-content{--vot-settings-control-width:200px;--vot-settings-row-gap:var(--vot-space-2);padding:0 var(--vot-space-1) var(--vot-space-1);flex-direction:column;display:flex}.vot-settings-section-content>*{margin:0!important}.vot-settings-section-content>*+*{margin-top:var(--vot-settings-row-gap)!important}.vot-settings-section-content>.vot-checkbox,.vot-settings-section-content>.vot-hotkey,.vot-settings-section-content>.vot-textfield,.vot-settings-section-content>.vot-select,.vot-settings-section-content>.vot-slider{padding:var(--vot-space-1);box-sizing:border-box;width:100%!important}.vot-settings-section-content>.vot-textfield{gap:var(--vot-space-1);flex-direction:column;padding-top:0!important;display:flex!important}.vot-settings-section-content>.vot-textfield>span{order:0;width:auto!important;max-height:none!important;color:rgba(var(--vot-helper-onsurface-rgb), .72)!important;cursor:default!important;pointer-events:none!important;font-size:13px!important;line-height:1.2!important;display:block!important;position:static!important}.vot-settings-section-content>.vot-textfield>span:before,.vot-settings-section-content>.vot-textfield>span:after{content:none!important;display:none!important}.vot-settings-section-content>.vot-textfield>input,.vot-settings-section-content>.vot-textfield>textarea{transition:border-color var(--vot-duration-fast) var(--vot-easing-standard), background-color var(--vot-duration-fast) var(--vot-easing-standard);order:1;width:100%!important;height:36px!important;padding:0 var(--vot-space-3)!important;border:1px solid var(--vot-border-color)!important;border-radius:var(--vot-radius-s)!important;background:rgba(var(--vot-helper-onsurface-rgb), .04)!important;color:rgba(var(--vot-helper-onsurface-rgb), .9)!important;-webkit-text-fill-color:currentColor!important;box-shadow:none!important}.vot-settings-section-content>.vot-textfield>textarea{resize:vertical;height:auto!important;min-height:84px!important;padding:var(--vot-space-2) var(--vot-space-3)!important}.vot-settings-section-content>.vot-textfield>input::placeholder,.vot-settings-section-content>.vot-textfield>textarea::placeholder{color:rgba(var(--vot-helper-onsurface-rgb), .55)!important}.vot-settings-section-content>.vot-textfield:hover>input,.vot-settings-section-content>.vot-textfield:hover>textarea{border-color:var(--vot-border-color-hover)!important}.vot-settings-section-content>.vot-textfield>input:not(:focus):placeholder-shown,.vot-settings-section-content>.vot-textfield>textarea:not(:focus):placeholder-shown{border-color:var(--vot-border-color)!important}.vot-settings-section-content>.vot-textfield>input:focus,.vot-settings-section-content>.vot-textfield>textarea:focus{border-color:rgba(var(--vot-primary-rgb), .7)!important}.vot-lang-select{--vot-helper-theme-rgb:var(--vot-onsurface-rgb,0, 0, 0);--vot-helper-theme:rgba(var(--vot-helper-theme-rgb), .87);color:var(--vot-helper-theme);fill:var(--vot-helper-theme);justify-content:space-between;align-items:center;display:flex}.vot-lang-select-icon{justify-content:center;align-items:center;width:32px;height:32px;display:flex}.vot-lang-select-icon svg{fill:inherit;stroke:inherit}.vot-segmented-button{--vot-helper-theme-rgb:var(--vot-onsurface-rgb,0, 0, 0);--vot-helper-theme:rgba(var(--vot-helper-theme-rgb), .87);-webkit-user-select:none;user-select:none;background:rgb(var(--vot-surface-rgb,255, 255, 255));max-width:100vw;height:36px;color:var(--vot-helper-theme);fill:var(--vot-helper-theme);cursor:default;transition:opacity var(--vot-duration-slow) var(--vot-easing-standard);z-index:2147483647;align-items:center;font-size:16px;line-height:1.5;display:flex;position:absolute;top:5rem;left:50%;overflow:hidden;transform:translate(-50%);opacity:1!important;pointer-events:auto!important;touch-action:none!important;border:1px solid var(--vot-border-color)!important;border-radius:var(--vot-radius-s)!important;box-shadow:var(--vot-shadow-1)!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important}.vot-segmented-button.vot-segmented-button--hidden{opacity:0!important;pointer-events:none!important}.vot-segmented-button *{box-sizing:border-box!important}.vot-segmented-button .vot-separator{background:rgba(var(--vot-helper-theme-rgb), .1);width:1px;height:50%}.vot-segmented-button .vot-segment,.vot-segmented-button .vot-segment-only-icon{height:100%;color:inherit;transition:background-color var(--vot-duration-fast) var(--vot-easing-standard);-webkit-tap-highlight-color:transparent;background-color:#0000;outline:none;justify-content:center;align-items:center;display:flex;position:relative;overflow:hidden;padding:0 var(--vot-space-2)!important;border:none!important}.vot-segmented-button .vot-segment:focus,.vot-segmented-button .vot-segment-only-icon:focus{box-shadow:inset 0 0 0 2px var(--vot-focus-ring-color);outline:none}.vot-segmented-button .vot-segment:focus:not(:focus-visible),.vot-segmented-button .vot-segment-only-icon:focus:not(:focus-visible){box-shadow:none}.vot-segmented-button .vot-segment:before,.vot-segmented-button .vot-segment-only-icon:before,.vot-segmented-button .vot-segment:after,.vot-segmented-button .vot-segment-only-icon:after{content:\"\";opacity:0;position:absolute;inset:0;border-radius:inherit!important}.vot-segmented-button .vot-segment:before,.vot-segmented-button .vot-segment-only-icon:before{background-color:rgb(var(--vot-helper-theme-rgb));transition:opacity var(--vot-duration-medium) var(--vot-easing-standard)}.vot-segmented-button .vot-segment:after,.vot-segmented-button .vot-segment-only-icon:after{transition:opacity var(--vot-duration-slow) var(--vot-easing-standard), background-size var(--vot-duration-slow) var(--vot-easing-standard);background:radial-gradient(circle,currentColor 1%,#0000 1%) 50%/10000% 10000% no-repeat}.vot-segmented-button .vot-segment:hover:before,.vot-segmented-button .vot-segment-only-icon:hover:before{opacity:.04}.vot-segmented-button .vot-segment:active:after,.vot-segmented-button .vot-segment-only-icon:active:after{opacity:.16;background-size:100% 100%;transition:background-size}.vot-segmented-button .vot-segment-only-icon{min-width:36px;padding:0!important}.vot-segmented-button .vot-segment-label{white-space:nowrap;color:inherit;margin-left:var(--vot-space-2)!important;font-weight:400!important}.vot-segmented-button[data-status=success] .vot-translate-button{color:rgb(var(--vot-primary-rgb,33, 150, 243));fill:rgb(var(--vot-primary-rgb,33, 150, 243))}.vot-segmented-button[data-status=error] .vot-translate-button{color:#f28b82;fill:#f28b82}.vot-segmented-button[data-loading=true] #vot-loading-icon{display:block!important}.vot-segmented-button[data-loading=true] #vot-translate-icon{display:none!important}.vot-segmented-button[data-direction=column]{flex-direction:column;height:fit-content}.vot-segmented-button[data-direction=column] .vot-segment-label{display:none}.vot-segmented-button[data-direction=column]>.vot-segment-only-icon,.vot-segmented-button[data-direction=column]>.vot-segment{padding:8px!important}.vot-segmented-button[data-direction=column] .vot-separator{width:50%;height:1px}.vot-segmented-button[data-position=left]{top:12.5vh;left:50px}.vot-segmented-button[data-position=right]{top:12.5vh;left:auto;right:0}.vot-segmented-button svg{width:24px;fill:inherit;stroke:inherit}.vot-tooltip{--vot-helper-theme-rgb:var(--vot-onsurface-rgb,0, 0, 0);--vot-helper-theme:rgba(var(--vot-helper-theme-rgb), .87);--vot-helper-ondialog:rgb(var(--vot-ondialog-rgb,37, 38, 40));--vot-helper-border:rgb(var(--vot-tooltip-border,69, 69, 69));-webkit-user-select:none;user-select:none;background:rgb(var(--vot-surface-rgb,255, 255, 255));color:var(--vot-helper-theme);fill:var(--vot-helper-theme);cursor:default;z-index:2147483647;opacity:0;align-items:center;width:max-content;max-width:calc(100vw - 10px);height:max-content;font-size:14px;line-height:1.5;transition:opacity .5s;display:flex;position:absolute;inset:0;overflow:hidden;box-shadow:0 1px 3px #0000001f;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;border-radius:4px!important;padding:4px 8px!important}.vot-tooltip[data-trigger=click]{-webkit-user-select:text;user-select:text}.vot-tooltip.vot-tooltip-bordered{border:1px solid var(--vot-helper-border)}.vot-tooltip *{box-sizing:border-box!important;font-family:inherit!important}.vot-menu{--vot-helper-surface-rgb:var(--vot-surface-rgb,255, 255, 255);--vot-helper-surface:rgb(var(--vot-helper-surface-rgb));--vot-helper-onsurface-rgb:var(--vot-onsurface-rgb,0, 0, 0);--vot-helper-onsurface:rgba(var(--vot-helper-onsurface-rgb), .87);--vot-settings-control-width:clamp(120px, 45%, 200px);-webkit-user-select:none;user-select:none;background-color:var(--vot-helper-surface);color:var(--vot-helper-onsurface);cursor:default;z-index:2147483646;visibility:visible;opacity:1;transform-origin:top;width:fit-content;min-width:320px;max-width:min(90vw,560px);transition:opacity var(--vot-duration-medium) var(--vot-easing-standard), transform var(--vot-duration-medium) var(--vot-easing-standard);font-size:16px;line-height:1.5;position:absolute;top:calc(5rem + 48px);left:50%;overflow:hidden;transform:translate(-50%)scale(1);border:1px solid var(--vot-border-color)!important;border-radius:var(--vot-radius-m)!important;box-shadow:var(--vot-shadow-2)!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important}.vot-menu *{box-sizing:border-box!important}.vot-menu[hidden]{pointer-events:none;visibility:hidden;opacity:0;transform:translate(-50%,-4px)scale(.98);display:block!important}.vot-menu-content-wrapper{min-width:320px;min-height:100px;max-height:calc(var(--vot-container-height,75vh) - (5rem + 32px + 16px) * 2);flex-direction:column;display:flex;overflow:auto}.vot-menu-header-container{flex-shrink:0;align-items:center;min-height:31px;display:flex;padding-inline-end:var(--vot-space-2)!important}.vot-menu-header-container:empty{padding:0 0 16px!important}.vot-menu-header-container>.vot-icon-button{margin-inline-end:var(--vot-space-1)!important;margin-top:var(--vot-space-1)!important}.vot-menu-title-container{font-size:inherit;text-align:start;outline:0;flex:1;display:flex;font-weight:inherit!important;margin:0!important}.vot-menu-title{flex:1;font-size:16px;line-height:1;padding:var(--vot-space-4)!important;font-weight:500!important}.vot-menu-body-container{box-sizing:border-box;gap:var(--vot-space-2);overscroll-behavior:contain;flex-direction:column;min-height:1.375rem;display:flex;overflow:auto;padding:0 var(--vot-space-4)!important;scrollbar-color:rgba(var(--vot-helper-onsurface-rgb), .1) var(--vot-helper-surface)!important}.vot-menu-body-container::-webkit-scrollbar{background:var(--vot-helper-surface)!important;width:12px!important;height:12px!important}.vot-menu-body-container::-webkit-scrollbar-track{background:var(--vot-helper-surface)!important;width:12px!important;height:12px!important}.vot-menu-body-container::-webkit-scrollbar-thumb{border-radius:1ex;background:rgba(var(--vot-helper-onsurface-rgb), .1)!important;border:5px solid var(--vot-helper-surface)!important}.vot-menu-body-container::-webkit-scrollbar-thumb:hover{border-width:3px!important}.vot-menu-body-container::-webkit-scrollbar-corner{background:var(--vot-helper-surface)!important}.vot-menu-footer-container{flex-shrink:0;justify-content:flex-end;display:flex;padding:var(--vot-space-4)!important}.vot-menu-footer-container:empty{padding:var(--vot-space-4) 0 0 0!important}.vot-menu .vot-select--labeled>.vot-select-outer{margin-left:auto}.vot-menu[data-position=left]{transform-origin:0;top:12.5vh;left:240px}.vot-menu[data-position=right]{transform-origin:100%;top:12.5vh;left:auto;right:-80px}.vot-dialog{--vot-helper-surface-rgb:var(--vot-surface-rgb,255, 255, 255);--vot-helper-surface:rgb(var(--vot-helper-surface-rgb));--vot-helper-onsurface-rgb:var(--vot-onsurface-rgb,0, 0, 0);--vot-helper-onsurface:rgba(var(--vot-helper-onsurface-rgb), .87);--vot-dialog-viewport-margin:16px;--vot-dialog-max-height:75vh;max-width:initial;max-height:initial;width:min(var(--vot-dialog-width,512px), 100%);border:1px solid var(--vot-border-color);border-radius:var(--vot-radius-l);background-color:var(--vot-helper-surface);height:fit-content;color:var(--vot-helper-onsurface);box-shadow:var(--vot-shadow-2);-webkit-user-select:none;user-select:none;visibility:visible;opacity:1;transform-origin:50%;transition:opacity var(--vot-duration-medium) var(--vot-easing-standard), transform var(--vot-duration-medium) var(--vot-easing-standard);font-size:16px;line-height:1.5;display:block;position:fixed;inset-block:0;inset-inline:0;overflow:auto hidden;transform:scale(1);font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;margin:auto!important;padding:0!important}[hidden]>.vot-dialog{pointer-events:none;opacity:0;transition:opacity var(--vot-duration-fast) var(--vot-easing-standard), transform var(--vot-duration-medium) var(--vot-easing-standard);transform:translateY(-4px)scale(.98)}.vot-dialog[data-vertical-align=top]{inset-block-start:var(--vot-dialog-viewport-margin);inset-block-end:auto;margin:0 auto!important}.vot-dialog-container{visibility:visible;z-index:2147483647;position:absolute}.vot-dialog-container[hidden]{pointer-events:none;visibility:hidden;display:block!important}.vot-dialog-container *{box-sizing:border-box!important}.vot-dialog-backdrop{opacity:1;background-color:#0009;transition:opacity .3s;position:fixed;inset:0}[hidden]>.vot-dialog-backdrop{pointer-events:none;opacity:0}.vot-dialog-content-wrapper{max-height:var(--vot-dialog-max-height,75vh);flex-direction:column;display:flex;overflow:auto}.vot-dialog-header-container{flex-shrink:0;align-items:flex-start;min-height:31px;display:flex}.vot-dialog-header-container:empty{padding:0 0 20px}.vot-dialog-header-container>.vot-icon-button{margin-inline-end:var(--vot-space-1)!important;margin-top:var(--vot-space-1)!important}.vot-dialog-title-container{font-size:inherit;outline:0;flex:1;display:flex;font-weight:inherit!important;margin:0!important}.vot-dialog-title{flex:1;font-size:115.385%;line-height:1;padding:var(--vot-space-5) var(--vot-space-5) var(--vot-space-4)!important;font-weight:700!important}.vot-dialog-body-container{box-sizing:border-box;gap:var(--vot-space-4);overscroll-behavior:contain;flex-direction:column;min-height:1.375rem;display:flex;overflow:auto;padding:0 var(--vot-space-5)!important;scrollbar-color:rgba(var(--vot-helper-onsurface-rgb), .1) var(--vot-helper-surface)!important}.vot-dialog-body-container::-webkit-scrollbar{background:var(--vot-helper-surface)!important;width:12px!important;height:12px!important}.vot-dialog-body-container::-webkit-scrollbar-track{background:var(--vot-helper-surface)!important;width:12px!important;height:12px!important}.vot-dialog-body-container::-webkit-scrollbar-thumb{border-radius:1ex;background:rgba(var(--vot-helper-onsurface-rgb), .1)!important;border:5px solid var(--vot-helper-surface)!important}.vot-dialog-body-container::-webkit-scrollbar-thumb:hover{border-width:3px!important}.vot-dialog-body-container::-webkit-scrollbar-corner{background:var(--vot-helper-surface)!important}.vot-dialog-footer-container{justify-content:flex-end;gap:var(--vot-space-2);flex-wrap:wrap;flex-shrink:0;display:flex;padding:var(--vot-space-4)!important}.vot-dialog-footer-container:empty{padding:var(--vot-space-5) 0 0 0!important}@media (width<=480px){.vot-dialog-footer-container{flex-direction:column;align-items:stretch}.vot-dialog-footer-container>:is(.vot-button,.vot-outlined-button,.vot-text-button){white-space:normal;text-overflow:clip;text-align:center;justify-content:center;align-items:center;width:100%;height:auto;min-height:36px;padding:8px 16px;line-height:1.2;display:flex;overflow:visible}}.vot-inline-loader{aspect-ratio:5;--vot-loader-bg:no-repeat radial-gradient(farthest-side, rgba(var(--vot-onsurface-rgb,0, 0, 0), .38) 94%, transparent);background:var(--vot-loader-bg), var(--vot-loader-bg), var(--vot-loader-bg), var(--vot-loader-bg);background-size:20% 100%;height:8px;animation:.75s infinite alternate dotsSlide,1.5s infinite alternate dotsFlip}.vot-loader-progress{--vot-helper-theme:var(--vot-theme-rgb,var(--vot-primary-rgb,33, 150, 243));fill:none;stroke:rgb(var(--vot-helper-theme));stroke-width:2px;stroke-linecap:round;transform-origin:50%;transform:rotate(-90deg)}@keyframes dotsSlide{0%,10%{background-position:0 0,0 0,0 0,0 0}33%{background-position:0 0,33.3333% 0,33.3333% 0,33.3333% 0}66%{background-position:0 0,33.3333% 0,66.6667% 0,66.6667% 0}90%,to{background-position:0 0,33.3333% 0,66.6667% 0,100% 0}}@keyframes dotsFlip{0%,49.99%{transform:scale(1)}50%,to{transform:scale(-1)}}.vot-label{font-family:inherit;font-size:16px;line-height:1.5;display:block}.vot-label-text{display:inline}.vot-label-icon{vertical-align:text-bottom;cursor:help;justify-content:center;align-items:center;width:20px;height:20px;margin-left:4px;display:inline-flex}.vot-label-icon>svg{width:20px;height:20px;display:block}.vot-account{justify-content:space-between;align-items:center;gap:1rem;display:flex}.vot-account-container,.vot-account-wrapper,.vot-account-buttons{align-items:center;gap:1rem;display:flex}.vot-account-avatar{min-width:36px;max-width:36px;min-height:36px;max-height:36px;overflow:hidden}.vot-account-avatar-img{object-fit:cover;border-radius:50%;width:36px;height:36px}@property --vot-subtitles-opacity{syntax:\"<number>\";inherits:true;initial-value:.8}@property --vot-subtitles-scale-compensation{syntax:\"<number>\";inherits:true;initial-value:1}.vot-subtitles{--vot-subtitles-background:rgba(var(--vot-surface-rgb,46, 47, 52), var(--vot-subtitles-opacity,.8));--vot-subtitles-effective-max-width:var(--vot-subtitles-max-width,var(--vot-subtitles-smart-max-width,70vw));max-width:var(--vot-subtitles-effective-max-width);max-inline-size:var(--vot-subtitles-effective-max-width);background:var(--vot-subtitles-background,#2e2f34cc);width:max-content;inline-size:max-content;color:var(--vot-subtitles-color,#e3e3e3);pointer-events:all;touch-action:none;font-size:calc(var(--vot-subtitles-font-size,clamp(18px, var(--vot-subtitles-smart-font-preferred,2.2vw), 50px)) * var(--vot-subtitles-scale-compensation,1));-webkit-text-stroke:var(--vot-subtitles-text-stroke-width,clamp(1px, .08em, 2px)) var(--vot-subtitles-text-stroke-color,#000000eb);paint-order:stroke fill;text-shadow:var(--vot-subtitles-text-shadow,0 1px 2px #00000073, 0 2px 8px #00000040);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-synthesis:none;position:relative;--vot-subtitles-font-family:var(--vot-subtitles-font-family-custom,var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif))!important;font-family:var(--vot-subtitles-font-family)!important;font-style:normal!important;font-weight:var(--vot-subtitles-font-weight,500)!important;text-transform:none!important;letter-spacing:normal!important;border-radius:.5em!important;padding:.5em .75em!important;line-height:1.25!important}.vot-subtitles,.vot-subtitles *{-webkit-text-stroke:inherit;paint-order:inherit;font-family:var(--vot-subtitles-font-family)!important}.vot-subtitles{box-sizing:border-box;-webkit-user-select:none;user-select:none;contain:layout paint;isolation:isolate;text-align:center;text-wrap:balance;white-space:normal;overflow-wrap:anywhere;unicode-bidi:plaintext;margin:0 auto;display:block}.vot-subtitles-widget{--vot-subtitles-anchor-width:100vw;--vot-subtitles-anchor-height:100vh;--vot-subtitles-effective-max-width:var(--vot-subtitles-max-width,var(--vot-subtitles-smart-max-width,70vw));--vot-subtitles-smart-target-width:48ch;--vot-subtitles-smart-min-width-ratio:.62;--vot-subtitles-smart-max-width-ratio:.78;--vot-subtitles-smart-font-preferred:calc(var(--vot-subtitles-anchor-height) * .0333);--vot-subtitles-smart-max-width:clamp(calc(var(--vot-subtitles-anchor-width) * var(--vot-subtitles-smart-min-width-ratio)), var(--vot-subtitles-smart-target-width), calc(var(--vot-subtitles-anchor-width) * var(--vot-subtitles-smart-max-width-ratio)));box-sizing:border-box;z-index:2147483647;--vot-subtitles-fallback-bottom-inset:calc(env(safe-area-inset-bottom,0px) + clamp(56px, 10vh, 220px) + 10px);left:50%;top:calc(100% - var(--vot-subtitles-fallback-bottom-inset));width:max-content;inline-size:max-content;max-width:var(--vot-subtitles-effective-max-width);max-inline-size:var(--vot-subtitles-effective-max-width);pointer-events:none;will-change:left, top, transform;max-height:100%;display:block;position:absolute;transform:translate(-50%,-100%)}.vot-subtitles-info{flex-direction:column;gap:2px;max-width:100%;display:flex;padding:6px!important}.vot-subtitles-info-service,.vot-subtitles-info-header,.vot-subtitles-info-context{overflow-wrap:anywhere;word-break:break-word;white-space:normal!important}.vot-subtitles-info-service{color:var(--vot-subtitles-context-color,#86919b);margin-bottom:8px!important;font-size:10px!important;line-height:1!important}.vot-subtitles-info-header{color:var(--vot-subtitles-header-color,#fff);margin-bottom:6px!important;font-size:20px!important;font-weight:500!important;line-height:1!important}.vot-subtitles-info-context{color:var(--vot-subtitles-context-color,#86919b);font-size:12px!important;line-height:1.2!important}.vot-subtitles span[data-vot-highlight-index].passed{color:var(--vot-subtitles-passed-color,#2196f3)}.vot-subtitles span[data-vot-token=\"1\"]{cursor:pointer;white-space:normal;overflow-wrap:inherit;word-break:normal;position:relative;font-size:inherit!important;font-family:inherit!important;font-style:inherit!important;font-weight:inherit!important;line-height:inherit!important;text-transform:inherit!important;text-decoration:none!important}.vot-subtitles span[data-vot-token=\"1\"]:before{content:\"\";z-index:-1;position:absolute;inset:2px -2px;border-radius:4px!important}.vot-subtitles span[data-vot-token=\"1\"]:hover:before{background:var(--vot-subtitles-hover-color,#ffffff8c)}.vot-subtitles span[data-vot-token=\"1\"].selected:before{background:var(--vot-subtitles-passed-color,#2196f3)}.vot-subtitles span[data-vot-style-italic=\"1\"]{font-style:italic!important}.vot-subtitles span[data-vot-style-bold=\"1\"]{font-weight:700!important}.vot-subtitles span[data-vot-style-underline=\"1\"]{text-decoration:underline!important}.vot-subtitles span[data-vot-style-color=\"1\"]{color:var(--vot-subtitles-inline-color)!important}.vot-subtitles-layer{pointer-events:none;z-index:2147483647;contain:layout paint;width:100vw!important;height:100vh!important;position:fixed!important;inset:0!important}.vot-subtitles-guides{pointer-events:none;z-index:2147483646;position:absolute;inset:0}.vot-subtitles-guide{background:rgba(var(--vot-primary-rgb,33, 150, 243), .7);box-shadow:0 0 0 1px rgba(var(--vot-primary-rgb,33, 150, 243), .12);opacity:0;transition:opacity .12s linear;position:absolute}.vot-subtitles-guide[data-visible=true]{opacity:1}.vot-subtitles-guide--vertical{width:2px;transform:translate(-50%)}.vot-subtitles-guide--horizontal{height:2px;transform:translateY(-50%)}@media (aspect-ratio<=1){.vot-subtitles-widget{--vot-subtitles-smart-target-width:28ch;--vot-subtitles-smart-min-width-ratio:.8;--vot-subtitles-smart-max-width-ratio:.92;--vot-subtitles-smart-font-preferred:calc(var(--vot-subtitles-anchor-height) * .0296)}}@media (aspect-ratio>=1) and (aspect-ratio<=7/5){.vot-subtitles-widget{--vot-subtitles-smart-target-width:32ch;--vot-subtitles-smart-min-width-ratio:.55;--vot-subtitles-smart-max-width-ratio:.9;--vot-subtitles-smart-font-preferred:calc(var(--vot-subtitles-anchor-height) * .0333)}}@media (width<=900px) and (pointer:coarse){.vot-subtitles-widget{--vot-subtitles-fallback-bottom-inset:env(safe-area-inset-bottom,0px)}}@media (prefers-contrast:more){.vot-subtitles{--vot-subtitles-background:rgba(var(--vot-surface-rgb,46, 47, 52), .92);--vot-subtitles-text-stroke-width:max(2px, .1em);--vot-subtitles-text-shadow:0 2px 10px #0000008c}}:is(:fullscreen .vot-subtitles-widget,:fullscreen .vot-subtitles-widget){--vot-subtitles-smart-max-width-ratio:.8}:is(:fullscreen .vot-subtitles,:fullscreen .vot-subtitles){font-size:calc(var(--vot-subtitles-font-size,clamp(18px, var(--vot-subtitles-smart-font-preferred,2vw), 50px)) * var(--vot-subtitles-fullscreen-scale,1) * .95 * var(--vot-subtitles-scale-compensation,1))}#vot-subtitles-info.vot-subtitles-info *{-webkit-user-select:text!important;user-select:text!important}:root{--vot-font-family:\"Roboto\", \"Segoe UI\", system-ui, sans-serif;--vot-primary-rgb:139, 180, 245;--vot-onprimary-rgb:32, 33, 36;--vot-surface-rgb:32, 33, 36;--vot-onsurface-rgb:227, 227, 227;--vot-subtitles-color:rgb(var(--vot-onsurface-rgb,227, 227, 227));--vot-subtitles-passed-color:rgb(var(--vot-primary-rgb,33, 150, 243));--vot-space-1:4px;--vot-space-2:8px;--vot-space-3:12px;--vot-space-4:16px;--vot-space-5:20px;--vot-space-6:24px;--vot-radius-xs:6px;--vot-radius-s:10px;--vot-radius-m:14px;--vot-radius-l:18px;--vot-border-color:rgba(var(--vot-onsurface-rgb,227, 227, 227), .14);--vot-border-color-hover:rgba(var(--vot-onsurface-rgb,227, 227, 227), .22);--vot-shadow-1:0 1px 2px #0000002e, 0 8px 24px #00000024;--vot-shadow-2:0 2px 4px #00000038, 0 12px 32px #00000038;--vot-duration-fast:.12s;--vot-duration-medium:.2s;--vot-duration-slow:.32s;--vot-easing-standard:cubic-bezier(.4, 0, .2, 1);--vot-focus-ring-color:rgba(var(--vot-primary-rgb,139, 180, 245), .9);--vot-focus-ring:0 0 0 2px var(--vot-focus-ring-color);--vot-focus-ring-offset:0 0 0 4px rgba(var(--vot-surface-rgb,32, 33, 36), .9)}vot-block,vot-block *{box-sizing:border-box;-webkit-tap-highlight-color:transparent}vot-block[hidden]:not(.vot-menu):not(.vot-dialog-container),vot-block [hidden]:not(.vot-menu):not(.vot-dialog-container){display:none!important}vot-block{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizelegibility;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;display:block;--vot-font-family:\"Roboto\", \"Segoe UI\", system-ui, sans-serif!important;font-family:var(--vot-font-family,\"Roboto\", \"Segoe UI\", system-ui, sans-serif)!important;visibility:visible!important;font-weight:400!important}vot-block *{font-weight:inherit!important}.vot-portal-local,.vot-subtitles-widget{isolation:isolate}vot-block:focus,vot-block :focus{box-shadow:none!important;outline:none!important}html.vot-keyboard-nav vot-block:focus-visible,html.vot-keyboard-nav vot-block :focus-visible{box-shadow:var(--vot-focus-ring), var(--vot-focus-ring-offset)!important}@supports not selector(:focus-visible){html.vot-keyboard-nav vot-block:focus,html.vot-keyboard-nav vot-block :focus{box-shadow:var(--vot-focus-ring), var(--vot-focus-ring-offset)!important}}@media (prefers-reduced-motion:reduce){.vot-portal-local *,.vot-portal *,.vot-subtitles-widget *{scroll-behavior:auto!important;transition-duration:.001ms!important;animation-duration:.001ms!important;animation-iteration-count:1!important}}.vot-portal{display:inline}.vot-portal-local{z-index:2147483647;position:fixed;top:0;left:0}.vot-overlay-root{pointer-events:none}.vot-overlay-root>.vot-segmented-button,.vot-overlay-root>.vot-menu{pointer-events:auto}");
var sharedShadowStyleSheet = null;
var sharedShadowStyleSheetReady = false;
function scopeCssForShadowRoots(cssText) {
return cssText.replace(/:root\b/g, ":host").replace(/html\.vot-keyboard-nav/g, ":host-context(.vot-keyboard-nav)").replace(/:fullscreen(?=\s|,)/g, ":host-context(:fullscreen)").replace(/:-webkit-full-screen(?=\s|,)/g, ":host-context(:-webkit-full-screen)");
}
function applyClasses(element, classes) {
if (classes?.length) element.classList.add(...classes);
}
function applyInlineStyles(element, styles) {
if (!styles) return;
for (const [name, value] of Object.entries(styles)) {
if (typeof value !== "string") continue;
element.style.setProperty(name, value);
}
}
function createMountElement({ tag, classes = [], styles }) {
const element = document.createElement(tag);
applyClasses(element, classes);
applyInlineStyles(element, styles);
return element;
}
function getSharedShadowStyleSheet() {
if (sharedShadowStyleSheetReady) return sharedShadowStyleSheet;
sharedShadowStyleSheetReady = true;
if (!(typeof CSSStyleSheet !== "undefined" && typeof CSSStyleSheet.prototype.replaceSync === "function")) return null;
const sheet = new CSSStyleSheet();
sheet.replaceSync(shadowScopedCssText);
sharedShadowStyleSheet = sheet;
return sharedShadowStyleSheet;
}
function adoptScopedStyles(shadowRoot) {
const sharedSheet = getSharedShadowStyleSheet();
if (sharedSheet) {
if (!shadowRoot.adoptedStyleSheets.includes(sharedSheet)) shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, sharedSheet];
return;
}
const style = document.createElement("style");
style.textContent = shadowScopedCssText;
shadowRoot.append(style);
}
function createShadowMount({ parent, hostTag = "vot-shadow-host", rootTag = "vot-block", hostClasses = [], rootClasses = [], hostStyles, rootStyles, delegatesFocus = false }) {
const host = createMountElement({
tag: hostTag,
classes: hostClasses,
styles: hostStyles
});
const shadowRoot = host.attachShadow({
mode: "open",
delegatesFocus
});
adoptScopedStyles(shadowRoot);
const root = createMountElement({
tag: rootTag,
classes: rootClasses,
styles: rootStyles
});
shadowRoot.append(root);
parent.append(host);
return {
host,
root,
shadowRoot
};
}
function reparentShadowMount(mount, parent) {
if (!mount) return;
if (mount.host.parentNode !== parent) parent.append(mount.host);
}
function destroyShadowMount(mount) {
mount?.host.remove();
}
//#endregion
//#region src/subtitles/layoutController.ts
function isTimeInLine(time, line) {
return time >= line.startMs && time < line.startMs + line.durationMs;
}
//#endregion
//#region src/subtitles/activeCues.ts
var createFallbackTokens = (line) => {
if (line.tokens.length) return line.tokens;
const text = line.text.trim();
if (!text) return [];
return [{
text,
startMs: line.startMs,
durationMs: line.durationMs,
isWordLike: Boolean(text)
}];
};
var toRenderableTextKey = (line) => {
return (line.text || createFallbackTokens(line).map((token) => token.text).join("")).replaceAll(/\s+/gu, " ").trim();
};
var linesOverlapInTime = (left, right) => {
const leftEnd = left.startMs + Math.max(0, left.durationMs);
const rightEnd = right.startMs + Math.max(0, right.durationMs);
return left.startMs < rightEnd && right.startMs < leftEnd;
};
var dedupeActiveLines = (lines) => {
const deduped = [];
for (const entry of lines) {
const textKey = toRenderableTextKey(entry.line);
if (!textKey) continue;
if (!deduped.some((existing) => {
return textKey === toRenderableTextKey(existing.line) && existing.line.speakerId === entry.line.speakerId && linesOverlapInTime(existing.line, entry.line);
})) deduped.push(entry);
}
return deduped;
};
var findLastCueIndexStartingAtOrBefore = (time, subtitlesList) => {
let low = 0;
let high = subtitlesList.length - 1;
let candidate = -1;
while (low <= high) {
const mid = low + high >> 1;
if (subtitlesList[mid].startMs <= time) {
candidate = mid;
low = mid + 1;
continue;
}
high = mid - 1;
}
return candidate;
};
var findActiveSubtitleLineIndices = (time, subtitlesList, maxCueDurationMs = Number.POSITIVE_INFINITY) => {
const lastCueIndex = findLastCueIndexStartingAtOrBefore(time, subtitlesList);
if (lastCueIndex < 0) return [];
const minStartMs = Number.isFinite(maxCueDurationMs) ? Math.max(0, time - Math.max(0, maxCueDurationMs)) : Number.NEGATIVE_INFINITY;
const activeLineIndices = [];
for (let index = lastCueIndex; index >= 0; index -= 1) {
const line = subtitlesList[index];
if (line.startMs < minStartMs) break;
if (isTimeInLine(time, line)) activeLineIndices.push(index);
}
activeLineIndices.reverse();
return activeLineIndices;
};
var buildActiveSubtitleRenderLine = (time, subtitlesList, maxCueDurationMs = Number.POSITIVE_INFINITY) => {
const activeLineIndices = findActiveSubtitleLineIndices(time, subtitlesList, maxCueDurationMs);
if (!activeLineIndices.length) return null;
const activeEntries = dedupeActiveLines(activeLineIndices.map((index) => ({
index,
line: subtitlesList[index]
})));
if (!activeEntries.length) return null;
if (activeEntries.length === 1) {
const [entry] = activeEntries;
return {
line: entry.line,
lineKey: `${entry.index}`
};
}
const tokens = [];
const textParts = [];
const rawTextParts = [];
const lineKeyParts = [];
let earliestStartMs = Number.POSITIVE_INFINITY;
let latestEndMs = 0;
for (const entry of activeEntries) {
const lineTokens = createFallbackTokens(entry.line);
if (!lineTokens.length) continue;
if (tokens.length > 0) {
const breakStartMs = Math.max(earliestStartMs, entry.line.startMs);
tokens.push({
text: "\n",
startMs: breakStartMs,
durationMs: 0,
isWordLike: false
});
}
tokens.push(...lineTokens);
textParts.push(entry.line.text || lineTokens.map((token) => token.text).join(""));
rawTextParts.push(entry.line.metadata?.rawText ?? entry.line.text);
lineKeyParts.push(`${entry.index}`);
earliestStartMs = Math.min(earliestStartMs, entry.line.startMs);
latestEndMs = Math.max(latestEndMs, entry.line.startMs + Math.max(0, entry.line.durationMs));
}
if (!tokens.length || !lineKeyParts.length) return null;
return {
line: {
text: textParts.join("\n"),
startMs: earliestStartMs,
durationMs: Math.max(0, latestEndMs - earliestStartMs),
speakerId: activeEntries[0]?.line.speakerId ?? "0",
tokens,
metadata: rawTextParts.length ? { rawText: rawTextParts.join("\n") } : void 0
},
lineKey: lineKeyParts.join(",")
};
};
//#endregion
//#region src/types/subtitles.ts
var subtitleFormats = [
"srt",
"vtt",
"ass",
"json"
];
var subtitleFontFamilies = [
"default-sans",
"arial",
"helvetica",
"roboto",
"verdana",
"open-sans",
"poppins",
"lato",
"montserrat",
"barlow"
];
var subtitleFontFamilyCss = {
"default-sans": `"Roboto", "Segoe UI", system-ui, sans-serif`,
arial: `Arial, "Helvetica Neue", Helvetica, sans-serif`,
helvetica: `"Helvetica Neue", Helvetica, Arial, sans-serif`,
roboto: `"Roboto", "Segoe UI", system-ui, sans-serif`,
verdana: `Verdana, Geneva, sans-serif`,
"open-sans": `"Open Sans", "Segoe UI", system-ui, sans-serif`,
poppins: `"Poppins", "Segoe UI", system-ui, sans-serif`,
lato: `"Lato", "Segoe UI", system-ui, sans-serif`,
montserrat: `"Montserrat", "Segoe UI", system-ui, sans-serif`,
barlow: `"Barlow", "Segoe UI", system-ui, sans-serif`
};
//#endregion
//#region src/subtitles/types.ts
var subtitleFormatsSet = new Set(subtitleFormats);
function isSubtitleFormat(value) {
return typeof value === "string" && subtitleFormatsSet.has(value);
}
function isRecord(value) {
return value !== null && typeof value === "object";
}
function parseSubtitleDescriptor(value) {
if (!isRecord(value)) return null;
const format = value.format;
if (typeof value.source !== "string" || typeof value.language !== "string" || typeof value.url !== "string" || !isSubtitleFormat(format)) return null;
return {
source: value.source,
format,
language: value.language,
url: value.url,
translatedFromLanguage: typeof value.translatedFromLanguage === "string" ? value.translatedFromLanguage : void 0,
isAutoGenerated: typeof value.isAutoGenerated === "boolean" ? value.isAutoGenerated : void 0
};
}
function isBuiltInSubtitleFontFamily(fontFamily) {
return subtitleFontFamilies.includes(fontFamily);
}
//#endregion
//#region src/subtitles/fonts.ts
var GOOGLE_SUBTITLE_FONT_PREFIX = "google:";
var GOOGLE_FONTS_CSS_API_URL = "https://fonts.googleapis.com/css2";
var GOOGLE_FONTS_METADATA_URL = "https://fonts.google.com/metadata/fonts";
var subtitleGoogleFontFamilyNames = {
roboto: "Roboto",
"open-sans": "Open Sans",
poppins: "Poppins",
lato: "Lato",
montserrat: "Montserrat",
barlow: "Barlow"
};
var loadedSubtitleGoogleFonts = /* @__PURE__ */ new Set();
var pendingSubtitleGoogleFonts = /* @__PURE__ */ new Map();
var googleFontsCatalogPromise = null;
function toGoogleSubtitleFontFamily(familyName) {
return `${GOOGLE_SUBTITLE_FONT_PREFIX}${familyName}`;
}
function getGoogleSubtitleFontFamilyName(fontFamily) {
if (fontFamily.startsWith(GOOGLE_SUBTITLE_FONT_PREFIX)) {
const familyName = fontFamily.slice(7).trim();
return familyName.length > 0 ? familyName : null;
}
return subtitleGoogleFontFamilyNames[fontFamily] ?? null;
}
function getSubtitleFontFamilyCssValue(fontFamily) {
if (isBuiltInSubtitleFontFamily(fontFamily)) return subtitleFontFamilyCss[fontFamily];
const googleFontFamilyName = getGoogleSubtitleFontFamilyName(fontFamily);
if (googleFontFamilyName) return `"${googleFontFamilyName}", "Segoe UI", system-ui, sans-serif`;
return subtitleFontFamilyCss["default-sans"];
}
function buildGoogleFontsCssUrl(fontFamily) {
const googleFontFamily = getGoogleSubtitleFontFamilyName(fontFamily);
if (!googleFontFamily) return null;
return `${GOOGLE_FONTS_CSS_API_URL}?family=${googleFontFamily.trim().replaceAll(/\s+/g, "+")}&display=swap`;
}
function injectFontStylesheet(fontFamily, cssText) {
const styleId = `vot-google-font-${fontFamily}`;
if (document.getElementById(styleId)) return;
const gmAddStyle = globalThis.GM_addStyle;
const styleElement = typeof gmAddStyle === "function" ? gmAddStyle(cssText) : document.createElement("style");
if (!(styleElement instanceof HTMLElement)) return;
styleElement.id = styleId;
if (!styleElement.textContent) styleElement.textContent = cssText;
if (!styleElement.parentElement) (document.head || document.documentElement).appendChild(styleElement);
}
async function ensureGoogleSubtitleFontLoaded(fontFamily, options = {}) {
if (loadedSubtitleGoogleFonts.has(fontFamily)) return;
const existingLoad = pendingSubtitleGoogleFonts.get(fontFamily);
if (existingLoad !== void 0) {
await existingLoad;
return;
}
const cssUrl = buildGoogleFontsCssUrl(fontFamily);
if (!cssUrl) {
loadedSubtitleGoogleFonts.add(fontFamily);
return;
}
const googleFontFamily = getGoogleSubtitleFontFamilyName(fontFamily);
const loadPromise = (async () => {
try {
const response = await GM_fetch(cssUrl, {
timeout: 1e4,
forceGmXhr: options.forceGmXhr ?? true,
headers: { Accept: "text/css,*/*;q=0.1" }
});
if (!response.ok) throw new Error(`Google Fonts CSS request failed with ${response.status}`);
const cssText = await response.text();
if (!cssText.trim()) throw new Error("Google Fonts CSS response is empty");
injectFontStylesheet(fontFamily, cssText);
loadedSubtitleGoogleFonts.add(fontFamily);
if (document.fonts && googleFontFamily) await document.fonts.load(`500 20px "${googleFontFamily}"`);
options.onLoaded?.();
} catch (error) {
debug.log("Failed to load Google Font for subtitles", {
fontFamily,
error
});
} finally {
pendingSubtitleGoogleFonts.delete(fontFamily);
}
})();
pendingSubtitleGoogleFonts.set(fontFamily, loadPromise);
await loadPromise;
}
async function loadGoogleFontsCatalog() {
if (googleFontsCatalogPromise !== null) return await googleFontsCatalogPromise;
googleFontsCatalogPromise = (async () => {
const response = await GM_fetch(GOOGLE_FONTS_METADATA_URL, {
timeout: 15e3,
forceGmXhr: isSupportGMXhr
});
if (!response.ok) throw new Error(`Google Fonts metadata request failed with ${response.status}`);
const sanitizedText = (await response.text()).replace(/^\)\]\}'\n?/, "");
const fontFamilies = JSON.parse(sanitizedText).familyMetadataList?.map((entry) => entry.family?.trim() ?? "").filter((familyName) => familyName.length > 0);
return Array.from(new Set(fontFamilies)).sort((left, right) => left.localeCompare(right));
})().catch((error) => {
googleFontsCatalogPromise = null;
debug.log("Failed to load Google Fonts catalog", error);
return [];
});
return await googleFontsCatalogPromise;
}
//#endregion
//#region src/subtitles/fullscreenLayerController.ts
var FullscreenLayerController = class {
container;
constructor({ container }) {
this.container = container;
}
updateContainer(container) {
this.container = container;
}
getWidgetParentElement() {
return this.container;
}
getLayoutRootElement() {
return this.container instanceof ShadowRoot ? this.container.host : this.container;
}
syncWidgetContainer(widgetContainer) {
const containerEl = this.container instanceof ShadowRoot ? this.container.host : this.container;
if (getComputedStyle(containerEl).position === "static") containerEl.style.position = "relative";
if (widgetContainer && widgetContainer.parentNode !== this.container) this.container.appendChild(widgetContainer);
}
release() {}
};
//#endregion
//#region src/subtitles/inlineStyle.ts
var SAFE_CSS_COLOR_NAME_RE = /^[a-z]+$/iu;
var SAFE_HEX_COLOR_RE = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/iu;
var SAFE_CSS_FUNCTION_COLOR_RE = /^(?:rgb|rgba|hsl|hsla)\(\s*[0-9.,%\s/+-]+\)$/iu;
var SAFE_CLASS_NAME_RE = /^[a-z0-9_-]+$/iu;
var normalizeClassNames = (classes) => {
if (!classes?.length) return void 0;
const normalized = Array.from(new Set(classes.map((value) => value.trim()).filter((value) => value && SAFE_CLASS_NAME_RE.test(value)))).sort((left, right) => left.localeCompare(right));
return normalized.length ? normalized : void 0;
};
var normalizeCssColorValue = (value) => {
const normalized = value.trim();
if (!normalized) return void 0;
if (SAFE_HEX_COLOR_RE.test(normalized)) return normalized.toLowerCase();
if (SAFE_CSS_COLOR_NAME_RE.test(normalized)) return normalized.toLowerCase();
if (SAFE_CSS_FUNCTION_COLOR_RE.test(normalized)) return normalized;
};
var normalizeSubtitleInlineStyle = (style) => {
if (!style) return void 0;
const normalized = {};
if (style.italic) normalized.italic = true;
if (style.bold) normalized.bold = true;
if (style.underline) normalized.underline = true;
const normalizedColor = typeof style.color === "string" ? normalizeCssColorValue(style.color) : void 0;
if (normalizedColor) normalized.color = normalizedColor;
const normalizedClasses = normalizeClassNames(style.classes);
if (normalizedClasses) normalized.classes = normalizedClasses;
return Object.keys(normalized).length ? normalized : void 0;
};
var sanitizeSubtitleInlineStyle = (value) => {
if (!value || typeof value !== "object") return void 0;
const raw = value;
return normalizeSubtitleInlineStyle({
italic: raw.italic === true,
bold: raw.bold === true,
underline: raw.underline === true,
color: typeof raw.color === "string" ? raw.color : void 0,
classes: Array.isArray(raw.classes) ? raw.classes.filter((entry) => typeof entry === "string") : void 0
});
};
var subtitleInlineStylesEqual = (left, right) => {
const leftNormalized = normalizeSubtitleInlineStyle(left);
const rightNormalized = normalizeSubtitleInlineStyle(right);
const leftClasses = leftNormalized?.classes ?? [];
const rightClasses = rightNormalized?.classes ?? [];
return Boolean(leftNormalized?.italic) === Boolean(rightNormalized?.italic) && Boolean(leftNormalized?.bold) === Boolean(rightNormalized?.bold) && Boolean(leftNormalized?.underline) === Boolean(rightNormalized?.underline) && (leftNormalized?.color ?? "") === (rightNormalized?.color ?? "") && leftClasses.length === rightClasses.length && leftClasses.every((value, index) => value === rightClasses[index]);
};
var buildSubtitleInlineStyleCssText = (style) => {
const normalized = normalizeSubtitleInlineStyle(style);
if (!normalized?.color) return "";
return `--vot-subtitles-inline-color:${normalized.color};`;
};
//#endregion
//#region src/subtitles/positionController.ts
function clampToRange(value, min, max) {
return Math.max(min, Math.min(value, max));
}
function hasDragThresholdBeenExceeded(startClientX, startClientY, nextClientX, nextClientY, thresholdPx) {
const dx = nextClientX - startClientX;
const dy = nextClientY - startClientY;
return dx * dx + dy * dy >= thresholdPx * thresholdPx;
}
function getVerticalAnchorBounds({ elementHeight, boxHeight, bottomInset }) {
const minAnchorY = Math.max(0, elementHeight || 0);
const baselineAnchorY = Math.max(minAnchorY, boxHeight - bottomInset);
return {
minAnchorY,
baselineAnchorY,
travelPx: Math.max(0, baselineAnchorY - minAnchorY)
};
}
function captureCustomVerticalAnchorState({ anchorY, elementHeight, boxHeight, bottomInset }) {
const { minAnchorY, baselineAnchorY, travelPx } = getVerticalAnchorBounds({
elementHeight,
boxHeight,
bottomInset
});
return {
offsetFromBaselinePx: clampToRange(anchorY, minAnchorY, baselineAnchorY) - baselineAnchorY,
travelPx
};
}
function resolveCustomVerticalAnchor({ state, elementHeight, boxHeight, bottomInset }) {
const { minAnchorY, baselineAnchorY, travelPx } = getVerticalAnchorBounds({
elementHeight,
boxHeight,
bottomInset
});
if (!state || travelPx <= 0) return baselineAnchorY;
const storedTravelPx = Math.max(0, state.travelPx || 0);
const storedLiftPx = Math.max(0, -(state.offsetFromBaselinePx || 0));
if (storedTravelPx <= 0 || storedLiftPx <= 0) return baselineAnchorY;
const ratioLiftPx = storedLiftPx / storedTravelPx * travelPx;
return clampToRange(baselineAnchorY - (travelPx >= storedTravelPx ? Math.min(storedLiftPx, ratioLiftPx) : Math.min(travelPx, ratioLiftPx)), minAnchorY, baselineAnchorY);
}
function clampAnchorWithinBox({ anchorX, anchorY, elementWidth, elementHeight, boxWidth, boxHeight, bottomInset }) {
let nextAnchorX = anchorX;
let nextAnchorY = anchorY;
const maxAnchorY = Math.max(0, boxHeight - bottomInset);
const minAnchorY = elementHeight || 0;
if (elementWidth) {
let leftPx = nextAnchorX - elementWidth / 2;
const maxLeftPx = boxWidth - elementWidth;
if (maxLeftPx >= 0) leftPx = clampToRange(leftPx, 0, maxLeftPx);
else leftPx = maxLeftPx / 2;
nextAnchorX = leftPx + elementWidth / 2;
}
nextAnchorY = clampToRange(nextAnchorY, minAnchorY, maxAnchorY);
return {
anchorX: nextAnchorX,
anchorY: nextAnchorY
};
}
function snapValueToNearestCandidate({ current, candidates, thresholdPx }) {
let closestValue = current;
let closestDistance = Number.POSITIVE_INFINITY;
for (const candidate of candidates) {
const distance = Math.abs(candidate - current);
if (distance < closestDistance) {
closestDistance = distance;
closestValue = candidate;
}
}
if (!Number.isFinite(closestDistance) || closestDistance > thresholdPx) return {
snapped: false,
value: current
};
return {
snapped: true,
value: closestValue
};
}
//#endregion
//#region src/subtitles/renderPlan.ts
var LEADING_PUNCTUATION_RE = /^[\p{P}\p{S}]+/u;
var TRAILING_PUNCTUATION_RE = /[\p{P}\p{S}]+$/u;
var PUNCTUATION_ONLY_RE = /^[\p{P}\p{S}]+$/u;
var TEXT_TOKEN_SLICE_RE = /\s+|[\p{P}\p{S}]+|[^\s\p{P}\p{S}]+/gu;
var pushTextPart = (plan, text, style, options = {}) => {
plan.push({
kind: "text",
text,
style,
highlightIndex: options.highlightIndex
});
if (options.withBreak) plan.push({ kind: "break" });
};
var skipWhitespaceTokens = (tokens, startIndex, renderEndTokenIndex) => {
let index = startIndex;
while (index <= renderEndTokenIndex && !tokens[index]?.isWordLike && !tokens[index]?.text.trim()) index += 1;
return index;
};
var hasFutureWordToken = (tokens, startIndex, renderEndTokenIndex) => {
for (let index = startIndex; index <= renderEndTokenIndex; index += 1) {
const tokenText = tokens[index]?.text ?? "";
if (!tokens[index]?.isWordLike || !tokenText.trim()) continue;
if (tokenText.trimStart().replace(LEADING_PUNCTUATION_RE, "").replace(TRAILING_PUNCTUATION_RE, "")) return true;
}
return false;
};
var consumeWordToken = (plan, tokens, startIndex, renderEndTokenIndex, breakAfterTokenIndexSet, highlightIndex) => {
const token = tokens[startIndex];
const leadingWhitespace = /^\s+/u.exec(token.text)?.[0] ?? "";
const body = token.text.slice(leadingWhitespace.length);
if (leadingWhitespace) pushTextPart(plan, leadingWhitespace, token.style);
const leadingPunctuation = LEADING_PUNCTUATION_RE.exec(body)?.[0] ?? "";
const bodyWithoutLeadingPunctuation = body.slice(leadingPunctuation.length);
const trailingPunctuation = TRAILING_PUNCTUATION_RE.exec(bodyWithoutLeadingPunctuation)?.[0] ?? "";
const wordText = trailingPunctuation ? bodyWithoutLeadingPunctuation.slice(0, bodyWithoutLeadingPunctuation.length - trailingPunctuation.length) : bodyWithoutLeadingPunctuation;
if (!wordText) {
if (body) pushTextPart(plan, body, token.style);
if (!breakAfterTokenIndexSet?.has(startIndex)) return {
consumedWord: false,
nextTokenIndex: startIndex + 1
};
plan.push({ kind: "break" });
return {
consumedWord: false,
nextTokenIndex: skipWhitespaceTokens(tokens, startIndex + 1, renderEndTokenIndex)
};
}
if (leadingPunctuation) pushTextPart(plan, leadingPunctuation, token.style, { highlightIndex });
plan.push({
kind: "word",
text: wordText,
style: token.style,
highlightIndex
});
if (trailingPunctuation) pushTextPart(plan, trailingPunctuation, token.style, { highlightIndex });
if (!breakAfterTokenIndexSet?.has(startIndex)) return {
consumedWord: true,
nextTokenIndex: startIndex + 1
};
plan.push({ kind: "break" });
return {
consumedWord: true,
nextTokenIndex: skipWhitespaceTokens(tokens, startIndex + 1, renderEndTokenIndex)
};
};
var consumeTextToken = (plan, tokenIndex, tokens, renderEndTokenIndex, token, tokenText, hasBreakAfter, lastWordHighlightIndex, nextWordHighlightIndex) => {
const fallbackHighlightIndex = lastWordHighlightIndex ?? (hasFutureWordToken(tokens, tokenIndex + 1, renderEndTokenIndex) ? nextWordHighlightIndex : void 0);
const textParts = tokenText.match(TEXT_TOKEN_SLICE_RE) ?? [tokenText];
for (const textPart of textParts) pushTextPart(plan, textPart, token.style, { highlightIndex: PUNCTUATION_ONLY_RE.test(textPart) ? fallbackHighlightIndex : void 0 });
if (hasBreakAfter) {
plan.push({ kind: "break" });
return skipWhitespaceTokens(tokens, tokenIndex + 1, renderEndTokenIndex);
}
return tokenIndex + 1;
};
/**
* Build a render plan for subtitle tokens preserving existing grouping rules.
*
* Important detail: leading punctuation before a word (for example "(" or "\"")
* should be visually highlighted together with that word.
*/
function buildSubtitleRenderPlan(tokens, renderEndTokenIndex, breakAfterTokenIndexSet) {
const plan = [];
let wordHighlightIndex = 0;
let lastWordHighlightIndex = null;
for (let i = 0; i <= renderEndTokenIndex;) {
const token = tokens[i];
const tokenText = token?.text ?? "";
if (!tokenText) {
i += 1;
continue;
}
if (tokenText === "\n") {
plan.push({ kind: "break" });
i += 1;
continue;
}
if (token.isWordLike) {
const result = consumeWordToken(plan, tokens, i, renderEndTokenIndex, breakAfterTokenIndexSet, wordHighlightIndex);
i = result.nextTokenIndex;
if (result.consumedWord) {
lastWordHighlightIndex = wordHighlightIndex;
wordHighlightIndex += 1;
}
continue;
}
const hasBreakAfter = Boolean(breakAfterTokenIndexSet?.has(i));
i = consumeTextToken(plan, i, tokens, renderEndTokenIndex, token, tokenText, hasBreakAfter, lastWordHighlightIndex, wordHighlightIndex);
}
return plan;
}
//#endregion
//#region src/subtitles/smartLayout.ts
var clampNumber = (value, min, max) => Math.min(max, Math.max(min, value));
var roundToInt = (value) => Math.round(value);
var resolveAspectBand = (aspect) => {
if (aspect < .8) return {
widthRatio: .9,
charsPerLine: 27,
fontHeightRatio: .03
};
if (aspect < 1.1) return {
widthRatio: .84,
charsPerLine: 31,
fontHeightRatio: .031
};
if (aspect < 1.5) return {
widthRatio: .76,
charsPerLine: 36,
fontHeightRatio: .033
};
if (aspect < 1.95) return {
widthRatio: .72,
charsPerLine: 40,
fontHeightRatio: .034
};
return {
widthRatio: .68,
charsPerLine: 44,
fontHeightRatio: .035
};
};
var resolveWidthBoost = (width) => {
if (width >= 1920) return {
extraChars: 4,
widthScale: 1.04
};
if (width >= 1440) return {
extraChars: 3,
widthScale: 1.03
};
if (width >= 960) return {
extraChars: 2,
widthScale: 1.02
};
if (width >= 640) return {
extraChars: 1,
widthScale: 1.01
};
return {
extraChars: 0,
widthScale: 1
};
};
var estimateAverageGlyphWidth = (fontSizePx) => Math.max(7, fontSizePx * .56);
function computeSmartLayoutForBox(box, cssMetrics = null) {
const width = Number.isFinite(box.w) ? Math.max(0, box.w) : 0;
const height = Number.isFinite(box.h) ? Math.max(0, box.h) : 0;
if (width <= 0 || height <= 0) return {
fontSizePx: cssMetrics?.fontSizePx ?? 20,
maxWidthPx: cssMetrics?.maxWidthPx ?? null
};
const { widthRatio, charsPerLine, fontHeightRatio } = resolveAspectBand(width / height);
const { extraChars, widthScale } = resolveWidthBoost(width);
const derivedFontSizePx = clampNumber(height * fontHeightRatio, 16, 42);
const fontSizePx = cssMetrics?.fontSizePx ?? derivedFontSizePx;
const averageGlyphWidth = estimateAverageGlyphWidth(fontSizePx);
const minWidthPx = width * Math.min(.92, widthRatio);
const maxWidthPx = width * clampNumber(widthRatio * widthScale, .66, .92);
return {
fontSizePx,
maxWidthPx: roundToInt(clampNumber(clampNumber(charsPerLine + extraChars, 25, 48) * averageGlyphWidth, minWidthPx, maxWidthPx))
};
}
//#endregion
//#region src/subtitles/smartWrap.ts
var STRONG_BREAK_RE = /[.!?…:;][)"'\]»”]*\s*$/u;
var SOFT_BREAK_RE = /[,،、][)"'\]»”]*\s*$/u;
var WHITESPACE_CHAR_RE = /\s/u;
var DISCOURAGED_LINE_START_CHAR_RE = /^[\p{Pe}\p{Pf},.;:!?%\u2030\u2026]$/u;
var DISCOURAGED_LINE_END_CHAR_RE = /^[\p{Ps}\p{Pi}\u00BF\u00A1([{\u00AB\u201C"'`-]$/u;
var normalizeTokenText = (text) => text.replaceAll(/\s+/gu, " ").trim();
var getNextChar = (text, index) => {
if (index >= text.length) return null;
const codePoint = text.codePointAt(index);
if (codePoint === void 0) return null;
const char = String.fromCodePoint(codePoint);
return {
char,
nextIndex: index + char.length
};
};
var getPreviousChar = (text, index) => {
if (index <= 0) return null;
let start = index - 1;
const lastCodeUnit = text.charCodeAt(start);
if (lastCodeUnit >= 56320 && lastCodeUnit <= 57343 && start > 0) start -= 1;
return {
char: text.slice(start, index),
previousIndex: start
};
};
var isWhitespaceChar = (char) => WHITESPACE_CHAR_RE.test(char);
var buildTokenTextBuffer = (tokens) => {
const offsets = new Array(tokens.length + 1);
offsets[0] = 0;
let fullText = "";
for (let index = 0; index < tokens.length; index += 1) {
fullText += tokens[index]?.text ?? "";
offsets[index + 1] = fullText.length;
}
return {
fullText,
offsets
};
};
var getBufferedTokenText = (buffer, startToken, endToken) => {
if (endToken <= startToken) return "";
return buffer.fullText.slice(buffer.offsets[startToken], buffer.offsets[endToken]);
};
var resolveBoundary = (text) => {
if (STRONG_BREAK_RE.test(text)) return "strong";
if (SOFT_BREAK_RE.test(text)) return "soft";
return "neutral";
};
var rangeStartsWithDiscouragedLineStart = (buffer, startToken, endToken) => {
let index = buffer.offsets[startToken];
const end = buffer.offsets[endToken];
while (index < end) {
const next = getNextChar(buffer.fullText, index);
if (!next) return false;
if (!isWhitespaceChar(next.char)) return DISCOURAGED_LINE_START_CHAR_RE.test(next.char);
index = next.nextIndex;
}
return false;
};
var rangeEndsWithDiscouragedLineEnd = (buffer, startToken, endToken) => {
const start = buffer.offsets[startToken];
let index = buffer.offsets[endToken];
while (index > start) {
const previous = getPreviousChar(buffer.fullText, index);
if (!previous) return false;
if (!isWhitespaceChar(previous.char)) return DISCOURAGED_LINE_END_CHAR_RE.test(previous.char);
index = previous.previousIndex;
}
return false;
};
var isWordToken = (token) => Boolean(token?.isWordLike && token.text.trim());
var getTokenStartMs = (token) => token && Number.isFinite(token.startMs) ? token.startMs : 0;
var getTokenEndMs = (token) => token ? getTokenStartMs(token) + Math.max(0, token.durationMs) : 0;
var getRangeStartMs = (tokens, start, end) => {
for (let index = start; index < end; index += 1) {
const token = tokens[index];
if (isWordToken(token)) return getTokenStartMs(token);
}
return getTokenStartMs(tokens[start]);
};
var getRangeEndMs = (tokens, start, end) => {
for (let index = end - 1; index >= start; index -= 1) {
const token = tokens[index];
if (isWordToken(token)) return getTokenEndMs(token);
}
return getTokenEndMs(tokens[end - 1]);
};
var createForcedBreakSlice = (tokens, tokenIndex) => {
const token = tokens[tokenIndex];
const startMs = getTokenStartMs(token);
return {
text: "\n",
tokenIndex,
breakAfterTokenIndex: tokenIndex,
startToken: tokenIndex,
endToken: tokenIndex + 1,
charLength: 0,
startMs,
endMs: startMs,
boundary: "strong",
forcesLineBreak: true
};
};
var buildSliceFromWord = (tokens, wordTokenIndex, textBuffer, includeMetrics) => {
let startToken = wordTokenIndex;
while (startToken > 0 && tokens[startToken - 1]?.text !== "\n" && !isWordToken(tokens[startToken - 1])) startToken -= 1;
let endToken = wordTokenIndex + 1;
while (endToken < tokens.length && tokens[endToken]?.text !== "\n" && !isWordToken(tokens[endToken])) endToken += 1;
const text = getBufferedTokenText(textBuffer, startToken, endToken);
return {
text,
tokenIndex: wordTokenIndex,
breakAfterTokenIndex: endToken - 1,
startToken,
endToken,
charLength: includeMetrics ? normalizeTokenText(text).length : 0,
startMs: includeMetrics ? getRangeStartMs(tokens, startToken, endToken) : 0,
endMs: includeMetrics ? getRangeEndMs(tokens, startToken, endToken) : 0,
boundary: resolveBoundary(text),
forcesLineBreak: false
};
};
function buildWordSlicesFromBuffer(tokens, textBuffer, collectKey) {
const slices = [];
const keyParts = collectKey ? [] : null;
let index = 0;
while (index < tokens.length) {
const token = tokens[index];
if (!token?.text) {
index += 1;
continue;
}
if (token.text === "\n") {
const slice = createForcedBreakSlice(tokens, index);
slices.push(slice);
keyParts?.push("\n");
index += 1;
continue;
}
if (!isWordToken(token)) {
index += 1;
continue;
}
const slice = buildSliceFromWord(tokens, index, textBuffer, collectKey);
slices.push(slice);
keyParts?.push(normalizeTokenText(slice.text));
index = slice.breakAfterTokenIndex + 1;
}
if (!slices.length && tokens.length) {
const text = textBuffer.fullText;
slices.push({
text,
tokenIndex: 0,
breakAfterTokenIndex: tokens.length - 1,
startToken: 0,
endToken: tokens.length,
charLength: collectKey ? normalizeTokenText(text).length : 0,
startMs: collectKey ? getRangeStartMs(tokens, 0, tokens.length) : 0,
endMs: collectKey ? getRangeEndMs(tokens, 0, tokens.length) : 0,
boundary: resolveBoundary(text),
forcesLineBreak: false
});
keyParts?.push(normalizeTokenText(text));
}
return {
slices,
key: keyParts?.join("|") ?? ""
};
}
function buildWordSlices(tokens) {
return buildWordSlicesFromBuffer(tokens, buildTokenTextBuffer(tokens), true);
}
function measureWordSlices(wordSlices, measureText) {
return wordSlices.map((slice) => ({
...slice,
width: slice.forcesLineBreak ? 0 : measureText(slice.text)
}));
}
var getSegmentEndMs = (tokens, endTokenExclusive) => {
if (endTokenExclusive <= 0) return 0;
return getTokenEndMs(tokens[endTokenExclusive - 1]);
};
var finalizeSegment = (out, tokens, startToken, endToken) => {
if (endToken <= startToken) return;
const startMs = getRangeStartMs(tokens, startToken, endToken);
const endMs = getSegmentEndMs(tokens, endToken);
out.push({
startToken,
endToken,
startMs,
endMs: Math.max(startMs, endMs)
});
};
function computeTwoLineSegments(tokens, metrics, maxWidthPx, maxLength) {
if (!metrics.length || !tokens.length) return [];
const maxWidth = Math.max(1, Number.isFinite(maxWidthPx) ? maxWidthPx : 0);
const charBudget = Math.max(1, Number.isFinite(maxLength) ? maxLength : 0);
const segments = [];
let segmentStartToken = metrics[0].startToken;
let segmentCharLength = 0;
let currentLineWidth = 0;
let currentLineCount = 1;
let lastTokenInSegment = segmentStartToken;
for (const metric of metrics) {
if (metric.forcesLineBreak) {
currentLineCount += 1;
currentLineWidth = 0;
lastTokenInSegment = metric.endToken;
if (currentLineCount > 2) {
const splitToken = Math.max(metric.startToken, metric.tokenIndex);
finalizeSegment(segments, tokens, segmentStartToken, splitToken);
segmentStartToken = splitToken;
segmentCharLength = 0;
lastTokenInSegment = segmentStartToken;
}
continue;
}
const nextCharLength = segmentCharLength + metric.charLength;
if ((currentLineWidth === 0 || currentLineWidth + metric.width <= maxWidth) && nextCharLength <= charBudget) {
currentLineWidth += metric.width;
segmentCharLength = nextCharLength;
lastTokenInSegment = metric.endToken;
continue;
}
if (currentLineCount === 1) {
currentLineCount = 2;
currentLineWidth = metric.width;
segmentCharLength = nextCharLength;
lastTokenInSegment = metric.endToken;
continue;
}
const splitToken = Math.max(metric.startToken, metric.tokenIndex);
finalizeSegment(segments, tokens, segmentStartToken, splitToken);
segmentStartToken = splitToken;
segmentCharLength = metric.charLength;
currentLineWidth = metric.width;
currentLineCount = 1;
lastTokenInSegment = metric.endToken;
}
finalizeSegment(segments, tokens, segmentStartToken, lastTokenInSegment);
if (!segments.length) return [{
startToken: 0,
endToken: tokens.length,
startMs: getRangeStartMs(tokens, 0, tokens.length),
endMs: getRangeEndMs(tokens, 0, tokens.length)
}];
for (let index = 0; index < segments.length - 1; index += 1) {
const current = segments[index];
const next = segments[index + 1];
if (next.startMs > current.startMs) current.endMs = next.startMs;
}
const last = segments.at(-1);
if (last) last.endMs = Math.max(last.endMs, getRangeEndMs(tokens, last.startToken, last.endToken));
return segments.filter((segment) => segment.endToken > segment.startToken);
}
var measureTokenRange = (textBuffer, startToken, endToken, measureText) => {
if (endToken <= startToken) return 0;
return measureText(getBufferedTokenText(textBuffer, startToken, endToken));
};
var resolveSafeBreakAfterTokenIndex = (tokens, breakAfterTokenIndex) => {
let safeBreakIndex = breakAfterTokenIndex;
while (safeBreakIndex + 1 < tokens.length && tokens[safeBreakIndex + 1]?.text !== "\n" && !tokens[safeBreakIndex + 1]?.isWordLike) safeBreakIndex += 1;
return safeBreakIndex;
};
var findFallbackBreakAfterTokenIndex = (tokens, textBuffer, measureText, maxWidthPx) => {
let bestBreakAfterTokenIndex = null;
let bestScore = Number.POSITIVE_INFINITY;
for (let index = 0; index < tokens.length - 1; index += 1) {
const token = tokens[index];
const nextToken = tokens[index + 1];
if (!token?.text || !nextToken?.text || nextToken.text === "\n") continue;
const candidateBreakAfterTokenIndex = resolveSafeBreakAfterTokenIndex(tokens, index);
if (candidateBreakAfterTokenIndex >= tokens.length - 1) continue;
const firstEndToken = candidateBreakAfterTokenIndex + 1;
const secondStartToken = firstEndToken;
const firstWidth = measureTokenRange(textBuffer, 0, firstEndToken, measureText);
const secondWidth = measureTokenRange(textBuffer, secondStartToken, tokens.length, measureText);
const score = Math.max(0, firstWidth - maxWidthPx) * 12 + Math.max(0, secondWidth - maxWidthPx) * 12 + Math.abs(secondWidth - firstWidth) * .4 + (rangeStartsWithDiscouragedLineStart(textBuffer, secondStartToken, tokens.length) ? 260 : 0) + (rangeEndsWithDiscouragedLineEnd(textBuffer, 0, firstEndToken) ? 70 : 0);
if (score < bestScore) {
bestScore = score;
bestBreakAfterTokenIndex = candidateBreakAfterTokenIndex;
}
}
return bestBreakAfterTokenIndex;
};
var scoreBreakCandidate = ({ firstWidth, secondWidth, lineStartPenalty, lineEndPenalty, firstWordCount, secondWordCount, maxWidthPx, boundary }) => {
const overflowPenalty = Math.max(0, firstWidth - maxWidthPx) * 12 + Math.max(0, secondWidth - maxWidthPx) * 12;
const balancePenalty = Math.abs(secondWidth / Math.max(firstWidth, 1) - 1.08) * 120;
const shortTopPenalty = firstWordCount < 2 ? 80 : 0;
const orphanPenalty = secondWordCount < 2 ? 80 : 0;
const boundaryBonus = boundary === "strong" ? -28 : boundary === "soft" ? -14 : 0;
return overflowPenalty + balancePenalty + shortTopPenalty + orphanPenalty + lineStartPenalty + lineEndPenalty + boundaryBonus;
};
function computeTokenWrapPlan(tokens, measureText, maxWidthPx) {
if (!tokens.length) return { breakAfterTokenIndices: [] };
for (const token of tokens) if (token.text === "\n") return { breakAfterTokenIndices: [] };
const textBuffer = buildTokenTextBuffer(tokens);
const safeMaxWidthPx = Number.isFinite(maxWidthPx) ? maxWidthPx : 0;
if (safeMaxWidthPx <= 0) return { breakAfterTokenIndices: [] };
const { slices } = buildWordSlicesFromBuffer(tokens, textBuffer, false);
const measurableSlices = slices.filter((slice) => !slice.forcesLineBreak);
if (!measurableSlices.length) return { breakAfterTokenIndices: [] };
if (measureTokenRange(textBuffer, 0, tokens.length, measureText) <= safeMaxWidthPx) return { breakAfterTokenIndices: [] };
let bestBreakAfterTokenIndex = null;
let bestScore = Number.POSITIVE_INFINITY;
for (let index = 0; index < measurableSlices.length - 1; index += 1) {
const slice = measurableSlices[index];
const nextSlice = measurableSlices[index + 1];
const candidateBreakAfterTokenIndex = Math.max(slice.breakAfterTokenIndex, nextSlice.tokenIndex - 1);
const firstEndToken = candidateBreakAfterTokenIndex + 1;
const secondStartToken = nextSlice.tokenIndex;
const score = scoreBreakCandidate({
firstWidth: measureTokenRange(textBuffer, 0, firstEndToken, measureText),
secondWidth: measureTokenRange(textBuffer, secondStartToken, tokens.length, measureText),
lineStartPenalty: rangeStartsWithDiscouragedLineStart(textBuffer, secondStartToken, tokens.length) ? 260 : 0,
lineEndPenalty: rangeEndsWithDiscouragedLineEnd(textBuffer, 0, firstEndToken) ? 70 : 0,
firstWordCount: index + 1,
secondWordCount: measurableSlices.length - (index + 1),
maxWidthPx: safeMaxWidthPx,
boundary: slice.boundary
});
if (score < bestScore) {
bestScore = score;
bestBreakAfterTokenIndex = candidateBreakAfterTokenIndex;
}
}
if (bestBreakAfterTokenIndex !== null) return { breakAfterTokenIndices: [bestBreakAfterTokenIndex] };
const fallbackBreakAfterTokenIndex = findFallbackBreakAfterTokenIndex(tokens, textBuffer, measureText, safeMaxWidthPx);
if (fallbackBreakAfterTokenIndex !== null) return { breakAfterTokenIndices: [fallbackBreakAfterTokenIndex] };
return { breakAfterTokenIndices: [] };
}
//#endregion
//#region src/subtitles/widget.ts
var WRAP_WIDTH_GUARD_PX = 8;
var WRAP_WIDTH_GUARD_RATIO = .97;
var MIN_EFFECTIVE_WRAP_WIDTH_PX = 24;
function applyWrapWidthGuard(maxWidthPx) {
if (!Number.isFinite(maxWidthPx) || maxWidthPx <= 0) return 0;
const byPixelGuard = maxWidthPx - WRAP_WIDTH_GUARD_PX;
const byRatioGuard = maxWidthPx * WRAP_WIDTH_GUARD_RATIO;
return Math.max(MIN_EFFECTIVE_WRAP_WIDTH_PX, Math.min(byPixelGuard, byRatioGuard));
}
var SubtitlesWidget = class {
video;
container;
fullscreenLayerController;
tooltipMount;
subtitlesContainer = null;
subtitlesBlock = null;
renderedHighlightEls = [];
passedFlagsBuffer = [];
subtitles = null;
subtitleLang;
lastRenderKey = null;
lastActiveLineKey = null;
maxActiveCueLookbackMs = 0;
highlightWords = false;
fontSize = 20;
fontSizeOverridden = false;
fontFamily = "default-sans";
maxLength = 300;
smartLayoutEnabled = true;
smartFontSizePx = 0;
smartMaxWidthPx = 0;
smartAnchorWidthPx = 0;
smartAnchorHeightPx = 0;
lastSmartLayoutKey = null;
lastSmartLayoutCheckTs = 0;
opacity = "0.2";
repositionPending = false;
positionRefreshPending = false;
updatePending = false;
lastUpdateRequestTs = 0;
updateMinIntervalMs = 100;
updateMinIntervalHighlightMs = 33;
useVideoFrameCallbacks;
videoFrameRequestId = null;
lastPlaybackTimeMs = null;
dragAbortController = null;
lastPositionRefreshTs = 0;
positionRefreshIntervalMs = 250;
subtitleMaxWidthPx = 0;
breakAfterTokenIndices = [];
breakAfterTokenIndexSet = null;
wrapPending = false;
lastWrapKey = null;
lastWrapTokens = null;
measureCanvas = null;
measureCtx = null;
tokenProcessingMemo = null;
tokenPrecomputeMemo = null;
lineMeasureMemo = null;
lastSegmentIndex = 0;
lastAppliedLeftPct = null;
lastAppliedTopPct = null;
position = {
left: 50,
top: 100
};
customVerticalAnchorState = null;
positionPreset = "bottom-center";
dragging = {
pointerId: null,
candidate: false,
active: false,
moved: false,
startClientX: 0,
startClientY: 0,
offset: {
x: 0,
y: 0
}
};
dragStartThresholdPx = 4;
snapThresholdPx = 18;
suppressTokenClicksUntil = 0;
abortController = new AbortController();
resizeObserver;
tokenTooltip;
tooltipTranslationRequestId = 0;
intervalIdleChecker;
checkerUnsubscribe = null;
edgePunctuationTrimRe = /(?:^[\p{P}\p{S}]+|[\p{P}\p{S}]+$)/gu;
strTokens = "";
strTranslatedTokens = "";
passedStateKey = null;
passedThresholds = [];
normalizeTokenTextForTranslation(raw) {
return raw.trim().replace(this.edgePunctuationTrimRe, "");
}
bottomInsetCachedPx = 0;
safeAreaBottomInsetCachedPx = 0;
containerPaddingBottomCachedPx = 0;
insetCacheReady = false;
bottomInsetByMode = {
normal: {
ratio: .1,
minPx: 56,
maxPx: 220,
gapPx: 10
},
fullscreen: {
ratio: .07,
minPx: 44,
maxPx: 140,
gapPx: 9
}
};
safeAreaProbeEl = null;
guidesLayer = null;
verticalGuide = null;
horizontalGuide = null;
onPointerDownBound;
onPointerUpBound;
onPointerMoveBound;
onPlaybackStateChangeBound;
onVisualViewportChangeBound;
constructor(video, container, intervalIdleChecker) {
this.video = video;
this.container = container;
this.fullscreenLayerController = new FullscreenLayerController({ container });
this.intervalIdleChecker = intervalIdleChecker;
this.useVideoFrameCallbacks = !!this.video && typeof this.video.requestVideoFrameCallback === "function";
this.onPointerDownBound = (event) => this.onPointerDown(event);
this.onPointerUpBound = (event) => this.onPointerUp(event);
this.onPointerMoveBound = (event) => this.onPointerMove(event);
this.onPlaybackStateChangeBound = () => this.handlePlaybackStateChange();
this.onVisualViewportChangeBound = () => this.scheduleReposition();
this.checkerUnsubscribe = this.intervalIdleChecker.subscribe(() => {
this.onCheckerTick();
});
this.bindEvents();
}
updateMount({ container }) {
const containerChanged = this.container !== container;
this.container = container;
this.fullscreenLayerController.updateContainer(container);
this.syncWidgetMount();
if (containerChanged) {
const parentElement = this.getTokenTooltipParentElement();
this.tokenTooltip?.updateMount({
parentElement,
layoutRoot: this.tooltipMount?.host
});
}
if (this.subtitles) {
this.insetCacheReady = false;
this.lastAppliedLeftPct = null;
this.lastAppliedTopPct = null;
this.updateContainerRect();
this.requestUpdate();
}
}
resetTranslationContext(releaseTooltip = false) {
this.strTranslatedTokens = "";
if (releaseTooltip) this.releaseTooltip();
}
resetSegmentationMemo() {
this.tokenProcessingMemo = null;
this.tokenPrecomputeMemo = null;
this.lineMeasureMemo = null;
this.lastSegmentIndex = 0;
}
resetWrapMemo() {
this.setBreakAfterTokenIndices([]);
this.lastWrapKey = null;
}
resetRenderMemo() {
this.lastRenderKey = null;
}
computeAnchorBoxLayout(layout) {
const fallback = {
left: 0,
top: 0,
w: layout.w,
h: layout.h
};
const video = this.video;
if (!video) return fallback;
const videoRect = video.getBoundingClientRect();
if (!(videoRect.width > 0 && videoRect.height > 0)) return fallback;
const containerRect = layout.rect;
if (!(videoRect.right > containerRect.left && videoRect.left < containerRect.right && videoRect.bottom > containerRect.top && videoRect.top < containerRect.bottom)) return fallback;
const w = videoRect.width / layout.scaleX;
const h = videoRect.height / layout.scaleY;
if (!(w > 0 && h > 0)) return fallback;
const rawLeft = (videoRect.left - containerRect.left) / layout.scaleX;
const rawTop = (videoRect.top - containerRect.top) / layout.scaleY;
const maxLeft = layout.w - w;
const maxTop = layout.h - h;
return {
left: maxLeft >= 0 ? clampToRange(rawLeft, 0, maxLeft) : (layout.w - w) / 2,
top: maxTop >= 0 ? clampToRange(rawTop, 0, maxTop) : (layout.h - h) / 2,
w,
h
};
}
readSmartCssMetrics() {
const block = this.subtitlesBlock;
if (!block) return null;
const cs = getComputedStyle(block);
const fontSizePx = Number.parseFloat(cs.fontSize);
const maxWidthRawPx = Number.parseFloat(cs.maxWidth);
if (!Number.isFinite(fontSizePx) || !Number.isFinite(maxWidthRawPx) || fontSizePx <= 0 || maxWidthRawPx <= 0) return null;
this.subtitleMaxWidthPx = maxWidthRawPx;
const paddingLeft = Number.parseFloat(cs.paddingLeft) || 0;
const paddingRight = Number.parseFloat(cs.paddingRight) || 0;
const maxWidthPx = Math.max(0, maxWidthRawPx - paddingLeft - paddingRight);
if (maxWidthPx <= 0) return null;
return {
fontSizePx,
maxWidthPx
};
}
ensureSmartLayout(anchorBox) {
if (!this.smartLayoutEnabled) return null;
const cssMetrics = this.readSmartCssMetrics();
const nextFontSizePx = cssMetrics?.fontSizePx ?? this.smartFontSizePx;
const next = computeSmartLayoutForBox(anchorBox, cssMetrics);
const nextMaxWidthPx = next.maxWidthPx ?? this.smartMaxWidthPx;
const nextKey = `${Math.round(nextFontSizePx)}|${Math.round(nextMaxWidthPx)}|${Math.round(next.maxWidthPx ?? 0)}`;
const fontChanged = Math.abs(nextFontSizePx - this.smartFontSizePx) > .5;
const widthChanged = Math.abs(nextMaxWidthPx - this.smartMaxWidthPx) > .5;
if (nextKey !== this.lastSmartLayoutKey) {
this.lastSmartLayoutKey = nextKey;
this.smartFontSizePx = nextFontSizePx;
this.smartMaxWidthPx = nextMaxWidthPx;
this.resetRenderMemo();
}
this.setSubtitlesContainerVar("--vot-subtitles-max-width", next.maxWidthPx && next.maxWidthPx > 0 ? `${next.maxWidthPx}px` : null);
if ((fontChanged || widthChanged) && this.lastWrapTokens) {
this.lastWrapKey = null;
this.resetSegmentationMemo();
this.scheduleWrapRecompute();
}
return next;
}
scheduleReposition() {
if (this.abortController.signal.aborted) return;
if (!this.subtitles) return;
this.repositionPending = true;
this.intervalIdleChecker.markActivity("subtitles-reposition");
this.intervalIdleChecker.requestImmediateTick();
}
setSubtitlesContainerVar(name, value) {
const container = this.subtitlesContainer;
if (!container) return;
if (value === null) {
container.style.removeProperty(name);
return;
}
container.style.setProperty(name, value);
}
applyOpacityStyle() {
this.setSubtitlesContainerVar("--vot-subtitles-opacity", this.opacity);
}
applyManualFontSizeStyle() {
if (!this.smartLayoutEnabled && this.fontSizeOverridden) {
this.setSubtitlesContainerVar("--vot-subtitles-font-size", `${this.fontSize}px`);
return;
}
this.setSubtitlesContainerVar("--vot-subtitles-font-size", null);
}
applyFontFamilyStyle() {
const fontFamily = this.fontFamily;
this.setSubtitlesContainerVar("--vot-subtitles-font-family-custom", getSubtitleFontFamilyCssValue(fontFamily));
ensureGoogleSubtitleFontLoaded(fontFamily, {
forceGmXhr: true,
onLoaded: () => {
if (this.fontFamily !== fontFamily) return;
this.lastWrapKey = null;
this.resetSegmentationMemo();
this.scheduleWrapRecompute();
this.scheduleReposition();
}
});
}
syncVisualStyleVars() {
this.applyOpacityStyle();
this.applyManualFontSizeStyle();
this.applyFontFamilyStyle();
}
ensureGuidesLayer() {
if (this.guidesLayer) return this.guidesLayer;
const layer = document.createElement("vot-block");
layer.classList.add("vot-subtitles-guides");
const verticalGuide = document.createElement("vot-block");
verticalGuide.classList.add("vot-subtitles-guide", "vot-subtitles-guide--vertical");
const horizontalGuide = document.createElement("vot-block");
horizontalGuide.classList.add("vot-subtitles-guide", "vot-subtitles-guide--horizontal");
layer.append(verticalGuide, horizontalGuide);
this.guidesLayer = layer;
this.verticalGuide = verticalGuide;
this.horizontalGuide = horizontalGuide;
this.hideSnapGuides();
return layer;
}
hideSnapGuides() {
this.verticalGuide?.removeAttribute("data-visible");
this.horizontalGuide?.removeAttribute("data-visible");
}
updateSnapGuides(anchorBox, options) {
const { showVerticalCenter = false, showHorizontalCenter = false } = options;
if (!this.ensureGuidesLayer().isConnected) this.syncGuideLayerMount();
if (this.verticalGuide) {
this.verticalGuide.style.left = `${anchorBox.left + anchorBox.w / 2}px`;
this.verticalGuide.style.top = `${anchorBox.top}px`;
this.verticalGuide.style.height = `${anchorBox.h}px`;
if (showVerticalCenter) this.verticalGuide.dataset.visible = "true";
else delete this.verticalGuide.dataset.visible;
}
if (this.horizontalGuide) {
this.horizontalGuide.style.left = `${anchorBox.left}px`;
this.horizontalGuide.style.top = `${anchorBox.top + anchorBox.h / 2}px`;
this.horizontalGuide.style.width = `${anchorBox.w}px`;
if (showHorizontalCenter) this.horizontalGuide.dataset.visible = "true";
else delete this.horizontalGuide.dataset.visible;
}
}
syncGuideLayerMount() {
const guidesLayer = this.ensureGuidesLayer();
if (guidesLayer.parentElement !== this.container) this.container.appendChild(guidesLayer);
}
syncWidgetMount() {
this.fullscreenLayerController.syncWidgetContainer(null);
if (this.subtitlesContainer && this.subtitlesContainer.parentElement !== this.container) this.container.appendChild(this.subtitlesContainer);
if (this.tooltipMount) reparentShadowMount(this.tooltipMount, this.container);
this.syncGuideLayerMount();
}
ensureTooltipMount() {
if (!this.tooltipMount) this.tooltipMount = createShadowMount({
parent: this.container,
rootClasses: ["vot-portal-local"],
hostStyles: {
position: "absolute",
inset: "0",
display: "block",
"pointer-events": "none"
},
rootStyles: {
position: "relative",
display: "block",
width: "100%",
height: "100%",
"pointer-events": "none"
}
});
else reparentShadowMount(this.tooltipMount, this.container);
return this.tooltipMount;
}
getTokenTooltipParentElement() {
return this.ensureTooltipMount().root;
}
createSubtitlesContainer() {
if (this.subtitlesContainer) return this.subtitlesContainer;
const container = document.createElement("vot-block");
container.classList.add("vot-subtitles-widget");
this.subtitlesContainer = container;
this.syncWidgetMount();
container.addEventListener("pointerdown", this.onPointerDownBound, {
signal: this.abortController.signal,
passive: false,
capture: true
});
this.syncVisualStyleVars();
this.insetCacheReady = false;
this.updateContainerRect();
return container;
}
bindEvents() {
const { signal } = this.abortController;
const opts = { signal };
for (const eventName of [
"play",
"pause",
"seeking",
"seeked",
"ended"
]) this.video?.addEventListener(eventName, this.onPlaybackStateChangeBound, opts);
this.resizeObserver = new ResizeObserver(() => this.onResize());
const resizeTarget = this.container instanceof ShadowRoot ? this.container.host : this.container;
this.resizeObserver.observe(resizeTarget);
if (this.video) this.resizeObserver.observe(this.video);
globalThis.visualViewport?.addEventListener("resize", this.onVisualViewportChangeBound, opts);
globalThis.visualViewport?.addEventListener("scroll", this.onVisualViewportChangeBound, opts);
}
getUpdateMinIntervalMs() {
return this.highlightWords ? this.updateMinIntervalHighlightMs : this.updateMinIntervalMs;
}
requestUpdate(playbackTimeMs, now = performance.now()) {
if (this.abortController.signal.aborted) return;
if (!this.subtitles) return;
if (typeof playbackTimeMs === "number" && Number.isFinite(playbackTimeMs)) this.lastPlaybackTimeMs = Math.max(0, playbackTimeMs);
else if (this.video) this.lastPlaybackTimeMs = Math.max(0, this.video.currentTime * 1e3);
const minInterval = this.getUpdateMinIntervalMs();
if (now - this.lastUpdateRequestTs < minInterval) return;
this.lastUpdateRequestTs = now;
this.updatePending = true;
this.intervalIdleChecker.requestImmediateTick();
}
resolvePlaybackTimeMs() {
if (typeof this.lastPlaybackTimeMs === "number" && Number.isFinite(this.lastPlaybackTimeMs)) return this.lastPlaybackTimeMs;
return this.video ? Math.max(0, this.video.currentTime * 1e3) : 0;
}
handlePlaybackStateChange() {
if (!this.subtitles) {
this.stopVideoFrameLoop();
return;
}
this.scheduleReposition();
this.requestUpdate(this.video ? Math.max(0, this.video.currentTime * 1e3) : 0);
this.syncVideoFrameLoop();
}
syncVideoFrameLoop() {
if (!this.useVideoFrameCallbacks) return;
const video = this.video;
if (!video) return;
if (!this.subtitles || video.paused || video.ended) {
this.stopVideoFrameLoop();
return;
}
this.startVideoFrameLoop();
}
startVideoFrameLoop() {
if (!this.useVideoFrameCallbacks) return;
const video = this.video;
if (!video) return;
if (this.videoFrameRequestId !== null) return;
this.videoFrameRequestId = video.requestVideoFrameCallback(this.onVideoFrame);
}
stopVideoFrameLoop() {
if (!this.useVideoFrameCallbacks) return;
const video = this.video;
if (!video) return;
if (this.videoFrameRequestId === null) return;
try {
video.cancelVideoFrameCallback(this.videoFrameRequestId);
} catch {}
this.videoFrameRequestId = null;
}
onVideoFrame = (now, metadata) => {
this.videoFrameRequestId = null;
if (this.abortController.signal.aborted) return;
const video = this.video;
if (!video || video.paused || video.ended || !this.subtitles) return;
const mediaTime = Number.isFinite(metadata.mediaTime) ? metadata.mediaTime : null;
const rawTime = mediaTime === 0 && video.currentTime > 0 ? video.currentTime : mediaTime;
const playbackTimeMs = rawTime != null ? rawTime * 1e3 : void 0;
this.requestUpdate(playbackTimeMs, now);
this.startVideoFrameLoop();
};
onCheckerTick() {
if (this.abortController.signal.aborted) return;
if (this.repositionPending) {
this.repositionPending = false;
this.updateContainerRect();
this.updatePending = true;
}
if (this.wrapPending) {
this.wrapPending = false;
this.recomputeWrapNow();
}
if (this.positionRefreshPending) {
this.positionRefreshPending = false;
this.applySubtitlePosition();
}
if (this.updatePending) {
this.updatePending = false;
this.update();
}
}
attachDragDocumentListeners() {
if (this.dragAbortController) return;
const dragAbortController = new AbortController();
const { signal } = dragAbortController;
document.addEventListener("pointermove", this.onPointerMoveBound, {
signal,
passive: false,
capture: true
});
document.addEventListener("pointerup", this.onPointerUpBound, {
signal,
capture: true
});
document.addEventListener("pointercancel", this.onPointerUpBound, {
signal,
capture: true
});
this.dragAbortController = dragAbortController;
}
detachDragDocumentListeners() {
this.dragAbortController?.abort();
this.dragAbortController = null;
}
onResize() {
this.syncWidgetMount();
this.scheduleReposition();
}
updateContainerRect() {
const layout = this.getLayoutSize();
if (!layout.w || !layout.h) return;
const anchorBox = this.computeAnchorBoxLayout(layout);
if (!anchorBox.w || !anchorBox.h) return;
this.refreshBottomInsetNow(layout, anchorBox);
this.applySubtitlePositionWithLayout(layout, anchorBox);
}
getLayoutSize() {
const layoutRoot = this.fullscreenLayerController.getLayoutRootElement();
const rect = layoutRoot.getBoundingClientRect();
const w = layoutRoot.clientWidth || rect.width;
const h = layoutRoot.clientHeight || rect.height;
return {
w,
h,
rect,
scaleX: rect.width && w ? rect.width / w : 1,
scaleY: rect.height && h ? rect.height / h : 1
};
}
ensureSafeAreaProbe() {
if (this.safeAreaProbeEl) return;
const el = document.createElement("div");
el.style.position = "fixed";
el.style.left = "0";
el.style.right = "0";
el.style.bottom = "0";
el.style.height = "env(safe-area-inset-bottom, 0px)";
el.style.pointerEvents = "none";
el.style.opacity = "0";
el.style.zIndex = "-1";
document.documentElement.appendChild(el);
this.safeAreaProbeEl = el;
}
getSafeAreaBottomInsetPx() {
this.ensureSafeAreaProbe();
if (!this.safeAreaProbeEl) return 0;
return this.safeAreaProbeEl.offsetHeight || 0;
}
refreshInsetCache() {
const layoutRoot = this.fullscreenLayerController.getLayoutRootElement();
this.safeAreaBottomInsetCachedPx = this.getSafeAreaBottomInsetPx();
this.containerPaddingBottomCachedPx = Number.parseFloat(getComputedStyle(layoutRoot).paddingBottom || "0") || 0;
this.insetCacheReady = true;
}
isMobileViewport() {
return globalThis.matchMedia?.("(max-width: 900px) and (pointer: coarse)")?.matches ?? false;
}
getBottomInsetPreset() {
const doc = document;
const fullscreenEl = doc.fullscreenElement ?? doc.webkitFullscreenElement;
if (!(fullscreenEl instanceof Element)) return this.bottomInsetByMode.normal;
const { container, video } = this;
if (fullscreenEl === container || fullscreenEl.contains(container) || container.contains(fullscreenEl)) return this.bottomInsetByMode.fullscreen;
if (video && (fullscreenEl === video || fullscreenEl.contains(video) || video.contains(fullscreenEl))) return this.bottomInsetByMode.fullscreen;
return this.bottomInsetByMode.normal;
}
computeReservedBottomInsetPx(anchorBoxH, preset = this.getBottomInsetPreset()) {
return clampToRange(anchorBoxH * preset.ratio, preset.minPx, preset.maxPx);
}
refreshBottomInsetNow(layout, anchorBox) {
this.refreshInsetCache();
const anchorH = anchorBox?.h ?? this.computeAnchorBoxLayout(layout ?? this.getLayoutSize()).h;
if (!anchorH) {
this.bottomInsetCachedPx = 0;
return;
}
const preset = this.getBottomInsetPreset();
this.bottomInsetCachedPx = this.computeReservedBottomInsetPx(anchorH, preset);
}
getBottomInsetPx(layout, anchorBox) {
if (!this.insetCacheReady) this.refreshInsetCache();
const preset = this.getBottomInsetPreset();
const safeAreaBottom = this.safeAreaBottomInsetCachedPx;
const paddingBottom = this.containerPaddingBottomCachedPx;
if (this.isMobileViewport()) return Math.max(paddingBottom, safeAreaBottom);
const anchorH = anchorBox?.h ?? this.computeAnchorBoxLayout(layout ?? this.getLayoutSize()).h;
const reserved = anchorH ? this.computeReservedBottomInsetPx(anchorH, preset) : preset.minPx;
const stableInset = Math.max(this.bottomInsetCachedPx, reserved);
return Math.max(paddingBottom, safeAreaBottom, stableInset) + preset.gapPx;
}
onPointerDown(event) {
const subtitlesContainer = this.subtitlesContainer;
if (!subtitlesContainer) return;
const target = event.target;
if (!(target instanceof Node) || !subtitlesContainer.contains(target)) return;
if (!event.isPrimary) return;
if (event.pointerType === "mouse" && event.button !== 0) return;
event.stopPropagation();
const layout = this.getLayoutSize();
const { rect: containerRect, w, h, scaleX, scaleY } = layout;
if (!w || !h) return;
const anchorBox = this.computeAnchorBoxLayout(layout);
if (!anchorBox.w || !anchorBox.h) return;
this.lastPositionRefreshTs = performance.now();
const subRect = subtitlesContainer.getBoundingClientRect();
const pointerX = (event.clientX - containerRect.left) / scaleX - anchorBox.left;
const pointerY = (event.clientY - containerRect.top) / scaleY - anchorBox.top;
const anchorX = (subRect.left - containerRect.left + subRect.width / 2) / scaleX - anchorBox.left;
const anchorY = (subRect.top - containerRect.top + subRect.height) / scaleY - anchorBox.top;
this.dragging.pointerId = event.pointerId;
this.dragging.candidate = true;
this.dragging.active = false;
this.dragging.moved = false;
this.dragging.startClientX = event.clientX;
this.dragging.startClientY = event.clientY;
this.dragging.offset.x = anchorX - pointerX;
this.dragging.offset.y = anchorY - pointerY;
this.hideSnapGuides();
this.attachDragDocumentListeners();
}
onPointerUp(event) {
if (this.dragging.pointerId === null) return;
if (event.pointerId !== this.dragging.pointerId) return;
if (this.dragging.moved) this.suppressTokenClicksUntil = performance.now() + 450;
this.dragging.pointerId = null;
this.dragging.candidate = false;
this.dragging.active = false;
this.dragging.moved = false;
this.hideSnapGuides();
this.detachDragDocumentListeners();
}
onPointerMove(event) {
if (!this.dragging.candidate || this.dragging.pointerId === null) return;
if (event.pointerId !== this.dragging.pointerId) return;
if (!this.dragging.active) {
if (!hasDragThresholdBeenExceeded(this.dragging.startClientX, this.dragging.startClientY, event.clientX, event.clientY, this.dragStartThresholdPx)) return;
this.dragging.active = true;
this.dragging.moved = true;
this.suppressTokenClicksUntil = performance.now() + 450;
this.releaseTooltip();
try {
this.subtitlesContainer?.setPointerCapture(event.pointerId);
} catch {}
} else this.dragging.moved = true;
event.preventDefault();
event.stopPropagation();
const layout = this.getLayoutSize();
const { rect: containerRect, w, h, scaleX, scaleY } = layout;
if (!w || !h) return;
const anchorBox = this.computeAnchorBoxLayout(layout);
if (!anchorBox.w || !anchorBox.h) return;
const pointerX = (event.clientX - containerRect.left) / scaleX - anchorBox.left;
const pointerY = (event.clientY - containerRect.top) / scaleY - anchorBox.top;
let anchorX = pointerX + this.dragging.offset.x;
let anchorY = pointerY + this.dragging.offset.y;
const elW = this.subtitlesContainer?.offsetWidth ?? 0;
const elH = this.subtitlesContainer?.offsetHeight ?? 0;
const bottomInset = this.getBottomInsetPx(layout, anchorBox);
const snappedX = snapValueToNearestCandidate({
current: anchorX,
candidates: [anchorBox.w / 2],
thresholdPx: this.snapThresholdPx
});
if (snappedX.snapped) anchorX = snappedX.value;
const verticalCenterAnchor = anchorBox.h / 2 + elH / 2;
const snappedY = snapValueToNearestCandidate({
current: anchorY,
candidates: [verticalCenterAnchor],
thresholdPx: this.snapThresholdPx
});
if (snappedY.snapped) anchorY = snappedY.value;
({anchorX, anchorY} = clampAnchorWithinBox({
anchorX,
anchorY,
elementWidth: elW,
elementHeight: elH,
boxWidth: anchorBox.w,
boxHeight: anchorBox.h,
bottomInset
}));
this.positionPreset = "custom";
this.customVerticalAnchorState = captureCustomVerticalAnchorState({
anchorY,
elementHeight: elH,
boxHeight: anchorBox.h,
bottomInset
});
this.position.left = anchorX / anchorBox.w * 100;
this.position.top = anchorY / anchorBox.h * 100;
this.updateSnapGuides(anchorBox, {
showVerticalCenter: snappedX.snapped,
showHorizontalCenter: snappedY.snapped
});
this.applySubtitlePositionWithLayout(layout, anchorBox);
}
applySubtitlePosition() {
if (!this.subtitlesContainer) return;
const layout = this.getLayoutSize();
if (!layout.w || !layout.h) return;
const anchorBox = this.computeAnchorBoxLayout(layout);
if (!anchorBox.w || !anchorBox.h) return;
this.applySubtitlePositionWithLayout(layout, anchorBox);
}
applySubtitlePositionWithLayout(layout, anchorBox) {
const subtitlesContainer = this.subtitlesContainer;
if (!subtitlesContainer) return;
this.applyScaleCompensation(subtitlesContainer, layout);
this.syncAnchorDimensions(subtitlesContainer, anchorBox);
if (this.smartLayoutEnabled) this.ensureSmartLayout(anchorBox);
const elW = subtitlesContainer.offsetWidth;
const elH = subtitlesContainer.offsetHeight;
const bottomInset = this.getBottomInsetPx(layout, anchorBox);
const anchorPosition = this.resolveCurrentAnchorPosition(anchorBox, elW, elH, bottomInset);
const containerPosition = this.clampContainerPosition(anchorBox, anchorPosition.anchorX, anchorPosition.anchorY, elW, elH, bottomInset);
const anchorX = containerPosition.anchorX;
const anchorY = containerPosition.anchorY;
const containerAnchorX = anchorBox.left + anchorX;
const containerAnchorY = anchorBox.top + anchorY;
const leftPct = containerAnchorX / layout.w * 100;
const topPct = containerAnchorY / layout.h * 100;
this.updateContainerPosition(subtitlesContainer, leftPct, topPct);
this.tokenTooltip?.updatePos();
}
applyScaleCompensation(subtitlesContainer, layout) {
const visualScale = Math.min(layout.scaleX || 1, layout.scaleY || 1);
const compensate = visualScale > 0 && visualScale < .999 ? Math.min(1 / visualScale, 3) : 1;
if (Math.abs(compensate - 1) < .001) {
subtitlesContainer.style.removeProperty("--vot-subtitles-scale-compensation");
return;
}
subtitlesContainer.style.setProperty("--vot-subtitles-scale-compensation", compensate.toFixed(3));
}
syncAnchorDimensions(subtitlesContainer, anchorBox) {
const anchorWidthPx = Math.max(1, Math.round(anchorBox.w));
const anchorHeightPx = Math.max(1, Math.round(anchorBox.h));
if (!(anchorWidthPx !== this.smartAnchorWidthPx || anchorHeightPx !== this.smartAnchorHeightPx)) return;
this.smartAnchorWidthPx = anchorWidthPx;
this.smartAnchorHeightPx = anchorHeightPx;
subtitlesContainer.style.setProperty("--vot-subtitles-anchor-width", `${anchorWidthPx}px`);
subtitlesContainer.style.setProperty("--vot-subtitles-anchor-height", `${anchorHeightPx}px`);
if (this.lastWrapTokens) {
this.lastWrapKey = null;
this.resetSegmentationMemo();
this.scheduleWrapRecompute();
}
}
resolveCurrentAnchorPosition(anchorBox, elementWidth, elementHeight, bottomInset) {
let anchorX = this.position.left / 100 * anchorBox.w;
let anchorY = this.position.top / 100 * anchorBox.h;
if (this.positionPreset === "custom") {
anchorY = resolveCustomVerticalAnchor({
state: this.customVerticalAnchorState,
elementHeight,
boxHeight: anchorBox.h,
bottomInset
});
return {
anchorX,
anchorY
};
}
const presetPosition = this.resolvePresetAnchorPosition({
preset: this.positionPreset,
anchorBox,
elementWidth,
elementHeight,
bottomInset
});
anchorX = presetPosition.anchorX;
anchorY = presetPosition.anchorY;
if (anchorBox.w > 0) this.position.left = anchorX / anchorBox.w * 100;
if (anchorBox.h > 0) this.position.top = anchorY / anchorBox.h * 100;
return {
anchorX,
anchorY
};
}
clampContainerPosition(anchorBox, anchorX, anchorY, elementWidth, elementHeight, bottomInset) {
let leftPx = anchorX - elementWidth / 2;
let topPx = anchorY - elementHeight;
const maxLeftPx = anchorBox.w - elementWidth;
const maxTopPx = anchorBox.h - bottomInset - elementHeight;
leftPx = maxLeftPx >= 0 ? clampToRange(leftPx, 0, maxLeftPx) : maxLeftPx / 2;
topPx = maxTopPx >= 0 ? clampToRange(topPx, 0, maxTopPx) : 0;
return {
anchorX: leftPx + elementWidth / 2,
anchorY: topPx + elementHeight
};
}
updateContainerPosition(subtitlesContainer, leftPct, topPct) {
if (this.lastAppliedLeftPct === null || Math.abs(leftPct - this.lastAppliedLeftPct) >= .01) {
subtitlesContainer.style.left = `${leftPct}%`;
this.lastAppliedLeftPct = leftPct;
}
if (this.lastAppliedTopPct === null || Math.abs(topPct - this.lastAppliedTopPct) >= .01) {
subtitlesContainer.style.top = `${topPct}%`;
this.lastAppliedTopPct = topPct;
}
}
resolvePresetAnchorPosition({ preset, anchorBox, elementWidth, elementHeight, bottomInset }) {
let anchorX = anchorBox.w / 2;
let anchorY = anchorBox.h - bottomInset;
switch (preset) {
case "top-center":
anchorY = elementHeight;
break;
case "center":
anchorY = anchorBox.h / 2 + elementHeight / 2;
break;
case "bottom-left":
anchorX = elementWidth / 2;
break;
case "bottom-right":
anchorX = anchorBox.w - elementWidth / 2;
break;
case "bottom-center":
case "custom": break;
}
return clampAnchorWithinBox({
anchorX,
anchorY,
elementWidth,
elementHeight,
boxWidth: anchorBox.w,
boxHeight: anchorBox.h,
bottomInset
});
}
applyPositionAfterContentRender() {
const layout = this.getLayoutSize();
if (layout.w && layout.h) {
const anchorBox = this.computeAnchorBoxLayout(layout);
if (anchorBox.w && anchorBox.h) {
this.refreshBottomInsetNow(layout, anchorBox);
this.applySubtitlePositionWithLayout(layout, anchorBox);
return;
}
this.refreshBottomInsetNow(layout);
this.applySubtitlePosition();
return;
}
this.refreshBottomInsetNow();
this.applySubtitlePosition();
}
trimEdgeWhitespaceTokens(tokens) {
if (!tokens.length) return tokens;
let s = 0;
let e = tokens.length;
while (s < e && !tokens[s]?.text.trim()) s += 1;
while (e > s && !tokens[e - 1]?.text.trim()) e -= 1;
if (s === 0 && e === tokens.length) return tokens;
return s >= e ? [] : tokens.slice(s, e);
}
selectTokensByMaxLength(tokens, time) {
if (!tokens.length) return tokens;
let start = 0;
let length = 0;
let overflowed = false;
let chosenStart = 0;
let chosenEnd = tokens.length;
let hasChosenRange = false;
let matchedByTime = false;
const considerRange = (rangeStart, rangeEnd) => {
if (rangeEnd <= rangeStart) return;
if (!hasChosenRange) {
chosenStart = rangeStart;
chosenEnd = rangeEnd;
hasChosenRange = true;
}
if (matchedByTime) return;
const first = tokens[rangeStart];
const last = tokens[rangeEnd - 1];
if (!first || !last) return;
const endMs = (rangeEnd < tokens.length ? tokens[rangeEnd]?.startMs : void 0) ?? last.startMs + (last.durationMs ?? 0);
if (first.startMs <= time && time < endMs) {
chosenStart = rangeStart;
chosenEnd = rangeEnd;
matchedByTime = true;
}
};
for (const [index, token] of tokens.entries()) {
const nextLength = length + token.text.length;
if (nextLength > this.maxLength && index > start) {
overflowed = true;
considerRange(start, index);
start = index;
length = token.text.length;
continue;
}
length = nextLength;
}
if (!overflowed) return this.trimEdgeWhitespaceTokens(tokens);
considerRange(start, tokens.length);
return this.trimEdgeWhitespaceTokens(tokens.slice(chosenStart, chosenEnd));
}
buildTokenPrecomputeInput(tokens) {
const cached = this.tokenPrecomputeMemo;
if (cached?.tokens === tokens) return cached.value;
const { slices, key } = buildWordSlices(tokens);
const value = {
wordSlices: slices,
normalizedWordsKey: key
};
this.tokenPrecomputeMemo = {
tokens,
value
};
return value;
}
getTokenLayoutInputs(ctx) {
const block = this.subtitlesBlock;
if (block) {
const cs = getComputedStyle(block);
const fontKey = `${cs.fontStyle} ${cs.fontVariant} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
ctx.font = fontKey;
const cssMaxWidth = Number.parseFloat(cs.maxWidth);
const paddingLeft = Number.parseFloat(cs.paddingLeft) || 0;
const paddingRight = Number.parseFloat(cs.paddingRight) || 0;
const baseMaxWidth = Number.isFinite(cssMaxWidth) ? cssMaxWidth : this.subtitleMaxWidthPx || globalThis.innerWidth * .8;
if (Number.isFinite(baseMaxWidth) && baseMaxWidth > 0) this.subtitleMaxWidthPx = baseMaxWidth;
return {
fontKey,
maxWidthPx: Math.max(0, baseMaxWidth - paddingLeft - paddingRight)
};
}
const remPx = Number.parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
const baseMaxWidth = Math.min(remPx * 52, this.subtitleMaxWidthPx || globalThis.innerWidth * .8);
const fontSizePx = this.fontSizeOverridden ? this.fontSize : Math.min(24, Math.max(14, globalThis.innerWidth * .016));
const fontKey = `normal normal 500 ${fontSizePx}px ${getSubtitleFontFamilyCssValue(this.fontFamily)}`;
ctx.font = fontKey;
return {
fontKey,
maxWidthPx: Math.max(0, baseMaxWidth - fontSizePx)
};
}
getActiveLineKey(tokens) {
if (this.lastActiveLineKey !== null) return this.lastActiveLineKey;
return `${tokens[0]?.startMs ?? 0}:${tokens[0]?.durationMs ?? 0}:${tokens.length}`;
}
getLineMeasureMemo(tokens, activeLineKey) {
const { wordSlices, normalizedWordsKey } = this.buildTokenPrecomputeInput(tokens);
if (!wordSlices.length) return null;
const ctx = this.getMeasureContext();
if (!ctx) return null;
const { fontKey, maxWidthPx } = this.getTokenLayoutInputs(ctx);
if (!Number.isFinite(maxWidthPx) || maxWidthPx < 24) return null;
const key = `${activeLineKey}|${fontKey}|${Math.round(maxWidthPx)}|${normalizedWordsKey}`;
if (this.lineMeasureMemo?.key === key) return this.lineMeasureMemo;
const memo = {
key,
metrics: measureWordSlices(wordSlices, (text) => ctx.measureText(text).width),
maxWidthPx
};
this.lineMeasureMemo = memo;
return memo;
}
buildTokenProcessingMemo(tokens, activeLineKey) {
const lineMeasure = this.getLineMeasureMemo(tokens, activeLineKey);
if (!lineMeasure) return null;
const memoKey = `${lineMeasure.key}|${this.maxLength}`;
if (this.tokenProcessingMemo?.key === memoKey) return this.tokenProcessingMemo;
const safeMaxWidthPx = applyWrapWidthGuard(lineMeasure.maxWidthPx);
const memo = {
key: memoKey,
segmentRanges: computeTwoLineSegments(tokens, lineMeasure.metrics, safeMaxWidthPx, this.maxLength)
};
this.tokenProcessingMemo = memo;
this.lastSegmentIndex = 0;
return memo;
}
selectSegmentIndexFromRanges(segmentRanges, time) {
if (!segmentRanges.length) return -1;
let idx = this.lastSegmentIndex;
if (idx >= segmentRanges.length) idx = 0;
while (idx < segmentRanges.length - 1 && time >= segmentRanges[idx].endMs) idx += 1;
while (idx > 0 && time < segmentRanges[idx].startMs) idx -= 1;
if (!(time >= segmentRanges[idx].startMs && time < segmentRanges[idx].endMs)) {
const found = segmentRanges.findIndex((s) => time >= s.startMs && time < s.endMs);
if (found >= 0) idx = found;
else idx = time < segmentRanges[0].startMs ? 0 : segmentRanges.length - 1;
}
this.lastSegmentIndex = idx;
return idx;
}
processTokens(tokens, time) {
if (!tokens.length) return tokens;
const activeLineKey = this.getActiveLineKey(tokens);
const memo = this.buildTokenProcessingMemo(tokens, activeLineKey);
if (!memo) return this.selectTokensByMaxLength(tokens, time);
const { segmentRanges } = memo;
if (!segmentRanges.length) return this.trimEdgeWhitespaceTokens(tokens);
const segmentIndex = this.selectSegmentIndexFromRanges(segmentRanges, time);
if (segmentIndex < 0) return this.trimEdgeWhitespaceTokens(tokens);
const seg = segmentRanges[segmentIndex];
return this.trimEdgeWhitespaceTokens(tokens.slice(seg.startToken, seg.endToken));
}
async translateStrTokens(text) {
const fromLang = this.subtitleLang ?? "";
const toLang = localizationProvider.lang;
if (this.strTranslatedTokens) {
const translated = await translate(text, fromLang, toLang);
return [this.strTranslatedTokens, typeof translated === "string" ? translated : ""];
}
const translated = await translate([this.strTokens, text], fromLang, toLang);
const pair = Array.isArray(translated) ? translated : [translated, translated];
const context = typeof pair[0] === "string" ? pair[0] : "";
const current = typeof pair[1] === "string" ? pair[1] : "";
this.strTranslatedTokens = context;
return [context, current];
}
findTokenSpan(candidate, root) {
const token = (candidate instanceof Element ? candidate : candidate instanceof Text ? candidate.parentElement : null)?.closest("span[data-vot-token=\"1\"]");
return token instanceof HTMLSpanElement && root.contains(token) ? token : null;
}
resolveTokenSpanFromClick(event) {
const root = this.subtitlesBlock ?? this.subtitlesContainer;
if (!root) return null;
const fromTarget = this.findTokenSpan(event.target, root);
if (fromTarget) return fromTarget;
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
for (const node of path) {
const fromPath = this.findTokenSpan(node, root);
if (fromPath) return fromPath;
}
const x = event.clientX;
const y = event.clientY;
return Number.isFinite(x) && Number.isFinite(y) ? this.findTokenSpan(document.elementFromPoint(x, y), root) : null;
}
releaseTooltip() {
this.tooltipTranslationRequestId += 1;
if (this.tokenTooltip?.target) this.tokenTooltip.target.classList.remove("selected");
this.tokenTooltip?.release();
this.tokenTooltip = void 0;
destroyShadowMount(this.tooltipMount);
this.tooltipMount = void 0;
return this;
}
clearPendingSchedulerState() {
this.repositionPending = false;
this.updatePending = false;
this.wrapPending = false;
this.positionRefreshPending = false;
}
clearRenderedContent({ releaseTooltip = false } = {}) {
if (releaseTooltip) this.releaseTooltip();
this.resetRenderMemo();
this.lastActiveLineKey = null;
this.strTokens = "";
this.resetTranslationContext();
this.subtitlesBlock = null;
this.renderedHighlightEls = [];
this.resetWrapMemo();
this.lastWrapTokens = null;
this.subtitleMaxWidthPx = 0;
this.smartAnchorWidthPx = 0;
this.smartAnchorHeightPx = 0;
this.smartFontSizePx = 0;
this.smartMaxWidthPx = 0;
this.lastAppliedLeftPct = null;
this.lastAppliedTopPct = null;
this.passedStateKey = null;
this.passedThresholds.length = 0;
this.insetCacheReady = false;
this.hideSnapGuides();
this.resetSegmentationMemo();
this.clearPendingSchedulerState();
if (this.subtitlesContainer) D(null, this.subtitlesContainer);
}
onClick = async (event) => {
if (performance.now() < this.suppressTokenClicksUntil) {
event.preventDefault();
event.stopPropagation();
return;
}
const target = this.resolveTokenSpanFromClick(event);
if (!target) return;
if (this.toggleCurrentTooltipTarget(target)) return;
this.releaseTooltip();
const requestId = this.tooltipTranslationRequestId;
const text = this.normalizeTokenTextForTranslation(target.textContent ?? "");
if (!text) return;
const service = await votStorage.get("translationService", defaultTranslationService);
if (requestId !== this.tooltipTranslationRequestId) return;
target.classList.add("selected");
const subtitlesInfo = UI.createSubtitleInfo(text, this.strTranslatedTokens || this.strTokens, service);
const tooltip = this.createTokenTooltip(target, subtitlesInfo.container);
this.tokenTooltip = tooltip;
tooltip.onClick();
const strTokens = this.strTokens;
const translated = await this.translateStrTokens(text);
if (requestId !== this.tooltipTranslationRequestId) return;
if (this.shouldSkipTooltipUpdate(requestId, tooltip, target, strTokens)) return;
subtitlesInfo.header.textContent = translated[1];
subtitlesInfo.context.textContent = translated[0];
tooltip.setContent(subtitlesInfo.container);
};
toggleCurrentTooltipTarget(target) {
if (this.tokenTooltip?.target !== target || !this.tokenTooltip?.container) return false;
if (this.tokenTooltip.showed) target.classList.add("selected");
else target.classList.remove("selected");
return true;
}
createTokenTooltip(target, content) {
const tooltipMaxWidth = Math.max(this.subtitleMaxWidthPx, this.subtitlesContainer?.offsetWidth ?? 0, this.subtitlesBlock?.offsetWidth ?? 0, Math.min(globalThis.innerWidth * .6, 320));
const tooltipMount = this.ensureTooltipMount();
return new Tooltip({
target,
anchor: this.subtitlesBlock ?? target,
content,
parentElement: tooltipMount.root,
layoutRoot: tooltipMount.host,
offset: {
x: 4,
y: 12
},
maxWidth: tooltipMaxWidth,
borderRadius: 12,
bordered: false,
position: "top",
trigger: "click"
});
}
shouldSkipTooltipUpdate(requestId, tooltip, target, strTokens) {
return requestId !== this.tooltipTranslationRequestId || strTokens !== this.strTokens || this.tokenTooltip !== tooltip || tooltip.target !== target || !tooltip.showed;
}
buildPassedState(tokens, time, stateKey) {
if (this.passedStateKey !== stateKey) {
this.passedStateKey = stateKey;
this.passedThresholds.length = 0;
for (const token of tokens) {
if (!token.isWordLike) continue;
const halfway = token.startMs + token.durationMs / 2;
const earlyPassThreshold = Math.max(token.startMs - 100, halfway - 275);
this.passedThresholds.push(Math.min(halfway, earlyPassThreshold));
}
}
const flags = this.passedFlagsBuffer;
const thresholds = this.passedThresholds;
for (let i = 0; i < thresholds.length; i += 1) flags[i] = time > thresholds[i];
flags.length = thresholds.length;
return flags;
}
renderTokens(tokens) {
return buildSubtitleRenderPlan(tokens, tokens.length - 1, this.breakAfterTokenIndexSet).map((part) => this.renderPlanPart(part));
}
renderStyledSpan(text, style, isWordToken = false, highlightIndex) {
if (!style && !isWordToken && highlightIndex === void 0) return text;
return b`<span
data-vot-token=${isWordToken ? "1" : A}
data-vot-highlight-index=${highlightIndex ?? A}
data-vot-style-italic=${style?.italic ? "1" : "0"}
data-vot-style-bold=${style?.bold ? "1" : "0"}
data-vot-style-underline=${style?.underline ? "1" : "0"}
data-vot-style-color=${style?.color ? "1" : "0"}
style=${buildSubtitleInlineStyleCssText(style)}
>${text}</span
>`;
}
renderPlanPart(part) {
if (part.kind === "break") return b`<br class="vot-subtitles-br" />`;
return this.renderStyledSpan(part.text, part.style, part.kind === "word", part.highlightIndex);
}
updatePassedClasses(passedFlags) {
for (const tokenEl of this.renderedHighlightEls) {
const highlightIndex = Number.parseInt(tokenEl.dataset.votHighlightIndex ?? "", 10);
const isPassed = Number.isInteger(highlightIndex) && highlightIndex >= 0 && highlightIndex < passedFlags.length ? passedFlags[highlightIndex] : false;
tokenEl.classList.toggle("passed", isPassed);
}
}
clearPassedClasses() {
for (const tokenEl of this.renderedHighlightEls) tokenEl.classList.remove("passed");
}
setBreakAfterTokenIndices(indices) {
this.breakAfterTokenIndices = indices;
this.breakAfterTokenIndexSet = indices.length ? new Set(indices) : null;
}
scheduleWrapRecompute(tokens = null) {
if (tokens) this.lastWrapTokens = tokens;
const shouldRequestTick = !this.wrapPending;
this.wrapPending = true;
if (shouldRequestTick) this.intervalIdleChecker.requestImmediateTick();
}
maybeRefreshPosition(force = false) {
if (this.abortController.signal.aborted) return;
if (!this.subtitlesContainer) return;
const now = performance.now();
if (!force && now - this.lastPositionRefreshTs < this.positionRefreshIntervalMs) return;
this.lastPositionRefreshTs = now;
this.positionRefreshPending = true;
this.intervalIdleChecker.requestImmediateTick();
}
getMeasureContext(font) {
if (!this.measureCanvas) {
this.measureCanvas = document.createElement("canvas");
this.measureCanvas.width = 1;
this.measureCanvas.height = 1;
}
if (!this.measureCtx) this.measureCtx = this.measureCanvas.getContext("2d", { alpha: false }) ?? this.measureCanvas.getContext("2d");
if (!this.measureCtx) return null;
if (typeof font === "string" && font) this.measureCtx.font = font;
return this.measureCtx;
}
recomputeWrapNow() {
const tokens = this.lastWrapTokens;
const block = this.subtitlesBlock;
if (!tokens || !block) return;
const ctx = this.getMeasureContext();
if (!ctx) return;
const { fontKey, maxWidthPx } = this.getTokenLayoutInputs(ctx);
if (!Number.isFinite(maxWidthPx) || maxWidthPx < 50) return;
const safeMaxWidthPx = applyWrapWidthGuard(maxWidthPx);
if (safeMaxWidthPx < 50) return;
const wrapKey = `${this.getActiveLineKey(tokens)}|${fontKey}|${Math.round(safeMaxWidthPx)}|${this.stringifyTokens(tokens)}`;
if (wrapKey === this.lastWrapKey) return;
this.lastWrapKey = wrapKey;
const next = computeTokenWrapPlan(tokens, (text) => ctx.measureText(text).width, safeMaxWidthPx);
if (next.breakAfterTokenIndices.length !== this.breakAfterTokenIndices.length || next.breakAfterTokenIndices.some((value, index) => value !== this.breakAfterTokenIndices[index])) {
this.setBreakAfterTokenIndices(next.breakAfterTokenIndices);
this.resetRenderMemo();
this.update();
}
}
setContent(subtitles, lang = void 0) {
this.releaseTooltip();
this.subtitleLang = lang;
if (!subtitles || !this.video) {
this.clearRenderedContent();
this.subtitles = null;
this.maxActiveCueLookbackMs = 0;
this.lastPlaybackTimeMs = null;
this.clearPendingSchedulerState();
this.stopVideoFrameLoop();
this.detachDragDocumentListeners();
return;
}
this.createSubtitlesContainer();
this.subtitles = subtitles;
this.maxActiveCueLookbackMs = subtitles.subtitles.reduce((maxDurationMs, line) => Math.max(maxDurationMs, Math.max(0, line.durationMs)), 0);
this.lastPlaybackTimeMs = Math.max(0, this.video.currentTime * 1e3);
this.lastActiveLineKey = null;
this.syncVideoFrameLoop();
this.updateContainerRect();
this.update();
this.intervalIdleChecker.requestImmediateTick();
}
setMaxLength(len) {
if (typeof len === "number" && len > 0) {
this.maxLength = len;
this.resetSegmentationMemo();
this.update();
this.scheduleReposition();
}
}
setHighlightWords(value) {
const wasEnabled = this.highlightWords;
this.highlightWords = Boolean(value);
if (wasEnabled && !this.highlightWords) this.clearPassedClasses();
this.update();
}
setSmartLayout(enabled) {
const next = enabled !== false;
if (next === this.smartLayoutEnabled) return;
this.smartLayoutEnabled = next;
this.subtitlesContainer?.style.removeProperty("--vot-subtitles-max-width");
this.lastSmartLayoutKey = null;
this.resetWrapMemo();
this.resetRenderMemo();
this.resetSegmentationMemo();
this.applyManualFontSizeStyle();
this.update();
this.scheduleWrapRecompute();
this.scheduleReposition();
}
setFontSize(size) {
this.fontSize = size;
this.fontSizeOverridden = true;
if (!this.smartLayoutEnabled) {
this.applyManualFontSizeStyle();
this.lastWrapKey = null;
this.resetSegmentationMemo();
this.scheduleWrapRecompute();
this.scheduleReposition();
}
}
setFontFamily(fontFamily) {
this.fontFamily = fontFamily;
this.applyFontFamilyStyle();
this.lastWrapKey = null;
this.resetSegmentationMemo();
this.scheduleWrapRecompute();
this.scheduleReposition();
}
setOpacity(rate) {
const numericRate = Number(rate);
this.opacity = ((100 - (Number.isFinite(numericRate) ? clampToRange(numericRate, 0, 100) : 0)) / 100).toFixed(2);
this.applyOpacityStyle();
}
stringifyTokens(tokens) {
return tokens.map((token) => token.text).join("");
}
resolveActiveLine(time, subtitlesList) {
return buildActiveSubtitleRenderLine(time, subtitlesList, this.maxActiveCueLookbackMs);
}
clearInactiveLineState() {
this.lastActiveLineKey = null;
if (this.subtitlesBlock || this.lastRenderKey !== null || this.strTokens) {
this.clearRenderedContent({ releaseTooltip: true });
return;
}
this.releaseTooltip();
}
refreshSmartLayoutIfNeeded() {
if (!this.smartLayoutEnabled) return;
const now = performance.now();
if (this.lastSmartLayoutKey !== null && now - this.lastSmartLayoutCheckTs <= 500) return;
this.lastSmartLayoutCheckTs = now;
const layout = this.getLayoutSize();
if (!layout.w || !layout.h) return;
const anchorBox = this.computeAnchorBoxLayout(layout);
if (anchorBox.w && anchorBox.h) this.ensureSmartLayout(anchorBox);
}
getRenderState(line, activeLineKey, time) {
const tokens = this.processTokens(line.tokens, time);
this.lastWrapTokens = tokens;
const strTokens = this.stringifyTokens(tokens);
const tokensChanged = strTokens !== this.strTokens;
if (tokensChanged) {
this.releaseTooltip();
this.strTokens = strTokens;
this.resetTranslationContext();
this.resetWrapMemo();
}
const passedStateKey = `${activeLineKey}:${strTokens}`;
return {
tokens,
tokensChanged,
passedFlags: this.highlightWords ? this.buildPassedState(tokens, time, passedStateKey) : null,
renderKey: `${activeLineKey}:${strTokens}:${this.breakAfterTokenIndices.join(",")}`
};
}
syncRenderedTokens(tokens) {
this.subtitlesContainer = this.subtitlesContainer ?? this.createSubtitlesContainer();
D(b`<vot-block
class="vot-subtitles"
dir="auto"
lang=${this.subtitleLang ?? ""}
@click=${this.onClick}
>
${this.renderTokens(tokens)}
</vot-block>`, this.subtitlesContainer);
const firstChild = this.subtitlesContainer.firstElementChild;
this.subtitlesBlock = firstChild instanceof HTMLElement && firstChild.classList.contains("vot-subtitles") ? firstChild : null;
this.renderedHighlightEls = this.subtitlesBlock ? Array.from(this.subtitlesBlock.querySelectorAll("span[data-vot-highlight-index]")) : [];
}
update() {
if (!this.video || !this.subtitles) return;
const time = this.resolvePlaybackTimeMs();
const subtitlesList = this.subtitles.subtitles;
const activeLine = this.resolveActiveLine(time, subtitlesList);
if (!activeLine) {
this.clearInactiveLineState();
return;
}
this.lastActiveLineKey = activeLine.lineKey;
this.refreshSmartLayoutIfNeeded();
const { tokens, tokensChanged, passedFlags, renderKey } = this.getRenderState(activeLine.line, activeLine.lineKey, time);
if (renderKey === this.lastRenderKey) {
if (this.highlightWords && !tokensChanged && passedFlags) this.updatePassedClasses(passedFlags);
this.maybeRefreshPosition();
return;
}
this.lastRenderKey = renderKey;
this.syncRenderedTokens(tokens);
if (this.highlightWords && passedFlags) this.updatePassedClasses(passedFlags);
if (tokensChanged) {
this.applyPositionAfterContentRender();
this.scheduleWrapRecompute(tokens);
this.scheduleReposition();
} else this.maybeRefreshPosition();
}
release() {
this.detachDragDocumentListeners();
this.stopVideoFrameLoop();
this.abortController.abort();
this.resizeObserver?.disconnect();
this.clearPendingSchedulerState();
this.checkerUnsubscribe?.();
this.checkerUnsubscribe = null;
this.releaseTooltip();
if (this.subtitlesContainer) {
this.subtitlesContainer.remove();
this.subtitlesContainer = null;
}
destroyShadowMount(this.tooltipMount);
this.tooltipMount = void 0;
this.fullscreenLayerController.release();
if (this.safeAreaProbeEl) {
this.safeAreaProbeEl.remove();
this.safeAreaProbeEl = null;
}
if (this.guidesLayer) {
this.guidesLayer.remove();
this.guidesLayer = null;
this.verticalGuide = null;
this.horizontalGuide = null;
}
this.measureCtx = null;
this.measureCanvas = null;
this.lastAppliedLeftPct = null;
this.lastAppliedTopPct = null;
this.passedStateKey = null;
this.passedThresholds.length = 0;
this.insetCacheReady = false;
}
};
//#endregion
//#region src/subtitles/displayModel.ts
var HTML_TAG_RE = /^<\s*(\/?)\s*([a-z0-9]+)([^>]*)>/iu;
var ASS_OVERRIDE_RE = /^\{([^}]*)\}/u;
var LEADING_SPEAKER_MARKER_RE = /^(\s*)>>\s*/u;
var ATTACHED_TIME_WORD_RE = /(\d{1,2}:\d{2}(?::\d{2})?)(?=[\p{L}\p{M}])/gu;
var GLUED_WORD_NUMBER_RE = /([\p{L}\p{M}]+)(\d+)|(\d+)([\p{L}\p{M}]+)/gu;
var ASS_DIRECTIVE_RE = /\\[^\\]+/gu;
var ASS_STYLE_TOGGLE_RE = /^\\([ibu])([01])$/u;
var ASS_PRIMARY_COLOR_RE = /^\\(?:1?c|c)&H([0-9a-f]{6,8})&$/iu;
var ASS_STYLE_RESET_RE = /^\\r(?:[^\\}]*)?$/u;
var COMPLEX_DISPLAY_CONTROL_RE = /[<{\\]/u;
var cloneMutableInlineStyle = (style) => ({
italic: style.italic,
bold: style.bold,
underline: style.underline,
color: style.color,
classes: [...style.classes]
});
var assignMutableInlineStyle = (target, source) => {
target.italic = source.italic;
target.bold = source.bold;
target.underline = source.underline;
target.color = source.color;
target.classes = [...source.classes];
};
var resetMutableInlineStyle = (style) => {
style.italic = false;
style.bold = false;
style.underline = false;
style.color = void 0;
style.classes = [];
};
var toTokenStyle = (style) => normalizeSubtitleInlineStyle({
italic: style.italic,
bold: style.bold,
underline: style.underline,
color: style.color,
classes: style.classes
});
var pushSegment = (segments, text, style) => {
if (!text) return;
const tokenStyle = toTokenStyle(style);
const previous = segments.at(-1);
if (previous && subtitleInlineStylesEqual(previous.style, tokenStyle)) {
previous.text += text;
return;
}
segments.push({
text,
style: tokenStyle
});
};
var parseAssColorToCssHex = (value) => {
const normalized = value.trim();
if (!/^[0-9a-f]{6,8}$/iu.test(normalized)) return;
const bgr = normalized.slice(-6);
const blue = bgr.slice(0, 2);
const green = bgr.slice(2, 4);
return normalizeCssColorValue(`#${bgr.slice(4, 6)}${green}${blue}`);
};
var applyAssStyleDirective = (directive, style) => {
const toggleMatch = ASS_STYLE_TOGGLE_RE.exec(directive.trim());
if (toggleMatch) {
const enabled = toggleMatch[2] === "1";
if (toggleMatch[1] === "i") {
style.italic = enabled;
return;
}
if (toggleMatch[1] === "b") {
style.bold = enabled;
return;
}
if (toggleMatch[1] === "u") {
style.underline = enabled;
return;
}
}
if (ASS_STYLE_RESET_RE.test(directive.trim())) {
resetMutableInlineStyle(style);
return;
}
const colorMatch = ASS_PRIMARY_COLOR_RE.exec(directive.trim());
if (colorMatch) style.color = parseAssColorToCssHex(colorMatch[1]);
};
var applyAssOverrideBlock = (rawDirectives, style) => {
const directives = rawDirectives.match(ASS_DIRECTIVE_RE) ?? [];
for (const directive of directives) applyAssStyleDirective(directive, style);
};
var normalizeLeadingSpeakerMarker = (segments) => {
for (const segment of segments) {
if (!segment.text) continue;
const normalized = segment.text.replace(LEADING_SPEAKER_MARKER_RE, "$1");
segment.text = normalized;
if (normalized.length > 0) break;
}
while (segments[0]?.text === "") segments.shift();
};
var normalizeAttachedTimeExpressions = (segments) => {
for (const segment of segments) {
if (!segment.text) continue;
segment.text = segment.text.replaceAll(ATTACHED_TIME_WORD_RE, "$1 ");
}
};
var normalizeAttachedWordNumberExpressions = (segments) => {
for (const segment of segments) {
if (!segment.text) continue;
segment.text = normalizeAttachedWordNumberExpression(segment.text);
}
};
var normalizeAttachedWordNumberExpression = (text) => text.replaceAll(GLUED_WORD_NUMBER_RE, (match, leftLetters, leftDigits, rightDigits, rightLetters) => {
const letters = leftLetters ?? rightLetters ?? "";
const digits = leftDigits ?? rightDigits ?? "";
if (/^[A-Za-z]{1,3}$/u.test(letters) || letters.length === 1 && letters === letters.toLocaleUpperCase() && letters !== letters.toLocaleLowerCase()) return match;
return leftLetters ? `${letters} ${digits}` : `${digits} ${letters}`;
});
var extractHtmlTagClasses = (attrsRaw) => {
const normalized = attrsRaw.trim();
if (!normalized.startsWith(".")) return;
const classNames = normalized.split(/\s+/u, 1)[0].split(".").filter(Boolean);
return classNames.length ? classNames : void 0;
};
var extractHtmlFontColor = (attrsRaw) => {
const match = /\bcolor\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s>]+))/iu.exec(attrsRaw);
const rawColor = match?.[1] ?? match?.[2] ?? match?.[3];
return rawColor ? normalizeCssColorValue(rawColor) : void 0;
};
var popHtmlStyleFrame = (tagName, stack, activeStyle) => {
for (let i = stack.length - 1; i >= 0; i -= 1) {
if (stack[i].tagName !== tagName) continue;
const [frame] = stack.splice(i, 1);
if (!frame) return;
assignMutableInlineStyle(activeStyle, frame.previousStyle);
return;
}
if (tagName === "b") {
activeStyle.bold = false;
return;
}
if (tagName === "i") {
activeStyle.italic = false;
return;
}
if (tagName === "u") {
activeStyle.underline = false;
return;
}
if (tagName === "font") {
activeStyle.color = void 0;
return;
}
if (tagName === "c") activeStyle.classes = [];
};
var applyHtmlTagStyle = (htmlMatch, segments, activeStyle, styleStack) => {
const isClosing = htmlMatch[1] === "/";
const tagName = htmlMatch[2].toLowerCase();
const attrsRaw = htmlMatch[3] ?? "";
if (tagName === "br") {
pushSegment(segments, "\n", activeStyle);
return;
}
if (isClosing) {
popHtmlStyleFrame(tagName, styleStack, activeStyle);
return;
}
if (![
"b",
"i",
"u",
"font",
"c"
].includes(tagName)) return;
styleStack.push({
tagName,
previousStyle: cloneMutableInlineStyle(activeStyle)
});
if (tagName === "b") {
activeStyle.bold = true;
return;
}
if (tagName === "i") {
activeStyle.italic = true;
return;
}
if (tagName === "u") {
activeStyle.underline = true;
return;
}
if (tagName === "font") {
const color = extractHtmlFontColor(attrsRaw);
if (color) activeStyle.color = color;
return;
}
activeStyle.classes = extractHtmlTagClasses(attrsRaw) ?? [];
};
var consumeDisplayControlToken = (rawText, cursor, segments, activeStyle, styleStack) => {
const remainder = rawText.slice(cursor);
if (remainder.startsWith("\\N") || remainder.startsWith("\\n")) {
pushSegment(segments, "\n", activeStyle);
return cursor + 2;
}
if (remainder.startsWith("\\h")) {
pushSegment(segments, " ", activeStyle);
return cursor + 2;
}
if (remainder.startsWith("\n")) {
pushSegment(segments, "\n", activeStyle);
return cursor + 1;
}
const assMatch = ASS_OVERRIDE_RE.exec(remainder);
if (assMatch) {
applyAssOverrideBlock(assMatch[1], activeStyle);
return cursor + assMatch[0].length;
}
const htmlMatch = HTML_TAG_RE.exec(remainder);
if (!htmlMatch) return null;
applyHtmlTagStyle(htmlMatch, segments, activeStyle, styleStack);
return cursor + htmlMatch[0].length;
};
var buildStyledSpans = (segments) => {
const styledSpans = [];
let text = "";
for (const segment of segments) {
if (!segment.text) continue;
const start = text.length;
text += segment.text;
styledSpans.push({
start,
end: text.length,
style: segment.style
});
}
return {
text,
styledSpans
};
};
var trimStyledDisplayResult = (text, styledSpans) => {
const normalizedText = text.replaceAll("\xA0", " ");
const leadingTrim = /^\s*/u.exec(normalizedText)?.[0].length ?? 0;
const trailingTrim = /\s*$/u.exec(normalizedText)?.[0].length ?? 0;
const trimmedEnd = Math.max(leadingTrim, normalizedText.length - trailingTrim);
const finalText = normalizedText.slice(leadingTrim, trimmedEnd);
return {
text: finalText,
styledSpans: styledSpans.map((span) => ({
start: Math.max(0, span.start - leadingTrim),
end: Math.max(0, span.end - leadingTrim),
style: span.style
})).filter((span) => span.end > span.start && span.start < finalText.length).map((span) => ({
...span,
end: Math.min(span.end, finalText.length)
}))
};
};
var trimPlainDisplayText = (text) => {
const normalizedText = text.replaceAll("\xA0", " ");
const leadingTrim = /^\s*/u.exec(normalizedText)?.[0].length ?? 0;
const trailingTrim = /\s*$/u.exec(normalizedText)?.[0].length ?? 0;
const trimmedEnd = Math.max(leadingTrim, normalizedText.length - trailingTrim);
return normalizedText.slice(leadingTrim, trimmedEnd);
};
var buildPlainDisplayModel = (rawText) => ({
text: trimPlainDisplayText(normalizeAttachedWordNumberExpression(rawText.replace(LEADING_SPEAKER_MARKER_RE, "$1").replaceAll(ATTACHED_TIME_WORD_RE, "$1 "))),
styledSpans: []
});
var buildStyledDisplayModel = (rawText) => {
if (!rawText) return {
text: "",
styledSpans: []
};
if (!COMPLEX_DISPLAY_CONTROL_RE.test(rawText)) return buildPlainDisplayModel(rawText);
const segments = [];
const activeStyle = {
italic: false,
bold: false,
underline: false,
color: void 0,
classes: []
};
const styleStack = [];
let cursor = 0;
while (cursor < rawText.length) {
const nextCursor = consumeDisplayControlToken(rawText, cursor, segments, activeStyle, styleStack);
if (nextCursor !== null) {
cursor = nextCursor;
continue;
}
pushSegment(segments, rawText[cursor], activeStyle);
cursor += 1;
}
normalizeLeadingSpeakerMarker(segments);
normalizeAttachedTimeExpressions(segments);
normalizeAttachedWordNumberExpressions(segments);
const built = buildStyledSpans(segments);
return trimStyledDisplayResult(built.text, built.styledSpans);
};
var getStyleForRange = (styledSpans, start, end) => {
if (!styledSpans?.length || end <= start) return;
return styledSpans.find((span) => start < span.end && end > span.start && span.style)?.style;
};
//#endregion
//#region src/subtitles/standards.ts
var BOM = "";
var ASS_OVERRIDE_TAG_RE = /\{[^}]*\}/gu;
var SRT_TIMING_RE = /^\s*(?<start>\d{1,2}:\d{2}:\d{2}[,.]\d{1,3})\s*-->\s*(?<end>\d{1,2}:\d{2}:\d{2}[,.]\d{1,3})\s*$/u;
var VTT_TIMING_RE = /^(?<start>(?:\d{2}:)?\d{2}:\d{2}\.\d{3})\s+-->\s+(?<end>(?:\d{2}:)?\d{2}:\d{2}\.\d{3})(?<settings>(?:[ \t]+.+)?)$/u;
var normalizeNewlines = (value) => value.replace(BOM, "").replaceAll(/\r\n?/gu, "\n");
var padMilliseconds = (value) => value.length >= 3 ? value.slice(0, 3) : value.padEnd(3, "0");
var parseClockTime = (value, millisecondsLength) => {
const parts = value.trim().split(":");
if (parts.length < 2 || parts.length > 3) return null;
const [hoursRaw, minutesRaw, secondsRaw] = parts.length === 2 ? ["00", ...parts] : parts;
const [secondsPart, millisecondsPart = "0"] = secondsRaw.split(/[.,]/u);
if (!/^\d+$/u.test(hoursRaw) || !/^\d{2}$/u.test(minutesRaw) || !/^\d{2}$/u.test(secondsPart) || !/^\d+$/u.test(millisecondsPart)) return null;
const hours = Number(hoursRaw);
const minutes = Number(minutesRaw);
const seconds = Number(secondsPart);
const milliseconds = Number(padMilliseconds(millisecondsPart).slice(0, millisecondsLength));
if (!Number.isFinite(hours) || !Number.isFinite(minutes) || !Number.isFinite(seconds) || minutes > 59 || seconds > 59) return null;
return ((hours * 60 + minutes) * 60 + seconds) * 1e3 + milliseconds;
};
var formatClockTime = (totalMs, { delimiter, allowOptionalHours, fractionDigits }) => {
const { hours, minutes, seconds, milliseconds } = splitTimestampParts(totalMs, Math.round);
const fraction = milliseconds.toString().padStart(3, "0").slice(0, fractionDigits);
return `${allowOptionalHours && hours === 0 ? "" : `${hours.toString().padStart(2, "0")}:`}${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}${delimiter}${fraction}`;
};
var splitTimestampParts = (totalMs, normalizeMs) => {
const safeMs = Math.max(0, normalizeMs(totalMs));
return {
hours: Math.floor(safeMs / 36e5),
minutes: Math.floor(safeMs % 36e5 / 6e4),
seconds: Math.floor(safeMs % 6e4 / 1e3),
milliseconds: safeMs % 1e3
};
};
var formatAssTime = (totalMs) => {
const { hours, minutes, seconds, milliseconds } = splitTimestampParts(totalMs, Math.round);
const centiseconds = Math.floor(milliseconds / 10);
return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}.${centiseconds.toString().padStart(2, "0")}`;
};
var parseAssTime = (value) => {
const match = /^(?<hours>\d+):(?<minutes>\d{2}):(?<seconds>\d{2})\.(?<centiseconds>\d{2})$/u.exec(value.trim());
if (!match?.groups) return null;
const hours = Number(match.groups.hours);
const minutes = Number(match.groups.minutes);
const seconds = Number(match.groups.seconds);
const centiseconds = Number(match.groups.centiseconds);
if (minutes > 59 || seconds > 59 || !Number.isFinite(hours) || !Number.isFinite(centiseconds)) return null;
return ((hours * 60 + minutes) * 60 + seconds) * 1e3 + centiseconds * 10;
};
var normalizeSubtitleTextForDisplay = (value) => buildStyledDisplayModel(value).text;
var getSubtitleLineEndMs = (line) => line.startMs + Math.max(0, line.durationMs);
var getCueDurationMs = (startMs, endMs) => Math.max(0, endMs - startMs);
var toComparableSubtitleOrder = (subtitles) => subtitles.toSorted((left, right) => {
const startDiff = left.line.startMs - right.line.startMs;
if (startDiff !== 0) return startDiff;
const endDiff = getSubtitleLineEndMs(left.line) - getSubtitleLineEndMs(right.line);
if (endDiff !== 0) return endDiff;
return left.index - right.index;
}).map(({ line }) => line);
var trimEmptyBoundaryLines = (lines) => {
let start = 0;
let end = lines.length;
while (start < end && lines[start] === "") start += 1;
while (end > start && lines[end - 1] === "") end -= 1;
return lines.slice(start, end);
};
var parseCueSettings = (rawSettings) => {
const raw = rawSettings.trim();
if (!raw) return void 0;
const values = {};
for (const token of raw.split(/\s+/u)) {
const separatorIndex = token.indexOf(":");
if (separatorIndex <= 0) continue;
values[token.slice(0, separatorIndex)] = token.slice(separatorIndex + 1);
}
return {
raw,
values
};
};
var extractVttVoice = (payload) => {
const voice = (/^\s*<v(?:\.[^ >]+)?(?:\s+([^>]*?))?>/iu.exec(payload) ?? /^\s*<v\s+([^>]*?)>/iu.exec(payload))?.[1]?.trim();
return voice ? voice : void 0;
};
var isSrtCueStart = (lines, index) => {
const current = lines[index]?.trim() ?? "";
const next = lines[index + 1]?.trim() ?? "";
return SRT_TIMING_RE.test(current) || /^\d+$/u.test(current) && SRT_TIMING_RE.test(next);
};
var getSrtTimingLineIndex = (lines, cursor) => /^\d+$/u.test(lines[cursor]?.trim() ?? "") && SRT_TIMING_RE.test(lines[cursor + 1]?.trim() ?? "") ? cursor + 1 : cursor;
var parseSrtTiming = (lines, timingLineIndex) => {
const timingMatch = SRT_TIMING_RE.exec(lines[timingLineIndex]?.trim() ?? "");
if (!timingMatch?.groups) return null;
const startMs = parseClockTime(timingMatch.groups.start, 3);
const endMs = parseClockTime(timingMatch.groups.end, 3);
return startMs == null || endMs == null || endMs < startMs ? null : {
startMs,
endMs
};
};
var readSrtPayload = (lines, startCursor) => {
let cursor = startCursor;
const payloadLines = [];
while (cursor < lines.length) {
if (lines[cursor].trim() === "") {
if (isSrtCueStart(lines, cursor + 1)) break;
cursor += 1;
continue;
}
if (payloadLines.length > 0 && isSrtCueStart(lines, cursor)) break;
payloadLines.push(lines[cursor]);
cursor += 1;
}
return {
rawText: trimEmptyBoundaryLines(payloadLines).join("\n"),
nextCursor: cursor
};
};
var createStyledCueDraft = (index, { rawText, startMs, endMs, speakerId, displayModel, metadata }) => ({
index,
line: {
text: displayModel.text,
startMs,
durationMs: getCueDurationMs(startMs, endMs),
speakerId,
tokens: [],
metadata: {
rawText,
styledSpans: displayModel.styledSpans,
...metadata
}
}
});
var createEmptyVttResult = () => ({
format: "vtt",
subtitles: [],
metadata: { vtt: {
headerText: "",
blocks: []
} }
});
var isWebVttDocumentBlock = (line) => line.startsWith("NOTE") || line === "STYLE" || line === "REGION";
var readVttBlockLines = (lines, startCursor) => {
const blockLines = [];
let cursor = startCursor;
while (cursor < lines.length && lines[cursor].trim() !== "") {
blockLines.push(lines[cursor]);
cursor += 1;
}
return {
blockLines,
nextCursor: cursor
};
};
var resolveVttCueIdentity = (lines, cursor) => {
if (!VTT_TIMING_RE.test(lines[cursor] ?? "") && VTT_TIMING_RE.test(lines[cursor + 1] ?? "")) return {
cueId: lines[cursor],
timingCursor: cursor + 1
};
return {
cueId: void 0,
timingCursor: cursor
};
};
var parseVttTiming = (line) => {
const timingMatch = VTT_TIMING_RE.exec(line);
if (!timingMatch?.groups) return null;
const startMs = parseClockTime(timingMatch.groups.start, 3);
const endMs = parseClockTime(timingMatch.groups.end, 3);
if (startMs == null || endMs == null || endMs < startMs) return null;
return {
startMs,
endMs,
settingsRaw: timingMatch.groups.settings ?? ""
};
};
var readVttPayloadLines = (lines, startCursor) => {
const payloadLines = [];
let cursor = startCursor;
while (cursor < lines.length && lines[cursor].trim() !== "") {
payloadLines.push(lines[cursor]);
cursor += 1;
}
return {
payloadLines,
nextCursor: cursor
};
};
var parseAssEventFormatFields = (formatLine) => formatLine.slice(7).split(",").map((field) => field.trim());
var ensureAssEventFields = (eventFields, metadata) => eventFields.length || !metadata.eventFormat ? eventFields : parseAssEventFormatFields(metadata.eventFormat);
var buildAssEventRecord = (eventFields, value) => {
const values = splitAssFields(value, eventFields.length);
return Object.fromEntries(eventFields.map((field, fieldIndex) => [field, values[fieldIndex] ?? ""]));
};
var applyAssStyleSectionLine = (metadata, line) => {
if (line.startsWith("Format:")) {
metadata.styleFormat = line;
return;
}
if (line.startsWith("Style:")) {
metadata.styleLines.push(line);
return;
}
metadata.preEventLines.push(line);
};
var createAssCueDraft = (index, event) => {
const startMs = parseAssTime(event.Start ?? "");
const endMs = parseAssTime(event.End ?? "");
if (startMs == null || endMs == null || endMs < startMs) return null;
const rawText = event.Text ?? "";
const displayModel = buildStyledDisplayModel(rawText);
const assMetadata = {
kind: "dialogue",
layer: event.Layer ?? "0",
style: event.Style ?? "Default",
name: event.Name ?? "",
marginL: event.MarginL ?? "0",
marginR: event.MarginR ?? "0",
marginV: event.MarginV ?? "0",
effect: event.Effect ?? "",
rawText,
overrideTags: rawText.match(ASS_OVERRIDE_TAG_RE) ?? []
};
return {
index,
line: createStyledCueDraft(index, {
rawText,
startMs,
endMs,
speakerId: assMetadata.name || "0",
displayModel,
metadata: { ass: assMetadata }
}).line
};
};
var processAssEventLine = (metadata, line, index, eventFields) => {
if (line.startsWith("Format:")) return {
eventFields: parseAssEventFormatFields(line),
cue: null
};
if (!line.includes(":")) {
metadata.preEventLines.push(line);
return {
eventFields,
cue: null
};
}
const separatorIndex = line.indexOf(":");
const kind = line.slice(0, separatorIndex).trim();
const value = line.slice(separatorIndex + 1).trim();
const resolvedEventFields = ensureAssEventFields(eventFields, metadata);
const event = buildAssEventRecord(resolvedEventFields, value);
if (kind === "Comment") {
metadata.commentLines.push(line);
return {
eventFields: resolvedEventFields,
cue: null
};
}
if (kind !== "Dialogue") {
metadata.preEventLines.push(line);
return {
eventFields: resolvedEventFields,
cue: null
};
}
return {
eventFields: resolvedEventFields,
cue: createAssCueDraft(index, event)
};
};
var parseSrt = (text) => {
const lines = normalizeNewlines(text).split("\n");
const cues = [];
let cursor = 0;
let cueIndex = 0;
while (cursor < lines.length) {
while (cursor < lines.length && lines[cursor].trim() === "") cursor += 1;
if (cursor >= lines.length) break;
const timingLineIndex = getSrtTimingLineIndex(lines, cursor);
const timing = parseSrtTiming(lines, timingLineIndex);
if (!timing) {
cursor += 1;
continue;
}
const payload = readSrtPayload(lines, timingLineIndex + 1);
cursor = payload.nextCursor;
const rawText = payload.rawText;
const displayModel = buildStyledDisplayModel(rawText);
cues.push(createStyledCueDraft(cueIndex, {
rawText,
startMs: timing.startMs,
endMs: timing.endMs,
speakerId: "0",
displayModel
}));
cueIndex += 1;
}
return {
format: "srt",
subtitles: toComparableSubtitleOrder(cues)
};
};
var resolveSerializedText = (processed, line, format) => {
if (processed.format === format) return line.metadata?.rawText ?? line.text;
if (format === "ass") return line.text.replaceAll("\n", "\\N");
return line.text;
};
var serializeSrt = (processed) => processed.subtitles.map((line, index) => {
const rawText = resolveSerializedText(processed, line, "srt");
const endMs = getSubtitleLineEndMs(line);
return [
String(index + 1),
`${formatClockTime(line.startMs, {
delimiter: ",",
allowOptionalHours: false,
fractionDigits: 3
})} --> ${formatClockTime(endMs, {
delimiter: ",",
allowOptionalHours: false,
fractionDigits: 3
})}`,
rawText
].join("\n");
}).join("\n\n");
var pushWebVttBlock = (blocks, cueIndex, lines) => {
blocks.push({
cueIndex,
lines
});
};
var parseVtt = (text) => {
const lines = normalizeNewlines(text).split("\n");
const headerLine = lines[0] ?? "";
if (!headerLine.startsWith("WEBVTT")) return createEmptyVttResult();
const metadata = {
headerText: headerLine.slice(6).trim(),
blocks: []
};
const cues = [];
let cursor = 1;
while (cursor < lines.length) {
while (cursor < lines.length && lines[cursor].trim() === "") cursor += 1;
if (cursor >= lines.length) break;
if (isWebVttDocumentBlock(lines[cursor])) {
const block = readVttBlockLines(lines, cursor);
cursor = block.nextCursor;
pushWebVttBlock(metadata.blocks, cues.length, block.blockLines);
continue;
}
const identity = resolveVttCueIdentity(lines, cursor);
const timing = parseVttTiming(lines[identity.timingCursor] ?? "");
if (!timing) {
cursor += 1;
continue;
}
const payload = readVttPayloadLines(lines, identity.timingCursor + 1);
cursor = payload.nextCursor;
const payloadLines = payload.payloadLines;
const rawText = payloadLines.join("\n");
const displayModel = buildStyledDisplayModel(rawText);
const voice = extractVttVoice(rawText);
cues.push({
index: cues.length,
line: createStyledCueDraft(cues.length, {
rawText,
startMs: timing.startMs,
endMs: timing.endMs,
speakerId: voice ?? "0",
displayModel,
metadata: { vtt: {
cueId: identity.cueId,
settings: parseCueSettings(timing.settingsRaw),
voice,
rawPayload: payloadLines
} }
}).line
});
}
return {
format: "vtt",
subtitles: toComparableSubtitleOrder(cues),
metadata: { vtt: metadata }
};
};
var serializeVttTiming = (line) => {
const endMs = getSubtitleLineEndMs(line);
const settings = line.metadata?.vtt?.settings?.raw;
const settingsSuffix = settings ? ` ${settings}` : "";
return `${formatClockTime(line.startMs, {
delimiter: ".",
allowOptionalHours: true,
fractionDigits: 3
})} --> ${formatClockTime(endMs, {
delimiter: ".",
allowOptionalHours: true,
fractionDigits: 3
})}${settingsSuffix}`;
};
var serializeVtt = (processed) => {
const metadata = processed.metadata?.vtt;
const sections = [`WEBVTT${metadata?.headerText ? ` ${metadata.headerText}` : ""}`];
const blocksByIndex = /* @__PURE__ */ new Map();
for (const block of metadata?.blocks ?? []) {
const existing = blocksByIndex.get(block.cueIndex) ?? [];
existing.push(block.lines);
blocksByIndex.set(block.cueIndex, existing);
}
const emitBlocks = (cueIndex) => {
for (const lines of blocksByIndex.get(cueIndex) ?? []) sections.push(lines.join("\n"));
};
emitBlocks(0);
processed.subtitles.forEach((line, index) => {
const cueId = line.metadata?.vtt?.cueId;
const cueSections = [
...cueId ? [cueId] : [],
serializeVttTiming(line),
(processed.format === "vtt" ? line.metadata?.vtt?.rawPayload : line.text.split("\n"))?.join("\n") ?? line.text
];
sections.push(cueSections.join("\n"));
emitBlocks(index + 1);
});
return sections.filter(Boolean).join("\n\n");
};
var splitAssFields = (value, fieldCount) => {
if (fieldCount <= 1) return [value];
const fields = [];
let cursor = 0;
for (let i = 0; i < fieldCount - 1; i += 1) {
const separatorIndex = value.indexOf(",", cursor);
if (separatorIndex < 0) {
fields.push(value.slice(cursor).trim());
cursor = value.length;
break;
}
fields.push(value.slice(cursor, separatorIndex).trim());
cursor = separatorIndex + 1;
}
fields.push(value.slice(cursor).trim());
return fields;
};
var createEmptyAssMetadata = () => ({
scriptInfoLines: [],
styleFormat: "",
styleLines: [],
eventFormat: "",
preEventLines: [],
commentLines: []
});
var createDefaultAssMetadata = (title = "Exported subtitles") => ({
scriptInfoLines: [
`Title: ${title}`,
"ScriptType: v4.00+",
"WrapStyle: 0",
"ScaledBorderAndShadow: yes"
],
styleFormat: "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding",
styleLines: ["Style: Default,Arial,42,&H00FFFFFF,&H000000FF,&H00000000,&H64000000,0,0,0,0,100,100,0,0,1,2,0,2,20,20,20,1"],
eventFormat: "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
preEventLines: [],
commentLines: []
});
var parseAss = (text) => {
const lines = normalizeNewlines(text).split("\n");
const metadata = createEmptyAssMetadata();
const cues = [];
let currentSection = "";
let eventFields = [];
lines.forEach((line, index) => {
const trimmed = line.trim();
if (!trimmed) return;
const sectionMatch = /^\[(.+)\]$/u.exec(trimmed);
if (sectionMatch) {
currentSection = sectionMatch[1];
return;
}
if (currentSection === "Script Info") {
metadata.scriptInfoLines.push(line);
return;
}
if (currentSection === "V4+ Styles" || currentSection === "V4 Styles") {
applyAssStyleSectionLine(metadata, line);
return;
}
if (currentSection === "Events") {
if (line.startsWith("Format:")) metadata.eventFormat = line;
const result = processAssEventLine(metadata, line, index, eventFields);
eventFields = result.eventFields;
if (result.cue) cues.push(result.cue);
}
});
return {
format: "ass",
subtitles: toComparableSubtitleOrder(cues),
metadata: { ass: metadata }
};
};
var serializeAssDialogue = (line) => {
const ass = line.metadata?.ass;
const endMs = getSubtitleLineEndMs(line);
const rawText = ass?.rawText ?? line.metadata?.rawText ?? line.text.replaceAll("\n", "\\N");
return [
ass?.kind === "comment" ? "Comment" : "Dialogue",
": ",
[
ass?.layer ?? "0",
formatAssTime(line.startMs),
formatAssTime(endMs),
ass?.style ?? "Default",
ass?.name ?? "",
ass?.marginL ?? "0",
ass?.marginR ?? "0",
ass?.marginV ?? "0",
ass?.effect ?? "",
rawText
].join(",")
].join("");
};
var withAssTitle = (metadata, assTitle) => {
if (!assTitle) return metadata;
const titleLine = `Title: ${assTitle}`;
const scriptInfoLines = [...metadata.scriptInfoLines];
const existingIndex = scriptInfoLines.findIndex((line) => line.startsWith("Title:"));
if (existingIndex >= 0) {
if (scriptInfoLines[existingIndex] === "Title: Exported subtitles") scriptInfoLines[existingIndex] = titleLine;
} else scriptInfoLines.unshift(titleLine);
return {
...metadata,
scriptInfoLines
};
};
var serializeAss = (processed, options) => {
const sourceMetadata = processed.metadata?.ass;
const metadata = withAssTitle(sourceMetadata && sourceMetadata.scriptInfoLines.length > 0 && sourceMetadata.styleFormat && sourceMetadata.styleLines.length > 0 && sourceMetadata.eventFormat ? sourceMetadata : createDefaultAssMetadata(options?.assTitle), options?.assTitle);
return [
"[Script Info]",
...metadata.scriptInfoLines,
"",
"[V4+ Styles]",
metadata.styleFormat,
...metadata.styleLines,
...metadata.preEventLines,
"",
"[Events]",
metadata.eventFormat,
...metadata.commentLines,
...processed.subtitles.map((line) => serializeAssDialogue({
...line,
metadata: processed.format === "ass" ? line.metadata : {
...line.metadata,
rawText: resolveSerializedText(processed, line, "ass")
}
}))
].join("\n").trim();
};
var parseSubtitleText = (text, format) => {
if (format === "srt") return parseSrt(text);
if (format === "vtt") return parseVtt(text);
return parseAss(text);
};
var sortProcessedSubtitles = (processed) => ({
...processed,
subtitles: toComparableSubtitleOrder(processed.subtitles.map((line, index) => ({
index,
line
})))
});
var toSubtitlesData = (processed) => {
const subtitles = processed.subtitles.map((line) => ({
text: line.text,
startMs: line.startMs,
durationMs: line.durationMs,
speakerId: line.speakerId,
tokens: line.tokens.map((token) => ({
text: token.text,
startMs: token.startMs,
durationMs: token.durationMs
}))
}));
return {
containsTokens: subtitles.some((line) => line.tokens.length > 0),
subtitles
};
};
var serializeProcessedSubtitles = (processed, format, options) => {
if (format === "json") return toSubtitlesData(processed);
if (format === "srt") return serializeSrt(processed);
if (format === "vtt") return serializeVtt(processed);
return serializeAss(processed, options);
};
//#endregion
//#region src/utils/download.ts
function toUint32BE(value) {
return new Uint8Array([
value >>> 24 & 255,
value >>> 16 & 255,
value >>> 8 & 255,
value & 255
]);
}
function toSynchsafeInt(value) {
return new Uint8Array([
value >>> 21 & 127,
value >>> 14 & 127,
value >>> 7 & 127,
value & 127
]);
}
function addTitleId3Tag(mp3Buffer, title) {
const titleBytes = new TextEncoder().encode(title);
const frameData = new Uint8Array(titleBytes.length + 1);
frameData[0] = 3;
frameData.set(titleBytes, 1);
const frame = new Uint8Array(10 + frameData.length);
frame.set([
84,
73,
84,
50
], 0);
frame.set(toUint32BE(frameData.length), 4);
frame.set(frameData, 10);
const header = new Uint8Array(10);
header.set([
73,
68,
51,
3,
0,
0
], 0);
header.set(toSynchsafeInt(frame.length), 6);
const audioBytes = new Uint8Array(mp3Buffer);
const out = new Uint8Array(header.length + frame.length + audioBytes.length);
out.set(header, 0);
out.set(frame, header.length);
out.set(audioBytes, header.length + frame.length);
return new Blob([out], { type: "audio/mpeg" });
}
function appendChunkToOutputBuffer(out, value, loaded) {
const needed = loaded + value.byteLength;
let nextOut = out;
if (needed > nextOut.length) {
const grown = new Uint8Array(Math.max(needed, nextOut.length * 2));
grown.set(nextOut.subarray(0, loaded));
nextOut = grown;
}
nextOut.set(value, loaded);
return {
out: nextOut,
loaded: needed
};
}
function mergeChunks(chunks, loaded) {
const merged = new Uint8Array(loaded);
let offset = 0;
for (const chunk of chunks) {
merged.set(chunk, offset);
offset += chunk.byteLength;
}
return merged.buffer;
}
async function readResponseArrayBuffer(res, onProgress) {
const total = Number(res.headers.get("Content-Length") ?? 0);
if (!res.body) return res.arrayBuffer();
const reader = res.body.getReader();
let loaded = 0;
let out = total > 0 ? new Uint8Array(total) : null;
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (!value || value.byteLength === 0) continue;
if (out) {
const appended = appendChunkToOutputBuffer(out, value, loaded);
out = appended.out;
loaded = appended.loaded;
} else {
chunks.push(value);
loaded += value.byteLength;
}
if (total > 0) onProgress(clamp(Math.round(loaded / total * 100)));
}
if (out) return out.buffer.slice(0, loaded);
return mergeChunks(chunks, loaded);
}
/**
* Downloads a translation file and saves it as an MP3 file with metadata,
* tracking progress when Content-Length is available.
*/
async function downloadTranslation(res, filename, onProgress = () => {}, saveOptions = {}) {
return await downloadBlob(await buildTranslationBlob(res, filename, onProgress), `${filename}.mp3`, saveOptions);
}
async function buildTranslationBlob(res, filename, onProgress = () => {}) {
const arrayBuffer = await readResponseArrayBuffer(res, onProgress);
onProgress(100);
return addTitleId3Tag(arrayBuffer, filename);
}
//#endregion
//#region src/utils/translationVolume.ts
function normalizeMediaElementVolume(volume) {
if (!Number.isFinite(volume)) return 0;
return Math.max(0, Math.min(1, volume));
}
function normalizeGainVolume(volume) {
if (!Number.isFinite(volume)) return 0;
return Math.max(0, volume);
}
function setAudioParamInstant(param, value, context) {
const now = context?.currentTime;
if (typeof now === "number" && Number.isFinite(now)) {
try {
if (typeof param.cancelAndHoldAtTime === "function") param.cancelAndHoldAtTime(now);
else if (typeof param.cancelScheduledValues === "function") param.cancelScheduledValues(now);
} catch {}
if (typeof param.setValueAtTime === "function") {
param.setValueAtTime(value, now);
return;
}
}
param.value = value;
}
function safeSetPlayerVolume(player, volume) {
const gainNode = player.gainNode;
if (gainNode?.gain) {
setAudioParamInstant(gainNode.gain, normalizeGainVolume(volume), gainNode.context);
return;
}
player.volume = normalizeMediaElementVolume(volume);
}
function applyTranslationPlaybackVolume(player, volumePercent, fallbackVolumePercent) {
const nextVolume = typeof volumePercent === "number" && Number.isFinite(volumePercent) ? volumePercent : fallbackVolumePercent;
if (!player || typeof nextVolume !== "number" || !Number.isFinite(nextVolume)) return;
safeSetPlayerVolume(player, nextVolume / 100);
}
//#endregion
//#region src/ui/mount.ts
/**
* Compare overlay mount points by DOM identity.
*
* Mount updates should only run when one of the attachment roots actually
* changes (root/portal/tooltip layout root).
*/
function isSameOverlayMount(previous, next) {
return previous.root === next.root && previous.portalContainer === next.portalContainer && previous.subtitlesMountContainer === next.subtitlesMountContainer;
}
/**
* Runs `onChanged` only when mount targets are actually different.
* Returns the mount that should become current state.
*/
function applyOverlayMountUpdate(previous, next, onChanged) {
if (isSameOverlayMount(previous, next)) return previous;
onChanged(next);
return next;
}
//#endregion
//#region src/ui/translationCommands.ts
async function getVideoDataForTranslation(videoHandler) {
if (!videoHandler.videoData?.videoId) throw new VOTLocalizedError("VOTNoVideoIDFound");
if (shouldRefreshVideoDataBeforeTranslation(videoHandler)) videoHandler.videoData = await videoHandler.getVideoData();
if (!videoHandler.videoData?.videoId) throw new VOTLocalizedError("VOTNoVideoIDFound");
return videoHandler.videoData;
}
function shouldRefreshVideoDataBeforeTranslation(videoHandler) {
return videoHandler.site.host === "vk" && videoHandler.site.additionalData === "clips" || videoHandler.site.host === "douyin";
}
async function handleTranslationButtonCommand(deps) {
const videoHandler = deps.videoHandler;
if (!videoHandler) return;
debug.log("[handleTranslationBtnClick] click translationBtn");
if (videoHandler.hasActiveSource()) {
debug.log("[handleTranslationBtnClick] video has active source");
await videoHandler.stopTranslation();
return;
}
if (deps.currentStatus === "error" && !deps.currentLoading) deps.transformBtn("none", localizationProvider.get("translateVideo"));
if (deps.currentStatus !== "none" || deps.currentLoading) {
debug.log("[handleTranslationBtnClick] translationBtn isn't in none state");
videoHandler.actionsAbortController.abort();
await videoHandler.stopTranslation();
return;
}
try {
debug.log("[handleTranslationBtnClick] trying execute translation");
const videoData = await getVideoDataForTranslation(videoHandler);
await videoHandler.videoManager.ensureDetectedLanguageForTranslation(videoData);
debug.log("[handleTranslationBtnClick] Run translateFunc", videoData.videoId);
await videoHandler.translateFunc(videoData.videoId, videoData.isStream, videoData.detectedLanguage, videoData.responseLanguage, videoData.translationHelp);
} catch (err) {
if (isAbortError(err)) {
deps.transformBtn("none", localizationProvider.get("translateVideo"));
return;
}
console.error("[VOT]", err);
if (!(err instanceof Error)) {
deps.transformBtn("error", String(err));
return;
}
const message = err.name === "VOTLocalizedError" ? err.localizedMessage : err.message;
deps.transformBtn("error", message);
}
}
//#endregion
//#region src/utils/fullscreenHelper.ts
var FullscreenHelper = class {
container;
video;
fullscreenChangeListeners = /* @__PURE__ */ new Set();
handleFullscreenChange = () => {
this.notifyFullscreenChange();
};
nativeFullscreenListenersActive = false;
constructor({ container, video }) {
this.container = container;
this.video = video;
}
/**
* Gets the current fullscreen element with proper ShadowDOM support
*/
getFullscreenElement() {
const doc = document;
const fullscreenEl = doc.fullscreenElement ?? doc.webkitFullscreenElement;
if (!(fullscreenEl instanceof HTMLElement)) return null;
return fullscreenEl;
}
/**
* Gets comprehensive fullscreen information including ShadowDOM details
*/
getFullscreenInfo() {
const element = this.getFullscreenElement();
const isFullscreen = Boolean(element);
if (!element) return {
element: null,
shadowRoot: null,
isFullscreen: false,
belongsToCurrentVideo: false
};
return {
element,
shadowRoot: element.shadowRoot ?? null,
isFullscreen,
belongsToCurrentVideo: this.isElementBelongsToCurrentVideo(element)
};
}
/**
* Checks if the given element belongs to the current video/container
*/
isElementBelongsToCurrentVideo(element) {
return element === this.container || containsCrossShadow(element, this.container) || containsCrossShadow(this.container, element) || this.video && (element === this.video || containsCrossShadow(element, this.video) || containsCrossShadow(this.video, element));
}
/**
* Gets the appropriate root element for overlay mounting in fullscreen mode
* For Shadow DOM players (e.g., Reddit's shreddit-player), returns shadowRoot
* to ensure UI is mounted inside the shadow tree, not in the light DOM.
*/
getOverlayRoot() {
const { element, belongsToCurrentVideo, shadowRoot } = this.getFullscreenInfo();
if (!element || !belongsToCurrentVideo) return null;
return shadowRoot ?? element;
}
/**
* Gets the appropriate element for ResizeObserver to watch for size changes
* Handles both regular DOM and ShadowDOM scenarios
*/
getResizeObserverTarget() {
const { element, belongsToCurrentVideo, shadowRoot } = this.getFullscreenInfo();
if (element && belongsToCurrentVideo) return shadowRoot?.host ?? element;
return this.container;
}
/**
* Checks if the current container should be considered "big" for button positioning
* Takes into account fullscreen state and ShadowDOM
*/
isBigContainer(threshold = 550) {
const target = this.getResizeObserverTarget();
const rect = target.getBoundingClientRect();
if (rect.width > 0) return rect.width > threshold;
return target.clientWidth > threshold;
}
/**
* Adds a listener for fullscreen changes
*/
addFullscreenChangeListener(listener) {
this.fullscreenChangeListeners.add(listener);
if (this.fullscreenChangeListeners.size === 1) this.setupFullscreenListeners();
}
/**
* Removes a fullscreen change listener
*/
removeFullscreenChangeListener(listener) {
this.fullscreenChangeListeners.delete(listener);
if (this.fullscreenChangeListeners.size === 0) this.cleanupFullscreenListeners();
}
/**
* Sets up native fullscreen event listeners
*/
setupFullscreenListeners() {
if (this.nativeFullscreenListenersActive) return;
document.addEventListener("fullscreenchange", this.handleFullscreenChange);
document.addEventListener("webkitfullscreenchange", this.handleFullscreenChange);
if (this.video) {
this.video.addEventListener("webkitbeginfullscreen", this.handleFullscreenChange);
this.video.addEventListener("webkitendfullscreen", this.handleFullscreenChange);
}
this.nativeFullscreenListenersActive = true;
}
/**
* Cleans up fullscreen event listeners
*/
cleanupFullscreenListeners() {
if (!this.nativeFullscreenListenersActive) return;
document.removeEventListener("fullscreenchange", this.handleFullscreenChange);
document.removeEventListener("webkitfullscreenchange", this.handleFullscreenChange);
if (this.video) {
this.video.removeEventListener("webkitbeginfullscreen", this.handleFullscreenChange);
this.video.removeEventListener("webkitendfullscreen", this.handleFullscreenChange);
}
this.nativeFullscreenListenersActive = false;
}
/**
* Notifies all listeners about fullscreen state changes
*/
notifyFullscreenChange() {
for (const listener of this.fullscreenChangeListeners) try {
listener();
} catch (error) {
console.warn("[FullscreenHelper] Error in fullscreen change listener:", error);
}
}
/**
* Updates the container reference (useful when video container changes)
*/
updateContainer(container) {
this.container = container;
}
/**
* Updates the video reference
*/
updateVideo(video) {
const shouldRebind = this.nativeFullscreenListenersActive && this.video !== video;
if (shouldRebind) this.cleanupFullscreenListeners();
this.video = video;
if (shouldRebind && this.fullscreenChangeListeners.size > 0) this.setupFullscreenListeners();
}
/**
* Cleans up all resources
*/
destroy() {
this.cleanupFullscreenListeners();
this.fullscreenChangeListeners.clear();
}
};
//#endregion
//#region src/ui/icons.ts
var TRANSLATE_ICON_SVG = w`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path
id="vot-translate-icon"
fill-rule="evenodd"
d="M15.778 18.95L14.903 21.375C14.8364 21.5583 14.7197 21.7083 14.553 21.825C14.3864 21.9417 14.203 22 14.003 22C13.6697 22 13.3989 21.8625 13.1905 21.5875C12.9822 21.3125 12.9447 21.0083 13.078 20.675L16.878 10.625C16.9614 10.4417 17.0864 10.2917 17.253 10.175C17.4197 10.0583 17.603 10 17.803 10H18.553C18.753 10 18.9364 10.0583 19.103 10.175C19.2697 10.2917 19.3947 10.4417 19.478 10.625L23.278 20.7C23.4114 21.0167 23.378 21.3125 23.178 21.5875C22.978 21.8625 22.7114 22 22.378 22C22.1614 22 21.9739 21.9375 21.8155 21.8125C21.6572 21.6875 21.5364 21.525 21.453 21.325L20.628 18.95H15.778ZM19.978 17.2H16.378L18.228 12.25L19.978 17.2Z"
></path>
<path
d="M9 14L4.7 18.3C4.51667 18.4833 4.28333 18.575 4 18.575C3.71667 18.575 3.48333 18.4833 3.3 18.3C3.11667 18.1167 3.025 17.8833 3.025 17.6C3.025 17.3167 3.11667 17.0833 3.3 16.9L7.65 12.55C7.01667 11.85 6.4625 11.125 5.9875 10.375C5.5125 9.625 5.1 8.83333 4.75 8H6.85C7.15 8.6 7.47083 9.14167 7.8125 9.625C8.15417 10.1083 8.56667 10.6167 9.05 11.15C9.78333 10.35 10.3917 9.52917 10.875 8.6875C11.3583 7.84583 11.7667 6.95 12.1 6H2C1.71667 6 1.47917 5.90417 1.2875 5.7125C1.09583 5.52083 1 5.28333 1 5C1 4.71667 1.09583 4.47917 1.2875 4.2875C1.47917 4.09583 1.71667 4 2 4H8V3C8 2.71667 8.09583 2.47917 8.2875 2.2875C8.47917 2.09583 8.71667 2 9 2C9.28333 2 9.52083 2.09583 9.7125 2.2875C9.90417 2.47917 10 2.71667 10 3V4H16C16.2833 4 16.5208 4.09583 16.7125 4.2875C16.9042 4.47917 17 4.71667 17 5C17 5.28333 16.9042 5.52083 16.7125 5.7125C16.5208 5.90417 16.2833 6 16 6H14.1C13.75 7.18333 13.275 8.33333 12.675 9.45C12.075 10.5667 11.3333 11.6167 10.45 12.6L12.85 15.05L12.1 17.1L9 14Z"
></path>
<path
id="vot-loading-icon"
style="display:none"
d="M19.8081 16.3697L18.5842 15.6633V13.0832C18.5842 12.9285 18.5228 12.7801 18.4134 12.6707C18.304 12.5613 18.1556 12.4998 18.0009 12.4998C17.8462 12.4998 17.6978 12.5613 17.5884 12.6707C17.479 12.7801 17.4176 12.9285 17.4176 13.0832V15.9998C17.4176 16.1022 17.4445 16.2028 17.4957 16.2915C17.5469 16.3802 17.6205 16.4538 17.7092 16.505L19.2247 17.38C19.2911 17.4189 19.3645 17.4443 19.4407 17.4547C19.5169 17.4652 19.5945 17.4604 19.6688 17.4407C19.7432 17.4211 19.813 17.3869 19.8741 17.3402C19.9352 17.2934 19.9864 17.2351 20.0249 17.1684C20.0634 17.1018 20.0883 17.0282 20.0982 16.952C20.1081 16.8757 20.1028 16.7982 20.0827 16.7239C20.0625 16.6497 20.0279 16.5802 19.9808 16.5194C19.9336 16.4586 19.8749 16.4077 19.8081 16.3697ZM18.0015 10C16.8478 10 15.6603 10.359 14.7011 11C13.7418 11.641 12.9415 12.4341 12.5 13.5C12.0585 14.5659 11.8852 16.0369 12.1103 17.1684C12.3353 18.3 12.8736 19.4942 13.6894 20.31C14.5053 21.1258 15.8684 21.7749 17 22C18.1316 22.2251 19.4341 21.9415 20.5 21.5C21.5659 21.0585 22.359 20.2573 23 19.298C23.641 18.3387 24.0015 17.1537 24.0015 16C23.9998 14.4534 23.5951 13.0936 22.5015 12C21.4079 10.9064 19.5481 10.0017 18.0015 10ZM18.0009 20.6665C17.0779 20.6665 16.1757 20.3928 15.4082 19.88C14.6408 19.3672 14.0427 18.6384 13.6894 17.7857C13.3362 16.933 13.2438 15.9947 13.4239 15.0894C13.604 14.1842 14.0484 13.3527 14.7011 12.7C15.3537 12.0474 16.1852 11.6029 17.0905 11.4228C17.9957 11.2428 18.934 11.3352 19.7867 11.6884C20.6395 12.0416 21.3683 12.6397 21.8811 13.4072C22.3939 14.1746 22.6676 15.0769 22.6676 15.9998C22.666 17.237 22.1738 18.4231 21.299 19.298C20.4242 20.1728 19.2381 20.665 18.0009 20.6665Z"
></path>
</svg>`;
var PIP_ICON_SVG = w`<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path
d="M120-520q-17 0-28.5-11.5T80-560q0-17 11.5-28.5T120-600h104L80-743q-12-12-12-28.5T80-800q12-12 28.5-12t28.5 12l143 144v-104q0-17 11.5-28.5T320-800q17 0 28.5 11.5T360-760v200q0 17-11.5 28.5T320-520H120Zm40 360q-33 0-56.5-23.5T80-240v-160q0-17 11.5-28.5T120-440q17 0 28.5 11.5T160-400v160h280q17 0 28.5 11.5T480-200q0 17-11.5 28.5T440-160H160Zm680-280q-17 0-28.5-11.5T800-480v-240H480q-17 0-28.5-11.5T440-760q0-17 11.5-28.5T480-800h320q33 0 56.5 23.5T880-720v240q0 17-11.5 28.5T840-440ZM600-160q-17 0-28.5-11.5T560-200v-120q0-17 11.5-28.5T600-360h240q17 0 28.5 11.5T880-320v120q0 17-11.5 28.5T840-160H600Z"
/>
</svg>`;
var MENU_ICON = w`<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path
d="M480-160q-33 0-56.5-23.5T400-240q0-33 23.5-56.5T480-320q33 0 56.5 23.5T560-240q0 33-23.5 56.5T480-160Zm0-240q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm0-240q-33 0-56.5-23.5T400-720q0-33 23.5-56.5T480-800q33 0 56.5 23.5T560-720q0 33-23.5 56.5T480-640Z"
/>
</svg>`;
var DOWNLOAD_ICON = w`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="100%" viewBox="0 0 24 24" class="vot-loader" id="vot-loader-download">
<path class="vot-loader-main" d="M12 15.575C11.8667 15.575 11.7417 15.5542 11.625 15.5125C11.5083 15.4708 11.4 15.4 11.3 15.3L7.7 11.7C7.5 11.5 7.40417 11.2667 7.4125 11C7.42083 10.7333 7.51667 10.5 7.7 10.3C7.9 10.1 8.1375 9.99583 8.4125 9.9875C8.6875 9.97917 8.925 10.075 9.125 10.275L11 12.15V5C11 4.71667 11.0958 4.47917 11.2875 4.2875C11.4792 4.09583 11.7167 4 12 4C12.2833 4 12.5208 4.09583 12.7125 4.2875C12.9042 4.47917 13 4.71667 13 5V12.15L14.875 10.275C15.075 10.075 15.3125 9.97917 15.5875 9.9875C15.8625 9.99583 16.1 10.1 16.3 10.3C16.4833 10.5 16.5792 10.7333 16.5875 11C16.5958 11.2667 16.5 11.5 16.3 11.7L12.7 15.3C12.6 15.4 12.4917 15.4708 12.375 15.5125C12.2583 15.5542 12.1333 15.575 12 15.575ZM6 20C5.45 20 4.97917 19.8042 4.5875 19.4125C4.19583 19.0208 4 18.55 4 18V16C4 15.7167 4.09583 15.4792 4.2875 15.2875C4.47917 15.0958 4.71667 15 5 15C5.28333 15 5.52083 15.0958 5.7125 15.2875C5.90417 15.4792 6 15.7167 6 16V18H18V16C18 15.7167 18.0958 15.4792 18.2875 15.2875C18.4792 15.0958 18.7167 15 19 15C19.2833 15 19.5208 15.0958 19.7125 15.2875C19.9042 15.4792 20 15.7167 20 16V18C20 18.55 19.8042 19.0208 19.4125 19.4125C19.0208 19.8042 18.55 20 18 20H6Z"/>
<circle class="vot-loader-progress" cx="12" cy="12" r="9"></circle>
</svg>`;
var SUBTITLES_ICON = w`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="100%" viewBox="0 0 24 24">
<path d="M4 20q-.825 0-1.413-.588T2 18V6q0-.825.588-1.413T4 4h16q.825 0 1.413.588T22 6v12q0 .825-.588 1.413T20 20H4Zm2-4h8v-2H6v2Zm10 0h2v-2h-2v2ZM6 12h2v-2H6v2Zm4 0h8v-2h-8v2Z"/>
</svg>`;
var SETTINGS_ICON = w`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="100%" viewBox="0 -960 960 960">
<path d="M555-80H405q-15 0-26-10t-13-25l-12-93q-13-5-24.5-12T307-235l-87 36q-14 5-28 1t-22-17L96-344q-8-13-5-28t15-24l75-57q-1-7-1-13.5v-27q0-6.5 1-13.5l-75-57q-12-9-15-24t5-28l74-129q7-14 21.5-17.5T220-761l87 36q11-8 23-15t24-12l12-93q2-15 13-25t26-10h150q15 0 26 10t13 25l12 93q13 5 24.5 12t22.5 15l87-36q14-5 28-1t22 17l74 129q8 13 5 28t-15 24l-75 57q1 7 1 13.5v27q0 6.5-2 13.5l75 57q12 9 15 24t-5 28l-74 128q-8 13-22.5 17.5T738-199l-85-36q-11 8-23 15t-24 12l-12 93q-2 15-13 25t-26 10Zm-73-260q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm0-80q-25 0-42.5-17.5T422-480q0-25 17.5-42.5T482-540q25 0 42.5 17.5T542-480q0 25-17.5 42.5T482-420Zm-2-60Zm-40 320h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Z"/>
</svg>`;
var CHEVRON_ICON = w`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" >
<path
d="M12 14.975q-.2 0-.375-.062T11.3 14.7l-4.6-4.6q-.275-.275-.275-.7t.275-.7q.275-.275.7-.275t.7.275l3.9 3.9l3.9-3.9q.275-.275.7-.275t.7.275q.275.275.275.7t-.275.7l-4.6 4.6q-.15.15-.325.213t-.375.062Z"
/>
</svg>`;
var ARROW_RIGHT_ICON = w`<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path
d="M647-440H200q-17 0-28.5-11.5T160-480q0-17 11.5-28.5T200-520h447L451-716q-12-12-11.5-28t12.5-28q12-11 28-11.5t28 11.5l264 264q6 6 8.5 13t2.5 15q0 8-2.5 15t-8.5 13L508-188q-11 11-27.5 11T452-188q-12-12-12-28.5t12-28.5l195-195Z"
/>
</svg>`;
var CLOSE_ICON = w`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="100%" viewBox="0 -960 960 960">
<path d="M480-424 284-228q-11 11-28 11t-28-11q-11-11-11-28t11-28l196-196-196-196q-11-11-11-28t11-28q11-11 28-11t28 11l196 196 196-196q11-11 28-11t28 11q11 11 11 28t-11 28L536-480l196 196q11 11 11 28t-11 28q-11 11-28 11t-28-11L480-424Z"/>
</svg>`;
var WARNING_ICON = w`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"/><path fill="currentColor" d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2m0 2a8 8 0 1 0 0 16a8 8 0 0 0 0-16m0 11a1 1 0 1 1 0 2a1 1 0 0 1 0-2m0-9a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V7a1 1 0 0 1 1-1"/>
</g>
</svg>`;
var HELP_ICON = w`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"/><path fill="currentColor" d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2m0 2a8 8 0 1 0 0 16a8 8 0 0 0 0-16m0 12a1 1 0 1 1 0 2a1 1 0 0 1 0-2m0-9.5a3.625 3.625 0 0 1 1.348 6.99a.8.8 0 0 0-.305.201c-.044.05-.051.114-.05.18L13 14a1 1 0 0 1-1.993.117L11 14v-.25c0-1.153.93-1.845 1.604-2.116a1.626 1.626 0 1 0-2.229-1.509a1 1 0 1 1-2 0A3.625 3.625 0 0 1 12 6.5"/>
</g>
</svg>`;
var REFRESH_ICON = w`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z"/>
<path fill="currentColor" d="M20 9a1 1 0 0 1 1 1v1a8 8 0 0 1-8 8H9.414l.793.793a1 1 0 0 1-1.414 1.414l-2.496-2.496a1 1 0 0 1-.287-.567L6 17.991a1 1 0 0 1 .237-.638l.056-.06l2.5-2.5a1 1 0 0 1 1.414 1.414L9.414 17H13a6 6 0 0 0 6-6v-1a1 1 0 0 1 1-1m-4.793-6.207l2.5 2.5a1 1 0 0 1 0 1.414l-2.5 2.5a1 1 0 1 1-1.414-1.414L14.586 7H11a6 6 0 0 0-6 6v1a1 1 0 1 1-2 0v-1a8 8 0 0 1 8-8h3.586l-.793-.793a1 1 0 0 1 1.414-1.414"/>
</g>
</svg>`;
var KEY_ICON = w`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M7 15q1.25 0 2.125-.875T10 12t-.875-2.125T7 9t-2.125.875T4 12t.875 2.125T7 15m0 3q-2.5 0-4.25-1.75T1 12t1.75-4.25T7 6q2.025 0 3.538 1.15T12.65 10h8.375L23 11.975l-3.5 4L17 14l-2 2l-2-2h-.35q-.625 1.8-2.175 2.9T7 18"/>
</svg>`;
//#endregion
//#region src/ui/components/downloadButton.ts
var DownloadButton = class {
button;
loaderMain;
loaderCircle;
onClick = new EventImpl();
_progress = 0;
constructor() {
const elements = this.createElements();
this.button = elements.button;
this.loaderMain = elements.loaderMain;
this.loaderCircle = elements.loaderCircle;
this.progress = 0;
}
createElements() {
const button = UI.createIconButton(DOWNLOAD_ICON, { ariaLabel: "Download translation" });
const loaderMain = button.querySelector(".vot-loader-main");
if (!loaderMain) throw new Error("[VOT] DownloadButton loader main element not found");
const loaderCircle = button.querySelector(".vot-loader-progress");
if (!loaderCircle) throw new Error("[VOT] DownloadButton loader circle element not found");
button.addEventListener("click", () => {
this.onClick.dispatch();
});
return {
button,
loaderMain,
loaderCircle
};
}
addEventListener(_type, listener) {
this.onClick.addListener(listener);
return this;
}
removeEventListener(_type, listener) {
this.onClick.removeListener(listener);
return this;
}
get progress() {
return this._progress;
}
set progress(value) {
const normalized = clampProgress(value);
this._progress = normalized;
const circumference = this.getCircleCircumference();
this.loaderCircle.style.strokeDasharray = `${circumference}`;
const offset = circumference * (1 - normalized / 100);
this.loaderCircle.style.strokeDashoffset = `${offset}`;
this.loaderMain.style.opacity = normalized === 0 ? "1" : "0";
this.loaderCircle.style.opacity = normalized === 0 ? "0" : "1";
}
getCircleCircumference() {
const radius = this.loaderCircle.r?.baseVal?.value ?? 0;
return 2 * Math.PI * radius;
}
set hidden(isHidden) {
this.button.hidden = isHidden;
}
get hidden() {
return this.button.hidden;
}
};
function clampProgress(value) {
if (!Number.isFinite(value)) return 0;
return clampPercentInt(value < 1 ? value * 100 : value);
}
//#endregion
//#region src/ui/components/label.ts
var Label = class {
container;
icon;
text;
_labelText;
_icon;
constructor({ labelText, icon }) {
this._labelText = labelText;
this._icon = icon;
const elements = this.createElements();
this.container = elements.container;
this.icon = elements.icon;
this.text = elements.text;
}
createElements() {
const container = UI.createEl("vot-block", ["vot-label"]);
const text = UI.createEl("span", ["vot-label-text"]);
text.textContent = this._labelText;
const icon = UI.createEl("span", ["vot-label-icon"]);
if (this._icon) D(this._icon, icon);
else icon.hidden = true;
container.append(text, icon);
return {
container,
icon,
text
};
}
set hidden(isHidden) {
this.container.hidden = isHidden;
}
get hidden() {
return this.container.hidden;
}
};
//#endregion
//#region src/ui/components/dialog.ts
var Dialog = class {
container;
backdrop;
box;
contentWrapper;
headerContainer;
titleContainer;
title;
closeButton;
bodyContainer;
footerContainer;
onClose = new EventImpl();
previouslyFocused = null;
keydownListener;
adaptiveAlignObserver;
adaptiveAlignRaf = null;
handleViewportChange = () => {
this.scheduleAdaptiveVerticalAlign();
};
titleId = createDomId("vot-dialog-title");
_titleHtml;
_isTemp;
constructor({ titleHtml, isTemp = false }) {
this._titleHtml = titleHtml;
this._isTemp = isTemp;
const elements = this.createElements();
this.container = elements.container;
this.backdrop = elements.backdrop;
this.box = elements.box;
this.contentWrapper = elements.contentWrapper;
this.headerContainer = elements.headerContainer;
this.titleContainer = elements.titleContainer;
this.title = elements.title;
this.closeButton = elements.closeButton;
this.bodyContainer = elements.bodyContainer;
this.footerContainer = elements.footerContainer;
}
createElements() {
const container = UI.createEl("vot-block", ["vot-dialog-container"]);
if (this._isTemp) container.classList.add("vot-dialog-temp");
container.hidden = !this._isTemp;
setInteractiveHiddenState(container, container.hidden);
const backdrop = UI.createEl("vot-block", ["vot-dialog-backdrop"]);
const box = UI.createEl("vot-block", ["vot-dialog"]);
box.dataset.verticalAlign = "center";
box.setAttribute("role", "dialog");
box.setAttribute("aria-modal", "true");
box.tabIndex = -1;
const contentWrapper = UI.createEl("vot-block", ["vot-dialog-content-wrapper"]);
const headerContainer = UI.createEl("vot-block", ["vot-dialog-header-container"]);
const titleContainer = UI.createEl("vot-block", ["vot-dialog-title-container"]);
const title = UI.createEl("vot-block", ["vot-dialog-title"]);
title.id = this.titleId;
title.append(this._titleHtml);
titleContainer.appendChild(title);
box.setAttribute("aria-labelledby", this.titleId);
const closeButton = UI.createIconButton(CLOSE_ICON, { ariaLabel: "Close" });
closeButton.classList.add("vot-dialog-close-button");
backdrop.addEventListener("click", () => {
this.close();
});
closeButton.addEventListener("click", () => {
this.close();
});
headerContainer.append(titleContainer, closeButton);
const bodyContainer = UI.createEl("vot-block", ["vot-dialog-body-container"]);
const footerContainer = UI.createEl("vot-block", ["vot-dialog-footer-container"]);
contentWrapper.append(headerContainer, bodyContainer, footerContainer);
box.appendChild(contentWrapper);
container.append(backdrop, box);
box.addEventListener("click", (e) => {
e.stopPropagation();
});
return {
container,
backdrop,
box,
contentWrapper,
headerContainer,
titleContainer,
title,
closeButton,
bodyContainer,
footerContainer
};
}
addEventListener(_type, listener) {
this.onClose.addListener(listener);
return this;
}
removeEventListener(_type, listener) {
this.onClose.removeListener(listener);
return this;
}
open() {
this.previouslyFocused ??= getDeepActiveElement(document);
this.hidden = false;
this.attachKeydownTrap();
this.attachAdaptiveVerticalAlign();
queueMicrotask(() => this.focusFirst());
return this;
}
remove() {
this.detachAdaptiveVerticalAlign();
this.detachKeydownTrap();
this.container.remove();
this.restoreFocus();
this.onClose.dispatch();
return this;
}
close() {
if (this._isTemp) return this.remove();
this.detachAdaptiveVerticalAlign();
this.detachKeydownTrap();
this.hidden = true;
this.restoreFocus();
this.onClose.dispatch();
return this;
}
attachAdaptiveVerticalAlign() {
if (this.adaptiveAlignObserver) {
this.scheduleAdaptiveVerticalAlign();
return;
}
if (typeof ResizeObserver !== "undefined") {
this.adaptiveAlignObserver = new ResizeObserver(() => {
this.scheduleAdaptiveVerticalAlign();
});
this.adaptiveAlignObserver.observe(this.contentWrapper);
}
globalThis.addEventListener("resize", this.handleViewportChange, { passive: true });
if (globalThis.visualViewport) {
globalThis.visualViewport.addEventListener("resize", this.handleViewportChange, { passive: true });
globalThis.visualViewport.addEventListener("scroll", this.handleViewportChange, { passive: true });
}
this.scheduleAdaptiveVerticalAlign();
}
detachAdaptiveVerticalAlign() {
if (this.adaptiveAlignObserver) {
this.adaptiveAlignObserver.disconnect();
this.adaptiveAlignObserver = void 0;
}
globalThis.removeEventListener("resize", this.handleViewportChange);
globalThis.visualViewport?.removeEventListener("resize", this.handleViewportChange);
globalThis.visualViewport?.removeEventListener("scroll", this.handleViewportChange);
if (this.adaptiveAlignRaf !== null) {
cancelAnimationFrame(this.adaptiveAlignRaf);
this.adaptiveAlignRaf = null;
}
}
scheduleAdaptiveVerticalAlign() {
if (this.adaptiveAlignRaf !== null) cancelAnimationFrame(this.adaptiveAlignRaf);
this.adaptiveAlignRaf = requestAnimationFrame(() => {
this.adaptiveAlignRaf = null;
this.updateAdaptiveVerticalAlign();
});
}
updateAdaptiveVerticalAlign() {
const viewportHeight = globalThis.visualViewport?.height ?? globalThis.innerHeight;
if (!viewportHeight || viewportHeight <= 0) return;
const marginPx = 16;
const centerMaxPx = Math.max(160, Math.round(viewportHeight * .75));
const topMaxPx = Math.max(160, Math.round(viewportHeight - marginPx * 2));
const contentHeightPx = this.contentWrapper.scrollHeight;
const currentlyTop = this.box.dataset.verticalAlign === "top";
const enterTopThresholdPx = centerMaxPx - 8;
const exitTopThresholdPx = Math.round(viewportHeight * .6);
if (currentlyTop ? contentHeightPx > exitTopThresholdPx : contentHeightPx >= enterTopThresholdPx) {
this.box.dataset.verticalAlign = "top";
this.box.style.setProperty("--vot-dialog-max-height", `${topMaxPx}px`);
} else {
this.box.dataset.verticalAlign = "center";
this.box.style.setProperty("--vot-dialog-max-height", `${centerMaxPx}px`);
}
}
restoreFocus() {
const el = this.previouslyFocused;
this.previouslyFocused = null;
if (el && el instanceof HTMLElement && document.contains(el)) el.focus();
}
getFocusableElements() {
return Array.from(this.container.querySelectorAll([
"button:not([disabled])",
"[href]",
"input:not([disabled])",
"select:not([disabled])",
"textarea:not([disabled])",
"[tabindex]:not([tabindex='-1'])",
"[role='button']:not([aria-disabled='true'])"
].join(","))).filter((el) => !el.hidden && el.getClientRects().length > 0);
}
focusFirst() {
(this.getFocusableElements()[0] ?? this.closeButton ?? this.box).focus?.();
}
attachKeydownTrap() {
if (this.keydownListener) return;
this.keydownListener = (e) => {
if (e.key === "Escape") {
e.preventDefault();
this.close();
return;
}
if (e.key !== "Tab") return;
const focusables = this.getFocusableElements();
if (!focusables.length) {
e.preventDefault();
this.box.focus();
return;
}
const first = focusables[0];
const last = focusables.at(-1) ?? first;
const active = getDeepActiveElement(this.container.getRootNode());
if (e.shiftKey) {
if (active === first || active === this.box) {
e.preventDefault();
last.focus();
}
} else if (active === last) {
e.preventDefault();
first.focus();
}
};
this.container.addEventListener("keydown", this.keydownListener);
}
detachKeydownTrap() {
if (!this.keydownListener) return;
this.container.removeEventListener("keydown", this.keydownListener);
this.keydownListener = void 0;
}
set hidden(isHidden) {
setInteractiveHiddenState(this.container, isHidden);
}
get hidden() {
return this.container.hidden;
}
get isDialogOpen() {
return !this.container.hidden;
}
};
//#endregion
//#region src/ui/components/textfield.ts
var Textfield = class {
container;
input;
label;
onInput = new EventImpl();
onChange = new EventImpl();
events = {
input: this.onInput,
change: this.onChange
};
_labelHtml;
_multiline;
_placeholder;
_value;
constructor({ labelHtml = "", placeholder = "", value = "", multiline = false }) {
this._labelHtml = labelHtml;
this._multiline = multiline;
this._placeholder = placeholder;
this._value = value;
const elements = this.createElements();
this.container = elements.container;
this.input = elements.input;
this.label = elements.label;
}
createElements() {
const container = UI.createEl("vot-block", ["vot-textfield"]);
const input = document.createElement(this._multiline ? "textarea" : "input");
if (!this._labelHtml) input.classList.add("vot-show-placeholer", "vot-show-placeholder");
input.placeholder = this._placeholder;
input.value = this._value;
const label = UI.createEl("span");
label.append(this._labelHtml);
container.append(input, label);
input.addEventListener("input", () => {
this._value = this.input.value;
this.onInput.dispatch(this._value);
});
input.addEventListener("change", () => {
this._value = this.input.value;
this.onChange.dispatch(this._value);
});
return {
container,
label,
input
};
}
addEventListener(type, listener) {
this.events[type].addListener(listener);
return this;
}
removeEventListener(type, listener) {
this.events[type].removeListener(listener);
return this;
}
get value() {
return this._value;
}
/**
* If you set a different new value, it will trigger the change event
*/
set value(val) {
if (this._value === val) return;
this.input.value = this._value = val;
this.onChange.dispatch(this._value);
}
get placeholder() {
return this._placeholder;
}
set placeholder(text) {
this.input.placeholder = this._placeholder = text;
}
get disabled() {
return this.input.disabled;
}
set disabled(isDisabled) {
this.input.disabled = isDisabled;
}
set hidden(isHidden) {
this.container.hidden = isHidden;
}
get hidden() {
return this.container.hidden;
}
};
//#endregion
//#region src/ui/components/select.ts
var Select = class {
container;
outer;
arrowIcon;
title;
dialogParent;
labelElement;
_selectTitle;
_dialogTitle;
multiSelect;
baseItems;
_items;
searchItemsProvider;
isLoading = false;
isDialogOpen = false;
searchRequestId = 0;
onSelectItem = new EventImpl();
onBeforeOpen = new EventImpl();
events = {
selectItem: this.onSelectItem,
beforeOpen: this.onBeforeOpen
};
contentList;
contentItemSearchDatasetKey = "votSearchLabel";
contentItemIndexDatasetKey = "votIndex";
selectedItems = [];
selectedValues;
constructor({ selectTitle, dialogTitle, items, searchItemsProvider, labelElement, dialogParent = document.documentElement, multiSelect }) {
this._selectTitle = selectTitle;
this._dialogTitle = dialogTitle;
this.baseItems = this.cloneItems(items);
this._items = this.cloneItems(items);
this.searchItemsProvider = searchItemsProvider;
this.multiSelect = multiSelect ?? false;
this.labelElement = labelElement;
this.dialogParent = dialogParent;
this.selectedValues = this.calcSelectedValues();
const elements = this.createElements();
this.container = elements.container;
this.outer = elements.outer;
this.arrowIcon = elements.arrowIcon;
this.title = elements.title;
}
cloneItems(items) {
return items.map((item) => ({ ...item }));
}
static genLanguageItems(langs, conditionString) {
return langs.map((lang) => {
const phrase = `langs.${lang}`;
const label = localizationProvider.get(phrase);
return {
label: label === phrase ? lang.toUpperCase() : label,
value: lang,
selected: conditionString === lang
};
});
}
multiSelectItemHandle = (item) => {
const value = item.value;
if (this.selectedValues.has(value) && this.selectedValues.size > 1) this.selectedValues.delete(value);
else this.selectedValues.add(value);
this.syncItemsSelectionState();
this.syncItemsSelectionState(this.baseItems);
this.updateSelectedState();
this.onSelectItem.dispatch(Array.from(this.selectedValues));
};
singleSelectItemHandle = (item) => {
const value = item.value;
this.selectedValues = new Set([value]);
this.syncItemsSelectionState();
this.syncItemsSelectionState(this.baseItems);
this.updateSelectedState();
this.onSelectItem.dispatch(value);
};
onContentItemClick = (event) => {
if (!(event.target instanceof HTMLElement)) return;
const contentItem = event.target.closest(".vot-select-content-item");
if (!contentItem || contentItem.inert || !this.contentList?.contains(contentItem)) return;
const rawIndex = contentItem.dataset[this.contentItemIndexDatasetKey];
if (!rawIndex) return;
const item = this._items[Number(rawIndex)];
if (!item) return;
if (this.multiSelect) {
this.multiSelectItemHandle(item);
return;
}
this.singleSelectItemHandle(item);
};
syncItemsSelectionState(items = this._items) {
for (const item of items) item.selected = this.selectedValues.has(item.value);
}
restoreBaseItems() {
this._items = this.cloneItems(this.baseItems);
this.syncItemsSelectionState();
this.updateSelectedState();
}
createDialogContentList() {
const contentList = UI.createEl("vot-block", ["vot-select-content-list"]);
for (const [index, item] of this._items.entries()) {
const contentItem = UI.createEl("vot-block", ["vot-select-content-item"]);
contentItem.textContent = item.label;
contentItem.dataset.votSelected = item.selected === true ? "true" : "false";
contentItem.dataset.votValue = item.value;
contentItem.dataset[this.contentItemSearchDatasetKey] = item.label.toLowerCase();
contentItem.dataset[this.contentItemIndexDatasetKey] = String(index);
if (item.disabled) contentItem.inert = true;
contentList.appendChild(contentItem);
}
contentList.addEventListener("click", this.onContentItemClick);
this.selectedItems = Array.from(contentList.children);
return contentList;
}
createElements() {
const container = UI.createEl("vot-block", ["vot-select"]);
if (this.labelElement) {
container.classList.add("vot-select--labeled");
container.append(this.labelElement);
} else container.classList.add("vot-select--control-only");
const outer = UI.createEl("vot-block", ["vot-select-outer"]);
UI.makeButtonLike(outer);
outer.setAttribute("aria-haspopup", "dialog");
outer.setAttribute("aria-expanded", "false");
const title = UI.createEl("vot-block", ["vot-select-title"]);
title.textContent = this.visibleText;
const arrowIcon = UI.createEl("vot-block", ["vot-select-arrow-icon"]);
D(CHEVRON_ICON, arrowIcon);
outer.append(title, arrowIcon);
outer.addEventListener("click", () => {
if (this.disabled) return;
if (this.isLoading || this.isDialogOpen) return;
try {
this.isLoading = true;
const tempDialog = new Dialog({
titleHtml: this._dialogTitle,
isTemp: true
});
this.onBeforeOpen.dispatch(tempDialog);
this.dialogParent.appendChild(tempDialog.container);
this.isDialogOpen = true;
outer.setAttribute("aria-expanded", "true");
const votSearchLangTextfield = new Textfield({ labelHtml: localizationProvider.get("searchField") });
votSearchLangTextfield.addEventListener("input", async (searchText) => {
const requestId = ++this.searchRequestId;
if (this.searchItemsProvider) {
const providedItems = await this.searchItemsProvider(searchText);
if (requestId !== this.searchRequestId) return;
this.updateItems(providedItems, { persist: false });
}
const normalizedSearchText = searchText.toLowerCase();
for (const contentItem of this.selectedItems) contentItem.hidden = !(contentItem.dataset[this.contentItemSearchDatasetKey] ?? "").includes(normalizedSearchText);
});
this.contentList = this.createDialogContentList();
tempDialog.bodyContainer.append(votSearchLangTextfield.container, this.contentList);
tempDialog.addEventListener("close", () => {
this.isDialogOpen = false;
this.restoreBaseItems();
this.selectedItems = [];
this.contentList = void 0;
outer.setAttribute("aria-expanded", "false");
});
tempDialog.open();
} finally {
this.isLoading = false;
}
});
container.appendChild(outer);
return {
container,
outer,
arrowIcon,
title
};
}
calcSelectedValues() {
return new Set(this._items.filter((item) => item.selected).map((item) => item.value));
}
addEventListener(type, listener) {
this.events[type].addListener(listener);
return this;
}
removeEventListener(type, listener) {
this.events[type].removeListener(listener);
return this;
}
updateTitle() {
this.title.textContent = this.visibleText;
return this;
}
updateSelectedState() {
if (this.selectedItems.length > 0) for (const item of this.selectedItems) {
const val = item.dataset.votValue;
if (val === void 0) continue;
item.dataset.votSelected = this.selectedValues.has(val).toString();
}
this.updateTitle();
return this;
}
setSelectedValue(value) {
const values = Array.isArray(value) ? value : [value];
let selectedValues;
if (this.multiSelect) selectedValues = values;
else selectedValues = values.length > 0 ? [values[0]] : [];
this.selectedValues = new Set(selectedValues);
this.syncItemsSelectionState();
this.syncItemsSelectionState(this.baseItems);
this.updateSelectedState();
return this;
}
/**
* @warning Use chaining with this method or reassign to variable to get the updated type of instance
*/
updateItems(newItems, options = {}) {
const { persist = true } = options;
const nextItems = this.cloneItems(newItems);
if (persist) this.baseItems = this.cloneItems(nextItems);
this._items = nextItems;
this.selectedValues = this.calcSelectedValues();
this.updateSelectedState();
const dialogContainer = this.contentList?.parentElement;
if (!this.contentList || !dialogContainer) return this;
const oldContentList = this.contentList;
this.contentList = this.createDialogContentList();
dialogContainer.replaceChild(this.contentList, oldContentList);
return this;
}
get visibleText() {
if (!this.multiSelect) return this._items.find((item) => item.selected)?.label ?? this._selectTitle;
return this._items.filter((item) => this.selectedValues.has(item.value)).map((item) => item.label).join(", ") || this._selectTitle;
}
set selectTitle(title) {
this._selectTitle = title;
this.updateTitle();
}
set hidden(isHidden) {
this.container.hidden = isHidden;
}
get hidden() {
return this.container.hidden;
}
get disabled() {
return this.outer.getAttribute("disabled") === "true" || this.outer.getAttribute("aria-disabled") === "true";
}
set disabled(isDisabled) {
if (isDisabled) this.outer.setAttribute("disabled", "true");
else this.outer.removeAttribute("disabled");
}
};
//#endregion
//#region src/ui/components/languagePairSelect.ts
var LanguagePairSelect = class {
container;
fromSelect;
directionIcon;
toSelect;
dialogParent;
_fromSelectTitle;
_fromDialogTitle;
_fromItems;
_toSelectTitle;
_toDialogTitle;
_toItems;
constructor({ from: { selectTitle: fromSelectTitle = localizationProvider.get("videoLanguage"), dialogTitle: fromDialogTitle = localizationProvider.get("videoLanguage"), items: fromItems }, to: { selectTitle: toSelectTitle = localizationProvider.get("translationLanguage"), dialogTitle: toDialogTitle = localizationProvider.get("translationLanguage"), items: toItems }, dialogParent = document.documentElement }) {
this._fromSelectTitle = fromSelectTitle;
this._fromDialogTitle = fromDialogTitle;
this._fromItems = fromItems;
this._toSelectTitle = toSelectTitle;
this._toDialogTitle = toDialogTitle;
this._toItems = toItems;
this.dialogParent = dialogParent;
const elements = this.createElements();
this.container = elements.container;
this.fromSelect = elements.fromSelect;
this.directionIcon = elements.directionIcon;
this.toSelect = elements.toSelect;
}
createElements() {
const container = UI.createEl("vot-block", ["vot-lang-select"]);
const fromSelect = new Select({
selectTitle: this._fromSelectTitle,
dialogTitle: this._fromDialogTitle,
items: this._fromItems,
dialogParent: this.dialogParent
});
const directionIcon = UI.createEl("vot-block", ["vot-lang-select-icon"]);
D(ARROW_RIGHT_ICON, directionIcon);
const toSelect = new Select({
selectTitle: this._toSelectTitle,
dialogTitle: this._toDialogTitle,
items: this._toItems,
dialogParent: this.dialogParent
});
container.append(fromSelect.container, directionIcon, toSelect.container);
return {
container,
fromSelect,
directionIcon,
toSelect
};
}
setSelectedValues(from, to) {
this.fromSelect.setSelectedValue(from);
this.toSelect.setSelectedValue(to);
return this;
}
updateItems(fromItems, toItems) {
this._fromItems = fromItems;
this._toItems = toItems;
this.fromSelect = this.fromSelect.updateItems(fromItems);
this.toSelect = this.toSelect.updateItems(toItems);
return this;
}
};
//#endregion
//#region src/ui/components/slider.ts
var Slider = class {
container;
input;
label;
onInput = new EventImpl();
_labelHtml;
_value;
_min;
_max;
_step;
constructor({ labelHtml, value = 50, min = 0, max = 100, step = 1 }) {
this._labelHtml = labelHtml;
this._value = value;
this._min = min;
this._max = max;
this._step = step;
const elements = this.createElements();
this.container = elements.container;
this.input = elements.input;
this.label = elements.label;
this.update();
}
updateProgress() {
const range = this._max - this._min;
const raw = range <= 0 ? 0 : (this._value - this._min) / range;
const progress = Math.max(0, Math.min(1, raw));
this.container.style.setProperty("--vot-progress", progress.toString());
return this;
}
update() {
this._value = this.input.valueAsNumber;
this._min = +this.input.min;
this._max = +this.input.max;
this.updateProgress();
return this;
}
createElements() {
const container = UI.createEl("vot-block", ["vot-slider"]);
const input = document.createElement("input");
input.type = "range";
input.min = this._min.toString();
input.max = this._max.toString();
input.step = this._step.toString();
input.value = this._value.toString();
const label = UI.createEl("span");
D(this._labelHtml, label);
container.append(input, label);
input.addEventListener("input", () => {
this.update();
this.onInput.dispatch(this._value, false);
});
return {
container,
label,
input
};
}
addEventListener(_type, listener) {
this.onInput.addListener(listener);
return this;
}
removeEventListener(_type, listener) {
this.onInput.removeListener(listener);
return this;
}
get value() {
return this._value;
}
/**
* If you set a different new value, it will trigger the input event
*/
set value(val) {
this._value = clampNumber$1(val, this._min, this._max);
this.input.value = this._value.toString();
this.updateProgress();
this.onInput.dispatch(this._value, true);
}
get min() {
return this._min;
}
set min(val) {
this._min = val;
this.input.min = this._min.toString();
this._value = clampNumber$1(this._value, this._min, this._max);
this.input.value = this._value.toString();
this.updateProgress();
}
get max() {
return this._max;
}
set max(val) {
this._max = val;
this.input.max = this._max.toString();
this._value = clampNumber$1(this._value, this._min, this._max);
this.input.value = this._value.toString();
this.updateProgress();
}
get step() {
return this._step;
}
set step(val) {
this._step = val;
this.input.step = this._step.toString();
}
get disabled() {
return this.input.disabled;
}
set disabled(isDisabled) {
this.input.disabled = isDisabled;
}
set hidden(isHidden) {
this.container.hidden = isHidden;
}
get hidden() {
return this.container.hidden;
}
};
//#endregion
//#region src/ui/components/sliderLabel.ts
var SliderLabel = class {
container;
strong;
text;
_labelText;
_labelEOL;
_value;
_symbol;
constructor({ labelText, labelEOL = "", value = 50, symbol = "%" }) {
this._labelText = labelText;
this._labelEOL = labelEOL;
this._value = value;
this._symbol = symbol;
const elements = this.createElements();
this.container = elements.container;
this.strong = elements.strong;
this.text = elements.text;
}
createElements() {
const container = UI.createEl("vot-block", ["vot-slider-label"]);
const text = UI.createEl("span", ["vot-slider-label-text"]);
text.textContent = this.labelText;
const strong = UI.createEl("span", ["vot-slider-label-value"]);
strong.textContent = this.valueText;
container.append(text, strong);
return {
container,
strong,
text
};
}
get labelText() {
return `${this._labelText}${this._labelEOL}`;
}
get valueText() {
return `${this._value}${this._symbol}`;
}
get value() {
return this._value;
}
set value(val) {
this._value = val;
this.strong.textContent = this.valueText;
}
set hidden(isHidden) {
this.container.hidden = isHidden;
}
get hidden() {
return this.container.hidden;
}
};
//#endregion
//#region src/ui/components/votButton.ts
var VOTButton = class {
container;
translateButton;
separator;
pipButton;
separator2;
menuButton;
label;
_opacity = 1;
_position;
_direction;
_status;
/** Text shown next to the translate icon (plain text, not HTML). */
_labelText;
constructor({ position = "default", direction = "default", status = "none", labelHtml = "" }) {
this._position = position;
this._direction = direction;
this._status = status;
this._labelText = labelHtml;
const elements = this.createElements();
this.container = elements.container;
this.translateButton = elements.translateButton;
this.separator = elements.separator;
this.pipButton = elements.pipButton;
this.separator2 = elements.separator2;
this.menuButton = elements.menuButton;
this.label = elements.label;
}
static calcPosition(percentX, isBigContainer) {
if (!isBigContainer) return "default";
if (percentX <= 44) return "left";
if (percentX >= 66) return "right";
return "default";
}
static calcDirection(position) {
return ["default", "top"].includes(position) ? "row" : "column";
}
createElements() {
const container = UI.createEl("vot-block", ["vot-segmented-button"]);
container.dataset.position = this._position;
container.dataset.direction = this._direction;
container.dataset.status = this._status;
const translateButton = UI.createEl("vot-block", ["vot-segment", "vot-translate-button"]);
translateButton.setAttribute("role", "button");
translateButton.tabIndex = 0;
translateButton.setAttribute("aria-label", this._labelText || "Translate");
D(TRANSLATE_ICON_SVG, translateButton);
const label = UI.createEl("span", ["vot-segment-label"]);
label.textContent = this._labelText;
translateButton.appendChild(label);
const separator = UI.createEl("vot-block", ["vot-separator"]);
const pipButton = UI.createEl("vot-block", ["vot-segment-only-icon"]);
pipButton.setAttribute("role", "button");
pipButton.tabIndex = 0;
pipButton.setAttribute("aria-label", "Picture in picture");
D(PIP_ICON_SVG, pipButton);
const separator2 = UI.createEl("vot-block", ["vot-separator"]);
const menuButton = UI.createEl("vot-block", ["vot-segment-only-icon"]);
menuButton.setAttribute("role", "button");
menuButton.tabIndex = 0;
menuButton.setAttribute("aria-label", "Menu");
menuButton.setAttribute("aria-haspopup", "dialog");
menuButton.setAttribute("aria-expanded", "false");
D(MENU_ICON, menuButton);
container.append(translateButton, separator, pipButton, separator2, menuButton);
return {
container,
translateButton,
separator,
pipButton,
separator2,
menuButton,
label
};
}
showPiPButton(visible) {
this.separator2.hidden = this.pipButton.hidden = !visible;
return this;
}
setText(labelText) {
this._labelText = labelText;
this.label.textContent = labelText;
this.translateButton.setAttribute("aria-label", labelText || "Translate");
return this;
}
remove() {
this.container.remove();
return this;
}
get tooltipPos() {
switch (this.position) {
case "left": return "right";
case "right": return "left";
default: return "bottom";
}
}
set status(status) {
this._status = this.container.dataset.status = status;
}
get status() {
return this._status;
}
set loading(isLoading) {
this.container.dataset.loading = isLoading.toString();
}
get loading() {
return this.container.dataset.loading === "true";
}
set hidden(isHidden) {
this.container.hidden = isHidden;
}
get hidden() {
return this.container.hidden;
}
get position() {
return this._position;
}
set position(position) {
this._position = this.container.dataset.position = position;
}
get direction() {
return this._direction;
}
set direction(direction) {
this._direction = this.container.dataset.direction = direction;
}
set opacity(opacity) {
const o = Number.isFinite(opacity) ? opacity : 1;
this._opacity = o;
const isHidden = o <= .01;
this.container.classList.toggle("vot-segmented-button--hidden", isHidden);
}
get opacity() {
return this._opacity;
}
};
//#endregion
//#region src/ui/components/votMenu.ts
var VOTMenu = class {
container;
contentWrapper;
headerContainer;
bodyContainer;
footerContainer;
titleContainer;
title;
_position;
_titleHtml;
menuId = createDomId("vot-menu");
titleId = createDomId("vot-menu-title");
constructor({ position = "default", titleHtml = "" }) {
this._position = position;
this._titleHtml = titleHtml;
const elements = this.createElements();
this.container = elements.container;
this.contentWrapper = elements.contentWrapper;
this.headerContainer = elements.headerContainer;
this.bodyContainer = elements.bodyContainer;
this.footerContainer = elements.footerContainer;
this.titleContainer = elements.titleContainer;
this.title = elements.title;
}
createElements() {
const container = UI.createEl("vot-block", ["vot-menu"]);
container.hidden = true;
container.id = this.menuId;
container.dataset.position = this._position;
container.setAttribute("role", "dialog");
container.setAttribute("aria-modal", "false");
setInteractiveHiddenState(container, true);
const contentWrapper = UI.createEl("vot-block", ["vot-menu-content-wrapper"]);
container.appendChild(contentWrapper);
const headerContainer = UI.createEl("vot-block", ["vot-menu-header-container"]);
const titleContainer = UI.createEl("vot-block", ["vot-menu-title-container"]);
headerContainer.appendChild(titleContainer);
const title = UI.createEl("vot-block", ["vot-menu-title"]);
title.id = this.titleId;
title.append(this._titleHtml);
titleContainer.appendChild(title);
container.setAttribute("aria-labelledby", this.titleId);
const bodyContainer = UI.createEl("vot-block", ["vot-menu-body-container"]);
const footerContainer = UI.createEl("vot-block", ["vot-menu-footer-container"]);
contentWrapper.append(headerContainer, bodyContainer, footerContainer);
return {
container,
contentWrapper,
headerContainer,
bodyContainer,
footerContainer,
titleContainer,
title
};
}
setText(titleText) {
this._titleHtml = this.title.textContent = titleText;
return this;
}
remove() {
this.container.remove();
return this;
}
set hidden(isHidden) {
setInteractiveHiddenState(this.container, isHidden);
}
get hidden() {
return this.container.hidden;
}
get position() {
return this._position;
}
set position(position) {
this._position = this.container.dataset.position = position;
}
};
//#endregion
//#region src/ui/views/overlay.ts
var OverlayView = class OverlayView {
static BIG_CONTAINER_WIDTH_PX = 550;
resizeObserver;
lastIsBigContainer = false;
fullscreenHelper;
mount;
globalPortal;
abortController = null;
defaultVolumePersistTimer;
defaultVolumePersistDelayMs = 250;
dragging = false;
dragCandidate = false;
dragDirty = false;
dragStartX = 0;
dragStartY = 0;
currentClientX = 0;
activePointerId = null;
dragThresholdPx = 6;
containerRect = null;
dragIsBigContainer = null;
checkerUnsubscribe = null;
initialized = false;
data;
videoHandler;
intervalIdleChecker;
overlayMount;
events = {
"click:settings": new EventImpl(),
"click:pip": new EventImpl(),
"click:downloadTranslation": new EventImpl(),
"click:downloadSubtitles": new EventImpl(),
"click:translate": new EventImpl(),
"input:videoVolume": new EventImpl(),
"input:translationVolume": new EventImpl(),
"select:fromLanguage": new EventImpl(),
"select:toLanguage": new EventImpl(),
"select:subtitles": new EventImpl()
};
votButton;
votButtonTooltip;
votMenu;
downloadTranslationButton;
downloadSubtitlesButton;
openSettingsButton;
languagePairSelect;
subtitlesSelectLabel;
subtitlesSelect;
videoVolumeSliderLabel;
videoVolumeSlider;
translationVolumeSliderLabel;
translationVolumeSlider;
constructor({ mount, globalPortal, data = {}, videoHandler, intervalIdleChecker }) {
this.mount = mount;
this.globalPortal = globalPortal;
this.data = data;
this.videoHandler = videoHandler;
this.intervalIdleChecker = intervalIdleChecker;
this.fullscreenHelper = new FullscreenHelper({
container: videoHandler?.container || mount.root,
video: videoHandler?.video
});
}
get root() {
return this.overlayMount?.root ?? this.mount.root;
}
get portalContainer() {
return this.mount.portalContainer;
}
/**
* Update mount points when the player container changes.
* Moves already-mounted UI nodes and rebinds root-bound listeners (dragging).
*/
updateMount(nextMount) {
const prevRoot = this.mount.root;
const nextRoot = nextMount.root;
this.mount = nextMount;
if (!this.isInitialized()) return this;
if (prevRoot !== nextRoot) if (this.overlayMount) reparentShadowMount(this.overlayMount, nextRoot);
else {
if (this.votButton) nextRoot.appendChild(this.votButton.container);
if (this.votMenu) nextRoot.appendChild(this.votMenu.container);
}
if (this.votButtonTooltip && prevRoot !== nextRoot) this.votButtonTooltip.updateMount({
parentElement: this.root instanceof ShadowRoot ? this.root.host : this.root,
layoutRoot: this.overlayMount?.host
});
return this;
}
isInitialized() {
return this.initialized;
}
calcButtonLayout(position) {
if (this.isBigContainer && isSidePosition(position)) return {
direction: "column",
position
};
return {
direction: "row",
position: "default"
};
}
addEventListener(type, listener) {
this.events[type].addListener(listener);
return this;
}
removeEventListener(type, listener) {
this.events[type].removeListener(listener);
return this;
}
scheduleDefaultVolumePersist() {
if (this.defaultVolumePersistTimer !== void 0) globalThis.clearTimeout(this.defaultVolumePersistTimer);
this.defaultVolumePersistTimer = globalThis.setTimeout(() => {
this.defaultVolumePersistTimer = void 0;
this.flushDefaultVolumePersist();
}, this.defaultVolumePersistDelayMs);
}
bindPrimaryAction(element, handler, signal, options = {}) {
element.addEventListener("pointerdown", (event) => {
if (!isPrimaryPointerAction(event)) return;
if (options.preventPointerDefault) event.preventDefault();
handler();
}, { signal });
addKeyboardActivationListener(element, handler, { signal });
}
flushDefaultVolumePersist() {
if (this.defaultVolumePersistTimer !== void 0) {
globalThis.clearTimeout(this.defaultVolumePersistTimer);
this.defaultVolumePersistTimer = void 0;
}
if (typeof this.data.defaultVolume !== "number") return;
votStorage.set("defaultVolume", this.data.defaultVolume);
}
initUI(buttonPosition = "default") {
if (this.isInitialized()) throw new Error("[VOT] OverlayView is already initialized");
this.initialized = true;
this.lastIsBigContainer = this.isBigContainer;
this.overlayMount = createShadowMount({
parent: this.mount.root,
rootClasses: ["vot-overlay-root"],
hostStyles: {
position: "absolute",
inset: "0",
display: "block",
"pointer-events": "none"
},
rootStyles: {
position: "relative",
display: "block",
width: "100%",
height: "100%",
"pointer-events": "none"
}
});
const { position, direction } = this.calcButtonLayout(buttonPosition);
this.votButton = new VOTButton({
position,
direction,
status: "none",
labelHtml: localizationProvider.get("translateVideo")
});
this.votButton.opacity = 0;
if (!this.pipButtonVisible) this.votButton.showPiPButton(false);
this.root.appendChild(this.votButton.container);
this.votButtonTooltip = new Tooltip({
target: this.votButton.translateButton,
content: localizationProvider.get("translateVideo"),
position: this.votButton.tooltipPos,
autoLayout: false,
hidden: direction === "row",
bordered: false,
parentElement: this.root instanceof ShadowRoot ? this.root.host : this.root,
layoutRoot: this.overlayMount.host
});
this.votMenu = new VOTMenu({
titleHtml: localizationProvider.get("VOTSettings"),
position
});
this.root.appendChild(this.votMenu.container);
this.setupResizeObserver();
this.votButton.menuButton.setAttribute("aria-controls", this.votMenu.container.id);
this.downloadTranslationButton = new DownloadButton();
this.downloadTranslationButton.hidden = true;
this.downloadSubtitlesButton = UI.createIconButton(SUBTITLES_ICON, { ariaLabel: "Download subtitles" });
this.downloadSubtitlesButton.hidden = true;
this.openSettingsButton = UI.createIconButton(SETTINGS_ICON, { ariaLabel: localizationProvider.get("VOTSettings") });
this.votMenu.headerContainer.append(this.downloadTranslationButton.button, this.downloadSubtitlesButton, this.openSettingsButton);
const detectedLanguage = this.videoHandler?.videoData?.detectedLanguage ?? "en";
const responseLanguage = this.data.responseLanguage ?? "ru";
this.languagePairSelect = new LanguagePairSelect({
from: {
selectTitle: localizationProvider.get(`langs.${detectedLanguage}`),
items: Select.genLanguageItems(availableLangs, detectedLanguage)
},
to: {
selectTitle: localizationProvider.get(`langs.${responseLanguage}`),
items: Select.genLanguageItems(availableTTS, responseLanguage)
},
dialogParent: this.globalPortal
});
this.subtitlesSelectLabel = new Label({ labelText: localizationProvider.get("VOTSubtitles") });
this.subtitlesSelect = new Select({
selectTitle: localizationProvider.get("VOTSubtitlesDisabled"),
dialogTitle: localizationProvider.get("VOTSubtitles"),
labelElement: this.subtitlesSelectLabel.container,
dialogParent: this.globalPortal,
items: [{
label: localizationProvider.get("VOTSubtitlesDisabled"),
value: "disabled",
selected: true
}]
});
const videoVolume = this.videoHandler ? this.videoHandler.getVideoVolume() * 100 : 100;
this.videoVolumeSliderLabel = new SliderLabel({
labelText: localizationProvider.get("VOTVolume"),
value: videoVolume
});
this.videoVolumeSlider = new Slider({
labelHtml: this.videoVolumeSliderLabel.container,
value: videoVolume
});
this.videoVolumeSlider.hidden = !this.data.showVideoSlider || this.votButton.status !== "success";
const defaultVolume = this.data.defaultVolume ?? 100;
this.translationVolumeSliderLabel = new SliderLabel({
labelText: localizationProvider.get("VOTVolumeTranslation"),
value: defaultVolume
});
this.translationVolumeSlider = new Slider({
labelHtml: this.translationVolumeSliderLabel.container,
value: defaultVolume,
max: this.data.audioBooster && !this.data.syncVolume ? 900 : 100
});
this.translationVolumeSlider.hidden = this.votButton.status !== "success";
this.votMenu.bodyContainer.append(this.languagePairSelect.container, this.subtitlesSelect.container, this.videoVolumeSlider.container, this.translationVolumeSlider.container);
return this;
}
initUIEvents() {
if (!this.isInitialized()) throw new Error("[VOT] OverlayView isn't initialized");
this.abortController = new AbortController();
const signal = this.abortController.signal;
this.checkerUnsubscribe?.();
this.checkerUnsubscribe = this.intervalIdleChecker.subscribe(() => {
this.onCheckerTick();
});
this.votButton.container.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}, { signal });
const setMenuOpen = (open, { returnFocusToToggle = false } = {}) => {
if (!this.isInitialized()) return;
this.votMenu.hidden = !open;
this.votButton.menuButton.setAttribute("aria-expanded", open.toString());
if (this.votButtonTooltip) this.votButtonTooltip.hidden = open || this.votButton.direction === "row";
if (open) queueMicrotask(() => this.openSettingsButton?.focus?.());
else if (returnFocusToToggle) queueMicrotask(() => this.votButton.menuButton.focus?.());
else this.votButton.menuButton.blur();
};
const toggleMenu = () => setMenuOpen(this.votMenu.hidden);
const closeMenu = (returnFocusToToggle = false) => setMenuOpen(false, { returnFocusToToggle });
this.bindPrimaryAction(this.votButton.translateButton, () => {
closeMenu();
this.events["click:translate"].dispatch();
}, signal);
this.bindPrimaryAction(this.votButton.pipButton, () => {
closeMenu();
this.events["click:pip"].dispatch();
}, signal);
this.bindPrimaryAction(this.votButton.menuButton, toggleMenu, signal, { preventPointerDefault: true });
const touchAction = "none";
this.votButton.container.style.touchAction = touchAction;
this.votButton.translateButton.style.touchAction = touchAction;
this.votButton.pipButton.style.touchAction = touchAction;
this.votButton.menuButton.style.touchAction = touchAction;
this.votButton.container.addEventListener("pointerdown", this.onDragStart, { signal });
this.votButton.container.addEventListener("pointermove", this.onPointerMove, { signal });
this.votButton.container.addEventListener("pointerup", this.onDragEnd, { signal });
this.votButton.container.addEventListener("pointercancel", this.onDragEnd, { signal });
this.votButton.container.addEventListener("lostpointercapture", this.onDragEnd, { signal });
this.votMenu.container.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}, { signal });
for (const event of ["pointerdown", "mousedown"]) this.votMenu.container.addEventListener(event, (e) => {
e.stopImmediatePropagation();
}, { signal });
document.addEventListener("pointerdown", (e) => {
if (this.votMenu.hidden) return;
const isInsideDialog = (typeof e.composedPath === "function" ? e.composedPath() : []).some((node) => node instanceof HTMLElement && node.classList.contains("vot-dialog-container"));
if (isEventInside(e, this.votMenu.container) || isEventInside(e, this.votButton.menuButton) || isEventInside(e, this.votButton.container) || isInsideDialog) return;
closeMenu(false);
}, {
signal,
capture: true,
passive: true
});
this.votMenu.container.addEventListener("keydown", (e) => {
if (e.key !== "Escape") return;
const keyboardNav = document.documentElement.classList.contains("vot-keyboard-nav");
e.preventDefault();
e.stopPropagation();
closeMenu(keyboardNav);
if (!(this.votButton.container.matches(":hover") || this.votMenu.container.matches(":hover"))) this.videoHandler?.overlayVisibility?.queueAutoHide?.();
}, { signal });
this.downloadTranslationButton.addEventListener("click", () => {
this.events["click:downloadTranslation"].dispatch();
});
this.downloadSubtitlesButton.addEventListener("click", () => {
this.events["click:downloadSubtitles"].dispatch();
}, { signal });
this.openSettingsButton.addEventListener("click", () => {
closeMenu();
this.events["click:settings"].dispatch();
}, { signal });
this.languagePairSelect.fromSelect.addEventListener("selectItem", (language) => {
if (this.videoHandler?.videoData) {
this.videoHandler.videoData.detectedLanguage = language;
this.videoHandler.videoManager.rememberUserLanguageSelection(this.videoHandler.videoData.videoId, language);
}
this.events["select:fromLanguage"].dispatch(language);
});
this.languagePairSelect.toSelect.addEventListener("selectItem", async (language) => {
if (this.videoHandler?.videoData) this.videoHandler.translateToLang = this.videoHandler.videoData.responseLanguage = language;
const prevResponseLanguage = this.data.responseLanguage;
if (prevResponseLanguage !== language) {
this.data.responseLanguage = language;
await votStorage.set("responseLanguage", this.data.responseLanguage);
}
if (this.data.enabledDontTranslateLanguages && Array.isArray(this.data.dontTranslateLanguages) && this.data.dontTranslateLanguages.length === 1 && prevResponseLanguage !== language && typeof prevResponseLanguage === "string" && this.data.dontTranslateLanguages[0] === prevResponseLanguage) {
this.data.dontTranslateLanguages = [language];
await votStorage.set("dontTranslateLanguages", this.data.dontTranslateLanguages);
}
this.events["select:toLanguage"].dispatch(language);
});
this.subtitlesSelect.addEventListener("beforeOpen", async (dialog) => {
if (!this.videoHandler?.videoData) return;
const subtitleLanguage = this.videoHandler.getPreferredSubtitlesLanguage(this.videoHandler.videoData.detectedLanguage, this.videoHandler.videoData.responseLanguage);
if (!subtitleLanguage) return;
const cacheKey = this.videoHandler.getSubtitlesCacheKey(this.videoHandler.videoData.videoId, this.videoHandler.videoData.detectedLanguage, subtitleLanguage);
if (this.videoHandler.subtitlesCacheKey === cacheKey) return;
if (this.videoHandler.cacheManager.getSubtitles(cacheKey) !== void 0) {
await this.videoHandler.ensureSubtitlesForCurrentLangPair();
return;
}
const prevLoading = this.votButton?.loading ?? false;
if (this.votButton) this.votButton.loading = true;
const loadingEl = UI.createInlineLoader();
loadingEl.style.margin = "0 auto";
dialog.footerContainer.appendChild(loadingEl);
try {
await this.videoHandler.ensureSubtitlesForCurrentLangPair();
} finally {
loadingEl.remove();
if (this.votButton) this.votButton.loading = prevLoading;
}
});
this.subtitlesSelect.addEventListener("selectItem", (data) => {
this.events["select:subtitles"].dispatch(data);
});
this.videoVolumeSlider.addEventListener("input", (value, fromSetter) => {
if (this.videoVolumeSliderLabel) this.videoVolumeSliderLabel.value = value;
if (fromSetter) return;
this.events["input:videoVolume"].dispatch(value);
});
this.translationVolumeSlider.addEventListener("input", (value, fromSetter) => {
if (this.translationVolumeSliderLabel) this.translationVolumeSliderLabel.value = value;
if (this.data.defaultVolume !== value) {
this.data.defaultVolume = value;
this.scheduleDefaultVolumePersist();
}
if (fromSetter) return;
this.events["input:translationVolume"].dispatch(value);
});
return this;
}
updateButtonLayout(position, direction) {
if (!this.isInitialized()) return this;
this.votMenu.position = position;
this.votButton.position = position;
this.votButton.direction = direction;
this.votButtonTooltip.hidden = direction === "row";
this.votButtonTooltip.setPosition(this.votButton.tooltipPos);
return this;
}
moveButton(percentX) {
if (!this.isInitialized()) return this;
const isBigContainer = this.dragIsBigContainer ?? this.isBigContainer;
const position = VOTButton.calcPosition(percentX, isBigContainer);
if (position === this.votButton.position) return this;
const direction = VOTButton.calcDirection(position);
this.data.buttonPos = position;
this.updateButtonLayout(position, direction);
return this;
}
startDragSession(clientX, clientY, activitySource) {
this.dragCandidate = true;
this.dragging = false;
this.dragStartX = clientX;
this.dragStartY = clientY;
this.currentClientX = clientX;
this.containerRect = (this.root instanceof ShadowRoot ? this.root.host : this.root).getBoundingClientRect();
this.dragIsBigContainer = this.isBigContainer;
this.dragDirty = false;
this.intervalIdleChecker.markActivity(activitySource);
this.intervalIdleChecker.requestImmediateTick();
}
queueDragTick(activitySource) {
if (this.dragDirty) return;
this.dragDirty = true;
this.intervalIdleChecker.markActivity(activitySource);
this.intervalIdleChecker.requestImmediateTick();
}
updateDragFromMove(clientX, clientY, activitySource) {
this.currentClientX = clientX;
if (!this.dragCandidate) return;
if (!this.dragging) {
if (Math.abs(this.currentClientX - this.dragStartX) + Math.abs(clientY - this.dragStartY) >= this.dragThresholdPx) this.dragging = true;
}
if (!this.dragging) return;
this.queueDragTick(activitySource);
}
onDragStart = (event) => {
if (!event.isPrimary || event.button !== 0) return;
event.preventDefault();
this.activePointerId = event.pointerId;
this.startDragSession(event.clientX, event.clientY, "overlay-pointer-down");
};
onPointerMove = (event) => {
if (this.activePointerId !== event.pointerId) return;
const wasDragging = this.dragging;
this.updateDragFromMove(event.clientX, event.clientY, "overlay-pointer-move");
if (!wasDragging && this.dragging) try {
this.votButton?.container.setPointerCapture(event.pointerId);
} catch {}
if (this.dragging) event.preventDefault();
};
applyDragFromState = () => {
if (!this.dragging || !this.dragDirty || !this.containerRect) return;
const width = this.containerRect.width;
if (!(width > 0 && Number.isFinite(width))) return;
this.dragDirty = false;
const x = this.currentClientX - this.containerRect.left;
const percentX = Math.max(0, Math.min(x, width)) / width * 100;
this.moveButton(percentX);
};
onCheckerTick = () => {
this.applyDragFromState();
};
onDragEnd = (event) => {
if (event && this.activePointerId !== null && event.pointerId !== this.activePointerId) return;
const pointerId = this.activePointerId;
if (pointerId !== null) try {
if (this.votButton?.container.hasPointerCapture(pointerId)) this.votButton.container.releasePointerCapture(pointerId);
} catch {}
this.applyDragFromState();
const isBigContainer = this.dragIsBigContainer ?? this.isBigContainer;
if (this.dragging && isBigContainer && this.data.buttonPos) votStorage.set("buttonPos", this.data.buttonPos);
this.dragging = false;
this.dragCandidate = false;
this.dragDirty = false;
this.containerRect = null;
this.dragIsBigContainer = null;
this.activePointerId = null;
};
updateButtonOpacity(opacity) {
if (!this.isInitialized() || !this.votMenu.hidden) return this;
if (Math.abs(this.votButton.opacity - opacity) > .01) this.votButton.opacity = opacity;
return this;
}
doReleaseUI() {
this.votButton?.remove();
this.votMenu?.remove();
this.votButtonTooltip?.release();
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = void 0;
}
this.fullscreenHelper.destroy();
destroyShadowMount(this.overlayMount);
this.overlayMount = void 0;
}
doReleaseUIEvents() {
this.abortController?.abort();
this.abortController = null;
this.checkerUnsubscribe?.();
this.checkerUnsubscribe = null;
this.onDragEnd();
this.flushDefaultVolumePersist();
for (const event of Object.values(this.events)) event.clear();
}
release() {
if (!this.isInitialized()) return this;
this.doReleaseUIEvents();
this.doReleaseUI();
this.initialized = false;
return this;
}
get isBigContainer() {
return this.fullscreenHelper.isBigContainer(OverlayView.BIG_CONTAINER_WIDTH_PX);
}
setupResizeObserver() {
if (this.resizeObserver) return;
this.resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width } = entry.contentRect;
const currentIsBigContainer = width > OverlayView.BIG_CONTAINER_WIDTH_PX;
if (this.lastIsBigContainer !== currentIsBigContainer) {
this.lastIsBigContainer = currentIsBigContainer;
this.handleContainerSizeChange(currentIsBigContainer);
}
this.updateMenuHeight(entry.contentRect.height);
}
});
const target = this.fullscreenHelper.getResizeObserverTarget();
this.resizeObserver.observe(target);
}
updateMenuHeight(containerHeight) {
if (!this.isInitialized() || !this.votMenu?.container) return;
let height;
if (containerHeight && containerHeight > 200) height = containerHeight;
else {
const target = this.fullscreenHelper.getResizeObserverTarget();
height = target.getBoundingClientRect().height || target.clientHeight || window.innerHeight * .75;
}
if (!height || height < 200) height = window.innerHeight * .75;
this.votMenu.container.style.setProperty("--vot-container-height", `${height}px`);
}
handleContainerSizeChange(isBigContainer) {
if (!this.isInitialized()) return;
const currentPosition = this.votButton.position;
const newPosition = isBigContainer ? currentPosition : "default";
if (currentPosition !== newPosition) {
const direction = VOTButton.calcDirection(newPosition);
this.updateButtonLayout(newPosition, direction);
}
}
get pipButtonVisible() {
return isPiPAvailable() && !!this.data.showPiPButton;
}
};
function isSidePosition(position) {
return position === "left" || position === "right";
}
//#endregion
//#region src/types/components/votButton.ts
var positions = [
"default",
"top",
"left",
"right"
];
//#endregion
//#region src/utils/environment.ts
var UNKNOWN_VALUE = "unknown";
var joinParts = (...parts) => {
return parts.filter(Boolean).join(" ").trim() || UNKNOWN_VALUE;
};
function isDocumentHidden() {
return typeof document !== "undefined" && document.hidden;
}
function getEnvironmentInfo() {
return {
os: joinParts(browserInfo.os?.name, browserInfo.os?.version),
browser: joinParts(browserInfo.browser?.name, browserInfo.browser?.version),
loader: (() => {
const handler = GM_info?.scriptHandler;
const version = GM_info?.version;
if (handler && version) return `${handler} v${version}`;
return handler || version || UNKNOWN_VALUE;
})(),
scriptVersion: GM_info?.script?.version ?? UNKNOWN_VALUE,
scriptName: GM_info?.script?.name ?? UNKNOWN_VALUE,
url: globalThis?.location?.href ?? UNKNOWN_VALUE
};
}
//#endregion
//#region src/ui/components/accountButton.ts
var AccountButton = class {
container;
accountWrapper;
buttons;
usernameEl;
avatarEl;
avatarImg;
actionButton;
refreshButton;
tokenButton;
onClick = new EventImpl();
onRefresh = new EventImpl();
onClickSecret = new EventImpl();
events = {
click: this.onClick,
"click:secret": this.onClickSecret,
refresh: this.onRefresh
};
_loggedIn;
_username;
_avatarId;
constructor({ loggedIn = false, username = "unnamed", avatarId = "0/0-0" } = {}) {
this._loggedIn = loggedIn;
this._username = username;
this._avatarId = avatarId;
const elements = this.createElements();
this.container = elements.container;
this.accountWrapper = elements.accountWrapper;
this.buttons = elements.buttons;
this.usernameEl = elements.usernameEl;
this.avatarEl = elements.avatarEl;
this.avatarImg = elements.avatarImg;
this.actionButton = elements.actionButton;
this.refreshButton = elements.refreshButton;
this.tokenButton = elements.tokenButton;
}
createElements() {
const container = UI.createEl("vot-block", ["vot-account"]);
const accountWrapper = UI.createEl("vot-block", ["vot-account-wrapper"]);
accountWrapper.hidden = !this._loggedIn;
const avatarImg = UI.createEl("img", ["vot-account-avatar-img"]);
avatarImg.src = `${avatarServerUrl}/${this._avatarId}/islands-retina-middle`;
avatarImg.loading = "lazy";
avatarImg.alt = "user avatar";
const avatarEl = UI.createEl("vot-block", ["vot-account-avatar"], avatarImg);
const usernameEl = UI.createEl("vot-block", ["vot-account-username"]);
usernameEl.textContent = this._username;
accountWrapper.append(avatarEl, usernameEl);
const buttons = UI.createEl("vot-block", ["vot-account-buttons"]);
const actionButton = UI.createOutlinedButton(this.buttonText);
actionButton.addEventListener("click", () => {
this.onClick.dispatch();
});
const tokenButton = UI.createIconButton(KEY_ICON, { ariaLabel: localizationProvider.get("VOTLoginViaToken") });
tokenButton.hidden = this._loggedIn;
tokenButton.addEventListener("click", () => {
this.onClickSecret.dispatch();
});
const refreshButton = UI.createIconButton(REFRESH_ICON, { ariaLabel: localizationProvider.get("VOTRefresh") });
refreshButton.addEventListener("click", () => {
this.onRefresh.dispatch();
});
buttons.append(actionButton, tokenButton, refreshButton);
container.append(accountWrapper, buttons);
return {
container,
accountWrapper,
buttons,
usernameEl,
avatarImg,
avatarEl,
actionButton,
refreshButton,
tokenButton
};
}
addEventListener(type, listener) {
this.events[type].addListener(listener);
return this;
}
removeEventListener(type, listener) {
this.events[type].removeListener(listener);
return this;
}
get buttonText() {
return this._loggedIn ? localizationProvider.get("VOTLogout") : localizationProvider.get("VOTLogin");
}
get loggedIn() {
return this._loggedIn;
}
set loggedIn(isLoggedIn) {
this._loggedIn = isLoggedIn;
this.accountWrapper.hidden = !this._loggedIn;
this.actionButton.textContent = this.buttonText;
this.tokenButton.hidden = this._loggedIn;
}
get avatarId() {
return this._avatarId;
}
set avatarId(avatarId) {
this._avatarId = avatarId ?? "0/0-0";
this.avatarImg.src = `${avatarServerUrl}/${this._avatarId}/islands-retina-middle`;
}
get username() {
return this._username;
}
set username(username) {
this._username = username ?? "unnamed";
this.usernameEl.textContent = this._username;
}
set hidden(isHidden) {
this.container.hidden = isHidden;
}
get hidden() {
return this.container.hidden;
}
};
//#endregion
//#region src/ui/components/checkbox.ts
var Checkbox = class {
container;
input;
label;
onChange = new EventImpl();
_labelHtml;
_checked;
_isSubCheckbox;
constructor({ labelHtml, checked = false, isSubCheckbox = false }) {
this._labelHtml = labelHtml;
this._checked = checked;
this._isSubCheckbox = isSubCheckbox;
const elements = this.createElements();
this.container = elements.container;
this.input = elements.input;
this.label = elements.label;
}
createElements() {
const container = UI.createEl("label", ["vot-checkbox"]);
if (this._isSubCheckbox) container.classList.add("vot-checkbox-sub");
const input = document.createElement("input");
input.type = "checkbox";
input.checked = this._checked;
input.addEventListener("change", () => {
this._checked = input.checked;
this.onChange.dispatch(this._checked);
});
const label = UI.createEl("span");
D(this._labelHtml, label);
container.append(input, label);
return {
container,
input,
label
};
}
addEventListener(_type, listener) {
this.onChange.addListener(listener);
return this;
}
removeEventListener(_type, listener) {
this.onChange.removeListener(listener);
return this;
}
set hidden(isHidden) {
this.container.hidden = isHidden;
}
get hidden() {
return this.container.hidden;
}
get disabled() {
return this.input.disabled;
}
set disabled(isDisabled) {
this.input.disabled = isDisabled;
}
get checked() {
return this._checked;
}
/**
* If you set a different new value, it will trigger the change event
*/
set checked(isChecked) {
if (this._checked === isChecked) return;
this._checked = this.input.checked = isChecked;
this.onChange.dispatch(this._checked);
}
};
//#endregion
//#region src/ui/components/details.ts
var Details = class {
container;
header;
arrowIcon;
onClick = new EventImpl();
_titleHtml;
constructor({ titleHtml }) {
this._titleHtml = titleHtml;
const elements = this.createElements();
this.container = elements.container;
this.header = elements.header;
this.arrowIcon = elements.arrowIcon;
}
createElements() {
const container = UI.createEl("vot-block", ["vot-details"]);
UI.makeButtonLike(container);
const header = UI.createEl("vot-block");
header.append(this._titleHtml);
const arrowIcon = UI.createEl("vot-block", ["vot-details-arrow-icon"]);
D(CHEVRON_ICON, arrowIcon);
container.append(header, arrowIcon);
container.addEventListener("click", () => {
this.onClick.dispatch();
});
return {
container,
header,
arrowIcon
};
}
addEventListener(_type, listener) {
this.onClick.addListener(listener);
return this;
}
removeEventListener(_type, listener) {
this.onClick.removeListener(listener);
return this;
}
set hidden(isHidden) {
this.container.hidden = isHidden;
}
get hidden() {
return this.container.hidden;
}
};
//#endregion
//#region src/ui/components/hotkeyButton.ts
var HotkeyButton = class {
container;
button;
onChange = new EventImpl();
_labelHtml;
_key;
pressedKeys;
comboKeys;
recording = false;
constructor({ labelHtml, key = null }) {
this._labelHtml = labelHtml;
this._key = key;
this.pressedKeys = /* @__PURE__ */ new Set();
this.comboKeys = /* @__PURE__ */ new Set();
const elements = this.createElements();
this.container = elements.container;
this.button = elements.button;
}
stopRecordingKeys() {
this.recording = false;
document.removeEventListener("keydown", this.keydownHandle);
document.removeEventListener("keyup", this.keyupOrBlurHandle);
globalThis.removeEventListener("blur", this.blurHandle);
delete this.button.dataset.status;
this.pressedKeys.clear();
this.comboKeys.clear();
}
keydownHandle = (event) => {
if (!this.recording || event.repeat) return;
event.preventDefault();
if (event.code === "Escape") {
this.key = null;
this.button.textContent = this.keyText;
this.stopRecordingKeys();
return;
}
this.pressedKeys.add(event.code);
this.comboKeys.add(event.code);
this.button.textContent = formatKeysComboDisplay(this.pressedKeys);
};
keyupOrBlurHandle = (event) => {
if (!this.recording) return;
if (event) {
this.pressedKeys.delete(event.code);
this.button.textContent = this.pressedKeys.size ? formatKeysComboDisplay(this.pressedKeys) : formatKeysComboDisplay(this.comboKeys);
if (this.pressedKeys.size) return;
}
this.key = this.comboKeys.size ? formatKeysCombo(this.comboKeys) : null;
this.stopRecordingKeys();
};
blurHandle = () => {
this.keyupOrBlurHandle();
};
createElements() {
const container = UI.createEl("vot-block", ["vot-hotkey"]);
const label = UI.createEl("vot-block", ["vot-hotkey-label"]);
label.textContent = this._labelHtml;
const button = UI.createEl("vot-block", ["vot-hotkey-button"]);
UI.makeButtonLike(button);
button.textContent = this.keyText;
button.addEventListener("click", () => {
if (this.recording) {
this.stopRecordingKeys();
this.button.textContent = this.keyText;
return;
}
button.dataset.status = "active";
this.recording = true;
this.pressedKeys.clear();
this.comboKeys.clear();
this.button.textContent = localizationProvider.get("PressTheKeyCombination");
document.addEventListener("keydown", this.keydownHandle);
document.addEventListener("keyup", this.keyupOrBlurHandle);
globalThis.addEventListener("blur", this.blurHandle);
});
container.append(label, button);
return {
container,
button,
label
};
}
addEventListener(_type, listener) {
this.onChange.addListener(listener);
return this;
}
removeEventListener(_type, listener) {
this.onChange.removeListener(listener);
return this;
}
set hidden(isHidden) {
this.container.hidden = isHidden;
}
get hidden() {
return this.container.hidden;
}
get key() {
return this._key;
}
get keyText() {
if (!this._key) return localizationProvider.get("None");
return formatKeysComboDisplay(this._key);
}
/**
* If you set a different new value, it will trigger the change event
*/
set key(newKey) {
if (this._key === newKey) return;
this._key = newKey;
this.button.textContent = this.keyText;
this.onChange.dispatch(this._key);
}
};
/**
* Formats a set of key codes into a string representing a key combination
*/
function formatKeysCombo(keys) {
return (Array.isArray(keys) ? keys : Array.from(keys)).map((code) => code.replace("Key", "").replace("Digit", "")).join("+");
}
/**
* Human-friendly formatting for hotkeys. Does not change stored semantics.
*/
function formatKeysComboDisplay(keys) {
let parts;
if (typeof keys === "string") parts = keys.split("+").filter(Boolean);
else if (Array.isArray(keys)) parts = keys;
else parts = Array.from(keys);
const map = (k) => {
switch (k) {
case "ControlLeft":
case "ControlRight":
case "Control": return "Ctrl";
case "ShiftLeft":
case "ShiftRight":
case "Shift": return "Shift";
case "AltLeft":
case "AltRight":
case "Alt": return "Alt";
case "MetaLeft":
case "MetaRight":
case "Meta": return "Meta";
case "Space": return "Space";
case "ArrowUp": return "↑";
case "ArrowDown": return "↓";
case "ArrowLeft": return "←";
case "ArrowRight": return "→";
default: return k.replace("Key", "").replace("Digit", "");
}
};
const priority = (k) => {
const m = map(k);
if (m === "Ctrl") return 0;
if (m === "Alt") return 1;
if (m === "Shift") return 2;
if (m === "Meta") return 3;
return 10;
};
return parts.slice().sort((a, b) => priority(a) - priority(b)).map(map).join("+");
}
//#endregion
//#region src/ui/views/settings.ts
var SETTINGS_EVENT_KEYS = [
"click:bugReport",
"click:resetSettings",
"update:account",
"change:autoTranslate",
"change:autoSubtitles",
"change:showVideoVolume",
"change:audioBooster",
"change:syncVolume",
"change:useLivelyVoice",
"change:subtitlesHighlightWords",
"change:subtitlesSmartLayout",
"select:responseLanguageSubtitles",
"select:subtitlesFontFamily",
"change:proxyWorkerHost",
"change:useNewAudioPlayer",
"change:onlyBypassMediaCSP",
"change:showPiPButton",
"input:subtitlesMaxLength",
"input:subtitlesFontSize",
"input:subtitlesBackgroundOpacity",
"input:autoHideButtonDelay",
"select:proxyTranslationStatus",
"select:translationTextService",
"select:buttonPosition",
"select:menuLanguage"
];
function createSettingsEvents() {
const events = {};
for (const key of SETTINGS_EVENT_KEYS) events[key] = new EventImpl();
return events;
}
var GOOGLE_FONTS_SEARCH_LIMIT = 30;
var [AUTO_SUBTITLE_LANGUAGE_VALUE$1, ORIGINAL_SUBTITLE_LANGUAGE_VALUE$1] = subtitleResponseLanguageModes;
var subtitleFontFamilyLabels = {
"default-sans": "Default Sans",
arial: "Arial",
helvetica: "Helvetica",
roboto: "Roboto",
verdana: "Verdana",
"open-sans": "Open Sans",
poppins: "Poppins",
lato: "Lato",
montserrat: "Montserrat",
barlow: "Barlow"
};
function getSubtitleFontFamilyLabel(fontFamily) {
if (isBuiltInSubtitleFontFamily(fontFamily)) return subtitleFontFamilyLabels[fontFamily];
return getGoogleSubtitleFontFamilyName(fontFamily) ?? "Default Sans";
}
function getAvailableSubtitleLanguages() {
return Object.keys(localizationProvider.defaultLocale).filter((key) => key.startsWith("langs.") && key !== "langs.auto").map((key) => key.slice(6)).toSorted((left, right) => localizationProvider.getLangLabel(left).localeCompare(localizationProvider.getLangLabel(right)));
}
function getSubtitleLanguageSettingLabel(value) {
if (value === ORIGINAL_SUBTITLE_LANGUAGE_VALUE$1) return localizationProvider.get("VOTOriginalVideoLanguage");
return localizationProvider.getLangLabel(value);
}
function buildSubtitleLanguageSettingItems(selectedValue) {
return [
{
label: getSubtitleLanguageSettingLabel(AUTO_SUBTITLE_LANGUAGE_VALUE$1),
value: AUTO_SUBTITLE_LANGUAGE_VALUE$1,
selected: selectedValue === AUTO_SUBTITLE_LANGUAGE_VALUE$1
},
{
label: getSubtitleLanguageSettingLabel(ORIGINAL_SUBTITLE_LANGUAGE_VALUE$1),
value: ORIGINAL_SUBTITLE_LANGUAGE_VALUE$1,
selected: selectedValue === ORIGINAL_SUBTITLE_LANGUAGE_VALUE$1
},
...getAvailableSubtitleLanguages().map((language) => ({
label: getSubtitleLanguageSettingLabel(language),
value: language,
selected: selectedValue === language
}))
];
}
var SettingsView = class SettingsView {
static PERSIST_DELAY_MS = 250;
globalPortal;
initialized = false;
data;
videoHandler;
suppressSubtitlesSmartLayoutCheckboxChange = false;
events = createSettingsEvents();
persistTimerIds = {};
onAuthRefreshMessage = (event) => {
if (!isAuthRefreshMessage(event.data)) return;
this.refreshAccountFromStorage();
};
dialog;
accountButton;
accountButtonRefreshTooltip;
accountButtonTokenTooltip;
accountStorageListenerCleanup;
autoTranslateCheckbox;
autoSubtitlesCheckbox;
dontTranslateLanguagesCheckbox;
dontTranslateLanguagesSelect;
autoSetVolumeSliderLabel;
autoSetVolumeCheckbox;
smartDuckingCheckbox;
autoSetVolumeSlider;
showVideoVolumeSliderCheckbox;
audioBoosterCheckbox;
audioBoosterTooltip;
syncVolumeCheckbox;
downloadWithNameCheckbox;
sendNotifyOnCompleteCheckbox;
useLivelyVoiceCheckbox;
useLivelyVoiceTooltip;
useAudioDownloadCheckbox;
useAudioDownloadCheckboxLabel;
useAudioDownloadCheckboxTooltip;
responseLanguageSubtitlesSelectLabel;
responseLanguageSubtitlesSelect;
subtitlesDownloadFormatSelectLabel;
subtitlesDownloadFormatSelect;
subtitlesHighlightWordsCheckbox;
subtitlesSmartLayoutCheckbox;
subtitlesMaxLengthSliderLabel;
subtitlesMaxLengthSlider;
subtitlesFontSizeSliderLabel;
subtitlesFontSizeSlider;
subtitlesFontFamilySelectLabel;
subtitlesFontFamilySelect;
subtitlesBackgroundOpacitySliderLabel;
subtitlesBackgroundOpacitySlider;
translateHotkeyButton;
subtitlesHotkeyButton;
proxyWorkerHostTextfield;
proxyTranslationStatusSelectLabel;
proxyTranslationStatusSelectTooltip;
proxyTranslationStatusSelect;
translateAPIErrorsCheckbox;
useNewAudioPlayerCheckbox;
useNewAudioPlayerTooltip;
onlyBypassMediaCSPCheckbox;
onlyBypassMediaCSPTooltip;
translationTextServiceLabel;
translationTextServiceSelect;
translationTextServiceTooltip;
detectServiceLabel;
detectServiceSelect;
showPiPButtonCheckbox;
autoHideButtonDelaySliderLabel;
autoHideButtonDelaySlider;
buttonPositionSelectLabel;
buttonPositionSelect;
buttonPositionTooltip;
menuLanguageSelectLabel;
menuLanguageSelect;
bugReportButton;
resetSettingsButton;
constructor({ globalPortal, data = {}, videoHandler }) {
this.globalPortal = globalPortal;
this.data = data;
this.videoHandler = videoHandler;
}
isInitialized() {
return this.initialized;
}
createAccordionSection(title, options = {}) {
const section = UI.createEl("vot-block", ["vot-settings-section"]);
const header = new Details({ titleHtml: title });
header.container.classList.add("vot-settings-section-header");
const sectionId = createDomId("vot-settings-section");
const headerId = `${sectionId}-header`;
const contentId = `${sectionId}-content`;
header.container.id = headerId;
const content = UI.createEl("vot-block", ["vot-settings-section-content"]);
content.id = contentId;
content.setAttribute("role", "region");
content.setAttribute("aria-labelledby", headerId);
header.container.setAttribute("aria-controls", contentId);
const setOpen = (open) => {
header.container.dataset.open = open ? "true" : "false";
header.container.setAttribute("aria-expanded", open ? "true" : "false");
content.hidden = !open;
};
const getOpen = () => header.container.dataset.open === "true";
setOpen(!!options.open);
header.addEventListener("click", () => {
setOpen(!(header.container.dataset.open === "true"));
});
section.append(header.container, content);
return {
title,
container: section,
header: header.container,
content,
setOpen,
getOpen
};
}
setSubtitlesSmartLayout(checked) {
this.data.subtitlesSmartLayout = checked;
votStorage.set("subtitlesSmartLayout", checked);
debug.log("subtitlesSmartLayout value changed. New value:", checked);
if (this.subtitlesSmartLayoutCheckbox?.checked !== checked) {
this.suppressSubtitlesSmartLayoutCheckboxChange = true;
this.subtitlesSmartLayoutCheckbox.checked = checked;
this.suppressSubtitlesSmartLayoutCheckboxChange = false;
}
this.events["change:subtitlesSmartLayout"].dispatch(checked);
}
scheduleStoragePersist(key, value) {
const prevTimerId = this.persistTimerIds[key];
if (prevTimerId !== void 0) globalThis.clearTimeout(prevTimerId);
this.persistTimerIds[key] = globalThis.setTimeout(() => {
this.persistTimerIds[key] = void 0;
votStorage.set(key, value);
}, SettingsView.PERSIST_DELAY_MS);
}
flushStoragePersists() {
for (const key of Object.keys(this.persistTimerIds)) {
const timerId = this.persistTimerIds[key];
if (timerId === void 0) continue;
globalThis.clearTimeout(timerId);
this.persistTimerIds[key] = void 0;
const value = this.data[key];
if (typeof value === "number") votStorage.set(key, value);
}
}
bindPersistedSetting({ control, event, apply, storageKey, readPersistedValue, logLabel, dispatch, afterPersist }) {
control.addEventListener(event, async (value) => {
apply(value);
await votStorage.set(storageKey, readPersistedValue());
debug.log(`${logLabel} value changed. New value:`, value);
if (afterPersist) await afterPersist(value);
dispatch?.(value);
});
}
bindBufferedNumericSetting({ control, label, storageKey, logLabel, toStoredValue = (value) => value, beforeApply, dispatch }) {
control.addEventListener("input", (value) => {
label.value = value;
beforeApply?.();
const storedValue = toStoredValue(value);
this.data[storageKey] = storedValue;
this.scheduleStoragePersist(storageKey, storedValue);
debug.log(`${logLabel} value changed. New value:`, storedValue);
dispatch?.(value);
});
}
createSettingsSections() {
const sections = [
this.createAccordionSection(localizationProvider.get("VOTMyAccount"), { open: true }),
this.createAccordionSection(localizationProvider.get("translationSettings"), { open: true }),
this.createAccordionSection(localizationProvider.get("subtitlesSettings")),
this.createAccordionSection(localizationProvider.get("hotkeysSettings")),
this.createAccordionSection(localizationProvider.get("proxySettings")),
this.createAccordionSection(localizationProvider.get("miscSettings")),
this.createAccordionSection(localizationProvider.get("appearance")),
this.createAccordionSection(localizationProvider.get("aboutExtension"))
];
return {
accountSection: sections[0],
translationSection: sections[1],
subtitlesSection: sections[2],
hotkeysSection: sections[3],
proxySection: sections[4],
miscSection: sections[5],
appearanceSection: sections[6],
aboutSection: sections[7],
sections
};
}
initAccountControls() {
this.accountButton = new AccountButton({
avatarId: this.data.account?.avatarId,
username: this.data.account?.username,
loggedIn: !!this.data.account?.token
});
if (votStorage.isSupportOnlyLS) {
this.accountButton.refreshButton.setAttribute("disabled", "true");
this.accountButton.actionButton.setAttribute("disabled", "true");
} else this.accountButtonRefreshTooltip = new Tooltip({
target: this.accountButton.refreshButton,
content: localizationProvider.get("VOTRefresh"),
position: "bottom",
backgroundColor: "var(--vot-helper-ondialog)",
parentElement: this.globalPortal
});
this.accountButtonTokenTooltip = new Tooltip({
target: this.accountButton.tokenButton,
content: localizationProvider.get("VOTLoginViaToken"),
position: "bottom",
backgroundColor: "var(--vot-helper-ondialog)",
parentElement: this.globalPortal
});
}
bindAccountStorageListener() {
this.accountStorageListenerCleanup?.();
this.accountStorageListenerCleanup = votStorage.addValueChangeListener("account", (_key, _oldValue, account) => {
this.data.account = account ?? {};
if (!this.isInitialized() || !this.accountButton) return;
this.updateAccountInfo();
});
}
async refreshAccountFromStorage() {
if (votStorage.isSupportOnlyLS) return;
this.data.account = await votStorage.get("account", {});
if (!this.isInitialized() || !this.accountButton) return;
this.updateAccountInfo();
}
buildSubtitleFontItems(selectedFontFamily, dynamicFontFamilies = []) {
const items = subtitleFontFamilies.map((fontFamily) => ({
label: subtitleFontFamilyLabels[fontFamily],
value: fontFamily,
selected: fontFamily === selectedFontFamily
}));
const dynamicItems = dynamicFontFamilies.filter((familyName) => {
const lowerFamilyName = familyName.toLowerCase();
return !items.some((item) => item.label.toLowerCase() === lowerFamilyName);
}).map((familyName) => {
const fontValue = toGoogleSubtitleFontFamily(familyName);
return {
label: familyName,
value: fontValue,
selected: fontValue === selectedFontFamily
};
});
if (!isBuiltInSubtitleFontFamily(selectedFontFamily) && !dynamicItems.some((item) => item.value === selectedFontFamily)) {
const currentGoogleFontFamily = getGoogleSubtitleFontFamilyName(selectedFontFamily);
if (currentGoogleFontFamily) dynamicItems.unshift({
label: currentGoogleFontFamily,
value: selectedFontFamily,
selected: true
});
}
return [...items, ...dynamicItems];
}
async searchSubtitleFontItems(query, fallbackFontFamily) {
const activeFontFamily = Array.from(this.subtitlesFontFamilySelect?.selectedValues ?? [])[0] ?? fallbackFontFamily;
const normalizedQuery = query.trim().toLowerCase();
if (!normalizedQuery) return this.buildSubtitleFontItems(activeFontFamily);
const matchingGoogleFonts = (await loadGoogleFontsCatalog()).filter((familyName) => familyName.toLowerCase().includes(normalizedQuery)).slice(0, GOOGLE_FONTS_SEARCH_LIMIT);
return this.buildSubtitleFontItems(activeFontFamily, matchingGoogleFonts);
}
initUI() {
if (this.isInitialized()) throw new Error("[VOT] SettingsView is already initialized");
this.dialog = new Dialog({ titleHtml: localizationProvider.get("VOTSettings") });
this.globalPortal.appendChild(this.dialog.container);
const { accountSection, translationSection, subtitlesSection, hotkeysSection, proxySection, miscSection, appearanceSection, aboutSection, sections } = this.createSettingsSections();
this.dialog.bodyContainer.append(...sections.map((section) => section.container));
this.initAccountControls();
this.autoTranslateCheckbox = new Checkbox({
labelHtml: localizationProvider.get("VOTAutoTranslate"),
checked: this.data.autoTranslate
});
this.autoSubtitlesCheckbox = new Checkbox({
labelHtml: localizationProvider.get("VOTAutoSubtitles"),
checked: this.data.autoSubtitles
});
const dontTranslateLanguages = this.data.dontTranslateLanguages ?? [];
this.dontTranslateLanguagesCheckbox = new Checkbox({
labelHtml: localizationProvider.get("DontTranslateSelectedLanguages"),
checked: this.data.enabledDontTranslateLanguages
});
this.dontTranslateLanguagesSelect = new Select({
dialogParent: this.globalPortal,
dialogTitle: localizationProvider.get("DontTranslateSelectedLanguages"),
selectTitle: dontTranslateLanguages.map((lang) => localizationProvider.get(`langs.${lang}`)).join(", ") || localizationProvider.get("DontTranslateSelectedLanguages"),
items: Select.genLanguageItems(availableLangs).map((item) => ({
...item,
selected: dontTranslateLanguages.includes(item.value)
})),
multiSelect: true,
labelElement: this.dontTranslateLanguagesCheckbox.container
});
this.dontTranslateLanguagesSelect.disabled = !this.dontTranslateLanguagesCheckbox.checked;
const autoVolume = this.data.autoVolume ?? 15;
this.autoSetVolumeSliderLabel = new SliderLabel({
labelText: localizationProvider.get("VOTAutoSetVolume"),
value: autoVolume
});
this.autoSetVolumeCheckbox = new Checkbox({
labelHtml: this.autoSetVolumeSliderLabel.container,
checked: this.data.enabledAutoVolume ?? true
});
this.autoSetVolumeSlider = new Slider({
labelHtml: this.autoSetVolumeCheckbox.container,
value: autoVolume,
min: 0
});
const syncVolumeEnabled = Boolean(this.data.syncVolume);
this.autoSetVolumeSlider.disabled = !this.autoSetVolumeCheckbox.checked;
this.smartDuckingCheckbox = new Checkbox({
labelHtml: localizationProvider.get("smartDucking"),
checked: this.data.enabledSmartDucking ?? true
});
this.smartDuckingCheckbox.disabled = syncVolumeEnabled || !this.autoSetVolumeCheckbox.checked;
this.showVideoVolumeSliderCheckbox = new Checkbox({
labelHtml: localizationProvider.get("showVideoVolumeSlider"),
checked: this.data.showVideoSlider
});
this.audioBoosterCheckbox = new Checkbox({
labelHtml: localizationProvider.get("VOTAudioBooster"),
checked: this.data.audioBooster
});
if (!this.videoHandler?.isAudioContextSupported) {
this.audioBoosterCheckbox.disabled = true;
this.audioBoosterTooltip = new Tooltip({
target: this.audioBoosterCheckbox.container,
content: localizationProvider.get("VOTNeedWebAudioAPI"),
position: "bottom",
backgroundColor: "var(--vot-helper-ondialog)",
parentElement: this.globalPortal
});
}
this.syncVolumeCheckbox = new Checkbox({
labelHtml: localizationProvider.get("VOTSyncVolume"),
checked: this.data.syncVolume
});
this.downloadWithNameCheckbox = new Checkbox({
labelHtml: localizationProvider.get("VOTDownloadWithName"),
checked: this.data.downloadWithName
});
this.downloadWithNameCheckbox.disabled = !isSupportGMXhr;
this.sendNotifyOnCompleteCheckbox = new Checkbox({
labelHtml: localizationProvider.get("VOTSendNotifyOnComplete"),
checked: this.data.sendNotifyOnComplete
});
this.useLivelyVoiceCheckbox = new Checkbox({
labelHtml: localizationProvider.get("VOTUseLivelyVoice"),
checked: this.data.useLivelyVoice
});
this.useLivelyVoiceTooltip = new Tooltip({
target: this.useLivelyVoiceCheckbox.container,
content: localizationProvider.get("VOTAccountRequired"),
position: "bottom",
backgroundColor: "var(--vot-helper-ondialog)",
parentElement: this.globalPortal,
hidden: !!this.data.account?.token
});
if (!this.data.account?.token) this.useLivelyVoiceCheckbox.disabled = true;
this.useAudioDownloadCheckboxLabel = new Label({
labelText: localizationProvider.get("VOTUseAudioDownload"),
icon: WARNING_ICON
});
this.useAudioDownloadCheckbox = new Checkbox({
labelHtml: this.useAudioDownloadCheckboxLabel.container,
checked: this.data.useAudioDownload
});
if (!isSupportGMXhr) this.useAudioDownloadCheckbox.disabled = true;
this.useAudioDownloadCheckboxTooltip = new Tooltip({
target: this.useAudioDownloadCheckboxLabel.container,
content: localizationProvider.get("VOTUseAudioDownloadWarning"),
position: "bottom",
backgroundColor: "var(--vot-helper-ondialog)",
parentElement: this.globalPortal
});
accountSection.content.append(this.accountButton.container);
translationSection.content.append(this.autoTranslateCheckbox.container, this.autoSubtitlesCheckbox.container, this.dontTranslateLanguagesSelect.container, this.autoSetVolumeSlider.container, this.smartDuckingCheckbox.container, this.showVideoVolumeSliderCheckbox.container, this.audioBoosterCheckbox.container, this.syncVolumeCheckbox.container, this.downloadWithNameCheckbox.container, this.sendNotifyOnCompleteCheckbox.container, this.useLivelyVoiceCheckbox.container, this.useAudioDownloadCheckbox.container);
this.subtitlesDownloadFormatSelectLabel = new Label({ labelText: localizationProvider.get("VOTSubtitlesDownloadFormat") });
this.subtitlesDownloadFormatSelect = new Select({
selectTitle: this.data.subtitlesDownloadFormat ?? localizationProvider.get("VOTSubtitlesDownloadFormat"),
dialogTitle: localizationProvider.get("VOTSubtitlesDownloadFormat"),
dialogParent: this.globalPortal,
labelElement: this.subtitlesDownloadFormatSelectLabel.container,
items: subtitleFormats.map((format) => ({
label: format.toUpperCase(),
value: format,
selected: format === this.data.subtitlesDownloadFormat
}))
});
const responseLanguageSubtitles = this.data.responseLanguageSubtitles ?? AUTO_SUBTITLE_LANGUAGE_VALUE$1;
this.responseLanguageSubtitlesSelectLabel = new Label({ labelText: localizationProvider.get("VOTDefaultSubtitlesLanguage") });
this.responseLanguageSubtitlesSelect = new Select({
selectTitle: getSubtitleLanguageSettingLabel(responseLanguageSubtitles),
dialogTitle: localizationProvider.get("VOTDefaultSubtitlesLanguage"),
dialogParent: this.globalPortal,
labelElement: this.responseLanguageSubtitlesSelectLabel.container,
items: buildSubtitleLanguageSettingItems(responseLanguageSubtitles)
});
this.subtitlesHighlightWordsCheckbox = new Checkbox({
labelHtml: localizationProvider.get("VOTHighlightWords"),
checked: this.data.highlightWords
});
const subtitlesSmartLayout = this.data.subtitlesSmartLayout ?? true;
this.subtitlesSmartLayoutCheckbox = new Checkbox({
labelHtml: localizationProvider.get("subtitlesSmartLayout"),
checked: subtitlesSmartLayout
});
const subtitlesMaxLength = this.data.subtitlesMaxLength ?? 300;
this.subtitlesMaxLengthSliderLabel = new SliderLabel({
labelText: localizationProvider.get("VOTSubtitlesMaxLength"),
labelEOL: ":",
value: subtitlesMaxLength,
symbol: ""
});
this.subtitlesMaxLengthSlider = new Slider({
labelHtml: this.subtitlesMaxLengthSliderLabel.container,
value: subtitlesMaxLength,
min: 50,
max: 300
});
const subtitlesFontSize = this.data.subtitlesFontSize ?? 20;
this.subtitlesFontSizeSliderLabel = new SliderLabel({
labelText: localizationProvider.get("VOTSubtitlesFontSize"),
labelEOL: ":",
value: subtitlesFontSize,
symbol: "px"
});
this.subtitlesFontSizeSlider = new Slider({
labelHtml: this.subtitlesFontSizeSliderLabel.container,
value: subtitlesFontSize,
min: 8,
max: 50
});
const storedSubtitlesFontFamily = typeof this.data.subtitlesFontFamily === "string" ? this.data.subtitlesFontFamily : void 0;
const subtitlesFontFamily = storedSubtitlesFontFamily && (isBuiltInSubtitleFontFamily(storedSubtitlesFontFamily) || getGoogleSubtitleFontFamilyName(storedSubtitlesFontFamily)) ? storedSubtitlesFontFamily : "default-sans";
this.subtitlesFontFamilySelectLabel = new Label({ labelText: localizationProvider.get("VOTSubtitlesFont") });
this.subtitlesFontFamilySelect = new Select({
selectTitle: getSubtitleFontFamilyLabel(subtitlesFontFamily),
dialogTitle: localizationProvider.get("VOTSubtitlesFont"),
dialogParent: this.globalPortal,
labelElement: this.subtitlesFontFamilySelectLabel.container,
items: this.buildSubtitleFontItems(subtitlesFontFamily),
searchItemsProvider: (query) => this.searchSubtitleFontItems(query, subtitlesFontFamily)
});
this.subtitlesFontFamilySelect.addEventListener("selectItem", (item) => {
if (!this.subtitlesFontFamilySelect) return;
this.subtitlesFontFamilySelect.updateItems(this.buildSubtitleFontItems(item));
this.subtitlesFontFamilySelect.selectTitle = getSubtitleFontFamilyLabel(item);
});
const subtitlesOpacity = this.data.subtitlesOpacity ?? 20;
this.subtitlesBackgroundOpacitySliderLabel = new SliderLabel({
labelText: localizationProvider.get("VOTSubtitlesOpacity"),
labelEOL: ":",
value: subtitlesOpacity,
symbol: "%"
});
this.subtitlesBackgroundOpacitySlider = new Slider({
labelHtml: this.subtitlesBackgroundOpacitySliderLabel.container,
value: subtitlesOpacity,
min: 0,
max: 100
});
subtitlesSection.content.append(this.responseLanguageSubtitlesSelect.container, this.subtitlesDownloadFormatSelect.container, this.subtitlesFontFamilySelect.container, this.subtitlesHighlightWordsCheckbox.container, this.subtitlesSmartLayoutCheckbox.container, this.subtitlesMaxLengthSlider.container, this.subtitlesFontSizeSlider.container, this.subtitlesBackgroundOpacitySlider.container);
this.translateHotkeyButton = new HotkeyButton({
labelHtml: localizationProvider.get("translateVideo"),
key: this.data.translationHotkey
});
this.subtitlesHotkeyButton = new HotkeyButton({
labelHtml: localizationProvider.get("VOTSubtitles"),
key: this.data.subtitlesHotkey
});
hotkeysSection.content.append(this.translateHotkeyButton.container, this.subtitlesHotkeyButton.container);
this.proxyWorkerHostTextfield = new Textfield({
labelHtml: localizationProvider.get("VOTProxyWorkerHost"),
value: this.data.proxyWorkerHost,
placeholder: proxyWorkerHost
});
const proxyEnabledLabels = [
localizationProvider.get("VOTTranslateProxyDisabled"),
localizationProvider.get("VOTTranslateProxyEnabled"),
localizationProvider.get("VOTTranslateProxyEverything")
];
const translateProxyEnabled = this.data.translateProxyEnabled ?? 0;
const isTranslateProxyRequired = countryCode && proxyOnlyCountries.includes(countryCode);
this.proxyTranslationStatusSelectLabel = new Label({
icon: isTranslateProxyRequired ? WARNING_ICON : void 0,
labelText: localizationProvider.get("VOTTranslateProxyStatus")
});
if (isTranslateProxyRequired) this.proxyTranslationStatusSelectTooltip = new Tooltip({
target: this.proxyTranslationStatusSelectLabel.icon,
content: localizationProvider.get("VOTTranslateProxyStatusDefault"),
position: "bottom",
backgroundColor: "var(--vot-helper-ondialog)",
parentElement: this.globalPortal
});
this.proxyTranslationStatusSelect = new Select({
selectTitle: proxyEnabledLabels[translateProxyEnabled],
dialogTitle: localizationProvider.get("VOTTranslateProxyStatus"),
dialogParent: this.globalPortal,
labelElement: this.proxyTranslationStatusSelectLabel.container,
items: proxyEnabledLabels.map((label, idx) => ({
label,
value: idx.toString(),
selected: idx === translateProxyEnabled,
disabled: idx === 0 && isProxyOnlyExtension
}))
});
proxySection.content.append(this.proxyWorkerHostTextfield.container, this.proxyTranslationStatusSelect.container);
this.translateAPIErrorsCheckbox = new Checkbox({
labelHtml: localizationProvider.get("VOTTranslateAPIErrors"),
checked: this.data.translateAPIErrors ?? true
});
this.translateAPIErrorsCheckbox.hidden = localizationProvider.lang === "ru";
this.useNewAudioPlayerCheckbox = new Checkbox({
labelHtml: localizationProvider.get("VOTNewAudioPlayer"),
checked: this.data.newAudioPlayer
});
if (!this.videoHandler?.isAudioContextSupported) {
this.useNewAudioPlayerCheckbox.disabled = true;
this.useNewAudioPlayerTooltip = new Tooltip({
target: this.useNewAudioPlayerCheckbox.container,
content: localizationProvider.get("VOTNeedWebAudioAPI"),
position: "bottom",
backgroundColor: "var(--vot-helper-ondialog)",
parentElement: this.globalPortal
});
}
this.onlyBypassMediaCSPCheckbox = new Checkbox({
labelHtml: this.videoHandler?.site.needBypassCSP ? `${localizationProvider.get("VOTOnlyBypassMediaCSP")} (${localizationProvider.get("VOTMediaCSPEnabledOnSite")})` : localizationProvider.get("VOTOnlyBypassMediaCSP"),
checked: this.data.onlyBypassMediaCSP,
isSubCheckbox: true
});
if (!this.videoHandler?.isAudioContextSupported) this.onlyBypassMediaCSPTooltip = new Tooltip({
target: this.onlyBypassMediaCSPCheckbox.container,
content: localizationProvider.get("VOTNeedWebAudioAPI"),
position: "bottom",
backgroundColor: "var(--vot-helper-ondialog)",
parentElement: this.globalPortal
});
this.onlyBypassMediaCSPCheckbox.disabled = !this.data.newAudioPlayer && !!this.videoHandler?.isAudioContextSupported;
if (!this.data.newAudioPlayer) this.onlyBypassMediaCSPCheckbox.hidden = true;
this.translationTextServiceLabel = new Label({
labelText: localizationProvider.get("VOTTranslationTextService"),
icon: HELP_ICON
});
const translationService = this.data.translationService ?? "yandexbrowser";
this.translationTextServiceSelect = new Select({
selectTitle: localizationProvider.get(`services.${translationService}`),
dialogTitle: localizationProvider.get("VOTTranslationTextService"),
dialogParent: this.globalPortal,
labelElement: this.translationTextServiceLabel.container,
items: foswlyServices.map((service) => ({
label: localizationProvider.get(`services.${service}`),
value: service,
selected: service === translationService
}))
});
this.translationTextServiceTooltip = new Tooltip({
target: this.translationTextServiceLabel.icon,
content: localizationProvider.get("VOTNotAffectToVoice"),
position: "bottom",
backgroundColor: "var(--vot-helper-ondialog)",
parentElement: this.globalPortal
});
this.detectServiceLabel = new Label({ labelText: localizationProvider.get("VOTDetectService") });
const detectService = this.data.detectService ?? "yandexbrowser";
this.detectServiceSelect = new Select({
selectTitle: localizationProvider.get(`services.${detectService}`),
dialogTitle: localizationProvider.get("VOTDetectService"),
dialogParent: this.globalPortal,
labelElement: this.detectServiceLabel.container,
items: detectServices.map((service) => ({
label: localizationProvider.get(`services.${service}`),
value: service,
selected: service === detectService
}))
});
this.showPiPButtonCheckbox = new Checkbox({
labelHtml: localizationProvider.get("VOTShowPiPButton"),
checked: this.data.showPiPButton
});
this.showPiPButtonCheckbox.hidden = !isPiPAvailable();
const autoHideButtonDelaySec = Math.round((this.data.autoHideButtonDelay ?? 1e3) / 1e3 * 10) / 10;
this.autoHideButtonDelaySliderLabel = new SliderLabel({
labelText: localizationProvider.get("autoHideButtonDelay"),
labelEOL: ":",
value: autoHideButtonDelaySec,
symbol: ` ${localizationProvider.get("secs")}`
});
this.autoHideButtonDelaySlider = new Slider({
labelHtml: this.autoHideButtonDelaySliderLabel.container,
value: autoHideButtonDelaySec,
min: .1,
max: 3,
step: .1
});
this.buttonPositionSelectLabel = new Label({
labelText: localizationProvider.get("buttonPosition"),
icon: HELP_ICON
});
const buttonPos = this.data.buttonPos ?? "default";
this.buttonPositionSelect = new Select({
selectTitle: localizationProvider.get(`position.${buttonPos}`),
dialogTitle: localizationProvider.get("buttonPosition"),
labelElement: this.buttonPositionSelectLabel.container,
dialogParent: this.globalPortal,
items: positions.map((position) => ({
label: localizationProvider.get(`position.${position}`),
value: position,
selected: position === buttonPos
}))
});
this.buttonPositionTooltip = new Tooltip({
target: this.buttonPositionSelectLabel.icon,
content: localizationProvider.get("minButtonPositionContainer"),
position: "bottom",
backgroundColor: "var(--vot-helper-ondialog)",
parentElement: this.globalPortal
});
this.menuLanguageSelectLabel = new Label({ labelText: localizationProvider.get("VOTMenuLanguage") });
this.menuLanguageSelect = new Select({
selectTitle: localizationProvider.get(`langs.${localizationProvider.langOverride}`),
dialogTitle: localizationProvider.get("VOTMenuLanguage"),
labelElement: this.menuLanguageSelectLabel.container,
dialogParent: this.globalPortal,
items: Select.genLanguageItems(localizationProvider.getAvailableLangs(), localizationProvider.langOverride)
});
this.bugReportButton = UI.createOutlinedButton(localizationProvider.get("VOTBugReport"));
this.resetSettingsButton = UI.createButton(localizationProvider.get("resetSettings"));
miscSection.content.append(this.translateAPIErrorsCheckbox.container, this.useNewAudioPlayerCheckbox.container, this.onlyBypassMediaCSPCheckbox.container);
translationSection.content.append(this.translationTextServiceSelect.container, this.detectServiceSelect.container);
appearanceSection.content.append(this.showPiPButtonCheckbox.container, this.autoHideButtonDelaySlider.container, this.buttonPositionSelect.container, this.menuLanguageSelect.container);
const envInfo = getEnvironmentInfo();
const versionInfo = UI.createInformation(`${localizationProvider.get("VOTVersion")}:`, envInfo.scriptVersion || GM_info.script.version || localizationProvider.get("notFound"));
const buildAuthors = String("Toil, SashaXser, MrSoczekXD, mynovelhost, sodapng");
const authorsInfo = UI.createInformation(`${localizationProvider.get("VOTAuthors")}:`, GM_info.script.author || buildAuthors || localizationProvider.get("notFound"));
const loaderInfo = UI.createInformation(`${localizationProvider.get("VOTLoader")}:`, envInfo.loader);
const userBrowserInfo = UI.createInformation(`${localizationProvider.get("VOTBrowser")}:`, `${envInfo.browser} (${envInfo.os})`);
const localeUpdatedAt = (/* @__PURE__ */ new Date((this.data.localeUpdatedAt ?? 0) * 1e3)).toLocaleString();
const localeInfoValue = b`${this.data.localeHash ?? localizationProvider.get("notFound")}<br />(${localizationProvider.get("VOTUpdatedAt")}
${localeUpdatedAt})`;
const localeInfo = UI.createInformation(`${localizationProvider.get("VOTLocaleHash")}:`, localeInfoValue);
const updateLocaleFilesButton = UI.createOutlinedButton(localizationProvider.get("VOTUpdateLocaleFiles"));
updateLocaleFilesButton.addEventListener("click", async () => {
await votStorage.set("localeHash", "");
await localizationProvider.update(true);
globalThis.location.reload();
});
aboutSection.content.append(versionInfo.container, authorsInfo.container, loaderInfo.container, userBrowserInfo.container, localeInfo.container, updateLocaleFilesButton);
this.dialog.footerContainer.append(this.bugReportButton, this.resetSettingsButton);
this.initialized = true;
return this;
}
initUIEvents() {
if (!this.isInitialized()) throw new Error("[VOT] SettingsView isn't initialized");
globalThis.addEventListener("message", this.onAuthRefreshMessage);
this.accountButton.addEventListener("click", async () => {
if (votStorage.isSupportOnlyLS) return;
if (this.accountButton.loggedIn) {
await votStorage.delete("account");
this.data.account = {};
return this.updateAccountInfo();
}
globalThis.open(authLoginUrl, "_blank")?.focus();
});
this.accountButton.addEventListener("click:secret", async () => {
const dialog = new Dialog({
titleHtml: localizationProvider.get("VOTLoginViaToken"),
isTemp: true
});
this.globalPortal.appendChild(dialog.container);
const tokenInfoEl = UI.createEl("vot-block", void 0, localizationProvider.get("VOTYandexTokenInfo"));
const tokenTextfield = new Textfield({
labelHtml: localizationProvider.get("VOTYandexToken"),
value: this.data.account?.token
});
tokenTextfield.addEventListener("change", async (token) => {
this.data.account = token ? {
expires: Date.now() + 3153418e4,
token
} : {};
await votStorage.set("account", this.data.account);
this.updateAccountInfo();
});
dialog.bodyContainer.append(tokenInfoEl, tokenTextfield.container);
dialog.open();
});
this.accountButton.addEventListener("refresh", async () => {
await this.refreshAccountFromStorage();
});
this.bindAccountStorageListener();
this.bindPersistedSetting({
control: this.autoTranslateCheckbox,
event: "change",
apply: (checked) => {
this.data.autoTranslate = checked;
},
storageKey: "autoTranslate",
readPersistedValue: () => this.data.autoTranslate,
logLabel: "autoTranslate",
dispatch: (checked) => this.events["change:autoTranslate"].dispatch(checked)
});
this.bindPersistedSetting({
control: this.autoSubtitlesCheckbox,
event: "change",
apply: (checked) => {
this.data.autoSubtitles = checked;
},
storageKey: "autoSubtitles",
readPersistedValue: () => this.data.autoSubtitles,
logLabel: "autoSubtitles",
dispatch: (checked) => this.events["change:autoSubtitles"].dispatch(checked)
});
this.dontTranslateLanguagesCheckbox.addEventListener("change", async (checked) => {
this.data.enabledDontTranslateLanguages = checked;
this.dontTranslateLanguagesSelect.disabled = !checked;
await votStorage.set("enabledDontTranslateLanguages", this.data.enabledDontTranslateLanguages);
debug.log("enabledDontTranslateLanguages value changed. New value:", checked);
});
this.dontTranslateLanguagesSelect.addEventListener("selectItem", async (values) => {
this.data.dontTranslateLanguages = values;
await votStorage.set("dontTranslateLanguages", this.data.dontTranslateLanguages);
debug.log("dontTranslateLanguages value changed. New value:", values);
});
this.bindPersistedSetting({
control: this.autoSetVolumeCheckbox,
event: "change",
apply: (checked) => {
this.data.enabledAutoVolume = checked;
this.autoSetVolumeSlider.disabled = !checked;
this.smartDuckingCheckbox.disabled = !checked || Boolean(this.syncVolumeCheckbox?.checked);
},
storageKey: "enabledAutoVolume",
readPersistedValue: () => this.data.enabledAutoVolume,
logLabel: "enabledAutoVolume",
afterPersist: async () => this.videoHandler?.setupAudioSettings?.()
});
this.bindPersistedSetting({
control: this.smartDuckingCheckbox,
event: "change",
apply: (checked) => {
this.data.enabledSmartDucking = checked;
},
storageKey: "enabledSmartDucking",
readPersistedValue: () => this.data.enabledSmartDucking,
logLabel: "enabledSmartDucking",
afterPersist: async () => this.videoHandler?.setupAudioSettings?.()
});
this.bindPersistedSetting({
control: this.autoSetVolumeSlider,
event: "input",
apply: (value) => {
this.data.autoVolume = this.autoSetVolumeSliderLabel.value = value;
},
storageKey: "autoVolume",
readPersistedValue: () => this.data.autoVolume,
logLabel: "autoVolume"
});
this.bindPersistedSetting({
control: this.showVideoVolumeSliderCheckbox,
event: "change",
apply: (checked) => {
this.data.showVideoSlider = checked;
},
storageKey: "showVideoSlider",
readPersistedValue: () => this.data.showVideoSlider,
logLabel: "showVideoVolumeSlider",
dispatch: (checked) => this.events["change:showVideoVolume"].dispatch(checked)
});
this.bindPersistedSetting({
control: this.audioBoosterCheckbox,
event: "change",
apply: (checked) => {
this.data.audioBooster = checked;
},
storageKey: "audioBooster",
readPersistedValue: () => this.data.audioBooster,
logLabel: "audioBooster",
dispatch: (checked) => this.events["change:audioBooster"].dispatch(checked)
});
this.bindPersistedSetting({
control: this.syncVolumeCheckbox,
event: "change",
apply: (checked) => {
this.data.syncVolume = checked;
this.autoSetVolumeSlider.disabled = !this.autoSetVolumeCheckbox?.checked;
this.smartDuckingCheckbox.disabled = checked || !this.autoSetVolumeCheckbox?.checked;
if (checked && this.smartDuckingCheckbox?.checked) this.smartDuckingCheckbox.checked = false;
},
storageKey: "syncVolume",
readPersistedValue: () => this.data.syncVolume,
logLabel: "syncVolume",
dispatch: (checked) => this.events["change:syncVolume"].dispatch(checked)
});
this.bindPersistedSetting({
control: this.downloadWithNameCheckbox,
event: "change",
apply: (checked) => {
this.data.downloadWithName = checked;
},
storageKey: "downloadWithName",
readPersistedValue: () => this.data.downloadWithName,
logLabel: "downloadWithName"
});
this.bindPersistedSetting({
control: this.sendNotifyOnCompleteCheckbox,
event: "change",
apply: (checked) => {
this.data.sendNotifyOnComplete = checked;
},
storageKey: "sendNotifyOnComplete",
readPersistedValue: () => this.data.sendNotifyOnComplete,
logLabel: "sendNotifyOnComplete"
});
this.bindPersistedSetting({
control: this.useLivelyVoiceCheckbox,
event: "change",
apply: (checked) => {
this.data.useLivelyVoice = checked;
},
storageKey: "useLivelyVoice",
readPersistedValue: () => this.data.useLivelyVoice,
logLabel: "useLivelyVoice",
dispatch: (checked) => this.events["change:useLivelyVoice"].dispatch(checked)
});
this.bindPersistedSetting({
control: this.useAudioDownloadCheckbox,
event: "change",
apply: (checked) => {
this.data.useAudioDownload = checked;
},
storageKey: "useAudioDownload",
readPersistedValue: () => this.data.useAudioDownload,
logLabel: "useAudioDownload"
});
this.bindPersistedSetting({
control: this.responseLanguageSubtitlesSelect,
event: "selectItem",
apply: (item) => {
this.data.responseLanguageSubtitles = item;
this.responseLanguageSubtitlesSelect?.updateItems(buildSubtitleLanguageSettingItems(item));
if (this.responseLanguageSubtitlesSelect) this.responseLanguageSubtitlesSelect.selectTitle = getSubtitleLanguageSettingLabel(item);
},
storageKey: "responseLanguageSubtitles",
readPersistedValue: () => this.data.responseLanguageSubtitles,
logLabel: "responseLanguageSubtitles",
dispatch: (item) => this.events["select:responseLanguageSubtitles"].dispatch(item)
});
this.bindPersistedSetting({
control: this.subtitlesDownloadFormatSelect,
event: "selectItem",
apply: (item) => {
this.data.subtitlesDownloadFormat = item;
},
storageKey: "subtitlesDownloadFormat",
readPersistedValue: () => this.data.subtitlesDownloadFormat,
logLabel: "subtitlesDownloadFormat"
});
this.bindPersistedSetting({
control: this.subtitlesHighlightWordsCheckbox,
event: "change",
apply: (checked) => {
this.data.highlightWords = checked;
},
storageKey: "highlightWords",
readPersistedValue: () => this.data.highlightWords,
logLabel: "highlightWords",
dispatch: (checked) => this.events["change:subtitlesHighlightWords"].dispatch(checked)
});
this.subtitlesSmartLayoutCheckbox?.addEventListener("change", (checked) => {
if (this.suppressSubtitlesSmartLayoutCheckboxChange) return;
this.setSubtitlesSmartLayout(checked);
});
const disableSmartLayout = () => {
if ((this.data.subtitlesSmartLayout ?? true) === true) this.setSubtitlesSmartLayout(false);
};
this.bindBufferedNumericSetting({
control: this.subtitlesMaxLengthSlider,
label: this.subtitlesMaxLengthSliderLabel,
storageKey: "subtitlesMaxLength",
logLabel: "subtitlesMaxLength",
beforeApply: disableSmartLayout,
dispatch: (value) => this.events["input:subtitlesMaxLength"].dispatch(value)
});
this.bindBufferedNumericSetting({
control: this.subtitlesFontSizeSlider,
label: this.subtitlesFontSizeSliderLabel,
storageKey: "subtitlesFontSize",
logLabel: "subtitlesFontSize",
beforeApply: disableSmartLayout,
dispatch: (value) => this.events["input:subtitlesFontSize"].dispatch(value)
});
this.bindBufferedNumericSetting({
control: this.subtitlesBackgroundOpacitySlider,
label: this.subtitlesBackgroundOpacitySliderLabel,
storageKey: "subtitlesOpacity",
logLabel: "subtitlesOpacity",
dispatch: (value) => this.events["input:subtitlesBackgroundOpacity"].dispatch(value)
});
this.bindPersistedSetting({
control: this.subtitlesFontFamilySelect,
event: "selectItem",
apply: (item) => {
this.data.subtitlesFontFamily = item;
},
storageKey: "subtitlesFontFamily",
readPersistedValue: () => this.data.subtitlesFontFamily,
logLabel: "subtitlesFontFamily",
dispatch: (item) => this.events["select:subtitlesFontFamily"].dispatch(item)
});
this.bindPersistedSetting({
control: this.translateHotkeyButton,
event: "change",
apply: (key) => {
this.data.translationHotkey = key;
},
storageKey: "translationHotkey",
readPersistedValue: () => this.data.translationHotkey,
logLabel: "translationHotkey"
});
this.bindPersistedSetting({
control: this.subtitlesHotkeyButton,
event: "change",
apply: (key) => {
this.data.subtitlesHotkey = key;
},
storageKey: "subtitlesHotkey",
readPersistedValue: () => this.data.subtitlesHotkey,
logLabel: "subtitlesHotkey"
});
this.proxyWorkerHostTextfield.addEventListener("change", async (value) => {
this.data.proxyWorkerHost = value || "vot-worker.eu.cc";
await votStorage.set("proxyWorkerHost", this.data.proxyWorkerHost);
debug.log("proxyWorkerHost value changed. New value:", this.data.proxyWorkerHost);
this.events["change:proxyWorkerHost"].dispatch(value);
});
this.proxyTranslationStatusSelect.addEventListener("selectItem", async (item) => {
this.data.translateProxyEnabled = Number.parseInt(item, 10);
await votStorage.set("translateProxyEnabled", this.data.translateProxyEnabled);
await votStorage.set("translateProxyEnabledDefault", false);
debug.log("translateProxyEnabled value changed. New value:", this.data.translateProxyEnabled);
this.events["select:proxyTranslationStatus"].dispatch(item);
});
this.bindPersistedSetting({
control: this.translateAPIErrorsCheckbox,
event: "change",
apply: (checked) => {
this.data.translateAPIErrors = checked;
},
storageKey: "translateAPIErrors",
readPersistedValue: () => this.data.translateAPIErrors,
logLabel: "translateAPIErrors"
});
this.bindPersistedSetting({
control: this.useNewAudioPlayerCheckbox,
event: "change",
apply: (checked) => {
this.data.newAudioPlayer = checked;
this.onlyBypassMediaCSPCheckbox.disabled = this.onlyBypassMediaCSPCheckbox.hidden = !checked;
},
storageKey: "newAudioPlayer",
readPersistedValue: () => this.data.newAudioPlayer,
logLabel: "newAudioPlayer",
dispatch: (checked) => this.events["change:useNewAudioPlayer"].dispatch(checked)
});
this.bindPersistedSetting({
control: this.onlyBypassMediaCSPCheckbox,
event: "change",
apply: (checked) => {
this.data.onlyBypassMediaCSP = checked;
},
storageKey: "onlyBypassMediaCSP",
readPersistedValue: () => this.data.onlyBypassMediaCSP,
logLabel: "onlyBypassMediaCSP",
dispatch: (checked) => this.events["change:onlyBypassMediaCSP"].dispatch(checked)
});
this.bindPersistedSetting({
control: this.translationTextServiceSelect,
event: "selectItem",
apply: (item) => {
this.data.translationService = item;
},
storageKey: "translationService",
readPersistedValue: () => this.data.translationService,
logLabel: "translationService",
dispatch: (item) => this.events["select:translationTextService"].dispatch(item)
});
this.bindPersistedSetting({
control: this.detectServiceSelect,
event: "selectItem",
apply: (item) => {
this.data.detectService = item;
},
storageKey: "detectService",
readPersistedValue: () => this.data.detectService,
logLabel: "detectService"
});
this.bindPersistedSetting({
control: this.showPiPButtonCheckbox,
event: "change",
apply: (checked) => {
this.data.showPiPButton = checked;
},
storageKey: "showPiPButton",
readPersistedValue: () => this.data.showPiPButton,
logLabel: "showPiPButton",
dispatch: (checked) => this.events["change:showPiPButton"].dispatch(checked)
});
this.bindBufferedNumericSetting({
control: this.autoHideButtonDelaySlider,
label: this.autoHideButtonDelaySliderLabel,
storageKey: "autoHideButtonDelay",
logLabel: "autoHideButtonDelay",
toStoredValue: (value) => Math.round(value * 1e3),
dispatch: (value) => this.events["input:autoHideButtonDelay"].dispatch(value)
});
this.bindPersistedSetting({
control: this.buttonPositionSelect,
event: "selectItem",
apply: (item) => {
this.data.buttonPos = item;
},
storageKey: "buttonPos",
readPersistedValue: () => this.data.buttonPos,
logLabel: "buttonPos",
dispatch: (item) => this.events["select:buttonPosition"].dispatch(item)
});
this.menuLanguageSelect.addEventListener("selectItem", async (item) => {
if (!await localizationProvider.changeLang(item)) return;
this.data.localeUpdatedAt = await votStorage.get("localeUpdatedAt", 0);
this.events["select:menuLanguage"].dispatch(item);
});
this.bugReportButton.addEventListener("click", () => this.events["click:bugReport"].dispatch());
this.resetSettingsButton.addEventListener("click", () => this.events["click:resetSettings"].dispatch());
return this;
}
addEventListener(type, listener) {
this.events[type].addListener(listener);
return this;
}
removeEventListener(type, listener) {
this.events[type].removeListener(listener);
return this;
}
doReleaseUI() {
this.dialog?.remove();
for (const tooltip of [
this.accountButtonRefreshTooltip,
this.accountButtonTokenTooltip,
this.audioBoosterTooltip,
this.useLivelyVoiceTooltip,
this.useAudioDownloadCheckboxTooltip,
this.useNewAudioPlayerTooltip,
this.onlyBypassMediaCSPTooltip,
this.translationTextServiceTooltip,
this.proxyTranslationStatusSelectTooltip,
this.buttonPositionTooltip
]) tooltip?.release();
}
doReleaseUIEvents() {
this.accountStorageListenerCleanup?.();
this.accountStorageListenerCleanup = void 0;
globalThis.removeEventListener("message", this.onAuthRefreshMessage);
this.flushStoragePersists();
for (const event of Object.values(this.events)) event.clear();
}
release() {
if (!this.isInitialized()) return this;
this.doReleaseUIEvents();
this.doReleaseUI();
this.initialized = false;
return this;
}
updateAccountInfo() {
if (!this.isInitialized()) throw new Error("[VOT] SettingsView isn't initialized");
const loggedIn = !!this.data.account?.token;
this.accountButton.avatarId = this.data.account?.avatarId;
this.useLivelyVoiceTooltip.hidden = this.accountButton.loggedIn = loggedIn;
this.accountButton.username = this.data.account?.username;
this.useLivelyVoiceCheckbox.disabled = !loggedIn;
this.events["update:account"].dispatch(this.data.account);
return this;
}
open() {
if (!this.isInitialized()) throw new Error("[VOT] SettingsView isn't initialized");
return this.dialog.open();
}
close() {
if (!this.isInitialized()) throw new Error("[VOT] SettingsView isn't initialized");
return this.dialog.close();
}
};
//#endregion
//#region src/ui/manager.ts
var UIManager = class {
mount;
initialized = false;
videoHandler;
intervalIdleChecker;
data;
votGlobalPortal;
globalPortalMount;
/**
* Contains all elements over video player e.g. button, menu and etc
*/
votOverlayView;
/**
* Dialog settings menu
*/
votSettingsView;
constructor({ mount, data = {}, videoHandler, intervalIdleChecker }) {
this.mount = mount;
this.videoHandler = videoHandler;
this.data = data;
this.intervalIdleChecker = intervalIdleChecker;
}
get root() {
return this.mount.root;
}
get portalContainer() {
return this.mount.portalContainer;
}
getSubtitlesMountContainer() {
return this.votOverlayView?.root ?? this.mount.subtitlesMountContainer;
}
isInitialized() {
return this.initialized;
}
initUI() {
if (this.isInitialized()) throw new Error("[VOT] UIManager is already initialized");
this.initialized = true;
this.globalPortalMount = createShadowMount({
parent: this.getGlobalPortalHost(this.mount),
rootClasses: ["vot-portal"]
});
this.votGlobalPortal = this.globalPortalMount.root;
this.votOverlayView = new OverlayView({
mount: this.mount,
globalPortal: this.votGlobalPortal,
data: this.data,
videoHandler: this.videoHandler,
intervalIdleChecker: this.intervalIdleChecker
});
this.votOverlayView.initUI(this.data.buttonPos ?? "default");
this.votSettingsView = new SettingsView({
globalPortal: this.votGlobalPortal,
data: this.data,
videoHandler: this.videoHandler
});
this.votSettingsView.initUI();
this.videoHandler?.subtitlesWidget?.updateMount({ container: this.getSubtitlesMountContainer() });
return this;
}
updateMount(mount) {
reparentShadowMount(this.globalPortalMount, this.getGlobalPortalHost(mount));
this.mount = applyOverlayMountUpdate(this.mount, mount, (nextMount) => {
this.votOverlayView?.updateMount(nextMount);
});
this.videoHandler?.subtitlesWidget?.updateMount({ container: this.getSubtitlesMountContainer() });
return this;
}
getGlobalPortalHost(_mount) {
const fullscreenInfo = this.videoHandler?.fullscreenHelper?.getFullscreenInfo();
if (fullscreenInfo?.element && fullscreenInfo.belongsToCurrentVideo) return fullscreenInfo.shadowRoot ?? fullscreenInfo.element;
return document.documentElement;
}
initUIEvents() {
if (!this.isInitialized()) throw new Error("[VOT] UIManager isn't initialized");
this.votOverlayView.initUIEvents();
this.bindOverlayViewEvents();
this.votSettingsView.initUIEvents();
this.bindSettingsViewEvents();
}
bindOverlayViewEvents() {
const overlayView = this.votOverlayView;
if (!overlayView) return;
overlayView.addEventListener("click:translate", async () => {
await this.handleTranslationBtnClick();
}).addEventListener("click:pip", async () => {
if (!this.videoHandler) return;
try {
if (document.pictureInPictureElement != null) await document.exitPictureInPicture();
else await this.videoHandler.video.requestPictureInPicture();
} catch (err) {
debug.warn("[VOT] Failed to toggle Picture-in-Picture", err);
}
}).addEventListener("click:settings", async () => {
this.videoHandler?.subtitlesWidget?.releaseTooltip();
this.videoHandler?.overlayVisibility?.cancel();
this.videoHandler?.overlayVisibility?.show();
this.votSettingsView.open();
}).addEventListener("click:downloadTranslation", async () => {
await this.handleDownloadTranslationClick();
}).addEventListener("click:downloadSubtitles", async () => {
await this.handleDownloadSubtitlesClick();
}).addEventListener("input:videoVolume", (volume) => {
if (!this.videoHandler) return;
const nextVolume01 = volume / 100;
this.videoHandler.setVideoVolume(nextVolume01);
this.videoHandler.applyManualVideoVolumeOverride(nextVolume01);
if (!this.data.syncVolume) {
this.videoHandler.onVideoVolumeSliderSynced(volume);
return;
}
this.videoHandler.syncVolumeWrapper("video", volume);
}).addEventListener("input:translationVolume", (volume) => {
if (!this.videoHandler) return;
const nextVolume = volume ?? this.data.defaultVolume ?? 100;
safeSetPlayerVolume(this.videoHandler.audioPlayer.player, nextVolume / 100);
if (!this.data.syncVolume) {
this.videoHandler.onTranslationVolumeSliderSynced(nextVolume);
return;
}
const syncResult = this.videoHandler.syncVolumeWrapper("translation", nextVolume);
if (typeof syncResult?.nextVideo === "number") this.videoHandler.applyManualVideoVolumeOverride(syncResult.nextVideo / 100);
}).addEventListener("select:fromLanguage", async () => {
if (!this.videoHandler) return;
await this.videoHandler.refreshAutoSubtitlesForCurrentLangPair();
}).addEventListener("select:subtitles", (data) => {
if (!this.videoHandler) return;
this.runDetached(this.videoHandler.changeSubtitlesLang(data), "Failed to change subtitles language");
});
}
bindSettingsViewEvents() {
const settingsView = this.votSettingsView;
if (!settingsView) return;
settingsView.addEventListener("update:account", async (account) => {
if (!this.videoHandler) return;
this.videoHandler.votClient.apiToken = account?.token;
}).addEventListener("change:autoTranslate", async (checked) => {
const videoHandler = this.videoHandler;
if (checked && videoHandler && !videoHandler.hasActiveSource()) await this.handleTranslationBtnClick();
}).addEventListener("change:autoSubtitles", async (checked) => {
if (!checked || !this.videoHandler?.videoData?.videoId) return;
await this.videoHandler.refreshAutoSubtitlesForCurrentLangPair();
}).addEventListener("select:responseLanguageSubtitles", async () => {
if (!this.videoHandler?.data.autoSubtitles || !this.videoHandler.videoData) return;
await this.videoHandler.refreshAutoSubtitlesForCurrentLangPair();
}).addEventListener("change:showVideoVolume", () => {
this.withInitializedOverlayView((overlayView) => {
if (!overlayView.videoVolumeSlider || !overlayView.votButton) return;
overlayView.videoVolumeSlider.container.hidden = !this.data.showVideoSlider || overlayView.votButton.status !== "success";
});
}).addEventListener("change:audioBooster", async () => {
this.withInitializedOverlayView((overlayView) => {
if (!overlayView.translationVolumeSlider) return;
const currentVolume = overlayView.translationVolumeSlider.value;
const maxVolume = this.data.audioBooster && !this.data.syncVolume ? 900 : 100;
overlayView.translationVolumeSlider.max = maxVolume;
const nextVolume = clamp(currentVolume, 0, maxVolume);
overlayView.translationVolumeSlider.value = nextVolume;
this.videoHandler?.onTranslationVolumeSliderSynced(nextVolume);
this.videoHandler?.syncTranslationPlaybackVolume();
});
}).addEventListener("change:syncVolume", (checked) => {
if (!this.videoHandler) return;
this.videoHandler.setupAudioSettings();
this.withInitializedOverlayView((overlayView) => {
const videoSlider = overlayView.videoVolumeSlider;
const translationSlider = overlayView.translationVolumeSlider;
if (!videoSlider || !translationSlider) return;
const maxVolume = this.data.audioBooster && !checked ? 900 : 100;
translationSlider.max = maxVolume;
const nextTranslation = clamp(translationSlider.value, 0, maxVolume);
translationSlider.value = nextTranslation;
this.videoHandler.onTranslationVolumeSliderSynced(nextTranslation);
this.videoHandler.syncTranslationPlaybackVolume();
if (!checked) return;
this.videoHandler.resetVolumeLinkState(Number(videoSlider.value), nextTranslation);
});
}).addEventListener("change:useLivelyVoice", () => {
if (!this.videoHandler) return;
this.runDetached(this.videoHandler.stopTranslate(), "Failed to stop translation after voice mode change");
}).addEventListener("change:subtitlesHighlightWords", (checked) => {
this.updateSubtitlesWidgetSetting(checked, this.data.highlightWords, (widget, value) => {
widget.setHighlightWords(value);
});
}).addEventListener("change:subtitlesSmartLayout", (checked) => {
this.updateSubtitlesWidgetSetting(checked, this.data.subtitlesSmartLayout, (widget, value) => {
widget.setSmartLayout(value);
});
}).addEventListener("input:subtitlesMaxLength", (value) => {
this.updateSubtitlesWidgetSetting(value, this.data.subtitlesMaxLength, (widget, nextValue) => {
widget.setMaxLength(nextValue);
});
}).addEventListener("input:subtitlesFontSize", (value) => {
this.updateSubtitlesWidgetSetting(value, this.data.subtitlesFontSize, (widget, nextValue) => {
widget.setFontSize(nextValue);
});
}).addEventListener("select:subtitlesFontFamily", (item) => {
this.updateSubtitlesWidgetSetting(item, this.data.subtitlesFontFamily, (widget, nextValue) => {
widget.setFontFamily(nextValue);
});
}).addEventListener("input:subtitlesBackgroundOpacity", (value) => {
this.updateSubtitlesWidgetSetting(value, this.data.subtitlesOpacity, (widget, nextValue) => {
widget.setOpacity(nextValue);
});
}).addEventListener("change:proxyWorkerHost", (_value) => {
if (!this.videoHandler) return;
this.runDetached(this.videoHandler.handleProxySettingsChanged("proxyWorkerHost"), "Failed to apply proxyWorkerHost change");
}).addEventListener("select:proxyTranslationStatus", () => {
if (!this.videoHandler) return;
this.runDetached(this.videoHandler.handleProxySettingsChanged("proxyTranslationStatus"), "Failed to apply proxyTranslationStatus change");
}).addEventListener("change:useNewAudioPlayer", () => {
this.restartAudioPlayer();
}).addEventListener("change:onlyBypassMediaCSP", () => {
this.restartAudioPlayer();
}).addEventListener("select:translationTextService", () => {
this.withSubtitlesWidget((widget) => {
widget.resetTranslationContext(true);
});
}).addEventListener("change:showPiPButton", () => {
this.withInitializedOverlayView((overlayView) => {
if (!overlayView.votButton) return;
overlayView.votButton.pipButton.hidden = overlayView.votButton.separator2.hidden = !overlayView.pipButtonVisible;
});
}).addEventListener("select:buttonPosition", (item) => {
this.withInitializedOverlayView((overlayView) => {
const preferredPosition = this.data.buttonPos ?? item;
const { position, direction } = overlayView.calcButtonLayout(preferredPosition);
overlayView.updateButtonLayout(position, direction);
});
}).addEventListener("select:menuLanguage", async () => {
await this.reloadMenu();
}).addEventListener("click:bugReport", () => {
if (!this.videoHandler) return;
const params = new URLSearchParams(this.videoHandler.collectReportInfo()).toString();
globalThis.open(`${repositoryUrl}/issues/new?${params}`, "_blank")?.focus();
}).addEventListener("click:resetSettings", async () => {
const valuesForClear = await votStorage.list();
await Promise.all(valuesForClear.map((key) => votStorage.delete(key)));
await votStorage.set("compatVersion", actualCompatVersion);
globalThis.location.reload();
});
}
async handleDownloadTranslationClick() {
const overlayView = this.votOverlayView;
const videoHandler = this.videoHandler;
const download = videoHandler?.downloadTranslation;
if (!overlayView?.isInitialized() || !download || !videoHandler.videoData) return;
const downloadVideoData = await this.getDownloadVideoData(videoHandler, download.videoId);
if (!downloadVideoData) return;
const downloadButton = overlayView.downloadTranslationButton;
const downloadUrl = download.url;
const filename = this.data.downloadWithName ? clearFileName(downloadVideoData.downloadTitle) : `translation_${downloadVideoData.videoId}`;
const saveOptions = { preferShare: this.isLikelyMobileDownloadContext() };
const setProgress = (progress) => {
if (downloadButton) downloadButton.progress = progress;
};
setProgress(0);
try {
await this.downloadTranslationAudio(downloadUrl, filename, setProgress, saveOptions);
} catch (err) {
console.error("[VOT] Download translation failed:", err);
if (!this.triggerUrlDownload(downloadUrl, `${filename}.mp3`)) globalThis.open(downloadUrl, "_blank")?.focus();
} finally {
setProgress(0);
}
}
async getDownloadVideoData(videoHandler, downloadVideoId) {
if (videoHandler.videoData?.videoId !== downloadVideoId) {
this.clearDownloadTranslation(videoHandler);
return null;
}
let videoData;
try {
videoData = await videoHandler.getVideoData();
} catch (err) {
debug.log("[VOT] Failed to refresh video data before download", err);
return null;
}
if (videoData.videoId !== downloadVideoId) {
this.clearDownloadTranslation(videoHandler);
return null;
}
videoHandler.videoData = videoData;
return videoData;
}
clearDownloadTranslation(videoHandler) {
videoHandler.downloadTranslation = null;
if (this.votOverlayView?.downloadTranslationButton) this.votOverlayView.downloadTranslationButton.hidden = true;
}
async downloadTranslationAudio(downloadUrl, filename, onProgress, saveOptions) {
const response = await GM_fetch(downloadUrl, { timeout: 0 });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
await downloadTranslation(response, filename, onProgress, saveOptions);
}
async handleDownloadSubtitlesClick() {
const videoHandler = this.videoHandler;
if (!videoHandler?.yandexSubtitles || !videoHandler.videoData) return;
const subsFormat = this.data.subtitlesDownloadFormat ?? "json";
const subsContent = serializeProcessedSubtitles(videoHandler.yandexSubtitles, subsFormat, { assTitle: videoHandler.videoData.localizedTitle ?? videoHandler.videoData.title ?? videoHandler.videoData.downloadTitle });
await downloadBlob(new Blob([subsFormat === "json" ? JSON.stringify(subsContent) : subsContent], { type: "text/plain" }), `${this.data.downloadWithName ? clearFileName(videoHandler.videoData.downloadTitle) : `subtitles_${videoHandler.videoData.videoId}`}.${subsFormat}`, { preferShare: this.isLikelyMobileDownloadContext() });
}
async reloadMenu() {
if (!this.votOverlayView?.isInitialized()) throw new Error("[VOT] OverlayView isn't initialized");
const prevButtonOpacity = this.votOverlayView.votButton.opacity;
const prevButtonHidden = this.votOverlayView.votButton.container.hidden;
const prevMenuHidden = this.votOverlayView.votMenu.hidden;
const prevButtonPos = this.data.buttonPos ?? "default";
const settingsWasOpen = this.votSettingsView?.dialog?.container?.hidden === false;
await this.videoHandler?.stopTranslation();
this.release();
this.initUI();
this.initUIEvents();
if (!this.videoHandler) return this;
try {
const { position, direction } = this.votOverlayView.calcButtonLayout(prevButtonPos);
this.votOverlayView.updateButtonLayout(position, direction);
this.votOverlayView.votMenu.hidden = prevMenuHidden;
this.votOverlayView.votButton.container.hidden = prevButtonHidden;
this.votOverlayView.votButton.opacity = prevButtonOpacity;
} catch (err) {
debug.warn("[VOT] Failed to restore overlay state after menu reload", err);
}
try {
this.videoHandler.rebindOverlayVisibilityTargets();
} catch (err) {
debug.warn("[VOT] Failed to rebind overlay visibility targets", err);
}
if (settingsWasOpen) try {
this.votSettingsView?.open();
} catch (err) {
debug.warn("[VOT] Failed to reopen settings after menu reload", err);
}
await this.videoHandler.updateSubtitlesLangSelect();
const widget = this.videoHandler.subtitlesWidget;
if (widget) widget.resetTranslationContext(true);
return this;
}
async handleTranslationBtnClick() {
if (!this.votOverlayView?.isInitialized()) throw new Error("[VOT] OverlayView isn't initialized");
await handleTranslationButtonCommand({
videoHandler: this.videoHandler,
currentStatus: this.votOverlayView.votButton.status,
currentLoading: this.votOverlayView.votButton.loading,
transformBtn: (status, text) => {
this.transformBtn(status, text);
}
});
return this;
}
isLoadingText(text) {
const delayed = localizationProvider.get("TranslationDelayed");
return typeof text === "string" && (text.includes(localizationProvider.get("translationTake")) || (delayed ? text.includes(delayed) : false));
}
transformBtn(status, text) {
if (!this.votOverlayView?.isInitialized()) throw new Error("[VOT] OverlayView isn't initialized");
this.votOverlayView.votButton.status = status;
this.votOverlayView.votButton.loading = status === "error" && this.isLoadingText(text);
this.votOverlayView.votButton.setText(text);
this.votOverlayView.votButtonTooltip.setContent(text);
return this;
}
release() {
if (!this.isInitialized()) return this;
this.votOverlayView.release();
this.votSettingsView.release();
destroyShadowMount(this.globalPortalMount);
this.globalPortalMount = void 0;
this.votGlobalPortal = void 0;
this.initialized = false;
return this;
}
withInitializedOverlayView(callback) {
if (!this.votOverlayView?.isInitialized()) return;
callback(this.votOverlayView);
}
withSubtitlesWidget(callback) {
const widget = this.videoHandler?.subtitlesWidget;
if (!widget) return;
callback(widget);
}
updateSubtitlesWidgetSetting(nextValue, storedValue, apply) {
this.withSubtitlesWidget((widget) => {
apply(widget, storedValue ?? nextValue);
});
}
runDetached(task, errorMessage) {
task.catch((err) => {
debug.warn(`[VOT] ${errorMessage}`, err);
});
}
triggerUrlDownload(url, filename) {
try {
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.target = "_blank";
a.rel = "noopener noreferrer";
a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
return true;
} catch {
return false;
}
}
isLikelyMobileDownloadContext() {
if (this.videoHandler?.site.additionalData === "mobile") return true;
return typeof matchMedia === "function" && matchMedia("(pointer: coarse)").matches;
}
async restartAudioPlayer() {
const videoHandler = this.videoHandler;
if (!videoHandler) return;
try {
await videoHandler.stopTranslate();
videoHandler.createPlayer();
} catch (err) {
debug.warn("[VOT] Failed to restart audio player", err);
}
}
};
//#endregion
//#region src/ui/overlayVisibilityController.ts
/**
* Centralizes overlay visibility behavior: showing, hiding and deadline checks.
*/
var OverlayVisibilityController = class {
deps;
hideDeadlineMs = 0;
hideArmed = false;
unsubscribeChecker;
constructor(deps) {
this.deps = deps;
this.unsubscribeChecker = this.deps.checker.subscribe(() => {
this.onCheckerTick();
});
}
/**
* Ensures overlay is visible immediately and returns current view.
*/
show() {
const view = this.getView();
if (!view) return null;
view.updateButtonOpacity(1);
return view;
}
/**
* Cancels scheduled auto-hide.
*/
cancel() {
this.hideDeadlineMs = 0;
this.hideArmed = false;
}
release() {
this.cancel();
this.unsubscribeChecker();
}
/**
* Schedules overlay auto-hide after configured delay.
*/
queueAutoHide() {
if (!this.show()) return;
const delay = this.deps.getAutoHideDelay();
this.hideDeadlineMs = this.nowMs() + Math.max(0, delay);
this.hideArmed = true;
this.deps.checker.markActivity("overlay-queue-hide");
this.deps.checker.requestImmediateTick();
}
/**
* Handles pointer/focus interactions originating from overlay elements.
*/
handleOverlayInteraction(event) {
const type = event?.type;
if (!type) return;
if (type === "focusin") {
this.handleFocusIn();
return;
}
if (type.startsWith("pointer")) {
this.cancel();
this.show();
this.deps.checker.markActivity("overlay-interaction");
event.stopPropagation?.();
return;
}
this.handleHostInteraction(event);
}
/**
* Handles interactions from the broader host container (video, document etc.).
*/
handleHostInteraction(event) {
const type = event?.type;
if (!type) return;
if (type === "focusin") {
this.handleFocusIn();
return;
}
if (type.startsWith("pointer")) {
const target = event.target;
if (this.deps.isInteractiveNode(target)) event.stopPropagation?.();
this.deps.checker.markActivity("overlay-host-pointer");
}
this.queueAutoHide();
}
/**
* Schedules hide if focus leaves overlay tree entirely.
*/
scheduleHide(event) {
if (!this.getView()) return;
const currentTarget = event?.currentTarget;
let relatedTarget = event?.relatedTarget ?? null;
if (!relatedTarget && typeof event?.composedPath === "function") relatedTarget = event.composedPath()[1] ?? null;
const relatedNode = relatedTarget instanceof Node ? relatedTarget : null;
const currentNode = currentTarget instanceof Node ? currentTarget : null;
if (relatedNode && (currentNode && containsCrossShadow(currentNode, relatedNode) || this.deps.isInteractiveNode(relatedNode))) return;
this.queueAutoHide();
}
onCheckerTick() {
if (!this.hideArmed || this.hideDeadlineMs <= 0) return;
if (this.nowMs() + 2 < this.hideDeadlineMs) return;
this.hideArmed = false;
let active = null;
if (typeof document !== "undefined" && typeof document.hasFocus === "function" && document.hasFocus()) active = getDeepActiveElement(document);
if (active && this.deps.isInteractiveNode(active)) {
debug.log("[OverlayVisibility] skip hide (focus inside overlay)");
return;
}
this.getView()?.updateButtonOpacity(0);
}
handleFocusIn() {
this.cancel();
this.show();
this.deps.checker.markActivity("overlay-focus-in");
}
getView() {
const view = this.deps.getOverlayView();
return view?.isInitialized() ? view : null;
}
nowMs() {
if (this.deps.nowMs) return this.deps.nowMs();
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
}
};
//#endregion
//#region src/utils/intervalIdleChecker.ts
var DEFAULT_PROFILE = {
checkIntervalMs: 250,
idleAfterMs: 180
};
function normalizePositiveMs(value, fallback) {
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
return Math.max(1, Math.trunc(value));
}
function normalizeNonNegativeMs(value, fallback) {
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
return Math.max(0, Math.trunc(value));
}
function normalizeProfile(profile = {}) {
return {
checkIntervalMs: normalizePositiveMs(profile.checkIntervalMs, DEFAULT_PROFILE.checkIntervalMs),
idleAfterMs: normalizeNonNegativeMs(profile.idleAfterMs, DEFAULT_PROFILE.idleAfterMs)
};
}
function getDefaultRuntime() {
return {
nowMs: () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now(),
setInterval: globalThis.setInterval.bind(globalThis),
clearInterval: globalThis.clearInterval.bind(globalThis),
queueMicrotask: (fn) => {
if (typeof globalThis.queueMicrotask === "function") {
globalThis.queueMicrotask(fn);
return;
}
Promise.resolve().then(fn);
},
onVisibilityChange: (listener) => {
if (typeof document === "undefined" || typeof document.addEventListener !== "function") return () => void 0;
document.addEventListener("visibilitychange", listener);
return () => {
if (typeof document.removeEventListener === "function") document.removeEventListener("visibilitychange", listener);
};
}
};
}
var IntervalIdleChecker = class {
profile;
runtime;
subscribers = /* @__PURE__ */ new Set();
intervalId = null;
unsubscribeVisibilityChange = null;
running = false;
destroyed = false;
immediateQueued = false;
currentMode = "active";
lastActivityAt;
onVisibilityChangeHandler = () => {
if (this.destroyed || !this.running) return;
if (isDocumentHidden()) this.clearIntervalTimer();
else this.armInterval();
this.requestImmediateTick();
};
constructor(options = {}) {
this.profile = normalizeProfile(options.profile);
this.runtime = {
...getDefaultRuntime(),
...options.runtime
};
this.lastActivityAt = this.runtime.nowMs();
}
start() {
if (this.destroyed || this.running) return;
this.running = true;
this.lastActivityAt = this.runtime.nowMs();
this.subscribeVisibilityChange();
this.armInterval();
this.runTick("start");
}
stop() {
if (!this.running) return;
this.running = false;
this.clearIntervalTimer();
this.immediateQueued = false;
this.unsubscribeFromVisibilityChange();
}
destroy() {
if (this.destroyed) return;
this.stop();
this.subscribers.clear();
this.destroyed = true;
}
subscribe(fn) {
if (this.destroyed) return () => void 0;
this.subscribers.add(fn);
return () => {
this.subscribers.delete(fn);
};
}
markActivity(_source) {
if (this.destroyed) return;
this.lastActivityAt = this.runtime.nowMs();
if (!this.running) return;
const nextMode = this.resolveMode(this.lastActivityAt);
if (nextMode !== this.currentMode) this.currentMode = nextMode;
}
requestImmediateTick() {
if (this.destroyed || !this.running || this.immediateQueued) return;
this.immediateQueued = true;
this.runtime.queueMicrotask(() => {
this.immediateQueued = false;
if (this.destroyed || !this.running) return;
this.runTick("immediate");
});
}
resolveMode(nowMs) {
if (isDocumentHidden()) return "hidden";
return nowMs - this.lastActivityAt >= this.profile.idleAfterMs ? "idle" : "active";
}
clearIntervalTimer() {
if (this.intervalId === null) return;
this.runtime.clearInterval(this.intervalId);
this.intervalId = null;
}
armInterval() {
if (this.intervalId !== null) return;
this.intervalId = this.runtime.setInterval(() => {
this.runTick("interval");
}, this.profile.checkIntervalMs);
}
runTick(source) {
if (this.destroyed || !this.running) return;
if (this.subscribers.size === 0) return;
const nowMs = this.runtime.nowMs();
const nextMode = this.resolveMode(nowMs);
if (nextMode !== this.currentMode) this.currentMode = nextMode;
const ctx = {
nowMs,
mode: nextMode,
source
};
for (const sub of this.subscribers) try {
sub(ctx);
} catch {}
}
subscribeVisibilityChange() {
if (this.unsubscribeVisibilityChange !== null) return;
this.unsubscribeVisibilityChange = this.runtime.onVisibilityChange(this.onVisibilityChangeHandler);
}
unsubscribeFromVisibilityChange() {
if (this.unsubscribeVisibilityChange === null) return;
this.unsubscribeVisibilityChange();
this.unsubscribeVisibilityChange = null;
}
};
function createIntervalIdleChecker(profile) {
return new IntervalIdleChecker({ profile });
}
//#endregion
//#region src/utils/notify.ts
var now = () => Date.now();
function getScriptTitle() {
return GM_info?.script?.name || "VOT";
}
function safeL10n(key, fallback) {
try {
return localizationProvider?.get?.(key) || fallback;
} catch {
return fallback;
}
}
function canSend(lastSentAt, key, cooldownMs) {
if (!cooldownMs) return true;
const prev = lastSentAt.get(key) ?? 0;
return now() - prev >= cooldownMs;
}
function markSent(lastSentAt, key) {
lastSentAt.set(key, now());
}
function localizePhraseText(message) {
const key = message.trim();
if (!key) return null;
try {
const localized = localizationProvider.get(key);
const defaultText = localizationProvider.getDefault(key);
return localized !== key || defaultText !== key ? localized || defaultText || key : null;
} catch {
return null;
}
}
function resolveLocalizedErrorFromObject(message) {
if (!message || typeof message !== "object") return null;
const localizedError = message;
if (localizedError.name !== "VOTLocalizedError") return null;
if (typeof localizedError.localizedMessage === "string" && localizedError.localizedMessage.trim()) return localizedError.localizedMessage;
if (typeof localizedError.unlocalizedMessage === "string") return localizePhraseText(localizedError.unlocalizedMessage);
return null;
}
function localizeExtractedErrorMessage(message) {
const extracted = getErrorMessage(message);
if (!extracted) return null;
return localizePhraseText(extracted) || extracted;
}
function resolveLocalizedErrorMessage(message) {
const localizedObjectMessage = resolveLocalizedErrorFromObject(message);
if (localizedObjectMessage) return localizedObjectMessage;
if (typeof message === "string") {
const byPhraseKey = localizePhraseText(message);
if (byPhraseKey) return byPhraseKey;
}
const extractedMessage = localizeExtractedErrorMessage(message);
if (extractedMessage) return extractedMessage;
return safeL10n("requestTranslationFailed", "Translation failed");
}
function trySendViaUserscriptApi(details) {
try {
if (typeof GM_notification === "function") {
GM_notification(details);
return true;
}
const gmApi = globalThis.GM;
if (gmApi !== void 0 && typeof gmApi.notification === "function") {
const gmDetails = {
text: details.text,
title: details.title,
image: details.image,
onclick: details.onclick,
ondone: details.ondone
};
gmApi.notification(gmDetails);
return true;
}
} catch (err) {
debug.log("[notify] userscript api error", err);
}
return false;
}
/**
* Notification helper with dedupe/rate-limit and safe fallbacks.
*/
var Notifier = class {
lastSentAt = /* @__PURE__ */ new Map();
send(details, opts = {}) {
try {
const key = opts.key || details.tag || `${details.title ?? ""}|${details.text ?? ""}`;
const cooldownMs = opts.cooldownMs ?? 0;
if (!canSend(this.lastSentAt, key, cooldownMs)) return;
const normalized = {
...details,
title: details.title ?? getScriptTitle()
};
if (trySendViaUserscriptApi(normalized)) markSent(this.lastSentAt, key);
else debug.log("[notify] unavailable", normalized);
} catch (err) {
debug.log("[notify] send error", err);
}
}
translationCompleted(host) {
const text = safeL10n("VOTTranslationCompletedNotify", "The translation on the {0} has been completed!").replace("{0}", host);
this.send({
text,
title: getScriptTitle(),
timeout: 5e3,
silent: true,
tag: "VOTTranslationCompleted",
onclick: () => {
try {
globalThis.focus();
} catch {}
}
}, {
key: `translation_completed_${host}`,
cooldownMs: 1e4
});
}
translationFailed(params) {
const { videoId, message } = params;
if (isAbortError(message)) return;
const msg = resolveLocalizedErrorMessage(message);
const title = getScriptTitle();
this.send({
text: msg,
title,
timeout: 8e3,
silent: true,
tag: `VOTtranslationFailed_${videoId || "unknown"}`,
onclick: () => {
try {
globalThis.focus();
} catch {}
}
}, {
key: `translation_failed_${videoId || "unknown"}`,
cooldownMs: 3e4
});
}
};
//#endregion
//#region src/utils/domTraversal.ts
function getComposedParentElement(node) {
if (!node) return null;
const parentElement = node.parentElement ?? null;
if (parentElement) return parentElement;
if (typeof node.getRootNode !== "function") return null;
const root = node.getRootNode();
if (root && "host" in root) return root.host ?? null;
return null;
}
function someComposedAncestor(node, predicate) {
for (let parent = getComposedParentElement(node); parent; parent = getComposedParentElement(parent)) if (predicate(parent)) return true;
return false;
}
function isArrayLikeChildren(children) {
return "length" in children;
}
function walkShadowIncludingSubtree(root, adapter, visit) {
const stack = [root];
const { getChildren, getShadowRoot } = adapter;
let stackSize = 1;
while (stackSize > 0) {
const node = stack[stackSize - 1];
stackSize -= 1;
visit(node);
const children = getChildren(node);
if (isArrayLikeChildren(children)) {
const arrayLike = children;
for (let index = 0; index < arrayLike.length; index += 1) {
const child = arrayLike[index];
if (child !== void 0 && child !== null) {
stack[stackSize] = child;
stackSize += 1;
}
}
} else for (const child of children) if (child !== void 0 && child !== null) {
stack[stackSize] = child;
stackSize += 1;
}
const shadowRoot = getShadowRoot(node);
if (shadowRoot) {
stack[stackSize] = shadowRoot;
stackSize += 1;
}
}
}
//#endregion
//#region src/utils/VideoObserver.ts
var AD_ATTRS = [
"class",
"id",
"title"
];
var AD_KEYWORD_PATTERN = new RegExp([
"advertise",
"advertisement",
"promo",
"sponsor",
"banner",
"commercial",
"preroll",
"midroll",
"postroll",
"ad-container",
"sponsored"
].map((keyword) => keyword.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)).join("|"));
var ATTACH_SHADOW_HOOK_KEY = Symbol.for("vot.attachShadowHook");
function getAttachShadowDescriptor() {
const descriptor = Object.getOwnPropertyDescriptor(Element.prototype, "attachShadow");
if (!descriptor || typeof descriptor.value !== "function") return null;
return descriptor;
}
function getOrInstallAttachShadowHook() {
const g = globalThis;
const existing = g[ATTACH_SHADOW_HOOK_KEY];
if (existing?.descriptor && existing.subscribers instanceof Set) return existing;
const descriptor = getAttachShadowDescriptor();
if (!descriptor) return null;
const original = descriptor.value;
const state = {
descriptor,
subscribers: /* @__PURE__ */ new Set()
};
const patchedAttachShadow = function(init) {
const root = original.call(this, init);
for (const sub of state.subscribers) try {
sub(root);
} catch (error) {
debug.error("attachShadow subscriber failed", error);
}
return root;
};
try {
Object.defineProperty(Element.prototype, "attachShadow", {
...descriptor,
value: patchedAttachShadow
});
} catch {
return null;
}
g[ATTACH_SHADOW_HOOK_KEY] = state;
return state;
}
function removeAttachShadowSubscriber(subscriber) {
const g = globalThis;
const state = g[ATTACH_SHADOW_HOOK_KEY];
if (!state) return;
state.subscribers.delete(subscriber);
if (state.subscribers.size > 0) return;
try {
Object.defineProperty(Element.prototype, "attachShadow", state.descriptor);
} catch {
const original = state.descriptor.value;
if (typeof original === "function") Element.prototype.attachShadow = original;
}
delete g[ATTACH_SHADOW_HOOK_KEY];
}
var VideoObserver = class VideoObserver {
seenVideos = /* @__PURE__ */ new WeakSet();
activeVideos = /* @__PURE__ */ new WeakSet();
observedRoots = /* @__PURE__ */ new WeakSet();
videoListenerControllers = /* @__PURE__ */ new Map();
pendingAdded = /* @__PURE__ */ new Set();
pendingRemoved = /* @__PURE__ */ new Set();
flushPending = false;
static MAX_FLUSH_BUDGET_MS = 6;
static MAX_NODES_PER_SLICE = 120;
onVideoAdded = new EventImpl();
onVideoRemoved = new EventImpl();
observer = new MutationObserver((muts) => this.onMutations(muts));
intervalIdleChecker;
checkerUnsubscribe = null;
enabled = false;
attachShadowSubscriber = null;
onDocumentReady = null;
onPageShow = () => {
const root = document.documentElement;
if (!root) return;
this.pendingAdded.add(root);
this.scheduleFlush();
};
constructor(intervalIdleChecker = createIntervalIdleChecker()) {
this.intervalIdleChecker = intervalIdleChecker;
}
static containsAdKeyword(value) {
return value.length > 0 && AD_KEYWORD_PATTERN.test(value);
}
isAdRelated(element) {
for (const attr of AD_ATTRS) {
const rawValue = element.getAttribute(attr);
if (!rawValue) continue;
if (VideoObserver.containsAdKeyword(rawValue.toLowerCase())) return true;
}
return false;
}
isInsideAd(video) {
return someComposedAncestor(video, (p) => this.isAdRelated(p));
}
getCapturedAudioTrackCount(video) {
const candidate = video;
const captureStream = candidate.captureStream ?? candidate.mozCaptureStream;
if (typeof captureStream !== "function") return null;
try {
return captureStream.call(video).getAudioTracks().length;
} catch {
return null;
}
}
isLikelySilentDecorativeVideo(video) {
if (!(video.muted || video.defaultMuted)) return false;
if (!video.autoplay || !video.loop) return false;
if (video.controls) return false;
const v = video;
if (typeof v.mozHasAudio === "boolean") return !v.mozHasAudio;
if ("audioTracks" in v && typeof v.audioTracks?.length === "number") {
if (v.audioTracks.length > 0) return false;
const capturedTrackCount = this.getCapturedAudioTrackCount(video);
if (capturedTrackCount !== null) return capturedTrackCount === 0;
return true;
}
const capturedTrackCount = this.getCapturedAudioTrackCount(video);
if (capturedTrackCount !== null) return capturedTrackCount === 0;
return false;
}
hasAudio(video) {
const v = video;
if (video.srcObject instanceof MediaStream) return video.srcObject.getAudioTracks().length > 0;
if (typeof v.mozHasAudio === "boolean") return v.mozHasAudio;
if (typeof v.webkitAudioDecodedByteCount === "number" && v.webkitAudioDecodedByteCount > 0) return true;
if ("audioTracks" in v && typeof v.audioTracks?.length === "number") {
if (v.audioTracks.length > 0) return true;
}
if (this.isLikelySilentDecorativeVideo(video)) return false;
return true;
}
isValidVideo(video) {
if (this.isAdRelated(video)) return false;
if (this.isInsideAd(video)) return false;
if (!this.hasAudio(video)) {
debug.log("Ignoring video without audio:", video);
return false;
}
return true;
}
observeRoot(root) {
if (this.observedRoots.has(root)) return;
this.observedRoots.add(root);
this.observer.observe(root, {
childList: true,
subtree: true
});
}
static domAdapter = {
getChildren: (node) => Array.from(node.children ?? []),
getShadowRoot: (node) => node.shadowRoot
};
scan(root) {
if (root instanceof HTMLVideoElement) {
this.trackVideo(root);
return;
}
if (root.nodeType !== Node.ELEMENT_NODE && root.nodeType !== Node.DOCUMENT_FRAGMENT_NODE && root.nodeType !== Node.DOCUMENT_NODE) return;
walkShadowIncludingSubtree(root, VideoObserver.domAdapter, (el) => {
if (el instanceof HTMLVideoElement) {
this.trackVideo(el);
return;
}
const sr = el.shadowRoot;
if (sr) this.observeRoot(sr);
});
}
getVideoListenerSignal(video) {
const existingController = this.videoListenerControllers.get(video);
if (existingController) existingController.abort();
const controller = new AbortController();
this.videoListenerControllers.set(video, controller);
return controller.signal;
}
cleanupVideoListeners(video) {
const controller = this.videoListenerControllers.get(video);
if (!controller) return;
controller.abort();
this.videoListenerControllers.delete(video);
}
cleanupAllVideoListeners() {
for (const controller of this.videoListenerControllers.values()) controller.abort();
this.videoListenerControllers.clear();
}
trackVideo(video) {
if (this.seenVideos.has(video)) return;
this.seenVideos.add(video);
const listenerSignal = this.getVideoListenerSignal(video);
const tryValidate = () => {
if (this.isValidVideo(video)) {
if (!this.activeVideos.has(video)) {
this.activeVideos.add(video);
this.onVideoAdded.dispatch(video);
}
}
};
if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) tryValidate();
else {
video.addEventListener("loadeddata", tryValidate, {
once: true,
signal: listenerSignal
});
const handlePlay = () => {
if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) tryValidate();
};
video.addEventListener("play", handlePlay, {
once: true,
passive: true,
signal: listenerSignal
});
}
video.addEventListener("emptied", () => {
if (!video.isConnected) this.untrackVideo(video);
}, {
passive: true,
signal: listenerSignal
});
}
untrackVideo(video) {
this.cleanupVideoListeners(video);
if (this.activeVideos.has(video)) {
this.onVideoRemoved.dispatch(video);
this.activeVideos.delete(video);
}
this.seenVideos.delete(video);
}
collectVideos(node) {
const set = /* @__PURE__ */ new Set();
if (node instanceof HTMLVideoElement) set.add(node);
if (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.DOCUMENT_FRAGMENT_NODE && node.nodeType !== Node.DOCUMENT_NODE) return Array.from(set);
walkShadowIncludingSubtree(node, VideoObserver.domAdapter, (el) => {
if (el instanceof HTMLVideoElement) set.add(el);
});
return Array.from(set);
}
getNowMs() {
if (typeof performance !== "undefined" && typeof performance.now === "function") return performance.now();
return Date.now();
}
isSliceBudgetReached(startMs, processed) {
if (processed >= VideoObserver.MAX_NODES_PER_SLICE) return true;
return this.getNowMs() - startMs >= VideoObserver.MAX_FLUSH_BUDGET_MS;
}
processPendingAdded(startMs) {
let processed = 0;
while (this.pendingAdded.size > 0) {
const next = this.pendingAdded.values().next();
if (next.done) break;
this.pendingAdded.delete(next.value);
this.scan(next.value);
processed += 1;
if (this.isSliceBudgetReached(startMs, processed)) break;
}
return processed;
}
processPendingRemoved(startMs, processed) {
let processedCount = processed;
while (this.pendingRemoved.size > 0) {
if (this.isSliceBudgetReached(startMs, processedCount)) break;
const next = this.pendingRemoved.values().next();
if (next.done) break;
this.pendingRemoved.delete(next.value);
for (const video of this.collectVideos(next.value)) if (!video.isConnected) this.untrackVideo(video);
processedCount += 1;
}
return processedCount;
}
flushSlice = () => {
if (!this.enabled) {
this.pendingAdded.clear();
this.pendingRemoved.clear();
this.flushPending = false;
return;
}
const startMs = this.getNowMs();
const processedAdded = this.processPendingAdded(startMs);
this.processPendingRemoved(startMs, processedAdded);
this.flushPending = this.pendingAdded.size > 0 || this.pendingRemoved.size > 0;
if (this.flushPending) this.intervalIdleChecker.requestImmediateTick();
};
onCheckerTick = () => {
if (!this.flushPending) return;
this.flushSlice();
};
scheduleFlush = () => {
if (!this.enabled) return;
this.flushPending = true;
this.intervalIdleChecker.requestImmediateTick();
};
installAttachShadowHook() {
if (this.attachShadowSubscriber) return;
const state = getOrInstallAttachShadowHook();
if (!state) return;
const subscriber = (root) => {
if (!this.enabled) return;
this.observeRoot(root);
this.pendingAdded.add(root);
this.scheduleFlush();
};
state.subscribers.add(subscriber);
this.attachShadowSubscriber = subscriber;
}
uninstallAttachShadowHook() {
if (!this.attachShadowSubscriber) return;
removeAttachShadowSubscriber(this.attachShadowSubscriber);
this.attachShadowSubscriber = null;
}
enqueueAddedNode(node) {
if (node.nodeType === Node.ELEMENT_NODE) {
const shadowRoot = node.shadowRoot;
if (shadowRoot) this.observeRoot(shadowRoot);
}
this.pendingAdded.add(node);
}
enqueueMutation(mutation) {
for (const node of mutation.addedNodes) this.enqueueAddedNode(node);
for (const node of mutation.removedNodes) this.pendingRemoved.add(node);
}
onMutations(mutations) {
for (const mutation of mutations) {
if (mutation.type !== "childList") continue;
this.enqueueMutation(mutation);
}
if (this.pendingAdded.size > 0 || this.pendingRemoved.size > 0) this.scheduleFlush();
}
enable() {
if (this.enabled) return;
this.enabled = true;
this.checkerUnsubscribe?.();
this.checkerUnsubscribe = this.intervalIdleChecker.subscribe(this.onCheckerTick);
this.intervalIdleChecker.start();
this.intervalIdleChecker.markActivity("video-observer-enable");
this.installAttachShadowHook();
globalThis.addEventListener("pageshow", this.onPageShow, { passive: true });
const root = document.documentElement;
if (root) {
this.observeRoot(root);
this.scan(root);
return;
}
const onReady = () => {
const r = document.documentElement;
if (!r) return;
document.removeEventListener("readystatechange", onReady);
this.onDocumentReady = null;
if (!this.enabled) return;
this.observeRoot(r);
this.scan(r);
};
this.onDocumentReady = onReady;
document.addEventListener("readystatechange", onReady);
if (typeof queueMicrotask === "function") queueMicrotask(onReady);
else Promise.resolve().then(onReady);
}
disable() {
if (!this.enabled) return;
this.enabled = false;
globalThis.removeEventListener("pageshow", this.onPageShow);
if (this.onDocumentReady) {
document.removeEventListener("readystatechange", this.onDocumentReady);
this.onDocumentReady = null;
}
this.uninstallAttachShadowHook();
this.observer.disconnect();
this.cleanupAllVideoListeners();
this.flushPending = false;
this.checkerUnsubscribe?.();
this.checkerUnsubscribe = null;
this.intervalIdleChecker.stop();
this.pendingAdded.clear();
this.pendingRemoved.clear();
this.seenVideos = /* @__PURE__ */ new WeakSet();
this.activeVideos = /* @__PURE__ */ new WeakSet();
this.observedRoots = /* @__PURE__ */ new WeakSet();
}
};
//#endregion
//#region src/utils/volumeLink.ts
function syncVideoLinkSnapshot(state, volumePercent) {
state.lastVideoPercent = clampPercentInt(volumePercent);
}
function syncTranslationLinkSnapshot(state, volumePercent) {
state.lastTranslationPercent = clampPercentInt(volumePercent);
}
function getSharedTranslationRange(translationMin, translationMax) {
const min = clampPercentInt(translationMin);
return {
min,
max: clampPercentInt(Math.min(100, translationMax), min, 100)
};
}
/**
* Applies delta-based volume linking and mutates `state` in place.
*
* Rules:
* - "video" initiator: translation changes by the same delta as video.
* - "translation" initiator: video changes by the same delta as translation.
*
* This preserves relative offset between sliders (until clamped by bounds),
* instead of forcing a 1:1 mirror.
*/
function applyVolumeLinkDelta({ state, fromType, newVolume, currentVideo, currentTranslation, translationMin, translationMax }) {
const sharedTranslationRange = getSharedTranslationRange(translationMin, translationMax);
if (!state.initialized) {
state.lastVideoPercent = clampPercentInt(currentVideo);
state.lastTranslationPercent = clampInt(Number(currentTranslation), sharedTranslationRange.min, sharedTranslationRange.max);
state.initialized = true;
}
if (fromType === "video") {
const normalizedVideo = clampPercentInt(newVolume);
const delta = normalizedVideo - clampPercentInt(state.lastVideoPercent);
state.lastVideoPercent = normalizedVideo;
const nextTranslation = clampInt(state.lastTranslationPercent + delta, sharedTranslationRange.min, sharedTranslationRange.max);
state.lastTranslationPercent = nextTranslation;
return { nextTranslation };
}
const normalizedTranslation = clampInt(Number.isFinite(newVolume) ? newVolume : currentTranslation, sharedTranslationRange.min, sharedTranslationRange.max);
const delta = normalizedTranslation - state.lastTranslationPercent;
state.lastTranslationPercent = normalizedTranslation;
const nextVideo = clampPercentInt(state.lastVideoPercent + delta);
state.lastVideoPercent = nextVideo;
return { nextVideo };
}
//#endregion
//#region src/utils/platformEvents.ts
var defaultPlatformConfig = {
allowTouchMoveHandler: true,
disableContainerDrag: false
};
var platformOverrides = {
xvideos: { allowTouchMoveHandler: false },
youtube: { disableContainerDrag: true }
};
function getPlatformEventConfig(host) {
if (!host) return defaultPlatformConfig;
const overrides = platformOverrides[host] ?? {};
return {
...defaultPlatformConfig,
...overrides
};
}
//#endregion
//#region src/videoHandler/modules/ducking.ts
var VOLUME_MIN_01 = 0;
var VOLUME_MAX_01 = 1;
var SMART_DUCKING_DEFAULT_CONFIG = Object.freeze({
tickMs: 50,
thresholdOnRms: .012,
thresholdOffRms: .009,
rmsAttackTauMs: 60,
rmsReleaseTauMs: 240,
holdMs: 520,
attackTauMs: 110,
releaseTauMs: 600,
maxDownPerSec: 3.5,
maxUpPerSec: .9,
rmsMissingGraceMs: 200,
maxDtMs: 250,
externalBaselineDelta01: .02,
unduckTolerance01: .01,
volumeStep01: VIDEO_VOLUME_STEP_01,
applyDeltaThreshold01: VIDEO_VOLUME_STEP_01 / 2
});
function initSmartDuckingRuntime(baseline) {
return {
isDucked: false,
speechGateOpen: false,
rmsEnvelope: 0,
baseline: normalizeVolume01(baseline),
lastApplied: void 0,
lastTickAt: 0,
lastSoundAt: 0,
rmsMissingSinceAt: null
};
}
function updateSpeechGate(input, runtime, config, now, hasRms) {
const gateOpen = runtime.speechGateOpen;
if (!input.smartEnabled) {
runtime.lastSoundAt = now;
runtime.rmsMissingSinceAt = null;
return true;
}
if (input.audioIsPlaying && !hasRms) {
runtime.rmsMissingSinceAt ??= now;
if (gateOpen) runtime.lastSoundAt = now;
if (runtime.rmsMissingSinceAt !== null && now - runtime.rmsMissingSinceAt >= config.rmsMissingGraceMs) {
runtime.lastSoundAt = now;
return true;
}
return gateOpen;
}
runtime.rmsMissingSinceAt = null;
if (!input.audioIsPlaying) return gateOpen && now - runtime.lastSoundAt <= config.holdMs;
if (!gateOpen && runtime.rmsEnvelope >= config.thresholdOnRms) {
runtime.lastSoundAt = now;
return true;
}
if (gateOpen && runtime.rmsEnvelope >= config.thresholdOffRms) {
runtime.lastSoundAt = now;
return true;
}
return gateOpen && now - runtime.lastSoundAt <= config.holdMs;
}
function resolveBaseline(runtime, currentVideoVolume, volumeOnStart, config) {
if (runtime.isDucked && isFiniteNumber(runtime.lastApplied) && Math.abs(currentVideoVolume - runtime.lastApplied) > config.externalBaselineDelta01) runtime.baseline = currentVideoVolume;
if (!runtime.isDucked) runtime.baseline = currentVideoVolume;
const baseline = runtime.baseline ?? volumeOnStart ?? currentVideoVolume;
runtime.baseline = baseline;
return baseline;
}
function resolveDesiredVolume(runtime, gateOpen, currentVideoVolume, baseline, duckingTarget01, config) {
const duckedTarget = Math.min(baseline, duckingTarget01);
if (gateOpen) {
runtime.isDucked = true;
return duckedTarget;
}
if (runtime.isDucked && Math.abs(baseline - currentVideoVolume) < config.unduckTolerance01) runtime.isDucked = false;
return baseline;
}
function smoothVolumeChange(desired, currentVideoVolume, dtMs, dtSec, config) {
const smoothingTauMs = desired < currentVideoVolume ? config.attackTauMs : config.releaseTauMs;
const smoothingAlpha = smoothingTauMs > 0 ? -Math.expm1(-dtMs / smoothingTauMs) : 1;
let nextVolume = currentVideoVolume + (desired - currentVideoVolume) * smoothingAlpha;
const maxDelta = (desired < currentVideoVolume ? config.maxDownPerSec : config.maxUpPerSec) * dtSec;
if (maxDelta > 0) nextVolume = clamp(nextVolume, currentVideoVolume - maxDelta, currentVideoVolume + maxDelta);
return clamp(nextVolume, VOLUME_MIN_01, VOLUME_MAX_01);
}
function buildVolumeDecision(runtime, currentVideoVolume, quantized, applyDeltaThreshold01) {
if (Math.abs(quantized - currentVideoVolume) < applyDeltaThreshold01) {
runtime.lastApplied = quantized;
return {
kind: "noop",
runtime
};
}
if (!isFiniteNumber(runtime.lastApplied) || Math.abs(quantized - runtime.lastApplied) >= applyDeltaThreshold01) {
runtime.lastApplied = quantized;
return {
kind: "apply",
runtime,
volume01: quantized
};
}
return {
kind: "noop",
runtime
};
}
function computeSmartDuckingStep(input, runtime, config = SMART_DUCKING_DEFAULT_CONFIG) {
const nextRuntime = normalizeRuntime(runtime);
const volumeOnStart = normalizeVolume01(input.volumeOnStart);
if (!input.translationActive || !input.enabledAutoVolume) return {
kind: "stop",
runtime: nextRuntime,
restoreVolume: nextRuntime.baseline ?? volumeOnStart
};
const now = Number.isFinite(input.nowMs) ? input.nowMs : Date.now();
const dtMs = clamp(now - (nextRuntime.lastTickAt || now), 0, config.maxDtMs);
const dtSec = dtMs / 1e3;
nextRuntime.lastTickAt = now;
const hasRms = isFiniteNumber(input.rms);
const rmsValue = hasRms ? clamp(input.rms, VOLUME_MIN_01, VOLUME_MAX_01) : 0;
const prevEnv = nextRuntime.rmsEnvelope;
const envTauMs = rmsValue > prevEnv ? config.rmsAttackTauMs : config.rmsReleaseTauMs;
const envAlpha = envTauMs > 0 ? -Math.expm1(-dtMs / envTauMs) : 1;
nextRuntime.rmsEnvelope = clamp(prevEnv + (rmsValue - prevEnv) * envAlpha, VOLUME_MIN_01, VOLUME_MAX_01);
const gateOpen = updateSpeechGate(input, nextRuntime, config, now, hasRms);
nextRuntime.speechGateOpen = gateOpen;
const currentVideoVolume = normalizeVolume01(input.currentVideoVolume);
if (!isFiniteNumber(currentVideoVolume)) return {
kind: "noop",
runtime: nextRuntime
};
const baseline = resolveBaseline(nextRuntime, currentVideoVolume, volumeOnStart, config);
if (!input.hostVideoActive) {
nextRuntime.lastApplied = currentVideoVolume;
return {
kind: "noop",
runtime: nextRuntime
};
}
const desired = resolveDesiredVolume(nextRuntime, gateOpen, currentVideoVolume, baseline, normalizeVolume01(input.duckingTarget01) ?? baseline, config);
const quantized = snapVolume01Towards(smoothVolumeChange(desired, currentVideoVolume, dtMs, dtSec, config), currentVideoVolume, desired, config.volumeStep01);
const applyDeltaThreshold01 = config.applyDeltaThreshold01;
return buildVolumeDecision(nextRuntime, currentVideoVolume, quantized, applyDeltaThreshold01);
}
function normalizeRuntime(runtime) {
return {
isDucked: Boolean(runtime.isDucked),
speechGateOpen: Boolean(runtime.speechGateOpen),
rmsEnvelope: normalizeVolume01(runtime.rmsEnvelope) ?? 0,
baseline: normalizeVolume01(runtime.baseline),
lastApplied: normalizeVolume01(runtime.lastApplied),
lastTickAt: isFiniteNumber(runtime.lastTickAt) ? runtime.lastTickAt : 0,
lastSoundAt: isFiniteNumber(runtime.lastSoundAt) ? runtime.lastSoundAt : 0,
rmsMissingSinceAt: isFiniteNumber(runtime.rmsMissingSinceAt) ? runtime.rmsMissingSinceAt : null
};
}
function normalizeVolume01(value) {
if (!isFiniteNumber(value)) return void 0;
return clamp(value, VOLUME_MIN_01, VOLUME_MAX_01);
}
function isFiniteNumber(value) {
return typeof value === "number" && Number.isFinite(value);
}
//#endregion
//#region src/videoHandler/modules/smartDuckingRuntime.ts
var SMART_DUCKING_TICK_MS = SMART_DUCKING_DEFAULT_CONFIG.tickMs;
var smartDuckingAnalyserState = /* @__PURE__ */ new WeakMap();
function isAudioNode(node) {
if (!node || typeof node !== "object") return false;
const candidate = node;
return typeof candidate.connect === "function" && typeof candidate.disconnect === "function";
}
function getPlayerMediaElement(player) {
return player?.audio ?? player?.audioElement;
}
function getNowMs() {
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
}
function getAutoVolumeMode(handler) {
if (!handler.data?.enabledAutoVolume) return "off";
if (handler.data?.syncVolume) return "classic";
return handler.data?.enabledSmartDucking ?? true ? "smart" : "classic";
}
function getSmartDuckingAudioContext(handler) {
return handler.audioPlayer?.audioContext ?? handler.audioContext;
}
function disconnectSmartDuckingAnalyser(state) {
if (state.connectedInputNode && state.analyser) try {
state.connectedInputNode.disconnect(state.analyser);
} catch {}
state.connectedInputNode = void 0;
if (state.createdMediaSource) try {
state.createdMediaSource.disconnect();
} catch {}
state.createdMediaSource = void 0;
if (state.analyser) try {
state.analyser.disconnect();
} catch {}
state.analyser = void 0;
state.analyserFloatData = void 0;
state.analyserData = void 0;
state.mediaElement = void 0;
state.audioContext = void 0;
state.mediaSourceCreationFailed = false;
}
function releaseSmartDuckingAnalyser(handler) {
const state = smartDuckingAnalyserState.get(handler);
if (!state) return;
disconnectSmartDuckingAnalyser(state);
smartDuckingAnalyserState.delete(handler);
}
function resolveSmartDuckingInputNode(player, media, audioContext, state) {
if (isAudioNode(player?.audioSource)) return player.audioSource;
if (isAudioNode(player?.mediaElementSource)) return player.mediaElementSource;
if (state.mediaSourceCreationFailed && state.mediaElement === media && state.audioContext === audioContext) return;
if (state.createdMediaSource && state.mediaElement === media && state.audioContext === audioContext) return state.createdMediaSource;
try {
const source = audioContext.createMediaElementSource(media);
state.createdMediaSource = source;
state.mediaSourceCreationFailed = false;
return source;
} catch (err) {
state.mediaSourceCreationFailed = true;
debug.log("[SmartDucking] failed to create media source", err);
return;
}
}
function ensureSmartDuckingAnalyser(handler, player, media) {
const audioContext = getSmartDuckingAudioContext(handler);
if (!audioContext) return void 0;
let state = smartDuckingAnalyserState.get(handler);
if (!state) {
state = {};
smartDuckingAnalyserState.set(handler, state);
}
if (state.mediaElement && state.mediaElement !== media || state.audioContext && state.audioContext !== audioContext) disconnectSmartDuckingAnalyser(state);
state.mediaElement = media;
state.audioContext = audioContext;
if (!state.analyser) {
const analyser = audioContext.createAnalyser();
analyser.fftSize = 512;
state.analyser = analyser;
}
const inputNode = resolveSmartDuckingInputNode(player, media, audioContext, state);
const analyser = state.analyser;
if (!inputNode || !analyser) return void 0;
if (state.connectedInputNode !== inputNode) {
if (state.connectedInputNode) try {
state.connectedInputNode.disconnect(analyser);
} catch {}
try {
analyser.disconnect();
} catch {}
try {
inputNode.connect(analyser);
state.connectedInputNode = inputNode;
if (state.createdMediaSource === inputNode) try {
analyser.connect(audioContext.destination);
} catch (err) {
debug.log("[SmartDucking] failed to bridge analyser output", err);
}
} catch (err) {
debug.log("[SmartDucking] failed to connect analyser", err);
return;
}
}
return {
analyser,
state
};
}
function readSmartDuckingRuntime(handler) {
return {
isDucked: handler.smartVolumeIsDucked,
speechGateOpen: handler.smartVolumeSpeechGateOpen,
rmsEnvelope: handler.smartVolumeRmsEnvelope,
baseline: handler.smartVolumeDuckingBaseline,
lastApplied: handler.smartVolumeLastApplied,
lastTickAt: handler.smartVolumeLastTickAt,
lastSoundAt: handler.smartVolumeLastSoundAt,
rmsMissingSinceAt: handler.smartVolumeRmsMissingSinceAt
};
}
function writeSmartDuckingRuntime(handler, runtime) {
handler.smartVolumeIsDucked = runtime.isDucked;
handler.smartVolumeSpeechGateOpen = runtime.speechGateOpen;
handler.smartVolumeRmsEnvelope = runtime.rmsEnvelope;
handler.smartVolumeDuckingBaseline = runtime.baseline;
handler.smartVolumeLastApplied = runtime.lastApplied;
handler.smartVolumeLastTickAt = runtime.lastTickAt;
handler.smartVolumeLastSoundAt = runtime.lastSoundAt;
handler.smartVolumeRmsMissingSinceAt = runtime.rmsMissingSinceAt;
}
function restoreAutoVolumeMute(handler) {
if (typeof handler.autoVolumeMutedOnStart !== "boolean") return;
try {
handler.setVideoMuted(handler.autoVolumeMutedOnStart);
} catch {}
handler.autoVolumeMutedOnStart = void 0;
}
function stopSmartVolumeDucking(handler, options = {}) {
const { restoreVolume } = options;
if (handler.smartVolumeDuckingInterval !== void 0) {
clearTimeout(handler.smartVolumeDuckingInterval);
handler.smartVolumeDuckingInterval = void 0;
}
const baseline = typeof restoreVolume === "number" ? restoreVolume : handler.smartVolumeDuckingBaseline ?? handler.volumeOnStart;
if (typeof baseline === "number" && (typeof restoreVolume === "number" || handler.smartVolumeIsDucked)) try {
handler.setVideoVolume(baseline);
} catch {}
restoreAutoVolumeMute(handler);
releaseSmartDuckingAnalyser(handler);
writeSmartDuckingRuntime(handler, initSmartDuckingRuntime());
}
function scheduleNextSmartDuckingTick(handler) {
if (typeof globalThis === "undefined") return;
if (handler.smartVolumeDuckingInterval === void 0) return;
handler.smartVolumeDuckingInterval = globalThis.setTimeout(() => {
if (handler.smartVolumeDuckingInterval === void 0) return;
try {
smartDuckingTick(handler);
} catch (err) {
debug.log("[SmartDucking] tick failed, stopping smart ducking", err);
stopSmartVolumeDucking(handler);
return;
}
if (handler.smartVolumeDuckingInterval === void 0) return;
scheduleNextSmartDuckingTick(handler);
}, SMART_DUCKING_TICK_MS);
}
function startSmartVolumeDucking(handler) {
if (typeof globalThis === "undefined") return;
if (handler.smartVolumeDuckingInterval !== void 0) return;
if (getAutoVolumeMode(handler) !== "smart") return;
const currentVideoVolume = handler.getVideoVolume();
const baseline = typeof handler.smartVolumeDuckingBaseline === "number" ? handler.smartVolumeDuckingBaseline : currentVideoVolume;
const runtime = initSmartDuckingRuntime(baseline);
if (Number.isFinite(currentVideoVolume) && Number.isFinite(baseline) && currentVideoVolume < baseline - SMART_DUCKING_DEFAULT_CONFIG.externalBaselineDelta01) {
const now = getNowMs();
runtime.isDucked = true;
runtime.speechGateOpen = true;
runtime.lastApplied = currentVideoVolume;
runtime.lastSoundAt = now;
}
writeSmartDuckingRuntime(handler, runtime);
handler.smartVolumeDuckingInterval = globalThis.setTimeout(() => {}, 0);
clearTimeout(handler.smartVolumeDuckingInterval);
scheduleNextSmartDuckingTick(handler);
}
function getTranslatedAudioRms(handler, media) {
const player = handler.audioPlayer?.player;
const analyserBundle = ensureSmartDuckingAnalyser(handler, player, media);
if (!analyserBundle) return void 0;
const { analyser, state } = analyserBundle;
try {
if (typeof analyser.getFloatTimeDomainData === "function") {
let floatData = state.analyserFloatData;
if (floatData?.length !== analyser.fftSize) {
floatData = new Float32Array(analyser.fftSize);
state.analyserFloatData = floatData;
}
analyser.getFloatTimeDomainData(floatData);
let sum = 0;
for (const value of floatData) sum += value * value;
return clamp(Math.sqrt(sum / floatData.length), 0, 1);
}
let data = state.analyserData;
if (data?.length !== analyser.fftSize) {
data = new Uint8Array(analyser.fftSize);
state.analyserData = data;
}
analyser.getByteTimeDomainData(data);
let sum = 0;
for (const rawValue of data) {
const normalizedValue = (rawValue - 128) / 128;
sum += normalizedValue * normalizedValue;
}
return clamp(Math.sqrt(sum / data.length), 0, 1);
} catch {
return;
}
}
function smartDuckingTick(handler) {
if (getAutoVolumeMode(handler) !== "smart") {
setupAudioSettings.call(handler);
return;
}
const player = handler.audioPlayer?.player;
const media = getPlayerMediaElement(player);
const audioIsPlaying = !!media && !media.paused && !media.muted && (media.volume ?? 1) > .001;
const now = getNowMs();
const currentVideoVolume = handler.getVideoVolume();
const hostVideo = handler.video;
const hostVideoActive = !(hostVideo && (hostVideo.paused || hostVideo.ended));
const dynamicDuckingTarget = clamp(handler.data?.autoVolume ?? 15, 0, 100) / 100;
const rms = audioIsPlaying && media ? getTranslatedAudioRms(handler, media) : 0;
const decision = computeSmartDuckingStep({
nowMs: now,
translationActive: handler.hasActiveSource(),
enabledAutoVolume: true,
smartEnabled: true,
audioIsPlaying,
rms,
currentVideoVolume,
hostVideoActive,
duckingTarget01: dynamicDuckingTarget,
volumeOnStart: handler.volumeOnStart
}, readSmartDuckingRuntime(handler), SMART_DUCKING_DEFAULT_CONFIG);
switch (decision.kind) {
case "stop":
stopSmartVolumeDucking(handler, { restoreVolume: decision.restoreVolume });
return;
case "apply":
handler.setVideoVolume(decision.volume01, { preserveYoutubeVolumeStorage: true });
writeSmartDuckingRuntime(handler, decision.runtime);
return;
case "noop":
writeSmartDuckingRuntime(handler, decision.runtime);
return;
default: throw new TypeError("Unhandled smart ducking decision");
}
}
function setupAudioSettings() {
if (typeof this.data?.defaultVolume === "number") safeSetPlayerVolume(this.audioPlayer.player, this.data.defaultVolume / 100);
const autoVolumeMode = getAutoVolumeMode(this);
if (autoVolumeMode === "off") {
stopSmartVolumeDucking(this, { restoreVolume: this.smartVolumeDuckingBaseline ?? this.volumeOnStart });
return;
}
const targetVolume = clamp(this.data.autoVolume ?? 15, 0, 100) / 100;
if (!this.hasActiveSource()) return;
if (targetVolume === 0) {
if (this.smartVolumeDuckingInterval !== void 0) {
clearTimeout(this.smartVolumeDuckingInterval);
this.smartVolumeDuckingInterval = void 0;
}
if (typeof this.smartVolumeDuckingBaseline !== "number") this.smartVolumeDuckingBaseline = this.getVideoVolume();
if (typeof this.autoVolumeMutedOnStart !== "boolean") this.autoVolumeMutedOnStart = Boolean(this.isMuted());
this.setVideoVolume(0, { preserveYoutubeVolumeStorage: true });
this.setVideoMuted(true, { preserveYoutubeVolumeStorage: true });
writeSmartDuckingRuntime(this, initSmartDuckingRuntime(this.smartVolumeDuckingBaseline));
this.smartVolumeIsDucked = true;
return;
}
restoreAutoVolumeMute(this);
if (autoVolumeMode === "smart") {
startSmartVolumeDucking(this);
return;
}
if (this.smartVolumeDuckingInterval !== void 0) {
clearTimeout(this.smartVolumeDuckingInterval);
this.smartVolumeDuckingInterval = void 0;
}
if (typeof this.smartVolumeDuckingBaseline !== "number") this.smartVolumeDuckingBaseline = this.getVideoVolume();
const baseline = this.smartVolumeDuckingBaseline ?? this.getVideoVolume();
const nextVolume = Math.min(baseline, targetVolume);
this.setVideoVolume(nextVolume, { preserveYoutubeVolumeStorage: true });
writeSmartDuckingRuntime(this, initSmartDuckingRuntime(this.smartVolumeDuckingBaseline));
this.smartVolumeIsDucked = true;
}
function applyManualVideoVolumeOverride(volume01) {
if (!this.data?.enabledAutoVolume || !this.hasActiveSource()) return;
const nextVolume = snapVolume01(volume01);
this.smartVolumeDuckingBaseline = nextVolume;
this.smartVolumeLastApplied = nextVolume;
}
//#endregion
//#region src/videoHandler/modules/proxyShared.ts
var AUDIO_SOURCE_PREFIX = "https://vtrans.s3-private.mds.yandex.net/tts/prod/";
var AUDIO_PROXY_PATH_PREFIX = "/video-translation/audio-proxy/";
var SUBTITLE_SOURCE_PREFIX = "https://brosubs.s3-private.mds.yandex.net/vtrans/";
var SUBTITLE_PROXY_PATH_PREFIX = "/video-subtitles/subtitles-proxy/";
function resolveProxyWorkerHost(host) {
return host ?? "vot-worker.eu.cc";
}
function isProxyClientEnabled(config) {
return Boolean(config.translateProxyEnabled);
}
function isProxyRoutingEnabled(config) {
return config.translateProxyEnabled === 2;
}
function shouldForceProxyClientGmXhr(config) {
return Boolean(config.gmXhrSupported && isProxyClientEnabled(config));
}
function proxifyYandexAudioUrl(audioUrl, config) {
if (!isProxyRoutingEnabled(config) || !audioUrl.startsWith(AUDIO_SOURCE_PREFIX)) return audioUrl;
return audioUrl.replace(AUDIO_SOURCE_PREFIX, `https://${resolveProxyWorkerHost(config.proxyWorkerHost)}${AUDIO_PROXY_PATH_PREFIX}`);
}
function unproxifyYandexAudioUrl(audioUrl) {
const str = String(audioUrl || "");
if (!str) return str;
try {
const url = new URL(str);
if (!url.pathname.startsWith(AUDIO_PROXY_PATH_PREFIX)) return str;
url.host = "vtrans.s3-private.mds.yandex.net";
url.pathname = `/tts/prod/${url.pathname.slice(31).replace(/^\/+/, "")}`;
url.protocol = "https:";
return url.toString();
} catch {
return str;
}
}
function isYandexAudioUrlOrProxy(url, config) {
return url.startsWith(AUDIO_SOURCE_PREFIX) || url.startsWith(`https://${resolveProxyWorkerHost(config.proxyWorkerHost)}${AUDIO_PROXY_PATH_PREFIX}`);
}
function proxifyYandexSubtitlesUrl(subtitlesUrl, config) {
if (!isProxyRoutingEnabled(config) || !subtitlesUrl.startsWith(SUBTITLE_SOURCE_PREFIX)) return subtitlesUrl;
const subtitlesPath = subtitlesUrl.slice(49);
return `https://${resolveProxyWorkerHost(config.proxyWorkerHost)}${SUBTITLE_PROXY_PATH_PREFIX}${subtitlesPath}`;
}
//#endregion
//#region src/videoHandler/modules/subtitlesShared.ts
var DISABLED_SUBTITLES_VALUE = "disabled";
var SUBTITLES_INDEX_OPTION_PATTERN = /^\d+$/u;
var [AUTO_SUBTITLE_LANGUAGE_VALUE, ORIGINAL_SUBTITLE_LANGUAGE_VALUE] = subtitleResponseLanguageModes;
function getIndexedSubtitleDescriptors(subtitles) {
const descriptors = [];
for (let index = 0; index < subtitles.length; index += 1) {
const descriptor = parseSubtitleDescriptor(subtitles[index]);
if (!descriptor) continue;
descriptors.push({
descriptor,
index
});
}
return descriptors;
}
function parseSubtitlesOptionIndex(value) {
if (!SUBTITLES_INDEX_OPTION_PATTERN.test(value)) return null;
const parsed = Number(value);
if (!Number.isSafeInteger(parsed)) return null;
return parsed;
}
function getSubtitleDescriptorAtIndex(subtitles, index) {
if (!Number.isInteger(index) || index < 0 || index >= subtitles.length) return null;
return parseSubtitleDescriptor(subtitles[index]);
}
function createDisabledSubtitlesOption() {
return {
label: localizationProvider.get("VOTSubtitlesDisabled"),
value: DISABLED_SUBTITLES_VALUE,
selected: true,
disabled: false
};
}
function buildSubtitleLabel(subtitle) {
return `${localizationProvider.getLangLabel(subtitle.language)}${subtitle.translatedFromLanguage ? ` ${localizationProvider.get("VOTTranslatedFrom")} ${localizationProvider.getLangLabel(subtitle.translatedFromLanguage)}` : ""}${subtitle.source === "yandex" ? "" : `, ${globalThis.location.hostname}`}${subtitle.isAutoGenerated ? ` (${localizationProvider.get("VOTAutogenerated")})` : ""}`;
}
function buildSubtitlesSelectOptions(subtitleDescriptors) {
const options = [createDisabledSubtitlesOption()];
for (const { descriptor, index } of subtitleDescriptors) options.push({
label: buildSubtitleLabel(descriptor),
value: String(index),
selected: false,
disabled: false
});
return options;
}
function getSelectedSubtitlesValue(selectedValues) {
const first = selectedValues[Symbol.iterator]().next();
return first.done ? void 0 : first.value;
}
function normalizeLang(lang) {
return (lang ?? "").toLowerCase();
}
function baseLang(lang) {
return normalizeLang(lang).split(/[-_]/)[0];
}
function langMatches(candidate, desired) {
if (!candidate || !desired) return false;
const cand = normalizeLang(candidate);
const want = normalizeLang(desired);
return cand === want || baseLang(cand) === baseLang(want);
}
function resolveSubtitlesLanguage(preference, detectedLanguage, responseLanguage) {
if (preference === ORIGINAL_SUBTITLE_LANGUAGE_VALUE) {
const originalLanguage = normalizeLang(detectedLanguage);
return originalLanguage && originalLanguage !== AUTO_SUBTITLE_LANGUAGE_VALUE ? originalLanguage : void 0;
}
if (typeof preference === "string" && preference && preference !== AUTO_SUBTITLE_LANGUAGE_VALUE) return normalizeLang(preference);
return normalizeLang(responseLanguage) || normalizeLang(detectedLanguage);
}
function pickBestSubtitlesIndex(subtitles, fromLang, toLang) {
if (!subtitles.length) return null;
const from = normalizeLang(fromLang);
const to = normalizeLang(toLang);
const fromIsAuto = from === "auto" || from === "";
const fromBase = baseLang(from);
const toBase = baseLang(to);
const isYandex = (descriptor) => descriptor.source === "yandex";
const isAutoGenerated = (descriptor) => Boolean(descriptor.isAutoGenerated);
const matchesPair = (descriptor, wantFrom, wantTo) => {
if (!langMatches(descriptor.language, wantTo)) return false;
if (fromIsAuto) return true;
return langMatches(descriptor.translatedFromLanguage, wantFrom);
};
const isSameLangOriginal = (descriptor, lang) => {
if (!langMatches(descriptor.language, lang)) return false;
if (!descriptor.translatedFromLanguage) return true;
return langMatches(descriptor.translatedFromLanguage, lang);
};
const find = (predicate) => subtitles.find(({ descriptor }) => predicate(descriptor))?.index ?? null;
const findOtherTarget = () => {
const otherTargetManual = find((descriptor) => !isYandex(descriptor) && langMatches(descriptor.language, to) && !isAutoGenerated(descriptor));
if (otherTargetManual != null) return otherTargetManual;
return find((descriptor) => !isYandex(descriptor) && langMatches(descriptor.language, to) && isAutoGenerated(descriptor));
};
if (!fromIsAuto && fromBase && toBase && fromBase === toBase) {
const nativeManual = find((descriptor) => isSameLangOriginal(descriptor, to) && !isAutoGenerated(descriptor));
if (nativeManual != null) return nativeManual;
const nativeAuto = find((descriptor) => isSameLangOriginal(descriptor, to) && isAutoGenerated(descriptor));
if (nativeAuto != null) return nativeAuto;
const otherTarget = findOtherTarget();
if (otherTarget != null) return otherTarget;
const yandexTargetSameLang = find((descriptor) => isYandex(descriptor) && langMatches(descriptor.language, to));
if (yandexTargetSameLang != null) return yandexTargetSameLang;
}
const yandexPair = find((descriptor) => isYandex(descriptor) && matchesPair(descriptor, from, to));
if (yandexPair != null) return yandexPair;
const yandexTarget = find((descriptor) => isYandex(descriptor) && langMatches(descriptor.language, to));
if (yandexTarget != null) return yandexTarget;
const otherPair = find((descriptor) => !isYandex(descriptor) && matchesPair(descriptor, from, to));
if (otherPair != null) return otherPair;
const otherTarget = findOtherTarget();
if (otherTarget != null) return otherTarget;
return null;
}
//#endregion
//#region src/videoHandler/modules/translationPlayback.ts
var AUDIO_PROBE_TIMEOUT_MS = 5e3;
var AUDIO_PROBE_RETRY_DELAY_MS = 150;
var AUDIO_PROBE_MAX_ATTEMPTS = 2;
async function resumePlayerAudioContextIfNeeded(handler) {
const ctx = handler.audioPlayer?.audioContext;
if (!ctx || ctx.state !== "suspended") return "not-needed";
const RESUME_TIMEOUT_MS = 1500;
const resumePromise = (async () => {
try {
await ctx.resume();
return "resumed";
} catch (err) {
debug.log("[updateTranslation] Failed to resume AudioContext", err);
return "failed";
}
})();
let timeoutId;
const timeoutPromise = new Promise((resolve) => {
timeoutId = setTimeout(() => resolve("timeout"), RESUME_TIMEOUT_MS);
});
const result = await Promise.race([resumePromise, timeoutPromise]);
if (timeoutId !== void 0) clearTimeout(timeoutId);
if (result === "resumed") debug.log("[updateTranslation] AudioContext resumed");
else if (result === "timeout") debug.log("[updateTranslation] AudioContext resume timeout");
return result;
}
async function rollbackStaleAppliedSourceIfStillCurrent(handler, appliedSourceUrl) {
if (!appliedSourceUrl || !handler.audioPlayer) return;
const player = handler.audioPlayer.player;
const currentSource = String(player.currentSrc || player.src || "");
if (handler.proxifyAudio(handler.unproxifyAudio(currentSource)) !== handler.proxifyAudio(handler.unproxifyAudio(appliedSourceUrl))) return;
try {
await player.clear();
player.src = "";
debug.log("[updateTranslation] cleared stale partially-applied source");
} catch (err) {
debug.log("[updateTranslation] failed to clear stale source", err);
}
}
function waitForProbeRetry(delayMs, signal) {
if (delayMs <= 0 || signal.aborted) return Promise.resolve();
return new Promise((resolve) => {
const timeoutId = setTimeout(() => {
signal.removeEventListener("abort", onAbort);
resolve();
}, delayMs);
const onAbort = () => {
clearTimeout(timeoutId);
signal.removeEventListener("abort", onAbort);
resolve();
};
signal.addEventListener("abort", onAbort, { once: true });
});
}
async function probeAudioUrl(handler, audioUrl, actionContext) {
const signal = handler.actionsAbortController.signal;
const fetchOpts = {
headers: { range: "bytes=0-0" },
signal,
timeout: AUDIO_PROBE_TIMEOUT_MS
};
for (let attempt = 1; attempt <= AUDIO_PROBE_MAX_ATTEMPTS; attempt++) {
if (isProbeCancelled(handler, actionContext, signal)) return false;
try {
const response = await GM_fetch(audioUrl, fetchOpts);
if (isProbeCancelled(handler, actionContext, signal)) return false;
debug.log("[validateAudioUrl] probe response", {
audioUrl,
attempt,
ok: response.ok,
status: response.status
});
if (response.ok) return true;
} catch (err) {
if (isProbeCancelled(handler, actionContext, signal)) return false;
debug.log("[validateAudioUrl] probe error", {
audioUrl,
attempt,
err
});
}
if (!await shouldRetryAudioProbe(attempt, handler, actionContext, signal)) return false;
}
return false;
}
function isProbeCancelled(handler, actionContext, signal) {
return handler.isActionStale(actionContext) || signal.aborted;
}
async function shouldRetryAudioProbe(attempt, handler, actionContext, signal) {
if (attempt >= AUDIO_PROBE_MAX_ATTEMPTS) return true;
if (isProbeCancelled(handler, actionContext, signal)) return false;
await waitForProbeRetry(AUDIO_PROBE_RETRY_DELAY_MS, signal);
return !isProbeCancelled(handler, actionContext, signal);
}
async function validateAudioUrl(audioUrl, actionContext) {
if (this.isActionStale(actionContext)) return audioUrl;
if (await probeAudioUrl(this, audioUrl, actionContext)) return audioUrl;
const directUrl = this.unproxifyAudio(audioUrl);
if (directUrl !== audioUrl) {
if (await probeAudioUrl(this, directUrl, actionContext)) {
debug.log("[validateAudioUrl] switching to direct audio URL after probe");
return directUrl;
}
}
return audioUrl;
}
function scheduleTranslationRefresh() {
if (!this.videoData || this.videoData.isStream) return;
if (!this.hasActiveSource()) return;
this.refreshTranslationAudio().catch((error) => {
debug.log("[scheduleTranslationRefresh] refresh failed", error);
});
}
async function handlePlaybackResumedTranslationRefresh() {
if (!this.videoData || this.videoData.isStream) return;
if (!this.hasActiveSource()) return;
const videoId = this.videoData.videoId;
if (!videoId) return;
const normalizedTranslationHelp = normalizeTranslationHelp(this.videoData.translationHelp);
const cacheKey = this.getTranslationCacheKey(videoId, this.translateFromLang, this.translateToLang, normalizedTranslationHelp);
if (!this.cacheManager.getTranslation(cacheKey)?.url) {
debug.log("[scheduleTranslationRefresh] translation cache expired after resume, refreshing now");
await this.refreshTranslationAudio();
}
}
async function requestApplyAndCacheTranslation(self, options) {
const translateRes = await requestAndApplyTranslation({
requester: self.translationHandler,
request: {
videoData: options.videoData,
requestLang: options.requestLang,
responseLang: options.responseLang,
translationHelp: options.translationHelp,
useAudioDownload: Boolean(self.data?.useAudioDownload),
signal: self.actionsAbortController.signal
},
actionContext: options.actionContext,
isActionStale: (ctx) => self.isActionStale(ctx),
updateTranslation: (url, ctx) => self.updateTranslation(url, ctx)
});
if (!translateRes) return null;
if (options.onBeforeCache) await options.onBeforeCache(translateRes);
setTranslationCacheValue({
cacheKey: options.cacheKey,
setTranslation: (key, value) => self.cacheManager.setTranslation(key, value),
videoId: options.cacheVideoId,
requestLang: options.cacheRequestLang,
responseLang: options.cacheResponseLang,
fallbackUrl: translateRes.url,
downloadTranslationUrl: self.downloadTranslation?.url,
usedLivelyVoice: translateRes.usedLivelyVoice
});
return translateRes;
}
async function refreshTranslationAudio() {
if (!this.videoData || this.videoData.isStream) return;
if (!this.hasActiveSource()) return;
if (this.isRefreshingTranslation) return;
const videoId = this.videoData.videoId;
if (!videoId) return;
if (this.actionsAbortController?.signal?.aborted) this.resetActionsAbortController("refreshTranslationAudio");
this.isRefreshingTranslation = true;
const actionContext = {
gen: this.actionsGeneration,
videoId
};
const normalizedTranslationHelp = normalizeTranslationHelp(this.videoData.translationHelp);
try {
if (!await requestApplyAndCacheTranslation(this, {
videoData: this.videoData,
requestLang: this.translateFromLang,
responseLang: this.translateToLang,
translationHelp: normalizedTranslationHelp,
actionContext,
cacheKey: this.getTranslationCacheKey(videoId, this.translateFromLang, this.translateToLang, normalizedTranslationHelp),
cacheVideoId: videoId,
cacheRequestLang: this.translateFromLang,
cacheResponseLang: this.translateToLang
})) return;
} finally {
this.isRefreshingTranslation = false;
}
}
function proxifyAudio(audioUrl) {
const proxiedAudioUrl = proxifyYandexAudioUrl(audioUrl, {
translateProxyEnabled: this.data?.translateProxyEnabled,
proxyWorkerHost: this.data?.proxyWorkerHost
});
if (proxiedAudioUrl !== audioUrl) debug.log(`[VOT] Audio proxied via ${proxiedAudioUrl}`);
return proxiedAudioUrl;
}
function unproxifyAudio(audioUrl) {
return unproxifyYandexAudioUrl(audioUrl);
}
async function handleProxySettingsChanged(reason = "proxySettingsChanged") {
debug.log(`[VOT] ${reason}: clearing translation/subtitles cache`);
try {
this.cacheManager.clear();
this.activeTranslation = null;
} catch {}
try {
await this.stopTranslation();
} catch {}
await this.initVOTClient();
}
function isMultiMethodS3(url) {
return isYandexAudioUrlOrProxy(url, { proxyWorkerHost: this.data?.proxyWorkerHost });
}
function normalizeManagedAudioUrl(handler, url) {
return handler.proxifyAudio(handler.unproxifyAudio(url));
}
async function applyTranslationSource(handler, sourceUrl, actionContext) {
const didSetSource = handler.audioPlayer.player.src !== sourceUrl;
let appliedSourceUrl = null;
if (didSetSource) {
handler.audioPlayer.player.src = sourceUrl;
appliedSourceUrl = sourceUrl;
}
try {
if (didSetSource) await handler.audioPlayer.init();
if (handler.isActionStale(actionContext)) {
await rollbackStaleAppliedSourceIfStillCurrent(handler, appliedSourceUrl);
return {
status: "stale",
didSetSource,
appliedSourceUrl
};
}
const resumeResult = await resumePlayerAudioContextIfNeeded(handler);
if (resumeResult === "timeout") debug.log("[updateTranslation] continuing after AudioContext resume timeout");
else if (resumeResult === "failed") debug.log("[updateTranslation] AudioContext resume failed, continue without deferred resume");
if (handler.isActionStale(actionContext)) {
await rollbackStaleAppliedSourceIfStillCurrent(handler, appliedSourceUrl);
return {
status: "stale",
didSetSource,
appliedSourceUrl
};
}
if (!handler.video.paused && handler.audioPlayer.player.src) handler.audioPlayer.player.lipSync("play");
return {
status: "success",
didSetSource,
appliedSourceUrl
};
} catch (error) {
return {
status: "error",
didSetSource,
appliedSourceUrl,
error
};
}
}
async function updateTranslation(audioUrl, actionContext) {
await this.waitForPendingStopTranslate();
if (this.isActionStale(actionContext)) return;
if (!this.audioPlayer) this.createPlayer();
if (this.audioPlayer.audioContext?.state === "closed") {
debug.log("[updateTranslation] AudioContext is closed, recreating player");
this.createPlayer();
}
const normalizedTargetUrl = normalizeManagedAudioUrl(this, audioUrl);
const currentSource = this.audioPlayer.player.currentSrc || this.audioPlayer.player.src || "";
const nextAudioUrl = normalizedTargetUrl !== normalizeManagedAudioUrl(this, currentSource) ? await this.validateAudioUrl(normalizedTargetUrl, actionContext) : normalizedTargetUrl;
if (this.isActionStale(actionContext)) return;
const resolvedSource = await applyTranslationWithDirectFallback(this, nextAudioUrl, actionContext);
const resolvedAudioUrl = resolvedSource.nextAudioUrl;
const applyResult = resolvedSource.applyResult;
const appliedSourceUrl = applyResult.appliedSourceUrl;
if (applyResult.status === "stale") return;
if (applyResult.status === "error") {
debug.log("this.audioPlayer.init() error", applyResult.error);
await rollbackStaleAppliedSourceIfStillCurrent(this, appliedSourceUrl);
const msg = toErrorMessage(applyResult.error);
this.transformBtn("error", msg);
return;
}
this.clearVolumeLinkState();
this.setupAudioSettings();
this.transformBtn("success", localizationProvider.get("disableTranslate"));
this.afterUpdateTranslation(resolvedAudioUrl);
}
function syncTranslationPlaybackVolume() {
const player = this.audioPlayer?.player;
const nextVolume = this.uiManager.votOverlayView?.translationVolumeSlider?.value;
applyTranslationPlaybackVolume(player, nextVolume, this.data?.defaultVolume);
}
async function applyTranslationWithDirectFallback(handler, audioUrl, actionContext) {
const nextAudioUrl = audioUrl;
const applyResult = await applyTranslationSource(handler, nextAudioUrl, actionContext);
if (!shouldRetryTranslationSource(handler, applyResult, actionContext, nextAudioUrl)) return {
nextAudioUrl,
applyResult
};
const retried = await retryTranslationWithDirectSource(handler, nextAudioUrl, applyResult.appliedSourceUrl, actionContext);
if (retried) return retried;
return {
nextAudioUrl,
applyResult
};
}
function shouldRetryTranslationSource(handler, applyResult, actionContext, audioUrl) {
return applyResult.status === "error" && applyResult.didSetSource && !handler.isActionStale(actionContext) && handler.unproxifyAudio(audioUrl) !== audioUrl;
}
async function retryTranslationWithDirectSource(handler, audioUrl, appliedSourceUrl, actionContext) {
const directUrl = handler.unproxifyAudio(audioUrl);
debug.log("[updateTranslation] proxied audio init failed, retrying direct URL");
try {
const validatedDirectUrl = await handler.validateAudioUrl(directUrl, actionContext);
if (handler.isActionStale(actionContext)) {
await rollbackStaleAppliedSourceIfStillCurrent(handler, appliedSourceUrl);
return {
nextAudioUrl: validatedDirectUrl,
applyResult: {
status: "stale",
didSetSource: true,
appliedSourceUrl
}
};
}
return {
nextAudioUrl: validatedDirectUrl,
applyResult: await applyTranslationSource(handler, validatedDirectUrl, actionContext)
};
} catch (fallbackErr) {
return {
nextAudioUrl: audioUrl,
applyResult: {
status: "error",
didSetSource: true,
appliedSourceUrl,
error: fallbackErr
}
};
}
}
async function translateFunc(VIDEO_ID, _isStream, requestLang, responseLang, translationHelp) {
await this.waitForPendingStopTranslate();
debug.log("Run videoValidator");
await this.videoValidator();
if (this.actionsAbortController?.signal?.aborted) this.resetActionsAbortController("translateFunc");
const overlayView = this.uiManager.votOverlayView;
if (!overlayView?.votButton) {
debug.log("[translateFunc] Overlay view missing, skipping translation");
return;
}
overlayView.votButton.loading = true;
this.hadAsyncWait = false;
this.volumeOnStart = this.getVideoVolume();
if (!VIDEO_ID) {
debug.log("Skip translation - no VIDEO_ID resolved yet");
await this.updateTranslationErrorMsg(new VOTLocalizedError("VOTNoVideoIDFound"), this.actionsAbortController.signal);
return;
}
const videoData = this.videoData;
if (!videoData) {
await this.updateTranslationErrorMsg(new VOTLocalizedError("VOTNoVideoIDFound"), this.actionsAbortController.signal);
return;
}
const normalizedTranslationHelp = normalizeTranslationHelp(translationHelp);
const cacheKey = this.getTranslationCacheKey(VIDEO_ID, requestLang, responseLang, normalizedTranslationHelp);
const activeKey = `video_${cacheKey}`;
if (this.activeTranslation?.key === activeKey) {
debug.log("[translateFunc] Reusing in-flight translation");
await this.activeTranslation.promise;
return;
}
const actionContext = {
gen: this.actionsGeneration,
videoId: VIDEO_ID
};
const translationPromise = (async () => {
if (this.isActionStale(actionContext)) {
debug.log("[translateFunc] Stale translation task - skipping");
return;
}
const reqLang = requestLang;
const resLang = responseLang;
const applyTranslationUrl = async (url) => await this.updateTranslation(url, actionContext);
const cachedEntry = this.cacheManager.getTranslation(cacheKey);
if (cachedEntry?.url) {
await applyTranslationUrl(cachedEntry.url);
debug.log("[translateFunc] Cached translation was received");
return;
}
const translateRes = await requestApplyAndCacheTranslation(this, {
videoData,
requestLang: reqLang,
responseLang: resLang,
translationHelp: normalizedTranslationHelp,
actionContext,
cacheKey,
cacheVideoId: VIDEO_ID,
cacheRequestLang: requestLang,
cacheResponseLang: responseLang,
onBeforeCache: async () => {
const preferredSubtitleLanguage = this.getPreferredSubtitlesLanguage(videoData.detectedLanguage, videoData.responseLanguage);
if (!preferredSubtitleLanguage) return;
const subsCacheKey = this.videoData ? this.getSubtitlesCacheKey(VIDEO_ID, this.videoData.detectedLanguage, preferredSubtitleLanguage) : null;
const cachedSubs = subsCacheKey ? this.cacheManager.getSubtitles(subsCacheKey) : null;
if (!(Array.isArray(cachedSubs) && pickBestSubtitlesIndex(getIndexedSubtitleDescriptors(cachedSubs), videoData.detectedLanguage, preferredSubtitleLanguage) != null)) {
if (subsCacheKey) this.cacheManager.deleteSubtitles(subsCacheKey);
this.subtitles = [];
this.subtitlesCacheKey = null;
}
}
});
debug.log("[translateRes]", translateRes);
if (!translateRes) debug.log("Skip translation");
})();
this.activeTranslation = {
key: activeKey,
promise: translationPromise
};
try {
return await translationPromise;
} catch (err) {
this.hadAsyncWait = notifyTranslationFailureIfNeeded({
aborted: this.actionsAbortController.signal.aborted,
translateApiErrorsEnabled: Boolean(this.data?.translateAPIErrors),
hadAsyncWait: this.hadAsyncWait,
videoId: VIDEO_ID,
error: err,
notify: (params) => this.notifier.translationFailed(params)
});
throw err;
} finally {
if (this.activeTranslation?.promise === translationPromise) this.activeTranslation = null;
const overlayBtn = this.uiManager.votOverlayView?.votButton;
if (!this.activeTranslation && overlayBtn?.loading && !this.hasActiveSource()) {
debug.log("[translateFunc] clearing stale loading state");
this.transformBtn("none", localizationProvider.get("translateVideo"));
}
}
}
function isYouTubeHosts() {
return isTranslationDownloadHost(this.site.host);
}
//#endregion
//#region src/videoHandler/modules/events.ts
function mergeListenerSignals(primary, secondary) {
if (!secondary || secondary === primary) return primary;
const signals = [primary, secondary];
if (typeof AbortSignal.any === "function") return AbortSignal.any(signals);
const controller = new AbortController();
for (const signal of signals) {
const abort = () => controller.abort(signal.reason);
if (signal.aborted) {
abort();
break;
}
signal.addEventListener("abort", abort, {
once: true,
signal: controller.signal
});
}
return controller.signal;
}
function createScopedListeners(signal) {
const add = (element, event, handler, options) => {
element.addEventListener(event, handler, {
...options,
signal: mergeListenerSignals(signal, options?.signal)
});
};
const addMany = (element, events, handler, options) => {
for (const event of events) add(element, event, handler, options);
};
return {
add,
addMany
};
}
function bindOverlayHoverFocusEvents(addMany, target, overlayVisibility) {
const handleInteraction = (event) => overlayVisibility.handleOverlayInteraction(event);
const scheduleHide = (event) => overlayVisibility.scheduleHide(event);
if (isIframe() && globalThis.window !== void 0) {
addMany(target, ["focusin"], handleInteraction);
addMany(target, ["focusout"], scheduleHide);
return;
}
addMany(target, ["focusin", "pointerenter"], handleInteraction);
addMany(target, ["pointermove"], handleInteraction, { passive: true });
addMany(target, ["focusout", "pointerleave"], scheduleHide);
}
function toPercentInt(value, fallback = 0) {
const numeric = typeof value === "number" ? value : Number(value);
return Number.isFinite(numeric) ? clampPercentInt(numeric) : fallback;
}
function syncAudioTranslationVolumeFromVideo(self, videoPercent, options = {}) {
if (options.skipYouTubeLikeHosts && isYouTubeLikeHost(self.site.host)) return;
if (self.smartVolumeDuckingInterval !== void 0) return;
if (!self.data?.syncVolume || !self.audioPlayer?.player?.src) return;
if (self.isLikelyInternalVideoVolumeChange(videoPercent)) return;
self.syncVolumeWrapper("video", videoPercent);
}
function applyOverlayLayout(self, overlayView, heightPx) {
const menu = overlayView.votMenu?.container;
if (menu) {
let height;
if (heightPx) height = heightPx;
else if (self.fullscreenHelper) {
const target = self.fullscreenHelper.getResizeObserverTarget();
height = target.getBoundingClientRect().height || target.clientHeight || window.innerHeight * .75;
} else height = self.video.getBoundingClientRect().height;
if (!height || height < 200) height = window.innerHeight * .75;
menu.style.setProperty("--vot-container-height", `${height}px`);
}
const { position, direction } = overlayView.calcButtonLayout(self.data?.buttonPos ?? "default");
overlayView.updateButtonLayout(position, direction);
}
function normalizeHotkeyPart(value) {
return value.replace("Key", "").replace("Digit", "");
}
function buildPressedHotkeyPartsSet(userPressedKeys) {
const pressedParts = /* @__PURE__ */ new Set();
for (const key of userPressedKeys) pressedParts.add(normalizeHotkeyPart(key));
return pressedParts;
}
function getParsedHotkey(hotkey, cache) {
if (!hotkey) return null;
const cached = cache.get(hotkey);
if (cached) return cached;
const parts = hotkey.split("+").filter(Boolean).map(normalizeHotkeyPart);
const parsed = {
parts,
partsSet: new Set(parts)
};
cache.set(hotkey, parsed);
return parsed;
}
function isHotkeyMatch(pressedParts, hotkey) {
if (!hotkey) return false;
if (pressedParts.size !== hotkey.parts.length) return false;
for (const key of hotkey.partsSet) if (!pressedParts.has(key)) return false;
return true;
}
function bindOverlayLayoutEvents(ctx) {
const { self, overlayView, addMany } = ctx;
const syncMountAndLayout = () => {
self.refreshOverlayMount();
applyOverlayLayout(self, overlayView);
};
self.resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) applyOverlayLayout(self, overlayView, entry.contentRect.height);
});
self.resizeObserver.observe(self.video);
syncMountAndLayout();
addMany(document, ["fullscreenchange", "webkitfullscreenchange"], () => syncMountAndLayout());
addMany(self.video, ["webkitbeginfullscreen", "webkitendfullscreen"], () => syncMountAndLayout());
}
function bindYouTubeVolumeSync(ctx) {
const { self } = ctx;
if (!isDesktopYouTubeLikeSite(self.site)) return;
self.syncVolumeObserver = new MutationObserver((mutations) => {
if (!self.audioPlayer?.player?.src) return;
let hasVolumeMutation = false;
for (const mutation of mutations) {
if (mutation.type !== "attributes" || mutation.attributeName !== "aria-valuenow") continue;
hasVolumeMutation = true;
}
if (!hasVolumeMutation) return;
self.syncVideoVolumeSlider();
const activeOverlayView = self.uiManager.votOverlayView;
if (!activeOverlayView?.isInitialized()) return;
syncAudioTranslationVolumeFromVideo(self, toPercentInt(activeOverlayView.videoVolumeSlider.value));
});
const ytpVolumePanel = document.querySelector(".ytp-volume-panel");
if (!ytpVolumePanel) return;
self.syncVolumeObserver.observe(ytpVolumePanel, {
attributes: true,
subtree: true,
attributeFilter: ["aria-valuenow"]
});
}
function bindAudioTrackLanguageSync(ctx) {
const { self } = ctx;
if (self.site.host !== "youtube" || self.site.additionalData === "mobile") return;
const syncAudioTrackLanguage = async () => {
try {
if (!self.videoData) return;
const player = YoutubeHelper.getPlayer();
const availableTracks = player?.getAvailableAudioTracks?.() ?? null;
if (!Array.isArray(availableTracks) || availableTracks.length <= 1) return;
const currentTrackId = (player?.getAudioTrack?.()?.getLanguageInfo?.())?.id;
const currentLanguageCode = currentTrackId && currentTrackId !== "und" ? currentTrackId.toLowerCase().split(/[-_.]/)[0] : void 0;
if (!currentLanguageCode) return;
if (!availableLangs.includes(currentLanguageCode)) return;
const currentLanguage = currentLanguageCode;
if (currentLanguage === self.videoData.detectedLanguage) return;
self.videoManager.rememberDetectedLanguage(self.videoData.videoId, currentLanguage);
self.setSelectMenuValues(currentLanguage, self.videoData.responseLanguage);
if (self.data?.autoTranslate && currentLanguage !== self.videoData.responseLanguage) {
debug.log(`[VOT] Audio track language changed to ${currentLanguage}, triggering auto-translation`);
try {
await self.uiManager.handleTranslationBtnClick();
} catch (error) {
debug.log("[VOT] Failed to trigger auto-translation on audio track change:", error);
}
}
} catch (error) {
debug.log("[VOT] Failed to sync audio track language", error);
}
};
const player = YoutubeHelper.getPlayer();
const listeners = ["onApiChange", "onStateChange"];
if (player?.addEventListener) for (const eventName of listeners) try {
player.addEventListener(eventName, syncAudioTrackLanguage);
} catch (error) {
debug.log(`[VOT] Failed to bind ${eventName}`, error);
}
syncAudioTrackLanguage();
self.abortController.signal.addEventListener("abort", () => {
if (!player?.removeEventListener) return;
for (const eventName of listeners) try {
player.removeEventListener(eventName, syncAudioTrackLanguage);
} catch (error) {
debug.log(`[VOT] Failed to unbind ${eventName}`, error);
}
}, { once: true });
}
function bindGlobalDismissAndHotkeys(ctx) {
const { self, overlayView, add, addMany, platformConfig } = ctx;
add(document, "click", (event) => {
const target = event.target;
const button = overlayView.votButton?.container;
const menu = overlayView.votMenu?.container;
const settings = self.uiManager.votSettingsView?.dialog?.container;
const path = event.composedPath();
const isInPath = (element) => Boolean(element && path.includes(element));
const isButton = isInPath(button);
const isMenu = isInPath(menu);
const isVideo = isInPath(self.container);
const isSettings = isInPath(settings);
const isTempDialog = target instanceof Element && target.closest(".vot-dialog-temp") instanceof Element;
debug.log(`[document click] ${isButton} ${isMenu} ${isVideo} ${isSettings} ${isTempDialog}`);
if (isButton || isMenu || isSettings || isTempDialog) return;
if (!isVideo) overlayView.updateButtonOpacity(0);
if (menu && !menu.hidden) {
menu.hidden = true;
self.overlayVisibility?.queueAutoHide();
}
});
const userPressedKeys = /* @__PURE__ */ new Set();
const hotkeyCache = /* @__PURE__ */ new Map();
const clearUserPressedKeys = () => userPressedKeys.clear();
const runHotkeyAction = (action, actionName) => {
action().catch((error) => {
debug.log(`[VOT] ${actionName} hotkey action failed`, error);
});
};
add(document, "keydown", (event) => {
const keyboardEvent = event;
if (keyboardEvent.repeat) return;
userPressedKeys.add(keyboardEvent.code);
const activeElement = getDeepActiveElement(document);
const activeTag = activeElement?.tagName?.toLowerCase?.() ?? "";
if (["input", "textarea"].includes(activeTag) || Boolean(activeElement?.isContentEditable)) return;
const pressedParts = buildPressedHotkeyPartsSet(userPressedKeys);
if (isHotkeyMatch(pressedParts, getParsedHotkey(self.data?.translationHotkey, hotkeyCache))) {
clearUserPressedKeys();
runHotkeyAction(() => self.uiManager.handleTranslationBtnClick(), "Translation");
return;
}
if (isHotkeyMatch(pressedParts, getParsedHotkey(self.data?.subtitlesHotkey, hotkeyCache))) {
clearUserPressedKeys();
runHotkeyAction(() => self.toggleSubtitlesForCurrentLangPair(), "Subtitles");
}
});
add(document, "keyup", (event) => userPressedKeys.delete(event.code));
add(document, "blur", clearUserPressedKeys);
add(document, "visibilitychange", () => {
if (document.hidden) clearUserPressedKeys();
});
add(globalThis, "blur", clearUserPressedKeys);
const eventContainer = self.getEventContainer();
if (eventContainer) {
const useWindowEvents = isIframe() && globalThis.window !== void 0;
const interactionTarget = useWindowEvents ? globalThis.window : eventContainer;
if (useWindowEvents) {
addMany(interactionTarget, ["pointermove", "pointerdown"], (event) => self.overlayVisibility.handleHostInteraction(event), { passive: true });
add(interactionTarget, "blur", () => self.overlayVisibility.scheduleHide());
} else {
addMany(interactionTarget, ["pointerenter", "pointerdown"], (event) => self.overlayVisibility.handleHostInteraction(event));
add(interactionTarget, "pointermove", (event) => self.overlayVisibility.handleHostInteraction(event), { passive: true });
add(interactionTarget, "pointerleave", (event) => self.overlayVisibility.scheduleHide(event));
}
}
self.rebindOverlayVisibilityTargets();
if (platformConfig.allowTouchMoveHandler) add(document, "touchmove", (event) => self.overlayVisibility.handleHostInteraction(event), { passive: true });
if (platformConfig.disableContainerDrag) self.container.draggable = false;
}
function bindPlaybackRefreshOnResume(ctx) {
const { self, add } = ctx;
let wasPausedSinceLastPlay = false;
const resetPauseState = () => {
wasPausedSinceLastPlay = false;
};
add(self.video, "pause", () => {
wasPausedSinceLastPlay = true;
});
add(self.video, "playing", () => {
if (!wasPausedSinceLastPlay) return;
wasPausedSinceLastPlay = false;
handlePlaybackResumedTranslationRefresh.call(self).catch((error) => {
debug.log("[VOT] Failed to refresh translation after playback resumed", error);
});
});
add(self.video, "loadstart", resetPauseState);
add(self.video, "emptied", resetPauseState);
}
function bindVideoLifecycleEvents(ctx) {
const { self, overlayView, add } = ctx;
const safeSetCanPlay = async () => {
try {
await self.setCanPlay();
} catch (err) {
debug.log("[VOT] setCanPlay() failed", err);
}
};
let setCanPlayQueued = false;
const queueSetCanPlay = () => {
if (setCanPlayQueued) return;
setCanPlayQueued = true;
queueMicrotask(async () => {
setCanPlayQueued = false;
await safeSetCanPlay();
});
};
add(self.video, "canplay", () => {
if (self.site.host === "rutube" && self.video.src) return;
queueSetCanPlay();
});
const handleVideoEmptied = async () => {
let videoId;
try {
videoId = await getVideoID(self.site, {
fetchFn: GM_fetch,
video: self.video
});
} catch (error) {
debug.log("[VOT] Failed to resolve video id on emptied", error);
}
if (self.videoData && videoId && videoId === self.videoData.videoId) return;
debug.log("lipsync mode is emptied");
resetAndHideLifecycle(self, overlayView, {
clearVideoData: true,
hideMenu: true
});
};
add(self.video, "emptied", () => {
handleVideoEmptied().catch((error) => {
debug.log("[VOT] Failed to handle emptied lifecycle event", error);
});
});
if (!isMuteSyncDisabledHost(self.site.host)) add(self.video, "volumechange", () => {
self.syncVideoVolumeSlider();
const activeOverlayView = self.uiManager.votOverlayView;
if (!activeOverlayView?.isInitialized()) return;
syncAudioTranslationVolumeFromVideo(self, toPercentInt(activeOverlayView.videoVolumeSlider.value), { skipYouTubeLikeHosts: true });
});
if (self.site.host === "youtube" && !self.site.additionalData) add(document, "yt-page-data-updated", () => {
debug.log("yt-page-data-updated");
if (!globalThis.location.pathname.startsWith("/shorts/")) return;
queueSetCanPlay();
});
}
function initExtraEvents() {
const overlayView = this.uiManager.votOverlayView;
if (!overlayView?.subtitlesSelect) return;
const { add, addMany } = createScopedListeners(this.abortController.signal);
const ctx = {
self: this,
overlayView,
platformConfig: getPlatformEventConfig(this.site.host),
add,
addMany
};
bindPlaybackRefreshOnResume(ctx);
bindOverlayLayoutEvents(ctx);
bindYouTubeVolumeSync(ctx);
bindAudioTrackLanguageSync(ctx);
bindGlobalDismissAndHotkeys(ctx);
bindVideoLifecycleEvents(ctx);
}
function rebindOverlayVisibilityTargets() {
this.overlayVisibilityTargetsAbortController?.abort();
this.overlayVisibilityTargetsAbortController = new AbortController();
const { signal } = this.overlayVisibilityTargetsAbortController;
const overlayButton = this.uiManager?.votOverlayView?.votButton?.container;
const overlayMenu = this.uiManager?.votOverlayView?.votMenu?.container;
if (!overlayButton || !overlayMenu || !this.overlayVisibility) return;
const overlayVisibility = this.overlayVisibility;
const { addMany } = createScopedListeners(signal);
bindOverlayHoverFocusEvents(addMany, overlayButton, overlayVisibility);
bindOverlayHoverFocusEvents(addMany, overlayMenu, overlayVisibility);
}
function isOverlayInteractiveNode(node) {
if (!(node instanceof Node)) return false;
const overlayView = this.uiManager?.votOverlayView;
const buttonContainer = overlayView?.votButton?.container;
const menuContainer = overlayView?.votMenu?.container;
return buttonContainer instanceof Node && containsCrossShadow(buttonContainer, node) || menuContainer instanceof Node && containsCrossShadow(menuContainer, node);
}
function getAutoHideDelay() {
const delay = this.data?.autoHideButtonDelay;
return typeof delay === "number" && Number.isFinite(delay) ? delay : defaultAutoHideDelay;
}
function releaseExtraEvents() {
this.resizeObserver?.disconnect();
this.overlayVisibilityTargetsAbortController?.abort();
this.overlayVisibilityTargetsAbortController = void 0;
if (isDesktopYouTubeLikeSite(this.site)) this.syncVolumeObserver?.disconnect();
}
//#endregion
//#region src/videoHandler/shared.ts
/**
* Country code used for proxy settings. Populated lazily during init.
*/
var countryCode;
function setCountryCode(next) {
countryCode = next;
}
//#endregion
//#region src/videoHandler/modules/init.ts
var countryCodeRequestInFlight = null;
async function ensureCountryCode() {
if (countryCode) return;
countryCodeRequestInFlight ??= (async () => {
try {
setCountryCode((await (await GM_fetch("https://cloudflare-dns.com/cdn-cgi/trace", { timeout: 7e3 })).text()).split("\n").find((line) => line.startsWith("loc="))?.slice(4, 6).toUpperCase());
} catch (err) {
console.error("[VOT] Error getting country:", err);
}
})().finally(() => {
countryCodeRequestInFlight = null;
});
await countryCodeRequestInFlight;
}
async function init() {
if (this.initialized) return;
const audioContextSupported = this.isAudioContextSupported;
this.data = await votStorage.getValues({
autoTranslate: false,
autoSubtitles: false,
dontTranslateLanguages: [calculatedResLang],
enabledDontTranslateLanguages: true,
enabledAutoVolume: true,
enabledSmartDucking: true,
autoVolume: 15,
buttonPos: "default",
showVideoSlider: true,
syncVolume: false,
downloadWithName: isSupportGMXhr,
sendNotifyOnComplete: false,
subtitlesMaxLength: 300,
subtitlesSmartLayout: true,
highlightWords: false,
subtitlesFontSize: 20,
subtitlesFontFamily: "default-sans",
subtitlesOpacity: 20,
subtitlesDownloadFormat: "srt",
responseLanguage: calculatedResLang,
responseLanguageSubtitles: "auto",
defaultVolume: 100,
onlyBypassMediaCSP: audioContextSupported,
newAudioPlayer: audioContextSupported,
showPiPButton: false,
translateAPIErrors: true,
translationService: defaultTranslationService,
detectService: defaultDetectService,
translationHotkey: null,
subtitlesHotkey: null,
m3u8ProxyHost,
proxyWorkerHost,
translateProxyEnabled: 0,
translateProxyEnabledDefault: true,
audioBooster: false,
useLivelyVoice: false,
autoHideButtonDelay: defaultAutoHideDelay,
useAudioDownload: isSupportGMXhr,
compatVersion: "",
account: {},
localeHash: "",
localeUpdatedAt: 0
});
if (this.data.compatVersion !== "2025-05-09") {
this.data = await updateConfig(this.data);
await votStorage.set("compatVersion", actualCompatVersion);
}
try {
if (calculatedResLang === "en" && this.data?.enabledDontTranslateLanguages && Array.isArray(this.data?.dontTranslateLanguages) && this.data.dontTranslateLanguages.length === 1 && this.data.dontTranslateLanguages[0] === "en" && typeof this.data.responseLanguage === "string" && this.data.responseLanguage !== "en") {
const responseLang = this.data.responseLanguage;
this.data.dontTranslateLanguages = [responseLang];
await votStorage.set("dontTranslateLanguages", this.data.dontTranslateLanguages);
}
} catch {}
this.uiManager.data = this.data;
console.log("[VOT] data from db:", this.data);
if (!this.data.translateProxyEnabled && isProxyOnlyExtension) this.data.translateProxyEnabled = 1;
await ensureCountryCode();
if (countryCode && proxyOnlyCountries.includes(countryCode) && this.data.translateProxyEnabledDefault) this.data.translateProxyEnabled = 2;
debug.log("translateProxyEnabled", this.data.translateProxyEnabled, this.data.translateProxyEnabledDefault);
debug.log("Extension compatibility passed...");
await this.initVOTClient();
this.uiManager.initUI();
this.uiManager.initUIEvents();
if (this.uiManager.votOverlayView?.votButton?.container) this.uiManager.votOverlayView.votButton.container.hidden = true;
this.createPlayer();
this.translateToLang = this.data.responseLanguage ?? "ru";
this.initExtraEvents();
this.initialized = true;
}
//#endregion
//#region src/subtitles/segmenter.ts
var DEFAULT_CACHE_LOCALE = "und";
var wordSegmenterCache = /* @__PURE__ */ new Map();
var sentenceSegmenterCache = /* @__PURE__ */ new Map();
var resolvedLocaleCache = /* @__PURE__ */ new Map();
var canonicalizeLocale = (locale) => {
if (!locale) return void 0;
try {
return Intl.getCanonicalLocales(locale)[0];
} catch {
return;
}
};
var resolveSegmenterLocale = (locale) => {
const cacheKey = locale ?? DEFAULT_CACHE_LOCALE;
if (resolvedLocaleCache.has(cacheKey)) return resolvedLocaleCache.get(cacheKey);
const canonicalLocale = canonicalizeLocale(locale);
if (!canonicalLocale) {
resolvedLocaleCache.set(cacheKey, void 0);
return;
}
const resolvedLocale = Intl.Segmenter.supportedLocalesOf([canonicalLocale])[0];
resolvedLocaleCache.set(cacheKey, resolvedLocale);
return resolvedLocale;
};
var getSegmenter = (locale, granularity) => {
const resolvedLocale = resolveSegmenterLocale(locale);
const cacheKey = `${granularity}:${resolvedLocale ?? DEFAULT_CACHE_LOCALE}`;
const segmenterCache = granularity === "sentence" ? sentenceSegmenterCache : wordSegmenterCache;
const cached = segmenterCache.get(cacheKey);
if (cached) return cached;
const segmenter = new Intl.Segmenter(resolvedLocale, { granularity });
segmenterCache.set(cacheKey, segmenter);
return segmenter;
};
var segmentText = (text, locale) => {
if (!text) return [];
return Array.from(getSegmenter(locale, "word").segment(text), (part) => ({
text: part.segment,
index: part.index,
isWordLike: Boolean(part.isWordLike)
}));
};
var segmentSentences = (text, locale) => {
if (!text) return [];
return Array.from(getSegmenter(locale, "sentence").segment(text), (part) => ({
text: part.segment,
index: part.index
}));
};
//#endregion
//#region src/subtitles/textSpacing.ts
var WHITESPACE_RE$1 = /\s/u;
var NO_SPACE_BEFORE_RE = /^[,.:;!?%)\]}>»]/u;
var NO_SPACE_AFTER_RE = /[([{<«'"-]$/u;
var CJK_CHAR_RE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
var shouldInsertSpaceBetweenTextFragments = (leftText, rightText) => {
const leftLastChar = leftText.at(-1) ?? "";
const rightFirstChar = rightText[0] ?? "";
if (!leftLastChar || !rightFirstChar) return false;
if (WHITESPACE_RE$1.test(leftLastChar) || WHITESPACE_RE$1.test(rightFirstChar)) return false;
if (NO_SPACE_AFTER_RE.test(leftLastChar) || NO_SPACE_BEFORE_RE.test(rightFirstChar)) return false;
if (CJK_CHAR_RE.test(leftLastChar) && CJK_CHAR_RE.test(rightFirstChar)) return false;
return true;
};
//#endregion
//#region src/subtitles/autoMerge.ts
var WHITESPACE_RE = /\s/u;
var hasVisibleText = (value) => Boolean(value.trim());
var buildNormalizedLineSpans = (lines) => {
let streamText = "";
let previousText = "";
const spans = [];
for (const line of lines) {
const normalizedText = line.text.trim();
if (!normalizedText) continue;
if (streamText && shouldInsertSpaceBetweenTextFragments(previousText, normalizedText)) streamText += " ";
const start = streamText.length;
streamText += normalizedText;
const end = streamText.length;
spans.push({
line: {
...line,
text: normalizedText
},
text: normalizedText,
start,
end
});
previousText = normalizedText;
}
return {
streamText,
spans
};
};
var trimSentenceRange = (streamText, start, end) => {
let trimmedStart = start;
let trimmedEnd = end;
while (trimmedStart < trimmedEnd && WHITESPACE_RE.test(streamText[trimmedStart] ?? "")) trimmedStart += 1;
while (trimmedEnd > trimmedStart && WHITESPACE_RE.test(streamText[trimmedEnd - 1] ?? "")) trimmedEnd -= 1;
if (trimmedStart >= trimmedEnd) return null;
return {
start: trimmedStart,
end: trimmedEnd
};
};
var sliceLineToken = (span, rangeStart, rangeEnd) => {
const overlapStart = Math.max(rangeStart, span.start);
const overlapEnd = Math.min(rangeEnd, span.end);
if (overlapStart >= overlapEnd) return null;
const localStart = overlapStart - span.start;
const localEnd = overlapEnd - span.start;
const text = span.text.slice(localStart, localEnd);
if (!text) return null;
const textLength = Math.max(span.text.length, 1);
const startMs = span.line.startMs + Math.round(span.line.durationMs * localStart / textLength);
const endMs = span.line.startMs + Math.round(span.line.durationMs * localEnd / textLength);
return {
text,
startMs,
durationMs: Math.max(0, endMs - startMs),
isWordLike: hasVisibleText(text)
};
};
var buildSentenceLine = (streamText, spans, segment) => {
const rawStart = segment.index;
const trimmedRange = trimSentenceRange(streamText, rawStart, rawStart + segment.text.length);
if (!trimmedRange) return null;
const text = streamText.slice(trimmedRange.start, trimmedRange.end);
if (!hasVisibleText(text)) return null;
const tokens = [];
let speakerId = "0";
for (const span of spans) {
const token = sliceLineToken(span, trimmedRange.start, trimmedRange.end);
if (!token) continue;
if (tokens.length === 0) speakerId = span.line.speakerId;
tokens.push(token);
}
if (!tokens.length) return null;
const startMs = Math.min(...tokens.map((token) => token.startMs));
const endMs = Math.max(...tokens.map((token) => token.startMs + Math.max(0, token.durationMs)));
return {
text,
startMs,
durationMs: Math.max(0, endMs - startMs),
speakerId,
tokens
};
};
var mergeAutoGeneratedSubtitleLines = (lines, locale) => {
const { streamText, spans } = buildNormalizedLineSpans(lines);
if (!streamText || !spans.length) return [];
const sentenceLines = segmentSentences(streamText, locale).map((segment) => buildSentenceLine(streamText, spans, segment)).filter((line) => line !== null);
return sentenceLines.length ? sentenceLines : spans.map(({ line }) => line);
};
//#endregion
//#region src/subtitles/processor.ts
var toFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
var toNonNegativeNumber = (value) => Math.max(0, toFiniteNumber(value));
var pickDescriptorFromVideoData = (videoData, requestLang, spokenLang) => {
const list = videoData.subtitles;
if (!Array.isArray(list) || list.length === 0) return null;
const desiredLang = requestLang ?? spokenLang;
if (desiredLang) {
const translated = list.find((subtitle) => subtitle.language === desiredLang && typeof subtitle.translatedFromLanguage === "string");
if (translated) return translated;
const original = list.find((subtitle) => subtitle.language === desiredLang);
if (original) return original;
}
return list[0] ?? null;
};
var isVideoDataForSubtitles = (value) => "host" in value && "videoId" in value && "detectedLanguage" in value && "duration" in value && typeof value.host === "string" && typeof value.videoId === "string" && typeof value.detectedLanguage === "string" && typeof value.duration === "number";
var appendYoutubePoTokenParams = (inputUrl) => {
const poToken = YoutubeHelper.getPoToken();
if (!poToken) return inputUrl;
let normalizedPoToken = poToken;
for (let i = 0; i < 10; i += 1) {
let nextValue;
try {
nextValue = decodeURIComponent(normalizedPoToken);
} catch {
break;
}
if (nextValue === normalizedPoToken) break;
normalizedPoToken = nextValue;
}
const deviceParamsRaw = YoutubeHelper.getDeviceParams();
const normalizedDeviceParams = typeof deviceParamsRaw === "string" ? deviceParamsRaw.replace(/^[?&]+/u, "") : "";
try {
const parsed = new URL(inputUrl);
parsed.searchParams.set("potc", "1");
parsed.searchParams.set("pot", normalizedPoToken);
if (normalizedDeviceParams) {
const deviceParams = new URLSearchParams(normalizedDeviceParams);
for (const [key, value] of deviceParams.entries()) parsed.searchParams.set(key, value);
}
return parsed.toString();
} catch {
const separator = inputUrl.includes("?") ? "&" : "?";
const serializedDeviceParams = normalizedDeviceParams ? `&${normalizedDeviceParams}` : "";
return `${inputUrl}${separator}potc=1&pot=${encodeURIComponent(normalizedPoToken)}${serializedDeviceParams}`;
}
};
var compareNumbers = (left, right) => left - right;
var compareStrings = (left, right) => {
if (left < right) return -1;
if (left > right) return 1;
return 0;
};
var compareRankArrays = (left, right) => {
const length = Math.min(left.length, right.length);
for (let i = 0; i < length; i += 1) {
const diff = left[i] - right[i];
if (diff !== 0) return diff;
}
if (left.length !== right.length) return left.length - right.length;
return 0;
};
var getYandexTranslationKindRank = (isYandex, requestLanguageRank, isTranslated) => {
if (!isYandex) return 0;
if (requestLanguageRank === 0) return isTranslated ? 1 : 0;
return isTranslated ? 0 : 1;
};
var getTranslatedFromRequestRank = (isYandex, isTranslated, translatedFromLanguage, requestLang) => {
if (!isYandex || !isTranslated) return 0;
return translatedFromLanguage === requestLang ? 0 : 1;
};
var buildSubtitleRank = (descriptor, requestLang) => {
const isYandex = descriptor.source === "yandex";
const sourceRank = isYandex ? 0 : 1;
const uiLanguageRank = descriptor.language === lang ? 0 : 1;
const isTranslated = Boolean(descriptor.translatedFromLanguage);
const requestLanguageRank = requestLang && descriptor.language === requestLang ? 0 : 1;
return [
sourceRank,
uiLanguageRank,
getYandexTranslationKindRank(isYandex, requestLanguageRank, isTranslated),
getTranslatedFromRequestRank(isYandex, isTranslated, descriptor.translatedFromLanguage, requestLang),
isYandex && !isTranslated ? requestLanguageRank : 0,
isYandex ? 0 : Number(Boolean(descriptor.isAutoGenerated))
];
};
var rankSubtitle = (descriptor, index, requestLang) => ({
descriptor,
index,
rank: buildSubtitleRank(descriptor, requestLang),
language: descriptor.language,
translatedFromLanguage: descriptor.translatedFromLanguage ?? "",
source: descriptor.source,
url: descriptor.url,
isAutoGenerated: Number(Boolean(descriptor.isAutoGenerated))
});
var sortSubtitles = (subtitles, requestLang) => {
const ranked = subtitles.map((descriptor, index) => rankSubtitle(descriptor, index, requestLang));
ranked.sort((left, right) => {
const rankDiff = compareRankArrays(left.rank, right.rank);
if (rankDiff !== 0) return rankDiff;
const descriptorOrder = compareStrings(left.language, right.language) || compareStrings(left.translatedFromLanguage, right.translatedFromLanguage) || compareStrings(left.source, right.source) || compareStrings(left.url, right.url) || compareNumbers(left.isAutoGenerated, right.isAutoGenerated);
if (descriptorOrder !== 0) return descriptorOrder;
return compareNumbers(left.index, right.index);
});
return ranked.map((entry) => entry.descriptor);
};
var resolveTokenWordLike = (value, text) => {
if (typeof value === "boolean") return value;
return Boolean(text.trim());
};
var sanitizeStyledSpan = (span) => {
if (!span || typeof span !== "object") return null;
const raw = span;
const start = toNonNegativeNumber(raw.start);
const end = toNonNegativeNumber(raw.end);
if (end <= start) return null;
return {
start,
end,
style: sanitizeSubtitleInlineStyle(raw.style)
};
};
var sanitizeLineMetadata = (value) => {
if (!value || typeof value !== "object") return;
const raw = value;
const metadata = {};
if (typeof raw.rawText === "string") metadata.rawText = raw.rawText;
const styledSpans = Array.isArray(raw.styledSpans) ? raw.styledSpans.map(sanitizeStyledSpan).filter((span) => span !== null) : [];
if (styledSpans.length) metadata.styledSpans = styledSpans;
if (raw.vtt && typeof raw.vtt === "object") metadata.vtt = raw.vtt;
if (raw.ass && typeof raw.ass === "object") metadata.ass = raw.ass;
return Object.keys(metadata).length ? metadata : void 0;
};
var sanitizeToken = (token) => {
if (!token || typeof token !== "object") return {
text: "",
startMs: 0,
durationMs: 0,
isWordLike: false
};
const raw = token;
const text = typeof raw.text === "string" ? raw.text : "";
return {
text,
startMs: toNonNegativeNumber(raw.startMs),
durationMs: toNonNegativeNumber(raw.durationMs),
isWordLike: resolveTokenWordLike(raw.isWordLike, text),
style: sanitizeSubtitleInlineStyle(raw.style)
};
};
var sanitizeLine = (line) => {
if (!line || typeof line !== "object") return {
text: "",
startMs: 0,
durationMs: 0,
speakerId: "0",
tokens: []
};
const raw = line;
const tokens = Array.isArray(raw.tokens) ? raw.tokens.map(sanitizeToken) : [];
return {
text: typeof raw.text === "string" ? raw.text : "",
startMs: toNonNegativeNumber(raw.startMs),
durationMs: toNonNegativeNumber(raw.durationMs),
speakerId: typeof raw.speakerId === "string" ? raw.speakerId : "0",
tokens,
metadata: sanitizeLineMetadata(raw.metadata)
};
};
var ensureProcessedSubtitles = (input) => {
if (!input || typeof input !== "object") return {
format: "json",
subtitles: []
};
const payload = input;
const subtitles = Array.isArray(payload.subtitles) ? payload.subtitles.map(sanitizeLine) : [];
return {
format: payload.format ?? "json",
subtitles
};
};
var getYoutubeEventDurationMs = (event, nextEvent) => {
if (!nextEvent) return Math.max(0, event.dDurationMs);
if (event.tStartMs + event.dDurationMs <= nextEvent.tStartMs) return Math.max(0, event.dDurationMs);
return Math.max(0, nextEvent.tStartMs - event.tStartMs);
};
var buildYoutubeSourceTokens = (event, segs, durationMs) => {
const sourceTokens = [];
let text = "";
let remainingDuration = durationMs;
let previousRawText = "";
for (let j = 0; j < segs.length; j += 1) {
const segment = segs[j];
const rawText = typeof segment.utf8 === "string" ? segment.utf8 : "";
if (!rawText) continue;
const offset = Math.max(0, segment.tOffsetMs ?? 0);
let segmentDuration = durationMs;
const nextSegment = segs[j + 1];
if (nextSegment?.tOffsetMs !== void 0) {
const nextOffset = Math.max(offset, nextSegment.tOffsetMs);
segmentDuration = Math.max(0, nextOffset - offset);
remainingDuration = Math.max(remainingDuration - segmentDuration, 0);
}
let tokenDuration = Math.max(0, remainingDuration);
if (nextSegment) tokenDuration = Math.max(0, segmentDuration);
if (text && shouldInsertSpaceBetweenTextFragments(previousRawText, rawText)) {
sourceTokens.push({
text: " ",
startMs: event.tStartMs + offset,
durationMs: 0,
isWordLike: false
});
text += " ";
}
sourceTokens.push({
text: rawText,
startMs: event.tStartMs + offset,
durationMs: tokenDuration,
isWordLike: Boolean(rawText.trim())
});
text += rawText;
previousRawText = rawText;
}
return {
text,
sourceTokens
};
};
var hasPositiveDuration = (token) => token.durationMs > 0;
var normalizeLineText = (line) => {
if (line.text) return normalizeSubtitleTextForDisplay(line.text);
if (!line.tokens.length) return "";
return normalizeSubtitleTextForDisplay(line.tokens.map((token) => token.text).join(""));
};
var allocateTimingsByLength = (texts, startMs, durationMs) => {
if (!texts.length) return [];
const safeDuration = Math.max(0, durationMs);
const weights = texts.map((text) => Math.max(text.length, 1));
const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
const prefixWeights = new Array(weights.length + 1).fill(0);
for (let i = 0; i < weights.length; i += 1) prefixWeights[i + 1] = prefixWeights[i] + weights[i];
const timings = [];
for (let i = 0; i < texts.length; i += 1) {
const from = Math.round(safeDuration * prefixWeights[i] / totalWeight);
const to = i === texts.length - 1 ? safeDuration : Math.round(safeDuration * prefixWeights[i + 1] / totalWeight);
timings.push({
startMs: startMs + from,
durationMs: Math.max(0, to - from)
});
}
return timings;
};
var splitSegmentText = (text) => {
const slices = [];
let sliceStart = 0;
for (let index = 0; index < text.length; index += 1) {
if (text[index] !== "\n") continue;
if (sliceStart < index) slices.push({
text: text.slice(sliceStart, index),
startOffset: sliceStart,
endOffset: index
});
slices.push({
text: "\n",
startOffset: index,
endOffset: index + 1
});
sliceStart = index + 1;
}
if (sliceStart < text.length) slices.push({
text: text.slice(sliceStart),
startOffset: sliceStart,
endOffset: text.length
});
return slices.length ? slices : [{
text,
startOffset: 0,
endOffset: text.length
}];
};
var getNormalizedTokenText = (tokens) => normalizeSubtitleTextForDisplay(tokens.map((token) => token.text).join(""));
var normalizeExistingTokensForDisplay = (tokens) => {
const normalizedTokens = [];
for (const token of tokens) {
if (!token.text.includes("\n")) {
normalizedTokens.push(token);
continue;
}
const slices = splitSegmentText(token.text);
const sliceTimings = allocateTimingsByLength(slices.map((slice) => slice.text === "\n" ? "" : slice.text), token.startMs, token.durationMs);
for (let index = 0; index < slices.length; index += 1) {
const slice = slices[index];
const isLineBreak = slice.text === "\n";
normalizedTokens.push({
...token,
text: slice.text,
startMs: sliceTimings[index]?.startMs ?? token.startMs,
durationMs: isLineBreak ? 0 : sliceTimings[index]?.durationMs ?? 0,
isWordLike: isLineBreak ? false : token.isWordLike
});
}
}
return normalizedTokens;
};
var canReuseExistingTokens = (line, descriptor, lineText) => {
if (!lineText || !line.tokens.length || descriptor.source === "youtube") return false;
if (line.metadata?.styledSpans?.length && !line.tokens.some((token) => token.style)) return false;
return getNormalizedTokenText(line.tokens) === lineText;
};
var collectSourceTimedWords = (sourceTokens, locale) => {
const timedWords = [];
for (const token of sourceTokens) {
const normalizedTokenText = normalizeSubtitleTextForDisplay(token.text);
if (!normalizedTokenText || !hasPositiveDuration(token)) continue;
const segmentedWords = segmentText(normalizedTokenText, locale).filter((segment) => segment.isWordLike && segment.text.trim());
if (!segmentedWords.length) continue;
const segmentTimings = allocateTimingsByLength(segmentedWords.map((segment) => segment.text), token.startMs, token.durationMs);
timedWords.push(...segmentTimings);
}
return timedWords;
};
var buildDirectSegmentToken = (segment, segmentStartMs, segmentDurationMs, styledSpans) => ({
text: segment.text,
startMs: segmentStartMs,
durationMs: segmentDurationMs,
isWordLike: segment.isWordLike,
style: getStyleForRange(styledSpans, segment.index, segment.index + segment.text.length)
});
var buildSegmentSliceTokens = (segment, segmentStartMs, segmentDurationMs, styledSpans) => {
const slices = splitSegmentText(segment.text);
if (slices.length === 1 && slices[0].text === segment.text) return [buildDirectSegmentToken(segment, segmentStartMs, segmentDurationMs, styledSpans)];
const sliceTimings = allocateTimingsByLength(slices.map((slice) => slice.text === "\n" ? "" : slice.text), segmentStartMs, segmentDurationMs);
return slices.map((slice, sliceIndex) => {
const isLineBreak = slice.text === "\n";
return {
text: slice.text,
startMs: sliceTimings[sliceIndex]?.startMs ?? segmentStartMs,
durationMs: isLineBreak ? 0 : sliceTimings[sliceIndex]?.durationMs ?? 0,
isWordLike: !isLineBreak && segment.isWordLike,
style: isLineBreak ? void 0 : getStyleForRange(styledSpans, segment.index + slice.startOffset, segment.index + slice.endOffset)
};
});
};
var collectWordIndices = (tokens) => tokens.reduce((indices, token, index) => {
if (token.isWordLike && token.text.trim()) indices.push(index);
return indices;
}, []);
var applySourceWordTimings = (tokens, sourceTimedWords) => {
if (!sourceTimedWords.length) return tokens;
const wordIndices = collectWordIndices(tokens);
if (!wordIndices.length) return tokens;
const totalTargetWords = wordIndices.length;
for (let i = 0; i < totalTargetWords; i += 1) {
const targetIndex = wordIndices[i];
const sourceIndex = Math.floor(i * sourceTimedWords.length / totalTargetWords);
const sourceTiming = sourceTimedWords[Math.min(sourceIndex, sourceTimedWords.length - 1)];
tokens[targetIndex] = {
...tokens[targetIndex],
startMs: sourceTiming.startMs,
durationMs: sourceTiming.durationMs
};
}
return tokens;
};
var buildLineTokens = (line, descriptor, lineText) => {
if (!lineText) return [];
if (canReuseExistingTokens(line, descriptor, lineText)) return normalizeExistingTokensForDisplay(line.tokens);
const locale = descriptor.language;
const styledSpans = line.metadata?.styledSpans ?? buildStyledDisplayModel(line.metadata?.rawText ?? line.text ?? lineText).styledSpans;
const segments = segmentText(lineText, locale);
if (!segments.length) return [];
const baseTimings = allocateTimingsByLength(segments.map((segment) => segment.text), line.startMs, line.durationMs);
const nextTokens = [];
for (let index = 0; index < segments.length; index += 1) {
const segment = segments[index];
const segmentStartMs = baseTimings[index]?.startMs ?? line.startMs;
const segmentDurationMs = baseTimings[index]?.durationMs ?? 0;
nextTokens.push(...buildSegmentSliceTokens(segment, segmentStartMs, segmentDurationMs, styledSpans));
}
return applySourceWordTimings(nextTokens, collectSourceTimedWords(line.tokens, locale));
};
var fetchRawSubtitles = async (url, format) => {
const response = await GM_fetch(url, { timeout: 7e3 });
if (format === "vtt" || format === "srt" || format === "ass") return parseSubtitleText(await response.text(), format);
return response.json();
};
var normalizeFetchedSubtitles = (rawSubtitles, descriptor) => {
if (descriptor.source === "youtube") return SubtitlesProcessor.formatYoutubeSubtitles(rawSubtitles, Boolean(descriptor.isAutoGenerated));
if (descriptor.format === "srt" || descriptor.format === "vtt" || descriptor.format === "ass") return sortProcessedSubtitles(ensureProcessedSubtitles(rawSubtitles));
return sortProcessedSubtitles(ensureProcessedSubtitles(rawSubtitles));
};
var processFetchedSubtitles = (subtitles, descriptor) => {
const maybeMerged = SubtitlesProcessor.autoMerge(subtitles, descriptor);
return {
...maybeMerged,
subtitles: SubtitlesProcessor.processTokens(maybeMerged, descriptor)
};
};
var buildYandexSubtitles = (response) => {
const subtitles = [];
const seenOriginal = /* @__PURE__ */ new Set();
for (const subtitle of response.subtitles ?? []) {
if (subtitle.language && !seenOriginal.has(subtitle.language)) {
seenOriginal.add(subtitle.language);
subtitles.push({
source: "yandex",
format: "json",
language: subtitle.language,
url: subtitle.url
});
}
if (!subtitle.translatedLanguage) continue;
subtitles.push({
source: "yandex",
format: "json",
language: subtitle.translatedLanguage,
translatedFromLanguage: subtitle.language,
url: subtitle.translatedUrl ?? subtitle.url
});
}
return subtitles;
};
var SubtitlesProcessor = {
processTokens(subtitles, descriptor) {
const lines = [];
for (const line of subtitles.subtitles) {
const text = normalizeLineText(line);
const tokens = buildLineTokens(line, descriptor, text);
lines.push({
...line,
text,
tokens
});
}
return lines;
},
formatYoutubeSubtitles(subtitles, isAsr = false) {
const events = subtitles.events ?? [];
if (!events.length) {
console.error("[VOT] Invalid YouTube subtitles format:", subtitles);
return {
format: "json",
subtitles: []
};
}
const processed = [];
for (let i = 0; i < events.length; i += 1) {
const event = events[i];
const segs = event.segs;
if (!segs?.length) continue;
const nextEvent = events[i + 1];
const durationMs = getYoutubeEventDurationMs(event, nextEvent);
const { text, sourceTokens } = buildYoutubeSourceTokens(event, segs, durationMs);
const normalizedText = text.trim();
if (!normalizedText) continue;
processed.push({
text: normalizedText,
startMs: event.tStartMs,
durationMs,
speakerId: "0",
tokens: isAsr ? sourceTokens : []
});
}
return {
format: "json",
subtitles: processed
};
},
autoMerge(subtitles, descriptor) {
if (!descriptor.isAutoGenerated) return subtitles;
if (subtitles.subtitles.length < 2 || !subtitles.subtitles.some((line) => line.tokens.length > 0)) return subtitles;
return {
...subtitles,
subtitles: mergeAutoGeneratedSubtitleLines(subtitles.subtitles, descriptor.language)
};
},
async fetchSubtitles(descriptorOrVideoData, requestLang, spokenLang) {
let descriptor = parseSubtitleDescriptor(descriptorOrVideoData);
if (!descriptor && isVideoDataForSubtitles(descriptorOrVideoData)) descriptor = pickDescriptorFromVideoData(descriptorOrVideoData, requestLang, spokenLang);
if (!descriptor) return {
format: "json",
subtitles: []
};
const { source, format } = descriptor;
let { url } = descriptor;
if (source === "youtube") url = appendYoutubePoTokenParams(url);
try {
const subtitlesWithTokens = processFetchedSubtitles(normalizeFetchedSubtitles(await fetchRawSubtitles(url, format), descriptor), descriptor);
debug.log("[VOT] Processed subtitles:", subtitlesWithTokens);
return subtitlesWithTokens;
} catch (error) {
console.error("[VOT] Failed to process subtitles:", error);
return {
format: "json",
subtitles: []
};
}
},
async getSubtitles(client, videoData) {
const { host, url, detectedLanguage: requestLang, videoId, duration, subtitles: extraSubtitles = [] } = videoData;
try {
const requestPayload = {
videoData: {
host,
url,
videoId,
duration
},
requestLang
};
const res = await Promise.race([client.getSubtitles(requestPayload), new Promise((_, reject) => {
setTimeout(() => reject(/* @__PURE__ */ new Error("Timeout")), 5e3);
})]);
debug.log("[VOT] Subtitles response:", res);
if (res.waiting) console.error("[VOT] Failed to get Yandex subtitles");
return sortSubtitles([...buildYandexSubtitles(res), ...extraSubtitles], requestLang);
} catch (error) {
let message = "Error in getSubtitles function";
if (error instanceof Error && error.message === "Timeout") message = "Failed to get Yandex subtitles: timeout";
console.error(`[VOT] ${message}`, error);
throw error;
}
}
};
//#endregion
//#region src/videoHandler/modules/subtitles.ts
var subtitlesSelectionRequestVersion = /* @__PURE__ */ new WeakMap();
function getPreferredSubtitlesLanguage(handler) {
const videoData = handler.videoData;
return handler.getPreferredSubtitlesLanguage(videoData?.detectedLanguage, videoData?.responseLanguage) ?? videoData?.responseLanguage ?? handler.translateToLang;
}
function getCacheDetectedLanguage(handler) {
const videoData = handler.videoData;
const detectedLanguage = videoData?.detectedLanguage?.toLowerCase();
if (detectedLanguage && detectedLanguage !== "auto") return detectedLanguage;
return videoData?.responseLanguage?.toLowerCase() ?? handler.translateToLang;
}
function getCurrentSubtitlesCacheKey(handler) {
const videoData = handler.videoData;
if (!videoData?.videoId) return null;
const detectedLanguage = getCacheDetectedLanguage(handler);
if (!detectedLanguage) return null;
const subtitleLanguage = getPreferredSubtitlesLanguage(handler);
if (!subtitleLanguage) return null;
return handler.getSubtitlesCacheKey(videoData.videoId, detectedLanguage, subtitleLanguage);
}
function buildSubtitleDescriptorKey(descriptor) {
return [
descriptor.source,
descriptor.format,
descriptor.language,
descriptor.translatedFromLanguage ?? "",
descriptor.isAutoGenerated ? "1" : "0",
descriptor.url
].join("|");
}
function dedupeSubtitles(subtitles) {
const seen = /* @__PURE__ */ new Set();
const result = [];
for (const descriptor of subtitles) {
const key = buildSubtitleDescriptorKey(descriptor);
if (seen.has(key)) continue;
seen.add(key);
result.push(descriptor);
}
return result;
}
function enrichYoutubeSubtitlesForPreference(handler, subtitleLanguage) {
const videoData = handler.videoData;
if (!videoData) throw new Error("Video data is required to load subtitles");
if (handler.site.host !== "youtube" || !subtitleLanguage) return videoData;
const preferredYoutubeSubtitles = YoutubeHelper.getSubtitles(subtitleLanguage);
if (!preferredYoutubeSubtitles.length) return videoData;
return {
...videoData,
subtitles: dedupeSubtitles([...Array.isArray(videoData.subtitles) ? videoData.subtitles : [], ...preferredYoutubeSubtitles])
};
}
function nextSubtitlesSelectionRequestVersion(handler) {
const nextVersion = (subtitlesSelectionRequestVersion.get(handler) ?? 0) + 1;
subtitlesSelectionRequestVersion.set(handler, nextVersion);
return nextVersion;
}
function isCurrentSubtitlesSelectionRequest(handler, requestVersion) {
return subtitlesSelectionRequestVersion.get(handler) === requestVersion;
}
function clearSelectedSubtitles(handler, overlayView) {
if (handler.hasSubtitlesWidget()) handler.subtitlesWidget?.setContent(null);
overlayView.downloadSubtitlesButton.hidden = true;
handler.yandexSubtitles = null;
return handler;
}
async function changeSubtitlesLang(subs) {
debug.log("[onchange] subtitles", subs);
const requestVersion = nextSubtitlesSelectionRequestVersion(this);
const overlayView = this.uiManager.votOverlayView;
if (!overlayView?.subtitlesSelect || !overlayView.downloadSubtitlesButton) return this;
overlayView.subtitlesSelect.setSelectedValue(subs);
if (subs === "disabled") return clearSelectedSubtitles(this, overlayView);
const subtitlesIndex = parseSubtitlesOptionIndex(subs);
if (subtitlesIndex == null) return clearSelectedSubtitles(this, overlayView);
const descriptor = getSubtitleDescriptorAtIndex(this.subtitles, subtitlesIndex);
if (!descriptor) return clearSelectedSubtitles(this, overlayView);
let subtitlesObj = { ...descriptor };
const proxiedSubtitlesUrl = proxifyYandexSubtitlesUrl(subtitlesObj.url, {
translateProxyEnabled: this.data?.translateProxyEnabled,
proxyWorkerHost: this.data?.proxyWorkerHost
});
if (proxiedSubtitlesUrl !== subtitlesObj.url) {
subtitlesObj = {
...subtitlesObj,
url: proxiedSubtitlesUrl
};
debug.log(`[VOT] Subs proxied via ${subtitlesObj.url}`);
}
const fetchedSubtitles = await SubtitlesProcessor.fetchSubtitles(subtitlesObj);
if (!isCurrentSubtitlesSelectionRequest(this, requestVersion)) return this;
this.yandexSubtitles = fetchedSubtitles;
this.getSubtitlesWidget().setContent(this.yandexSubtitles, subtitlesObj.language);
overlayView.downloadSubtitlesButton.hidden = false;
return this;
}
async function updateSubtitlesLangSelect() {
const overlayView = this.uiManager.votOverlayView;
if (!overlayView?.subtitlesSelect) return;
const updatedOptions = buildSubtitlesSelectOptions(getIndexedSubtitleDescriptors(this.subtitles));
overlayView.subtitlesSelect.updateItems(updatedOptions);
await this.changeSubtitlesLang(DISABLED_SUBTITLES_VALUE);
}
async function ensureSubtitlesForCurrentLangPair() {
const cacheKey = getCurrentSubtitlesCacheKey(this);
if (!cacheKey) {
if (this.subtitlesCacheKey !== null || this.subtitles.length > 0) {
this.subtitles = [];
this.subtitlesCacheKey = null;
await this.updateSubtitlesLangSelect();
}
return this;
}
if (this.subtitlesCacheKey === cacheKey) {
const hasCachedSubtitles = this.cacheManager.getSubtitles(cacheKey) !== void 0;
if (this.subtitles.length > 0 || hasCachedSubtitles) return this;
}
const cachedSubs = this.cacheManager.getSubtitles(cacheKey);
if (cachedSubs !== void 0) {
this.subtitles = Array.isArray(cachedSubs) ? cachedSubs : [];
this.subtitlesCacheKey = cacheKey;
await this.updateSubtitlesLangSelect();
return this;
}
await this.loadSubtitles();
return this;
}
/**
* Hotkey/helper: enables subtitles for the currently selected language pair.
*
* If an exact "from -> to" subtitles track is unavailable, falls back to any
* subtitles track in the target language.
* For same-language pair (from == to), prefer site subtitles before Yandex.
*/
async function enableSubtitlesForCurrentLangPair() {
const overlayView = this.uiManager.votOverlayView;
if (!overlayView?.subtitlesSelect) return this;
try {
await ensureSubtitlesForCurrentLangPair.call(this);
} catch {
return this;
}
const fromLang = this.videoData?.detectedLanguage ?? this.translateFromLang;
const toLang = getPreferredSubtitlesLanguage(this);
if (!toLang) return this;
const bestIdx = pickBestSubtitlesIndex(getIndexedSubtitleDescriptors(this.subtitles), fromLang, toLang);
if (bestIdx == null) return this;
if (getSelectedSubtitlesValue(overlayView.subtitlesSelect.selectedValues) === String(bestIdx)) return this;
await this.changeSubtitlesLang(String(bestIdx));
return this;
}
/**
* Re-evaluates the active subtitles track for the currently selected language
* pair, but only when auto-subtitles are enabled.
*/
async function refreshAutoSubtitlesForCurrentLangPair() {
if (!this.data?.autoSubtitles || !this.videoData?.videoId) return this;
await this.enableSubtitlesForCurrentLangPair();
return this;
}
/**
* Hotkey helper: toggles subtitles.
*
* - If subtitles are currently enabled (any non-"disabled" value), disable them.
* - If subtitles are disabled, enable the best subtitles for the current
* language pair.
*/
async function toggleSubtitlesForCurrentLangPair() {
const overlayView = this.uiManager.votOverlayView;
if (!overlayView?.subtitlesSelect) return this;
const currentValue = getSelectedSubtitlesValue(overlayView.subtitlesSelect.selectedValues);
if (currentValue && currentValue !== "disabled") {
await this.changeSubtitlesLang(DISABLED_SUBTITLES_VALUE);
return this;
}
await this.enableSubtitlesForCurrentLangPair();
return this;
}
async function loadSubtitles() {
if (!this.videoData?.videoId) {
console.error(`[VOT] ${localizationProvider.getDefault("VOTNoVideoIDFound")}`);
this.subtitles = [];
this.subtitlesCacheKey = null;
return;
}
const subtitleLanguage = getPreferredSubtitlesLanguage(this);
if (!subtitleLanguage) {
this.subtitles = [];
this.subtitlesCacheKey = null;
await this.updateSubtitlesLangSelect();
return;
}
const cacheKey = this.getSubtitlesCacheKey(this.videoData.videoId, this.videoData.detectedLanguage, subtitleLanguage);
try {
let cachedSubs = this.cacheManager.getSubtitles(cacheKey);
if (!cachedSubs) {
let inflight = this.subtitlesLoadPromises.get(cacheKey);
if (inflight === void 0) {
const videoDataForSubtitles = enrichYoutubeSubtitlesForPreference(this, subtitleLanguage);
inflight = SubtitlesProcessor.getSubtitles(this.votClient, videoDataForSubtitles);
this.subtitlesLoadPromises.set(cacheKey, inflight);
}
try {
cachedSubs = await inflight;
cachedSubs = Array.isArray(cachedSubs) ? cachedSubs : [];
this.cacheManager.setSubtitles(cacheKey, cachedSubs);
} finally {
if (this.subtitlesLoadPromises.get(cacheKey) === inflight) this.subtitlesLoadPromises.delete(cacheKey);
}
}
this.subtitles = Array.isArray(cachedSubs) ? cachedSubs : [];
this.subtitlesCacheKey = cacheKey;
} catch (error) {
console.error("[VOT] Failed to load subtitles:", error);
this.subtitles = [];
this.subtitlesCacheKey = null;
}
await this.updateSubtitlesLangSelect();
}
//#endregion
//#region src/index.ts
var RESOLVED_VOID_PROMISE = Promise.resolve();
var VideoHandler = class {
video;
container;
site;
translateFromLang = "auto";
translateToLang = calculatedResLang;
data;
videoData;
firstPlay = true;
audioContext;
votClient;
audioPlayer;
abortController;
actionsAbortController;
/** Increments whenever we reset/abort translation actions to invalidate stale async work */
actionsGeneration = 0;
notifier = new Notifier();
cacheManager;
votSessionStorage = new VOTSessionStorageCache();
/**
* In-flight subtitles list requests, keyed by subtitles cache key.
*
* Prevents duplicate parallel requests when the subtitles hotkey is spammed
* before the first request resolves.
*/
subtitlesLoadPromises = /* @__PURE__ */ new Map();
downloadTranslation = null;
isRefreshingTranslation = false;
autoRetry;
votOpts;
volumeOnStart;
autoVolumeMutedOnStart;
/**
* syncVolume (link translation and video volume) runtime state.
* We keep last-known slider values to apply deltas reliably.
*/
volumeLinkState = {
initialized: false,
lastVideoPercent: 0,
lastTranslationPercent: 0
};
/**
* Used to ignore our own programmatic video-volume updates when observing
* external UIs (e.g. YouTube volume panel aria mutations).
*/
internalVideoVolumeSetAt = 0;
internalVideoVolumeSetPercent = null;
internalVideoVolumeSuppressionMs = 250;
internalVideoVolumeSetHistory = [];
internalVideoVolumeSetHistoryLimit = 48;
smartVolumeDuckingInterval;
smartVolumeDuckingBaseline;
smartVolumeLastApplied;
smartVolumeLastTickAt = 0;
smartVolumeLastSoundAt = 0;
smartVolumeRmsMissingSinceAt = null;
/** Smoothed translated-track RMS envelope (0..1). */
smartVolumeRmsEnvelope = 0;
/**
* Internal speech gate state for Smart Auto-Volume ducking.
*
* This is a debounced/hysteresis-based boolean that tracks whether the
* translated track is considered "audible" for the purpose of ducking.
*/
smartVolumeSpeechGateOpen = false;
smartVolumeIsDucked = false;
longWaitingResCount = 0;
hadAsyncWait = false;
subtitles = [];
subtitlesCacheKey = null;
subtitlesWidget;
activeTranslation = null;
/**
* In-flight async teardown for translation/audio player cleanup.
* New translation starts should wait for this to avoid clear/init races.
*/
stopTranslatePromise = null;
interactionChecker;
uiManager;
overlayVisibility;
overlayVisibilityTargetsAbortController;
translationOrchestrator;
lifecycleController;
translationHandler;
videoManager;
yandexSubtitles = null;
resizeObserver;
syncVolumeObserver;
initialized = false;
/**
* Cached overlay mount points (root/portal). Recomputed when container changes.
* Avoids doing the same DOM/style walks multiple times per lifecycle update.
*/
mountCache;
/**
* In-memory cache for translated error strings (RU -> UI language).
* This avoids repeated translation API calls during retry loops when the
* same backend message is emitted multiple times.
*/
errorTranslationCache = /* @__PURE__ */ new Map();
/**
* Fullscreen helper for proper ShadowDOM support
*/
fullscreenHelper;
/**
* Returns fullscreen root for overlay if the active fullscreen session belongs
* to the current video/container. Otherwise returns null.
* For Shadow DOM players (e.g., Reddit's shreddit-player), returns shadowRoot
* to ensure UI is mounted inside the shadow tree, not in the light DOM.
*/
getFullscreenOverlayRoot() {
if (!this.fullscreenHelper) return null;
return this.fullscreenHelper.getOverlayRoot();
}
getOverlayMountPoints(container = this.container) {
const fullscreenRoot = this.getFullscreenOverlayRoot();
const { base, root, portalContainer, subtitlesMountContainer } = resolveOverlayMountTargets({
container,
site: this.site,
fullscreenRoot
});
const cache = this.mountCache;
if (cache?.container === container && cache.base === base && cache.subtitlesMountContainer === subtitlesMountContainer && cache.fullscreenRoot === fullscreenRoot && (cache.root.isConnected ?? document.documentElement.contains(cache.root))) return {
root: cache.root,
portalContainer: cache.portalContainer,
subtitlesMountContainer: cache.subtitlesMountContainer,
fullscreenRoot: cache.fullscreenRoot
};
this.mountCache = {
container,
base,
root,
portalContainer,
subtitlesMountContainer,
fullscreenRoot
};
return {
root,
portalContainer,
subtitlesMountContainer,
fullscreenRoot
};
}
getOverlayMount(container = this.container) {
const { root, portalContainer, subtitlesMountContainer } = this.getOverlayMountPoints(container);
return {
root,
portalContainer,
subtitlesMountContainer
};
}
/**
* Builds a stable cache key for translations.
*
* NOTE: Keep this in sync with CacheManager expectations.
* @param {string} videoId
* @param {string} from
* @param {string} to
*/
getTranslationCacheKey(videoId, from, to, translationHelp) {
const requestLangForApi = this.getRequestLangForTranslation(from, to);
const useLivelyVoice = this.isLivelyVoiceAllowed(requestLangForApi, to) && this.data?.useLivelyVoice;
const helpStr = translationHelp === void 0 || translationHelp === null ? "" : stableStringify(translationHelp);
return `${videoId}_${requestLangForApi}_${to}_${useLivelyVoice}_${helpStr ? fnv1a32ToKeyPart(helpStr) : "0"}`;
}
/**
* Builds a stable cache key for subtitles.
*
* Bugfix: subtitles cache key must match the key used by loadSubtitles().
* @param {string} videoId
* @param {string} detectedLanguage
* @param {string} subtitleLanguage
*/
getSubtitlesCacheKey(videoId, detectedLanguage, subtitleLanguage) {
return `${videoId}_${detectedLanguage}_${subtitleLanguage}_${Boolean(this.data?.useLivelyVoice)}`;
}
getPreferredSubtitlesLanguage(detectedLanguage = this.videoData?.detectedLanguage ?? "auto", responseLanguage = this.videoData?.responseLanguage ?? this.translateToLang, preference = this.data?.responseLanguageSubtitles) {
return resolveSubtitlesLanguage(preference, detectedLanguage, responseLanguage) ?? responseLanguage ?? detectedLanguage;
}
isActionStale(actionContext) {
if (!actionContext) return false;
return this.actionsGeneration !== actionContext.gen || this.videoData?.videoId !== actionContext.videoId;
}
updateVOTClientRequestSignal() {
if (!this.votClient) return;
this.votClient.fetchOpts = {
...this.votClient.fetchOpts,
signal: this.actionsAbortController.signal
};
}
resetActionsAbortController(reason) {
try {
this.actionsAbortController?.abort(reason);
} catch {}
this.actionsAbortController = new AbortController();
this.actionsGeneration++;
this.updateVOTClientRequestSignal();
}
/**
* Constructs a new VideoHandler instance.
* @param {HTMLVideoElement} video The video element to handle.
* @param {HTMLElement} container The container element for the video.
* @param {Object} site The site object associated with the video.
*/
constructor(video, container, site) {
debug.log("[VideoHandler] add video:", video, "container:", container, this);
this.video = video;
this.container = container;
this.site = site;
this.abortController = new AbortController();
this.actionsAbortController = new AbortController();
this.cacheManager = new InMemoryCacheManager();
this.interactionChecker = createIntervalIdleChecker();
this.interactionChecker.start();
this.uiManager = new UIManager({
mount: this.getOverlayMount(container),
data: this.data,
videoHandler: this,
intervalIdleChecker: this.interactionChecker
});
this.overlayVisibility = new OverlayVisibilityController({
checker: this.interactionChecker,
getOverlayView: () => this.uiManager.votOverlayView ?? null,
getAutoHideDelay: () => this.getAutoHideDelay(),
isInteractiveNode: (node) => this.isOverlayInteractiveNode(node)
});
this.translationOrchestrator = new TranslationOrchestrator({
isFirstPlay: () => this.firstPlay,
setFirstPlay: (next) => {
this.firstPlay = next;
},
isAutoTranslateEnabled: () => Boolean(this.data?.autoTranslate),
getVideoId: () => this.videoData?.videoId,
scheduleAutoTranslate: () => this.runAutoTranslate(),
isMobileYouTubeMuted: () => this.site.host === "youtube" && this.site.additionalData === "mobile" && this.video.muted,
setMuteWatcher: (callback) => {
let done = false;
const fireOnce = () => {
if (done) return;
done = true;
this.video.removeEventListener("volumechange", onVolumeChange);
callback();
};
const onVolumeChange = () => {
if (!this.video.muted) fireOnce();
};
this.video.addEventListener("volumechange", onVolumeChange, { signal: this.abortController.signal });
queueMicrotask(() => {
if (!this.video.muted) fireOnce();
});
}
});
this.lifecycleController = new VideoLifecycleController(createVideoLifecycleHost(this, (value) => this.getOverlayMount(value)));
this.translationHandler = new VOTTranslationHandler(this);
this.videoManager = new VOTVideoManager(this);
this.fullscreenHelper = new FullscreenHelper({
container: this.container,
video: this.video
});
this.fullscreenHelper.addFullscreenChangeListener(() => {
this.refreshOverlayMount();
});
}
/**
* Lazily creates the subtitles widget.
* @returns {SubtitlesWidget}
*/
getSubtitlesWidget() {
if (!this.subtitlesWidget) {
const { subtitlesMountContainer } = this.getOverlayMountPoints();
this.subtitlesWidget = new SubtitlesWidget(this.video, this.uiManager.votOverlayView?.root ?? subtitlesMountContainer, this.interactionChecker);
this.applySavedSubtitlesWidgetSettings(this.subtitlesWidget);
}
return this.subtitlesWidget;
}
applySavedSubtitlesWidgetSettings(widget) {
if (!this.data) return;
widget.setSmartLayout(typeof this.data.subtitlesSmartLayout === "boolean" ? this.data.subtitlesSmartLayout : true);
if (typeof this.data.subtitlesMaxLength === "number") widget.setMaxLength(this.data.subtitlesMaxLength);
if (typeof this.data.highlightWords === "boolean") widget.setHighlightWords(this.data.highlightWords);
if (typeof this.data.subtitlesFontSize === "number") widget.setFontSize(this.data.subtitlesFontSize);
if (typeof this.data.subtitlesFontFamily === "string") widget.setFontFamily(this.data.subtitlesFontFamily);
if (typeof this.data.subtitlesOpacity === "number") widget.setOpacity(this.data.subtitlesOpacity);
}
/**
* Determines whether subtitles widget is initialized\.
* @returns {boolean}
*/
hasSubtitlesWidget() {
return Boolean(this.subtitlesWidget);
}
resetSubtitlesWidget() {
if (this.hasSubtitlesWidget()) {
this.subtitlesWidget?.release();
this.subtitlesWidget = void 0;
}
}
/**
* Root element for overlay UI (buttons/menu) so it remains clickable on players
* that disable pointer events on inner layers.
*/
get uiRoot() {
const root = this.getOverlayMountPoints().root;
return root instanceof ShadowRoot ? root.host : root;
}
/**
* Determines the DOM container used for overlay portals.
* @returns {HTMLElement}
*/
get portalContainer() {
return this.getOverlayMountPoints().portalContainer;
}
/**
* Returns the container element for event listeners.
* @returns {HTMLElement} The event container.
*/
getEventContainer() {
if (!this.site.eventSelector) return this.container;
return document.querySelector(this.site.eventSelector) ?? this.container;
}
/**
* Run auto translate using orchestrator dependencies.
*/
async runAutoTranslate() {
await this.videoManager.videoValidator();
await this.uiManager.handleTranslationBtnClick();
}
/**
* Lazily initializes and returns the AudioContext.
* @returns {AudioContext | undefined}
*/
getAudioContext() {
if (this.audioContext) return this.audioContext;
if (!this.isAudioContextSupported) return void 0;
try {
this.audioContext = initAudioContext();
return this.audioContext;
} catch (err) {
console.warn("[VOT] Failed to init AudioContext, falling back:", err);
return;
}
}
get isAudioContextSupported() {
return globalThis.AudioContext !== void 0 || globalThis.webkitAudioContext !== void 0;
}
/**
* Determines if audio should be preferred.
* @returns {boolean} True if audio is preferred.
*/
getPreferAudio() {
if (!this.getAudioContext()) return true;
if (!this.data) return true;
if (!this.data.newAudioPlayer) return true;
if (this.videoData?.isStream) return true;
if (this.data.newAudioPlayer && !this.data.onlyBypassMediaCSP) return false;
return !this.site.needBypassCSP;
}
/**
* Creates the audio player.
* @returns {VideoHandler} The VideoHandler instance.
*/
createPlayer() {
const preferAudio = this.getPreferAudio();
const audioContext = this.getAudioContext();
debug.log("preferAudio:", preferAudio);
this.audioPlayer = new Chaimu({
video: this.video,
debug: Boolean(false),
fetchFn: GM_fetch,
fetchOpts: { timeout: 0 },
preferAudio
});
if (preferAudio && audioContext) this.audioPlayer.audioContext = audioContext;
return this;
}
/**
* Returns true if a detected external volume update is very likely caused by
* our own recent programmatic setVideoVolume call.
*/
isLikelyInternalVideoVolumeChange(observedPercent) {
const now = Date.now();
const history = this.internalVideoVolumeSetHistory;
if (history.length > 0) {
let writeIndex = 0;
let matchFound = false;
for (const entry of history) {
if (now - entry.at > entry.suppressMs) continue;
history[writeIndex++] = entry;
if (!matchFound && Math.abs(observedPercent - entry.percent) <= 1) matchFound = true;
}
history.length = writeIndex;
return matchFound;
}
if (this.internalVideoVolumeSetPercent === null) return false;
if (now - this.internalVideoVolumeSetAt > this.internalVideoVolumeSuppressionMs) return false;
return Math.abs(observedPercent - this.internalVideoVolumeSetPercent) <= 1;
}
/**
* Initializes the VideoHandler: loads settings, UI, video data, events, etc.
* @returns {Promise<void>}
*/
init() {
return init.call(this);
}
/**
* Initializes the VOT client.
* @returns {VideoHandler} This instance.
*/
async initVOTClient() {
const proxyClientEnabled = isProxyClientEnabled(this.data ?? {});
const transportHost = this.data?.translateProxyEnabled === 1 ? proxyWorkerHostMode1 : proxyClientEnabled ? resolveProxyWorkerHost(this.data?.proxyWorkerHost) : workerHost;
this.votOpts = {
fetchFn: GM_fetch,
fetchOpts: {
signal: this.actionsAbortController.signal,
forceGmXhr: shouldForceProxyClientGmXhr({
...this.data,
gmXhrSupported: isSupportGMXhr
})
},
apiToken: this.data?.account?.token,
hostVOT: votBackendUrl,
host: transportHost
};
this.votClient = new (proxyClientEnabled ? VOTWorkerClient : VOTClient)(this.votOpts);
this.votClient.sessions = await this.votSessionStorage.restore(transportHost, this.votClient.sessions);
const originalGetSession = this.votClient.getSession.bind(this.votClient);
this.votClient.getSession = async (module) => {
const session = await originalGetSession(module);
await this.votSessionStorage.persist(transportHost, this.votClient.sessions);
return session;
};
return this;
}
/**
* Sets the translation button state and text.
* @param {string} status The new status.
* @param {string} text The text to display.
* @returns {VideoHandler} This instance.
*/
transformBtn(status, text) {
this.uiManager.transformBtn(status, text);
return this;
}
/**
* @returns {boolean} True if the extension audio player has active audio source
*/
hasActiveSource() {
return !!this.audioPlayer?.player?.src;
}
/**
* Initializes extra event listeners (resize, click outside, keydown, etc.).
*/
initExtraEvents() {
return initExtraEvents.call(this);
}
/**
* Recomputes overlay mount points and rebinds interaction targets.
*
* Used when fullscreen state changes without changing `this.container`
* (common for players inside Shadow DOM).
*/
refreshOverlayMount() {
this.mountCache = void 0;
if (this.fullscreenHelper) {
this.fullscreenHelper.updateContainer(this.container);
this.fullscreenHelper.updateVideo(this.video);
}
const nextMount = this.getOverlayMount(this.container);
const mountChanged = !isSameOverlayMount(this.uiManager.mount, nextMount);
this.uiManager.updateMount(nextMount);
if (!mountChanged) return;
this.rebindOverlayVisibilityTargets();
}
/**
* Re-attach overlayVisibility listeners to the *current* overlay button/menu elements.
*
* The overlay UI gets recreated in some flows (e.g. menu language change),
* so listeners that were attached to the old DOM nodes must be re-bound.
*/
rebindOverlayVisibilityTargets = rebindOverlayVisibilityTargets;
/**
* Called when the video can play.
*/
setCanPlay = () => this.lifecycleController.setCanPlay();
isOverlayInteractiveNode(node) {
return isOverlayInteractiveNode.call(this, node);
}
/**
* Schedules hiding the overlay button with guard checks for internal navigation.
*/
getAutoHideDelay() {
return getAutoHideDelay.call(this);
}
/**
* Changes subtitles language based on user selection.
* @param {string} subs The subtitles selection value.
*/
changeSubtitlesLang = changeSubtitlesLang;
/**
* Updates the subtitles selection options.
*/
updateSubtitlesLangSelect = updateSubtitlesLangSelect;
/**
* Ensures the in-memory subtitles list matches the current language-pair cache key.
*/
ensureSubtitlesForCurrentLangPair = ensureSubtitlesForCurrentLangPair;
/**
* Loads subtitles for the current video.
*/
loadSubtitles = loadSubtitles;
/**
* Enables subtitles that match the currently selected language pair (from -> to).
*
* Used by the subtitles hotkey: prefers Yandex captions for the exact pair,
* then falls back to any captions in the target language.
*/
enableSubtitlesForCurrentLangPair() {
return enableSubtitlesForCurrentLangPair.call(this);
}
/**
* Re-evaluates the active subtitles track for the current language pair,
* but only when auto-subtitles are enabled.
*/
refreshAutoSubtitlesForCurrentLangPair() {
return refreshAutoSubtitlesForCurrentLangPair.call(this);
}
/**
* Toggles subtitles for the current video.
*
* - If subtitles are enabled, this disables them.
* - If subtitles are disabled, this enables the best subtitles track for the
* current language pair.
*/
toggleSubtitlesForCurrentLangPair() {
return toggleSubtitlesForCurrentLangPair.call(this);
}
getRequestLangForTranslation(requestLang, responseLang) {
if (this.data?.useLivelyVoice && this.data?.account?.token && responseLang === "ru") return "en";
return requestLang;
}
isLivelyVoiceAllowed(requestLang = this.videoData?.detectedLanguage ?? "auto", responseLang = this.videoData?.responseLanguage ?? this.translateToLang) {
if (this.getRequestLangForTranslation(requestLang, responseLang) !== "en" || responseLang !== "ru") return false;
if (!this.data?.account?.token) return false;
return true;
}
/**
* Gets the video volume.
* @returns {number} The video volume (0.0 - 1.0).
*/
getVideoVolume = () => this.videoManager.getVideoVolume();
/**
* Sets the video volume.
* @param {number} volume A number between 0 and 1.
* @returns {VideoHandler} This instance.
*/
setVideoVolume(volume, options = {}) {
const snapped = snapVolume01(volume);
const suppressSyncMs = typeof options.suppressSyncMs === "number" && Number.isFinite(options.suppressSyncMs) ? Math.max(0, options.suppressSyncMs) : this.internalVideoVolumeSuppressionMs;
const now = Date.now();
const percent = volume01ToPercent(snapped);
this.internalVideoVolumeSetAt = now;
this.internalVideoVolumeSetPercent = percent;
this.internalVideoVolumeSetHistory.push({
at: now,
percent,
suppressMs: suppressSyncMs
});
if (this.internalVideoVolumeSetHistory.length > this.internalVideoVolumeSetHistoryLimit) this.internalVideoVolumeSetHistory.splice(0, this.internalVideoVolumeSetHistory.length - this.internalVideoVolumeSetHistoryLimit);
this.videoManager.setVideoVolume(snapped, { preserveYoutubeVolumeStorage: options.preserveYoutubeVolumeStorage });
return this;
}
setVideoMuted(muted, options = {}) {
this.videoManager.setVideoMuted(muted, { preserveYoutubeVolumeStorage: options.preserveYoutubeVolumeStorage });
return this;
}
/**
* Keeps internal syncVolume state aligned with observed/programmatic video-volume changes.
*/
onVideoVolumeSliderSynced(volumePercent) {
const normalized = clampPercentInt(volumePercent);
if (!this.volumeLinkState.initialized) {
syncVideoLinkSnapshot(this.volumeLinkState, normalized);
return;
}
if (this.data?.syncVolume && this.hasActiveSource() && !this.isLikelyInternalVideoVolumeChange(normalized)) return;
syncVideoLinkSnapshot(this.volumeLinkState, normalized);
}
/**
* Keeps internal translation-volume snapshot aligned when syncVolume is
* temporarily disabled, so re-enabling link mode does not apply stale deltas.
*/
onTranslationVolumeSliderSynced(volumePercent) {
syncTranslationLinkSnapshot(this.volumeLinkState, volumePercent);
}
/**
* Re-seeds syncVolume baseline from current UI slider values.
* Useful when toggling syncVolume on/off to avoid stale delta state.
*/
resetVolumeLinkState(videoPercent, translationPercent) {
syncVideoLinkSnapshot(this.volumeLinkState, videoPercent);
syncTranslationLinkSnapshot(this.volumeLinkState, translationPercent);
this.volumeLinkState.initialized = true;
}
clearVolumeLinkState() {
this.volumeLinkState.initialized = false;
this.volumeLinkState.lastVideoPercent = 0;
this.volumeLinkState.lastTranslationPercent = 0;
}
/**
* Checks if the video is muted.
* @returns {boolean} True if muted.
*/
isMuted = () => this.videoManager.isMuted();
/**
* Syncs the video volume slider.
*/
syncVideoVolumeSlider = () => this.videoManager.syncVideoVolumeSlider();
/**
* Sets language select menu values.
* @param {string} from Source language.
* @param {string} to Target language.
*/
setSelectMenuValues = (from, to) => {
this.videoManager.setSelectMenuValues(from, to);
};
/**
* Keeps translation and video sliders linked (syncVolume option).
*
* The implementation is delta-based inside the shared 0..100 link range.
* Translation booster values above 100 remain available only while link mode
* is disabled.
*/
syncVolumeWrapper(fromType, newVolume) {
const overlayView = this.uiManager.votOverlayView;
if (!overlayView?.isInitialized()) return;
const videoSlider = overlayView.videoVolumeSlider;
const translationSlider = overlayView.translationVolumeSlider;
if (!videoSlider || !translationSlider) return;
const result = applyVolumeLinkDelta({
state: this.volumeLinkState,
fromType,
newVolume,
currentVideo: Number(videoSlider.value),
currentTranslation: Number(translationSlider.value),
translationMin: translationSlider.min,
translationMax: translationSlider.max
});
const { nextVideo, nextTranslation } = result;
if (typeof nextTranslation === "number") {
translationSlider.value = nextTranslation;
return result;
}
if (typeof nextVideo === "number") {
videoSlider.value = nextVideo;
this.setVideoVolume(nextVideo / 100);
}
return result;
}
/**
* Retrieves video data.
* @returns {Promise<Object>} The video data object.
*/
getVideoData = () => this.videoManager.getVideoData();
/**
* Validates the video.
* @returns {Promise<boolean>} True if valid.
*/
videoValidator = () => this.videoManager.videoValidator();
/**
* Stops translation and resets UI elements.
*/
stopTranslate() {
if (this.stopTranslatePromise !== null) return this.stopTranslatePromise;
const cleanup = async () => {
if (this.audioPlayer?.player) {
try {
this.audioPlayer.player.removeVideoEvents();
this.audioPlayer.player.src = "";
await this.audioPlayer.player.clear();
} catch (err) {
debug.log("[stopTranslate] audioPlayer cleanup error", err);
}
debug.log("audioPlayer after stopTranslate", this.audioPlayer);
}
this.activeTranslation = null;
const overlayView = this.uiManager.votOverlayView;
if (overlayView) {
if (overlayView.videoVolumeSlider) overlayView.videoVolumeSlider.hidden = true;
if (overlayView.translationVolumeSlider) overlayView.translationVolumeSlider.hidden = true;
if (overlayView.downloadTranslationButton) overlayView.downloadTranslationButton.hidden = true;
}
this.downloadTranslation = null;
this.longWaitingResCount = 0;
this.hadAsyncWait = false;
this.transformBtn("none", localizationProvider.get("translateVideo"));
debug.log(`Volume on start: ${this.volumeOnStart}`);
const restoreVolume = typeof this.smartVolumeDuckingBaseline === "number" ? this.smartVolumeDuckingBaseline : this.volumeOnStart;
stopSmartVolumeDucking(this, { restoreVolume });
this.volumeOnStart = void 0;
this.autoVolumeMutedOnStart = void 0;
if (this.autoRetry !== void 0) {
clearTimeout(this.autoRetry);
this.autoRetry = void 0;
}
this.resetActionsAbortController("stopTranslate");
};
const inFlight = cleanup().finally(() => {
if (this.stopTranslatePromise === inFlight) this.stopTranslatePromise = null;
});
this.stopTranslatePromise = inFlight;
return inFlight;
}
waitForPendingStopTranslate() {
return this.stopTranslatePromise ?? RESOLVED_VOID_PROMISE;
}
/**
* Updates the translation error message on the UI.
* @param {string|Error} errorMessage The error message.
*/
async updateTranslationErrorMsg(errorMessage, signal) {
if (signal?.aborted) return;
const translationTake = localizationProvider.get("translationTake");
const lang = localizationProvider.lang;
this.longWaitingResCount = errorMessage === localizationProvider.get("translationTakeAboutMinute") ? this.longWaitingResCount + 1 : 0;
debug.log("longWaitingResCount", this.longWaitingResCount);
if (this.longWaitingResCount > 5) errorMessage = new VOTLocalizedError("TranslationDelayed");
debug.log("updateTranslationErrorMsg message", errorMessage);
const resolvedMessage = await this.resolveTranslationErrorDisplayMessage(errorMessage, translationTake, lang, signal);
if (signal?.aborted || resolvedMessage === null) return;
this.transformBtn("error", resolvedMessage);
if (signal?.aborted) return;
if ([
"Подготавливаем перевод",
"Видео передано в обработку",
"Ожидаем перевод видео",
"Загружаем переведенное аудио"
].includes(errorMessage)) {
if (this.uiManager.votOverlayView?.votButton) this.uiManager.votOverlayView.votButton.loading = true;
}
}
async resolveTranslationErrorDisplayMessage(errorMessage, translationTake, lang, signal) {
if (errorMessage?.name === "VOTLocalizedError") return errorMessage.localizedMessage;
if (errorMessage instanceof Error) return errorMessage.message;
if (!this.shouldTranslateErrorMessage(errorMessage, translationTake, lang)) return this.stringifyTranslationError(errorMessage);
return await this.getTranslatedErrorMessage(errorMessage, lang, signal);
}
shouldTranslateErrorMessage(errorMessage, translationTake, lang) {
return Boolean(this.data?.translateAPIErrors) && lang !== "ru" && !errorMessage?.includes(translationTake);
}
stringifyTranslationError(errorMessage) {
return Array.isArray(errorMessage) ? errorMessage.join("\n") : String(errorMessage ?? "");
}
async getTranslatedErrorMessage(errorMessage, lang, signal) {
const overlayView = this.uiManager.votOverlayView;
if (!overlayView?.votButton) return null;
const messageStr = Array.isArray(errorMessage) ? errorMessage.join(" ") : String(errorMessage);
const cacheKey = `${lang}:${messageStr}`;
const cached = this.errorTranslationCache.get(cacheKey);
if (cached) return cached;
overlayView.votButton.loading = true;
const translatedMessage = await translate(messageStr, "ru", lang);
if (signal?.aborted) return null;
const translatedText = Array.isArray(translatedMessage) ? translatedMessage.join("\n") : String(translatedMessage);
this.errorTranslationCache.set(cacheKey, translatedText);
this.trimErrorTranslationCache();
return translatedText;
}
trimErrorTranslationCache() {
if (this.errorTranslationCache.size <= 50) return;
const oldestKey = this.errorTranslationCache.keys().next().value;
if (oldestKey) this.errorTranslationCache.delete(oldestKey);
}
/**
* Called after translation is updated.
* @param {string} audioUrl The URL of the translation audio.
*/
afterUpdateTranslation(audioUrl) {
const overlayView = this.uiManager.votOverlayView;
if (!overlayView?.votButton) return;
const isSuccess = overlayView.votButton.container.dataset.status === "success";
if (overlayView.videoVolumeSlider) overlayView.videoVolumeSlider.hidden = !this.data?.showVideoSlider || !isSuccess;
if (overlayView.translationVolumeSlider) overlayView.translationVolumeSlider.hidden = !isSuccess;
if (overlayView.videoVolumeSlider && overlayView.translationVolumeSlider) {
this.volumeLinkState.lastVideoPercent = Number(overlayView.videoVolumeSlider.value);
this.volumeLinkState.lastTranslationPercent = Number(overlayView.translationVolumeSlider.value);
this.volumeLinkState.initialized = true;
} else this.volumeLinkState.initialized = false;
if (this.videoData && !this.videoData.isStream) {
if (overlayView.downloadTranslationButton) overlayView.downloadTranslationButton.hidden = false;
this.downloadTranslation = {
url: audioUrl,
videoId: this.videoData.videoId
};
}
debug.log("afterUpdateTranslation downloadTranslation", this.downloadTranslation);
this.syncTranslationPlaybackVolume();
if (this.data?.sendNotifyOnComplete && this.hadAsyncWait && isSuccess) {
this.notifier.translationCompleted(globalThis.location.hostname);
this.hadAsyncWait = false;
}
}
/**
* Validates the audio URL by sending a request.
* @param {string} audioUrl The audio URL to validate.
* @returns {Promise<string>} The valid audio URL.
*/
validateAudioUrl(audioUrl, actionContext) {
return validateAudioUrl.call(this, audioUrl, actionContext);
}
scheduleTranslationRefresh() {
scheduleTranslationRefresh.call(this);
}
refreshTranslationAudio = refreshTranslationAudio;
/**
* Proxifies the audio URL if needed.
* @param {string} audioUrl The original audio URL.
* @returns {string} The proxified audio URL.
*/
proxifyAudio(audioUrl) {
return proxifyAudio.call(this, audioUrl);
}
/**
* Reverts a previously proxified audio URL back to the original Yandex S3 URL.
*
* This allows us to re-apply proxy settings when the proxy host/mode changes
* without permanently "locking in" the old proxy host in the current player
* src.
*/
unproxifyAudio(audioUrl) {
return unproxifyAudio.call(this, audioUrl);
}
/**
* Called when proxy-related settings are changed at runtime.
*
* - Clears in-memory caches so old failures/URLs don't persist.
* - Cancels any in-flight translation work.
* - Best-effort refreshes the active audio source so the new proxy host/mode
* takes effect immediately.
*/
handleProxySettingsChanged = handleProxySettingsChanged;
isMultiMethodS3(url) {
return isMultiMethodS3.call(this, url);
}
/**
* Updates the translation audio source.
* @param {string} audioUrl The audio URL.
*/
updateTranslation = updateTranslation;
syncTranslationPlaybackVolume() {
return syncTranslationPlaybackVolume.call(this);
}
/**
* Translates the video/audio.
* @param {string} VIDEO_ID The video ID.
* @param {boolean} isStream Whether the video is a stream.
* @param {string} requestLang Source language.
* @param {string} responseLang Target language.
* @param {any} translationHelp Optional translation helper data.
*/
translateFunc(VIDEO_ID, isStream, requestLang, responseLang, translationHelp) {
return translateFunc.call(this, VIDEO_ID, isStream, requestLang, responseLang, translationHelp);
}
/**
* used for enable audio downloader on this hosts
*/
isYouTubeHosts() {
return isYouTubeHosts.call(this);
}
/**
* Configures audio settings such as volume.
*/
setupAudioSettings() {
return setupAudioSettings.call(this);
}
applyManualVideoVolumeOverride(volume) {
return applyManualVideoVolumeOverride.call(this, volume);
}
/**
* Stops translation and synchronizes volume.
*/
stopTranslation = async () => {
this.translationOrchestrator?.reset();
this.overlayVisibility?.cancel();
await this.stopTranslate();
this.syncVideoVolumeSlider();
};
/**
* Handles video source change events.
*/
handleSrcChanged = () => this.lifecycleController.handleSrcChanged();
/**
* Releases resources and removes event listeners.
*/
async release() {
debug.log("[VideoHandler] release");
this.initialized = false;
try {
await this.stopTranslation();
} catch (err) {
debug.log("[VideoHandler] stopTranslation failed during release", err);
}
this.lifecycleController?.teardown();
this.abortController?.abort();
this.abortController = new AbortController();
this.fullscreenHelper?.destroy();
this.fullscreenHelper = void 0;
this.overlayVisibility?.release();
this.releaseExtraEvents();
if (this.hasSubtitlesWidget()) {
this.subtitlesWidget?.release();
this.subtitlesWidget = void 0;
}
this.interactionChecker?.destroy();
this.uiManager.release();
}
/**
* Collects report information for bug reporting.
* @returns {Object} Report info object.
*/
collectReportInfo() {
const info = getEnvironmentInfo();
const detectedLanguage = this.videoData?.detectedLanguage ?? "unknown";
const responseLanguage = this.videoData?.responseLanguage ?? "unknown";
const additionalInfo = `<details>
<summary>Autogenerated by VOT:</summary>
<ul>
<li>OS: ${info.os}</li>
<li>Browser: ${info.browser}</li>
<li>Loader: ${info.loader}</li>
<li>Script version: ${info.scriptVersion}</li>
<li>URL: <code>${info.url}</code></li>
<li>Lang: <code>${detectedLanguage}</code> -> <code>${responseLanguage}</code> (Lively voice: ${this.data?.useLivelyVoice ?? false} | Audio download: ${this.data?.useAudioDownload ?? false})</li>
<li>Player: ${this.data?.newAudioPlayer ? "New" : "Old"} (CSP only: ${this.data?.onlyBypassMediaCSP ?? false})</li>
<li>Proxying mode: ${this.data?.translateProxyEnabled ?? 0}</li>
</ul>
</details>`;
return {
assignees: "ilyhalight",
template: `1-bug-report-${localizationProvider.lang === "ru" ? "ru" : "en"}.yml`,
os: info.os,
"script-version": info.scriptVersion,
"additional-info": additionalInfo
};
}
/**
* Releases extra event listeners.
*/
releaseExtraEvents = releaseExtraEvents;
};
var videoObserver = new VideoObserver(createIntervalIdleChecker());
var videosWrappers = /* @__PURE__ */ new WeakMap();
var servicesCache = null;
var bootState = getOrCreateBootState();
function getFrameContext() {
return {
frame: isIframe() ? "iframe" : "top",
host: globalThis.location.hostname || "unknown",
path: globalThis.location.pathname || "/"
};
}
function logBootstrap(message, details) {
const ctx = getFrameContext();
const payload = {
host: ctx.host,
path: ctx.path
};
if (details) Object.assign(payload, details);
debug.log(`[VOT][bootstrap][${ctx.frame}] ${message}`, payload);
}
function getServicesCached() {
if (!servicesCache) servicesCache = getService();
return servicesCache;
}
/**
* Recursively finds the closest parent element matching a selector.
* @param {SiteData} site The site data.
* @param {HTMLElement} video The video element.
* @returns {HTMLElement|null} The matching parent element.
*/
function findContainer(site, video) {
debug.log("findContainer", site, site.selector, video);
if (!site.selector) {
debug.log("findContainer without selector, using parentElement");
return video.parentElement;
}
const matched = findConnectedContainerBySelector(video, site.selector);
if (site.shadowRoot) debug.log("findContainer with site.shadowRoot", matched);
else debug.log("findContainer without shadowRoot", matched);
return matched;
}
/**
* Main function to start the extension.
*/
async function main() {
const bootstrapMode = resolveBootstrapMode({
isIframe: isIframe(),
href: String(globalThis.location.href || ""),
origin: globalThis.location.origin,
authOrigin: authServerUrl
});
if (bootstrapMode === "skip") {
logBootstrap("Skipping bootstrap for non-runnable iframe");
return;
}
initIframeInteractor();
logBootstrap("Loading extension", { mode: bootstrapMode });
if (bootstrapMode === "auth-eager") await ensureRuntimeActivated("auth-page", logBootstrap);
else logBootstrap("Lazy bootstrap enabled; waiting for video detection");
bindObserverListeners({
videoObserver,
videosWrappers,
ensureRuntimeActivated: (reason) => ensureRuntimeActivated(reason, logBootstrap),
getServicesCached,
findContainer,
createVideoHandler: (video, container, site) => new VideoHandler(video, container, site)
});
videoObserver.enable();
}
function bootstrapContentScript() {
if (bootState.status === "booting" || bootState.status === "booted") {
logBootstrap("bootstrap already initialized, skipping duplicate run", { status: bootState.status });
return bootState.promise ?? Promise.resolve();
}
const runBootstrap = async () => {
try {
await main();
bootState.status = "booted";
} catch (e) {
bootState.status = "failed";
bootState.error = e;
console.error("[VOT]", e);
}
};
bootState.status = "booting";
bootState.promise = runBootstrap();
return bootState.promise;
}
bootstrapContentScript();
//#endregion
exports.VideoHandler = VideoHandler;
exports.bootstrapContentScript = bootstrapContentScript;
Object.defineProperty(exports, "countryCode", {
enumerable: true,
get: function() {
return countryCode;
}
});
exports.getEnvironmentInfo = getEnvironmentInfo;
return exports;
})({});