diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dee01e7e..2a9006ae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,7 +56,7 @@ jobs: with: name: vot-extension-${{ matrix.node-version }} path: | - dist-ext/vot-extension-chrome-*.crx + dist-ext/vot-extension-chrome-*.zip dist-ext/vot-extension-firefox-*.xpi bun: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69e8ce71..e7d1816f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,5 +33,5 @@ jobs: prerelease: false files: | vot/vot*.user.js - vot/vot-extension-chrome-*.crx + vot/vot-extension-chrome-*.zip vot/vot-extension-firefox-*.xpi diff --git a/biome.json b/biome.json index 7a8996c6..11165c8e 100644 --- a/biome.json +++ b/biome.json @@ -6,11 +6,7 @@ "useIgnoreFile": false }, "files": { - "includes": [ - "**", - "!**/dist/**/*.js", - "!**/*.d.ts" - ] + "includes": ["**", "!**/dist/**/*.js", "!**/*.d.ts"] }, "formatter": { "enabled": true, diff --git a/bun.lock b/bun.lock index 61b37f85..feeb36ee 100644 --- a/bun.lock +++ b/bun.lock @@ -14,140 +14,75 @@ }, "devDependencies": { "@toil/translate": "^1.0.8", - "@types/bun": "^1.3.9", + "@types/bun": "^1.3.11", "@vot.js/core": "^2.4.12", "crx3": "^2.0.0", - "lefthook": "^2.1.1", - "lightningcss": "^1.31.1", + "lefthook": "^2.1.4", + "lightningcss": "^1.32.0", "npm-run-all2": "^8.0.4", - "sass": "^1.97.3", + "sass": "^1.98.0", "typescript": "^5.9.3", - "vite": "^7.3.1", - "vite-plugin-monkey": "^7.1.9", - "zip-a-folder": "^4.0.4", + "vite": "^8.0.1", + "zip-a-folder": "^6.1.0", }, }, }, "packages": { "@bufbuild/protobuf": ["@bufbuild/protobuf@2.2.3", "", {}, "sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + "@emnapi/core": ["@emnapi/core@1.9.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + "@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], - - "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + "@oxc-project/types": ["@oxc-project/types@0.120.0", "", {}, "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg=="], "@parcel/watcher": ["@parcel/watcher@2.5.0", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-win32-x64": "2.5.0" } }, ""], "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.0", "", { "os": "win32", "cpu": "x64" }, ""], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.10", "", { "os": "android", "cpu": "arm64" }, "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm" }, "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "x64" }, "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.10", "", { "os": "linux", "cpu": "x64" }, "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.10", "", { "os": "none", "cpu": "arm64" }, "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.10", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.10", "", { "os": "win32", "cpu": "x64" }, "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="], - - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.10", "", {}, "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg=="], "@toil/gm-types": ["@toil/gm-types@1.0.4", "", { "peerDependencies": { "typescript": "^5.8.2" } }, "sha512-DbqZsYYrVXaBB4usDN3/HbkIhk2M+ECKv06lEScSoCMJUZ1qbtpbYLIqioEk2GE9adwWjNpH+y/Bf0ij3wQxhw=="], "@toil/translate": ["@toil/translate@1.0.8", "", { "peerDependencies": { "typescript": "^5.7.3" } }, "sha512-C4fCwQ6KPWxj5M2+ZCKukVUWLVVXS5WgjIhr1wMAwB4WSNNtPEqDWdjFlwMsreMGBMqLWWwuMXTxhL0tVLr8og=="], - "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], "@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, ""], @@ -159,160 +94,100 @@ "@vot.js/shared": ["@vot.js/shared@2.4.12", "", { "dependencies": { "@bufbuild/protobuf": "^2.0.0" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-XdMEXzLhKxKjCIkH9Lfy5MVmHz2WYcAcPTUZFN/kyJ6ErcBKsEPwDN7MLQDuD7uEXvCeUSPcbWM2i9b8R0A4dQ=="], - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - - "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], - "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, ""], - "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], - "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, ""], - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="], - "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], - - "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, ""], + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], "chaimu": ["chaimu@1.0.6", "", {}, "sha512-9PLbKaD1+RLc+BzLoQzSZ57rmuy0yKQ+5yrfHSwDS9CNkMvn8VZJDXiqPio8Ic7gSA5yiECpal8+4rJwQGBXiQ=="], "chokidar": ["chokidar@4.0.1", "", { "dependencies": { "readdirp": "^4.0.1" } }, ""], - "concat-map": ["concat-map@0.0.1", "", {}, ""], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "crx3": ["crx3@2.0.0", "", { "dependencies": { "mri": "^1.2.0", "pbf": "^4.0.1", "yazl": "^3.3.1" }, "bin": { "crx3": "bin/crx3.js" } }, "sha512-f23Oi2Zpl68aBSf5gHwn+lxQyPF+m2NAhMwwycXOxqOx6bpzDqzbcp6k/DRsyHxpsDvg5WwXcHOJSOgJ7Px5LQ=="], - "cuint": ["cuint@0.2.2", "", {}, "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw=="], - - "default-browser": ["default-browser@5.2.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, ""], - - "default-browser-id": ["default-browser-id@5.0.0", "", {}, ""], - - "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, ""], - "detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="], - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], - - "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], - - "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], - - "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - - "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], - - "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "glob": ["glob@12.0.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw=="], - - "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], - - "immutable": ["immutable@5.0.3", "", {}, "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw=="], - - "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], - - "is-docker": ["is-docker@3.0.0", "", { "bin": "cli.js" }, ""], + "immutable": ["immutable@5.1.5", "", {}, "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A=="], "is-extglob": ["is-extglob@2.1.1", "", {}, ""], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, ""], - "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": "cli.js" }, ""], - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, ""], - "isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], - "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], - "json-parse-even-better-errors": ["json-parse-even-better-errors@4.0.0", "", {}, "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA=="], - "lefthook": ["lefthook@2.1.1", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.1.1", "lefthook-darwin-x64": "2.1.1", "lefthook-freebsd-arm64": "2.1.1", "lefthook-freebsd-x64": "2.1.1", "lefthook-linux-arm64": "2.1.1", "lefthook-linux-x64": "2.1.1", "lefthook-openbsd-arm64": "2.1.1", "lefthook-openbsd-x64": "2.1.1", "lefthook-windows-arm64": "2.1.1", "lefthook-windows-x64": "2.1.1" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-Tl9h9c+sG3ShzTHKuR3LAIblnnh+Mgxnm2Ul7yu9cu260Z27LEbO3V6Zw4YZFP59/2rlD42pt/llYsQCkkCFzw=="], + "lefthook": ["lefthook@2.1.4", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.1.4", "lefthook-darwin-x64": "2.1.4", "lefthook-freebsd-arm64": "2.1.4", "lefthook-freebsd-x64": "2.1.4", "lefthook-linux-arm64": "2.1.4", "lefthook-linux-x64": "2.1.4", "lefthook-openbsd-arm64": "2.1.4", "lefthook-openbsd-x64": "2.1.4", "lefthook-windows-arm64": "2.1.4", "lefthook-windows-x64": "2.1.4" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-JNfJ5gAn0KADvJ1I6/xMcx70+/6TL6U9gqGkKvPw5RNMfatC7jIg0Evl97HN846xmfz959BV70l8r3QsBJk30w=="], - "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.1.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-O/RS1j03/Fnq5zCzEb2r7UOBsqPeBuf1C5pMkIJcO4TSE6hf3rhLUkcorKc2M5ni/n5zLGtzQUXHV08/fSAT3Q=="], + "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BUAAE9+rUrjr39a+wH/1zHmGrDdwUQ2Yq/z6BQbM/yUb9qtXBRcQ5eOXxApqWW177VhGBpX31aqIlfAZ5Q7wzw=="], - "lefthook-darwin-x64": ["lefthook-darwin-x64@2.1.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-mm/kdKl81ROPoYnj9XYk5JDqj+/6Al8w/SSPDfhItkLJyl4pqS+hWUOP6gDGrnuRk8S0DvJ2+hzhnDsQnZohWQ=="], + "lefthook-darwin-x64": ["lefthook-darwin-x64@2.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-K1ncIMEe84fe+ss1hQNO7rIvqiKy2TJvTFpkypvqFodT7mJXZn7GLKYTIXdIuyPAYthRa9DwFnx5uMoHwD2F1Q=="], - "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.1.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-F7JXlKmjxGqGbCWPLND0bVB4DMQezIe48pEwTlUQZbxh450c2gP5Q8FdttMZKOT163kBGGTqJAJSEC6zW+QSxA=="], + "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.1.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-PVUhjOhVN71YaYsVdQyNbFZ4a2jFB2Tg5hKrrn9kaWpx64aLz/XivLjwr8sEuTaP1GRlEWBpW6Bhrcsyo39qFw=="], - "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.1.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Po8/lJMqNzKSZPuEI46dLuWoBoXtAxCuRpeOh6DAV/M4RhBynaCu8rLMZ9BqF7cVbZEWoplOmYo6HdOuiYpCkQ=="], + "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.1.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ZWV9o/LeyWNEBoVO+BhLqxH3rGTba05nkm5NvMjEFSj7LbUNUDbQmupZwtHl1OMGJO66eZP0CalzRfUH6GhBxQ=="], - "lefthook-linux-arm64": ["lefthook-linux-arm64@2.1.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mI2ljFgPEqHxI8vrN9nKgnVu63Rz1KisDbPwlvs7BTYNwq3sncdK5ukpGR4zzWdh6saNJ5tCtHEtep5GQI11nw=="], + "lefthook-linux-arm64": ["lefthook-linux-arm64@2.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iWN0pGnTjrIvNIcSI1vQBJXUbybTqJ5CLMniPA0olabMXQfPDrdMKVQe+mgdwHK+E3/Y0H0ZNL3lnOj6Sk6szA=="], - "lefthook-linux-x64": ["lefthook-linux-x64@2.1.1", "", { "os": "linux", "cpu": "x64" }, "sha512-m3G/FaxC+crxeg9XeaUuHfEoL+i9gbkg2Hp2KD2IcVVIxprqlyqf0Hb8zbLV2NMXuo5RSGokJu44oAoTO3Ou2g=="], + "lefthook-linux-x64": ["lefthook-linux-x64@2.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-96bTBE/JdYgqWYAJDh+/e/0MaxJ25XTOAk7iy/fKoZ1ugf6S0W9bEFbnCFNooXOcxNVTan5xWKfcjJmPIKtsJA=="], - "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.1.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-gz/8FJPvhjOdOFt1GmFvuvDOe+W+BBRjoeAT1/mTgkN7HCXMXgqNjjvakQKQeGz1I1v08wXG1ZNf5y+T9XBCDQ=="], + "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.1.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-oYUoK6AIJNEr9lUSpIMj6g7sWzotvtc3ryw7yoOyQM6uqmEduw73URV/qGoUcm4nqqmR93ZalZwR2r3Gd61zvw=="], - "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.1.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-ch3lyMUtbmtWUufaQVn4IoEs/2hjK51XqaCdY1mh5ca//VctR1peknIwQ5feHu+vATCDviWQ7HsdNDewm3HMPg=="], + "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.1.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/Dv9Jcm68y9cggr1PhyUhOabBGP9+hzQPoiyOhKks7y9qrJl79A8XfG6LHekSuYc2VpiSu5wdnnrE1cj2nfTg=="], - "lefthook-windows-arm64": ["lefthook-windows-arm64@2.1.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mm3PZhKDs9FE/jQDimkfWxtoj9xQ2k8uw2MdhtC825bhvIh+MEi0WFj/MOW+ug0RBg0I55tGYzZ5aVuozAWpTQ=="], + "lefthook-windows-arm64": ["lefthook-windows-arm64@2.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-hSww7z+QX4YMnw2lK7DMrs3+w7NtxksuMKOkCKGyxUAC/0m1LAICo0ZbtdDtZ7agxRQQQ/SEbzFRhU5ysNcbjA=="], - "lefthook-windows-x64": ["lefthook-windows-x64@2.1.1", "", { "os": "win32", "cpu": "x64" }, "sha512-1L2oGIzmhfOTxfwbe5mpSQ+m3ilpvGNymwIhn4UHq6hwHsUL6HEhODqx02GfBn6OXpVIr56bvdBAusjL/SVYGQ=="], + "lefthook-windows-x64": ["lefthook-windows-x64@2.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-eE68LwnogxwcPgGsbVGPGxmghyMGmU9SdGwcc+uhGnUxPz1jL89oECMWJNc36zjVK24umNeDAzB5KA3lw1MuWw=="], - "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], "lit-html": ["lit-html@3.3.2", "", { "dependencies": { "@types/trusted-types": "^2.0.2" } }, "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw=="], - "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], - - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - - "make-dir": ["make-dir@3.1.0", "", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="], + "lzma": ["lzma@2.3.2", "", { "bin": { "lzma.js": "bin/lzma.js" } }, "sha512-DcfiawQ1avYbW+hsILhF38IKAlnguc/fjHrychs9hdxe4qLykvhT5VTGNs5YRWgaNePh7NTxGD4uv4gKsRomCQ=="], "memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "mime": ["mime@2.5.2", "", { "bin": { "mime": "cli.js" } }, "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg=="], - - "minimatch": ["minimatch@3.0.8", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q=="], - - "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], - "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "node-addon-api": ["node-addon-api@7.1.1", "", {}, ""], @@ -321,14 +196,8 @@ "npm-run-all2": ["npm-run-all2@8.0.4", "", { "dependencies": { "ansi-styles": "^6.2.1", "cross-spawn": "^7.0.6", "memorystream": "^0.3.1", "picomatch": "^4.0.2", "pidtree": "^0.6.0", "read-package-json-fast": "^4.0.0", "shell-quote": "^1.7.3", "which": "^5.0.0" }, "bin": { "run-p": "bin/run-p/index.js", "run-s": "bin/run-s/index.js", "npm-run-all": "bin/npm-run-all/index.js", "npm-run-all2": "bin/npm-run-all/index.js" } }, "sha512-wdbB5My48XKp2ZfJUlhnLVihzeuA1hgBnqB2J9ahV77wLS+/YAJAlN8I+X3DIFIPZ3m5L7nplmlbhNiFDmXRDA=="], - "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], - - "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], - "path-key": ["path-key@3.1.1", "", {}, ""], - "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], - "pbf": ["pbf@4.0.1", "", { "dependencies": { "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA=="], "picocolors": ["picocolors@1.1.1", "", {}, ""], @@ -337,9 +206,7 @@ "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - - "postcss-url": ["postcss-url@10.1.3", "", { "dependencies": { "make-dir": "~3.1.0", "mime": "~2.5.2", "minimatch": "~3.0.4", "xxhashjs": "~0.2.2" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw=="], + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], "protocol-buffers-schema": ["protocol-buffers-schema@3.6.0", "", {}, "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="], @@ -349,13 +216,9 @@ "resolve-protobuf-schema": ["resolve-protobuf-schema@2.1.0", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="], - "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="], + "rolldown": ["rolldown@1.0.0-rc.10", "", { "dependencies": { "@oxc-project/types": "=0.120.0", "@rolldown/pluginutils": "1.0.0-rc.10" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-x64": "1.0.0-rc.10", "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA=="], - "run-applescript": ["run-applescript@7.0.0", "", {}, ""], - - "sass": ["sass@1.97.3", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg=="], - - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "sass": ["sass@1.98.0", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, ""], @@ -363,50 +226,32 @@ "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "source-map-js": ["source-map-js@1.0.2", "", {}, ""], - - "systemjs": ["systemjs@6.15.1", "", {}, "sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@5.26.5", "", {}, ""], - "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], - - "vite-plugin-monkey": ["vite-plugin-monkey@7.1.9", "", { "dependencies": { "acorn": "^8.15.0", "acorn-walk": "^8.3.4", "cross-spawn": "^7.0.6", "htmlparser2": "^10.0.0", "import-meta-resolve": "^4.1.0", "magic-string": "^0.30.17", "mrmime": "^2.0.1", "open": "^10.2.0", "picocolors": "^1.1.1", "postcss-url": "^10.1.3", "systemjs": "^6.15.1" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["vite"] }, "sha512-kYtcRH8DsbrwfugrviYRuk50weY5yyVKcuGRkjUSc7mK0uPFtOHK8sUSEecpiS9725IF/4uyum8bYHs7dswDBg=="], + "vite": ["vite@8.0.1", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.10", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw=="], "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], - "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - - "xxhashjs": ["xxhashjs@0.2.2", "", { "dependencies": { "cuint": "^0.2.2" } }, "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw=="], - "yazl": ["yazl@3.3.1", "", { "dependencies": { "buffer-crc32": "^1.0.0" } }, "sha512-BbETDVWG+VcMUle37k5Fqp//7SDOK2/1+T7X8TD96M3D9G8jK5VLUdQVdVjGi8im7FGkazX7kk5hkU8X4L5Bng=="], - "zip-a-folder": ["zip-a-folder@4.0.4", "", { "dependencies": { "glob": "^12.0.0" } }, "sha512-sVqxEtf6cTMrdnw7XCS5ktV3h9fRSrVZ78h9ehrtcVs5hQ0th5dyT5NCH/7WAQQC3IkKFHWNvamY06JRCNHU5g=="], + "zip-a-folder": ["zip-a-folder@6.1.0", "", { "dependencies": { "lzma": "^2.3.2", "tinyglobby": "^0.2.15" }, "bin": { "zip-a-folder": "dist/cli.mjs" } }, "sha512-c/warR1v5M19y0EOov8P25NunZu37Un8ha7CQbylUm+3dO0hpS9dg/fR8MKb8bUTwCyCgfVeRdx6D05NNKjl9g=="], "@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": "bin/detect-libc.js" }, ""], "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, ""], - "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - - "glob/minimatch": ["minimatch@10.2.0", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w=="], - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, ""], - "postcss/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, ""], - - "glob/minimatch/brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], - - "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.2", "", { "dependencies": { "jackspeak": "^4.2.3" } }, "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg=="], } } diff --git a/changelog.md b/changelog.md index 0b9dc081..6af3197d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,14 @@ -# 1.11.3 +# 1.11.4 + +- Добавлена поддержка Mediafile Cloud (#1603), Jove (#1593), Datacamp (#1606), PreserveTube (#1366) +- Добавлена настройка "Язык субтитров по умолчанию": можно выбрать автоопределение, язык оригинального видео или конкретный язык +- Улучшено отображение субтитров: корректнее объединяются одновременно активные реплики, сохранены пробелы и пунктуация при переносах, улучшено разбиение длинных фраз +- Улучшено восстановление перевода после паузы/возобновления видео: если ссылка на аудиодорожку устарела, перевод автоматически обновляется +- Исправлена синхронизация громкости перевода и видео: устранен дрейф, корректно обрабатываются верхние границы и ручное изменение громкости (#1605) +- Улучшена авторизация: данные аккаунта в настройках теперь обновляются автоматически после входа без ручного обновления +- Исправлена некорректная работа на Udemy (#1617) + +# 1.11.3 - Исправлена ошибка, из-за которой UI исчезал в полноэкранном режиме - Улучшена обработка auto-generated субтитров YouTube: реплики объединяются в более цельные фразы и предложения diff --git a/dist-ext/vot-extension-chrome-1.11.3.zip b/dist-ext/vot-extension-chrome-1.11.3.zip deleted file mode 100644 index 60c35b98..00000000 Binary files a/dist-ext/vot-extension-chrome-1.11.3.zip and /dev/null differ diff --git a/dist-ext/vot-extension-chrome-1.11.4.zip b/dist-ext/vot-extension-chrome-1.11.4.zip new file mode 100644 index 00000000..f3cc5914 Binary files /dev/null and b/dist-ext/vot-extension-chrome-1.11.4.zip differ diff --git a/dist-ext/vot-extension-firefox-1.11.3.xpi b/dist-ext/vot-extension-firefox-1.11.3.xpi deleted file mode 100644 index a0c8dcab..00000000 Binary files a/dist-ext/vot-extension-firefox-1.11.3.xpi and /dev/null differ diff --git a/dist-ext/vot-extension-firefox-1.11.4.xpi b/dist-ext/vot-extension-firefox-1.11.4.xpi new file mode 100644 index 00000000..115829fa Binary files /dev/null and b/dist-ext/vot-extension-firefox-1.11.4.xpi differ diff --git a/dist-ext/vot-extension-firefox-updates.json b/dist-ext/vot-extension-firefox-updates.json index b518364c..9b5a1113 100644 --- a/dist-ext/vot-extension-firefox-updates.json +++ b/dist-ext/vot-extension-firefox-updates.json @@ -3,8 +3,8 @@ "vot-extension@firefox": { "updates": [ { - "version": "1.11.3", - "update_link": "https://raw.githubusercontent.com/ilyhalight/voice-over-translation/master/dist-ext/vot-extension-firefox-1.11.3.xpi" + "version": "1.11.4", + "update_link": "https://raw.githubusercontent.com/ilyhalight/voice-over-translation/master/dist-ext/vot-extension-firefox-1.11.4.xpi" } ] } diff --git a/dist/vot-min.user.js b/dist/vot-min.user.js index 760f540d..4de3bd46 100644 --- a/dist/vot-min.user.js +++ b/dist/vot-min.user.js @@ -1,257 +1,252 @@ // ==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.3 -// @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-min.user.js -// @updateURL https://raw.githubusercontent.com/ilyhalight/voice-over-translation/master/dist/vot-min.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 *://*.kodik.info/* -// @match *://*.kodik.biz/* -// @match *://*.kodik.cc/* -// @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 *://*/*.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 cloudflare-dns.com -// @connect porntn.com -// @connect youtube.com -// @connect googlevideo.com -// @grant GM.deleteValue -// @grant GM.getValue -// @grant GM.getValues -// @grant GM.listValues -// @grant GM.setValue -// @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 window.focus +// @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.4 +// @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-min.user.js +// @updateURL https://raw.githubusercontent.com/ilyhalight/voice-over-translation/master/dist/vot-min.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 *://*.kodik.info/* +// @match *://*.kodik.biz/* +// @match *://*.kodik.cc/* +// @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 *://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 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== -!function(){function e(e,t){return(t||"")+" (SystemJS Error#"+e+" https://github.com/systemjs/systemjs/blob/main/docs/errors.md#"+e+")"}function t(e,t){if(-1!==e.indexOf("\\")&&(e=e.replace(j,"/")),"/"===e[0]&&"/"===e[1])return t.slice(0,t.indexOf(":")+1)+e;if("."===e[0]&&("/"===e[1]||"."===e[1]&&("/"===e[2]||2===e.length&&(e+="/"))||1===e.length&&(e+="/"))||"/"===e[0]){var n,r=t.slice(0,t.indexOf(":")+1);if(n="/"===t[r.length+1]?"file:"!==r?(n=t.slice(r.length+2)).slice(n.indexOf("/")+1):t.slice(8):t.slice(r.length+("/"===t[r.length])),"/"===e[0])return t.slice(0,t.length-n.length-1)+e;for(var i=n.slice(0,n.lastIndexOf("/")+1)+e,o=[],s=-1,u=0;un.length&&"/"!==r[r.length-1]))return r+e.slice(n.length);u("W2",n,r,"should have a trailing '/'")}}function u(t,n,r,i){console.warn(e(t,"Package target "+i+", resolving target '"+r+"' for "+n))}function c(e,t,n){for(var r=e.scopes,i=n&&o(n,r);i;){var u=s(t,r[i]);if(u)return u;i=o(i.slice(0,i.lastIndexOf("/")),r)}return s(t,e.imports)||-1!==t.indexOf(":")&&t}function a(){this[M]={}}function f(e){return e.id}function l(e,t,n,r){if(e.onload(n,t.id,t.d&&t.d.map(f),!!r),n)throw n}function d(t,n,r,i){var o=t[M][n];if(o)return o;var s=[],u=Object.create(null);P&&Object.defineProperty(u,P,{value:"Module"});var c=Promise.resolve().then((function(){return t.instantiate(n,r,i)})).then((function(r){if(!r)throw Error(e(2,"Module "+n+" did not instantiate"));var i=r[1]((function(e,t){o.h=!0;var n=!1;if("string"==typeof e)e in u&&u[e]===t||(u[e]=t,n=!0);else{for(var r in e)t=e[r],r in u&&u[r]===t||(u[r]=t,n=!0);e&&e.__esModule&&(u.__esModule=e.__esModule)}if(n)for(var i=0;i-1){var n=document.createEvent("Event");n.initEvent("error",!1,!1),t.dispatchEvent(n)}return Promise.reject(e)}))}else if("systemjs-importmap"===t.type){t.sp=!0;var r=t.src?(System.fetch||fetch)(t.src,{integrity:t.integrity,priority:t.fetchPriority,passThrough:!0}).then((function(e){if(!e.ok)throw Error("Invalid status code: "+e.status);return e.text()})).catch((function(n){return n.message=e("W4","Error fetching systemjs-import map "+t.src)+"\n"+n.message,console.warn(n),"function"==typeof t.onerror&&t.onerror(),"{}"})):t.innerHTML;W=W.then((function(){return r})).then((function(n){!function(t,n,r){var o={};try{o=JSON.parse(n)}catch(s){console.warn(Error(e("W5","systemjs-importmap contains invalid JSON")+"\n\n"+n+"\n"))}i(o,r,t)}(N,n,t.src||g)}))}}))}var g,y="undefined"!=typeof Symbol,b="undefined"!=typeof self,S="undefined"!=typeof document,w=b?self:global;if(S){var O=document.querySelector("base[href]");O&&(g=O.href)}if(!g&&"undefined"!=typeof location){var E=(g=location.href.split("#")[0].split("?")[0]).lastIndexOf("/");-1!==E&&(g=g.slice(0,E+1))}var x,j=/\\/g,P=y&&Symbol.toStringTag,M=y?Symbol():"@",I=a.prototype;I.import=function(e,t,n){var r=this;return t&&"object"==typeof t&&(n=t,t=void 0),Promise.resolve(r.prepareImport()).then((function(){return r.resolve(e,t,n)})).then((function(e){var t=d(r,e,void 0,n);return t.C||p(r,t)}))},I.createContext=function(e){var t=this;return{url:e,resolve:function(n,r){return Promise.resolve(t.resolve(n,r||e))}}},I.onload=function(){},I.register=function(e,t,n){x=[e,t,n]},I.getRegister=function(){var e=x;return x=void 0,e};var L=Object.freeze(Object.create(null));w.System=new a;var C,R,W=Promise.resolve(),N={imports:{},scopes:{},depcache:{},integrity:{}},T=S;if(I.prepareImport=function(e){return(T||e)&&(m(),T=!1),W},I.getImportMap=function(){return JSON.parse(JSON.stringify(N))},S&&(m(),window.addEventListener("DOMContentLoaded",m)),I.addImportMap=function(e,t){i(e,t||g,N)},S){window.addEventListener("error",(function(e){J=e.filename,_=e.error}));var A=location.origin}I.createScript=function(e){var t=document.createElement("script");t.async=!0,e.indexOf(A+"/")&&(t.crossOrigin="anonymous");var n=N.integrity[e];return n&&(t.integrity=n),t.src=e,t};var J,_,k={},U=I.register;I.register=function(e,t){if(S&&"loading"===document.readyState&&"string"!=typeof e){var n=document.querySelectorAll("script[src]"),r=n[n.length-1];if(r){C=e;var i=this;R=setTimeout((function(){k[r.src]=[e,t],i.import(r.src)}))}}else C=void 0;return U.call(this,e,t)},I.instantiate=function(t,n){var r=k[t];if(r)return delete k[t],r;var i=this;return Promise.resolve(I.createScript(t)).then((function(r){return new Promise((function(o,s){r.addEventListener("error",(function(){s(Error(e(3,"Error loading "+t+(n?" from "+n:""))))})),r.addEventListener("load",(function(){if(document.head.removeChild(r),J===t)s(_);else{var e=i.getRegister(t);e&&e[0]===C&&clearTimeout(R),o(e)}})),document.head.appendChild(r)}))}))},I.shouldFetch=function(){return!1},"undefined"!=typeof fetch&&(I.fetch=fetch);var $=I.instantiate,B=/^(text|application)\/(x-)?javascript(;|$)/;I.instantiate=function(t,n,r){var i=this;return this.shouldFetch(t,n,r)?this.fetch(t,{credentials:"same-origin",integrity:N.integrity[t],meta:r}).then((function(r){if(!r.ok)throw Error(e(7,r.status+" "+r.statusText+", loading "+t+(n?" from "+n:"")));var o=r.headers.get("content-type");if(!o||!B.test(o))throw Error(e(4,'Unknown Content-Type "'+o+'", loading '+t+(n?" from "+n:"")));return r.text().then((function(e){return e.indexOf("//# sourceURL=")<0&&(e+="\n//# sourceURL="+t),(0,eval)(e),i.getRegister(t)}))})):$.apply(this,arguments)},I.resolve=function(n,r){return c(N,t(n,r=r||g)||n,r)||function(t,n){throw Error(e(8,"Unable to resolve bare specifier '"+t+(n?"' from "+n:"'")))}(n,r)};var F=I.instantiate;I.instantiate=function(e,t,n){var r=N.depcache[e];if(r)for(var i=0;i{t.has(e)||(t.add(e),(a=>GM_addStyle(a))(e));}; - - const Y={host:"api.browser.yandex.ru",hostVOT:"vot.toil.cc/v1",hostWorker:"vot-worker.toil.cc",mediaProxy:"media-proxy.toil.cc",userAgent:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 YaBrowser/25.12.0.0 Safari/537.36",componentVersion:"25.12.4.1198",hmac:"bt8xH3VOlb4mqf0nqAibnDOoiPlXsisf",defaultDuration:343,minChunkSize:5295308,loggerLevel:1,version:"2.4.14"};function Ta(){let n=0,t=0;for(let i=0;i<28;i+=7){let o=this.buf[this.pos++];if(n|=(o&127)<>4,(e&128)==0)return this.assertBounds(),[n,t];for(let i=3;i<=31;i+=7){let o=this.buf[this.pos++];if(t|=(o&127)<>>r,a=!(!(s>>>7)&&t==0),l=(a?s|128:s)&255;if(e.push(l),!a)return}const i=n>>>28&15|(t&7)<<4,o=t>>3!=0;if(e.push((o?i|128:i)&255),!!o){for(let r=3;r<31;r=r+7){const s=t>>>r,a=!!(s>>>7),l=(a?s|128:s)&255;if(e.push(l),!a)return}e.push(t>>>31&1);}}const Ge=4294967296;function Mi(n){const t=n[0]==="-";t&&(n=n.slice(1));const e=1e6;let i=0,o=0;function r(s,a){const l=Number(n.slice(s,a));o*=e,i=i*e+l,i>=Ge&&(o=o+(i/Ge|0),i=i%Ge);}return r(-24,-18),r(-18,-12),r(-12,-6),r(-6),t?Or(i,o):oi(i,o)}function La(n,t){let e=oi(n,t);const i=e.hi&2147483648;i&&(e=Or(e.lo,e.hi));const o=_r(e.lo,e.hi);return i?"-"+o:o}function _r(n,t){if({lo:n,hi:t}=Aa(n,t),t<=2097151)return String(Ge*t+n);const e=n&16777215,i=(n>>>24|t<<8)&16777215,o=t>>16&65535;let r=e+i*6777216+o*6710656,s=i+o*8147497,a=o*2;const l=1e7;return r>=l&&(s+=Math.floor(r/l),r%=l),s>=l&&(a+=Math.floor(s/l),s%=l),a.toString()+_i(s)+_i(r)}function Aa(n,t){return {lo:n>>>0,hi:t>>>0}}function oi(n,t){return {lo:n|0,hi:t|0}}function Or(n,t){return t=~t,n?n=~n+1:t+=1,oi(n,t)}const _i=n=>{const t=String(n);return "0000000".slice(t.length)+t};function Oi(n,t){if(n>=0){for(;n>127;)t.push(n&127|128),n=n>>>7;t.push(n);}else {for(let e=0;e<9;e++)t.push(n&127|128),n=n>>7;t.push(1);}}function Ia(){let n=this.buf[this.pos++],t=n&127;if((n&128)==0)return this.assertBounds(),t;if(n=this.buf[this.pos++],t|=(n&127)<<7,(n&128)==0)return this.assertBounds(),t;if(n=this.buf[this.pos++],t|=(n&127)<<14,(n&128)==0)return this.assertBounds(),t;if(n=this.buf[this.pos++],t|=(n&127)<<21,(n&128)==0)return this.assertBounds(),t;n=this.buf[this.pos++],t|=(n&15)<<28;for(let e=5;(n&128)!==0&&e<10;e++)n=this.buf[this.pos++];if((n&128)!=0)throw new Error("invalid varint");return this.assertBounds(),t>>>0}var Di={};const ht=Ca();function Ca(){const n=new DataView(new ArrayBuffer(8));if(typeof BigInt=="function"&&typeof n.getBigInt64=="function"&&typeof n.getBigUint64=="function"&&typeof n.setBigInt64=="function"&&typeof n.setBigUint64=="function"&&(typeof process!="object"||typeof Di!="object"||Di.BUF_BIGINT_DISABLE!=="1")){const e=BigInt("-9223372036854775808"),i=BigInt("9223372036854775807"),o=BigInt("0"),r=BigInt("18446744073709551615");return {zero:BigInt(0),supported:true,parse(s){const a=typeof s=="bigint"?s:BigInt(s);if(a>i||ar||a>>0)}raw(t){return this.buf.length&&(this.chunks.push(new Uint8Array(this.buf)),this.buf=[]),this.chunks.push(t),this}uint32(t){for(Fi(t);t>127;)this.buf.push(t&127|128),t=t>>>7;return this.buf.push(t),this}int32(t){return kn(t),Oi(t,this.buf),this}bool(t){return this.buf.push(t?1:0),this}bytes(t){return this.uint32(t.byteLength),this.raw(t)}string(t){let e=this.encodeUtf8(t);return this.uint32(e.byteLength),this.raw(e)}float(t){Oa(t);let e=new Uint8Array(4);return new DataView(e.buffer).setFloat32(0,t,true),this.raw(e)}double(t){let e=new Uint8Array(8);return new DataView(e.buffer).setFloat64(0,t,true),this.raw(e)}fixed32(t){Fi(t);let e=new Uint8Array(4);return new DataView(e.buffer).setUint32(0,t,true),this.raw(e)}sfixed32(t){kn(t);let e=new Uint8Array(4);return new DataView(e.buffer).setInt32(0,t,true),this.raw(e)}sint32(t){return kn(t),t=(t<<1^t>>31)>>>0,Oi(t,this.buf),this}sfixed64(t){let e=new Uint8Array(8),i=new DataView(e.buffer),o=ht.enc(t);return i.setInt32(0,o.lo,true),i.setInt32(4,o.hi,true),this.raw(e)}fixed64(t){let e=new Uint8Array(8),i=new DataView(e.buffer),o=ht.uEnc(t);return i.setInt32(0,o.lo,true),i.setInt32(4,o.hi,true),this.raw(e)}int64(t){let e=ht.enc(t);return Sn(e.lo,e.hi,this.buf),this}sint64(t){let e=ht.enc(t),i=e.hi>>31,o=e.lo<<1^i,r=(e.hi<<1|e.lo>>>31)^i;return Sn(o,r,this.buf),this}uint64(t){let e=ht.uEnc(t);return Sn(e.lo,e.hi,this.buf),this}}class M{constructor(t,e=Dr().decodeUtf8){this.decodeUtf8=e,this.varint64=Ta,this.uint32=Ia,this.buf=t,this.len=t.length,this.pos=0,this.view=new DataView(t.buffer,t.byteOffset,t.byteLength);}tag(){let t=this.uint32(),e=t>>>3,i=t&7;if(e<=0||i<0||i>5)throw new Error("illegal tag: field no "+e+" wire type "+i);return [e,i]}skip(t,e){let i=this.pos;switch(t){case St.Varint:for(;this.buf[this.pos++]&128;);break;case St.Bit64:this.pos+=4;case St.Bit32:this.pos+=4;break;case St.LengthDelimited:let o=this.uint32();this.pos+=o;break;case St.StartGroup:for(;;){const[r,s]=this.tag();if(s===St.EndGroup){if(e!==void 0&&r!==e)throw new Error("invalid end group tag");break}this.skip(s,r);}break;default:throw new Error("cant skip wire type "+t)}return this.assertBounds(),this.buf.subarray(i,this.pos)}assertBounds(){if(this.pos>this.len)throw new RangeError("premature EOF")}int32(){return this.uint32()|0}sint32(){let t=this.uint32();return t>>>1^-(t&1)}int64(){return ht.dec(...this.varint64())}uint64(){return ht.uDec(...this.varint64())}sint64(){let[t,e]=this.varint64(),i=-(t&1);return t=(t>>>1|(e&1)<<31)^i,e=e>>>1^i,ht.dec(t,e)}bool(){let[t,e]=this.varint64();return t!==0||e!==0}fixed32(){return this.view.getUint32((this.pos+=4)-4,true)}sfixed32(){return this.view.getInt32((this.pos+=4)-4,true)}fixed64(){return ht.uDec(this.sfixed32(),this.sfixed32())}sfixed64(){return ht.dec(this.sfixed32(),this.sfixed32())}float(){return this.view.getFloat32((this.pos+=4)-4,true)}double(){return this.view.getFloat64((this.pos+=8)-8,true)}bytes(){let t=this.uint32(),e=this.pos;return this.pos+=t,this.assertBounds(),this.buf.subarray(e,e+t)}string(){return this.decodeUtf8(this.bytes())}}function kn(n){if(typeof n=="string")n=Number(n);else if(typeof n!="number")throw new Error("invalid int32: "+typeof n);if(!Number.isInteger(n)||n>Ma||n<_a)throw new Error("invalid int32: "+n)}function Fi(n){if(typeof n=="string")n=Number(n);else if(typeof n!="number")throw new Error("invalid uint32: "+typeof n);if(!Number.isInteger(n)||n>Va||n<0)throw new Error("invalid uint32: "+n)}function Oa(n){if(typeof n=="string"){const t=n;if(n=Number(n),isNaN(n)&&t!=="NaN")throw new Error("invalid float32: "+t)}else if(typeof n!="number")throw new Error("invalid float32: "+typeof n);if(Number.isFinite(n)&&(n>Ea||n>>3){case 1:{if(r!==10)break;o.target=e.string();continue}case 2:{if(r!==18)break;o.targetUrl=e.string();continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {target:T(n.target)?globalThis.String(n.target):"",targetUrl:T(n.targetUrl)?globalThis.String(n.targetUrl):""}},toJSON(n){const t={};return n.target!==""&&(t.target=n.target),n.targetUrl!==""&&(t.targetUrl=n.targetUrl),t},create(n){return zt.fromPartial(n??{})},fromPartial(n){const t=Ni();return t.target=n.target??"",t.targetUrl=n.targetUrl??"",t}};function $i(){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:""}}const Rr={encode(n,t=new U){n.url!==""&&t.uint32(26).string(n.url),n.deviceId!==void 0&&t.uint32(34).string(n.deviceId),n.firstRequest!==false&&t.uint32(40).bool(n.firstRequest),n.duration!==0&&t.uint32(49).double(n.duration),n.unknown0!==0&&t.uint32(56).int32(n.unknown0),n.language!==""&&t.uint32(66).string(n.language),n.forceSourceLang!==false&&t.uint32(72).bool(n.forceSourceLang),n.unknown1!==0&&t.uint32(80).int32(n.unknown1);for(const e of n.translationHelp)zt.encode(e,t.uint32(90).fork()).join();return n.wasStream!==false&&t.uint32(104).bool(n.wasStream),n.responseLanguage!==""&&t.uint32(114).string(n.responseLanguage),n.unknown2!==0&&t.uint32(120).int32(n.unknown2),n.unknown3!==0&&t.uint32(128).int32(n.unknown3),n.bypassCache!==false&&t.uint32(136).bool(n.bypassCache),n.useLivelyVoice!==false&&t.uint32(144).bool(n.useLivelyVoice),n.videoTitle!==""&&t.uint32(154).string(n.videoTitle),t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=$i();for(;e.pos>>3){case 3:{if(r!==26)break;o.url=e.string();continue}case 4:{if(r!==34)break;o.deviceId=e.string();continue}case 5:{if(r!==40)break;o.firstRequest=e.bool();continue}case 6:{if(r!==49)break;o.duration=e.double();continue}case 7:{if(r!==56)break;o.unknown0=e.int32();continue}case 8:{if(r!==66)break;o.language=e.string();continue}case 9:{if(r!==72)break;o.forceSourceLang=e.bool();continue}case 10:{if(r!==80)break;o.unknown1=e.int32();continue}case 11:{if(r!==90)break;o.translationHelp.push(zt.decode(e,e.uint32()));continue}case 13:{if(r!==104)break;o.wasStream=e.bool();continue}case 14:{if(r!==114)break;o.responseLanguage=e.string();continue}case 15:{if(r!==120)break;o.unknown2=e.int32();continue}case 16:{if(r!==128)break;o.unknown3=e.int32();continue}case 17:{if(r!==136)break;o.bypassCache=e.bool();continue}case 18:{if(r!==144)break;o.useLivelyVoice=e.bool();continue}case 19:{if(r!==154)break;o.videoTitle=e.string();continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {url:T(n.url)?globalThis.String(n.url):"",deviceId:T(n.deviceId)?globalThis.String(n.deviceId):void 0,firstRequest:T(n.firstRequest)?globalThis.Boolean(n.firstRequest):false,duration:T(n.duration)?globalThis.Number(n.duration):0,unknown0:T(n.unknown0)?globalThis.Number(n.unknown0):0,language:T(n.language)?globalThis.String(n.language):"",forceSourceLang:T(n.forceSourceLang)?globalThis.Boolean(n.forceSourceLang):false,unknown1:T(n.unknown1)?globalThis.Number(n.unknown1):0,translationHelp:globalThis.Array.isArray(n?.translationHelp)?n.translationHelp.map(t=>zt.fromJSON(t)):[],wasStream:T(n.wasStream)?globalThis.Boolean(n.wasStream):false,responseLanguage:T(n.responseLanguage)?globalThis.String(n.responseLanguage):"",unknown2:T(n.unknown2)?globalThis.Number(n.unknown2):0,unknown3:T(n.unknown3)?globalThis.Number(n.unknown3):0,bypassCache:T(n.bypassCache)?globalThis.Boolean(n.bypassCache):false,useLivelyVoice:T(n.useLivelyVoice)?globalThis.Boolean(n.useLivelyVoice):false,videoTitle:T(n.videoTitle)?globalThis.String(n.videoTitle):""}},toJSON(n){const t={};return n.url!==""&&(t.url=n.url),n.deviceId!==void 0&&(t.deviceId=n.deviceId),n.firstRequest!==false&&(t.firstRequest=n.firstRequest),n.duration!==0&&(t.duration=n.duration),n.unknown0!==0&&(t.unknown0=Math.round(n.unknown0)),n.language!==""&&(t.language=n.language),n.forceSourceLang!==false&&(t.forceSourceLang=n.forceSourceLang),n.unknown1!==0&&(t.unknown1=Math.round(n.unknown1)),n.translationHelp?.length&&(t.translationHelp=n.translationHelp.map(e=>zt.toJSON(e))),n.wasStream!==false&&(t.wasStream=n.wasStream),n.responseLanguage!==""&&(t.responseLanguage=n.responseLanguage),n.unknown2!==0&&(t.unknown2=Math.round(n.unknown2)),n.unknown3!==0&&(t.unknown3=Math.round(n.unknown3)),n.bypassCache!==false&&(t.bypassCache=n.bypassCache),n.useLivelyVoice!==false&&(t.useLivelyVoice=n.useLivelyVoice),n.videoTitle!==""&&(t.videoTitle=n.videoTitle),t},create(n){return Rr.fromPartial(n??{})},fromPartial(n){const t=$i();return t.url=n.url??"",t.deviceId=n.deviceId??void 0,t.firstRequest=n.firstRequest??false,t.duration=n.duration??0,t.unknown0=n.unknown0??0,t.language=n.language??"",t.forceSourceLang=n.forceSourceLang??false,t.unknown1=n.unknown1??0,t.translationHelp=n.translationHelp?.map(e=>zt.fromPartial(e))||[],t.wasStream=n.wasStream??false,t.responseLanguage=n.responseLanguage??"",t.unknown2=n.unknown2??0,t.unknown3=n.unknown3??0,t.bypassCache=n.bypassCache??false,t.useLivelyVoice=n.useLivelyVoice??false,t.videoTitle=n.videoTitle??"",t}};function Hi(){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}}const Br={encode(n,t=new U){return n.url!==void 0&&t.uint32(10).string(n.url),n.duration!==void 0&&t.uint32(17).double(n.duration),n.status!==0&&t.uint32(32).int32(n.status),n.remainingTime!==void 0&&t.uint32(40).int32(n.remainingTime),n.unknown0!==void 0&&t.uint32(48).int32(n.unknown0),n.translationId!==""&&t.uint32(58).string(n.translationId),n.language!==void 0&&t.uint32(66).string(n.language),n.message!==void 0&&t.uint32(74).string(n.message),n.isLivelyVoice!==false&&t.uint32(80).bool(n.isLivelyVoice),n.unknown2!==void 0&&t.uint32(88).int32(n.unknown2),n.shouldRetry!==void 0&&t.uint32(96).int32(n.shouldRetry),n.unknown3!==void 0&&t.uint32(104).int32(n.unknown3),t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=Hi();for(;e.pos>>3){case 1:{if(r!==10)break;o.url=e.string();continue}case 2:{if(r!==17)break;o.duration=e.double();continue}case 4:{if(r!==32)break;o.status=e.int32();continue}case 5:{if(r!==40)break;o.remainingTime=e.int32();continue}case 6:{if(r!==48)break;o.unknown0=e.int32();continue}case 7:{if(r!==58)break;o.translationId=e.string();continue}case 8:{if(r!==66)break;o.language=e.string();continue}case 9:{if(r!==74)break;o.message=e.string();continue}case 10:{if(r!==80)break;o.isLivelyVoice=e.bool();continue}case 11:{if(r!==88)break;o.unknown2=e.int32();continue}case 12:{if(r!==96)break;o.shouldRetry=e.int32();continue}case 13:{if(r!==104)break;o.unknown3=e.int32();continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {url:T(n.url)?globalThis.String(n.url):void 0,duration:T(n.duration)?globalThis.Number(n.duration):void 0,status:T(n.status)?globalThis.Number(n.status):0,remainingTime:T(n.remainingTime)?globalThis.Number(n.remainingTime):void 0,unknown0:T(n.unknown0)?globalThis.Number(n.unknown0):void 0,translationId:T(n.translationId)?globalThis.String(n.translationId):"",language:T(n.language)?globalThis.String(n.language):void 0,message:T(n.message)?globalThis.String(n.message):void 0,isLivelyVoice:T(n.isLivelyVoice)?globalThis.Boolean(n.isLivelyVoice):false,unknown2:T(n.unknown2)?globalThis.Number(n.unknown2):void 0,shouldRetry:T(n.shouldRetry)?globalThis.Number(n.shouldRetry):void 0,unknown3:T(n.unknown3)?globalThis.Number(n.unknown3):void 0}},toJSON(n){const t={};return n.url!==void 0&&(t.url=n.url),n.duration!==void 0&&(t.duration=n.duration),n.status!==0&&(t.status=Math.round(n.status)),n.remainingTime!==void 0&&(t.remainingTime=Math.round(n.remainingTime)),n.unknown0!==void 0&&(t.unknown0=Math.round(n.unknown0)),n.translationId!==""&&(t.translationId=n.translationId),n.language!==void 0&&(t.language=n.language),n.message!==void 0&&(t.message=n.message),n.isLivelyVoice!==false&&(t.isLivelyVoice=n.isLivelyVoice),n.unknown2!==void 0&&(t.unknown2=Math.round(n.unknown2)),n.shouldRetry!==void 0&&(t.shouldRetry=Math.round(n.shouldRetry)),n.unknown3!==void 0&&(t.unknown3=Math.round(n.unknown3)),t},create(n){return Br.fromPartial(n??{})},fromPartial(n){const t=Hi();return t.url=n.url??void 0,t.duration=n.duration??void 0,t.status=n.status??0,t.remainingTime=n.remainingTime??void 0,t.unknown0=n.unknown0??void 0,t.translationId=n.translationId??"",t.language=n.language??void 0,t.message=n.message??void 0,t.isLivelyVoice=n.isLivelyVoice??false,t.unknown2=n.unknown2??void 0,t.shouldRetry=n.shouldRetry??void 0,t.unknown3=n.unknown3??void 0,t}};function Ui(){return {status:0,remainingTime:void 0,message:void 0,unknown0:void 0}}const ct={encode(n,t=new U){return n.status!==0&&t.uint32(8).int32(n.status),n.remainingTime!==void 0&&t.uint32(16).int32(n.remainingTime),n.message!==void 0&&t.uint32(26).string(n.message),n.unknown0!==void 0&&t.uint32(32).int32(n.unknown0),t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=Ui();for(;e.pos>>3){case 1:{if(r!==8)break;o.status=e.int32();continue}case 2:{if(r!==16)break;o.remainingTime=e.int32();continue}case 3:{if(r!==26)break;o.message=e.string();continue}case 4:{if(r!==32)break;o.unknown0=e.int32();continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {status:T(n.status)?globalThis.Number(n.status):0,remainingTime:T(n.remainingTime)?globalThis.Number(n.remainingTime):void 0,message:T(n.message)?globalThis.String(n.message):void 0,unknown0:T(n.unknown0)?globalThis.Number(n.unknown0):void 0}},toJSON(n){const t={};return n.status!==0&&(t.status=Math.round(n.status)),n.remainingTime!==void 0&&(t.remainingTime=Math.round(n.remainingTime)),n.message!==void 0&&(t.message=n.message),n.unknown0!==void 0&&(t.unknown0=Math.round(n.unknown0)),t},create(n){return ct.fromPartial(n??{})},fromPartial(n){const t=Ui();return t.status=n.status??0,t.remainingTime=n.remainingTime??void 0,t.message=n.message??void 0,t.unknown0=n.unknown0??void 0,t}};function zi(){return {url:"",duration:0,language:"",responseLanguage:""}}const Fr={encode(n,t=new U){return n.url!==""&&t.uint32(10).string(n.url),n.duration!==0&&t.uint32(17).double(n.duration),n.language!==""&&t.uint32(26).string(n.language),n.responseLanguage!==""&&t.uint32(34).string(n.responseLanguage),t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=zi();for(;e.pos>>3){case 1:{if(r!==10)break;o.url=e.string();continue}case 2:{if(r!==17)break;o.duration=e.double();continue}case 3:{if(r!==26)break;o.language=e.string();continue}case 4:{if(r!==34)break;o.responseLanguage=e.string();continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {url:T(n.url)?globalThis.String(n.url):"",duration:T(n.duration)?globalThis.Number(n.duration):0,language:T(n.language)?globalThis.String(n.language):"",responseLanguage:T(n.responseLanguage)?globalThis.String(n.responseLanguage):""}},toJSON(n){const t={};return n.url!==""&&(t.url=n.url),n.duration!==0&&(t.duration=n.duration),n.language!==""&&(t.language=n.language),n.responseLanguage!==""&&(t.responseLanguage=n.responseLanguage),t},create(n){return Fr.fromPartial(n??{})},fromPartial(n){const t=zi();return t.url=n.url??"",t.duration=n.duration??0,t.language=n.language??"",t.responseLanguage=n.responseLanguage??"",t}};function Wi(){return {default:void 0,cloning:void 0}}const Nr={encode(n,t=new U){return n.default!==void 0&&ct.encode(n.default,t.uint32(10).fork()).join(),n.cloning!==void 0&&ct.encode(n.cloning,t.uint32(18).fork()).join(),t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=Wi();for(;e.pos>>3){case 1:{if(r!==10)break;o.default=ct.decode(e,e.uint32());continue}case 2:{if(r!==18)break;o.cloning=ct.decode(e,e.uint32());continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {default:T(n.default)?ct.fromJSON(n.default):void 0,cloning:T(n.cloning)?ct.fromJSON(n.cloning):void 0}},toJSON(n){const t={};return n.default!==void 0&&(t.default=ct.toJSON(n.default)),n.cloning!==void 0&&(t.cloning=ct.toJSON(n.cloning)),t},create(n){return Nr.fromPartial(n??{})},fromPartial(n){const t=Wi();return t.default=n.default!==void 0&&n.default!==null?ct.fromPartial(n.default):void 0,t.cloning=n.cloning!==void 0&&n.cloning!==null?ct.fromPartial(n.cloning):void 0,t}};function qi(){return {audioFile:new Uint8Array(0),fileId:""}}const Wt={encode(n,t=new U){return n.audioFile.length!==0&&t.uint32(18).bytes(n.audioFile),n.fileId!==""&&t.uint32(10).string(n.fileId),t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=qi();for(;e.pos>>3){case 2:{if(r!==18)break;o.audioFile=e.bytes();continue}case 1:{if(r!==10)break;o.fileId=e.string();continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {audioFile:T(n.audioFile)?Yr(n.audioFile):new Uint8Array(0),fileId:T(n.fileId)?globalThis.String(n.fileId):""}},toJSON(n){const t={};return n.audioFile.length!==0&&(t.audioFile=Xr(n.audioFile)),n.fileId!==""&&(t.fileId=n.fileId),t},create(n){return Wt.fromPartial(n??{})},fromPartial(n){const t=qi();return t.audioFile=n.audioFile??new Uint8Array(0),t.fileId=n.fileId??"",t}};function Gi(){return {audioFile:new Uint8Array(0),chunkId:0}}const qt={encode(n,t=new U){return n.audioFile.length!==0&&t.uint32(18).bytes(n.audioFile),n.chunkId!==0&&t.uint32(8).int32(n.chunkId),t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=Gi();for(;e.pos>>3){case 2:{if(r!==18)break;o.audioFile=e.bytes();continue}case 1:{if(r!==8)break;o.chunkId=e.int32();continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {audioFile:T(n.audioFile)?Yr(n.audioFile):new Uint8Array(0),chunkId:T(n.chunkId)?globalThis.Number(n.chunkId):0}},toJSON(n){const t={};return n.audioFile.length!==0&&(t.audioFile=Xr(n.audioFile)),n.chunkId!==0&&(t.chunkId=Math.round(n.chunkId)),t},create(n){return qt.fromPartial(n??{})},fromPartial(n){const t=Gi();return t.audioFile=n.audioFile??new Uint8Array(0),t.chunkId=n.chunkId??0,t}};function Ki(){return {audioBuffer:void 0,audioPartsLength:0,fileId:"",version:0}}const Gt={encode(n,t=new U){return n.audioBuffer!==void 0&&qt.encode(n.audioBuffer,t.uint32(10).fork()).join(),n.audioPartsLength!==0&&t.uint32(16).int32(n.audioPartsLength),n.fileId!==""&&t.uint32(26).string(n.fileId),n.version!==0&&t.uint32(32).int32(n.version),t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=Ki();for(;e.pos>>3){case 1:{if(r!==10)break;o.audioBuffer=qt.decode(e,e.uint32());continue}case 2:{if(r!==16)break;o.audioPartsLength=e.int32();continue}case 3:{if(r!==26)break;o.fileId=e.string();continue}case 4:{if(r!==32)break;o.version=e.int32();continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {audioBuffer:T(n.audioBuffer)?qt.fromJSON(n.audioBuffer):void 0,audioPartsLength:T(n.audioPartsLength)?globalThis.Number(n.audioPartsLength):0,fileId:T(n.fileId)?globalThis.String(n.fileId):"",version:T(n.version)?globalThis.Number(n.version):0}},toJSON(n){const t={};return n.audioBuffer!==void 0&&(t.audioBuffer=qt.toJSON(n.audioBuffer)),n.audioPartsLength!==0&&(t.audioPartsLength=Math.round(n.audioPartsLength)),n.fileId!==""&&(t.fileId=n.fileId),n.version!==0&&(t.version=Math.round(n.version)),t},create(n){return Gt.fromPartial(n??{})},fromPartial(n){const t=Ki();return t.audioBuffer=n.audioBuffer!==void 0&&n.audioBuffer!==null?qt.fromPartial(n.audioBuffer):void 0,t.audioPartsLength=n.audioPartsLength??0,t.fileId=n.fileId??"",t.version=n.version??0,t}};function Yi(){return {translationId:"",url:"",partialAudioInfo:void 0,audioInfo:void 0}}const zn={encode(n,t=new U){return n.translationId!==""&&t.uint32(10).string(n.translationId),n.url!==""&&t.uint32(18).string(n.url),n.partialAudioInfo!==void 0&&Gt.encode(n.partialAudioInfo,t.uint32(34).fork()).join(),n.audioInfo!==void 0&&Wt.encode(n.audioInfo,t.uint32(50).fork()).join(),t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=Yi();for(;e.pos>>3){case 1:{if(r!==10)break;o.translationId=e.string();continue}case 2:{if(r!==18)break;o.url=e.string();continue}case 4:{if(r!==34)break;o.partialAudioInfo=Gt.decode(e,e.uint32());continue}case 6:{if(r!==50)break;o.audioInfo=Wt.decode(e,e.uint32());continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {translationId:T(n.translationId)?globalThis.String(n.translationId):"",url:T(n.url)?globalThis.String(n.url):"",partialAudioInfo:T(n.partialAudioInfo)?Gt.fromJSON(n.partialAudioInfo):void 0,audioInfo:T(n.audioInfo)?Wt.fromJSON(n.audioInfo):void 0}},toJSON(n){const t={};return n.translationId!==""&&(t.translationId=n.translationId),n.url!==""&&(t.url=n.url),n.partialAudioInfo!==void 0&&(t.partialAudioInfo=Gt.toJSON(n.partialAudioInfo)),n.audioInfo!==void 0&&(t.audioInfo=Wt.toJSON(n.audioInfo)),t},create(n){return zn.fromPartial(n??{})},fromPartial(n){const t=Yi();return t.translationId=n.translationId??"",t.url=n.url??"",t.partialAudioInfo=n.partialAudioInfo!==void 0&&n.partialAudioInfo!==null?Gt.fromPartial(n.partialAudioInfo):void 0,t.audioInfo=n.audioInfo!==void 0&&n.audioInfo!==null?Wt.fromPartial(n.audioInfo):void 0,t}};function Xi(){return {status:0,remainingChunks:[]}}const $r={encode(n,t=new U){n.status!==0&&t.uint32(8).int32(n.status);for(const e of n.remainingChunks)t.uint32(18).string(e);return t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=Xi();for(;e.pos>>3){case 1:{if(r!==8)break;o.status=e.int32();continue}case 2:{if(r!==18)break;o.remainingChunks.push(e.string());continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {status:T(n.status)?globalThis.Number(n.status):0,remainingChunks:globalThis.Array.isArray(n?.remainingChunks)?n.remainingChunks.map(t=>globalThis.String(t)):[]}},toJSON(n){const t={};return n.status!==0&&(t.status=Math.round(n.status)),n.remainingChunks?.length&&(t.remainingChunks=n.remainingChunks),t},create(n){return $r.fromPartial(n??{})},fromPartial(n){const t=Xi();return t.status=n.status??0,t.remainingChunks=n.remainingChunks?.map(e=>e)||[],t}};function Ji(){return {language:"",url:"",unknown0:0,translatedLanguage:"",translatedUrl:"",unknown1:0,unknown2:0}}const Kt={encode(n,t=new U){return n.language!==""&&t.uint32(10).string(n.language),n.url!==""&&t.uint32(18).string(n.url),n.unknown0!==0&&t.uint32(24).int32(n.unknown0),n.translatedLanguage!==""&&t.uint32(34).string(n.translatedLanguage),n.translatedUrl!==""&&t.uint32(42).string(n.translatedUrl),n.unknown1!==0&&t.uint32(48).int32(n.unknown1),n.unknown2!==0&&t.uint32(56).int32(n.unknown2),t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=Ji();for(;e.pos>>3){case 1:{if(r!==10)break;o.language=e.string();continue}case 2:{if(r!==18)break;o.url=e.string();continue}case 3:{if(r!==24)break;o.unknown0=e.int32();continue}case 4:{if(r!==34)break;o.translatedLanguage=e.string();continue}case 5:{if(r!==42)break;o.translatedUrl=e.string();continue}case 6:{if(r!==48)break;o.unknown1=e.int32();continue}case 7:{if(r!==56)break;o.unknown2=e.int32();continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {language:T(n.language)?globalThis.String(n.language):"",url:T(n.url)?globalThis.String(n.url):"",unknown0:T(n.unknown0)?globalThis.Number(n.unknown0):0,translatedLanguage:T(n.translatedLanguage)?globalThis.String(n.translatedLanguage):"",translatedUrl:T(n.translatedUrl)?globalThis.String(n.translatedUrl):"",unknown1:T(n.unknown1)?globalThis.Number(n.unknown1):0,unknown2:T(n.unknown2)?globalThis.Number(n.unknown2):0}},toJSON(n){const t={};return n.language!==""&&(t.language=n.language),n.url!==""&&(t.url=n.url),n.unknown0!==0&&(t.unknown0=Math.round(n.unknown0)),n.translatedLanguage!==""&&(t.translatedLanguage=n.translatedLanguage),n.translatedUrl!==""&&(t.translatedUrl=n.translatedUrl),n.unknown1!==0&&(t.unknown1=Math.round(n.unknown1)),n.unknown2!==0&&(t.unknown2=Math.round(n.unknown2)),t},create(n){return Kt.fromPartial(n??{})},fromPartial(n){const t=Ji();return t.language=n.language??"",t.url=n.url??"",t.unknown0=n.unknown0??0,t.translatedLanguage=n.translatedLanguage??"",t.translatedUrl=n.translatedUrl??"",t.unknown1=n.unknown1??0,t.unknown2=n.unknown2??0,t}};function ji(){return {url:"",language:""}}const Hr={encode(n,t=new U){return n.url!==""&&t.uint32(10).string(n.url),n.language!==""&&t.uint32(18).string(n.language),t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=ji();for(;e.pos>>3){case 1:{if(r!==10)break;o.url=e.string();continue}case 2:{if(r!==18)break;o.language=e.string();continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {url:T(n.url)?globalThis.String(n.url):"",language:T(n.language)?globalThis.String(n.language):""}},toJSON(n){const t={};return n.url!==""&&(t.url=n.url),n.language!==""&&(t.language=n.language),t},create(n){return Hr.fromPartial(n??{})},fromPartial(n){const t=ji();return t.url=n.url??"",t.language=n.language??"",t}};function Zi(){return {waiting:false,subtitles:[]}}const Ur={encode(n,t=new U){n.waiting!==false&&t.uint32(8).bool(n.waiting);for(const e of n.subtitles)Kt.encode(e,t.uint32(18).fork()).join();return t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=Zi();for(;e.pos>>3){case 1:{if(r!==8)break;o.waiting=e.bool();continue}case 2:{if(r!==18)break;o.subtitles.push(Kt.decode(e,e.uint32()));continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {waiting:T(n.waiting)?globalThis.Boolean(n.waiting):false,subtitles:globalThis.Array.isArray(n?.subtitles)?n.subtitles.map(t=>Kt.fromJSON(t)):[]}},toJSON(n){const t={};return n.waiting!==false&&(t.waiting=n.waiting),n.subtitles?.length&&(t.subtitles=n.subtitles.map(e=>Kt.toJSON(e))),t},create(n){return Ur.fromPartial(n??{})},fromPartial(n){const t=Zi();return t.waiting=n.waiting??false,t.subtitles=n.subtitles?.map(e=>Kt.fromPartial(e))||[],t}};function Qi(){return {url:"",timestamp:""}}const Yt={encode(n,t=new U){return n.url!==""&&t.uint32(10).string(n.url),n.timestamp!==""&&t.uint32(18).string(n.timestamp),t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=Qi();for(;e.pos>>3){case 1:{if(r!==10)break;o.url=e.string();continue}case 2:{if(r!==18)break;o.timestamp=e.string();continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {url:T(n.url)?globalThis.String(n.url):"",timestamp:T(n.timestamp)?globalThis.String(n.timestamp):""}},toJSON(n){const t={};return n.url!==""&&(t.url=n.url),n.timestamp!==""&&(t.timestamp=n.timestamp),t},create(n){return Yt.fromPartial(n??{})},fromPartial(n){const t=Qi();return t.url=n.url??"",t.timestamp=n.timestamp??"",t}};function to(){return {url:"",language:"",responseLanguage:"",unknown0:0,unknown1:0}}const zr={encode(n,t=new U){return n.url!==""&&t.uint32(10).string(n.url),n.language!==""&&t.uint32(18).string(n.language),n.responseLanguage!==""&&t.uint32(26).string(n.responseLanguage),n.unknown0!==0&&t.uint32(40).int32(n.unknown0),n.unknown1!==0&&t.uint32(48).int32(n.unknown1),t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=to();for(;e.pos>>3){case 1:{if(r!==10)break;o.url=e.string();continue}case 2:{if(r!==18)break;o.language=e.string();continue}case 3:{if(r!==26)break;o.responseLanguage=e.string();continue}case 5:{if(r!==40)break;o.unknown0=e.int32();continue}case 6:{if(r!==48)break;o.unknown1=e.int32();continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {url:T(n.url)?globalThis.String(n.url):"",language:T(n.language)?globalThis.String(n.language):"",responseLanguage:T(n.responseLanguage)?globalThis.String(n.responseLanguage):"",unknown0:T(n.unknown0)?globalThis.Number(n.unknown0):0,unknown1:T(n.unknown1)?globalThis.Number(n.unknown1):0}},toJSON(n){const t={};return n.url!==""&&(t.url=n.url),n.language!==""&&(t.language=n.language),n.responseLanguage!==""&&(t.responseLanguage=n.responseLanguage),n.unknown0!==0&&(t.unknown0=Math.round(n.unknown0)),n.unknown1!==0&&(t.unknown1=Math.round(n.unknown1)),t},create(n){return zr.fromPartial(n??{})},fromPartial(n){const t=to();return t.url=n.url??"",t.language=n.language??"",t.responseLanguage=n.responseLanguage??"",t.unknown0=n.unknown0??0,t.unknown1=n.unknown1??0,t}};function eo(){return {interval:0,translatedInfo:void 0,pingId:void 0}}const Wr={encode(n,t=new U){return n.interval!==0&&t.uint32(8).int32(n.interval),n.translatedInfo!==void 0&&Yt.encode(n.translatedInfo,t.uint32(18).fork()).join(),n.pingId!==void 0&&t.uint32(24).int32(n.pingId),t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=eo();for(;e.pos>>3){case 1:{if(r!==8)break;o.interval=e.int32();continue}case 2:{if(r!==18)break;o.translatedInfo=Yt.decode(e,e.uint32());continue}case 3:{if(r!==24)break;o.pingId=e.int32();continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {interval:T(n.interval)?Da(n.interval):0,translatedInfo:T(n.translatedInfo)?Yt.fromJSON(n.translatedInfo):void 0,pingId:T(n.pingId)?globalThis.Number(n.pingId):void 0}},toJSON(n){const t={};return n.interval!==0&&(t.interval=Ra(n.interval)),n.translatedInfo!==void 0&&(t.translatedInfo=Yt.toJSON(n.translatedInfo)),n.pingId!==void 0&&(t.pingId=Math.round(n.pingId)),t},create(n){return Wr.fromPartial(n??{})},fromPartial(n){const t=eo();return t.interval=n.interval??0,t.translatedInfo=n.translatedInfo!==void 0&&n.translatedInfo!==null?Yt.fromPartial(n.translatedInfo):void 0,t.pingId=n.pingId??void 0,t}};function no(){return {pingId:0}}const qr={encode(n,t=new U){return n.pingId!==0&&t.uint32(8).int32(n.pingId),t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=no();for(;e.pos>>3){case 1:{if(r!==8)break;o.pingId=e.int32();continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {pingId:T(n.pingId)?globalThis.Number(n.pingId):0}},toJSON(n){const t={};return n.pingId!==0&&(t.pingId=Math.round(n.pingId)),t},create(n){return qr.fromPartial(n??{})},fromPartial(n){const t=no();return t.pingId=n.pingId??0,t}};function io(){return {uuid:"",module:""}}const Gr={encode(n,t=new U){return n.uuid!==""&&t.uint32(10).string(n.uuid),n.module!==""&&t.uint32(18).string(n.module),t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=io();for(;e.pos>>3){case 1:{if(r!==10)break;o.uuid=e.string();continue}case 2:{if(r!==18)break;o.module=e.string();continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {uuid:T(n.uuid)?globalThis.String(n.uuid):"",module:T(n.module)?globalThis.String(n.module):""}},toJSON(n){const t={};return n.uuid!==""&&(t.uuid=n.uuid),n.module!==""&&(t.module=n.module),t},create(n){return Gr.fromPartial(n??{})},fromPartial(n){const t=io();return t.uuid=n.uuid??"",t.module=n.module??"",t}};function oo(){return {secretKey:"",expires:0}}const Kr={encode(n,t=new U){return n.secretKey!==""&&t.uint32(10).string(n.secretKey),n.expires!==0&&t.uint32(16).int32(n.expires),t},decode(n,t){const e=n instanceof M?n:new M(n);let i=t===void 0?e.len:e.pos+t;const o=oo();for(;e.pos>>3){case 1:{if(r!==10)break;o.secretKey=e.string();continue}case 2:{if(r!==16)break;o.expires=e.int32();continue}}if((r&7)===4||r===0)break;e.skip(r&7);}return o},fromJSON(n){return {secretKey:T(n.secretKey)?globalThis.String(n.secretKey):"",expires:T(n.expires)?globalThis.Number(n.expires):0}},toJSON(n){const t={};return n.secretKey!==""&&(t.secretKey=n.secretKey),n.expires!==0&&(t.expires=Math.round(n.expires)),t},create(n){return Kr.fromPartial(n??{})},fromPartial(n){const t=oo();return t.secretKey=n.secretKey??"",t.expires=n.expires??0,t}};function Yr(n){if(globalThis.Buffer)return Uint8Array.from(globalThis.Buffer.from(n,"base64"));{const t=globalThis.atob(n),e=new Uint8Array(t.length);for(let i=0;i{t.push(globalThis.String.fromCharCode(e));}),globalThis.btoa(t.join(""))}}function T(n){return n!=null}const Ba=(function(){const t=typeof document<"u"&&document.createElement("link").relList;return t&&t.supports&&t.supports("modulepreload")?"modulepreload":"preload"})(),Fa=function(n){return "/"+n},ro={},Na=function(t,e,i){let o=Promise.resolve();if(e&&e.length>0){let l=function(c){return Promise.all(c.map(u=>Promise.resolve(u).then(d=>({status:"fulfilled",value:d}),d=>({status:"rejected",reason:d}))))};document.getElementsByTagName("link");const s=document.querySelector("meta[property=csp-nonce]"),a=s?.nonce||s?.getAttribute("nonce");o=l(e.map(c=>{if(c=Fa(c),c in ro)return;ro[c]=true;const u=c.endsWith(".css"),d=u?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${d}`))return;const h=document.createElement("link");if(h.rel=u?"stylesheet":Ba,u||(h.as="script"),h.crossOrigin="",h.href=c,a&&h.setAttribute("nonce",a),document.head.appendChild(h),u)return new Promise((f,g)=>{h.addEventListener("load",f),h.addEventListener("error",()=>g(new Error(`Unable to preload CSS for ${c}`)));})}));}function r(s){const a=new Event("vite:preloadError",{cancelable:true});if(a.payload=s,window.dispatchEvent(a),!a.defaultPrevented)throw s}return o.then(s=>{for(const a of s||[])a.status==="rejected"&&r(a.reason);return t().catch(r)})};var jt;(function(n){n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENCE=4]="SILENCE";})(jt||(jt={}));const hn=`[vot.js v${Y.version}]`;function Ve(n){return Y.loggerLevel<=n}function $a(...n){Ve(jt.DEBUG)&&console.log(hn,...n);}function Ha(...n){Ve(jt.INFO)&&console.info(hn,...n);}function Ua(...n){Ve(jt.WARN)&&console.warn(hn,...n);}function za(...n){Ve(jt.ERROR)&&console.error(hn,...n);}const E={canLog:Ve,log:$a,info:Ha,warn:Ua,error:za},{componentVersion:Wn}=Y;async function Wa(){return typeof window<"u"&&window.crypto?window.crypto:await Na(()=>module.import('./__vite-browser-external-BIHI7g3E-DUmwvnCo.js'),void 0)}const ri=new TextEncoder;async function Jr(n,t,e){const i=await Wa(),o=await i.subtle.importKey("raw",ri.encode(t),{name:"HMAC",hash:{name:n}},false,["sign","verify"]);return await i.subtle.sign("HMAC",o,e)}async function qn(n){const t=await Jr("SHA-256",Y.hmac,n);return new Uint8Array(t).reduce((e,i)=>e+i.toString(16).padStart(2,"0"),"")}async function Ht(n,t,e,i){const{secretKey:o,uuid:r}=t,s=`${r}:${i}:${Wn}`,a=ri.encode(s),l=await qn(a);if(n==="Ya-Summary")return {[`X-${n}-Sk`]:o,[`X-${n}-Token`]:`${l}:${s}`};if(!e)throw new TypeError(`Body is required for sec type ${n}`);const c=await qn(e);return {[`${n}-Signature`]:c,[`Sec-${n}-Sk`]:o,[`Sec-${n}-Token`]:`${l}:${s}`}}function qa(){const n="0123456789ABCDEF";let t="";for(let e=0;e<32;e++){const i=Math.floor(Math.random()*16);t+=n[i];}return t}async function Ga(n,t){try{const e=ri.encode(t),i=await Jr("SHA-1",n,e);return btoa(String.fromCharCode(...new Uint8Array(i)))}catch(e){return E.error(e),false}}const jr={"sec-ch-ua":`"Chromium";v="142", "YaBrowser";v="${Wn.slice(0,5)}", "Not?A_Brand";v="24", "Yowser";v="2.5"`,"sec-ch-ua-full-version-list":`"Chromium";v="142.0.7444.59", "YaBrowser";v="${Wn}", "Not?A_Brand";v="24.0.0.0", "Yowser";v="2.5"`,"Sec-Fetch-Mode":"no-cors"},Ka={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 Zr(n,t={headers:{"User-Agent":Y.userAgent}}){const{timeout:e=3e3,signal:i,...o}=t;if(!i&&(!e||e<=0))return await fetch(n,o);const r=new AbortController,s=l=>{r.signal.aborted||r.abort(l);};i&&(i.aborted?s(i.reason):i.addEventListener("abort",()=>s(i.reason),{once:true}));let a;e&&e>0&&(a=setTimeout(()=>s(new Error("Fetch timeout")),e));try{return await fetch(n,{...o,signal:r.signal})}finally{a&&clearTimeout(a);}}function Ya(){return Math.floor(Date.now()/1e3)}function J(n){return n.length===3?Ka[n]:n.toLowerCase().split(/[_;-]/)[0].trim()}function ut(n,t="mp4"){const e=`https://${Y.mediaProxy}/v1/proxy/video.${t}?format=base64&force=true`;return n instanceof URL?`${e}&url=${btoa(n.href)}&origin=${n.origin}&referer=${n.origin}`:`${e}&url=${btoa(n)}`}function Tn(n,t){const e=n.replace(/^\/+/,""),i=new URL("https://vk.com/video");i.searchParams.set("z",e);for(const o of ["list","access_key"]){const r=t.searchParams.get(o);r&&i.searchParams.set(o,r);}return i.toString()}function Xa(n,t,e,i,o,{forceSourceLang:r=false,wasStream:s=false,videoTitle:a="",bypassCache:l=false,useLivelyVoice:c=false,firstRequest:u=true}={}){return Rr.encode({url:n,firstRequest:u,duration:t,unknown0:1,language:e,forceSourceLang:r,unknown1:0,translationHelp:o??[],responseLanguage:i,wasStream:s,unknown2:1,unknown3:2,bypassCache:l,useLivelyVoice:c,videoTitle:a}).finish()}function Ja(n){return Br.decode(new Uint8Array(n))}function ja(n,t,e,i){return Fr.encode({url:n,duration:t,language:e,responseLanguage:i}).finish()}function Za(n){return Nr.decode(new Uint8Array(n))}function Qr(n){return "chunkId"in n}function Qa(n,t,e,i){return i&&Qr(e)?zn.encode({url:n,translationId:t,partialAudioInfo:{...i,audioBuffer:e}}).finish():zn.encode({url:n,translationId:t,audioInfo:e}).finish()}function tl(n){return $r.decode(new Uint8Array(n))}function el(n,t){return Hr.encode({url:n,language:t}).finish()}function nl(n){return Ur.decode(new Uint8Array(n))}function il(n){return qr.encode({pingId:n}).finish()}function ol(n,t,e){return zr.encode({url:n,language:t,responseLanguage:e,unknown0:1,unknown1:0}).finish()}function rl(n){return Wr.decode(new Uint8Array(n))}const ot={encodeTranslationRequest:Xa,decodeTranslationResponse:Ja,encodeTranslationCacheRequest:ja,decodeTranslationCacheResponse:Za,isPartialAudioBuffer:Qr,encodeTranslationAudioRequest:Qa,decodeTranslationAudioResponse:tl,encodeSubtitlesRequest:el,decodeSubtitlesResponse:nl,encodeStreamPingRequest:il,encodeStreamRequest:ol,decodeStreamResponse:rl};function sl(n,t){return Gr.encode({uuid:n,module:t}).finish()}function al(n){return Kr.decode(new Uint8Array(n))}const so={encodeSessionRequest:sl,decodeSessionResponse:al};var dt;(function(n){n[n.FAILED=0]="FAILED",n[n.FINISHED=1]="FINISHED",n[n.WAITING=2]="WAITING",n[n.LONG_WAITING=3]="LONG_WAITING",n[n.PART_CONTENT=5]="PART_CONTENT",n[n.AUDIO_REQUESTED=6]="AUDIO_REQUESTED",n[n.SESSION_REQUIRED=7]="SESSION_REQUIRED";})(dt||(dt={}));var ye;(function(n){n.WEB_API_VIDEO_SRC_FROM_IFRAME="web_api_video_src_from_iframe",n.WEB_API_VIDEO_SRC="web_api_video_src",n.WEB_API_GET_ALL_GENERATING_URLS_DATA_FROM_IFRAME="web_api_get_all_generating_urls_data_from_iframe",n.WEB_API_GET_ALL_GENERATING_URLS_DATA_FROM_IFRAME_TMP_EXP="web_api_get_all_generating_urls_data_from_iframe_tmp_exp",n.WEB_API_REPLACED_FETCH_INSIDE_IFRAME="web_api_replaced_fetch_inside_iframe",n.ANDROID_API="android_api",n.WEB_API_SLOW="web_api_slow",n.WEB_API_STEAL_SIG_AND_N="web_api_steal_sig_and_n",n.WEB_API_COMBINED="web_api_get_all_generating_urls_data_from_iframe,web_api_steal_sig_and_n";})(ye||(ye={}));var b;(function(n){n.custom="custom",n.directlink="custom",n.youtube="youtube",n.piped="piped",n.invidious="invidious",n.niconico="niconico",n.vk="vk",n.nine_gag="nine_gag",n.gag="nine_gag",n.twitch="twitch",n.proxitok="proxitok",n.tiktok="tiktok",n.vimeo="vimeo",n.xvideos="xvideos",n.xhamster="xhamster",n.spankbang="spankbang",n.rule34video="rule34video",n.picarto="picarto",n.olympicsreplay="olympics_replay",n.pornhub="pornhub",n.twitter="twitter",n.x="twitter",n.rumble="rumble",n.facebook="facebook",n.rutube="rutube",n.coub="coub",n.bilibili="bilibili",n.mail_ru="mailru",n.mailru="mailru",n.bitchute="bitchute",n.eporner="eporner",n.peertube="peertube",n.dailymotion="dailymotion",n.trovo="trovo",n.yandexdisk="yandexdisk",n.ok_ru="okru",n.okru="okru",n.googledrive="googledrive",n.bannedvideo="bannedvideo",n.weverse="weverse",n.weibo="weibo",n.newgrounds="newgrounds",n.egghead="egghead",n.youku="youku",n.archive="archive",n.kodik="kodik",n.patreon="patreon",n.reddit="reddit",n.kick="kick",n.apple_developer="apple_developer",n.appledeveloper="apple_developer",n.epicgames="epicgames",n.odysee="odysee",n.coursehunterLike="coursehunterLike",n.sap="sap",n.watchpornto="watchpornto",n.linkedin="linkedin",n.incestflix="incestflix",n.porntn="porntn",n.dzen="dzen",n.bunnystream="bunnystream",n.cloudflarestream="cloudflarestream",n.loom="loom",n.rtnews="rtnews",n.bitview="bitview",n.thisvid="thisvid",n.ign="ign",n.zdf="zdf",n.bunkr="bunkr",n.imdb="imdb",n.telegram="telegram";})(b||(b={}));function ao(n,t,e){return n===b.patreon?{service:"mux",videoId:new URL(e).pathname.slice(1)}:{service:n,videoId:t}}class H extends Error{data;constructor(t,e=void 0){super(t),this.data=e,this.name="VOTJSError";}}class ll{host;schema;fetch;fetchOpts;sessions={};userAgent=Y.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:t=Y.host,fetchFn:e=Zr,fetchOpts:i={},headers:o={}}={}){const r=this.hostSchemaRe.exec(t)?.[1];this.host=r?t.replace(`${r}://`,""):t,this.schema=r??"https",this.fetch=e,this.fetchOpts=i,this.headers={...this.headers,...o};}async request(t,e,i={},o="POST"){const r=this.getOpts(new Blob([e]),i,o);try{const s=await this.fetch(`${this.schema}://${this.host}${t}`,r),a=await s.arrayBuffer();return {success:s.status===200,data:a}}catch(s){return {success:false,data:s?.message}}}async requestJSON(t,e=null,i={},o="POST"){const r=this.getOpts(e,{"Content-Type":"application/json",...i},o);try{const s=await this.fetch(`${this.schema}://${this.host}${t}`,r),a=await s.json();return {success:s.status===200,data:a}}catch(s){return {success:false,data:s?.message}}}getOpts(t,e={},i="POST"){return {method:i,headers:{...this.headers,...e},body:t,...this.fetchOpts}}async getSession(t){const e=Ya(),i=this.sessions[t];if(i&&i.timestamp+i.expires>e)return i;const{secretKey:o,expires:r,uuid:s}=await this.createSession(t);return this.sessions[t]={secretKey:o,expires:r,timestamp:e,uuid:s},this.sessions[t]}async createSession(t){const e=qa(),i=so.encodeSessionRequest(e,t),o=await this.request("/session/create",i,{"Vtrans-Signature":await qn(i)});if(!o.success)throw new H("Failed to request create session",o);return {...so.decodeSessionResponse(o.data),uuid:e}}}let ts=class extends ll{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(t){return !!(/\.(m3u8|m4(a|v)|mpd)/.exec(t)??/^https:\/\/cdn\.qstv\.on\.epicgames\.com/.exec(t))}headersVOT={"User-Agent":`vot.js/${Y.version}`,"Content-Type":"application/json",Pragma:"no-cache","Cache-Control":"no-cache"};constructor({host:t,hostVOT:e=Y.hostVOT,fetchFn:i,fetchOpts:o,requestLang:r="en",responseLang:s="ru",apiToken:a,headers:l}={}){super({host:t,fetchFn:i,fetchOpts:o,headers:l});const c=this.hostSchemaRe.exec(e)?.[1];this.hostVOT=c?e.replace(`${c}://`,""):e,this.schemaVOT=c??"https",this.requestLang=r,this.responseLang=s,this.apiToken=a;}get apiTokenHeader(){return this.apiToken?{Authorization:`OAuth ${this.apiToken}`}:{}}async requestVOT(t,e,i={}){const o=this.getOpts(JSON.stringify(e),{...this.headersVOT,...i});try{const r=await this.fetch(`${this.schemaVOT}://${this.hostVOT}${t}`,o),s=await r.json();return {success:r.status===200,data:s}}catch(r){return {success:false,data:r?.message}}}async translateVideoYAImpl({videoData:t,requestLang:e=this.requestLang,responseLang:i=this.responseLang,translationHelp:o=null,headers:r={},extraOpts:s={},shouldSendFailedAudio:a=true}){const{url:l,duration:c=Y.defaultDuration}=t,u=await this.getSession("video-translation"),d=ot.encodeTranslationRequest(l,c,e,i,o,s),h=this.paths.videoTranslation,f=await Ht("Vtrans",u,d,h),g=s.useLivelyVoice?this.apiTokenHeader:{},y=await this.request(h,d,{...f,...g,...r});if(!y.success)throw new H("Failed to request video translation",y);const w=ot.decodeTranslationResponse(y.data);E.log("translateVideo",w);const{status:x,translationId:m}=w;switch(x){case dt.FAILED:throw new H("Yandex couldn't translate video",w);case dt.FINISHED:case dt.PART_CONTENT:if(!w.url)throw new H("Audio link wasn't received from Yandex response",w);return {translationId:m,translated:true,url:w.url,status:x,remainingTime:w.remainingTime??-1};case dt.WAITING:case dt.LONG_WAITING:return {translationId:m,translated:false,status:x,remainingTime:w.remainingTime??-1};case dt.AUDIO_REQUESTED:return l.startsWith("https://youtu.be/")&&a?(await this.requestVtransFailAudio(l),await this.requestVtransAudio(l,w.translationId,{audioFile:new Uint8Array,fileId:ye.WEB_API_GET_ALL_GENERATING_URLS_DATA_FROM_IFRAME}),await this.translateVideoYAImpl({videoData:t,requestLang:e,responseLang:i,translationHelp:o,headers:r,shouldSendFailedAudio:false})):{translationId:m,translated:false,status:x,remainingTime:w.remainingTime??-1};case dt.SESSION_REQUIRED:throw new H("Yandex auth required to translate video. See docs for more info",w);default:throw E.error("Unknown response",w),new H("Unknown response from Yandex",w)}}async translateVideoVOTImpl({url:t,videoId:e,service:i,requestLang:o=this.requestLang,responseLang:r=this.responseLang,headers:s={},provider:a="yandex"}){const l=ao(i,e,t),c=await this.requestVOT(this.paths.videoTranslation,{provider:a,service:l.service,video_id:l.videoId,from_lang:o,to_lang:r,raw_video:t},{...s});if(!c.success)throw new H("Failed to request video translation",c);const u=c.data;switch(u.status){case "failed":throw new H("Yandex couldn't translate video",u);case "success":if(!u.translated_url)throw new H("Audio link wasn't received from VOT response",u);return {translationId:String(u.id),translated:true,url:u.translated_url,status:1,remainingTime:-1};case "waiting":return {translationId:"",translated:false,remainingTime:u.remaining_time,status:2,message:u.message}}}async requestVtransFailAudio(t){const e=await this.requestJSON(this.paths.videoTranslationFailAudio,JSON.stringify({video_url:t}),void 0,"PUT");if(!e.data||typeof e.data=="string"||e.data.status!==1)throw new H("Failed to request to fake video translation fail audio js",e);return e}async requestVtransAudio(t,e,i,o,r={}){const s=await this.getSession("video-translation");let a;if(ot.isPartialAudioBuffer(i)){if(!o)throw new H("Partial audio metadata is required for partial audio buffer",i);a=ot.encodeTranslationAudioRequest(t,e,i,o);}else a=ot.encodeTranslationAudioRequest(t,e,i,void 0);const l=this.paths.videoTranslationAudio,c=await Ht("Vtrans",s,a,l),u=await this.request(l,a,{...c,...r},"PUT");if(!u.success)throw new H("Failed to request video translation audio",u);return ot.decodeTranslationAudioResponse(u.data)}async translateVideoCache({videoData:t,requestLang:e=this.requestLang,responseLang:i=this.responseLang,headers:o={}}){const{url:r,duration:s=Y.defaultDuration}=t,a=await this.getSession("video-translation"),l=ot.encodeTranslationCacheRequest(r,s,e,i),c=this.paths.videoTranslationCache,u=await Ht("Vtrans",a,l,c),d=await this.request(c,l,{...u,...o},"POST");if(!d.success)throw new H("Failed to request video translation cache",d);return ot.decodeTranslationCacheResponse(d.data)}async translateVideo({videoData:t,requestLang:e=this.requestLang,responseLang:i=this.responseLang,translationHelp:o=null,headers:r={},extraOpts:s={},shouldSendFailedAudio:a=true}){const{url:l,videoId:c,host:u}=t;return this.isCustomLink(l)?await this.translateVideoVOTImpl({url:l,videoId:c,service:u,requestLang:e,responseLang:i,headers:r,provider:s.useLivelyVoice?"yandex_lively":"yandex"}):await this.translateVideoYAImpl({videoData:t,requestLang:e,responseLang:i,translationHelp:o,headers:r,extraOpts:s,shouldSendFailedAudio:a})}async getSubtitlesYAImpl({videoData:t,requestLang:e=this.requestLang,headers:i={}}){const{url:o}=t,r=await this.getSession("video-translation"),s=ot.encodeSubtitlesRequest(o,e),a=this.paths.videoSubtitles,l=await Ht("Vsubs",r,s,a),c=await this.request(a,s,{...l,...i});if(!c.success)throw new H("Failed to request video subtitles",c);const u=ot.decodeSubtitlesResponse(c.data),d=u.subtitles.map(h=>{const{language:f,url:g,translatedLanguage:y,translatedUrl:w}=h;return {language:f,url:g,translatedLanguage:y,translatedUrl:w}});return {waiting:u.waiting,subtitles:d}}async getSubtitlesVOTImpl({url:t,videoId:e,service:i,headers:o={}}){const r=ao(i,e,t),s=await this.requestVOT(this.paths.videoSubtitles,{provider:"yandex",service:r.service,video_id:r.videoId},o);if(!s.success)throw new H("Failed to request video subtitles",s);const a=s.data;return {waiting:false,subtitles:a.reduce((c,u)=>{if(!u.lang_from)return c;const d=a.find(h=>h.lang===u.lang_from);return d&&c.push({language:d.lang,url:d.subtitle_url,translatedLanguage:u.lang,translatedUrl:u.subtitle_url}),c},[])}}async getSubtitles({videoData:t,requestLang:e=this.requestLang,headers:i={}}){const{url:o,videoId:r,host:s}=t;return this.isCustomLink(o)?await this.getSubtitlesVOTImpl({url:o,videoId:r,service:s,headers:i}):await this.getSubtitlesYAImpl({videoData:t,requestLang:e,headers:i})}async pingStream({pingId:t,headers:e={}}){const i=await this.getSession("video-translation"),o=ot.encodeStreamPingRequest(t),r=this.paths.streamPing,s=await Ht("Vtrans",i,o,r),a=await this.request(r,o,{...s,...e});if(!a.success)throw new H("Failed to request stream ping",a);return true}async translateStream({videoData:t,requestLang:e=this.requestLang,responseLang:i=this.responseLang,headers:o={}}){const{url:r}=t;if(this.isCustomLink(r))throw new H("Unsupported video URL for getting stream translation");const s=await this.getSession("video-translation"),a=ot.encodeStreamRequest(r,e,i),l=this.paths.streamTranslation,c=await Ht("Vtrans",s,a,l),u=await this.request(l,a,{...c,...o});if(!u.success)throw new H("Failed to request stream translation",u);const d=ot.decodeStreamResponse(u.data),h=d.interval;switch(h){case et.NO_CONNECTION:case et.TRANSLATING:return {translated:false,interval:h,message:h===et.NO_CONNECTION?"streamNoConnectionToServer":"translationTakeFewMinutes"};case et.STREAMING:{if(d.pingId===void 0)throw new H("Stream ping id wasn't received from Yandex response",d);return {translated:true,interval:h,pingId:d.pingId,result:d.translatedInfo}}default:throw E.error("Unknown response",d),new H("Unknown response from Yandex",d)}}},cl=class extends ts{constructor(t={}){t.host=t.host??Y.hostWorker,super(t);}async request(t,e,i={},o="POST"){const r=this.getOpts(JSON.stringify({headers:{...this.headers,...i},body:Array.from(e)}),{"Content-Type":"application/json"},o);try{const s=await this.fetch(`${this.schema}://${this.host}${t}`,r),a=await s.arrayBuffer();return {success:s.status===200,data:a}}catch(s){return {success:false,data:s?.message}}}async requestJSON(t,e=null,i={},o="POST"){const r=this.getOpts(JSON.stringify({headers:{...this.headers,"Content-Type":"application/json",Accept:"application/json",...i},body:e}),{Accept:"application/json","Content-Type":"application/json"},o);try{const s=await this.fetch(`${this.schema}://${this.host}${t}`,r),a=await s.json();return {success:s.status===200,data:a}}catch(s){return {success:false,data:s?.message}}}};class ul extends ts{constructor(t){super(t),this.headers={...jr,...this.headers};}}class dl extends cl{constructor(t){super(t),this.headers={...jr,...this.headers};}}class ge extends Error{constructor(t){super(t),this.name="VideoDataError";}}const hl=/(file:\/\/(\/)?|(http(s)?:\/\/)(127\.0\.0\.1|localhost|192\.168\.(\d){1,3}\.(\d){1,3}))/,fl=["yewtu.be","inv.nadeko.net","invidious.nerdvpn.de","invidious.protokolla.fi","invidious.materialio.us","iv.melmac.space"],pl=["piped.video","piped.kavin.rocks","piped.private.coffee"],gl=["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"],ml=["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"],vl=["coursehunter.net","coursetrain.net"];var z;(function(n){n.udemy="udemy",n.coursera="coursera",n.douyin="douyin",n.artstation="artstation",n.kickstarter="kickstarter",n.oraclelearn="oraclelearn",n.deeplearningai="deeplearningai",n.netacad="netacad";})(z||(z={}));({...b,...z});const R={bilibiliPlayer:".bpx-player-video-wrap, div.player-mobile-box.player-mobile-autoplay",flowplayer:".fp-player",idPlayer:"#player",jwPlayer:".jwplayer, .jw-media",player:".player",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"},bl=[{additionalData:"mobile",host:b.youtube,url:"https://youtu.be/",match:/^m.youtube.com$/,selector:".player-container",needExtraData:true},{host:b.youtube,url:"https://youtu.be/",match:n=>/^(www.)?youtube(-nocookie|kids)?.com$/.test(n.hostname)&&n.pathname.startsWith("/tv"),selector:"#container",needExtraData:true},{host:b.youtube,url:"https://youtu.be/",match:/^(www.)?youtube(-nocookie|kids)?.com$/,selector:".html5-video-container:not(#inline-player *)",needExtraData:true},{host:b.invidious,url:"https://youtu.be/",match:fl,selector:R.idPlayer,needBypassCSP:true},{host:b.piped,url:"https://youtu.be/",match:pl,selector:".shaka-video-container",needBypassCSP:true},{host:b.zdf,url:"https://www.zdf.de/play/",match:[/^zdf.de$/,/^(www.)?zdf.de$/],selector:"div.zdfplayer-app.zdfplayer-desktop, div.zdfplayer-app"},{host:b.niconico,url:"https://www.nicovideo.jp/watch/",match:[/^(www\.|sp\.)?nicovideo\.jp$/,/^nico\.ms$/],selector:'[class*="grid-area_[player]"] > div'},{additionalData:"mobile",host:b.vk,url:"https://vk.com/video?z=",match:[/^m.vk.(com|ru)$/,/^m.vkvideo.ru$/],selector:R.vkVideoPlayer,shadowRoot:true,needExtraData:true},{additionalData:"clips",host:b.vk,url:"https://vk.com/video?z=",match:/^(www.|m.)?vk.(com|ru)$/,selector:'div[data-testid="clipcontainer-video"]',needExtraData:true},{host:b.vk,url:"https://vk.com/video?z=",match:[/^(www\.|m\.)?vk\.(com|ru)$/,/^(.*\.)?vkvideo\.ru$/],selector:R.vkVideoPlayer,needExtraData:true},{host:b.nine_gag,url:"https://9gag.com/gag/",match:/^9gag.com$/,selector:".video-post",needExtraData:true},{host:b.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:b.proxitok,url:"https://www.tiktok.com/",match:gl,selector:".column.has-text-centered"},{host:b.tiktok,url:"https://www.tiktok.com/",match:/^(www.)?tiktok.com$/,selector:null},{host:z.douyin,url:"https://www.douyin.com/",match:/^(www.)?douyin.com/,selector:".xg-video-container",needExtraData:true,needBypassCSP:true},{host:b.vimeo,url:"https://vimeo.com/",match:/^(www\.|m\.)?vimeo.com$/,needExtraData:true,selector:R.player},{host:b.vimeo,url:"https://player.vimeo.com/",match:/^player.vimeo.com$/,additionalData:"embed",needExtraData:true,needBypassCSP:true,selector:R.player},{host:b.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:b.xhamster,url:"https://xhamster.com/",match:n=>/^(?:[^.]+\.)?(?:xhamster\.(?:com|desi)|xhamster\d+\.(?:com|desi)|xhvid\.com)$/.test(n.host)&&/\/(?:videos\/[^/]+-[\dA-Za-z]+)\/?$/.test(n.pathname),selector:"#player-container"},{host:b.spankbang,url:"https://spankbang.com/",match:n=>/^(?:[^.]+\.)?spankbang\.com$/.test(n.host)&&/\/(?:[\da-z]+\/(?:video|play|embed)(?:\/[^/]+)?|[\da-z]+-[\da-z]+\/playlist\/[^/?#&]+)\/?$/i.test(n.pathname),selector:"#main_video_player"},{host:b.rule34video,url:"https://rule34video.com/video/",match:n=>/^(www\.)?rule34video\.com$/.test(n.host)&&/\/videos?\/\d+/.test(n.pathname),selector:R.flowplayer},{host:b.picarto,url:"https://picarto.tv/",match:n=>/^(www\.)?picarto\.tv$/.test(n.host)&&/^(?:\/[^/]+\/(?:profile\/)?videos\/[^/?#&]+|\/videopopout\/[^/?#&]+|\/[^/#?]+\/?)$/.test(n.pathname),selector:'[class*="VideosTab__PlayerWrapper"]'},{host:b.olympicsreplay,url:"https://olympics.com/",match:n=>/^(www\.)?olympics\.com$/.test(n.host)&&/^\/[a-z]{2}\/(?:[a-z0-9-]+\/)?(?:replay|videos?|original-series\/episode)\/[\w-]+\/?$/i.test(n.pathname),selector:R.videoJsUniversal},{host:b.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:b.pornhub,url:"https://rt.pornhub.com/view_video.php?viewkey=",match:n=>/^[a-z]+.pornhub.(com|org)$/.exec(n.host)&&n.pathname.startsWith("/embed/"),selector:R.idPlayer},{host:b.twitter,url:"https://twitter.com/i/status/",match:/^(twitter|x).com$/,selector:'div[data-testid="videoComponent"] > div:nth-child(1) > div',eventSelector:'div[data-testid="videoPlayer"]',needBypassCSP:true},{host:b.rumble,url:"https://rumble.com/",match:/^rumble.com$/,selector:"#videoPlayer > .videoPlayer-Rumble-cls > div"},{host:b.facebook,url:"https://facebook.com/",match:n=>n.host.includes("facebook.com")&&n.pathname.includes("/videos/"),selector:'div[role="main"] div[data-pagelet$="video" i]',needBypassCSP:true},{additionalData:"reels",host:b.facebook,url:"https://facebook.com/",match:n=>n.host.includes("facebook.com")&&n.pathname.includes("/reel/"),selector:'div[role="main"]',needBypassCSP:true},{host:b.rutube,url:"https://rutube.ru/video/",match:/^rutube.ru$/,selector:".video-player > div > div > div:nth-child(2)"},{additionalData:"embed",host:b.rutube,url:"https://rutube.ru/video/",match:/^rutube.ru$/,selector:"#app > div > div"},{host:b.bilibili,url:"https://www.bilibili.com/",match:/^(www|m|player).bilibili.com$/,selector:R.bilibiliPlayer},{host:b.bilibili,url:"https://www.bilibili.tv/",match:/^(?:www\.|m\.)?bilibili\.tv$/,selector:R.bilibiliPlayer},{additionalData:"old",host:b.bilibili,url:"https://www.bilibili.com/",match:/^(www|m).bilibili.com$/,selector:null},{host:b.mailru,url:"https://my.mail.ru/",match:/^my.mail.ru$/,selector:"#b-video-wrapper"},{host:b.bitchute,url:"https://www.bitchute.com/video/",match:/^(www.)?bitchute.com$/,selector:R.videoJsUniversal},{host:b.eporner,url:"https://www.eporner.com/",match:/^(www.)?eporner.com$/,selector:R.videoJsUniversal},{host:b.peertube,url:"stub",match:ml,selector:R.videoJsUniversal},{host:b.dailymotion,url:"https://www.dailymotion.com/video/",match:/^((www\.|player\.)?dailymotion\.com|geo(\d+)?\.dailymotion\.com|dai\.ly)$/,selector:R.player},{host:b.trovo,url:"https://trovo.live/s/",match:/^trovo.live$/,selector:".player-video"},{host:b.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)",eventSelector:".video-player__player",needBypassCSP:true,needExtraData:true},{host:b.okru,url:"https://ok.ru/video/",match:/^ok.ru$/,selector:R.vkVideoPlayer,shadowRoot:true},{host:b.googledrive,url:"https://drive.google.com/file/d/",match:/^youtube.googleapis.com$/,selector:".html5-video-container"},{host:b.bannedvideo,url:"https://madmaxworld.tv/watch?id=",match:/^(www.)?banned.video|madmaxworld.tv$/,selector:R.videoJsUniversal,needExtraData:true},{host:b.weverse,url:"https://weverse.io/",match:/^weverse.io$/,selector:".webplayer-internal-source-wrapper",needExtraData:true},{host:b.weibo,url:"https://weibo.com/",match:n=>/^(?:www\.)?weibo\.com$/.test(n.host)&&/^\/(?:\d+\/[A-Za-z0-9]+|0\/[A-Za-z0-9]+|tv\/show\/\d+:(?:[\da-f]{32}|\d{16,}))\/?$/.test(n.pathname)||/^video\.weibo\.com$/.test(n.host)&&/^\/show\/?$/.test(n.pathname)&&/^\d+:(?:[\da-f]{32}|\d{16,})$/i.test(n.searchParams.get("fid")??"")||/^(?:www\.)?weibo\.com$/.test(n.host)&&/^\/newlogin\/?$/.test(n.pathname)&&(n.searchParams.has("url")||/^[A-Za-z0-9]+$/.test(n.searchParams.get("layerid")??"")),selector:R.videoJsUniversal},{host:b.newgrounds,url:"https://www.newgrounds.com/",match:/^(www.)?newgrounds.com$/,selector:".ng-video-player"},{host:b.egghead,url:"https://egghead.io/",match:/^egghead.io$/,selector:".cueplayer-react-video-holder"},{host:b.youku,url:"https://v.youku.com/",match:/^v.youku.com$/,selector:"#ykPlayer"},{host:b.archive,url:"https://archive.org/details/",match:/^archive.org$/,selector:R.jwPlayer},{host:b.kodik,url:"stub",match:/^kodik.(info|biz|cc)$/,selector:R.flowplayer,needExtraData:true},{host:b.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:b.reddit,url:"stub",match:/^old.reddit.com$/,selector:".reddit-video-player-root",needExtraData:true,needBypassCSP:true},{host:b.reddit,url:"stub",match:/^(www.|new.)?reddit.com$/,selector:"div[slot=post-media-container]",shadowRoot:true,needExtraData:true,needBypassCSP:true},{host:b.kick,url:"https://kick.com/",match:/^kick.com$/,selector:"#injected-embedded-channel-player-video > div",needExtraData:true},{host:b.appledeveloper,url:"https://developer.apple.com/",match:/^developer.apple.com$/,selector:".developer-video-player",needExtraData:true,needBypassCSP:true},{host:b.epicgames,url:"https://dev.epicgames.com/community/learning/",match:/^dev.epicgames.com$/,selector:R.videoJsUniversal,needExtraData:true},{host:b.odysee,url:"stub",match:/^odysee.com$/,selector:R.videoJsUniversal,needExtraData:true},{host:b.coursehunterLike,url:"stub",match:vl,selector:"#oframeplayer > pjsdiv:has(video)",needExtraData:true},{host:b.sap,url:"https://learning.sap.com/courses/",match:/^learning.sap.com$/,selector:".playkit-container",eventSelector:".playkit-player",needExtraData:true,needBypassCSP:true},{host:z.udemy,url:"https://www.udemy.com/",match:/udemy.com$/,selector:'div[data-purpose="curriculum-item-viewer-content"] > section > div > div > div > div:nth-of-type(2)',needExtraData:true},{host:z.coursera,url:"https://www.coursera.org/",match:/coursera.org$/,selector:R.videoJsUniversal,needExtraData:true},{host:b.watchpornto,url:"https://watchporn.to/",match:/^watchporn.to$/,selector:R.flowplayer},{host:b.linkedin,url:"https://www.linkedin.com/learning/",match:/^(www.)?linkedin.com$/,selector:R.videoJsUniversal,needExtraData:true,needBypassCSP:true},{host:b.incestflix,url:"https://www.incestflix.net/watch/",match:/^(www.)?incestflix.(net|to|com)$/,selector:"#incflix-stream",needExtraData:true},{host:b.porntn,url:"https://porntn.com/videos/",match:/^porntn.com$/,selector:R.flowplayer,needExtraData:true},{host:b.dzen,url:"https://dzen.ru/video/watch/",match:/^dzen.ru$/,selector:'[class*="player__playerWrap"] > div'},{host:b.bunnystream,url:"stub",match:[/^video\.bunnycdn\.com$/,/^iframe\.mediadelivery\.net$/,/^(?:[^.]+\.)*b-cdn\.net$/],selector:null},{host:b.cloudflarestream,url:"stub",match:/^(watch|embed|iframe|customer-[^.]+).cloudflarestream.com$/,selector:null},{host:b.loom,url:"https://www.loom.com/share/",match:/^(www.)?loom.com$/,selector:".VideoLayersContainer",needExtraData:true,needBypassCSP:true},{host:z.artstation,url:"https://www.artstation.com/learning/",match:/^(www.)?artstation.com$/,selector:R.videoJsUniversal,needExtraData:true},{host:b.rtnews,url:"https://www.rt.com/",match:/^(www.)?rt.com$/,selector:R.jwPlayer,needExtraData:true},{host:b.bitview,url:"https://www.bitview.net/watch?v=",match:/^(www.)?bitview.net$/,selector:".vlScreen",needExtraData:true},{host:z.kickstarter,url:"https://www.kickstarter.com/",match:/^(www.)?kickstarter.com/,selector:".ksr-video-player",needExtraData:true},{host:b.thisvid,url:"https://thisvid.com/",match:/^(www.)?thisvid.com$/,selector:R.flowplayer},{additionalData:"regional",host:b.ign,url:"https://de.ign.com/",match:/^(\w{2}.)?ign.com$/,needExtraData:true,selector:".video-container"},{host:b.ign,url:"https://www.ign.com/",match:/^(www.)?ign.com$/,selector:R.player,needExtraData:true},{host:b.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:b.imdb,url:"https://www.imdb.com/video/",match:/^(www\.)?imdb\.com$/,selector:R.jwPlayer},{host:b.telegram,url:"https://t.me/",match:n=>/^web\.telegram\.org$/.test(n.hostname)&&n.pathname.startsWith("/k"),selector:".ckin__player"},{host:z.oraclelearn,url:"https://mylearn.oracle.com/ou/course/",match:/^mylearn\.oracle\.com/,selector:R.videoJsUniversal,needExtraData:true,needBypassCSP:true},{host:z.deeplearningai,url:"https://learn.deeplearning.ai/courses/",match:/^learn(-dev|-staging)?\.deeplearning\.ai/,selector:".lesson-video-player",needExtraData:true},{host:z.netacad,url:"https://www.netacad.com/",match:/^(www\.)?netacad\.com/,selector:R.videoJsUniversal,needExtraData:true},{host:b.custom,url:"stub",match:n=>/([^.]+)\.(mp4|webm)/.test(n.pathname),rawResult:true}];class _ extends Error{constructor(t){super(t),this.name="VideoHelperError";}}class L{API_ORIGIN=window.location.origin;fetch;extraInfo;referer;origin;service;video;language;constructor({fetchFn:t=Zr,extraInfo:e=true,referer:i=document.referrer??`${window.location.origin}/`,origin:o=window.location.origin,service:r,video:s,language:a="en"}={}){this.fetch=t,this.extraInfo=e,this.referer=i,this.origin=/^(http(s)?):\/\//.test(String(o))?o:window.location.origin,this.service=r,this.video=s,this.language=a;}getVideoData(t){return Promise.resolve(void 0)}getVideoId(t){return Promise.resolve(void 0)}returnBaseData(t){if(this.service)return {url:this.service.url+t,videoId:t,host:this.service.host,duration:void 0}}}class yl extends L{API_ORIGIN="https://developer.apple.com";async getVideoData(t){try{const e=document.querySelector("meta[property='og:video']")?.content;if(!e)throw new _("Failed to find content url");return {url:e}}catch(e){E.error(`Failed to get apple developer video data by video ID: ${t}`,e.message);return}}async getVideoId(t){return /videos\/play\/([^/]+)\/([\d]+)/.exec(t.pathname)?.[0]}}class wl extends L{async getVideoId(t){return /(details|embed)\/([^/]+)/.exec(t.pathname)?.[2]}}class Sl extends L{API_ORIGIN="https://www.artstation.com/api/v2/learning";getCSRFToken(){return document.querySelector('meta[name="public-csrf-token"]')?.content}async getCourseInfo(t){try{const e=this.getCSRFToken();return await(await this.fetch(`${this.API_ORIGIN}/courses/${t}/autoplay.json`,{method:"POST",headers:e?{"PUBLIC-CSRF-TOKEN":e}:{}})).json()}catch(e){return E.error(`Failed to get artstation course info by courseId: ${t}.`,e.message),false}}async getVideoUrl(t){try{return (await(await this.fetch(`${this.API_ORIGIN}/quicksilver/video_url.json?chapter_id=${t}`)).json()).url.replace("qsep://","https://")}catch(e){return E.error(`Failed to get artstation video url by chapterId: ${t}.`,e.message),false}}async getVideoData(t){const[,e,,,i]=t.split("/"),o=await this.getCourseInfo(e);if(!o)return;const r=o.chapters.find(d=>d.hash_id===i);if(!r)return;const s=await this.getVideoUrl(r.id);if(!s)return;const{title:a,duration:l,subtitles:c}=r,u=c.filter(d=>d.format==="vtt").map(d=>({language:J(d.locale),source:"artstation",format:"vtt",url:d.file_url}));return {url:s,title:a,duration:l,subtitles:u}}async getVideoId(t){return /courses\/(\w{3,5})\/([^/]+)\/chapters\/(\w{3,5})/.exec(t.pathname)?.[0]}}class xl extends L{API_ORIGIN="https://api.banned.video";async getVideoInfo(t){try{return await(await this.fetch(`${this.API_ORIGIN}/graphql`,{method:"POST",body:JSON.stringify({operationName:"GetVideo",query:`query GetVideo($id: String!) { +var vot=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=Object.create,n=Object.defineProperty,r=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.getPrototypeOf,o=Object.prototype.hasOwnProperty,s=(e,t)=>()=>(e&&(t=e(e=0)),t),c=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),l=(e,t)=>{let r={};for(var i in e)n(r,i,{get:e[i],enumerable:!0});return t||n(r,Symbol.toStringTag,{value:`Module`}),r},u=(e,t,a,s)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var c=i(t),l=0,u=c.length,d;lt[e]).bind(null,d),enumerable:!(s=r(t,d))||s.enumerable});return e},d=(e,r,i)=>(i=e==null?{}:t(a(e)),u(r||!e||!e.__esModule?n(i,`default`,{value:e,enumerable:!0}):i,e)),f={host:`api.browser.yandex.ru`,hostVOT:`vot.toil.cc/v1`,hostWorker:`vot-worker.toil.cc`,mediaProxy:`media-proxy.toil.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.1.981 Yowser/2.5 Safari/537.36`,componentVersion:`26.3.1.981`,hmac:`bt8xH3VOlb4mqf0nqAibnDOoiPlXsisf`,defaultDuration:310,minChunkSize:5295308,loggerLevel:1,version:`2.4.16`};function p(){let e=0,t=0;for(let n=0;n<28;n+=7){let r=this.buf[this.pos++];if(e|=(r&127)<>4,!(n&128))return this.assertBounds(),[e,t];for(let n=3;n<=31;n+=7){let r=this.buf[this.pos++];if(t|=(r&127)<>>r,a=!(!(i>>>7)&&t==0),o=(a?i|128:i)&255;if(n.push(o),!a)return}let r=e>>>28&15|(t&7)<<4,i=!!(t>>3);if(n.push((i?r|128:r)&255),i){for(let e=3;e<31;e+=7){let r=t>>>e,i=!!(r>>>7),a=(i?r|128:r)&255;if(n.push(a),!i)return}n.push(t>>>31&1)}}var h=4294967296;function g(e){let t=e[0]===`-`;t&&(e=e.slice(1));let n=1e6,r=0,i=0;function a(t,a){let o=Number(e.slice(t,a));i*=n,r=r*n+o,r>=h&&(i+=r/h|0,r%=h)}return a(-24,-18),a(-18,-12),a(-12,-6),a(-6),t?ne(r,i):te(r,i)}function _(e,t){let n=te(e,t),r=n.hi&2147483648;r&&(n=ne(n.lo,n.hi));let i=v(n.lo,n.hi);return r?`-`+i:i}function v(e,t){if({lo:e,hi:t}=ee(e,t),t<=2097151)return String(h*t+e);let n=e&16777215,r=(e>>>24|t<<8)&16777215,i=t>>16&65535,a=n+r*6777216+i*6710656,o=r+i*8147497,s=i*2,c=1e7;return a>=c&&(o+=Math.floor(a/c),a%=c),o>=c&&(s+=Math.floor(o/c),o%=c),s.toString()+re(o)+re(a)}function ee(e,t){return{lo:e>>>0,hi:t>>>0}}function te(e,t){return{lo:e|0,hi:t|0}}function ne(e,t){return t=~t,e?e=~e+1:t+=1,te(e,t)}var re=e=>{let t=String(e);return`0000000`.slice(t.length)+t};function ie(e,t){if(e>=0){for(;e>127;)t.push(e&127|128),e>>>=7;t.push(e)}else{for(let n=0;n<9;n++)t.push(e&127|128),e>>=7;t.push(1)}}function ae(){let e=this.buf[this.pos++],t=e&127;if(!(e&128)||(e=this.buf[this.pos++],t|=(e&127)<<7,!(e&128))||(e=this.buf[this.pos++],t|=(e&127)<<14,!(e&128))||(e=this.buf[this.pos++],t|=(e&127)<<21,!(e&128)))return this.assertBounds(),t;e=this.buf[this.pos++],t|=(e&15)<<28;for(let t=5;e&128&&t<10;t++)e=this.buf[this.pos++];if(e&128)throw Error(`invalid varint`);return this.assertBounds(),t>>>0}var y=oe();function oe(){let e=new DataView(new ArrayBuffer(8));if(typeof BigInt==`function`&&typeof e.getBigInt64==`function`&&typeof e.getBigUint64==`function`&&typeof e.setBigInt64==`function`&&typeof e.setBigUint64==`function`&&(typeof process!=`object`||typeof process.env!=`object`||process.env.BUF_BIGINT_DISABLE!==`1`)){let t=BigInt(`-9223372036854775808`),n=BigInt(`9223372036854775807`),r=BigInt(`0`),i=BigInt(`18446744073709551615`);return{zero:BigInt(0),supported:!0,parse(e){let r=typeof e==`bigint`?e:BigInt(e);if(r>n||ri||t>>0)}raw(e){return this.buf.length&&(this.chunks.push(new Uint8Array(this.buf)),this.buf=[]),this.chunks.push(e),this}uint32(e){for(pe(e);e>127;)this.buf.push(e&127|128),e>>>=7;return this.buf.push(e),this}int32(e){return fe(e),ie(e,this.buf),this}bool(e){return this.buf.push(e?1:0),this}bytes(e){return this.uint32(e.byteLength),this.raw(e)}string(e){let t=this.encodeUtf8(e);return this.uint32(t.byteLength),this.raw(t)}float(e){me(e);let t=new Uint8Array(4);return new DataView(t.buffer).setFloat32(0,e,!0),this.raw(t)}double(e){let t=new Uint8Array(8);return new DataView(t.buffer).setFloat64(0,e,!0),this.raw(t)}fixed32(e){pe(e);let t=new Uint8Array(4);return new DataView(t.buffer).setUint32(0,e,!0),this.raw(t)}sfixed32(e){fe(e);let t=new Uint8Array(4);return new DataView(t.buffer).setInt32(0,e,!0),this.raw(t)}sint32(e){return fe(e),e=(e<<1^e>>31)>>>0,ie(e,this.buf),this}sfixed64(e){let t=new Uint8Array(8),n=new DataView(t.buffer),r=y.enc(e);return n.setInt32(0,r.lo,!0),n.setInt32(4,r.hi,!0),this.raw(t)}fixed64(e){let t=new Uint8Array(8),n=new DataView(t.buffer),r=y.uEnc(e);return n.setInt32(0,r.lo,!0),n.setInt32(4,r.hi,!0),this.raw(t)}int64(e){let t=y.enc(e);return m(t.lo,t.hi,this.buf),this}sint64(e){let t=y.enc(e),n=t.hi>>31;return m(t.lo<<1^n,(t.hi<<1|t.lo>>>31)^n,this.buf),this}uint64(e){let t=y.uEnc(e);return m(t.lo,t.hi,this.buf),this}},x=class{constructor(e,t=ue().decodeUtf8){this.decodeUtf8=t,this.varint64=p,this.uint32=ae,this.buf=e,this.len=e.length,this.pos=0,this.view=new DataView(e.buffer,e.byteOffset,e.byteLength)}tag(){let e=this.uint32(),t=e>>>3,n=e&7;if(t<=0||n<0||n>5)throw Error(`illegal tag: field no `+t+` wire type `+n);return[t,n]}skip(e,t){let n=this.pos;switch(e){case de.Varint:for(;this.buf[this.pos++]&128;);break;case de.Bit64:this.pos+=4;case de.Bit32:this.pos+=4;break;case de.LengthDelimited:let n=this.uint32();this.pos+=n;break;case de.StartGroup:for(;;){let[e,n]=this.tag();if(n===de.EndGroup){if(t!==void 0&&e!==t)throw Error(`invalid end group tag`);break}this.skip(n,e)}break;default:throw Error(`cant skip wire type `+e)}return this.assertBounds(),this.buf.subarray(n,this.pos)}assertBounds(){if(this.pos>this.len)throw RangeError(`premature EOF`)}int32(){return this.uint32()|0}sint32(){let e=this.uint32();return e>>>1^-(e&1)}int64(){return y.dec(...this.varint64())}uint64(){return y.uDec(...this.varint64())}sint64(){let[e,t]=this.varint64(),n=-(e&1);return e=(e>>>1|(t&1)<<31)^n,t=t>>>1^n,y.dec(e,t)}bool(){let[e,t]=this.varint64();return e!==0||t!==0}fixed32(){return this.view.getUint32((this.pos+=4)-4,!0)}sfixed32(){return this.view.getInt32((this.pos+=4)-4,!0)}fixed64(){return y.uDec(this.sfixed32(),this.sfixed32())}sfixed64(){return y.dec(this.sfixed32(),this.sfixed32())}float(){return this.view.getFloat32((this.pos+=4)-4,!0)}double(){return this.view.getFloat64((this.pos+=8)-8,!0)}bytes(){let e=this.uint32(),t=this.pos;return this.pos+=e,this.assertBounds(),this.buf.subarray(t,t+e)}string(){return this.decodeUtf8(this.bytes())}};function fe(e){if(typeof e==`string`)e=Number(e);else if(typeof e!=`number`)throw Error(`invalid int32: `+typeof e);if(!Number.isInteger(e)||e>2147483647||e<-2147483648)throw Error(`invalid int32: `+e)}function pe(e){if(typeof e==`string`)e=Number(e);else if(typeof e!=`number`)throw Error(`invalid uint32: `+typeof e);if(!Number.isInteger(e)||e>4294967295||e<0)throw Error(`invalid uint32: `+e)}function me(e){if(typeof e==`string`){let t=e;if(e=Number(e),isNaN(e)&&t!==`NaN`)throw Error(`invalid float32: `+t)}else if(typeof e!=`number`)throw Error(`invalid float32: `+typeof e);if(Number.isFinite(e)&&(e>34028234663852886e22||e<-34028234663852886e22))throw Error(`invalid float32: `+e)}var S;(function(e){e[e.NO_CONNECTION=0]=`NO_CONNECTION`,e[e.TRANSLATING=10]=`TRANSLATING`,e[e.STREAMING=20]=`STREAMING`,e[e.UNRECOGNIZED=-1]=`UNRECOGNIZED`})(S||={});function he(e){switch(e){case 0:case`NO_CONNECTION`:return S.NO_CONNECTION;case 10:case`TRANSLATING`:return S.TRANSLATING;case 20:case`STREAMING`:return S.STREAMING;default:return S.UNRECOGNIZED}}function ge(e){switch(e){case S.NO_CONNECTION:return`NO_CONNECTION`;case S.TRANSLATING:return`TRANSLATING`;case S.STREAMING:return`STREAMING`;case S.UNRECOGNIZED:default:return`UNRECOGNIZED`}}function _e(){return{target:``,targetUrl:``}}var ve={encode(e,t=new b){return e.target!==``&&t.uint32(10).string(e.target),e.targetUrl!==``&&t.uint32(18).string(e.targetUrl),t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=_e();for(;n.pos>>3){case 1:if(e!==10)break;i.target=n.string();continue;case 2:if(e!==18)break;i.targetUrl=n.string();continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{target:w(e.target)?globalThis.String(e.target):``,targetUrl:w(e.targetUrl)?globalThis.String(e.targetUrl):``}},toJSON(e){let t={};return e.target!==``&&(t.target=e.target),e.targetUrl!==``&&(t.targetUrl=e.targetUrl),t},create(e){return ve.fromPartial(e??{})},fromPartial(e){let t=_e();return t.target=e.target??``,t.targetUrl=e.targetUrl??``,t}};function ye(){return{url:``,deviceId:void 0,firstRequest:!1,duration:0,unknown0:0,language:``,forceSourceLang:!1,unknown1:0,translationHelp:[],wasStream:!1,responseLanguage:``,unknown2:0,unknown3:0,bypassCache:!1,useLivelyVoice:!1,videoTitle:``}}var be={encode(e,t=new b){e.url!==``&&t.uint32(26).string(e.url),e.deviceId!==void 0&&t.uint32(34).string(e.deviceId),e.firstRequest!==!1&&t.uint32(40).bool(e.firstRequest),e.duration!==0&&t.uint32(49).double(e.duration),e.unknown0!==0&&t.uint32(56).int32(e.unknown0),e.language!==``&&t.uint32(66).string(e.language),e.forceSourceLang!==!1&&t.uint32(72).bool(e.forceSourceLang),e.unknown1!==0&&t.uint32(80).int32(e.unknown1);for(let n of e.translationHelp)ve.encode(n,t.uint32(90).fork()).join();return e.wasStream!==!1&&t.uint32(104).bool(e.wasStream),e.responseLanguage!==``&&t.uint32(114).string(e.responseLanguage),e.unknown2!==0&&t.uint32(120).int32(e.unknown2),e.unknown3!==0&&t.uint32(128).int32(e.unknown3),e.bypassCache!==!1&&t.uint32(136).bool(e.bypassCache),e.useLivelyVoice!==!1&&t.uint32(144).bool(e.useLivelyVoice),e.videoTitle!==``&&t.uint32(154).string(e.videoTitle),t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=ye();for(;n.pos>>3){case 3:if(e!==26)break;i.url=n.string();continue;case 4:if(e!==34)break;i.deviceId=n.string();continue;case 5:if(e!==40)break;i.firstRequest=n.bool();continue;case 6:if(e!==49)break;i.duration=n.double();continue;case 7:if(e!==56)break;i.unknown0=n.int32();continue;case 8:if(e!==66)break;i.language=n.string();continue;case 9:if(e!==72)break;i.forceSourceLang=n.bool();continue;case 10:if(e!==80)break;i.unknown1=n.int32();continue;case 11:if(e!==90)break;i.translationHelp.push(ve.decode(n,n.uint32()));continue;case 13:if(e!==104)break;i.wasStream=n.bool();continue;case 14:if(e!==114)break;i.responseLanguage=n.string();continue;case 15:if(e!==120)break;i.unknown2=n.int32();continue;case 16:if(e!==128)break;i.unknown3=n.int32();continue;case 17:if(e!==136)break;i.bypassCache=n.bool();continue;case 18:if(e!==144)break;i.useLivelyVoice=n.bool();continue;case 19:if(e!==154)break;i.videoTitle=n.string();continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{url:w(e.url)?globalThis.String(e.url):``,deviceId:w(e.deviceId)?globalThis.String(e.deviceId):void 0,firstRequest:w(e.firstRequest)?globalThis.Boolean(e.firstRequest):!1,duration:w(e.duration)?globalThis.Number(e.duration):0,unknown0:w(e.unknown0)?globalThis.Number(e.unknown0):0,language:w(e.language)?globalThis.String(e.language):``,forceSourceLang:w(e.forceSourceLang)?globalThis.Boolean(e.forceSourceLang):!1,unknown1:w(e.unknown1)?globalThis.Number(e.unknown1):0,translationHelp:globalThis.Array.isArray(e?.translationHelp)?e.translationHelp.map(e=>ve.fromJSON(e)):[],wasStream:w(e.wasStream)?globalThis.Boolean(e.wasStream):!1,responseLanguage:w(e.responseLanguage)?globalThis.String(e.responseLanguage):``,unknown2:w(e.unknown2)?globalThis.Number(e.unknown2):0,unknown3:w(e.unknown3)?globalThis.Number(e.unknown3):0,bypassCache:w(e.bypassCache)?globalThis.Boolean(e.bypassCache):!1,useLivelyVoice:w(e.useLivelyVoice)?globalThis.Boolean(e.useLivelyVoice):!1,videoTitle:w(e.videoTitle)?globalThis.String(e.videoTitle):``}},toJSON(e){let t={};return e.url!==``&&(t.url=e.url),e.deviceId!==void 0&&(t.deviceId=e.deviceId),e.firstRequest!==!1&&(t.firstRequest=e.firstRequest),e.duration!==0&&(t.duration=e.duration),e.unknown0!==0&&(t.unknown0=Math.round(e.unknown0)),e.language!==``&&(t.language=e.language),e.forceSourceLang!==!1&&(t.forceSourceLang=e.forceSourceLang),e.unknown1!==0&&(t.unknown1=Math.round(e.unknown1)),e.translationHelp?.length&&(t.translationHelp=e.translationHelp.map(e=>ve.toJSON(e))),e.wasStream!==!1&&(t.wasStream=e.wasStream),e.responseLanguage!==``&&(t.responseLanguage=e.responseLanguage),e.unknown2!==0&&(t.unknown2=Math.round(e.unknown2)),e.unknown3!==0&&(t.unknown3=Math.round(e.unknown3)),e.bypassCache!==!1&&(t.bypassCache=e.bypassCache),e.useLivelyVoice!==!1&&(t.useLivelyVoice=e.useLivelyVoice),e.videoTitle!==``&&(t.videoTitle=e.videoTitle),t},create(e){return be.fromPartial(e??{})},fromPartial(e){let t=ye();return t.url=e.url??``,t.deviceId=e.deviceId??void 0,t.firstRequest=e.firstRequest??!1,t.duration=e.duration??0,t.unknown0=e.unknown0??0,t.language=e.language??``,t.forceSourceLang=e.forceSourceLang??!1,t.unknown1=e.unknown1??0,t.translationHelp=e.translationHelp?.map(e=>ve.fromPartial(e))||[],t.wasStream=e.wasStream??!1,t.responseLanguage=e.responseLanguage??``,t.unknown2=e.unknown2??0,t.unknown3=e.unknown3??0,t.bypassCache=e.bypassCache??!1,t.useLivelyVoice=e.useLivelyVoice??!1,t.videoTitle=e.videoTitle??``,t}};function xe(){return{url:void 0,duration:void 0,status:0,remainingTime:void 0,unknown0:void 0,translationId:``,language:void 0,message:void 0,isLivelyVoice:!1,unknown2:void 0,shouldRetry:void 0,unknown3:void 0}}var Se={encode(e,t=new b){return e.url!==void 0&&t.uint32(10).string(e.url),e.duration!==void 0&&t.uint32(17).double(e.duration),e.status!==0&&t.uint32(32).int32(e.status),e.remainingTime!==void 0&&t.uint32(40).int32(e.remainingTime),e.unknown0!==void 0&&t.uint32(48).int32(e.unknown0),e.translationId!==``&&t.uint32(58).string(e.translationId),e.language!==void 0&&t.uint32(66).string(e.language),e.message!==void 0&&t.uint32(74).string(e.message),e.isLivelyVoice!==!1&&t.uint32(80).bool(e.isLivelyVoice),e.unknown2!==void 0&&t.uint32(88).int32(e.unknown2),e.shouldRetry!==void 0&&t.uint32(96).int32(e.shouldRetry),e.unknown3!==void 0&&t.uint32(104).int32(e.unknown3),t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=xe();for(;n.pos>>3){case 1:if(e!==10)break;i.url=n.string();continue;case 2:if(e!==17)break;i.duration=n.double();continue;case 4:if(e!==32)break;i.status=n.int32();continue;case 5:if(e!==40)break;i.remainingTime=n.int32();continue;case 6:if(e!==48)break;i.unknown0=n.int32();continue;case 7:if(e!==58)break;i.translationId=n.string();continue;case 8:if(e!==66)break;i.language=n.string();continue;case 9:if(e!==74)break;i.message=n.string();continue;case 10:if(e!==80)break;i.isLivelyVoice=n.bool();continue;case 11:if(e!==88)break;i.unknown2=n.int32();continue;case 12:if(e!==96)break;i.shouldRetry=n.int32();continue;case 13:if(e!==104)break;i.unknown3=n.int32();continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{url:w(e.url)?globalThis.String(e.url):void 0,duration:w(e.duration)?globalThis.Number(e.duration):void 0,status:w(e.status)?globalThis.Number(e.status):0,remainingTime:w(e.remainingTime)?globalThis.Number(e.remainingTime):void 0,unknown0:w(e.unknown0)?globalThis.Number(e.unknown0):void 0,translationId:w(e.translationId)?globalThis.String(e.translationId):``,language:w(e.language)?globalThis.String(e.language):void 0,message:w(e.message)?globalThis.String(e.message):void 0,isLivelyVoice:w(e.isLivelyVoice)?globalThis.Boolean(e.isLivelyVoice):!1,unknown2:w(e.unknown2)?globalThis.Number(e.unknown2):void 0,shouldRetry:w(e.shouldRetry)?globalThis.Number(e.shouldRetry):void 0,unknown3:w(e.unknown3)?globalThis.Number(e.unknown3):void 0}},toJSON(e){let t={};return e.url!==void 0&&(t.url=e.url),e.duration!==void 0&&(t.duration=e.duration),e.status!==0&&(t.status=Math.round(e.status)),e.remainingTime!==void 0&&(t.remainingTime=Math.round(e.remainingTime)),e.unknown0!==void 0&&(t.unknown0=Math.round(e.unknown0)),e.translationId!==``&&(t.translationId=e.translationId),e.language!==void 0&&(t.language=e.language),e.message!==void 0&&(t.message=e.message),e.isLivelyVoice!==!1&&(t.isLivelyVoice=e.isLivelyVoice),e.unknown2!==void 0&&(t.unknown2=Math.round(e.unknown2)),e.shouldRetry!==void 0&&(t.shouldRetry=Math.round(e.shouldRetry)),e.unknown3!==void 0&&(t.unknown3=Math.round(e.unknown3)),t},create(e){return Se.fromPartial(e??{})},fromPartial(e){let t=xe();return t.url=e.url??void 0,t.duration=e.duration??void 0,t.status=e.status??0,t.remainingTime=e.remainingTime??void 0,t.unknown0=e.unknown0??void 0,t.translationId=e.translationId??``,t.language=e.language??void 0,t.message=e.message??void 0,t.isLivelyVoice=e.isLivelyVoice??!1,t.unknown2=e.unknown2??void 0,t.shouldRetry=e.shouldRetry??void 0,t.unknown3=e.unknown3??void 0,t}};function Ce(){return{status:0,remainingTime:void 0,message:void 0,unknown0:void 0}}var C={encode(e,t=new b){return e.status!==0&&t.uint32(8).int32(e.status),e.remainingTime!==void 0&&t.uint32(16).int32(e.remainingTime),e.message!==void 0&&t.uint32(26).string(e.message),e.unknown0!==void 0&&t.uint32(32).int32(e.unknown0),t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=Ce();for(;n.pos>>3){case 1:if(e!==8)break;i.status=n.int32();continue;case 2:if(e!==16)break;i.remainingTime=n.int32();continue;case 3:if(e!==26)break;i.message=n.string();continue;case 4:if(e!==32)break;i.unknown0=n.int32();continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{status:w(e.status)?globalThis.Number(e.status):0,remainingTime:w(e.remainingTime)?globalThis.Number(e.remainingTime):void 0,message:w(e.message)?globalThis.String(e.message):void 0,unknown0:w(e.unknown0)?globalThis.Number(e.unknown0):void 0}},toJSON(e){let t={};return e.status!==0&&(t.status=Math.round(e.status)),e.remainingTime!==void 0&&(t.remainingTime=Math.round(e.remainingTime)),e.message!==void 0&&(t.message=e.message),e.unknown0!==void 0&&(t.unknown0=Math.round(e.unknown0)),t},create(e){return C.fromPartial(e??{})},fromPartial(e){let t=Ce();return t.status=e.status??0,t.remainingTime=e.remainingTime??void 0,t.message=e.message??void 0,t.unknown0=e.unknown0??void 0,t}};function we(){return{url:``,duration:0,language:``,responseLanguage:``}}var Te={encode(e,t=new b){return e.url!==``&&t.uint32(10).string(e.url),e.duration!==0&&t.uint32(17).double(e.duration),e.language!==``&&t.uint32(26).string(e.language),e.responseLanguage!==``&&t.uint32(34).string(e.responseLanguage),t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=we();for(;n.pos>>3){case 1:if(e!==10)break;i.url=n.string();continue;case 2:if(e!==17)break;i.duration=n.double();continue;case 3:if(e!==26)break;i.language=n.string();continue;case 4:if(e!==34)break;i.responseLanguage=n.string();continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{url:w(e.url)?globalThis.String(e.url):``,duration:w(e.duration)?globalThis.Number(e.duration):0,language:w(e.language)?globalThis.String(e.language):``,responseLanguage:w(e.responseLanguage)?globalThis.String(e.responseLanguage):``}},toJSON(e){let t={};return e.url!==``&&(t.url=e.url),e.duration!==0&&(t.duration=e.duration),e.language!==``&&(t.language=e.language),e.responseLanguage!==``&&(t.responseLanguage=e.responseLanguage),t},create(e){return Te.fromPartial(e??{})},fromPartial(e){let t=we();return t.url=e.url??``,t.duration=e.duration??0,t.language=e.language??``,t.responseLanguage=e.responseLanguage??``,t}};function Ee(){return{default:void 0,cloning:void 0}}var De={encode(e,t=new b){return e.default!==void 0&&C.encode(e.default,t.uint32(10).fork()).join(),e.cloning!==void 0&&C.encode(e.cloning,t.uint32(18).fork()).join(),t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=Ee();for(;n.pos>>3){case 1:if(e!==10)break;i.default=C.decode(n,n.uint32());continue;case 2:if(e!==18)break;i.cloning=C.decode(n,n.uint32());continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{default:w(e.default)?C.fromJSON(e.default):void 0,cloning:w(e.cloning)?C.fromJSON(e.cloning):void 0}},toJSON(e){let t={};return e.default!==void 0&&(t.default=C.toJSON(e.default)),e.cloning!==void 0&&(t.cloning=C.toJSON(e.cloning)),t},create(e){return De.fromPartial(e??{})},fromPartial(e){let t=Ee();return t.default=e.default!==void 0&&e.default!==null?C.fromPartial(e.default):void 0,t.cloning=e.cloning!==void 0&&e.cloning!==null?C.fromPartial(e.cloning):void 0,t}};function Oe(){return{audioFile:new Uint8Array,fileId:``}}var ke={encode(e,t=new b){return e.audioFile.length!==0&&t.uint32(18).bytes(e.audioFile),e.fileId!==``&&t.uint32(10).string(e.fileId),t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=Oe();for(;n.pos>>3){case 2:if(e!==18)break;i.audioFile=n.bytes();continue;case 1:if(e!==10)break;i.fileId=n.string();continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{audioFile:w(e.audioFile)?nt(e.audioFile):new Uint8Array,fileId:w(e.fileId)?globalThis.String(e.fileId):``}},toJSON(e){let t={};return e.audioFile.length!==0&&(t.audioFile=rt(e.audioFile)),e.fileId!==``&&(t.fileId=e.fileId),t},create(e){return ke.fromPartial(e??{})},fromPartial(e){let t=Oe();return t.audioFile=e.audioFile??new Uint8Array,t.fileId=e.fileId??``,t}};function Ae(){return{audioFile:new Uint8Array,chunkId:0}}var je={encode(e,t=new b){return e.audioFile.length!==0&&t.uint32(18).bytes(e.audioFile),e.chunkId!==0&&t.uint32(8).int32(e.chunkId),t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=Ae();for(;n.pos>>3){case 2:if(e!==18)break;i.audioFile=n.bytes();continue;case 1:if(e!==8)break;i.chunkId=n.int32();continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{audioFile:w(e.audioFile)?nt(e.audioFile):new Uint8Array,chunkId:w(e.chunkId)?globalThis.Number(e.chunkId):0}},toJSON(e){let t={};return e.audioFile.length!==0&&(t.audioFile=rt(e.audioFile)),e.chunkId!==0&&(t.chunkId=Math.round(e.chunkId)),t},create(e){return je.fromPartial(e??{})},fromPartial(e){let t=Ae();return t.audioFile=e.audioFile??new Uint8Array,t.chunkId=e.chunkId??0,t}};function Me(){return{audioBuffer:void 0,audioPartsLength:0,fileId:``,version:0}}var Ne={encode(e,t=new b){return e.audioBuffer!==void 0&&je.encode(e.audioBuffer,t.uint32(10).fork()).join(),e.audioPartsLength!==0&&t.uint32(16).int32(e.audioPartsLength),e.fileId!==``&&t.uint32(26).string(e.fileId),e.version!==0&&t.uint32(32).int32(e.version),t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=Me();for(;n.pos>>3){case 1:if(e!==10)break;i.audioBuffer=je.decode(n,n.uint32());continue;case 2:if(e!==16)break;i.audioPartsLength=n.int32();continue;case 3:if(e!==26)break;i.fileId=n.string();continue;case 4:if(e!==32)break;i.version=n.int32();continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{audioBuffer:w(e.audioBuffer)?je.fromJSON(e.audioBuffer):void 0,audioPartsLength:w(e.audioPartsLength)?globalThis.Number(e.audioPartsLength):0,fileId:w(e.fileId)?globalThis.String(e.fileId):``,version:w(e.version)?globalThis.Number(e.version):0}},toJSON(e){let t={};return e.audioBuffer!==void 0&&(t.audioBuffer=je.toJSON(e.audioBuffer)),e.audioPartsLength!==0&&(t.audioPartsLength=Math.round(e.audioPartsLength)),e.fileId!==``&&(t.fileId=e.fileId),e.version!==0&&(t.version=Math.round(e.version)),t},create(e){return Ne.fromPartial(e??{})},fromPartial(e){let t=Me();return t.audioBuffer=e.audioBuffer!==void 0&&e.audioBuffer!==null?je.fromPartial(e.audioBuffer):void 0,t.audioPartsLength=e.audioPartsLength??0,t.fileId=e.fileId??``,t.version=e.version??0,t}};function Pe(){return{translationId:``,url:``,partialAudioInfo:void 0,audioInfo:void 0}}var Fe={encode(e,t=new b){return e.translationId!==``&&t.uint32(10).string(e.translationId),e.url!==``&&t.uint32(18).string(e.url),e.partialAudioInfo!==void 0&&Ne.encode(e.partialAudioInfo,t.uint32(34).fork()).join(),e.audioInfo!==void 0&&ke.encode(e.audioInfo,t.uint32(50).fork()).join(),t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=Pe();for(;n.pos>>3){case 1:if(e!==10)break;i.translationId=n.string();continue;case 2:if(e!==18)break;i.url=n.string();continue;case 4:if(e!==34)break;i.partialAudioInfo=Ne.decode(n,n.uint32());continue;case 6:if(e!==50)break;i.audioInfo=ke.decode(n,n.uint32());continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{translationId:w(e.translationId)?globalThis.String(e.translationId):``,url:w(e.url)?globalThis.String(e.url):``,partialAudioInfo:w(e.partialAudioInfo)?Ne.fromJSON(e.partialAudioInfo):void 0,audioInfo:w(e.audioInfo)?ke.fromJSON(e.audioInfo):void 0}},toJSON(e){let t={};return e.translationId!==``&&(t.translationId=e.translationId),e.url!==``&&(t.url=e.url),e.partialAudioInfo!==void 0&&(t.partialAudioInfo=Ne.toJSON(e.partialAudioInfo)),e.audioInfo!==void 0&&(t.audioInfo=ke.toJSON(e.audioInfo)),t},create(e){return Fe.fromPartial(e??{})},fromPartial(e){let t=Pe();return t.translationId=e.translationId??``,t.url=e.url??``,t.partialAudioInfo=e.partialAudioInfo!==void 0&&e.partialAudioInfo!==null?Ne.fromPartial(e.partialAudioInfo):void 0,t.audioInfo=e.audioInfo!==void 0&&e.audioInfo!==null?ke.fromPartial(e.audioInfo):void 0,t}};function Ie(){return{status:0,remainingChunks:[]}}var Le={encode(e,t=new b){e.status!==0&&t.uint32(8).int32(e.status);for(let n of e.remainingChunks)t.uint32(18).string(n);return t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=Ie();for(;n.pos>>3){case 1:if(e!==8)break;i.status=n.int32();continue;case 2:if(e!==18)break;i.remainingChunks.push(n.string());continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{status:w(e.status)?globalThis.Number(e.status):0,remainingChunks:globalThis.Array.isArray(e?.remainingChunks)?e.remainingChunks.map(e=>globalThis.String(e)):[]}},toJSON(e){let t={};return e.status!==0&&(t.status=Math.round(e.status)),e.remainingChunks?.length&&(t.remainingChunks=e.remainingChunks),t},create(e){return Le.fromPartial(e??{})},fromPartial(e){let t=Ie();return t.status=e.status??0,t.remainingChunks=e.remainingChunks?.map(e=>e)||[],t}};function Re(){return{language:``,url:``,unknown0:0,translatedLanguage:``,translatedUrl:``,unknown1:0,unknown2:0}}var ze={encode(e,t=new b){return e.language!==``&&t.uint32(10).string(e.language),e.url!==``&&t.uint32(18).string(e.url),e.unknown0!==0&&t.uint32(24).int32(e.unknown0),e.translatedLanguage!==``&&t.uint32(34).string(e.translatedLanguage),e.translatedUrl!==``&&t.uint32(42).string(e.translatedUrl),e.unknown1!==0&&t.uint32(48).int32(e.unknown1),e.unknown2!==0&&t.uint32(56).int32(e.unknown2),t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=Re();for(;n.pos>>3){case 1:if(e!==10)break;i.language=n.string();continue;case 2:if(e!==18)break;i.url=n.string();continue;case 3:if(e!==24)break;i.unknown0=n.int32();continue;case 4:if(e!==34)break;i.translatedLanguage=n.string();continue;case 5:if(e!==42)break;i.translatedUrl=n.string();continue;case 6:if(e!==48)break;i.unknown1=n.int32();continue;case 7:if(e!==56)break;i.unknown2=n.int32();continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{language:w(e.language)?globalThis.String(e.language):``,url:w(e.url)?globalThis.String(e.url):``,unknown0:w(e.unknown0)?globalThis.Number(e.unknown0):0,translatedLanguage:w(e.translatedLanguage)?globalThis.String(e.translatedLanguage):``,translatedUrl:w(e.translatedUrl)?globalThis.String(e.translatedUrl):``,unknown1:w(e.unknown1)?globalThis.Number(e.unknown1):0,unknown2:w(e.unknown2)?globalThis.Number(e.unknown2):0}},toJSON(e){let t={};return e.language!==``&&(t.language=e.language),e.url!==``&&(t.url=e.url),e.unknown0!==0&&(t.unknown0=Math.round(e.unknown0)),e.translatedLanguage!==``&&(t.translatedLanguage=e.translatedLanguage),e.translatedUrl!==``&&(t.translatedUrl=e.translatedUrl),e.unknown1!==0&&(t.unknown1=Math.round(e.unknown1)),e.unknown2!==0&&(t.unknown2=Math.round(e.unknown2)),t},create(e){return ze.fromPartial(e??{})},fromPartial(e){let t=Re();return t.language=e.language??``,t.url=e.url??``,t.unknown0=e.unknown0??0,t.translatedLanguage=e.translatedLanguage??``,t.translatedUrl=e.translatedUrl??``,t.unknown1=e.unknown1??0,t.unknown2=e.unknown2??0,t}};function Be(){return{url:``,language:``}}var Ve={encode(e,t=new b){return e.url!==``&&t.uint32(10).string(e.url),e.language!==``&&t.uint32(18).string(e.language),t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=Be();for(;n.pos>>3){case 1:if(e!==10)break;i.url=n.string();continue;case 2:if(e!==18)break;i.language=n.string();continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{url:w(e.url)?globalThis.String(e.url):``,language:w(e.language)?globalThis.String(e.language):``}},toJSON(e){let t={};return e.url!==``&&(t.url=e.url),e.language!==``&&(t.language=e.language),t},create(e){return Ve.fromPartial(e??{})},fromPartial(e){let t=Be();return t.url=e.url??``,t.language=e.language??``,t}};function He(){return{waiting:!1,subtitles:[]}}var Ue={encode(e,t=new b){e.waiting!==!1&&t.uint32(8).bool(e.waiting);for(let n of e.subtitles)ze.encode(n,t.uint32(18).fork()).join();return t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=He();for(;n.pos>>3){case 1:if(e!==8)break;i.waiting=n.bool();continue;case 2:if(e!==18)break;i.subtitles.push(ze.decode(n,n.uint32()));continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{waiting:w(e.waiting)?globalThis.Boolean(e.waiting):!1,subtitles:globalThis.Array.isArray(e?.subtitles)?e.subtitles.map(e=>ze.fromJSON(e)):[]}},toJSON(e){let t={};return e.waiting!==!1&&(t.waiting=e.waiting),e.subtitles?.length&&(t.subtitles=e.subtitles.map(e=>ze.toJSON(e))),t},create(e){return Ue.fromPartial(e??{})},fromPartial(e){let t=He();return t.waiting=e.waiting??!1,t.subtitles=e.subtitles?.map(e=>ze.fromPartial(e))||[],t}};function We(){return{url:``,timestamp:``}}var Ge={encode(e,t=new b){return e.url!==``&&t.uint32(10).string(e.url),e.timestamp!==``&&t.uint32(18).string(e.timestamp),t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=We();for(;n.pos>>3){case 1:if(e!==10)break;i.url=n.string();continue;case 2:if(e!==18)break;i.timestamp=n.string();continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{url:w(e.url)?globalThis.String(e.url):``,timestamp:w(e.timestamp)?globalThis.String(e.timestamp):``}},toJSON(e){let t={};return e.url!==``&&(t.url=e.url),e.timestamp!==``&&(t.timestamp=e.timestamp),t},create(e){return Ge.fromPartial(e??{})},fromPartial(e){let t=We();return t.url=e.url??``,t.timestamp=e.timestamp??``,t}};function Ke(){return{url:``,language:``,responseLanguage:``,unknown0:0,unknown1:0}}var qe={encode(e,t=new b){return e.url!==``&&t.uint32(10).string(e.url),e.language!==``&&t.uint32(18).string(e.language),e.responseLanguage!==``&&t.uint32(26).string(e.responseLanguage),e.unknown0!==0&&t.uint32(40).int32(e.unknown0),e.unknown1!==0&&t.uint32(48).int32(e.unknown1),t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=Ke();for(;n.pos>>3){case 1:if(e!==10)break;i.url=n.string();continue;case 2:if(e!==18)break;i.language=n.string();continue;case 3:if(e!==26)break;i.responseLanguage=n.string();continue;case 5:if(e!==40)break;i.unknown0=n.int32();continue;case 6:if(e!==48)break;i.unknown1=n.int32();continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{url:w(e.url)?globalThis.String(e.url):``,language:w(e.language)?globalThis.String(e.language):``,responseLanguage:w(e.responseLanguage)?globalThis.String(e.responseLanguage):``,unknown0:w(e.unknown0)?globalThis.Number(e.unknown0):0,unknown1:w(e.unknown1)?globalThis.Number(e.unknown1):0}},toJSON(e){let t={};return e.url!==``&&(t.url=e.url),e.language!==``&&(t.language=e.language),e.responseLanguage!==``&&(t.responseLanguage=e.responseLanguage),e.unknown0!==0&&(t.unknown0=Math.round(e.unknown0)),e.unknown1!==0&&(t.unknown1=Math.round(e.unknown1)),t},create(e){return qe.fromPartial(e??{})},fromPartial(e){let t=Ke();return t.url=e.url??``,t.language=e.language??``,t.responseLanguage=e.responseLanguage??``,t.unknown0=e.unknown0??0,t.unknown1=e.unknown1??0,t}};function Je(){return{interval:0,translatedInfo:void 0,pingId:void 0}}var Ye={encode(e,t=new b){return e.interval!==0&&t.uint32(8).int32(e.interval),e.translatedInfo!==void 0&&Ge.encode(e.translatedInfo,t.uint32(18).fork()).join(),e.pingId!==void 0&&t.uint32(24).int32(e.pingId),t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=Je();for(;n.pos>>3){case 1:if(e!==8)break;i.interval=n.int32();continue;case 2:if(e!==18)break;i.translatedInfo=Ge.decode(n,n.uint32());continue;case 3:if(e!==24)break;i.pingId=n.int32();continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{interval:w(e.interval)?he(e.interval):0,translatedInfo:w(e.translatedInfo)?Ge.fromJSON(e.translatedInfo):void 0,pingId:w(e.pingId)?globalThis.Number(e.pingId):void 0}},toJSON(e){let t={};return e.interval!==0&&(t.interval=ge(e.interval)),e.translatedInfo!==void 0&&(t.translatedInfo=Ge.toJSON(e.translatedInfo)),e.pingId!==void 0&&(t.pingId=Math.round(e.pingId)),t},create(e){return Ye.fromPartial(e??{})},fromPartial(e){let t=Je();return t.interval=e.interval??0,t.translatedInfo=e.translatedInfo!==void 0&&e.translatedInfo!==null?Ge.fromPartial(e.translatedInfo):void 0,t.pingId=e.pingId??void 0,t}};function Xe(){return{pingId:0}}var Ze={encode(e,t=new b){return e.pingId!==0&&t.uint32(8).int32(e.pingId),t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=Xe();for(;n.pos>>3){case 1:if(e!==8)break;i.pingId=n.int32();continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{pingId:w(e.pingId)?globalThis.Number(e.pingId):0}},toJSON(e){let t={};return e.pingId!==0&&(t.pingId=Math.round(e.pingId)),t},create(e){return Ze.fromPartial(e??{})},fromPartial(e){let t=Xe();return t.pingId=e.pingId??0,t}};function Qe(){return{uuid:``,module:``}}var $e={encode(e,t=new b){return e.uuid!==``&&t.uint32(10).string(e.uuid),e.module!==``&&t.uint32(18).string(e.module),t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=Qe();for(;n.pos>>3){case 1:if(e!==10)break;i.uuid=n.string();continue;case 2:if(e!==18)break;i.module=n.string();continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{uuid:w(e.uuid)?globalThis.String(e.uuid):``,module:w(e.module)?globalThis.String(e.module):``}},toJSON(e){let t={};return e.uuid!==``&&(t.uuid=e.uuid),e.module!==``&&(t.module=e.module),t},create(e){return $e.fromPartial(e??{})},fromPartial(e){let t=Qe();return t.uuid=e.uuid??``,t.module=e.module??``,t}};function et(){return{secretKey:``,expires:0}}var tt={encode(e,t=new b){return e.secretKey!==``&&t.uint32(10).string(e.secretKey),e.expires!==0&&t.uint32(16).int32(e.expires),t},decode(e,t){let n=e instanceof x?e:new x(e),r=t===void 0?n.len:n.pos+t,i=et();for(;n.pos>>3){case 1:if(e!==10)break;i.secretKey=n.string();continue;case 2:if(e!==16)break;i.expires=n.int32();continue}if((e&7)==4||e===0)break;n.skip(e&7)}return i},fromJSON(e){return{secretKey:w(e.secretKey)?globalThis.String(e.secretKey):``,expires:w(e.expires)?globalThis.Number(e.expires):0}},toJSON(e){let t={};return e.secretKey!==``&&(t.secretKey=e.secretKey),e.expires!==0&&(t.expires=Math.round(e.expires)),t},create(e){return tt.fromPartial(e??{})},fromPartial(e){let t=et();return t.secretKey=e.secretKey??``,t.expires=e.expires??0,t}};function nt(e){if(globalThis.Buffer)return Uint8Array.from(globalThis.Buffer.from(e,`base64`));{let t=globalThis.atob(e),n=new Uint8Array(t.length);for(let e=0;e{t.push(globalThis.String.fromCharCode(e))}),globalThis.btoa(t.join(``))}}function w(e){return e!=null}var it;(function(e){e[e.DEBUG=0]=`DEBUG`,e[e.INFO=1]=`INFO`,e[e.WARN=2]=`WARN`,e[e.ERROR=3]=`ERROR`,e[e.SILENCE=4]=`SILENCE`})(it||={});var at=`[vot.js v${f.version}]`;function ot(e){return f.loggerLevel<=e}function st(...e){ot(it.DEBUG)&&console.log(at,...e)}function ct(...e){ot(it.INFO)&&console.info(at,...e)}function lt(...e){ot(it.WARN)&&console.warn(at,...e)}function ut(...e){ot(it.ERROR)&&console.error(at,...e)}var T={canLog:ot,log:st,info:ct,warn:lt,error:ut},dt=l({default:()=>ft,getRandomValues:()=>mt,randomUUID:()=>ht,subtle:()=>pt}),ft,pt,mt,ht,gt=s((()=>{if(ft=globalThis.crypto,!ft?.subtle)throw TypeError(`Web Crypto API is not available in this environment.`);pt=ft.subtle,mt=ft.getRandomValues.bind(ft),ht=typeof ft.randomUUID==`function`?ft.randomUUID.bind(ft):void 0})),{componentVersion:_t}=f;async function vt(){return typeof window<`u`&&window.crypto?window.crypto:await Promise.resolve().then(()=>(gt(),dt))}var bt=new TextEncoder;async function xt(e,t,n){let r=await vt(),i=await r.subtle.importKey(`raw`,bt.encode(t),{name:`HMAC`,hash:{name:e}},!1,[`sign`,`verify`]);return await r.subtle.sign(`HMAC`,i,n)}async function St(e){let t=await xt(`SHA-256`,f.hmac,e);return new Uint8Array(t).reduce((e,t)=>e+t.toString(16).padStart(2,`0`),``)}async function Ct(e,t,n,r){let{secretKey:i,uuid:a}=t,o=`${a}:${r}:${_t}`,s=await St(bt.encode(o));if(e===`Ya-Summary`)return{[`X-${e}-Sk`]:i,[`X-${e}-Token`]:`${s}:${o}`};if(!n)throw TypeError(`Body is required for sec type ${e}`);let c=await St(n);return{[`${e}-Signature`]:c,[`Sec-${e}-Sk`]:i,[`Sec-${e}-Token`]:`${s}:${o}`}}function wt(){let e=``;for(let t=0;t<32;t++){let t=Math.floor(Math.random()*16);e+=`0123456789ABCDEF`[t]}return e}async function Tt(e,t){try{let n=await xt(`SHA-1`,e,bt.encode(t));return btoa(String.fromCharCode(...new Uint8Array(n)))}catch(e){return T.error(e),!1}}var Et={"sec-ch-ua":`"Chromium";v="146", "YaBrowser";v="${_t.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="${_t}", "Not?A_Brand";v="26.0.0.0", "Yowser";v="2.5"`,"Sec-Fetch-Mode":`no-cors`},Dt={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 Ot(e,t={headers:{"User-Agent":f.userAgent}}){let{timeout:n=3e3,signal:r,...i}=t;if(!r&&(!n||n<=0))return await fetch(e,i);let a=new AbortController,o=e=>{a.signal.aborted||a.abort(e)};r&&(r.aborted?o(r.reason):r.addEventListener(`abort`,()=>o(r.reason),{once:!0}));let s;n&&n>0&&(s=setTimeout(()=>o(Error(`Fetch timeout`)),n));try{return await fetch(e,{...i,signal:a.signal})}finally{s&&clearTimeout(s)}}function kt(){return Math.floor(Date.now()/1e3)}function E(e){return e.length===3?Dt[e]:e.toLowerCase().split(/[_;-]/)[0].trim()}function D(e,t=`mp4`){let n=`https://${f.mediaProxy}/v1/proxy/video.${t}?format=base64&force=true`;return e instanceof URL?`${n}&url=${btoa(e.href)}&origin=${e.origin}&referer=${e.origin}`:`${n}&url=${btoa(e)}`}function At(e,t){let n=e.replace(/^\/+/,``),r=new URL(`https://vk.com/video`);r.searchParams.set(`z`,n);for(let e of[`list`,`access_key`]){let n=t.searchParams.get(e);n&&r.searchParams.set(e,n)}return r.toString()}function jt(e,t,n,r,i,{forceSourceLang:a=!1,wasStream:o=!1,videoTitle:s=``,bypassCache:c=!1,useLivelyVoice:l=!1,firstRequest:u=!0}={}){return be.encode({url:e,firstRequest:u,duration:t,unknown0:1,language:n,forceSourceLang:a,unknown1:0,translationHelp:i??[],responseLanguage:r,wasStream:o,unknown2:1,unknown3:2,bypassCache:c,useLivelyVoice:l,videoTitle:s}).finish()}function Mt(e){return Se.decode(new Uint8Array(e))}function Nt(e,t,n,r){return Te.encode({url:e,duration:t,language:n,responseLanguage:r}).finish()}function Pt(e){return De.decode(new Uint8Array(e))}function Ft(e){return`chunkId`in e}function It(e,t,n,r){return r&&Ft(n)?Fe.encode({url:e,translationId:t,partialAudioInfo:{...r,audioBuffer:n}}).finish():Fe.encode({url:e,translationId:t,audioInfo:n}).finish()}function Lt(e){return Le.decode(new Uint8Array(e))}function Rt(e,t){return Ve.encode({url:e,language:t}).finish()}function zt(e){return Ue.decode(new Uint8Array(e))}function Bt(e){return Ze.encode({pingId:e}).finish()}function Vt(e,t,n){return qe.encode({url:e,language:t,responseLanguage:n,unknown0:1,unknown1:0}).finish()}function Ht(e){return Ye.decode(new Uint8Array(e))}var O={encodeTranslationRequest:jt,decodeTranslationResponse:Mt,encodeTranslationCacheRequest:Nt,decodeTranslationCacheResponse:Pt,isPartialAudioBuffer:Ft,encodeTranslationAudioRequest:It,decodeTranslationAudioResponse:Lt,encodeSubtitlesRequest:Rt,decodeSubtitlesResponse:zt,encodeStreamPingRequest:Bt,encodeStreamRequest:Vt,decodeStreamResponse:Ht};function Ut(e,t){return $e.encode({uuid:e,module:t}).finish()}function Wt(e){return tt.decode(new Uint8Array(e))}var Gt={encodeSessionRequest:Ut,decodeSessionResponse:Wt},Kt;(function(e){e[e.FAILED=0]=`FAILED`,e[e.FINISHED=1]=`FINISHED`,e[e.WAITING=2]=`WAITING`,e[e.LONG_WAITING=3]=`LONG_WAITING`,e[e.PART_CONTENT=5]=`PART_CONTENT`,e[e.AUDIO_REQUESTED=6]=`AUDIO_REQUESTED`,e[e.SESSION_REQUIRED=7]=`SESSION_REQUIRED`})(Kt||={});var qt;(function(e){e.WEB_API_VIDEO_SRC_FROM_IFRAME=`web_api_video_src_from_iframe`,e.WEB_API_VIDEO_SRC=`web_api_video_src`,e.WEB_API_GET_ALL_GENERATING_URLS_DATA_FROM_IFRAME=`web_api_get_all_generating_urls_data_from_iframe`,e.WEB_API_GET_ALL_GENERATING_URLS_DATA_FROM_IFRAME_TMP_EXP=`web_api_get_all_generating_urls_data_from_iframe_tmp_exp`,e.WEB_API_REPLACED_FETCH_INSIDE_IFRAME=`web_api_replaced_fetch_inside_iframe`,e.ANDROID_API=`android_api`,e.WEB_API_SLOW=`web_api_slow`,e.WEB_API_STEAL_SIG_AND_N=`web_api_steal_sig_and_n`,e.WEB_API_COMBINED=`web_api_get_all_generating_urls_data_from_iframe,web_api_steal_sig_and_n`})(qt||={});var k;(function(e){e.custom=`custom`,e.directlink=`custom`,e.youtube=`youtube`,e.preservetube=`preservetube`,e.piped=`piped`,e.invidious=`invidious`,e.niconico=`niconico`,e.vk=`vk`,e.nine_gag=`nine_gag`,e.gag=`nine_gag`,e.twitch=`twitch`,e.proxitok=`proxitok`,e.tiktok=`tiktok`,e.vimeo=`vimeo`,e.xvideos=`xvideos`,e.xhamster=`xhamster`,e.spankbang=`spankbang`,e.rule34video=`rule34video`,e.picarto=`picarto`,e.olympicsreplay=`olympics_replay`,e.pornhub=`pornhub`,e.twitter=`twitter`,e.x=`twitter`,e.rumble=`rumble`,e.facebook=`facebook`,e.rutube=`rutube`,e.coub=`coub`,e.bilibili=`bilibili`,e.mail_ru=`mailru`,e.mailru=`mailru`,e.bitchute=`bitchute`,e.eporner=`eporner`,e.peertube=`peertube`,e.dailymotion=`dailymotion`,e.trovo=`trovo`,e.yandexdisk=`yandexdisk`,e.ok_ru=`okru`,e.okru=`okru`,e.googledrive=`googledrive`,e.bannedvideo=`bannedvideo`,e.weverse=`weverse`,e.weibo=`weibo`,e.newgrounds=`newgrounds`,e.egghead=`egghead`,e.youku=`youku`,e.archive=`archive`,e.kodik=`kodik`,e.patreon=`patreon`,e.reddit=`reddit`,e.kick=`kick`,e.apple_developer=`apple_developer`,e.appledeveloper=`apple_developer`,e.epicgames=`epicgames`,e.odysee=`odysee`,e.coursehunterLike=`coursehunterLike`,e.sap=`sap`,e.watchpornto=`watchpornto`,e.jove=`jove`,e.linkedin=`linkedin`,e.incestflix=`incestflix`,e.porntn=`porntn`,e.dzen=`dzen`,e.bunnystream=`bunnystream`,e.cloudflarestream=`cloudflarestream`,e.loom=`loom`,e.rtnews=`rtnews`,e.bitview=`bitview`,e.thisvid=`thisvid`,e.ign=`ign`,e.zdf=`zdf`,e.bunkr=`bunkr`,e.imdb=`imdb`,e.telegram=`telegram`})(k||={});function Jt(e,t,n){return e===k.patreon?{service:`mux`,videoId:new URL(n).pathname.slice(1)}:{service:e,videoId:t}}var A=class extends Error{data;constructor(e,t=void 0){super(e),this.data=t,this.name=`VOTJSError`}},Yt=class{host;schema;fetch;fetchOpts;sessions={};userAgent=f.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:e=f.host,fetchFn:t=Ot,fetchOpts:n={},headers:r={}}={}){let i=this.hostSchemaRe.exec(e)?.[1];this.host=i?e.replace(`${i}://`,``):e,this.schema=i??`https`,this.fetch=t,this.fetchOpts=n,this.headers={...this.headers,...r}}async request(e,t,n={},r=`POST`){let i=this.getOpts(new Blob([t]),n,r);try{let t=await this.fetch(`${this.schema}://${this.host}${e}`,i),n=await t.arrayBuffer();return{success:t.status===200,data:n}}catch(e){return{success:!1,data:e?.message}}}async requestJSON(e,t=null,n={},r=`POST`){let i=this.getOpts(t,{"Content-Type":`application/json`,...n},r);try{let t=await this.fetch(`${this.schema}://${this.host}${e}`,i),n=await t.json();return{success:t.status===200,data:n}}catch(e){return{success:!1,data:e?.message}}}getOpts(e,t={},n=`POST`){return{method:n,headers:{...this.headers,...t},body:e,...this.fetchOpts}}async getSession(e){let t=kt(),n=this.sessions[e];if(n&&n.timestamp+n.expires>t)return n;let{secretKey:r,expires:i,uuid:a}=await this.createSession(e);return this.sessions[e]={secretKey:r,expires:i,timestamp:t,uuid:a},this.sessions[e]}async createSession(e){let t=wt(),n=Gt.encodeSessionRequest(t,e),r=await this.request(`/session/create`,n,{"Vtrans-Signature":await St(n)});if(!r.success)throw new A(`Failed to request create session`,r);return{...Gt.decodeSessionResponse(r.data),uuid:t}}},Xt=class extends Yt{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(e){return!!(/\.(m3u8|m4(a|v)|mpd)/.exec(e)??/^https:\/\/cdn\.qstv\.on\.epicgames\.com/.exec(e))}headersVOT={"User-Agent":`vot.js/${f.version}`,"Content-Type":`application/json`,Pragma:`no-cache`,"Cache-Control":`no-cache`};constructor({host:e,hostVOT:t=f.hostVOT,fetchFn:n,fetchOpts:r,requestLang:i=`en`,responseLang:a=`ru`,apiToken:o,headers:s}={}){super({host:e,fetchFn:n,fetchOpts:r,headers:s});let c=this.hostSchemaRe.exec(t)?.[1];this.hostVOT=c?t.replace(`${c}://`,``):t,this.schemaVOT=c??`https`,this.requestLang=i,this.responseLang=a,this.apiToken=o}get apiTokenHeader(){return this.apiToken?{Authorization:`OAuth ${this.apiToken}`}:{}}async requestVOT(e,t,n={}){let r=this.getOpts(JSON.stringify(t),{...this.headersVOT,...n});try{let t=await this.fetch(`${this.schemaVOT}://${this.hostVOT}${e}`,r),n=await t.json();return{success:t.status===200,data:n}}catch(e){return{success:!1,data:e?.message}}}async translateVideoYAImpl({videoData:e,requestLang:t=this.requestLang,responseLang:n=this.responseLang,translationHelp:r=null,headers:i={},extraOpts:a={},shouldSendFailedAudio:o=!0}){let{url:s,duration:c=f.defaultDuration}=e,l=await this.getSession(`video-translation`),u=O.encodeTranslationRequest(s,c,t,n,r,a),d=this.paths.videoTranslation,p=await Ct(`Vtrans`,l,u,d),m=a.useLivelyVoice?this.apiTokenHeader:{},h=await this.request(d,u,{...p,...m,...i});if(!h.success)throw new A(`Failed to request video translation`,h);let g=O.decodeTranslationResponse(h.data);T.log(`translateVideo`,g);let{status:_,translationId:v}=g;switch(_){case Kt.FAILED:throw new A(`Yandex couldn't translate video`,g);case Kt.FINISHED:case Kt.PART_CONTENT:if(!g.url)throw new A(`Audio link wasn't received from Yandex response`,g);return{translationId:v,translated:!0,url:g.url,status:_,remainingTime:g.remainingTime??-1};case Kt.WAITING:case Kt.LONG_WAITING:return{translationId:v,translated:!1,status:_,remainingTime:g.remainingTime??-1};case Kt.AUDIO_REQUESTED:return s.startsWith(`https://youtu.be/`)&&o?(await this.requestVtransFailAudio(s),await this.requestVtransAudio(s,g.translationId,{audioFile:new Uint8Array,fileId:qt.WEB_API_GET_ALL_GENERATING_URLS_DATA_FROM_IFRAME}),await this.translateVideoYAImpl({videoData:e,requestLang:t,responseLang:n,translationHelp:r,headers:i,shouldSendFailedAudio:!1})):{translationId:v,translated:!1,status:_,remainingTime:g.remainingTime??-1};case Kt.SESSION_REQUIRED:throw new A(`Yandex auth required to translate video. See docs for more info`,g);default:throw T.error(`Unknown response`,g),new A(`Unknown response from Yandex`,g)}}async translateVideoVOTImpl({url:e,videoId:t,service:n,requestLang:r=this.requestLang,responseLang:i=this.responseLang,headers:a={},provider:o=`yandex`}){let s=Jt(n,t,e),c=await this.requestVOT(this.paths.videoTranslation,{provider:o,service:s.service,video_id:s.videoId,from_lang:r,to_lang:i,raw_video:e},{...a});if(!c.success)throw new A(`Failed to request video translation`,c);let l=c.data;switch(l.status){case`failed`:throw new A(`Yandex couldn't translate video`,l);case`success`:if(!l.translated_url)throw new A(`Audio link wasn't received from VOT response`,l);return{translationId:String(l.id),translated:!0,url:l.translated_url,status:1,remainingTime:-1};case`waiting`:return{translationId:``,translated:!1,remainingTime:l.remaining_time,status:2,message:l.message}}}async requestVtransFailAudio(e){let t=await this.requestJSON(this.paths.videoTranslationFailAudio,JSON.stringify({video_url:e}),void 0,`PUT`);if(!t.data||typeof t.data==`string`||t.data.status!==1)throw new A(`Failed to request to fake video translation fail audio js`,t);return t}async requestVtransAudio(e,t,n,r,i={}){let a=await this.getSession(`video-translation`),o;if(O.isPartialAudioBuffer(n)){if(!r)throw new A(`Partial audio metadata is required for partial audio buffer`,n);o=O.encodeTranslationAudioRequest(e,t,n,r)}else o=O.encodeTranslationAudioRequest(e,t,n,void 0);let s=this.paths.videoTranslationAudio,c=await Ct(`Vtrans`,a,o,s),l=await this.request(s,o,{...c,...i},`PUT`);if(!l.success)throw new A(`Failed to request video translation audio`,l);return O.decodeTranslationAudioResponse(l.data)}async translateVideoCache({videoData:e,requestLang:t=this.requestLang,responseLang:n=this.responseLang,headers:r={}}){let{url:i,duration:a=f.defaultDuration}=e,o=await this.getSession(`video-translation`),s=O.encodeTranslationCacheRequest(i,a,t,n),c=this.paths.videoTranslationCache,l=await Ct(`Vtrans`,o,s,c),u=await this.request(c,s,{...l,...r},`POST`);if(!u.success)throw new A(`Failed to request video translation cache`,u);return O.decodeTranslationCacheResponse(u.data)}async translateVideo({videoData:e,requestLang:t=this.requestLang,responseLang:n=this.responseLang,translationHelp:r=null,headers:i={},extraOpts:a={},shouldSendFailedAudio:o=!0}){let{url:s,videoId:c,host:l}=e;return this.isCustomLink(s)?await this.translateVideoVOTImpl({url:s,videoId:c,service:l,requestLang:t,responseLang:n,headers:i,provider:a.useLivelyVoice?`yandex_lively`:`yandex`}):await this.translateVideoYAImpl({videoData:e,requestLang:t,responseLang:n,translationHelp:r,headers:i,extraOpts:a,shouldSendFailedAudio:o})}async getSubtitlesYAImpl({videoData:e,requestLang:t=this.requestLang,headers:n={}}){let{url:r}=e,i=await this.getSession(`video-translation`),a=O.encodeSubtitlesRequest(r,t),o=this.paths.videoSubtitles,s=await Ct(`Vsubs`,i,a,o),c=await this.request(o,a,{...s,...n});if(!c.success)throw new A(`Failed to request video subtitles`,c);let l=O.decodeSubtitlesResponse(c.data),u=l.subtitles.map(e=>{let{language:t,url:n,translatedLanguage:r,translatedUrl:i}=e;return{language:t,url:n,translatedLanguage:r,translatedUrl:i}});return{waiting:l.waiting,subtitles:u}}async getSubtitlesVOTImpl({url:e,videoId:t,service:n,headers:r={}}){let i=Jt(n,t,e),a=await this.requestVOT(this.paths.videoSubtitles,{provider:`yandex`,service:i.service,video_id:i.videoId},r);if(!a.success)throw new A(`Failed to request video subtitles`,a);let o=a.data;return{waiting:!1,subtitles:o.reduce((e,t)=>{if(!t.lang_from)return e;let n=o.find(e=>e.lang===t.lang_from);return n&&e.push({language:n.lang,url:n.subtitle_url,translatedLanguage:t.lang,translatedUrl:t.subtitle_url}),e},[])}}async getSubtitles({videoData:e,requestLang:t=this.requestLang,headers:n={}}){let{url:r,videoId:i,host:a}=e;return this.isCustomLink(r)?await this.getSubtitlesVOTImpl({url:r,videoId:i,service:a,headers:n}):await this.getSubtitlesYAImpl({videoData:e,requestLang:t,headers:n})}async pingStream({pingId:e,headers:t={}}){let n=await this.getSession(`video-translation`),r=O.encodeStreamPingRequest(e),i=this.paths.streamPing,a=await Ct(`Vtrans`,n,r,i),o=await this.request(i,r,{...a,...t});if(!o.success)throw new A(`Failed to request stream ping`,o);return!0}async translateStream({videoData:e,requestLang:t=this.requestLang,responseLang:n=this.responseLang,headers:r={}}){let{url:i}=e;if(this.isCustomLink(i))throw new A(`Unsupported video URL for getting stream translation`);let a=await this.getSession(`video-translation`),o=O.encodeStreamRequest(i,t,n),s=this.paths.streamTranslation,c=await Ct(`Vtrans`,a,o,s),l=await this.request(s,o,{...c,...r});if(!l.success)throw new A(`Failed to request stream translation`,l);let u=O.decodeStreamResponse(l.data),d=u.interval;switch(d){case S.NO_CONNECTION:case S.TRANSLATING:return{translated:!1,interval:d,message:d===S.NO_CONNECTION?`streamNoConnectionToServer`:`translationTakeFewMinutes`};case S.STREAMING:if(u.pingId===void 0)throw new A(`Stream ping id wasn't received from Yandex response`,u);return{translated:!0,interval:d,pingId:u.pingId,result:u.translatedInfo};default:throw T.error(`Unknown response`,u),new A(`Unknown response from Yandex`,u)}}},Zt=class extends Xt{constructor(e={}){e.host=e.host??f.hostWorker,super(e)}async request(e,t,n={},r=`POST`){let i=this.getOpts(JSON.stringify({headers:{...this.headers,...n},body:Array.from(t)}),{"Content-Type":`application/json`},r);try{let t=await this.fetch(`${this.schema}://${this.host}${e}`,i),n=await t.arrayBuffer();return{success:t.status===200,data:n}}catch(e){return{success:!1,data:e?.message}}}async requestJSON(e,t=null,n={},r=`POST`){let i=this.getOpts(JSON.stringify({headers:{...this.headers,"Content-Type":`application/json`,Accept:`application/json`,...n},body:t}),{Accept:`application/json`,"Content-Type":`application/json`},r);try{let t=await this.fetch(`${this.schema}://${this.host}${e}`,i),n=await t.json();return{success:t.status===200,data:n}}catch(e){return{success:!1,data:e?.message}}}},Qt=class extends Xt{constructor(e){super(e),this.headers={...Et,...this.headers}}},$t=class extends Zt{constructor(e){super(e),this.headers={...Et,...this.headers}}},en=class extends Error{constructor(e){super(e),this.name=`VideoDataError`}},tn=/(file:\/\/(\/)?|(http(s)?:\/\/)(127\.0\.0\.1|localhost|192\.168\.(\d){1,3}\.(\d){1,3}))/,nn=[`yewtu.be`,`inv.nadeko.net`,`invidious.nerdvpn.de`,`invidious.protokolla.fi`,`invidious.materialio.us`,`iv.melmac.space`],rn=[`piped.video`,`piped.kavin.rocks`,`piped.private.coffee`],an=[`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`],on=[`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`],sn=[`coursehunter.net`,`coursetrain.net`],j;(function(e){e.udemy=`udemy`,e.coursera=`coursera`,e.douyin=`douyin`,e.artstation=`artstation`,e.kickstarter=`kickstarter`,e.datacamp=`datacamp`,e.oraclelearn=`oraclelearn`,e.deeplearningai=`deeplearningai`,e.netacad=`netacad`,e.mediafile=`mediafile`})(j||={}),{...k,...j};var M={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`},cn=[{additionalData:`mobile`,host:k.youtube,url:`https://youtu.be/`,match:/^m.youtube.com$/,selector:`.player-container`,needExtraData:!0},{host:k.youtube,url:`https://youtu.be/`,match:e=>/^(www.)?youtube(-nocookie|kids)?.com$/.test(e.hostname)&&e.pathname.startsWith(`/tv`),selector:`#container`,needExtraData:!0},{host:k.youtube,url:`https://youtu.be/`,match:/^(www.)?youtube(-nocookie|kids)?.com$/,selector:`.html5-video-container:not(#inline-player *)`,needExtraData:!0},{host:k.invidious,url:`https://youtu.be/`,match:nn,selector:M.idPlayer,needBypassCSP:!0},{host:k.piped,url:`https://youtu.be/`,match:rn,selector:M.shakaPlayer,needBypassCSP:!0},{host:k.preservetube,url:`https://preservetube.com/`,match:/^preservetube\.com$/,selector:`div.video-wrapper`,needExtraData:!0},{host:k.zdf,url:`https://www.zdf.de/play/`,match:[/^zdf.de$/,/^(www.)?zdf.de$/],selector:`div.zdfplayer-app.zdfplayer-desktop, div.zdfplayer-app`},{host:k.niconico,url:`https://www.nicovideo.jp/watch/`,match:[/^(www\.|sp\.)?nicovideo\.jp$/,/^nico\.ms$/],selector:`[class*="grid-area_[player]"] > div`},{additionalData:`mobile`,host:k.vk,url:`https://vk.com/video?z=`,match:[/^m.vk.(com|ru)$/,/^m.vkvideo.ru$/],selector:M.vkVideoPlayer,shadowRoot:!0,needExtraData:!0},{additionalData:`clips`,host:k.vk,url:`https://vk.com/video?z=`,match:/^(www.|m.)?vk.(com|ru)$/,selector:`div[data-testid="clipcontainer-video"]`,needExtraData:!0},{host:k.vk,url:`https://vk.com/video?z=`,match:[/^(www\.|m\.)?vk\.(com|ru)$/,/^(.*\.)?vkvideo\.ru$/],selector:M.vkVideoPlayer,needExtraData:!0},{host:k.nine_gag,url:`https://9gag.com/gag/`,match:/^9gag.com$/,selector:`.video-post`,needExtraData:!0},{host:k.twitch,url:`https://twitch.tv/`,match:[/^m.twitch.tv$/,/^(www.)?twitch.tv$/,/^clips.twitch.tv$/,/^player.twitch.tv$/],needExtraData:!0,selector:`.video-ref, main > div > section > div > div > div`},{host:k.proxitok,url:`https://www.tiktok.com/`,match:an,selector:`.column.has-text-centered`},{host:k.tiktok,url:`https://www.tiktok.com/`,match:/^(www.)?tiktok.com$/,selector:null},{host:j.douyin,url:`https://www.douyin.com/`,match:/^(www.)?douyin.com/,selector:`.xg-video-container`,needExtraData:!0,needBypassCSP:!0},{host:k.vimeo,url:`https://vimeo.com/`,match:/^(www\.|m\.)?vimeo.com$/,needExtraData:!0,selector:M.player},{host:k.vimeo,url:`https://player.vimeo.com/`,match:/^player.vimeo.com$/,additionalData:`embed`,needExtraData:!0,needBypassCSP:!0,selector:M.player},{host:k.xvideos,url:`https://www.xvideos.com/`,match:[/^(www.)?xvideos(-ar)?.com$/,/^(www.)?xvideos(\d\d\d).com$/,/^(www.)?xv-ru.com$/],selector:`#hlsplayer`,needBypassCSP:!0},{host:k.xhamster,url:`https://xhamster.com/`,match:e=>/^(?:[^.]+\.)?(?:xhamster\.(?:com|desi)|xhamster\d+\.(?:com|desi)|xhvid\.com)$/.test(e.host)&&/\/(?:videos\/[^/]+-[\dA-Za-z]+)\/?$/.test(e.pathname),selector:`#player-container`},{host:k.spankbang,url:`https://spankbang.com/`,match:e=>/^(?:[^.]+\.)?spankbang\.com$/.test(e.host)&&/\/(?:[\da-z]+\/(?:video|play|embed)(?:\/[^/]+)?|[\da-z]+-[\da-z]+\/playlist\/[^/?#&]+)\/?$/i.test(e.pathname),selector:`#main_video_player`},{host:k.rule34video,url:`https://rule34video.com/video/`,match:e=>/^(www\.)?rule34video\.com$/.test(e.host)&&/\/videos?\/\d+/.test(e.pathname),selector:M.flowplayer},{host:k.picarto,url:`https://picarto.tv/`,match:e=>/^(www\.)?picarto\.tv$/.test(e.host)&&/^(?:\/[^/]+\/(?:profile\/)?videos\/[^/?#&]+|\/videopopout\/[^/?#&]+|\/[^/#?]+\/?)$/.test(e.pathname),selector:`[class*="VideosTab__PlayerWrapper"]`},{host:k.olympicsreplay,url:`https://olympics.com/`,match:e=>/^(www\.)?olympics\.com$/.test(e.host)&&/^\/[a-z]{2}\/(?:[a-z0-9-]+\/)?(?:replay|videos?|original-series\/episode)\/[\w-]+\/?$/i.test(e.pathname),selector:M.videoJsUniversal},{host:k.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:k.pornhub,url:`https://rt.pornhub.com/view_video.php?viewkey=`,match:e=>/^[a-z]+.pornhub.(com|org)$/.exec(e.host)&&e.pathname.startsWith(`/embed/`),selector:M.idPlayer},{host:k.twitter,url:`https://twitter.com/i/status/`,match:/^(twitter|x).com$/,selector:`div[data-testid="videoComponent"]`,needBypassCSP:!0},{host:k.rumble,url:`https://rumble.com/`,match:/^rumble.com$/,selector:`[id^="vid_"] > div`},{host:k.facebook,url:`https://facebook.com/`,match:e=>e.host.includes(`facebook.com`)&&e.pathname.includes(`/videos/`),selector:`div[role="main"] div[data-pagelet$="video" i]`,needBypassCSP:!0},{additionalData:`reels`,host:k.facebook,url:`https://facebook.com/`,match:e=>e.host.includes(`facebook.com`)&&e.pathname.includes(`/reel/`),selector:`div[role="main"]`,needBypassCSP:!0},{host:k.rutube,url:`https://rutube.ru/video/`,match:/^rutube.ru$/,selector:`div[class*="videoWrapper"]`},{additionalData:`embed`,host:k.rutube,url:`https://rutube.ru/video/`,match:/^rutube.ru$/,selector:`#app > div > div`},{host:k.bilibili,url:`https://www.bilibili.com/`,match:/^(www|m|player).bilibili.com$/,selector:M.bilibiliPlayer},{host:k.bilibili,url:`https://www.bilibili.tv/`,match:/^(?:www\.|m\.)?bilibili\.tv$/,selector:M.bilibiliPlayer},{additionalData:`old`,host:k.bilibili,url:`https://www.bilibili.com/`,match:/^(www|m).bilibili.com$/,selector:null},{host:k.mailru,url:`https://my.mail.ru/`,match:/^my.mail.ru$/,selector:`#b-video-wrapper`},{host:k.bitchute,url:`https://www.bitchute.com/video/`,match:/^(www.)?bitchute.com$/,selector:M.videoJsUniversal},{host:k.eporner,url:`https://www.eporner.com/`,match:/^(www.)?eporner.com$/,selector:M.videoJsUniversal},{host:k.peertube,url:`stub`,match:on,selector:M.videoJsUniversal},{host:k.dailymotion,url:`https://www.dailymotion.com/video/`,match:/^((www\.|player\.)?dailymotion\.com|geo(\d+)?\.dailymotion\.com|dai\.ly)$/,selector:M.player},{host:k.trovo,url:`https://trovo.live/s/`,match:/^trovo.live$/,selector:`.player-video`},{host:k.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)`,eventSelector:`.video-player__player`,needBypassCSP:!0,needExtraData:!0},{host:k.okru,url:`https://ok.ru/video/`,match:/^ok.ru$/,selector:M.vkVideoPlayer,shadowRoot:!0},{host:k.googledrive,url:`https://drive.google.com/file/d/`,match:/^youtube.googleapis.com$/,selector:`.html5-video-container`},{host:k.bannedvideo,url:`https://madmaxworld.tv/watch?id=`,match:/^(www.)?banned.video|madmaxworld.tv$/,selector:M.videoJsUniversal,needExtraData:!0},{host:k.weverse,url:`https://weverse.io/`,match:/^weverse.io$/,selector:`.webplayer-internal-source-wrapper`,needExtraData:!0},{host:k.weibo,url:`https://weibo.com/`,match:e=>/^(?:www\.)?weibo\.com$/.test(e.host)&&/^\/(?:\d+\/[A-Za-z0-9]+|0\/[A-Za-z0-9]+|tv\/show\/\d+:(?:[\da-f]{32}|\d{16,}))\/?$/.test(e.pathname)||/^video\.weibo\.com$/.test(e.host)&&/^\/show\/?$/.test(e.pathname)&&/^\d+:(?:[\da-f]{32}|\d{16,})$/i.test(e.searchParams.get(`fid`)??``)||/^(?:www\.)?weibo\.com$/.test(e.host)&&/^\/newlogin\/?$/.test(e.pathname)&&(e.searchParams.has(`url`)||/^[A-Za-z0-9]+$/.test(e.searchParams.get(`layerid`)??``)),selector:M.videoJsUniversal||`#playVideo`},{host:k.newgrounds,url:`https://www.newgrounds.com/`,match:/^(www.)?newgrounds.com$/,selector:`.ng-video-player`},{host:k.egghead,url:`https://egghead.io/`,match:/^egghead.io$/,selector:`.cueplayer-react-video-holder`},{host:k.youku,url:`https://v.youku.com/`,match:/^v.youku.com$/,selector:`#ykPlayer`},{host:k.archive,url:`https://archive.org/details/`,match:/^archive.org$/,selector:M.jwPlayer},{host:k.kodik,url:`stub`,match:/^kodik.(info|biz|cc)$/,selector:M.flowplayer,needExtraData:!0},{host:k.patreon,url:`stub`,match:/^(www.)?patreon.com$/,selector:`div[data-tag="post-card"] div[elevation="subtle"] > div > div > div > div`,needExtraData:!0},{additionalData:`old`,host:k.reddit,url:`stub`,match:/^old.reddit.com$/,selector:`.reddit-video-player-root`,needExtraData:!0,needBypassCSP:!0},{host:k.reddit,url:`stub`,match:/^(www.|new.)?reddit.com$/,selector:`div[slot=post-media-container]`,shadowRoot:!0,needExtraData:!0,needBypassCSP:!0},{host:k.kick,url:`https://kick.com/`,match:/^kick.com$/,selector:`#injected-embedded-channel-player-video > div`,needExtraData:!0},{host:k.appledeveloper,url:`https://developer.apple.com/`,match:/^developer.apple.com$/,selector:`.developer-video-player`,needExtraData:!0,needBypassCSP:!0},{host:k.epicgames,url:`https://dev.epicgames.com/community/learning/`,match:/^dev.epicgames.com$/,selector:M.videoJsUniversal,needExtraData:!0},{host:k.odysee,url:`stub`,match:/^odysee.com$/,selector:M.videoJsUniversal,needExtraData:!0},{host:k.coursehunterLike,url:`stub`,match:sn,selector:null,needExtraData:!0},{host:k.sap,url:`https://learning.sap.com/courses/`,match:/^learning.sap.com$/,selector:`.playkit-container`,eventSelector:`.playkit-player`,needExtraData:!0,needBypassCSP:!0},{host:j.udemy,url:`https://www.udemy.com/`,match:/udemy.com$/,selector:M.shakaPlayer,needExtraData:!0},{host:j.datacamp,url:`https://www.datacamp.com/courses/`,match:e=>/^(?:campus\.|projector\.)?datacamp\.com$/.test(e.hostname),selector:M.videoJsUniversal,needExtraData:!0},{host:j.coursera,url:`https://www.coursera.org/`,match:/coursera.org$/,selector:M.videoJsUniversal,needExtraData:!0},{host:k.watchpornto,url:`https://watchporn.to/`,match:/^watchporn.to$/,selector:M.flowplayer},{host:k.jove,url:`https://jove.com/`,match:/^(?:app|www)\.jove\.com$/,selector:M.flowplayer},{host:k.linkedin,url:`https://www.linkedin.com/learning/`,match:/^(www.)?linkedin.com$/,selector:M.videoJsUniversal,needExtraData:!0,needBypassCSP:!0},{host:k.incestflix,url:`https://www.incestflix.net/watch/`,match:/^(www.)?incestflix.(net|to|com)$/,selector:`#incflix-stream`,needExtraData:!0},{host:k.porntn,url:`https://porntn.com/videos/`,match:/^porntn.com$/,selector:M.flowplayer,needExtraData:!0},{host:k.dzen,url:`https://dzen.ru/video/watch/`,match:/^dzen.ru$/,selector:`[class*="player__playerWrap"] > div`},{host:k.bunnystream,url:`stub`,match:[/^video\.bunnycdn\.com$/,/^iframe\.mediadelivery\.net$/,/^(?:[^.]+\.)*b-cdn\.net$/],selector:null},{host:k.cloudflarestream,url:`stub`,match:/^(watch|embed|iframe|customer-[^.]+).cloudflarestream.com$/,selector:null},{host:k.loom,url:`https://www.loom.com/share/`,match:/^(www.)?loom.com$/,selector:`.VideoLayersContainer`,needExtraData:!0,needBypassCSP:!0},{host:j.artstation,url:`https://www.artstation.com/learning/`,match:/^(www.)?artstation.com$/,selector:M.videoJsUniversal,needExtraData:!0},{host:k.rtnews,url:`https://www.rt.com/`,match:/^(www.)?rt.com$/,selector:M.jwPlayer,needExtraData:!0},{host:k.bitview,url:`https://www.bitview.net/watch?v=`,match:/^(www.)?bitview.net$/,selector:`.vlScreen`,needExtraData:!0},{host:j.kickstarter,url:`https://www.kickstarter.com/`,match:/^(www.)?kickstarter.com/,selector:`.ksr-video-player`,needExtraData:!0},{host:k.thisvid,url:`https://thisvid.com/`,match:/^(www.)?thisvid.com$/,selector:M.flowplayer},{additionalData:`regional`,host:k.ign,url:`https://de.ign.com/`,match:/^(\w{2}.)?ign.com$/,needExtraData:!0,selector:`.video-container`},{host:k.ign,url:`https://www.ign.com/`,match:/^(www.)?ign.com$/,selector:M.player,needExtraData:!0},{host:k.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:!0,selector:`.plyr__video-wrapper`},{host:k.imdb,url:`https://www.imdb.com/video/`,match:/^(www\.)?imdb\.com$/,selector:M.jwPlayer},{host:k.telegram,url:`https://t.me/`,match:e=>/^web\.telegram\.org$/.test(e.hostname)&&e.pathname.startsWith(`/k`),selector:`.ckin__player`},{host:j.oraclelearn,url:`https://mylearn.oracle.com/ou/course/`,match:/^mylearn\.oracle\.com/,selector:M.videoJsUniversal,needExtraData:!0,needBypassCSP:!0},{host:j.deeplearningai,url:`https://learn.deeplearning.ai/courses/`,match:/^learn(-dev|-staging)?\.deeplearning\.ai/,selector:`.lesson-video-player`,needExtraData:!0},{host:j.netacad,url:`https://www.netacad.com/`,match:/^(www\.)?netacad\.com/,selector:M.videoJsUniversal,needExtraData:!0},{host:j.mediafile,url:`https://mediafile.cc/`,match:/^(www\.)?mediafile\.cc$/,selector:`div#playerContainer`,needExtraData:!0},{host:k.custom,url:`stub`,match:e=>/([^.]+)\.(mp4|webm)/.test(e.pathname),rawResult:!0}],N=class extends Error{constructor(e){super(e),this.name=`VideoHelperError`}},P=class{API_ORIGIN=window.location.origin;fetch;extraInfo;referer;origin;service;video;language;constructor({fetchFn:e=Ot,extraInfo:t=!0,referer:n=document.referrer??`${window.location.origin}/`,origin:r=window.location.origin,service:i,video:a,language:o=`en`}={}){this.fetch=e,this.extraInfo=t,this.referer=n,this.origin=/^(http(s)?):\/\//.test(String(r))?r:window.location.origin,this.service=i,this.video=a,this.language=o}getVideoData(e){return Promise.resolve(void 0)}getVideoId(e){return Promise.resolve(void 0)}returnBaseData(e){if(this.service)return{url:this.service.url+e,videoId:e,host:this.service.host,duration:void 0}}},ln=class extends P{API_ORIGIN=`https://developer.apple.com`;async getVideoData(e){try{let e=document.querySelector(`meta[property='og:video']`)?.content;if(!e)throw new N(`Failed to find content url`);return{url:e}}catch(t){T.error(`Failed to get apple developer video data by video ID: ${e}`,t.message);return}}async getVideoId(e){return/videos\/play\/([^/]+)\/([\d]+)/.exec(e.pathname)?.[0]}},un=class extends P{async getVideoId(e){return/(details|embed)\/([^/]+)/.exec(e.pathname)?.[2]}},dn=class extends P{API_ORIGIN=`https://www.artstation.com/api/v2/learning`;getCSRFToken(){return document.querySelector(`meta[name="public-csrf-token"]`)?.content}async getCourseInfo(e){try{let t=this.getCSRFToken();return await(await this.fetch(`${this.API_ORIGIN}/courses/${e}/autoplay.json`,{method:`POST`,headers:t?{"PUBLIC-CSRF-TOKEN":t}:{}})).json()}catch(t){return T.error(`Failed to get artstation course info by courseId: ${e}.`,t.message),!1}}async getVideoUrl(e){try{return(await(await this.fetch(`${this.API_ORIGIN}/quicksilver/video_url.json?chapter_id=${e}`)).json()).url.replace(`qsep://`,`https://`)}catch(t){return T.error(`Failed to get artstation video url by chapterId: ${e}.`,t.message),!1}}async getVideoData(e){let[,t,,,n]=e.split(`/`),r=await this.getCourseInfo(t);if(!r)return;let i=r.chapters.find(e=>e.hash_id===n);if(!i)return;let a=await this.getVideoUrl(i.id);if(!a)return;let{title:o,duration:s,subtitles:c}=i;return{url:a,title:o,duration:s,subtitles:c.filter(e=>e.format===`vtt`).map(e=>({language:E(e.locale),source:`artstation`,format:`vtt`,url:e.file_url}))}}async getVideoId(e){return/courses\/(\w{3,5})\/([^/]+)\/chapters\/(\w{3,5})/.exec(e.pathname)?.[0]}},fn=class extends P{API_ORIGIN=`https://api.banned.video`;async getVideoInfo(e){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 @@ -259,53 +254,61 @@ System.register("./__entry.js", [], (function (exports, module) { videoUrl: directUrl isStream: live } - }`,variables:{id:t}}),headers:{"User-Agent":"bannedVideoFrontEnd","apollographql-client-name":"banned-web","apollographql-client-version":"1.3","content-type":"application/json"}})).json()}catch(e){return console.error(`Failed to get bannedvideo video info by videoId: ${t}.`,e.message),false}}async getVideoData(t){const e=await this.getVideoInfo(t);if(!e)return;const{videoUrl:i,duration:o,isStream:r,description:s,title:a}=e.data.getVideo;return {url:i,duration:o,isStream:r,title:a,description:s}}async getVideoId(t){return t.searchParams.get("id")??void 0}}class kl extends L{async getVideoId(t){const e=/bangumi\/play\/([^/]+)/.exec(t.pathname)?.[0];if(e)return e;const i=t.searchParams.get("bvid");if(i)return `video/${i}`;const o=/^\/(?:[a-z]{2}\/)?((?:play\/\d+(?:\/\d+)?|video\/\d+))\/?$/i.exec(t.pathname)?.[1];if(o)return o;let r=/video\/([^/]+)/.exec(t.pathname)?.[0];return r&&t.searchParams.get("p")!==null&&(r+=`/?p=${t.searchParams.get("p")}`),r}}class Tl extends L{async getVideoId(t){return /(video|embed)\/([^/]+)/.exec(t.pathname)?.[2]}}class Ll extends L{async getVideoData(t){try{const e=document.querySelector(".vlScreen > video")?.src;if(!e)throw new _("Failed to find video URL");return {url:e}}catch(e){E.error(`Failed to get Bitview data by videoId: ${t}`,e.message);return}}async getVideoId(t){return t.searchParams.get("v")}}class Al extends L{async getVideoData(t){const e=document.querySelector('#player > source[type="video/mp4"]')?.src;if(e)return {url:e}}async getVideoId(t){return /\/f\/([^/]+)/.exec(t.pathname)?.[1]}}class Il extends L{async getVideoId(t){return t.pathname+t.search}}class Cl extends L{async getVideoId(t){return t.pathname+t.search}}class El extends L{API_ORIGIN=this.origin??"https://coursehunter.net";async getCourseId(){const t=window.course_id;return t!==void 0?String(t):document.querySelector('input[name="course_id"]')?.value}async getLessonsData(t){const e=window.lessons;if(e?.length)return e;try{return await(await this.fetch(`${this.API_ORIGIN}/api/v1/course/${t}/lessons`)).json()}catch(i){E.error(`Failed to get CoursehunterLike lessons data by courseId: ${t}, because ${i.message}`);return}}getLessondId(t){let e=t.split("?lesson=")?.[1];return e||(e=document.querySelector(".lessons-item_active")?.dataset?.index,e)?+e:1}async getVideoData(t){const e=await this.getCourseId();if(!e)return;const i=await this.getLessonsData(e);if(!i)return;const o=this.getLessondId(t),r=i?.[o-1],{file:s,duration:a,title:l}=r;if(s)return {url:ut(s),duration:a,title:l}}async getVideoId(t){const e=/course\/([^/]+)/.exec(t.pathname)?.[0];return e?e+t.search:void 0}}const Lt=["auto","ru","en","zh","ko","lt","lv","ar","fr","it","es","de","ja"],si=["ru","en","kk"];class Rt extends L{SUBTITLE_SOURCE="videojs";SUBTITLE_FORMAT="vtt";static getPlayer(){const t=window.videojs,e=document.querySelector("video.vjs-tech[id], video[id$='_html5_api']"),i=e?.id?.endsWith("_html5_api")?e.id.slice(0,-10):void 0;if(t?.getPlayer){if(i){const r=t.getPlayer(i);if(r)return r}if(e){const r=t.getPlayer(e);if(r)return r}}const o=(typeof t?.getPlayers=="function"?t.getPlayers():t?.players)??{};for(const r of Object.values(o)){const s=r,l=(typeof s.el=="function"?s.el():null)?.querySelector?.("video.vjs-tech, video")??null;if(l&&e&&l===e||i&&typeof s.id=="function"&&s.id()===i)return r}}getVideoDataByPlayer(t){try{const e=Rt.getPlayer(),i=document.querySelector("video.vjs-tech, video[id$='_html5_api'], video[src]");if(!e&&!i)throw new Error(`Video player/video element not found, videoId ${t}`);const o=e?.duration?.()??i?.duration;let r;if(e){const l=typeof e.currentSources=="function"?e.currentSources():e.getCache?.()?.sources;r=(Array.isArray(l)?l.find(u=>u?.type==="video/mp4"||u?.type==="video/webm"||u?.src):void 0)?.src;}if(r??=i?.currentSrc||i?.src||i?.getAttribute?.("src")||void 0,!r)throw new Error(`Failed to find video url for videoID ${t}`);const a=(i?Array.from(i.querySelectorAll("track[src]")):[]).filter(l=>l.kind!=="metadata").flatMap(l=>{const c=l.getAttribute("src");if(!c)return [];const u=new URL(c,window.location.href).toString();return [{language:J(l.srclang||""),source:this.SUBTITLE_SOURCE,format:this.SUBTITLE_FORMAT,url:u}]});return {url:r,duration:o,subtitles:a}}catch(e){E.error("Failed to get videojs video data",e.message);return}}}class ai extends Rt{API_ORIGIN="https://www.coursera.org/api";SUBTITLE_SOURCE="coursera";async getCourseData(t){try{return (await(await this.fetch(`${this.API_ORIGIN}/onDemandCourses.v1/${t}`)).json())?.elements?.[0]}catch(e){E.error(`Failed to get course data by courseId: ${t}`,e.message);return}}static getPlayer(){return Rt.getPlayer()}async getVideoData(t){const e=this.getVideoDataByPlayer(t);if(!e)return;const{options_:i}=ai.getPlayer()??{};!e.subtitles?.length&&i&&(e.subtitles=i.tracks.map(h=>({url:h.src,language:J(h.srclang),source:this.SUBTITLE_SOURCE,format:this.SUBTITLE_FORMAT})));const o=i?.courseId;if(!o)return e;let r="en";const s=await this.getCourseData(o);if(s){const{primaryLanguageCodes:[h]}=s;r=h?J(h):"en";}Lt.includes(r)||(r="en");const l=(e.subtitles.find(h=>h.language===r)??e.subtitles?.[0])?.url;l||E.warn("Failed to find any subtitle file");const{url:c,duration:u}=e,d=l?[{target:"subtitles_file_url",targetUrl:l},{target:"video_file_url",targetUrl:c}]:null;return {...l?{url:this.service?.url+t,translationHelp:d}:{url:c,translationHelp:d},detectedLanguage:r,duration:u}}async getVideoId(t){return (/learn\/([^/]+)\/lecture\/([^/]+)/.exec(t.pathname)??/lecture\/([^/]+)\/([^/]+)/.exec(t.pathname))?.[0]}}class Pl extends L{getVideoIdFromUrl(t){const e=t.searchParams.get("video");if(e)return e}resolveVideoIdViaPostMessage(){return new Promise(t=>{const e="https://www.dailymotion.com",i=setTimeout(()=>{window.removeEventListener("message",o),t(void 0);},3e3),o=r=>{r.origin===e&&typeof r.data=="string"&&r.data.startsWith("getVideoId:")&&(clearTimeout(i),window.removeEventListener("message",o),t(r.data.replace("getVideoId:","")));};window.addEventListener("message",o),window.top?.postMessage("getVideoId:",e);})}async getVideoId(t){return window.self!==window.top?await this.resolveVideoIdViaPostMessage():this.getVideoIdFromUrl(t)}}class Vl extends L{async getVideoData(t){if(!this.video)return;const e=this.video.querySelector('source[type="application/x-mpegurl"]')?.src;if(e)return {url:e}}async getVideoId(t){return /courses\/(([^/]+)\/lesson\/([^/]+)\/([^/]+))/.exec(t.pathname)?.[1]}}class Ze extends L{static getPlayer(){if(!(typeof player>"u"))return player}async getVideoData(t){const e=Ze.getPlayer();if(!e)return;const{config:{url:i,duration:o,lang:r,isLive:s}}=e;if(!i)return;const a=i.find(l=>l.src.includes("www.douyin.com/aweme/v1/play/"));if(a)return {url:ut(a.src),duration:o,isStream:s,...Lt.includes(r)?{detectedLanguage:r}:{}}}async getVideoId(t){const e=/video\/([\d]+)/.exec(t.pathname)?.[0];return e||Ze.getPlayer()?.config.vid}}class Ml extends L{async getVideoId(t){return /video\/watch\/([^/]+)/.exec(t.pathname)?.[1]}}class _l extends L{async getVideoId(t){return t.pathname.slice(1)}}class Ol extends L{API_ORIGIN="https://dev.epicgames.com/community/api/learning";async getPostInfo(t){try{return await(await this.fetch(`${this.API_ORIGIN}/post.json?hash_id=${t}`)).json()}catch(e){return E.error(`Failed to get epicgames post info by videoId: ${t}.`,e.message),false}}getVideoBlock(){const t=/videoUrl\s?=\s"([^"]+)"?/,e=Array.from(document.body.querySelectorAll("script")).find(s=>t.exec(s.innerHTML));if(!e)return;const i=e.innerHTML.trim(),o=t.exec(i)?.[1]?.replace("qsep://","https://");if(!o)return;let r=/sources\s?=\s(\[([^\]]+)\])?/.exec(i)?.[1];if(!r)return {playlistUrl:o,subtitles:[]};try{r=`${r.replace(/src:(\s)+?(videoUrl)/g,'src:"removed"').substring(0,r.lastIndexOf("},"))}]`.split(` -`).map(l=>l.replace(/([^\s]+):\s?(?!.*\1)/,'"$1":')).join(` -`);const a=JSON.parse(r).filter(l=>l.type==="captions");return {playlistUrl:o,subtitles:a}}catch{return {playlistUrl:o,subtitles:[]}}}async getVideoData(t){const e=t.split(":")?.[1],i=await this.getPostInfo(e);if(!i)return;const o=this.getVideoBlock();if(!o)return;const{playlistUrl:r,subtitles:s}=o,{title:a,description:l}=i,c=s.map(u=>({language:J(u.srclang),source:"epicgames",format:"vtt",url:u.src}));return {url:r,title:a,description:l,subtitles:c}}async getVideoId(t){return new Promise(e=>{const i="https://dev.epicgames.com",o=btoa(window.location.href);window.addEventListener("message",r=>{if(r.origin!==i||!(typeof r.data=="string"&&r.data.startsWith("getVideoId:")))return;const s=r.data.replace("getVideoId:","");return e(s)}),window.top?.postMessage(`getVideoId:${o}`,i);})}}class Dl extends L{async getVideoId(t){return /video-([^/]+)\/([^/]+)/.exec(t.pathname)?.[0]}}class Rl extends L{async getVideoId(t){return t.pathname.slice(1)}}class Bl extends L{getPlayerData(){return document.querySelector("#movie_player")?.getVideoData?.()??void 0}async getVideoId(t){return this.getPlayerData()?.video_id}}class Fl extends L{getVideoDataBySource(t){const e=document.querySelector('.icms.video > source[type="video/mp4"][data-quality="360"]')?.src;return e?{url:ut(e)}:this.returnBaseData(t)}getVideoDataByNext(t){try{const e=document.getElementById("__NEXT_DATA__")?.textContent;if(!e)throw new ge("Not found __NEXT_DATA__ content");const i=JSON.parse(e),{props:{pageProps:{page:{description:o,title:r,video:{videoMetadata:{duration:s},assets:a}}}}}=i,l=a.find(c=>c.height===360&&c.url.includes(".mp4"))?.url;if(!l)throw new ge("Not found video URL in assets");return {url:ut(l),duration:s,title:r,description:o}}catch(e){return E.warn(`Failed to get ign video data by video ID: ${t}, because ${e.message}. Using clear link instead...`),this.returnBaseData(t)}}async getVideoData(t){return document.getElementById("__NEXT_DATA__")?this.getVideoDataByNext(t):this.getVideoDataBySource(t)}async getVideoId(t){return /([^/]+)\/([\d]+)\/video\/([^/]+)/.exec(t.pathname)?.[0]??/\/videos\/([^/]+)/.exec(t.pathname)?.[0]}}class Nl extends L{async getVideoId(t){return /video\/([^/]+)/.exec(t.pathname)?.[1]}}class $l extends L{async getVideoData(t){try{const e=document.querySelector("#incflix-stream source:first-of-type");if(!e)throw new _("Failed to find source element");const i=e.getAttribute("src");if(!i)throw new _("Failed to find source link");const o=new URL(i.startsWith("//")?`https:${i}`:i);return o.searchParams.append("media-proxy","video.mp4"),{url:ut(o)}}catch(e){E.error(`Failed to get Incestflix data by videoId: ${t}`,e.message);return}}async getVideoId(t){return /\/watch\/([^/]+)/.exec(t.pathname)?.[1]}}class Hl extends L{API_ORIGIN="https://kick.com/api";async getClipInfo(t){try{const i=await(await this.fetch(`${this.API_ORIGIN}/v2/clips/${t}`)).json(),{clip_url:o,duration:r,title:s}=i.clip;return {url:o,duration:r,title:s}}catch(e){E.error(`Failed to get kick clip info by clipId: ${t}.`,e.message);return}}async getVideoInfo(t){try{const i=await(await this.fetch(`${this.API_ORIGIN}/v1/video/${t}`)).json(),{source:o,livestream:r}=i,{session_title:s,duration:a}=r;return {url:o,duration:Math.round(a/1e3),title:s}}catch(e){E.error(`Failed to get kick video info by videoId: ${t}.`,e.message);return}}async getVideoData(t){return t.startsWith("videos")?await this.getVideoInfo(t.replace("videos/","")):await this.getClipInfo(t.replace("clips/",""))}async getVideoId(t){return /([^/]+)\/((videos|clips)\/([^/]+))/.exec(t.pathname)?.[2]}}class Ul extends L{async getVideoData(t){try{const e=document.querySelector(".ksr-video-player > video"),i=e?.querySelector("source[type^='video/mp4']")?.src;if(!i)throw new _("Failed to find video URL");const o=e?.querySelectorAll("track")??[];return {url:i,subtitles:Array.from(o).reduce((r,s)=>{const a=s.getAttribute("srclang"),l=s.getAttribute("src");return !a||!l||r.push({language:J(a),url:l,format:"vtt",source:"kickstarter"}),r},[])}}catch(e){E.error(`Failed to get Kickstarter data by videoId: ${t}`,e.message);return}}async getVideoId(t){return t.pathname.slice(1)}}class zl extends L{API_ORIGIN=window.location.origin;getSecureData(t){try{const[e,i,o]=t.split("/").filter(g=>g),r=Array.from(document.getElementsByTagName("script")),s=r.filter(g=>g.innerHTML.includes(`videoId = "${i}"`)||g.innerHTML.includes(`serialId = Number(${i})`));if(!s.length)throw new _("Failed to find secure script");const a=s[0]?.textContent?.trim();if(!a)throw new _("Secure script content is empty");const l=/'{[^']+}'/.exec(a)?.[0];if(!l)throw new _("Secure json wasn't found in secure script");const c=JSON.parse(l.replaceAll("'",""));if(e!=="serial")return {videoType:e,videoId:i,hash:o,...c};const u=r.find(g=>g.innerHTML.includes("var videoInfo = {}"))?.textContent?.trim();if(!u)throw new _("Failed to find videoInfo content");const d=/videoInfo\.type\s+?=\s+?'([^']+)'/.exec(u)?.[1],h=/videoInfo\.id\s+?=\s+?'([^']+)'/.exec(u)?.[1],f=/videoInfo\.hash\s+?=\s+?'([^']+)'/.exec(u)?.[1];if(!d||!h||!f)throw new _("Failed to parse videoInfo content");return {videoType:d,videoId:h,hash:f,...c}}catch(e){return E.error(`Failed to get kodik secure data by videoPath: ${t}.`,e.message),false}}async getFtor(t){const{videoType:e,videoId:i,hash:o,d:r,d_sign:s,pd:a,pd_sign:l,ref:c,ref_sign:u}=t;try{return await(await this.fetch(`${this.API_ORIGIN}/ftor`,{method:"POST",headers:{"User-Agent":Y.userAgent,Origin:this.API_ORIGIN,Referer:`${this.API_ORIGIN}/${e}/${i}/${o}/360p`},body:new URLSearchParams({d:r,d_sign:s,pd:a,pd_sign:l,ref:decodeURIComponent(c),ref_sign:u,bad_user:"false",cdn_is_working:"true",info:"{}",type:e,hash:o,id:i})})).json()}catch(d){return E.error(`Failed to get kodik video data (type: ${e}, id: ${i}, hash: ${o})`,d.message),false}}decryptUrl(t){return `https:${atob(t.replace(/[a-zA-Z]/g,i=>{const o=i.charCodeAt(0)+18,r=i<="Z"?90:122;return String.fromCharCode(r>=o?o:o-26)}))}`}async getVideoData(t){const e=this.getSecureData(t);if(!e)return;const i=await this.getFtor(e);if(!i)return;const r=Object.entries(i.links[i.default.toString()]).find(([,s])=>s.type==="application/x-mpegURL")?.[1];if(r)return {url:r.src.startsWith("//")?`https:${r.src}`:this.decryptUrl(r.src)}}async getVideoId(t){return /\/(uv|video|seria|episode|season|serial)\/([^/]+)\/([^/]+)\/([\d]+)p/.exec(t.pathname)?.[0]}}class Wl extends Rt{SUBTITLE_SOURCE="linkedin";async getVideoData(t){const e=this.getVideoDataByPlayer(t);if(!e)return;const{url:i,duration:o,subtitles:r}=e;return {url:ut(new URL(i)),duration:o,subtitles:r}}async getVideoId(t){return /\/learning\/(([^/]+)\/([^/]+))/.exec(t.pathname)?.[1]}}var lo;(function(n){n.Channel="Channel",n.Video="Video";})(lo||(lo={}));class ql extends L{getClientVersion(){if(!(typeof SENTRY_RELEASE>"u"))return SENTRY_RELEASE.id}async getVideoData(t){try{const e=this.getClientVersion();if(!e)throw new _("Failed to get client version");const i=await this.fetch("https://www.loom.com/graphql",{headers:{"User-Agent":Y.userAgent,"content-type":"application/json","x-loom-request-source":`loom_web_${e}`,"apollographql-client-name":"web","apollographql-client-version":e,"Alt-Used":"www.loom.com"},body:`{"operationName":"FetchCaptions","variables":{"videoId":"${t}"},"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(i.status!==200)throw new _("Failed to get data from graphql");const r=(await i.json()).data.fetchVideoTranscript;if(r.__typename==="GenericError")throw new _(r.message);return {url:this.service?.url+t,subtitles:[{format:"vtt",language:J(r.language),source:"loom",url:r.captions_source_url}]}}catch(e){return E.error(`Failed to get Loom video data, because: ${e.message}`),this.returnBaseData(t)}}async getVideoId(t){return /(embed|share)\/([^/]+)?/.exec(t.pathname)?.[2]}}class Gl extends L{API_ORIGIN="https://my.mail.ru";async getVideoMeta(t){try{return await(await this.fetch(`${this.API_ORIGIN}/+/video/meta/${t}?xemail=&ajax_call=1&func_name=&mna=&mnb=&ext=1&_=${Date.now()}`)).json()}catch(e){E.error("Failed to get mail.ru video data",e.message);return}}async getVideoId(t){const e=t.pathname;if(/\/(v|mail|bk|inbox)\//.exec(e))return e.slice(1);const i=/video\/embed\/([^/]+)/.exec(e)?.[1];if(!i)return;const o=await this.getVideoMeta(i);if(o)return o.meta.url.replace("//my.mail.ru/","")}}class Kl extends Rt{SUBTITLE_SOURCE="netacad";async getVideoData(t){const e=this.getVideoDataByPlayer(t);if(!e)return;const{url:i,duration:o,subtitles:r}=e;return {url:ut(new URL(i)),duration:o,subtitles:r}}async getVideoId(t){return t.pathname+t.search}}class Yl extends L{async getVideoId(t){return /([^/]+)\/(view)\/([^/]+)/.exec(t.pathname)?.[0]}}class Xl extends L{async getVideoId(t){return t.hostname==="nico.ms"?t.pathname.replace(/^\//,"").split("/")[0]||void 0:/\/watch\/([^/?#]+)/.exec(t.pathname)?.[1]}}class Jl extends L{async getVideoData(t){const e=this.returnBaseData(t);if(!e)return e;try{if(!this.video)throw new Error("Video element not found");const i=this.video.querySelector('source[type^="video/mp4"], source[type^="video/webm"]')?.src;if(!i||!/^https?:\/\//.test(i))throw new Error("Video source not found");return {...e,translationHelp:[{target:"video_file_url",targetUrl:i}]}}catch{return e}}async getVideoId(t){return /gag\/([^/]+)/.exec(t.pathname)?.[1]}}class jl extends L{API_ORIGIN="https://odysee.com";async getVideoData(t){try{const i=await(await this.fetch(`${this.API_ORIGIN}/${t}`)).text(),o=/"contentUrl":(\s)?"([^"]+)"/.exec(i)?.[2];if(!o)throw new _("Odysee url doesn't parsed");return {url:o}}catch(e){E.error(`Failed to get odysee video data by video ID: ${t}`,e.message);return}}async getVideoId(t){return t.pathname.slice(1)}}class Zl extends L{async getVideoId(t){return /\/video\/(\d+)/.exec(t.pathname)?.[1]}}class Ql extends L{async getVideoId(t){return /\/([a-z]{2}\/(?:[a-z0-9-]+\/)?(?:replay|videos?|original-series\/episode)\/[\w-]+)\/?$/i.exec(t.pathname)?.[1]}}class tc extends Rt{SUBTITLE_SOURCE="oraclelearn";async getVideoData(t){const e=this.getVideoDataByPlayer(t);if(!e)return;const{url:i,duration:o,subtitles:r}=e,s=this.returnBaseData(t),a=ut(new URL(i));return s?{url:s.url,duration:o,subtitles:r,translationHelp:[{target:"video_file_url",targetUrl:a}]}:{url:a,duration:o,subtitles:r}}async getVideoId(t){return /\/ou\/course\/(([^/]+)\/(\d+)\/(\d+))/.exec(t.pathname)?.[1]}}class ec extends L{API_ORIGIN="https://www.patreon.com/api";async getPosts(t){try{return await(await this.fetch(`${this.API_ORIGIN}/posts/${t}?json-api-use-default-includes=false`)).json()}catch(e){return E.error(`Failed to get patreon posts by postId: ${t}.`,e.message),false}}async getVideoData(t){const e=await this.getPosts(t);if(!e)return;const i=e.data.attributes.post_file.url;if(i)return {url:i}}async getVideoId(t){const e=/posts\/([^/]+)/.exec(t.pathname)?.[1];if(e)return e.replace(/[^\d.]/g,"")}}class nc extends L{async getVideoId(t){const e=t.pathname.replace(/\/+$/,""),i=/\/videos\/watch\/([^/]+)/.exec(e)?.[1];if(i)return `/videos/watch/${i}`;const o=/\/w\/([^/]+)/.exec(e)?.[1];if(o)return `/videos/watch/${o}`}}class ic extends L{async getVideoId(t){return /\/((?:videopopout|[^/]+(?:\/profile)?\/videos)\/[^/?#&/]+)\/?$/.exec(t.pathname)?.[1]??/^\/([^/#?]+)\/?$/.exec(t.pathname)?.[1]}}class oc extends L{async getVideoId(t){return t.searchParams.get("viewkey")??/embed\/([^/]+)/.exec(t.pathname)?.[1]}}class rc extends L{async getVideoData(t){try{if(typeof flashvars>"u")return;const{rnd:e,video_url:i,video_title:o}=flashvars;if(!i||!e)throw new _("Failed to find video source or rnd");const r=new URL(i);r.searchParams.append("rnd",e),E.log("PornTN get_file link",r.href);const s=await this.fetch(r.href,{method:"head"}),a=new URL(s.url);return E.log("PornTN cdn link",a.href),{url:ut(a),title:o}}catch(e){E.error(`Failed to get PornTN data by videoId: ${t}`,e.message);return}}async getVideoId(t){return /\/videos\/(([^/]+)\/([^/]+))/.exec(t.pathname)?.[1]}}class sc extends L{API_ORIGIN="https://www.reddit.com";async getContentUrl(t){if(this.service?.additionalData!=="old"){const i=document.querySelector("shreddit-player-2, shreddit-player");return (i?.getAttribute("src")??i?.querySelector('source[type="application/vnd.apple.mpegURL"]')?.getAttribute("src"))?.replaceAll("&","&")}return document.querySelector("[data-hls-url]")?.dataset.hlsUrl?.replaceAll("&","&")}async getVideoData(t){try{const e=await this.getContentUrl(t);if(!e)throw new _("Failed to find content url");return {url:decodeURIComponent(e)}}catch(e){E.error(`Failed to get reddit video data by video ID: ${t}`,e.message);return}}async getVideoId(t){return /\/r\/(([^/]+)\/([^/]+)\/([^/]+)\/([^/]+))/.exec(t.pathname)?.[1]}}class ac extends L{async getVideoData(t){const e=document.querySelector(".jw-video, .media__video_noscript");if(!e)return;let i=e.getAttribute("src");if(i)return i.endsWith(".MP4")&&(i=ut(i)),{videoId:t,url:i}}async getVideoId(t){return t.pathname.slice(1)}}class lc extends L{async getVideoId(t){const e=/\/videos?\/(\d+)(?:\/(.+))?\/?$/.exec(t.pathname);if(!e)return;const[,i,o]=e;return o?`${i}/${o.replace(/\/+$/,"")}/`:i}}class cc extends L{async getVideoId(t){return t.pathname.slice(1)}}class uc extends L{async getVideoId(t){return /(?:video|embed)\/([^/]+)/.exec(t.pathname)?.[1]}}class dc extends L{API_ORIGIN="https://learning.sap.com/";async requestKaltura(t,e,i){const o="html5:v3.17.22",r="3.3.0";try{return await(await this.fetch(`https://${t}/api_v3/service/multirequest`,{method:"POST",body:JSON.stringify({1:{service:"session",action:"startWidgetSession",widgetId:`_${e}`},2:{service:"baseEntry",action:"list",ks:"{1:result:ks}",filter:{redirectFromEntryId:i},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:r,format:1,ks:"",clientTag:o,partnerId:e}),headers:{"Content-Type":"application/json"}})).json()}catch(s){E.error("Failed to request kaltura data",s.message);return}}async getKalturaData(t){try{const e=document.querySelector('script[data-nscript="beforeInteractive"]');if(!e)throw new _("Failed to find script element");const i=/https:\/\/([^"]+)\/p\/([^"]+)\/embedPlaykitJs\/uiconf_id\/([^"]+)/.exec(e?.src);if(!i)throw new _(`Failed to get sap data for videoId: ${t}`);const[,o,r]=i;let s=document.querySelector("#shadow")?.firstChild?.getAttribute("id");if(!s){const a=document.querySelector("#__NEXT_DATA__");if(!a)throw new _("Failed to find next data element");s=/"sourceId":\s?"([^"]+)"/.exec(a.innerText)?.[1];}if(!o||Number.isNaN(+r)||!s)throw new _(`One of the necessary parameters for getting a link to a sap video in wasn't found for ${t}. Params: kalturaDomain = ${o}, partnerId = ${r}, entryId = ${s}`);return await this.requestKaltura(o,r,s)}catch(e){E.error("Failed to get kaltura data",e.message);return}}async getVideoData(t){const e=await this.getKalturaData(t);if(!e)return;const[,i,o]=e,{duration:r}=i.objects[0],s=o.sources.find(l=>l.format==="url"&&l.protocols==="http,https"&&l.url.includes(".mp4"))?.url;if(!s)return;const a=o.playbackCaptions.map(l=>({language:J(l.languageCode),source:"sap",format:"vtt",url:l.webVttUrl,isAutoGenerated:l.label.includes("auto-generated")}));return {url:s,subtitles:a,duration:r}}async getVideoId(t){return /((courses|learning-journeys)\/([^/]+)(\/[^/]+)?)/.exec(t.pathname)?.[1]}}class hc extends L{async getVideoId(t){return /\/([\da-z]+\/(?:video|play|embed)(?:\/[^/]+)?)\/?$/i.exec(t.pathname)?.[1]??/\/([\da-z]+-[\da-z]+\/playlist\/[^/]+)\/?$/i.exec(t.pathname)?.[1]}}class li extends L{static getMediaViewer(){if(!(typeof appMediaViewer>"u"))return appMediaViewer}async getVideoId(t){const e=li.getMediaViewer();if(!e||e.live)return;const i=e.target.message;if(i.peer_id._!=="peerChannel")return;const o=i.media;if(o._!=="messageMediaDocument"||o.document.type!=="video")return;const r=i.mid&4294967295;return `${await e.managers.appPeersManager.getPeerUsername(i.peerId)}/${r}`}}class fc extends L{async getVideoId(t){return /(videos|embed)\/[^/]+/.exec(t.pathname)?.[0]}}class co extends L{async getVideoId(t){return /([^/]+)\/video\/([^/]+)/.exec(t.pathname)?.[0]}}class pc extends L{async getVideoId(t){const e=t.searchParams.get("vid"),i=/([^/]+)\/([\d]+)/.exec(t.pathname)?.[0];if(!(!e||!i))return `${i}?vid=${e}`}}class gc extends L{API_ORIGIN="https://clips.twitch.tv";async getClipLink(t,e){const i=document.querySelector("script[type='application/ld+json']"),o=t.slice(1);if(i){const c=JSON.parse(i.innerText)["@graph"].find(d=>d["@type"]==="VideoObject")?.creator.url;if(!c)throw new _("Failed to find channel link");return `${c.replace("https://www.twitch.tv/","")}/clip/${o}`}const r=o==="embed",s=document.querySelector(r?".tw-link[data-test-selector='stream-info-card-component__stream-avatar-link']":".clips-player a:not([class])");return s?`${s.href.replace("https://www.twitch.tv/","")}/clip/${r?e:o}`:void 0}async getVideoData(t){const e=document.querySelector('[data-a-target="stream-title"], [data-test-selector="stream-info-card-component__subtitle"]')?.innerText,i=!!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+t,isStream:i,title:e}}async getVideoId(t){const e=t.pathname;if(/^m\.twitch\.tv$/.test(e))return /videos\/([^/]+)/.exec(t.href)?.[0]??e.slice(1);if(/^player\.twitch\.tv$/.test(t.hostname))return `videos/${t.searchParams.get("video")}`;const i=/([^/]+)\/(?:clip)\/([^/]+)/.exec(e);if(i)return i[0];if(/^clips\.twitch\.tv$/.test(t.hostname))return await this.getClipLink(e,t.searchParams.get("clip"));const r=/(?:videos)\/([^/]+)/.exec(e);if(r)return r[0];const s=document.querySelector(".home-offline-hero .tw-link");if(s?.href){const a=new URL(s.href);return /(?:videos)\/([^/]+)/.exec(a.pathname)?.[0]}return document.querySelector(".persistent-player")?e:void 0}}class mc extends L{async getVideoId(t){const e=/status\/([^/]+)/.exec(t.pathname)?.[1];if(e)return e;const o=this.video?.closest('[data-testid="tweet"]')?.querySelector('a[role="link"][aria-label]')?.href;return o?/status\/([^/]+)/.exec(o)?.[1]:void 0}}function Gn(n){return typeof n=="object"&&n!==null}function uo(n){return typeof n=="object"&&n!==null}function Ln(n){if(Array.isArray(n))return n.filter(uo);if(typeof n!="object"||n===null)return [];const t=n;return (Array.isArray(t.Video)?t.Video:Array.isArray(t.video)?t.video:[]).filter(uo)}function vc(n){if(!Gn(n))return [];const t=[];for(const[e,i]of Object.entries(n))!Gn(i)||typeof i.url!="string"||t.push({src:i.url,type:typeof i.type=="string"?i.type:void 0,label:typeof i.height=="number"||typeof i.height=="string"?i.height:e});return t}function bc(n){if(typeof n=="number"&&Number.isFinite(n))return n;const t=String(n??"").match(/(\d{3,4})/);return Number(t?.[1]??0)}function yc(n){if(typeof n.file=="string")return n.file;if(typeof n.src=="string")return n.src}function wc(n,t){return n.includes("mpegurl")||/\.m3u8(?:$|[?#])/i.test(t)}function Sc(n,t){return n.includes("dash")||/\.mpd(?:$|[?#])/i.test(t)}class xc extends L{API_ORIGIN=`${window.location.origin}/api-2.0`;getModuleData(){const e=(document.querySelector(".ud-app-loader[data-module-id='course-taking']")??document.querySelector("[data-module-id='course-taking']"))?.dataset?.moduleArgs;if(e)try{return JSON.parse(e)}catch{return}}getLectureId(t){const e=/(?:\/learn\/(?:v4\/t\/)?lecture\/|#\/?lecture\/|\/lecture\/view\/\?(?:[^#]*?&)*lecture(?:_|)id=)(\d+)/i;return e.exec(window.location.href)?.[1]??(t?e.exec(`/${t}`)?.[1]:void 0)}getCourseId(t){const e=t,i=this.normalizeId(e?.courseId??e?.course_id??e?.course?.id);if(i)return i;const o=this.normalizeId(document.querySelector("[data-course-id]")?.getAttribute("data-course-id"));if(o)return o;const r=document.documentElement?.innerHTML??"";return /data-course-id=["'](\d+)/i.exec(r)?.[1]??/"courseId"\s*:\s*(\d+)/i.exec(r)?.[1]??/"courseId"\s*:\s*(\d+)/i.exec(r)?.[1]}normalizeId(t){if(typeof t=="number"&&Number.isFinite(t))return String(t);if(typeof t=="string")return /^\d+$/.test(t)?t:void 0}parseJson(t){try{return JSON.parse(t)}catch{const e=t.replaceAll(""",'"').replaceAll(""",'"').replaceAll("'","'").replaceAll("'","'");try{return JSON.parse(e)}catch{return}}}getViewHtmlCandidates(t){if(typeof t!="string"||!t.trim())return [];const e=new DOMParser().parseFromString(t,"text/html"),i=[];for(const o of Array.from(e.querySelectorAll("source"))){const r=o.getAttribute("src");r&&i.push({src:r,type:o.getAttribute("type")??void 0,label:o.getAttribute("data-res")??void 0});}for(const o of Array.from(e.querySelectorAll("[videojs-setup-data]"))){const r=o.getAttribute("videojs-setup-data");if(!r)continue;const s=this.parseJson(r);s&&i.push(...Ln(s.sources));}return i}isErrorData(t){return Object.hasOwn(t,"error")||Object.hasOwn(t,"detail")&&!Object.hasOwn(t,"_class")}async getLectureData(t,e){try{const o=await(await this.fetch(`${this.API_ORIGIN}/users/me/subscribed-courses/${t}/lectures/${e}/?`+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(o))throw new _(o.detail??"unknown error");return o}catch(i){E.error(`Failed to get lecture data by courseId: ${t} and lectureId: ${e}`,i.message);return}}async getCourseLang(t){try{const i=await(await this.fetch(`${this.API_ORIGIN}/users/me/subscribed-courses/${t}?`+new URLSearchParams({"fields[course]":"locale"}).toString())).json();if(!this.isErrorData(i))return i;const r=await(await this.fetch(`${this.API_ORIGIN}/courses/${t}/?`+new URLSearchParams({"fields[course]":"locale"}).toString())).json();if(this.isErrorData(r))throw new _(r.detail??"unknown error");return r}catch(e){E.error(`Failed to get course lang by courseId: ${t}`,e.message);return}}findVideoUrl(t,e,i,o,r,s,a){const l=[],c=Array.isArray(t)?t:[];for(const x of c)l.push({src:x.src,type:x.type,label:x.label});l.push(...Ln(e)),l.push(...Ln(i)),l.push(...vc(s)),typeof a=="string"&&l.push(...this.getViewHtmlCandidates(a)),typeof o=="string"&&l.push({src:o}),typeof r=="string"&&l.push({src:r});const u=this.video?.currentSrc||this.video?.src;typeof u=="string"&&u&&l.push({src:u});const d=new Map;for(const x of l){const m=yc(x);if(!m||/^javascript:/i.test(m))continue;const k=bc(x.label??x.quality??x.height),P=String(x.type??"").toLowerCase(),D=d.get(m);(!D||k>D.quality)&&d.set(m,{url:m,type:P,quality:k,isYouTubeWatch:/:\/\/(?:www\.)?youtube\.com\/watch\?/i.test(m)});}const h=Array.from(d.values());if(!h.length)return;const f=h.filter(x=>x.type.includes("mp4")||/\.mp4(?:$|[?#])/i.test(x.url));if(f.length)return f.sort((x,m)=>m.quality-x.quality),f[0]?.url;const g=h.find(x=>wc(x.type,x.url))?.url;if(g)return g;const y=h.find(x=>Sc(x.type,x.url))?.url;if(y)return y;const w=h.find(x=>!x.isYouTubeWatch)?.url;return w||h[0]?.url}getCaptionLocale(t){const e=typeof t.locale_id=="string"?t.locale_id:typeof t.locale?.locale=="string"?t.locale.locale:void 0;return e?J(e):void 0}findSubtitleUrl(t,e){if(!Array.isArray(t))return;const i=t.filter(r=>Gn(r)&&(typeof r.url=="string"||typeof r.download_url=="string")),o=i.find(r=>this.getCaptionLocale(r)===e)??i.find(r=>this.getCaptionLocale(r)==="en")??i[0];return o?.url??o?.download_url}async getVideoData(t){const e=this.getModuleData(),i=this.getCourseId(e),o=this.getLectureId(t);if(E.log(`[Udemy] courseId: ${i}, lectureId: ${o}`),!o||!i)return;const r=await this.getLectureData(i,o);if(!r)return;const{title:s,description:a,asset:l,view_html:c}=r,{length:u,media_sources:d,captions:h}=l,f=l,g=f.stream_urls,y=f.download_urls,w=this.findVideoUrl(d,g,y,f.stream_url??f.streamUrl,f.external_url,f.data?.outputs,c);if(!w){E.log("Failed to find video file in asset sources",l);return}let x="en";const k=(await this.getCourseLang(i))?.locale?.locale;typeof k=="string"&&(x=J(k)),Lt.includes(x)||(x="en");const P=this.findSubtitleUrl(h,x);return P||E.log("Failed to find subtitle file in captions",h),{...P?{url:this.service?.url+t,translationHelp:[{target:"subtitles_file_url",targetUrl:P},{target:"video_file_url",targetUrl:w}],detectedLanguage:x}:{url:w,translationHelp:null},duration:u,title:s,description:a}}async getVideoId(t){return t.pathname.slice(1)}}class kc extends L{API_KEY="";DEFAULT_SITE_ORIGIN="https://vimeo.com";SITE_ORIGIN=this.service?.url?.slice(0,-1)??this.DEFAULT_SITE_ORIGIN;isErrorData(t){return Object.hasOwn(t,"error")}isPrivatePlayer(){return this.referer&&!this.referer.includes("vimeo.com")&&this.origin.endsWith("player.vimeo.com")}toPublicUrl(t){const[e,i]=t.split(":",2);return i?`${this.DEFAULT_SITE_ORIGIN}/${e}/${i}`:`${this.DEFAULT_SITE_ORIGIN}/${e}`}returnPublicBaseData(t){const e=this.returnBaseData(t);if(e)return {...e,url:this.toPublicUrl(t)}}normalizePublicVideoUrl(t,e){try{const i=new URL(t);if(i.hostname==="player.vimeo.com")return this.toPublicUrl(e);if(i.hostname.endsWith("vimeo.com")){const o=/^\/(\d+):([a-z0-9]+)$/i.exec(i.pathname);if(o)return `${this.DEFAULT_SITE_ORIGIN}/${o[1]}/${o[2]}`}}catch{}return t}async getViewerData(){try{const e=await(await this.fetch("https://vimeo.com/_next/viewer")).json(),{apiUrl:i,jwt:o}=e;return this.API_ORIGIN=`https://${i}`,this.API_KEY=`jwt ${o}`,e}catch(t){return E.error("Failed to get default viewer data.",t.message),false}}async getVideoInfo(t){try{const e=new URLSearchParams({fields:"name,link,description,duration"}).toString(),o=await(await this.fetch(`${this.API_ORIGIN}/videos/${t}?${e}`,{headers:{Authorization:this.API_KEY}})).json();if(this.isErrorData(o))throw new Error(o.developer_message??o.error);return o}catch(e){return E.error(`Failed to get video info by video ID: ${t}`,e.message),false}}async getPrivateVideoSource(t){try{const{default_cdn:e,cdns:i}=t.dash,o=i[e].url,r=await this.fetch(o);if(r.status!==200)throw new _(await r.text());const s=await r.json(),a=new URL(s.base_url,o),l=s.audio.find(f=>f.mime_type==="audio/mp4"&&f.format==="dash");if(!l)throw new _("Failed to find video data");const c=l.segments?.[0]?.url;if(!c)throw new _("Failed to find first segment url");const[u,d]=c.split("?",2),h=new URLSearchParams(d);return h.delete("range"),new URL(`${l.base_url}${u}?${h.toString()}`,a).href}catch(e){return E.error("Failed to get private video source",e.message),false}}async getPrivateVideoInfo(t){try{if(typeof playerConfig>"u")return;const e=await this.getPrivateVideoSource(playerConfig.request.files);if(!e)throw new _("Failed to get private video source");const{video:{title:i,duration:o},request:{text_tracks:r}}=playerConfig;return {url:`${this.SITE_ORIGIN}/${t}`,video_url:e,title:i,duration:o,subs:r}}catch(e){return E.error(`Failed to get private video info by video ID: ${t}`,e.message),false}}async getSubsInfo(t){try{const e=new URLSearchParams({per_page:"100",fields:"language,type,link"}).toString(),o=await(await this.fetch(`${this.API_ORIGIN}/videos/${t}/texttracks?${e}`,{headers:{Authorization:this.API_KEY}})).json();if(this.isErrorData(o))throw new Error(o.developer_message??o.error);return o.data}catch(e){return E.error(`Failed to get subtitles info by video ID: ${t}`,e.message),[]}}async getVideoData(t){if(this.isPrivatePlayer()){const h=await this.getPrivateVideoInfo(t);if(!h)return;const{url:f,subs:g,video_url:y,title:w,duration:x}=h,m=g.map(P=>({language:J(P.lang),source:"vimeo",format:"vtt",url:new URL(P.url,this.SITE_ORIGIN).href,isAutoGenerated:P.lang.includes("autogenerated")})),k=m.length?[{target:"video_file_url",targetUrl:y},{target:"subtitles_file_url",targetUrl:m[0].url}]:null;return {...k?{url:f,translationHelp:k}:{url:y},subtitles:m,title:w,duration:x}}if(!this.extraInfo)return this.returnPublicBaseData(t);if(t.includes("/")&&(t=t.replace("/",":")),!await this.getViewerData())return this.returnPublicBaseData(t);const o=await this.getVideoInfo(t);if(!o)return this.returnPublicBaseData(t);const s=(await this.getSubsInfo(t)).map(h=>({language:J(h.language),source:"vimeo",format:"vtt",url:h.link,isAutoGenerated:h.language.includes("autogen")})),{link:a,duration:l,name:c,description:u}=o;return {url:this.normalizePublicVideoUrl(a,t),title:c,description:u,subtitles:s,duration:l}}async getVideoId(t){const e=t.pathname.replace(/\/+$/,""),i=/video\/[^/]+$/.exec(e)?.[0];if(this.isPrivatePlayer())return i;if(i){const r=t.searchParams.get("h"),s=i.replace("video/","");return r?`${s}/${r}`:s}const o=/channels\/[^/]+\/([^/]+)/.exec(e)?.[1]??/groups\/[^/]+\/videos\/([^/]+)/.exec(e)?.[1]??/(showcase|album)\/[^/]+\/video\/([^/]+)/.exec(e)?.[2];return o||/([^/]+\/)?[^/]+$/.exec(e)?.[0]}}class ci extends L{static getPlayer(){if(!(typeof Videoview>"u"))try{return Videoview?.getPlayerObject?.()}catch{return}}async getVideoData(t){const e=new URL(window.location.href),i=ci.getPlayer();if(!i){const o=this.returnBaseData(t);return o&&{...o,url:Tn(t,e)}}try{const{description:o,duration:r,md_title:s}=i.vars,l=new DOMParser().parseFromString(o,"text/html"),c=Array.from(l.body.childNodes).filter(d=>d.nodeName!=="BR").map(d=>d.textContent).join(` -`);let u;return Object.hasOwn(i.vars,"subs")&&(u=i.vars.subs.map(d=>({language:J(d.lang),source:"vk",format:"vtt",url:d.url,isAutoGenerated:!!d.is_auto}))),{url:Tn(t,e),title:s,description:c,duration:r,subtitles:u}}catch(o){E.error(`Failed to get VK video data, because: ${o.message}`);const r=this.returnBaseData(t);return r&&{...r,url:Tn(t,e)}}}async getVideoId(t){const e=/^\/((?:video|clip)-?\d+_\d+)(?:\/)?$/.exec(t.pathname);if(e)return e[1];const i=/\/playlist\/[^/]+\/(video-?\d+_\d+)/.exec(t.pathname);if(i)return i[1];const o=t.searchParams.get("z");if(o)return o.split("/")[0];const r=t.searchParams.get("oid"),s=t.searchParams.get("id");if(r&&s){const a=Math.abs(Number.parseInt(r,10));if(!Number.isNaN(a))return `video-${a}_${s}`}}}class Tc extends L{async getVideoId(t){return /(video|embed)\/(\d+)(\/[^/]+\/)?/.exec(t.pathname)?.[0]}}const Lc=/^\d+:(?:[\da-f]{32}|\d{16,})$/i,Ac=/^[A-Za-z0-9]+$/,Ic=/^(?:www\.)?weibo\.com$/,Cc=/^\/newlogin\/?$/;class Ec extends L{async getVideoId(t){if(t.hostname==="video.weibo.com"){const i=t.searchParams.get("fid");return !i||!Lc.test(i)?void 0:`tv/show/${i}`}if(Ic.test(t.host)&&Cc.test(t.pathname)){const i=t.searchParams.get("url");if(i)try{const r=new URL(i,t.origin);if(r.href!==t.href){const s=await this.getVideoId(r);if(s)return s}}catch{}const o=t.searchParams.get("layerid");if(o&&Ac.test(o))return `0/${o}`}const e=t.pathname.replace(/\/+$/,"");if(/^\/\d+\/[A-Za-z0-9]+$/.test(e)||/^\/0\/[A-Za-z0-9]+$/.test(e)||/^\/tv\/show\/\d+:(?:[\da-f]{32}|\d{16,})$/i.test(e))return e.slice(1)}}class Pc extends L{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(t){const e=Date.now(),i=t.substring(0,Math.min(255,t.length))+e,o=await Ga(this.API_HMAC_KEY,i);if(!o)throw new _("Failed to get weverse HMAC signature");return {wmsgpad:e.toString(),wmd:o}}async getHashURLParams(t){const e=await this.createHash(t);return new URLSearchParams(e).toString()}async getPostPreview(t){const e=`/post/v1.0/post-${t}/preview?`+new URLSearchParams({fieldSet:"postForPreview",...this.getURLData()}).toString();try{const i=await this.getHashURLParams(e);return await(await this.fetch(`${this.API_ORIGIN+e}&${i}`,{headers:this.HEADERS})).json()}catch(i){return E.error(`Failed to get weverse post preview by postId: ${t}`,i.message),false}}async getVideoInKey(t){const e=`/video/v1.1/vod/${t}/inKey?`+new URLSearchParams({gcc:"RU",...this.getURLData()}).toString();try{const i=await this.getHashURLParams(e);return await(await this.fetch(`${this.API_ORIGIN+e}&${i}`,{method:"POST",headers:this.HEADERS})).json()}catch(i){return E.error(`Failed to get weverse InKey by videoId: ${t}`,i.message),false}}async getVideoInfo(t,e,i){const o=Date.now();try{const r=new URLSearchParams({key:e,sid:i,nonce:o.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/${t}?`+r,{headers:this.HEADERS})).json()}catch(r){return E.error(`Failed to get weverse video info (infraVideoId: ${t}, inkey: ${e}, serviceId: ${i}`,r.message),false}}extractVideoInfo(t){return t.find(e=>e.useP2P===false&&e.source.includes(".mp4"))}async getVideoData(t){const e=await this.getPostPreview(t);if(!e)return;const{videoId:i,serviceId:o,infraVideoId:r}=e.extension.video;if(!(i&&o&&r))return;const s=await this.getVideoInKey(i);if(!s)return;const a=await this.getVideoInfo(r,s.inKey,o);if(!a)return;const l=this.extractVideoInfo(a.videos.list);if(l)return {url:l.source,duration:l.duration}}async getVideoId(t){return /([^/]+)\/(live|media)\/([^/]+)/.exec(t.pathname)?.[3]}}class Vc extends L{async getVideoId(t){return /\/(videos\/[^/]+-[\dA-Za-z]+)\/?$/.exec(t.pathname)?.[1]}}class Mc extends L{async getVideoId(t){return /[^/]+\/[^/]+$/.exec(t.pathname)?.[0]}}class _c extends L{API_ORIGIN=window.location.origin;CLIENT_PREFIX="/client/disk";INLINE_PREFIX="/i/";DISK_PREFIX="/d/";isErrorData(t){return Object.hasOwn(t,"error")}async getClientVideoData(t){const i=new URL(window.location.href).searchParams.get("idDialog");if(!i)return;const o=document.querySelector("#preloaded-data");if(o)try{const r=JSON.parse(o.innerText),{idClient:s,sk:a}=r.config,c=await(await this.fetch(`${this.API_ORIGIN}/models-v2?m=mpfs/info`,{method:"POST",body:JSON.stringify({apiMethod:"mpfs/info",connection_id:s,requestParams:{path:i},sk:a}),headers:{"Content-Type":"application/json"}})).json();if(this.isErrorData(c))throw new _(c.error?.message??c.error?.code);if(c?.type!=="file")throw new _("Failed to get resource info");const{meta:{short_url:u,video_info:d},name:h}=c;if(!d)throw new _("There's no video open right now");if(!u)throw new _("Access to the video is limited");const f=this.clearTitle(h),g=Math.round(d.duration/1e3);return {url:u,title:f,duration:g}}catch(r){E.error(`Failed to get yandex disk video data by video ID: ${t}, because ${r.message}`);return}}clearTitle(t){return t.replace(/(\.[^.]+)$/,"")}getBodyHash(t,e){const i=JSON.stringify({hash:t,sk:e});return encodeURIComponent(i)}async fetchList(t,e){const i=this.getBodyHash(t,e),r=await(await this.fetch(`${this.API_ORIGIN}/public/api/fetch-list`,{method:"POST",body:i})).json();if(Object.hasOwn(r,"error"))throw new _("Failed to fetch folder list");return r.resources}async getDownloadUrl(t,e){const i=this.getBodyHash(t,e),r=await(await this.fetch(`${this.API_ORIGIN}/public/api/download-url`,{method:"POST",body:i})).json();if(r.error)throw new _("Failed to get download url");return r.data.url}async getDiskVideoData(t){try{const e=document.getElementById("store-prefetch");if(!e)throw new _("Failed to get prefetch data");const i=t.split("/").slice(3);if(!i.length)throw new _("Failed to find video file path");const o=JSON.parse(e.innerText),{resources:r,rootResourceId:s,environment:{sk:a}}=o,l=r[s],c=i.length-1,u=i.filter((D,F)=>F!==c).join("/");let d=Object.values(r);u.includes("/")&&(d=await this.fetchList(`${l.hash}:/${u}`,a));const h=d.find(D=>D.name===i[c]);if(!h)throw new _("Failed to find resource");if(h&&h.type==="dir")throw new _("Path is dir, but expected file");const{meta:{short_url:f,mediatype:g,videoDuration:y},path:w,name:x}=h;if(g!=="video")throw new _("Resource isn't a video");const m=this.clearTitle(x),k=Math.round(y/1e3);if(f)return {url:f,duration:k,title:m};const P=await this.getDownloadUrl(w,a);return {url:ut(new URL(P)),duration:k,title:m}}catch(e){E.error(`Failed to get yandex disk video data by disk video ID: ${t}`,e.message);return}}async getVideoData(t){return t.startsWith(this.INLINE_PREFIX)||/^\/d\/([^/]+)$/.exec(t)?{url:this.service?.url+t.slice(1)}:(t=decodeURIComponent(t),t.startsWith(this.CLIENT_PREFIX)?await this.getClientVideoData(t):await this.getDiskVideoData(t))}async getVideoId(t){if(t.pathname.startsWith(this.CLIENT_PREFIX))return t.pathname+t.search;const e=/\/i\/([^/]+)/.exec(t.pathname)?.[0];return e||(/\/d\/([^/]+)/.exec(t.pathname)?t.pathname:void 0)}}class Oc extends L{async getVideoId(t){return /v_show\/id_[\w=]+/.exec(t.pathname)?.[0]}}class B extends L{static isMobile(){return /^m\.youtube\.com$/.test(window.location.hostname)}static extractVideoId(t){const e=t.hash.replace(/^#/,"");if(e){const o=e.startsWith("!")?e.slice(1):e;let r=o;try{r=decodeURIComponent(o);}catch{}try{const s=r.startsWith("http")?new URL(r):new URL(r.startsWith("/")?r:`/${r}`,t.origin),a=B.extractVideoId(s);if(a)return a}catch{const s=/(?:^|[?&#])v=([^&#]+)/.exec(r)?.[1];if(s)return s}}if(t.hostname==="youtu.be")return t.pathname.replace(/^\/+/,"").split("/")[0]||void 0;const i=/\/(?:watch|embed|shorts|live|v|e)\/([^/?#]+)/.exec(t.pathname)?.[1]??void 0;return i||(t.searchParams.get("v")??void 0)}static getPlayer(){return window.location.pathname.startsWith("/shorts/")&&!B.isMobile()?document.querySelector("#shorts-player"):document.querySelector("#movie_player")}static getPlayerResponse(){return B.getPlayer()?.getPlayerResponse?.call(void 0)}static getPlayerData(){return B.getPlayer()?.getVideoData?.call(void 0)}static getVolume(){const t=B.getPlayer();return t?.getVolume?t.getVolume()/100:1}static setVolume(t){const e=B.getPlayer();return e?.setVolume?(e.setVolume(Math.round(t*100)),true):false}static isMuted(){const t=B.getPlayer();return t?.isMuted?t.isMuted():false}static videoSeek(t,e){E.log("videoSeek",e);const o=(B.getPlayer()?.getProgressState()?.seekableEnd??t.currentTime)-e;t.currentTime=o;}static getPoToken(){const t=B.getPlayer();if(!t)return;const e=t.getAudioTrack?.call(void 0);if(!e?.captionTracks?.length)return;const i=e.captionTracks.find(o=>o.url.includes("&pot="));if(i)return /&pot=([^&]+)/.exec(i.url)?.[1]}static getGlobalConfig(){return typeof yt<"u"?yt?.config_:typeof ytcfg<"u"?ytcfg?.data_:void 0}static getDeviceParams(){const t=B.getGlobalConfig();if(!t)return "c=WEB";const e=t.INNERTUBE_CONTEXT?.client,i=new URLSearchParams(t.DEVICE);return i.delete("ceng"),i.delete("cengver"),i.set("c",e?.clientName??t.INNERTUBE_CLIENT_NAME),i.set("cver",e?.clientVersion??t.INNERTUBE_CLIENT_VERSION),i.set("cplayer","UNIPLAYER"),i.toString()}static getSubtitles(t){const i=B.getPlayerResponse()?.captions?.playerCaptionsTracklistRenderer;if(!i)return [];const o=i.captionTracks??[],s=(i.translationLanguages??[]).find(u=>u.languageCode===t),l=o.find(u=>u?.kind==="asr")?.languageCode??"en",c=o.reduce((u,d)=>{if(!("languageCode"in d))return u;const h=d.languageCode?J(d.languageCode):void 0,f=d.baseUrl;if(!h||!f)return u;const g=`${f.startsWith("http")?f:`${window.location.origin}/${f}`}&fmt=json3`;return u.push({source:"youtube",format:"json",language:h,isAutoGenerated:d?.kind==="asr",url:g}),s&&d.isTranslatable&&d.languageCode===l&&t!==h&&u.push({source:"youtube",format:"json",language:t,isAutoGenerated:d?.kind==="asr",translatedFromLanguage:h,url:`${g}&tlang=${t}`}),u},[]);return E.log("youtube subtitles:",c),c}static getLanguage(){if(!B.isMobile()){const o=B.getPlayer()?.getAudioTrack?.call(void 0)?.getLanguageInfo();if(o&&o.id!=="und")return J(o.id.split(".")[0])}const e=B.getPlayerResponse()?.captions?.playerCaptionsTracklistRenderer.captionTracks.find(i=>i.kind==="asr"&&i.languageCode);return e?J(e.languageCode):void 0}async getVideoData(t){const{title:e}=B.getPlayerData()??{},{shortDescription:i,isLive:o,title:r}=B.getPlayerResponse()?.videoDetails??{},s=B.getSubtitles(this.language);let a=B.getLanguage();a&&!Lt.includes(a)&&(a=void 0);const l=B.getPlayer()?.getDuration?.call(void 0)??void 0;return {url:this.service?.url+t,isStream:o,title:r,localizedTitle:e,detectedLanguage:a,description:i,subtitles:s,duration:l}}async getVideoId(t){if(t.searchParams.has("enablejsapi")){const e=B.getPlayer()?.getVideoUrl();t=e?new URL(e):t;}return B.extractVideoId(t)}}const Dc=/^\/play\/([^/?#]+)\/([^/?#]+)\/([^/?#]+)\/?$/i;class Rc extends L{async getVideoId(t){const e=Dc.exec(t.pathname);if(!e)return;const[,i,o,r]=e;return `${i}/${o}/${r}`}}const es={[b.mailru]:Gl,[b.weverse]:Pc,[b.weibo]:Ec,[b.kodik]:zl,[b.patreon]:ec,[b.reddit]:sc,[b.bannedvideo]:xl,[b.kick]:Hl,[b.appledeveloper]:yl,[b.epicgames]:Ol,[b.odysee]:jl,[b.coursehunterLike]:El,[b.twitch]:gc,[b.sap]:dc,[b.linkedin]:Wl,[b.vimeo]:kc,[b.yandexdisk]:_c,[b.vk]:ci,[b.trovo]:pc,[b.incestflix]:$l,[b.porntn]:rc,[b.googledrive]:Bl,[b.bilibili]:kl,[b.xvideos]:Mc,[b.xhamster]:Vc,[b.spankbang]:hc,[b.rule34video]:lc,[b.picarto]:ic,[b.olympicsreplay]:Ql,[b.watchpornto]:Tc,[b.archive]:wl,[b.dailymotion]:Pl,[b.youku]:Oc,[b.egghead]:_l,[b.newgrounds]:Yl,[b.okru]:Zl,[b.peertube]:nc,[b.eporner]:Dl,[b.bitchute]:Tl,[b.rutube]:uc,[b.facebook]:Rl,[b.rumble]:cc,[b.twitter]:mc,[b.pornhub]:oc,[b.tiktok]:co,[b.proxitok]:co,[b.nine_gag]:Jl,[b.youtube]:B,[b.invidious]:B,[b.piped]:B,[b.zdf]:Rc,[b.dzen]:Ml,[b.bunnystream]:Il,[b.cloudflarestream]:Cl,[b.loom]:ql,[b.rtnews]:ac,[b.bitview]:Ll,[b.thisvid]:fc,[b.ign]:Fl,[b.bunkr]:Al,[b.imdb]:Nl,[b.telegram]:li,[b.niconico]:Xl,[z.udemy]:xc,[z.coursera]:ai,[z.douyin]:Ze,[z.artstation]:Sl,[z.kickstarter]:Ul,[z.oraclelearn]:tc,[z.deeplearningai]:Vl,[z.netacad]:Kl};class ns{helpersData;constructor(t={}){this.helpersData=t;}getHelper(t){return new es[t](this.helpersData)}}function is(n){return n in es}function ho(n,t){const e=n.replace(/^\/+/,""),i=new URL("https://vk.com/video");i.searchParams.set("z",e);for(const o of ["list","access_key"]){const r=t.searchParams.get(o);r&&i.searchParams.set(o,r);}return i.toString()}function Bc(){if(hl.exec(window.location.href))return [];const n=window.location.hostname,t=new URL(window.location.href),e=i=>i instanceof RegExp?i.test(n):typeof i=="string"?n.includes(i):typeof i=="function"?i(t):false;return bl.filter(i=>!!i.match&&(Array.isArray(i.match)?i.match.some(e):e(i.match))&&i.host&&i.url)}async function os(n,t={}){const e=new URL(window.location.href),i=n.host;return is(i)?await new ns(t).getHelper(i).getVideoId(e):i===b.custom?e.href:void 0}async function Fc(n,t={}){const e=new URL(window.location.href),i=await os(n,t);if(!i)throw new ge(`Entered unsupported link: "${n.host}"`);const o=e.origin;if([b.peertube,b.coursehunterLike,b.bunnystream,b.cloudflarestream].includes(n.host)&&(n.url=o),n.rawResult)return {url:i,videoId:i,host:n.host,duration:void 0};if(!n.needExtraData)return n.host===b.vk?{url:ho(i,e),videoId:i,host:n.host,duration:void 0}:{url:n.url+i,videoId:i,host:n.host,duration:void 0};if(!is(n.host))throw new ge(`No helper is available for "${n.host}"`);const s=await new ns({...t,service:n,origin:o}).getHelper(n.host).getVideoData(i);if(!s)throw new ge(`Failed to get video raw url for ${n.host}`);return {...s,url:n.host===b.vk?ho(i,e):s.url,videoId:i,host:n.host}}const me={version:"1.0.6",debug:false,fetchFn:fetch.bind(window)},G={log:(...n)=>{if(me.debug)return console.log(`%c✦ chaimu.js v${me.version} ✦`,"background: #000; color: #fff; padding: 0 8px",...n)}},fo=["playing","ratechange","play","waiting","pause","seeked"];function ui(){const n=window.AudioContext||window.webkitAudioContext;return n?new n:void 0}class rs{static name="BasePlayer";chaimu;fetch;_src;fetchOpts;constructor(t,e){this.chaimu=t,this._src=e,this.fetch=this.chaimu.fetchFn,this.fetchOpts=this.chaimu.fetchOpts;}async init(){return this}async clear(){return this}lipSync(t=false){return this}handleVideoEvent=t=>(G.log(`handle video ${t.type}`),this.lipSync(t.type),this);removeVideoEvents(){for(const t of fo)this.chaimu.video?.removeEventListener(t,this.handleVideoEvent);return this}addVideoEvents(){for(const t of fo)this.chaimu.video?.addEventListener(t,this.handleVideoEvent);return this}async play(){return this}async pause(){return this}get name(){return this.constructor.name}set src(t){this._src=t;}get src(){return this._src}get currentSrc(){return this._src}set volume(t){}get volume(){return 0}get playbackRate(){return 0}set playbackRate(t){}get currentTime(){return 0}}class Nc extends rs{static name="AudioPlayer";audio;gainNode;audioSource;constructor(t,e){super(t,e),this.updateAudio();}initAudioBooster(){return this.chaimu.audioContext?(this.disconnectAudioNodes(),this.gainNode=this.chaimu.audioContext.createGain(),this.gainNode.connect(this.chaimu.audioContext.destination),this.audioSource=this.chaimu.audioContext.createMediaElementSource(this.audio),this.audioSource.connect(this.gainNode),this):this}disconnectAudioNodes(){this.audioSource&&(this.audioSource.disconnect(),this.audioSource=void 0),this.gainNode&&(this.gainNode.disconnect(),this.gainNode=void 0);}updateAudio(){return this.audio=new Audio(this.src),this.audio.crossOrigin="anonymous",this}async init(){return this.updateAudio(),this.initAudioBooster(),this}audioErrorHandle=t=>{console.error("[AudioPlayer]",t);};lipSync(t=false){if(G.log("[AudioPlayer] lipsync video",this.chaimu.video),!this.chaimu.video)return this;if(this.audio.currentTime=this.chaimu.video.currentTime,this.audio.playbackRate=this.chaimu.video.playbackRate,!t)return G.log("[AudioPlayer] lipsync mode isn't set"),this;switch(G.log(`[AudioPlayer] lipsync mode is ${t}`),t){case "play":case "playing":case "seeked":return this.chaimu.video.paused||this.syncPlay(),this;case "pause":case "waiting":return this.pause(),this;default:return this}}async clear(){return this.audio.pause(),this.audio.src="",this.audio.removeAttribute("src"),this.disconnectAudioNodes(),this}syncPlay(){return G.log("[AudioPlayer] sync play called"),this.audio&&this.audio.play().catch(this.audioErrorHandle),this}async play(){return G.log("[AudioPlayer] play called"),this.audio&&await this.audio.play().catch(this.audioErrorHandle),this}async pause(){return G.log("[AudioPlayer] pause called"),this.audio&&this.audio.pause(),this}set src(t){if(this._src=t,!t){this.clear();return}this.audio.src=t;}get src(){return this._src}get currentSrc(){return this.audio.currentSrc}set volume(t){if(this.gainNode){this.gainNode.gain.value=t;return}this.audio.volume=t;}get volume(){return this.gainNode?this.gainNode.gain.value:this.audio.volume}get playbackRate(){return this.audio.playbackRate}set playbackRate(t){this.audio.playbackRate=t;}get currentTime(){return this.audio.currentTime}}class $c extends rs{static name="ChaimuPlayer";audioBuffer;audioElement;mediaElementSource;gainNode;blobUrl;isClearing=false;isInitializing=false;clearingPromise;async fetchAudio(){if(!this._src)throw new Error("No audio source provided");if(!this.chaimu.audioContext)throw new Error("No audio context available");G.log(`[ChaimuPlayer] Fetching audio from ${this._src}...`);let t;try{const e=await this.fetch(this._src,this.fetchOpts);G.log("[ChaimuPlayer] Decoding fetched audio...");const i=await e.arrayBuffer(),o=new Blob([i]);t=URL.createObjectURL(o),this.audioBuffer=await this.chaimu.audioContext.decodeAudioData(i),this.blobUrl&&URL.revokeObjectURL(this.blobUrl),this.blobUrl=t,t=void 0;}catch(e){throw t&&URL.revokeObjectURL(t),new Error(`Failed to fetch audio file, because ${e.message}`)}return this}initAudioBooster(){return this.chaimu.audioContext?(this.disconnectAudioNodes(),this.gainNode=this.chaimu.audioContext.createGain(),this):this}disconnectAudioNodes(){this.mediaElementSource&&(this.mediaElementSource.disconnect(),this.mediaElementSource=void 0),this.gainNode&&(this.gainNode.disconnect(),this.gainNode=void 0);}async init(){if(this.isInitializing)throw new Error("Initialization already in progress");this.isInitializing=true;try{return await this.fetchAudio(),this.initAudioBooster(),this.createAudioElement(),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 t=new Audio(this.blobUrl);t.crossOrigin="anonymous","preservesPitch"in t&&(t.preservesPitch=true,"mozPreservesPitch"in t&&(t.mozPreservesPitch=true),"webkitPreservesPitch"in t&&(t.webkitPreservesPitch=true)),this.audioElement=t,this.mediaElementSource=this.chaimu.audioContext.createMediaElementSource(t),this.mediaElementSource.connect(this.gainNode),this.gainNode.connect(this.chaimu.audioContext.destination);}lipSync(t=false){if(G.log("[ChaimuPlayer] lipsync video",this.chaimu.video,this),!this.chaimu.video)return this;if(!t)return G.log("[ChaimuPlayer] lipsync mode isn't set"),this;switch(G.log(`[ChaimuPlayer] lipsync mode is ${t}`),t){case "play":case "playing":case "ratechange":case "seeked":return this.chaimu.video.paused||this.start(),this;case "pause":case "waiting":return this.pause(),this;default:return this}}async reopenCtx(){if(!this.chaimu.audioContext)throw new Error("No audio context available");try{this.chaimu.audioContext.state!=="closed"&&await this.chaimu.audioContext.close();}catch(t){G.log("[ChaimuPlayer] Failed to close audio context:",t);}return this.chaimu.audioContext=ui(),this}async clear(){if(this.isClearing&&this.clearingPromise)return this.clearingPromise;if(!this.chaimu.audioContext)throw new Error("No audio context available");return G.log("clear audio context"),this.isClearing=true,this.clearingPromise=(async()=>{try{await this.pause(),this.audioElement&&(this.audioElement.pause(),this.audioElement=void 0),this.blobUrl&&(URL.revokeObjectURL(this.blobUrl),this.blobUrl=void 0),this.disconnectAudioNodes();const t=this.gainNode?this.gainNode.gain.value:1;return await this.reopenCtx(),this.chaimu.audioContext&&(this.initAudioBooster(),this.volume=t),this}finally{this.isClearing=false,this.clearingPromise=void 0;}})(),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");return this.isClearing&&this.clearingPromise&&(G.log("The other cleaner is still running, waiting..."),await this.clearingPromise),G.log("starting audio via HTMLAudioElement"),await this.play(),this.chaimu.video&&(this.audioElement.currentTime=this.chaimu.video.currentTime,this.audioElement.playbackRate=this.chaimu.video.playbackRate),this.audioElement.play().catch(t=>G.log("[ChaimuPlayer] Play audioElement failed:",t)),this}async pause(){if(!this.chaimu.audioContext)throw new Error("No audio context available");return this.audioElement&&this.audioElement.pause(),this.chaimu.audioContext.state==="running"&&await this.chaimu.audioContext.suspend(),this}async play(){if(!this.chaimu.audioContext)throw new Error("No audio context available");return await this.chaimu.audioContext.resume(),this}set src(t){this._src=t;}get src(){return this._src}get currentSrc(){return this._src}set volume(t){this.gainNode&&(this.gainNode.gain.value=t);}get volume(){return this.gainNode?this.gainNode.gain.value:0}set playbackRate(t){this.audioElement&&(this.audioElement.playbackRate=t);}get playbackRate(){return this.audioElement?this.audioElement.playbackRate:this.chaimu.video?.playbackRate??1}get currentTime(){return this.chaimu.video?.currentTime??0}}class Hc{_debug=false;audioContext;player;video;fetchFn;fetchOpts;constructor({url:t,video:e,debug:i=false,fetchFn:o=me.fetchFn,fetchOpts:r={},preferAudio:s=false}){this._debug=me.debug=i,this.fetchFn=o,this.fetchOpts=r,this.audioContext=ui(),this.player=this.audioContext&&!s?new $c(this,t):new Nc(this,t),this.video=e;}async init(){await this.player.init(),this.video&&!this.video.paused&&this.player.lipSync("play"),this.player.addVideoEvents();}set debug(t){this._debug=me.debug=t;}get debug(){return this._debug}}const Uc="__VOT_MAIN_BOOT_STATE__";function zc(n){return n==="idle"||n==="booting"||n==="booted"||n==="failed"}function Wc(n){return !n||typeof n!="object"?false:zc(n.status)}function qc(n=Uc){const t=globalThis,e=t[n];if(Wc(e))return e;const i={status:"idle",promise:null,error:null};return t[n]=i,i}const Gc="api.browser.yandex.ru",Kc="media-proxy.toil.cc/v1/proxy/m3u8",we="vot-worker.kload.workers.dev",Yc="https://vot.toil.cc/v1",Xc="https://translate-backend.transly.workers.dev/v2",Jc="https://rust-server-531j.onrender.com/detect",ss="https://rust-server-531j.onrender.com",po="https://avatars.mds.yandex.net/get-yapic",as="ilyhalight/voice-over-translation",go=`https://raw.githubusercontent.com/${as}`,jc=`https://github.com/${as}`,fn=15,ls=900,Zc=5,pn="yandexbrowser",di="yandexbrowser",Qc=["Tampermonkey","Violentmonkey"],cs=["UA","LV","LT"],hi=1e3,Se="2025-05-09",tu=["autoTranslate","autoSubtitles","dontTranslateLanguages","enabledDontTranslateLanguages","enabledAutoVolume","enabledSmartDucking","autoVolume","buttonPos","showVideoSlider","syncVolume","downloadWithName","sendNotifyOnComplete","subtitlesMaxLength","subtitlesSmartLayout","highlightWords","subtitlesFontSize","subtitlesFontFamily","subtitlesOpacity","subtitlesDownloadFormat","responseLanguage","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"],fi=()=>{},eu=fi,nu=fi,iu=fi,V={log:eu,warn:nu,error:iu};function ou(){return navigator.language?.substring(0,2).toLowerCase()||"en"}const ru=new Set(["uk","be","bg","mk","sr","bs","hr","sl","pl","sk","cs"]);function su(n){return si.includes(n)?n:ru.has(n)?"ru":"en"}const pi=ou(),Ke=su(pi),mo=3e4,au=/[\\/:*?"'<>|]+/g,lu=/^https?:\/\//i,cu=/-{2,}/g,uu=/^[.\s-]+|[.\s-]+$/g;function vo(){return new Date().toISOString().slice(0,10)}function du(n){let t="";for(let e=0;e=32&&i!==127&&(t+=n[e]);}return t}function hu(n){const t=new WeakSet;return JSON.stringify(n,(e,i)=>{if(i&&typeof i=="object"){if(t.has(i))return "[Circular]";if(t.add(i),Array.isArray(i))return i;const o={},r=Object.keys(i).sort((s,a)=>s.localeCompare(a));for(const s of r)o[s]=i[s];return o}return i})}function Kn(n){let t=2166136261,e=0;for(;e65535?2:1;}return (t>>>0).toString(36)}const us=()=>!!document.pictureInPictureEnabled;async function fu(n,t){try{const e=await n.createWritable();return await e.write(t),await e.close(),!0}catch{return false}}async function pu(n,t){const e=typeof navigator>"u"?void 0:navigator;if(!e?.share||typeof File>"u")return "unsupported";let i;try{i=new File([n],t,{type:n.type||"application/octet-stream"});}catch{return "unsupported"}if(typeof e.canShare=="function"&&!e.canShare({files:[i]}))return "unsupported";try{return await e.share({files:[i],title:t}),"shared"}catch(o){return o instanceof DOMException&&o.name==="AbortError"?"shared":"error"}}function gu(n,t){const e=URL.createObjectURL(n),i=document.createElement("a");i.href=e,i.download=t,i.rel="noopener noreferrer",i.target="_blank",i.style.position="fixed",i.style.left="-9999px",i.style.top="0",(document.body??document.documentElement).append(i);try{return i.click(),!0}catch{return false}finally{i.remove(),mu(e);}}async function ds(n,t,e={}){return e.fileHandle&&await fu(e.fileHandle,n)||e.preferShare&&await pu(n,t)==="shared"?true:gu(n,t)}function mu(n,t=mo){const e=Number.isFinite(t)&&t>=0?t:mo;globalThis.setTimeout(()=>URL.revokeObjectURL(n),e);}function bo(n){const t=n.trim();return t&&du(t).replace(lu,"").replaceAll(au,"-").replaceAll(cu,"-").replaceAll(uu,"")||vo()}const yo=()=>Math.floor(Date.now()/1e3),vu=n=>n?Object.fromEntries(new Headers(n).entries()):{};function $(n,t=0,e=100){const i=Math.min(t,e),o=Math.max(t,e);return Math.min(Math.max(n,i),o)}function hs(n){const t={},e=Object.entries(n);for(;e.length;){const i=e.pop();if(!i)continue;const[o,r]=i;if(r===void 0)continue;if(!(r!==null&&typeof r=="object"&&!Array.isArray(r))){t[o]=r;continue}for(const[a,l]of Object.entries(r))e.push([`${o}.${a}`,l]);}return t}const fs=7200*1e3,wo="x-vot-cache-created-at",se="x-vot-cache-key",bu="vot-http-cache-v1",yu=500,wu="VOTSession",An={translation:"translationExpiresAt",subtitles:"subtitlesExpiresAt"};function Su(){return Math.floor(Date.now()/1e3)}function xu(n){if(!n||typeof n!="object")return false;const t=n;return typeof t.expires=="number"&&Number.isFinite(t.expires)&&typeof t.timestamp=="number"&&Number.isFinite(t.timestamp)&&typeof t.uuid=="string"&&t.uuid.length>0&&typeof t.secretKey=="string"&&t.secretKey.length>0}function ku(n){if(!n||typeof n!="object")return {};const t=Su(),e=Object.entries(n).flatMap(([i,o])=>xu(o)?o.timestamp+o.expires<=t?[]:[[i,o]]:[]);return Object.fromEntries(e)}function Tu(n){if(!n||typeof n!="object")return false;const t=n;return typeof t.expiresAt=="number"&&Number.isFinite(t.expiresAt)&&typeof t.secretKey=="string"&&t.secretKey.length>0&&typeof t.uuid=="string"&&t.uuid.length>0}class Lu{constructor(t=I){this.storage=t;}getStorageKey(t){return wu}async restore(t,e={}){const i=this.getStorageKey(t),o=await this.storage.getRaw(i);if(!Tu(o))return e;const r=Date.now();if(o.expiresAt<=r)return await this.storage.deleteRaw(i),e;const s=Math.max(1,Math.ceil((o.expiresAt-r)/1e3));return {...e,"video-translation":{secretKey:o.secretKey,uuid:o.uuid,expires:s,timestamp:Math.floor(r/1e3)}}}async persist(t,e){const i=this.getStorageKey(t),o=ku(e)["video-translation"];if(!o){await this.storage.deleteRaw(i);return}await this.storage.setRaw(i,{secretKey:o.secretKey,uuid:o.uuid,expiresAt:(o.timestamp+o.expires)*1e3});}}class Au{cache=new Map;clear(){this.cache.clear();}getTranslation(t){return this.getValue(t,"translation")}setTranslation(t,e){this.setValue(t,"translation",e);}getSubtitles(t){return this.getValue(t,"subtitles")}setSubtitles(t,e){this.setValue(t,"subtitles",e);}deleteSubtitles(t){this.deleteValue(t,"subtitles");}getValue(t,e){const i=Date.now(),o=this.cache.get(t);if(!o)return;const r=An[e],s=o[r];if(s!==void 0&&s<=i){o[e]=void 0,o[r]=void 0,this.evictIfEmpty(t,o);return}return o[e]}setValue(t,e,i){const o=Date.now(),r=this.getOrCreateEntry(t),s=o+fs,a=An[e];r[e]=i,r[a]=s;}deleteValue(t,e){const i=this.cache.get(t);if(!i)return;const o=An[e];i[e]=void 0,i[o]=void 0,this.evictIfEmpty(t,i);}evictIfEmpty(t,e){e.translation===void 0&&e.subtitles===void 0&&this.cache.delete(t);}getOrCreateEntry(t){const e=this.cache.get(t);if(e)return e;const i={};return this.cache.set(t,i),i}}class Iu{memoryCache=new Map;inFlightRequests=new Map;async execute(t,e,i){if(!e||e.ttlMs<=0)return i();const o=e.key??this.buildDefaultCacheKey(t);if(!o)return i();const r=this.normalizeMethod(t.method),s=e.ttlMs,a=e.cacheName||bu,l=e.useMemory!==false,c=e.useCacheApi!==false&&r==="GET"&&this.supportsCacheApi(),u=c?Kn(o):"",d=e.dedupe!==false,h=e.allowStaleOnError!==false,f=Date.now();let g;if(l){const m=this.readMemoryCache(o,f);if(m.fresh)return m.fresh;g=m.stale??g;}if(c){const m=await this.readCacheApi(a,t.url,u,s,f,h);if(m.fresh)return l&&this.writeMemoryCache(o,m.fresh.clone(),m.expiresAt??f+s),m.fresh;g=g??m.stale;}const y=async()=>{const m=await i();if(!m.ok)return m;const k=Date.now(),P=this.computeExpiresAt(k,s);if(l&&this.writeMemoryCache(o,m.clone(),P),c){const D=this.toStorableResponse(m.clone(),k);await this.writeCacheApi(a,t.url,u,D);}return m};if(!d)try{return await y()}catch(m){if(h&&g)return g;throw m}const w=this.inFlightRequests.get(o);if(w!==void 0)return (await w).clone();const x=(async()=>{try{return await y()}catch(m){if(h&&g)return g.clone();throw m}})();this.inFlightRequests.set(o,x);try{return (await x).clone()}finally{this.inFlightRequests.delete(o);}}computeExpiresAt(t,e){if(!Number.isFinite(e)||e<=0)return t;const i=Number.MAX_SAFE_INTEGER-t;return e>=i?Number.MAX_SAFE_INTEGER:t+e}normalizeMethod(t){return (t||"GET").toUpperCase()}resolveBodyKey(t){if(t==null)return "";if(typeof t=="string")return t;if(t instanceof URLSearchParams)return t.toString()}buildDefaultCacheKey(t){const e=this.normalizeMethod(t.method);if(e==="GET"||e==="HEAD")return `${e}:${t.url}`;const i=this.resolveBodyKey(t.body);if(i!==void 0)return `${e}:${t.url}#${Kn(i)}`}supportsCacheApi(){return typeof caches<"u"&&typeof caches.open=="function"}readCreatedAtMs(t){const e=t.headers.get(wo);if(!e)return null;const i=Number(e);return Number.isFinite(i)?i:null}ensureVaryByCacheKey(t){const e=t.get("vary");if(!e){t.set("vary",se);return}const i=new Set(e.split(",").map(o=>o.trim().toLowerCase()));!i.has("*")&&!i.has(se)&&t.set("vary",`${e}, ${se}`);}toStorableResponse(t,e){const i=new Headers(t.headers);return i.set(wo,String(e)),this.ensureVaryByCacheKey(i),new Response(t.body,{status:t.status,statusText:t.statusText,headers:i})}readMemoryCache(t,e){const i=this.memoryCache.get(t);return i?i.expiresAt>e?(this.touchMemoryCache(t,i),{fresh:i.response.clone(),expiresAt:i.expiresAt}):(this.memoryCache.delete(t),{stale:i.response.clone(),expiresAt:i.expiresAt}):{}}touchMemoryCache(t,e){this.memoryCache.delete(t),this.memoryCache.set(t,e);}trimMemoryCache(){for(;this.memoryCache.size>yu;){const t=this.memoryCache.keys().next().value;if(typeof t!="string")break;this.memoryCache.delete(t);}}writeMemoryCache(t,e,i){this.memoryCache.has(t)&&this.memoryCache.delete(t),this.memoryCache.set(t,{response:e,expiresAt:i}),this.trimMemoryCache();}async readCacheApi(t,e,i,o,r,s){try{const a=new Request(e,{method:"GET",headers:{[se]:i}}),l=await caches.open(t),c=await l.match(a);if(!c)return {};const u=this.readCreatedAtMs(c);if(u===null)return await l.delete(a),{};const d=this.computeExpiresAt(u,o);if(d>r)return {fresh:c.clone(),expiresAt:d};if(!s)return await l.delete(a),{};const h=c.clone();return await l.delete(a),{stale:h,expiresAt:d}}catch{return {}}}async writeCacheApi(t,e,i,o){try{const r=new Request(e,{method:"GET",headers:{[se]:i}});await(await caches.open(t)).put(r,o);}catch{}}}const Cu=new Iu;async function Eu(n,t,e){return Cu.execute(n,t,e)}function Pu(n){const t=new WeakSet;try{return JSON.stringify(n,(i,o)=>typeof o!="object"||o===null?o:t.has(o)?"[Circular]":(t.add(o),o))??null}catch{return null}}function ps(n,t="Unknown error"){if(n instanceof Error)return n.message||t;if(typeof n=="string")return n||t;if(n==null)return t;if(typeof n=="object"){const e=n;if(typeof e?.data?.message=="string"&&e.data.message)return e.data.message;if(typeof e?.error?.message=="string"&&e.error.message)return e.error.message;if(typeof e?.message=="string"&&e.message)return e.message;const i=Pu(n);if(i&&i!=="{}")return i;const o=n.constructor?.name;return o?`[${o}]`:t}return typeof n=="number"||typeof n=="boolean"||typeof n=="bigint"?`${n}`:typeof n=="symbol"?n.description?`Symbol(${n.description})`:"Symbol":typeof n=="function"?n.name?`[Function ${n.name}]`:"[Function]":t}function gn(n){return ps(n,"")}function mn(n){const t=n;return typeof DOMException<"u"&&t instanceof DOMException&&t.name==="AbortError"||t instanceof Error&&t.name==="AbortError"||t?.message==="AbortError"}function Bt(n="Aborted"){try{return new DOMException(n,"AbortError")}catch{const t=new Error(n);return t.name="AbortError",t}}const gs=new AbortController().signal;function So(n){const t=n.throwIfAborted;if(typeof t=="function")try{t.call(n);return}catch(e){throw n.aborted||mn(e)?Bt():e instanceof Error?e:new Error(String(e))}if(n.aborted)throw Bt()}function Vu(n,t){if(!(Number.isFinite(n)&&n>0))return {signal:t??gs,cleanup:()=>{}};const i=new AbortController;let o;const r=()=>{o!==void 0&&(clearTimeout(o),o=void 0),i.abort(t?.reason);};return t&&(t.addEventListener("abort",r,{once:true}),t.aborted&&r()),i.signal.aborted||(o=setTimeout(()=>{i.abort(Bt("Timeout")),o=void 0;},n)),{signal:i.signal,cleanup:()=>{o!==void 0&&(clearTimeout(o),o=void 0),t?.removeEventListener("abort",r);}}}const xo="api.browser.yandex.ru",ko="googlevideo.com",Mu=/^([\w-]+):\s*(.+)$/,_u=/^[a-zA-Z][a-zA-Z\d+.-]*:/,To=typeof GM_info>"u"?void 0:GM_info?.scriptHandler,ms=!(typeof IS_EXTENSION<"u"&&IS_EXTENSION)&&!!To&&!Qc.includes(To),ae=typeof GM<"u",xe=typeof GM_xmlhttpRequest<"u";function Ou(n){const t=n.trim();try{return new URL(t).hostname.toLowerCase()}catch{if(!_u.test(t))try{return new URL(`https://${t}`).hostname.toLowerCase()}catch{}return}}function Lo(n,t){return n===t||n.endsWith(`.${t}`)}function Du(n,t,e=false){if(e)return true;if(!n){const i=t.toLowerCase();return i.includes(xo)||i.includes(ko)}return Lo(n,xo)||Lo(n,ko)}function Ru(n){return typeof n=="string"?n:n instanceof URL?n.href:n.url}function Bu(n,t){return t?t.toUpperCase():n instanceof Request?(n.method||"GET").toUpperCase():"GET"}function Fu(n){return typeof n!="string"||n.length===0?{}:n.split(/\r?\n/).reduce((t,e)=>{const i=Mu.exec(e);if(!i)return t;const[,o,r]=i;return t[o]=r,t},{})}function Nu(n){const t=n;return typeof t?.error=="string"?t.error:typeof t?.statusText=="string"?t.statusText:gn(n)||"Unknown error"}async function Ao(n,t,e){const i=vu(e.headers);return await new Promise((o,r)=>{const s=typeof GM_xmlhttpRequest>"u"?globalThis.GM_xmlhttpRequest:GM_xmlhttpRequest;if(typeof s!="function"){r(new TypeError("GM_xmlhttpRequest is not available"));return}let a=false,l;const c=()=>{l&&e.signal?.removeEventListener("abort",l);},u=h=>{a||(a=true,c(),r(h));},d=s({method:e.method||"GET",url:n,responseType:"blob",data:e.body,timeout:t,headers:i,onload:h=>{if(a)return;a=true,c();const f=Fu(h.responseHeaders),g=new Response(h.response,{status:h.status,statusText:typeof h.statusText=="string"?h.statusText:"",headers:f});Object.defineProperty(g,"url",{value:h.finalUrl??n}),o(g);},ontimeout:()=>u(new Error("Timeout")),onerror:h=>u(new Error(Nu(h))),onabort:()=>u(Bt())});if(l=()=>{try{d?.abort?.();}catch{}u(Bt());},e.signal&&(e.signal.addEventListener("abort",l,{once:true}),e.signal.aborted)){l();return}})}async function Q(n,t={}){const{timeout:e=15e3,forceGmXhr:i=false,responseCache:o,...r}=t,s=Ru(n),a=Ou(s),l=Bu(n,r.method),c=async()=>{if(Du(a,s,i))return await Ao(s,e,r);const{signal:u,cleanup:d}=Vu(e,r.signal);try{return await fetch(n,{...r,signal:u})}catch(h){if(u.aborted||mn(h))throw h;return V.log("GM_fetch preventing CORS by GM_xmlhttpRequest",gn(h)||"Unknown error"),await Ao(s,e,r)}finally{d();}};return o?await Eu({url:s,method:l,body:r.body},o,c):await c()}const $u={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"]]},vs=Object.entries($u).flatMap(([n,t])=>t.map(([e,i])=>({category:n,oldKey:e,newKey:i??e,shouldDeleteOldKey:!!i}))),Hu=new Map(vs.map(n=>[n.oldKey,n])),Uu=Array.from(new Set(vs.map(n=>n.oldKey)));function zu(n){const t={};for(const e of n)t[e]=void 0;return t}function Wu(n,t){switch(n){case "numToBool":case "number":return typeof t=="number";case "array":return Array.isArray(t);case "string":return typeof t=="string"||t===null;default:return false}}function qu(n,t){switch(n){case "string":case "array":case "number":return t;default:return !!t}}function Gu(n,t){let e=qu(n.category,t);return n.oldKey==="autoVolume"&&typeof t=="number"&&t<1&&(e=Math.round(t*100)),e}function Ku(n,t){return Array.isArray(n)&&Array.isArray(t)?n.length===t.length&&n.every((e,i)=>Object.is(e,t[i])):Object.is(n,t)}async function Yu(n){if(n.compatVersion===Se)return n;const t=new Set([...Object.keys(n),...Uu]),e=await I.getValues(zu(t)),i={...n},o=[],r=[];for(const[s,a]of Object.entries(e)){if(a===void 0)continue;const l=Hu.get(s);if(!l||!Wu(l.category,a))continue;const c=Gu(l,a);i[l.newKey]=c;const u=e[l.newKey];(l.shouldDeleteOldKey||!Ku(u,c))&&o.push(I.set(l.newKey,c)),l.shouldDeleteOldKey&&r.push(I.delete(l.oldKey));}return await Promise.all([...o,...r]),{...i,compatVersion:Se}}class Io{support=null;resolveSupport(){if(this.support)return this.support;const t={legacyGet:typeof GM_getValue=="function",legacySet:typeof GM_setValue=="function",legacyDelete:typeof GM_deleteValue=="function",legacyList:typeof GM_listValues=="function",promiseGet:ae&&typeof GM?.getValue=="function",promiseGetValues:ae&&typeof GM?.getValues=="function",promiseSet:ae&&typeof GM?.setValue=="function",promiseDelete:ae&&typeof GM?.deleteValue=="function",promiseList:ae&&typeof GM?.listValues=="function"};return this.support=t,V.log(`[VOT Storage] GM Promises: ${t.promiseGet} | GM legacy: ${t.legacyGet}`),t}get isSupportOnlyLS(){const t=this.resolveSupport();return !t.legacyGet&&!t.legacySet&&!t.legacyDelete&&!t.legacyList&&!t.promiseGet&&!t.promiseGetValues&&!t.promiseSet&&!t.promiseDelete&&!t.promiseList}syncGetByName(t,e,i){if(i.legacyGet)return GM_getValue(t,e);const o=globalThis.localStorage.getItem(t);if(o===null)return e;try{return JSON.parse(o)}catch{return e}}async getRaw(t,e){const i=this.resolveSupport();return i.promiseGet&&GM.getValue?await GM.getValue(t,e):this.syncGetByName(t,e,i)}async get(t,e){return this.getRaw(t,e)}async getValues(t){const e=this.resolveSupport();if(e.promiseGetValues&&GM.getValues)return await GM.getValues(t);const i=Object.entries(t);if(e.promiseGet&&GM.getValue){const o=await Promise.all(i.map(async([r,s])=>{const a=await GM.getValue(r,s);return [r,a]}));return Object.fromEntries(o)}return Object.fromEntries(i.map(([o,r])=>[o,this.syncGetByName(o,r,e)]))}syncSetByName(t,e,i){return i.legacySet?GM_setValue(t,e):globalThis.localStorage.setItem(t,JSON.stringify(e))}async setRaw(t,e){const i=this.resolveSupport();return i.promiseSet&&GM.setValue?await GM.setValue(t,e):this.syncSetByName(t,e,i)}async set(t,e){return this.setRaw(t,e)}syncDeleteByName(t,e){return e.legacyDelete?GM_deleteValue(t):globalThis.localStorage.removeItem(t)}async deleteRaw(t){const e=this.resolveSupport();return e.promiseDelete&&GM.deleteValue?await GM.deleteValue(t):this.syncDeleteByName(t,e)}async delete(t){return this.deleteRaw(t)}syncList(t){return t.legacyList?GM_listValues():tu}async list(){const t=this.resolveSupport();return t.promiseList&&GM.listValues?await GM.listValues():this.syncList(t)}}const Co="__VOT_STORAGE_SINGLETON__",I=(()=>{const n=globalThis,t=n[Co];if(t instanceof Io)return t;const e=new Io;return n[Co]=e,e})();function Xu(){const n=globalThis._userData;if(!n||typeof n!="object")return null;const t=n;return typeof t.avatar_id!="string"||typeof t.username!="string"||t.avatar_id.length===0||t.username.length===0?null:{avatar_id:t.avatar_id,username:t.username}}async function Ju(){const{access_token:n,expires_in:t}=Object.fromEntries(new URLSearchParams(globalThis.location.hash.slice(1)));if(!n||!t)throw new Error("[VOT] Invalid token response");const e=Number.parseInt(t,10);if(Number.isNaN(e))throw new TypeError("[VOT] Invalid expires_in value");await I.set("account",{token:n,expires:Date.now()+e*1e3,username:void 0,avatarId:void 0});}async function ju(){const n=Xu();if(!n)throw new Error("[VOT] Invalid user data");const{avatar_id:t,username:e}=n,i=await I.get("account");if(!i)throw new Error("[VOT] No account data found");await I.set("account",{...i,username:e,avatarId:t});}async function Zu(){if(globalThis.location.pathname==="/auth/callback")return Ju();if(globalThis.location.pathname==="/my/profile")return ju()}const Qu="recommended",td="Translate video",ed="Turn off",nd="Translation settings",id="Subtitles settings",od="Smart subtitle layout",rd="Reset settings",sd="The video is being translated",ad="Video language",ld="Translation language",cd="The translation will take",ud="The translation will take more than an hour",dd="The translation will take about a minute",hd="The translation will take a few minutes",fd="The translation will take approximately {0} minutes",pd="The translation will take approximately {0} minutes",gd="Failed to request video translation",md="Audio link not received",vd="Failed to download audio",bd="The audio format is not supported",yd="Translate on open",wd="Subtitles on open",Sd="Don't translate from my language",xd="Video volume:",kd="Translation volume:",Td="Reduce video volume to",Ld="Video volume slider",Ad="Link translation and video volume",Id="You have disabled the translation of the video in your language",Cd="Video is too long",Ed="No video ID found",Pd="Subtitles",Vd="Disabled",Md="Subtitles max length",_d="Highlight words",Od="translated from",Dd="autogenerated",Rd="VOT Settings",Bd="Menu language",Fd="Authors",Nd="Version",$d="Loader",Hd="Browser",Ud="Show PiP button",zd={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"},Wd="There is no connection to the server",qd="Search...",Gd="Translate errors from the API",Kd="Language detection service",Yd="Enter the proxy worker address",Xd="Enter the address of the m3u8 proxy worker",Jd="Proxy Settings",jd="The translation will take approximately {0} minutes",Zd="Extended translation volume increase",Qd="Subtitles design",th="Subtitle font",eh="Font size of subtitles",nh="Transparency of the subtitle background",ih="The format for downloading subtitles",oh="Download files with the video name",rh="Update localization files",sh="Locale hash",ah="Updated at",lh="To enable this, you must have a Web Audio API",ch="Media CSP is enabled on this site",uh="Use it only for bypassing Media CSP",dh="Use the new audio player",hh="Use an experimental variation of Yandex voices for some videos",fh="The translation is slightly delayed",ph="The translation on the {0} has been completed!",gh="Send a notification that the video has been translated",mh="Report a bug",vh="Disabled",bh="Enabled",yh="Proxy everything",wh="Proxying mode",Sh="Translated by {0}",xh="Translate stream isn't available",kh="Text translation service",Th="Doesn't affect the translation of text in voice over",Lh="Don't translate from selected languages",Ah="Display the video volume slider",Ih="Hotkeys settings",Ch="None",Eh="Use lively voices. Speakers sound like native Russians.",Ph="Misc settings",Vh={yandexbrowser:"Yandex Browser",msedge:"Microsoft Edge","rust-server":"Rust Server"},Mh="About extension",_h="Appearance",Oh="Button position in the player",Dh={left:"Left",right:"Right",top:"Top",default:"Default"},Rh="secs",Bh="Delay before hiding the translate button",Fh="not found",Nh="The button position only changes in players larger than 600 pixels.",$h="Completely disabling proxying in your country may break the extension",Hh="Press the key combination...",Uh="Use audio download",zh="Disabling audio downloads may affect the functionality of the extension",Wh="You need to log in to use this feature",qh="My account",Gh="Login",Kh="Logout",Yh="Refresh",Xh="Enter the Yandex OAuth Token",Jh="You can manually set the account token in this field. Please note that we don't check its validity before sending a translate request",jh="Login via token",Zh="Adaptive volume",Qh={recommended:Qu,translateVideo:td,disableTranslate:ed,translationSettings:nd,subtitlesSettings:id,subtitlesSmartLayout:od,resetSettings:rd,videoBeingTranslated:sd,videoLanguage:ad,translationLanguage:ld,translationTake:cd,translationTakeMoreThanHour:ud,translationTakeAboutMinute:dd,translationTakeFewMinutes:hd,translationTakeApproximatelyMinutes:fd,translationTakeApproximatelyMinute:pd,requestTranslationFailed:gd,audioNotReceived:md,VOTFailedDownloadAudio:vd,audioFormatNotSupported:bd,VOTAutoTranslate:yd,VOTAutoSubtitles:wd,VOTDontTranslateYourLang:Sd,VOTVolume:xd,VOTVolumeTranslation:kd,VOTAutoSetVolume:Td,VOTShowVideoSlider:Ld,VOTSyncVolume:Ad,VOTDisableFromYourLang:Id,VOTVideoIsTooLong:Cd,VOTNoVideoIDFound:Ed,VOTSubtitles:Pd,VOTSubtitlesDisabled:Vd,VOTSubtitlesMaxLength:Md,VOTHighlightWords:_d,VOTTranslatedFrom:Od,VOTAutogenerated:Dd,VOTSettings:Rd,VOTMenuLanguage:Bd,VOTAuthors:Fd,VOTVersion:Nd,VOTLoader:$d,VOTBrowser:Hd,VOTShowPiPButton:Ud,langs:zd,streamNoConnectionToServer:Wd,searchField:qd,VOTTranslateAPIErrors:Gd,VOTDetectService:Kd,VOTProxyWorkerHost:Yd,VOTM3u8ProxyHost:Xd,proxySettings:Jd,translationTakeApproximatelyMinute2:jd,VOTAudioBooster:Zd,VOTSubtitlesDesign:Qd,VOTSubtitlesFont:th,VOTSubtitlesFontSize:eh,VOTSubtitlesOpacity:nh,VOTSubtitlesDownloadFormat:ih,VOTDownloadWithName:oh,VOTUpdateLocaleFiles:rh,VOTLocaleHash:sh,VOTUpdatedAt:ah,VOTNeedWebAudioAPI:lh,VOTMediaCSPEnabledOnSite:ch,VOTOnlyBypassMediaCSP:uh,VOTNewAudioPlayer:dh,VOTUseNewModel:hh,TranslationDelayed:fh,VOTTranslationCompletedNotify:ph,VOTSendNotifyOnComplete:gh,VOTBugReport:mh,VOTTranslateProxyDisabled:vh,VOTTranslateProxyEnabled:bh,VOTTranslateProxyEverything:yh,VOTTranslateProxyStatus:wh,VOTTranslatedBy:Sh,VOTStreamNotAvailable:xh,VOTTranslationTextService:kh,VOTNotAffectToVoice:Th,DontTranslateSelectedLanguages:Lh,showVideoVolumeSlider:Ah,hotkeysSettings:Ih,None:Ch,VOTUseLivelyVoice:Eh,miscSettings:Ph,services:Vh,aboutExtension:Mh,appearance:_h,buttonPosition:Oh,position:Dh,secs:Rh,autoHideButtonDelay:Bh,notFound:Fh,minButtonPositionContainer:Nh,VOTTranslateProxyStatusDefault:$h,PressTheKeyCombination:Hh,VOTUseAudioDownload:Uh,VOTUseAudioDownloadWarning:zh,VOTAccountRequired:Wh,VOTMyAccount:qh,VOTLogin:Gh,VOTLogout:Kh,VOTRefresh:Yh,VOTYandexToken:Xh,VOTYandexTokenInfo:Jh,VOTLoginViaToken:jh,smartDucking:Zh};var In=["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"];const tf=["localePhrases","localeLang","localeHash","localeVersion","localeUpdatedAt","localeLangOverride"],ef=hs(Qh),Eo="master",nf=(()=>{const n=typeof In<"u"&&Array.isArray(In)?In:["en"];return n.includes("auto")?n:["auto",...n]})();function of(n,t){return n||t||"unknown"}function rf(){const n="1.11.3",t=typeof GM_info<"u"?String(GM_info?.script?.version||""):"";return of(n,t)}class sf{lang;locale;defaultLocale=ef;localesUrl=`${go}/${Eo}/src/localization/locales`;hashesUrl=`${go}/${Eo}/src/localization/hashes.json`;warnedMissingKeys=new Set;_langOverride="auto";constructor(){this.lang=this.getLang(),this.locale={};}async init(){const[t,e]=await Promise.all([I.get("localeLangOverride","auto"),I.get("localePhrases","")]);return this._langOverride=t,this.lang=this.getLang(),this.setLocaleFromJsonString(e),this}get langOverride(){return this._langOverride}getLang(){return this.langOverride!=="auto"?this.langOverride:pi}getAvailableLangs(){return [...nf]}async reset(){return await Promise.all(tf.map(t=>I.delete(t))),this}buildUrl(t,e="",i=false){const o=i?`?timestamp=${yo()}`:"";return `${t}${e}${o}`}async changeLang(t){return this.langOverride===t?false:(await I.set("localeLangOverride",t),this._langOverride=t,this.lang=this.getLang(),await this.update(true),true)}async checkUpdates(t=false){try{const e=await Q(this.buildUrl(this.hashesUrl,"",t));if(!e.ok)throw e.status;const i=await e.json();if(!i||typeof i!="object")throw new Error("Invalid locale hashes payload");const o=i[this.lang];return typeof o!="string"||!o||await I.get("localeHash","")===o?!1:o}catch(e){return console.error("[VOT] [localizationProvider] Failed to get locales hash:",e),null}}async update(t=false){const e=rf(),i=await I.get("localeVersion","");if(!t&&i===e)return this;const o=await this.checkUpdates(t);if(o===null)return this;if(!o)return this;const r=yo();try{const s=await Q(this.buildUrl(this.localesUrl,`/${this.lang}.json`,t));if(!s.ok)throw s.status;const a=await s.text();this.setLocaleFromJsonString(a),await Promise.all([I.set("localePhrases",a),I.set("localeHash",o),I.set("localeLang",this.lang),I.set("localeVersion",e),I.set("localeUpdatedAt",r)]);}catch(s){console.error("[VOT] [localizationProvider] Failed to get locale:",s),this.setLocaleFromJsonString(await I.get("localePhrases",""));}return this}setLocaleFromJsonString(t){const e=t.trim();if(!e)return this.locale={},this.warnedMissingKeys.clear(),this;try{const i=JSON.parse(e);if(!i||typeof i!="object"||Array.isArray(i))throw new Error("Locale payload should be a JSON object");this.locale=hs(i);}catch(i){console.error("[VOT] [localizationProvider]",i),this.locale={};}return this.warnedMissingKeys.clear(),this}getFromLocale(t,e,i="locale"){return t[e]??this.warnMissingKey(t,e,i)}warnMissingKey(t,e,i){const o=`${i}:${e}`;this.warnedMissingKeys.has(o)||(this.warnedMissingKeys.add(o),console.warn("[VOT] [localizationProvider] locale",t,"doesn't contain key",e));}getDefault(t){return this.getFromLocale(this.defaultLocale,t,"default")??t}get(t){return this.getFromLocale(this.locale,t)??this.getDefault(t)}getLangLabel(t){const e=`langs.${t}`;if(e in this.defaultLocale){const i=this.get(e);if(i)return i}return t.toUpperCase()}}const v=new sf;let Po=null;function af(){return Po??=v.init(),Po}const gi=()=>globalThis.self!==globalThis.top;function lf(){const t=Object.entries({"https://dev.epicgames.com":{targetOrigin:"https://dev.epicgames.com",dataFilter:e=>typeof e=="string"&&e.startsWith("getVideoId:"),extractVideoId:e=>e.pathname.split("/").at(-2)??null,responseFormatter:(e,i)=>`${typeof i=="string"?i:""}:${e}`},"https://www.dailymotion.com":{targetOrigin:"https://geo.dailymotion.com",dataFilter:e=>typeof e=="string"&&e.startsWith("getVideoId:"),extractVideoId:e=>/(?:^|\/)video\/([^/]+)/.exec(e.pathname)?.[1],responseFormatter:e=>`getVideoId:${e}`}}).find(([e])=>globalThis.location.origin===e)?.[1];t&&globalThis.addEventListener("message",e=>{try{if(e.origin!==t.targetOrigin||!t.dataFilter(e.data))return;const i=t.extractVideoId(new URL(globalThis.location.href));if(!i)return;const o=t.responseFormatter(i,e.data);e.source&&"postMessage"in e.source&&e.source.postMessage(o,t.targetOrigin);}catch(i){console.error("Iframe communication error:",i);}});}let Cn=false,le=null,Vo=false;async function Mo(n,t){if(!Cn){if(le!==null){await le;return}le=(async()=>{if(t("Activating runtime",{reason:n}),globalThis.location.origin===ss){await Zu(),Cn=true;return}await af(),gi()||await v.update(),V.log(`Selected menu language: ${v.lang}`),Vo||(Vo=true,lf()),Cn=true;})();try{await le;}finally{le=null;}}}const _o=new WeakSet;function cf(n){const{videoObserver:t,videosWrappers:e,ensureRuntimeActivated:i,getServicesCached:o,findContainer:r,createVideoHandler:s}=n;if(_o.has(t))return;_o.add(t);const a=new WeakSet,l=new WeakMap,c=new WeakMap,u=new WeakMap,d=m=>{const k=c.get(m);return k&&l.get(k)===m&&l.delete(k),c.delete(m),k??void 0},h=m=>{m&&u.delete(m);},f=async(m,k)=>{const P=e.get(m);if(P)try{await P.release();}catch(D){console.error(`[VOT] Failed to release videoHandler (${k})`,D);}finally{e.delete(m);}},g=m=>{for(const k of o()){const P=r(k,m);if(P)return {site:k,container:P}}return null},y=m=>{const k=String(m.host);return k==="peertube"||k==="directlink"?{...m,url:globalThis.location.origin}:m},w=async m=>{if(!m)return;const k=u.get(m);k&&(u.delete(m),!(!k.isConnected||e.has(k)||a.has(k))&&await x(k));},x=async m=>{if(!(e.has(m)||a.has(m))){a.add(m);try{try{await i("video-detected");}catch(tt){console.error("[VOT] Failed to activate runtime",tt);return}const k=g(m);if(!k)return;const{site:P,container:D}=k,F=l.get(D);if(F&&F!==m){if(F.isConnected){u.set(D,m);return}await f(F,"stale container"),d(F);}const lt=s(m,D,y(P));e.set(m,lt),c.set(m,D),l.set(D,m);try{if(await lt.init(),e.get(m)!==lt)return;try{await lt.setCanPlay();}catch(tt){console.error("[VOT] Failed to get video data",tt);}}catch(tt){if(e.get(m)===lt){await f(m,"init failed");const gt=d(m);h(gt),await w(gt);}console.error("[VOT] Failed to initialize videoHandler",tt);}}finally{a.delete(m);}}};t.onVideoAdded.addListener(x),t.onVideoRemoved.addListener(async m=>{const k=d(m);await f(m,"video removed"),a.delete(m),k&&u.get(k)===m&&h(k),await w(k);});}function uf(n){return n.isIframe?n.href==="about:blank"||n.href.startsWith("about:srcdoc")||n.origin==="null":false}function df(n){return uf(n)?"skip":n.isIframe?"iframe-lazy":"top-full"}function bs(n){return n?typeof ShadowRoot<"u"&&n instanceof ShadowRoot?n.host:n.parentNode??null:null}function ke(n,t){let e=t;for(;e;){if(e===n)return true;e=bs(e);}return false}function hf(n){return n===document.body||n===document.documentElement}function mi(n,t,e={}){const{allowDocumentViewport:i=false}=e;if(!(n instanceof HTMLElement)||hf(n)&&!i)return null;for(const o of t)if(o&&(ke(n,o)||ke(o,n)))return n;return null}function ff(n,t){if(!n||!t)return null;const e=n instanceof Document?null:n,i=o=>{if(!o)return null;if(o instanceof Document){if(e){const a=o.querySelectorAll(t);for(const l of a)if(ke(l,e))return l;return null}return o.querySelector(t)}const r=o.closest(t);if(r)return r;const s=o.getRootNode();if(s instanceof ShadowRoot)return i(s.host);if(s instanceof Document)return i(s);if(s!==o){const a=bs(s);if(a&&a!==o&&a instanceof Element)return i(a)}return null};return i(n)}function ys(n,t){if(!t)return null;const e=ff(n,t);return e instanceof HTMLElement&&e.isConnected&&ke(e,n)?e:null}function pf(n,t){return t.host==="youtube"&&t.additionalData!=="mobile"?n.parentElement??n:n}function gf(n){const t=pf(n.container,n.site),e=n.fullscreenRoot??t;return {base:t,root:e,portalContainer:t,subtitlesMountContainer:e}}class N{listeners=new Set;get size(){return this.listeners.size}addListener(t){return this.listeners.add(t),this}removeListener(t){return this.listeners.delete(t),this}dispatch(...t){for(const e of this.listeners)try{e(...t);}catch(i){console.error("[VOT]",i);}}async dispatchAsync(...t){const e=[];for(const o of this.listeners)try{const r=o(...t);r&&typeof r.then=="function"&&e.push(Promise.resolve(r));}catch(r){console.error("[VOT]",r);}if(!e.length)return;const i=await Promise.allSettled(e);for(const o of i)o.status==="rejected"&&console.error("[VOT]",o.reason);}clear(){this.listeners.clear();}}function Oo(n,t,e,i){return JSON.stringify({downloadType:n,itag:t,minChunkSize:i,fileSize:e})}function vi(n){return n?.toLowerCase()??""}function ws(n){const t=vi(n);return t.includes("audio/")&&!t.includes("video/")}function mf(n){const t=vi(n);return t.includes("audio/mp4")&&t.includes("mp4a.")}function Do(n){const t=vi(n);return t.includes("audio/webm")&&t.includes("opus")}function vf(n){if(!n)return "mp4a.40.2";const t=/codecs="([^"]+)"/i.exec(n);if(!t?.[1])return "mp4a.40.2";const e=t[1].split(",").map(i=>i.trim());return e.find(i=>i.toLowerCase().startsWith("mp4a."))??e[0]??"mp4a.40.2"}function bf(n,t){let e=null,i=t==="max"?-1/0:1/0;for(const o of n){const r=o.bitrate??0;(t==="max"&&r>i||t==="min"&&rws(r.mimeType));if(!e.length)throw new Error("No adaptive audio formats were found in player response");const i=t==="bestefficiency"?"min":"max",o=t==="bestefficiency"?[e.filter(r=>Do(r.mimeType)),e]:[e.filter(r=>mf(r.mimeType)),e.filter(r=>Do(r.mimeType)),e];for(const r of o){if(!r.length)continue;const s=bf(r,i);if(s)return s}throw new Error("No adaptive audio formats were found in player response")}class Ss extends Error{status;constructor(t=403){super(`Failed to load watch page: ${t}`),this.name="YtWatchContextForbiddenError",this.status=t;}}const ce=/^[a-zA-Z0-9_-]{11}$/,Zt="https://www.youtube.com",wf="19.44.38",Sf="1.60.19",xf="19.45.4",Ro=["ANDROID_VR","ANDROID","IOS","WEB","MWEB"],Re={accept:"*/*",origin:Zt,referer:`${Zt}/`},Bo=256*1024;function Be(n){return n?{signal:n}:{}}function kf(n,t,e){switch(n){case "ANDROID":case "YTMUSIC_ANDROID":case "YTSTUDIO_ANDROID":return {clientName:"ANDROID",clientVersion:wf,hl:"en",gl:"US",androidSdkVersion:34,osName:"Android",osVersion:"14",platform:"MOBILE"};case "ANDROID_VR":return {clientName:"ANDROID_VR",clientVersion:Sf,hl:"en",gl:"US",androidSdkVersion:31,osName:"Android",osVersion:"12",platform:"MOBILE"};case "IOS":return {clientName:"IOS",clientVersion:xf,hl:"en",gl:"US",platform:"MOBILE",osName:"iPhone",osVersion:"18.0.0.22A3354",deviceMake:"Apple",deviceModel:"iPhone16,2"};case "MWEB":return {clientName:"MWEB",clientVersion:t.clientVersion,hl:"en",gl:"US",originalUrl:`${Zt}/watch?v=${e}`};default:return {clientName:"WEB",clientVersion:t.clientVersion,hl:"en",gl:"US",utcOffsetMinutes:0,originalUrl:`${Zt}/watch?v=${e}`}}}function Tf(n){const t=n.trim();if(ce.test(t))return t;let e;try{e=new URL(t);}catch{throw new Error(`Cannot extract YouTube video id from: ${n}`)}const i=e.hostname.toLowerCase();if(i==="youtu.be"||i.endsWith(".youtu.be")){const l=e.pathname.split("/").find(Boolean);if(l&&ce.test(l))return l}const o=e.searchParams.get("v");if(o&&ce.test(o))return o;const r=e.pathname.split("/").filter(Boolean),s=r.indexOf("shorts");if(s!==-1){const l=r[s+1];if(l&&ce.test(l))return l}const a=r.indexOf("embed");if(a!==-1){const l=r[a+1];if(l&&ce.test(l))return l}throw new Error(`Cannot extract YouTube video id from: ${n}`)}function Lf(n){return n.replaceAll("\\u0026","&").replaceAll("\\/","/")}function Af(n){const t=n.videoId??n.videoUrl;if(!t)throw new Error("Either videoId or videoUrl is required");return Tf(t)}function Fe(n,t){for(const e of t){const i=e.exec(n)?.[1];if(i)return i}}async function If(n){return new Uint8Array(await n.arrayBuffer())}function Cf(n=16){const t="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";let e="";if(typeof crypto<"u"&&typeof crypto.getRandomValues=="function"){const i=new Uint8Array(n);crypto.getRandomValues(i);for(const o of i)e+=t[o%t.length]??"a";return e}for(let i=0;io)return false;const r=Pf(n.headers.get("content-range"));return r?r.start===e&&r.end===e+t.byteLength-1:n.status===206?t.byteLength===o:n.status===200?e===0&&t.byteLength===o:false}function _f(n,t){const e=n.headers.get("content-range")??"none",i=n.headers.get("content-length")??"none";return `status=${n.status}; bytes=${t.byteLength}; content-range=${e}; content-length=${i}`}function Of(n){const t=n?.toLowerCase()??"";return t.includes("audio/webm")?"audio/webm":t.includes("audio/mp4")?"audio/mp4":"audio/aac"}function Df(n){const t=n?[n,...Ro]:[...Ro],e=new Set;return t.filter(i=>e.has(i)?false:(e.add(i),true))}let Rf=class{fetchFn;constructor(t={}){this.fetchFn=t.fetchImplementation??fetch;}async fetchRangeChunk(t,e,i,o){const r=`bytes=${e}-${i}`,s=await this.fetchFn(t,{headers:{...Re,range:r},...Be(o)});if(!s.ok)throw new Error(`Failed to download stream chunk: ${s.status}`);const a=await If(s);if(!Mf(s,a,e,i))throw new Error(`Received unexpected stream chunk payload: ${_f(s,a)}`);return a}async downloadStreamByRanges(t,e,i){const o=await this.resolveStreamContentLength(t,e,i,true),r=new Uint8Array(o);let s=0;for(let a=0;ar.byteLength)throw new Error("Downloaded stream chunk exceeds probed stream content length");r.set(c,s),s+=c.byteLength;}return s===r.byteLength?r:r.slice(0,s)}async downloadAudioToChunkStream(t,e){if(e.chunkSize<=0)throw new RangeError("Audio downloader. ytAudio. chunkSize must be > 0");return this.withResolvedPlayableAudioFormat(t,t.videoQuality??"best","Chunk mode requires an adaptive audio stream format","Unable to resolve streamable format for chunk mode",async({resolved:i,signal:o})=>{const r=await this.resolveStreamContentLength(i.streamUrl,i.chosenFormat.contentLength,o,true),s=Math.max(1,Math.ceil(r/e.chunkSize));return {videoId:i.videoId,fileSize:r,itag:i.chosenFormat.itag??0,mediaPartsLength:s,getMediaBuffers:(async function*(){for(let a=0;a{const r=await this.downloadStreamByRanges(i.streamUrl,i.chosenFormat.contentLength,o),s=this.getExtractionHints(i.chosenFormat);return await e.write(r),{videoId:i.videoId,bytesWritten:r.byteLength,mimeType:Of(i.chosenFormat.mimeType),codec:s.codec,sampleRate:s.sampleRate,channels:s.channels}})}async withResolvedPlayableAudioFormat(t,e,i,o,r){const s=Af(t),{signal:a}=t,l=await this.fetchWatchContext(s,a),c=Df(t.client),u=[];for(const d of c)try{const h=await this.resolvePlayableFormatForClient({videoId:s,watchContext:l,client:d,quality:e,signal:a});if(!ws(h.chosenFormat.mimeType))throw new Error(i);return await r({resolved:h,signal:a})}catch(h){const f=h instanceof Error?h.message:String(h);u.push(`${d}: ${f}`);}throw new Error(`${o}. Attempts: ${u.join(" | ")}`)}async resolvePlayableFormatForClient({videoId:t,watchContext:e,client:i,quality:o,signal:r}){const l=((await this.fetchPlayerResponse(t,e,i,r)).streamingData?.adaptiveFormats??[]).filter(d=>!!d.url);if(!l.length)throw new Error("Player response did not contain direct adaptive audio stream URLs");const c=yf(l,o),u=this.resolveFormatUrl(c,e.clientVersion);return {videoId:t,chosenFormat:c,streamUrl:u}}async resolveStreamContentLength(t,e,i,o=false){const r=Ye(e);if(r!==null&&!o)return r;let s;try{s=await this.fetchFn(t,{headers:{...Re,range:"bytes=0-0"},...Be(i)});}catch(u){if(r!==null)return r;const d=u instanceof Error?u.message:String(u);throw new Error(`Failed to probe stream content length: ${d}`)}if(!s.ok){if(r!==null)return r;throw new Error(`Failed to probe stream content length: ${s.status}`)}const a=Ef(s.headers.get("content-range")),l=Ye(s.headers.get("x-goog-stored-content-length")),c=Ye(s.headers.get("content-length"));if(typeof s.body?.cancel=="function")try{await s.body.cancel();}catch{}return a??l??r??c??(()=>{throw new Error("Failed to resolve stream content length")})()}getExtractionHints(t){const e=vf(t.mimeType),i=Number.parseInt(t.audioSampleRate??"",10);return {codec:e,sampleRate:Number.isFinite(i)&&i>0?i:44100,channels:t.audioChannels&&t.audioChannels>0?t.audioChannels:2}}resolveFormatUrl(t,e){if(!t.url)throw new Error("Selected format does not contain a direct stream URL");const i=new URL(t.url);return i.searchParams.get("c")==="WEB"&&i.searchParams.set("cver",e),i.searchParams.set("cpn",Cf()),i.toString()}async fetchWatchContext(t,e){const i=`${Zt}/watch?v=${encodeURIComponent(t)}&hl=en`,o=await this.fetchFn(i,{headers:Re,...Be(e)});if(!o.ok)throw o.status===403?new Ss(o.status):new Error(`Failed to load watch page: ${o.status}`);const r=await o.text(),s=Fe(r,[/"INNERTUBE_API_KEY":"([^"]+)"/,/["']INNERTUBE_API_KEY["']\s*:\s*"([^"]+)"/]),a=Fe(r,[/"INNERTUBE_CLIENT_VERSION":"([^"]+)"/,/["']INNERTUBE_CLIENT_VERSION["']\s*:\s*"([^"]+)"/]),l=Fe(r,[/"STS":(\d+)/,/["']STS["']\s*:\s*(\d+)/]),c=Fe(r,[/"VISITOR_DATA":"([^"]+)"/,/"visitorData":"([^"]+)"/,/["'](?:VISITOR_DATA|visitorData)["']\s*:\s*"([^"]+)"/]);if(!s||!a)throw new Error("Failed to extract required player context from watch page");const u=l?Number.parseInt(l,10):void 0,d={apiKey:s,clientVersion:a};return typeof u=="number"&&Number.isFinite(u)&&(d.signatureTimestamp=u),c&&(d.visitorData=Lf(c)),d}async fetchPlayerResponse(t,e,i,o){const r=kf(i,e,t);e.visitorData&&(r.visitorData=e.visitorData);const s={context:{client:r},videoId:t,contentCheckOk:true,racyCheckOk:true};e.signatureTimestamp&&(s.playbackContext={contentPlaybackContext:{signatureTimestamp:e.signatureTimestamp}});const a=`${Zt}/youtubei/v1/player?key=${encodeURIComponent(e.apiKey)}`,l=await this.fetchFn(a,{method:"POST",headers:{...Re,"content-type":"application/json",...e.visitorData?{"x-goog-visitor-id":e.visitorData}:{}},body:JSON.stringify(s),...Be(o)});if(!l.ok)throw new Error(`Player API request failed with status ${l.status}`);const c=await l.json(),u=!!c.streamingData?.formats?.length,d=!!c.streamingData?.adaptiveFormats?.length;if(!u&&!d)throw new Error("Player response did not contain streaming formats");return c}};const Fo="bestefficiency",Bf=3e4;function Ff(n){if(n<=0)throw new RangeError("Audio downloader. ytAudio. chunkSize must be > 0")}function Nf({signal:n,timeoutMs:t}){return async(e,i={})=>await Q(e,{...i,signal:i.signal??n,forceGmXhr:true,timeout:t})}function $f(n){return n instanceof Ss?true:n instanceof Error&&/failed to load watch page:\s*403/i.test(n.message)}async function Hf({videoId:n,signal:t},e={}){const i=e.chunkSize??Y.minChunkSize;Ff(i);const o=Nf({signal:t,timeoutMs:e.fetchTimeoutMs??Bf}),r=e.createDownloader?.(o)??new Rf({fetchImplementation:o});try{const u=await r.downloadAudioToChunkStream({videoId:n,videoQuality:Fo,signal:t},{chunkSize:i});return {fileId:Oo(ye.WEB_API_STEAL_SIG_AND_N,u.itag,String(u.fileSize),i),mediaPartsLength:u.mediaPartsLength,getMediaBuffers:u.getMediaBuffers}}catch(u){if($f(u))throw u;console.warn("[VOT] ytAudio streaming mode failed, falling back to buffered mode",u);}const a=(await r.downloadAudioToUint8Array({videoId:n,videoQuality:Fo,signal:t})).bytes;if(!a||a.byteLength===0)throw new Error("Audio downloader. ytAudio. Empty audio");const l=Math.max(1,Math.ceil(a.byteLength/i));return {fileId:Oo(ye.WEB_API_STEAL_SIG_AND_N,0,String(a.byteLength),i),mediaPartsLength:l,async*getMediaBuffers(){for(let u=0;u=Gf&&(e+=1),e>=60)return t("translationTakeMoreThanHour");if(e<=1)return t("translationTakeAboutMinute");const r=String(e);return e!==11&&e%10===1?t("translationTakeApproximatelyMinute2").replace("{0}",r):![12,13,14].includes(e)&&[2,3,4].includes(e%10)?t("translationTakeApproximatelyMinute").replace("{0}",r):t("translationTakeApproximatelyMinutes").replace("{0}",r)}class nt extends Error{name="VOTLocalizedError";unlocalizedMessage;localizedMessage;constructor(t){super(v.getDefault(t)),this.unlocalizedMessage=t,this.localizedMessage=v.get(t);}}function bi(n){return n??null}async function Yf(n,t){const e=await n.translateVideoImpl(t.videoData,t.requestLang,t.responseLang,bi(t.translationHelp),!t.useAudioDownload,t.signal);return e?.url?{url:e.url,usedLivelyVoice:!!e.usedLivelyVoice}:null}function Xf(n){return {videoId:n.videoId,from:n.requestLang,to:n.responseLang,url:n.downloadTranslationUrl??n.fallbackUrl,useLivelyVoice:n.usedLivelyVoice}}async function ks(n){return n.isActionStale(n.actionContext)||(await n.updateTranslation(n.url,n.actionContext),n.isActionStale(n.actionContext))?false:(n.scheduleTranslationRefresh(),true)}async function Jf(n){const t=await Yf(n.requester,{videoData:n.request.videoData,requestLang:n.request.requestLang,responseLang:n.request.responseLang,translationHelp:n.request.translationHelp,useAudioDownload:n.request.useAudioDownload,signal:n.request.signal});return !t||!await ks({url:t.url,actionContext:n.actionContext,isActionStale:n.isActionStale,updateTranslation:n.updateTranslation,scheduleTranslationRefresh:n.scheduleTranslationRefresh})||n.isActionStale(n.actionContext)?null:t}function jf(n){n.setTranslation(n.cacheKey,Xf({videoId:n.videoId,requestLang:n.requestLang,responseLang:n.responseLang,fallbackUrl:n.fallbackUrl,downloadTranslationUrl:n.downloadTranslationUrl,usedLivelyVoice:n.usedLivelyVoice}));}function Ts(n){return n.aborted||!n.translateApiErrorsEnabled||!n.hadAsyncWait?n.hadAsyncWait:(n.notify({videoId:n.videoId,message:n.error}),false)}function Ls(n){if(!n||typeof n!="object")return null;const t=n,e=t.data&&typeof t.data=="object"?t.data:void 0;return {name:t.name,message:t.message,data:e}}function Zf(n){const e=Ls(n)?.data?.message;return typeof e=="string"&&e.length>0?e:void 0}function Qf(n){const t=Ls(n);if(!t||t.name!=="VOTJSError")return n;const e=typeof t.message=="string"?t.message:"",i=typeof t.data?.message=="string"&&t.data.message.length>0;return e==="Yandex couldn't translate video"&&!i?new nt("requestTranslationFailed"):e==="Failed to request video translation"?new nt("requestTranslationFailed"):e==="Audio link wasn't received"||e==="Audio link wasn't received from VOT response"?new nt("audioNotReceived"):n}class tp{videoHandler;audioDownloader;downloading;downloadWaiters=new Set;requestedFailAudio=new Set;constructor(t){this.videoHandler=t,this.audioDownloader=new qf,this.downloading=false,this.audioDownloader.addEventListener("downloadedAudio",this.onDownloadedAudio).addEventListener("downloadedPartialAudio",this.onDownloadedPartialAudio).addEventListener("downloadAudioError",this.onDownloadAudioError);}onDownloadedAudio=async(t,e)=>{if(!this.downloading)return;const{videoId:i,fileId:o,audioData:r}=e,s=this.getCanonicalUrl(i);try{await this.videoHandler.votClient.requestVtransAudio(s,t,{audioFile:r,fileId:o});}catch{this.finishDownloadFailure(new Error("Audio downloader failed while uploading full audio"));return}this.finishDownloadSuccess();};onDownloadedPartialAudio=async(t,e)=>{if(!this.downloading)return;const{audioData:i,fileId:o,videoId:r,amount:s,version:a,index:l}=e,c=this.getCanonicalUrl(r);try{await this.videoHandler.votClient.requestVtransAudio(c,t,{audioFile:i,chunkId:l},{audioPartsLength:s,fileId:o,version:a});}catch{this.finishDownloadFailure(new Error("Audio downloader failed while uploading chunk"));return}l===s-1&&this.finishDownloadSuccess();};onDownloadAudioError=async t=>{if(!this.downloading)return;const e=this.getCanonicalUrl(t);if(!(this.videoHandler.site.host==="youtube"&&!!this.videoHandler.data?.useAudioDownload)){this.finishDownloadFailure(new nt("VOTFailedDownloadAudio"));return}try{this.requestedFailAudio.has(e)?V.log("fail-audio-js request already sent for this video"):(V.log("Sending fail-audio-js request"),await this.videoHandler.votClient.requestVtransFailAudio(e),this.requestedFailAudio.add(e)),this.finishDownloadSuccess();}catch{this.finishDownloadFailure(new nt("VOTFailedDownloadAudio"));}};finishDownloadSuccess(){this.downloading=false,this.resolveDownloadWaiters();}finishDownloadFailure(t){this.downloading=false,this.rejectDownloadWaiters(t);}getCanonicalUrl(t){return `https://youtu.be/${t}`}isLivelyVoiceUnavailableError(t){const e=gn(t);return !!e&&e.toLowerCase().includes("обычная озвучка")}scheduleRetry(t,e,i){return new Promise((o,r)=>{let s=null;const a=()=>{s!==null&&clearTimeout(s),i.removeEventListener("abort",l);},l=()=>{a(),r(Bt());};if(i.addEventListener("abort",l,{once:true}),i.aborted){l();return}s=setTimeout(async()=>{if(i.aborted){l();return}a();try{const c=await t();o(c);}catch(c){r(c);}},e),s!==null&&(this.videoHandler.autoRetry=s);})}async translateVideoImpl(t,e,i,o=null,r=false,s=gs,a=false){clearTimeout(this.videoHandler.autoRetry),this.finishDownloadSuccess();const l=this.videoHandler.getRequestLangForTranslation(e,i);let c=a;try{So(s);const u=this.videoHandler.isLivelyVoiceAllowed(l,i);let d=!c&&u&&!!this.videoHandler.data?.useLivelyVoice,h;for(let g=0;g<2;g++){try{h=await this.videoHandler.votClient.translateVideo({videoData:t,requestLang:l,responseLang:i,translationHelp:o,extraOpts:{useLivelyVoice:d,videoTitle:this.videoHandler.videoData?.title},shouldSendFailedAudio:r});}catch(y){if(d&&this.isLivelyVoiceUnavailableError(y)){V.log("[translateVideoImpl] Lively voices are unavailable. Falling back to standard translation.",y),c=!0,d=!1;continue}throw y}if(d&&this.isLivelyVoiceUnavailableError(h)){V.log("[translateVideoImpl] Server responded that lively voices are unavailable. Falling back to standard translation.",h),c=!0,d=!1,h=void 0;continue}break}if(!h)throw new Error("Failed to get translation response");if(V.log("Translate video result",h),So(s),h.translated&&h.remainingTime<1)return V.log("Video translation finished with this data: ",h),{...h,usedLivelyVoice:d};const f=h.message??v.get("translationTakeFewMinutes");if(await this.videoHandler.updateTranslationErrorMsg(h.remainingTime>0?Kf(h.remainingTime,g=>v.get(g)):f,s),h.status===dt.AUDIO_REQUESTED&&this.videoHandler.isYouTubeHosts())return this.videoHandler.hadAsyncWait=!0,V.log("Start audio download"),this.downloading=!0,await this.audioDownloader.runAudioDownload(t.videoId,h.translationId,s),V.log("waiting downloading finish"),await this.waitForAudioDownloadCompletion(s,15e3),await this.translateVideoImpl(t,e,i,o,!0,s,c)}catch(u){if(mn(u))return null;const d=Qf(u);return await this.videoHandler.updateTranslationErrorMsg(Zf(d)??d,s),this.videoHandler.hadAsyncWait=Ts({aborted:!!this.videoHandler.actionsAbortController?.signal?.aborted,translateApiErrorsEnabled:!!this.videoHandler.data?.translateAPIErrors,hadAsyncWait:this.videoHandler.hadAsyncWait,videoId:t.videoId,error:u,notify:h=>this.videoHandler.notifier.translationFailed(h)}),console.error("[VOT]",u),null}return this.videoHandler.hadAsyncWait=true,this.scheduleRetry(()=>this.translateVideoImpl(t,e,i,o,r,s,c),2e4,s)}waitForAudioDownloadCompletion(t,e){return this.downloading?new Promise((i,o)=>{let r;const s=()=>{l(),o(Bt());},a=setTimeout(()=>{l(),i();},e),l=()=>{clearTimeout(a),t.removeEventListener("abort",s),this.downloadWaiters.delete(r);};r={resolve:()=>{l(),i();},reject:c=>{l(),o(c);}},this.downloadWaiters.add(r),t.addEventListener("abort",s,{once:true}),t.aborted&&s();}):Promise.resolve()}resolveDownloadWaiters(){this.forEachDownloadWaiter(t=>t.resolve());}rejectDownloadWaiters(t){this.forEachDownloadWaiter(e=>e.reject(t));}forEachDownloadWaiter(t){if(!this.downloadWaiters.size)return;const e=Array.from(this.downloadWaiters);this.downloadWaiters.clear();for(const i of e)t(i);}}class ep{state={status:"idle"};deps;constructor(t){this.deps=t;}get currentState(){return this.state}setState(t){this.state=t;}reset(){this.setState({status:"idle"});}async runAutoTranslationIfEligible(){if(this.state.status==="idle"&&this.deps.isFirstPlay()&&this.deps.isAutoTranslateEnabled()&&this.deps.getVideoId()){if(this.deps.isMobileYouTubeMuted?.()){this.setState({status:"deferred",reason:"muted"}),this.deps.setMuteWatcher?.(()=>{this.setState({status:"idle"}),this.runAutoTranslationIfEligible();});return}this.setState({status:"pending",reason:"auto"});try{await this.deps.scheduleAutoTranslate(),this.deps.setFirstPlay(!1),this.reset();}catch(t){throw this.setState({status:"error",message:t}),t}}}}function np(n,t={}){const{requireVideoData:e=false,clearVideoData:i=false}=t;e&&!n.videoData||(i&&(n.videoData=void 0),n.stopTranslation(),n.resetSubtitlesWidget());}function Xe(n,t={}){const{hideMenu:e=false}=t;n?.votButton?.container&&(n.votButton.container.hidden=true),e&&n?.votMenu&&(n.votMenu.hidden=true);}function As(n,t,e={}){const{requireVideoData:i,clearVideoData:o,hideMenu:r}=e;np(n,{requireVideoData:i,clearVideoData:o}),Xe(t,{hideMenu:r});}class ip{host;lifecycleGeneration=0;lastSetCanPlaySourceKey="";activeSetCanPlaySourceKey="";setCanPlayRequested=false;setCanPlayLoopPromise;constructor(t){this.host=t;}isStale(t){return t!==this.lifecycleGeneration}resetActions(t){if(typeof this.host.resetActionsAbortController=="function"){this.host.resetActionsAbortController(t);return}this.host.actionsAbortController?.abort(t);}invalidateActiveSession(t){this.lifecycleGeneration!==0&&(this.lifecycleGeneration+=1,this.resetActions(`[VideoLifecycle] ${t}`),V.log(`[VideoLifecycle] cancelled active session (active: ${this.lifecycleGeneration})`,{reason:t}));}startSession(t){this.lifecycleGeneration+=1;const e=this.lifecycleGeneration;return this.resetActions(`[VideoLifecycle][session:${e}] ${t}`),e}shouldAbortHandleSrcChanged(t,e){return this.isStale(t)?(V.log(`[VideoLifecycle][session:${t}] handleSrcChanged aborted at ${e} (active: ${this.lifecycleGeneration})`),true):false}showOverlayButton(t){t.votButton.container.hidden=false,t.votButton.opacity=1,this.host.queueOverlayAutoHide?.();}teardown(){this.setCanPlayRequested=false,this.invalidateActiveSession("teardown");}getCurrentSourceKey(){const t=this.host.video.srcObject?"1":"0";if(this.host.site.host==="youtube"){const i=globalThis.location.pathname;return `${`${globalThis.location.origin}${i}${globalThis.location.search}`}||${t}`}const e=this.host.video.currentSrc||this.host.video.src||"";return `${globalThis.location.href}||${e}||${t}`}resolveContainer(){const{site:t,video:e,container:i}=this.host;if(!t.selector)return e.parentElement??i;const o=ys(e,t.selector);return o||(i.isConnected&&ke(i,e)?i:e.parentElement??i)}async setCanPlay(){if(this.setCanPlayRequested=true,this.setCanPlayLoopPromise!==void 0){const e=this.getCurrentSourceKey();return this.activeSetCanPlaySourceKey&&e!==this.activeSetCanPlaySourceKey&&this.invalidateActiveSession("setCanPlay source changed while previous trigger is running"),await this.setCanPlayLoopPromise}const t=(async()=>{for(;this.setCanPlayRequested;)this.setCanPlayRequested=false,await this.runSetCanPlayOnce();})();this.setCanPlayLoopPromise=t;try{await t;}finally{this.setCanPlayLoopPromise===t&&(this.setCanPlayLoopPromise=void 0);}}async runSetCanPlayOnce(){const t=this.getCurrentSourceKey();if(this.host.videoData?.videoId&&t===this.lastSetCanPlaySourceKey)return;let e;try{e=await this.host.getVideoData();}catch{this.host.videoData=void 0,Xe(this.host.uiManager.votOverlayView,{hideMenu:true});return}if(this.getCurrentSourceKey()!==t)return;this.host.videoData=e,this.activeSetCanPlaySourceKey=t;const i=this.startSession(`setCanPlay (source: ${t})`);try{if(await this.handleSrcChanged(i,t),this.isStale(i)){V.log(`[VideoLifecycle][session:${i}] setCanPlay aborted after src change (active: ${this.lifecycleGeneration})`);return}const o=this.runAutoSubtitlesIfEnabled(i);if(await this.host.translationOrchestrator.runAutoTranslationIfEligible(),this.isStale(i)){V.log(`[VideoLifecycle][session:${i}] auto-translation result ignored (stale session)`);return}if(await o,this.isStale(i)){V.log(`[VideoLifecycle][session:${i}] auto-subtitles result ignored (stale session)`);return}V.log(`[VideoLifecycle][session:${i}] setCanPlay finished`);}finally{this.activeSetCanPlaySourceKey===t&&(this.activeSetCanPlaySourceKey="");}}async runAutoSubtitlesIfEnabled(t){if(!(!this.host.data.autoSubtitles||!this.host.videoData?.videoId))try{await this.host.enableSubtitlesForCurrentLangPair();}catch{}}async handleSrcChanged(t,e){const i=typeof t=="number"?t:this.startSession("manual handleSrcChanged"),o=typeof e=="string"&&e.length>0?e:this.getCurrentSourceKey();if(this.shouldAbortHandleSrcChanged(i,"before start"))return;this.host.translationOrchestrator.reset(),this.host.firstPlay=true;const r=this.host.uiManager.votOverlayView;As(this.host,r,{requireVideoData:true}),!this.host.video.src&&!this.host.video.currentSrc&&!this.host.video.srcObject&&Xe(r,{hideMenu:true});const a=this.resolveContainer();if(a!==this.host.container&&(this.host.container=a),this.shouldAbortHandleSrcChanged(i,"before getVideoData")||(this.showOverlayButton(r),this.shouldAbortHandleSrcChanged(i,"after getVideoData")))return;if(!this.host.videoData?.videoId){Xe(r,{hideMenu:true});return}const l=this.host.getSubtitlesCacheKey(this.host.videoData.videoId,this.host.videoData.detectedLanguage,this.host.videoData.responseLanguage),c=this.host.cacheManager.getSubtitles(l);this.host.subtitles=c??[],this.host.subtitlesCacheKey=c!==void 0?l:null,await this.host.updateSubtitlesLangSelect(),!this.shouldAbortHandleSrcChanged(i,"after subtitles update")&&(this.host.translateToLang=this.host.data.responseLanguage??"ru",this.host.setSelectMenuValues(this.host.videoData.detectedLanguage,this.host.videoData.responseLanguage),this.showOverlayButton(r),this.lastSetCanPlaySourceKey=o);}}const op=450,rp=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"),sp=/[\p{N}\p{P}\p{S}]+/gu,ap=/\s+/g,lp=new RegExp("\\p{L}","u");function cp(n,t){return n.length<=t?n:n.slice(0,t).trimEnd()}function up(n,t){const e=`${n??""} ${t??""}`.trim();if(!e)return "";const i=e.normalize("NFKC").replace(rp," ").replace(sp," ").replace(ap," ").trim();return lp.test(i)?cp(i,op):""}const Is=5e3,Cs=Number.MAX_SAFE_INTEGER;let Ne=null,$o=0,$e=null,Ho=0;async function dp(){const n=Date.now();if(Ne&&n-$oo){const s=_t(n,"up",i);return Math.min(r,s)}return _t(n,"nearest",i)}const Vs=new Set(["youtube","googledrive"]),Sp=Vs,xp=new Set(["rutube","ok"]),kp=new Set(["youtube","invidious","piped"]);function He(n){return Vs.has(n)}function Ms(n){return Sp.has(n)}function Tp(n){return xp.has(n)}function _s(n){return Ms(n.host)&&n.additionalData!=="mobile"}function Lp(n){return kp.has(n)}const Ap={rutube:"ru","ok.ru":"ru",mail_ru:"ru",weverse:"ko",niconico:"ja",youku:"zh",bilibili:"zh",weibo:"zh",zdf:"de"},zo=".ytp-volume-panel [aria-valuenow]",Ip=35,Cp=500,Ep=new Set(Lt),Xt=new Map;function Ut(n){const t=Xt.get(n);if(t)return t;const e={};for(Xt.set(n,e);Xt.size>Cp;){const i=Xt.keys().next().value;if(typeof i!="string")break;Xt.delete(i);}return e}function Vt(n){if(typeof n!="string")return;const t=n.toLowerCase().split(/[-_]/)[0];return Ep.has(t)?t:void 0}function Ot(n){return !!(n&&n!=="auto")}function Pp(n,t){return up(typeof n=="string"?n:"",typeof t=="string"?t:void 0)}function Vp(n){const t=Ap[n];if(t)return t;if(n==="vk"){const e=document.getElementsByTagName("track")?.[0]?.srclang;return Vt(e)}}function Mp(n){if(!Array.isArray(n)||n.length===0)return;const t=e=>{for(const i of n){if(!i||typeof i!="object")continue;const o=i;if(o.source!=="youtube"||typeof o.translatedFromLanguage=="string"||e&&o.isAutoGenerated===true)continue;const r=Vt(o.language);if(Ot(r))return r}};return t(true)??t(false)}async function Wo(n){if(n.isStream)return {detectedLanguage:"auto"};if(n.userOverrideLanguage)return {detectedLanguage:n.userOverrideLanguage};const t=Vp(n.host);if(Ot(t))return {detectedLanguage:t,cacheLanguage:t};const e=Vt(n.possibleLanguage);if(Ot(e))return {detectedLanguage:e,cacheLanguage:e};const i=n.host==="youtube"?Mp(n.subtitles):void 0;if(Ot(i))return {detectedLanguage:i,cacheLanguage:i};if(n.cachedDetectedLanguage)return {detectedLanguage:n.cachedDetectedLanguage};if(!n.allowTextLanguageDetection)return {detectedLanguage:"auto"};const o=Pp(n.title,n.description);if(!o||o.length0?ft(o/r*100):ft(o):null}class _p{videoHandler;constructor(t){this.videoHandler=t;}setDetectedLanguageCache(t,e){Ut(t).detectedLanguage=e;}rememberUserLanguageSelection(t,e){const i=Vt(e);if(!Ot(i)){const r=Xt.get(t);r&&delete r.userLanguageOverride;return}const o=Ut(t);o.userLanguageOverride=i,o.detectedLanguage=i;}rememberDetectedLanguage(t,e){const i=Vt(e);Ot(i)&&(this.setDetectedLanguageCache(t,i),this.videoHandler.videoData?.videoId===t&&(this.videoHandler.videoData.detectedLanguage=i));}async detectLanguageSingleFlight(t,e){const i=Ut(t),o=i.detectInFlight;if(o!==void 0)return o;const r=(async()=>{const s=Vt(await pp(e));return Ot(s)?s:void 0})();i.detectInFlight=r;try{return await r}finally{i.detectInFlight===r&&delete i.detectInFlight;}}async ensureDetectedLanguageForTranslation(t){if(!t?.videoId||t.detectedLanguage!=="auto")return;const e=Ut(t.videoId),{detectedLanguage:i,cacheLanguage:o}=await Wo({isStream:t.isStream,host:this.videoHandler.site.host,possibleLanguage:t.detectedLanguage,subtitles:t.subtitles,userOverrideLanguage:e.userLanguageOverride,cachedDetectedLanguage:e.detectedLanguage,title:t.title,description:t.description,allowTextLanguageDetection:true,detectLanguage:async r=>await this.detectLanguageSingleFlight(t.videoId,r)});o&&this.setDetectedLanguageCache(t.videoId,o),i!=="auto"&&(t.detectedLanguage=i,this.videoHandler.translateFromLang==="auto"&&(this.videoHandler.translateFromLang=i));}async getVideoData(){const{duration:t,url:e,videoId:i,host:o,title:r,translationHelp:s=null,localizedTitle:a,description:l,detectedLanguage:c,subtitles:u,isStream:d=false}=await Fc(this.videoHandler.site,{fetchFn:Q,video:this.videoHandler.video,language:v.lang}),h=Ut(i),{detectedLanguage:f,cacheLanguage:g}=await Wo({isStream:d,host:this.videoHandler.site.host,possibleLanguage:c,subtitles:u,userOverrideLanguage:h.userLanguageOverride,cachedDetectedLanguage:h.detectedLanguage,title:r,description:l,allowTextLanguageDetection:false,detectLanguage:async w=>await this.detectLanguageSingleFlight(i,w)});g&&this.setDetectedLanguageCache(i,g);const y={translationHelp:s,isStream:d,duration:t||this.videoHandler.video?.duration||Y.defaultDuration,videoId:i,url:e,host:o,detectedLanguage:f,responseLanguage:this.videoHandler.translateToLang,subtitles:u,title:r,localizedTitle:a,description:l,downloadTitle:a??r??i};return h.lastLoggedDetectedLanguage!==f&&(console.log("[VOT] Detected language:",f),h.lastLoggedDetectedLanguage=f),y}async videoValidator(){const t=this.videoHandler.videoData,e=this.videoHandler.data;if(!t||!e)throw new nt("VOTNoVideoIDFound");if(V.log("VideoValidator videoData: ",this.videoHandler.videoData),this.videoHandler.data.enabledDontTranslateLanguages&&this.videoHandler.data.dontTranslateLanguages?.includes(this.videoHandler.videoData.detectedLanguage))throw new nt("VOTDisableFromYourLang");if(this.videoHandler.videoData.isStream)throw new nt("VOTStreamNotAvailable");if(this.videoHandler.videoData.duration>14400)throw new nt("VOTVideoIsTooLong");return true}getVideoVolume(){const t=this.videoHandler.video;if(t){if(He(this.videoHandler.site.host)){const e=qo(zo);if(e!=null)return bp(e);const i=B.getVolume();if(typeof i=="number"&&Number.isFinite(i))return _t(i)}return _t(t.volume)}}setVideoVolume(t){const e=_t(t);if(!He(this.videoHandler.site.host))return this.videoHandler.video.volume=e,this;try{const i=B.setVolume(e);if(typeof i=="boolean"&&i||typeof i=="number"&&Number.isFinite(i))return this}catch{}return this.videoHandler.video.volume=e,this}isMuted(){return He(this.videoHandler.site.host)?B.isMuted():this.videoHandler.video?.muted}syncVideoVolumeSlider(){const t=this.videoHandler.uiManager.votOverlayView;if(!t?.isInitialized())return this;const e=He(this.videoHandler.site.host)?qo(zo):null,i=this.isMuted()?0:e??Ps(this.getVideoVolume()??0);return t.videoVolumeSlider.value=i,this.videoHandler.onVideoVolumeSliderSynced?.(i),this}setSelectMenuValues(t,e){const i=this.videoHandler.videoData;if(!i)return this;const o=Vt(t)??"auto",r=`${o}->${e}`,s=Ut(i.videoId);s.lastLoggedLangPair!==r&&(console.log(`[VOT] Set translation from ${o} to ${e}`),s.lastLoggedLangPair=r),i.detectedLanguage=o,i.responseLanguage=e,this.videoHandler.translateFromLang=o,this.videoHandler.translateToLang=e;const a=this.videoHandler.uiManager.votOverlayView;return a?.isInitialized()?(a.languagePairSelect.fromSelect.selectTitle=v.getLangLabel(o),a.languagePairSelect.toSelect.selectTitle=v.getLangLabel(e),a.languagePairSelect.fromSelect.setSelectedValue(o),a.languagePairSelect.toSelect.setSelectedValue(e),this):this}}const yi=globalThis,Go=n=>n,tn=yi.trustedTypes,Ko=tn?tn.createPolicy("lit-html",{createHTML:n=>n}):void 0,Os="$lit$",kt=`lit$${Math.random().toFixed(9).slice(2)}$`,Ds="?"+kt,Op=`<${Ds}>`,Ft=document,Te=()=>Ft.createComment(""),Le=n=>n===null||typeof n!="object"&&typeof n!="function",wi=Array.isArray,Dp=n=>wi(n)||typeof n?.[Symbol.iterator]=="function",En=`[ -\f\r]`,ue=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,Yo=/-->/g,Xo=/>/g,Ct=RegExp(`>|${En}(?:([^\\s"'>=/]+)(${En}*=${En}*(?:[^ -\f\r"'\`<>=]|("|')|))|$)`,"g"),Jo=/'/g,jo=/"/g,Rs=/^(?:script|style|textarea|title)$/i,Bs=n=>(t,...e)=>({_$litType$:n,strings:t,values:e}),fe=Bs(1),rt=Bs(2),Ae=Symbol.for("lit-noChange"),q=Symbol.for("lit-nothing"),Zo=new WeakMap,Mt=Ft.createTreeWalker(Ft,129);function Fs(n,t){if(!wi(n)||!n.hasOwnProperty("raw"))throw Error("invalid template strings array");return Ko!==void 0?Ko.createHTML(t):t}const Rp=(n,t)=>{const e=n.length-1,i=[];let o,r=t===2?"":t===3?"":"",s=ue;for(let a=0;a"?(s=o??ue,d=-1):u[1]===void 0?d=-2:(d=s.lastIndex-u[2].length,c=u[1],s=u[3]===void 0?Ct:u[3]==='"'?jo:Jo):s===jo||s===Jo?s=Ct:s===Yo||s===Xo?s=ue:(s=Ct,o=void 0);const f=s===Ct&&n[a+1].startsWith("/>")?" ":"";r+=s===ue?l+Op:d>=0?(i.push(c),l.slice(0,d)+Os+l.slice(d)+kt+f):l+kt+(d===-2?a:f);}return [Fs(n,r+(n[e]||"")+(t===2?"":t===3?"":"")),i]};class Ie{constructor({strings:t,_$litType$:e},i){let o;this.parts=[];let r=0,s=0;const a=t.length-1,l=this.parts,[c,u]=Rp(t,e);if(this.el=Ie.createElement(c,i),Mt.currentNode=this.el.content,e===2||e===3){const d=this.el.content.firstChild;d.replaceWith(...d.childNodes);}for(;(o=Mt.nextNode())!==null&&l.length0){o.textContent=tn?tn.emptyScript:"";for(let f=0;f2||i[0]!==""||i[1]!==""?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=q;}_$AI(t,e=this,i,o){const r=this.strings;let s=false;if(r===void 0)t=te(this,t,e,0),s=!Le(t)||t!==this._$AH&&t!==Ae,s&&(this._$AH=t);else {const a=t;let l,c;for(t=r[0],l=0;l{const i=t;let o=i._$litPart$;return o===void 0&&(i._$litPart$=o=new Me(t.insertBefore(Te(),null),null,void 0,{})),o._$AI(n),o},zp='.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 (min-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(max-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:"";inherits:true;initial-value:.8}@property --vot-subtitles-scale-compensation{syntax:"";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);width:max-content;background:var(--vot-subtitles-background,#2e2f34cc);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;margin:0 auto;display:block}.vot-subtitles.vot-subtitles--clamped{overflow:hidden}@supports (line-clamp:2){.vot-subtitles.vot-subtitles--clamped{line-clamp:2}}@supports not (line-clamp:2){.vot-subtitles.vot-subtitles--clamped{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box}}.vot-subtitles{text-wrap:balance;white-space:normal;overflow-wrap:anywhere}.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:42ch;--vot-subtitles-smart-min-width-ratio:.55;--vot-subtitles-smart-max-width-ratio:.68;--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-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"].passed{color:var(--vot-subtitles-passed-color,#2196f3)}.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-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(max-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(min-aspect-ratio:1)and (max-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(max-width:900px)and (pointer:coarse){.vot-subtitles-widget{--vot-subtitles-fallback-bottom-inset:env(safe-area-inset-bottom,0px)}}:-webkit-any(:-webkit-full-screen .vot-subtitles-widget,:-webkit-full-screen .vot-subtitles-widget){--vot-subtitles-smart-max-width-ratio:.8}:is(:fullscreen .vot-subtitles-widget){--vot-subtitles-smart-max-width-ratio:.8}:-webkit-any(:-webkit-full-screen .vot-subtitles,:-webkit-full-screen .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))}:is(: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;-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}';ka(zp);function Wp(){if(globalThis.__votKeyboardNavInitialized)return;globalThis.__votKeyboardNavInitialized=true;const n=document.documentElement,t="vot-keyboard-nav",e=()=>n.classList.add(t),i=()=>n.classList.remove(t);globalThis.addEventListener("keydown",o=>{o.key==="Tab"&&e();},true);for(const o of ["pointerdown","mousedown","touchstart"])globalThis.addEventListener(o,i,{capture:true,passive:true});}Wp();const S={makeButtonLike(n,{ariaLabel:t}={}){n.setAttribute("role","button"),n.hasAttribute("tabindex")||(n.tabIndex=0);const e=n.tabIndex,i=()=>{n.getAttribute("disabled")==="true"?(n.setAttribute("aria-disabled","true"),n.tabIndex=-1):(n.removeAttribute("aria-disabled"),n.tabIndex=e);};return i(),new MutationObserver(()=>i()).observe(n,{attributes:true,attributeFilter:["disabled"]}),t&&n.setAttribute("aria-label",t),n.addEventListener("keydown",o=>{n.getAttribute("disabled")==="true"||n.getAttribute("aria-disabled")==="true"||(o.key==="Enter"||o.key===" ")&&(o.preventDefault(),n.click());}),n},createEl(n,t=[],e=null){const i=document.createElement(n);return t.length&&i.classList.add(...t),e!==null&&i.append(e),i},createHeader(n,t=4){return S.createEl("vot-block",["vot-header",`vot-header-level-${t}`],n)},createInformation(n,t){const e=S.createEl("vot-block",["vot-info"]),i=S.createEl("vot-block");it(n,i);const o=S.createEl("vot-block");return it(t,o),e.append(i,o),{container:e,header:i,value:o}},createButton(n){const t=S.createEl("vot-block",["vot-button"],n);return S.makeButtonLike(t)},createTextButton(n){const t=S.createEl("vot-block",["vot-text-button"],n);return S.makeButtonLike(t)},createOutlinedButton(n){const t=S.createEl("vot-block",["vot-outlined-button"],n);return S.makeButtonLike(t)},createIconButton(n,t={}){const e=S.createEl("vot-block",["vot-icon-button"]);return it(n,e),S.makeButtonLike(e,t)},createInlineLoader(){return S.createEl("vot-block",["vot-inline-loader"])},createPortal(n=false){return S.createEl("vot-block",[`vot-portal${n?"-local":""}`])},createSubtitleInfo(n,t,e){const i=S.createEl("vot-block",["vot-subtitles-info"]);i.id="vot-subtitles-info";const o=S.createEl("vot-block",["vot-subtitles-info-service"],v.get("VOTTranslatedBy").replace("{0}",e)),r=S.createEl("vot-block",["vot-subtitles-info-header"],n),s=S.createEl("vot-block",["vot-subtitles-info-context"],t);return i.append(o,r,s),{container:i,translatedWith:o,header:r,context:s}}},qp=["left","top","right","bottom"],Gp=["hover","click"];class K{showed=false;target;anchor;content;position;preferredPosition;trigger;parentElement;layoutRoot;offsetX;offsetY;_hidden;autoLayout;pageWidth;pageHeight;globalOffsetX;globalOffsetY;maxWidth;backgroundColor;borderRadius;_bordered;container;onResizeObserver;intersectionObserver;scrollListening=false;positionRafId=null;destroyFallbackTimerId;static DESTROY_FALLBACK_MS=700;tooltipId=typeof crypto<"u"&&"randomUUID"in crypto?crypto.randomUUID():`vot-tooltip-${Math.random().toString(36).slice(2)}`;prevAriaDescribedBy=null;constructor({target:t,anchor:e=void 0,content:i="",position:o="top",trigger:r="hover",offset:s=4,maxWidth:a=void 0,hidden:l=false,autoLayout:c=true,backgroundColor:u=void 0,borderRadius:d=void 0,bordered:h=true,parentElement:f=document.body,layoutRoot:g=document.documentElement}){if(!(t instanceof HTMLElement))throw new TypeError("target must be a valid HTMLElement");this.target=t,this.anchor=e instanceof HTMLElement?e:t,this.content=i,typeof s=="number"?this.offsetY=this.offsetX=s:(this.offsetX=s.x,this.offsetY=s.y),this._hidden=l,this.autoLayout=c,this.trigger=K.validateTrigger(r)?r:"hover",this.position=K.validatePos(o)?o:"top",this.preferredPosition=this.position,this.parentElement=f,this.layoutRoot=g,this.borderRadius=d,this._bordered=h,this.maxWidth=a,this.backgroundColor=u,this.updatePageSize(),this.init();}static validatePos(t){return qp.includes(t)}static validateTrigger(t){return Gp.includes(t)}setPosition(t){return this.preferredPosition=K.validatePos(t)?t:"top",this.position=this.preferredPosition,this.schedulePositionUpdate(),this}setContent(t){return this.content=t,this.container?(this.container.replaceChildren(),typeof t=="string"?this.container.textContent=t:this.container.append(t),this.schedulePositionUpdate(),this):this}updateMount({parentElement:t,layoutRoot:e}){return t&&this.parentElement!==t&&(this.parentElement=t,this.container?.isConnected&&t.appendChild(this.container)),e&&this.layoutRoot!==e&&(this.layoutRoot=e),this.schedulePositionUpdate(),this}onResize=()=>{this.schedulePositionUpdate();};onClick=()=>{this.showed?this.destroy():this.create();};onTargetKeyDown=t=>{t.key!=="Escape"||!this.showed||this.destroy();};onScroll=()=>{this.schedulePositionUpdate();};onHoverPointerDown=t=>{t.pointerType!=="mouse"&&this.create();};onHoverPointerUp=t=>{t.pointerType!=="mouse"&&this.destroy();};onMouseEnter=()=>{this.create();};onMouseLeave=t=>{this.isInTooltipContext(t.relatedTarget)||this.destroy();};onTooltipMouseLeave=t=>{this.isInTooltipContext(t.relatedTarget)||this.destroy();};isInTooltipContext(t){return t instanceof Node?this.target.contains(t)||this.container?.contains(t):false}updatePageSize(){if(this.layoutRoot===document.documentElement)this.globalOffsetX=0,this.globalOffsetY=0;else {const{left:t,top:e}=this.layoutRoot.getBoundingClientRect();this.globalOffsetX=t,this.globalOffsetY=e;}return this.pageWidth=this.layoutRoot.clientWidth||document.documentElement.clientWidth,this.pageHeight=this.layoutRoot.clientHeight||document.documentElement.clientHeight,this}onIntersect=([t])=>{if(!t.isIntersecting)return this.destroy(true)};init(){return this.onResizeObserver=new ResizeObserver(this.onResize),this.intersectionObserver=new IntersectionObserver(this.onIntersect),this.target.addEventListener("keydown",this.onTargetKeyDown),this.trigger==="click"?(this.target.addEventListener("pointerdown",this.onClick),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),this)}release(){return this.destroy(true),this.detachScrollListener(),this.target.removeEventListener("keydown",this.onTargetKeyDown),this.trigger==="click"?(this.target.removeEventListener("pointerdown",this.onClick),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),this)}schedulePositionUpdate(){this.container&&this.positionRafId===null&&(this.positionRafId=requestAnimationFrame(()=>{this.positionRafId=null,this.updatePageSize(),this.updatePos();}));}cancelPositionUpdate(){this.positionRafId!==null&&(cancelAnimationFrame(this.positionRafId),this.positionRafId=null);}clearDestroyFallbackTimer(){this.destroyFallbackTimerId!==void 0&&(globalThis.clearTimeout(this.destroyFallbackTimerId),this.destroyFallbackTimerId=void 0);}create(){return this.destroy(true),this.showed=true,this.container=S.createEl("vot-block",["vot-tooltip"],this.content),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(),this.backgroundColor!==void 0&&(this.container.style.backgroundColor=this.backgroundColor),this.borderRadius!==void 0&&(this.container.style.borderRadius=`${this.borderRadius}px`),this.hidden?this.container.hidden=true:this.syncAriaDescribedBy(true),this.container.style.opacity="1",this.trigger==="hover"&&this.container.addEventListener("mouseleave",this.onTooltipMouseLeave),this.attachScrollListener(),this.onResizeObserver?.observe(this.layoutRoot),this.anchor!==this.layoutRoot&&this.onResizeObserver?.observe(this.anchor),this.intersectionObserver?.observe(this.target),this}updatePos(){if(!this.container)return this;const{top:t,left:e}=this.calcPos(this.autoLayout,this.preferredPosition),i=Math.max(0,this.pageWidth-this.offsetX*2),o=$(this.maxWidth??i,0,i);return this.container.style.transform=`translate(${e}px, ${t}px)`,this.container.dataset.position=this.position,this.container.style.maxWidth=`${o}px`,this}calcPos(t=true,e=this.preferredPosition){if(!this.container)return {top:0,left:0};const{left:i,right:o,top:r,bottom:s,width:a,height:l}=this.anchor.getBoundingClientRect(),{width:c,height:u}=this.container.getBoundingClientRect(),d=$(c,0,this.pageWidth),h=$(u,0,this.pageHeight),f=i-this.globalOffsetX,g=o-this.globalOffsetX,y=r-this.globalOffsetY,w=s-this.globalOffsetY;let x=e;if(t)switch(e){case "top":{$(y-h-this.offsetY,0,this.pageHeight)+this.offsetYthis.pageWidth-this.offsetX&&(x="left");break}case "bottom":{$(w+this.offsetY,0,this.pageHeight-h)+h>this.pageHeight-this.offsetY&&(x="top");break}case "left":{Math.max(0,f-d-this.offsetX)+d>f-this.offsetX&&(x="right");break}}let m;switch(x){case "top":m={top:$(y-h-this.offsetY,0,this.pageHeight),left:$(f-d/2+a/2,this.offsetX,this.pageWidth-d-this.offsetX)};break;case "right":m={top:$(y+(l-h)/2,this.offsetY,this.pageHeight-h-this.offsetY),left:$(g+this.offsetX,0,this.pageWidth-d)};break;case "bottom":m={top:$(w+this.offsetY,0,this.pageHeight-h),left:$(f-d/2+a/2,this.offsetX,this.pageWidth-d-this.offsetX)};break;case "left":m={top:$(y+(l-h)/2,this.offsetY,this.pageHeight-h-this.offsetY),left:Math.max(0,f-d-this.offsetX)};break;default:m={top:0,left:0};}return this.position=x,{top:m.top+this.globalOffsetY,left:m.left+this.globalOffsetX}}destroy(t=false){if(!this.container)return this;const e=this.container;if(this.cancelPositionUpdate(),this.clearDestroyFallbackTimer(),this.showed=false,this.syncAriaDescribedBy(false),this.onResizeObserver?.disconnect(),this.intersectionObserver?.disconnect(),this.detachScrollListener(),t)return e.remove(),this.container=void 0,this;e.removeEventListener("mouseleave",this.onTooltipMouseLeave),e.style.pointerEvents="none",e.style.opacity="0";const i=()=>{this.clearDestroyFallbackTimer(),e?.remove(),this.container===e&&(this.container=void 0);};return e.addEventListener("transitionend",i,{once:true}),e.addEventListener("transitioncancel",i,{once:true}),this.destroyFallbackTimerId=globalThis.setTimeout(i,K.DESTROY_FALLBACK_MS),this}syncAriaDescribedBy(t){const e=this.target.getAttribute("aria-describedby");if(this.prevAriaDescribedBy??=e,!t){this.prevAriaDescribedBy===null?this.target.removeAttribute("aria-describedby"):this.target.setAttribute("aria-describedby",this.prevAriaDescribedBy),this.prevAriaDescribedBy=null;return}const i=new Set((e??"").split(/\s+/).filter(Boolean));i.add(this.tooltipId),this.target.setAttribute("aria-describedby",Array.from(i).join(" "));}set bordered(t){this._bordered=t,this.container?.classList.toggle("vot-tooltip-bordered",t);}get bordered(){return this._bordered}set hidden(t){this._hidden=t,this.container&&(this.container.hidden=t),this.showed&&this.syncAriaDescribedBy(!t);}get hidden(){return this._hidden}attachScrollListener(){this.scrollListening||(this.scrollListening=true,document.addEventListener("scroll",this.onScroll,{passive:true,capture:true}));}detachScrollListener(){this.scrollListening&&(this.scrollListening=false,document.removeEventListener("scroll",this.onScroll,{capture:true}));}}const Kp=["srt","vtt","ass","json"],Ns=["default-sans","arial","helvetica","roboto","verdana","open-sans","poppins","lato","montserrat","barlow"],Qo={"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'};function en(n){return Ns.includes(n)}const jn="google:",Yp="https://fonts.googleapis.com/css2",Xp="https://fonts.google.com/metadata/fonts",Jp={roboto:"Roboto","open-sans":"Open Sans",poppins:"Poppins",lato:"Lato",montserrat:"Montserrat",barlow:"Barlow"},Pn=new Set,Vn=new Map;let de=null;function jp(n){return `${jn}${n}`}function ee(n){if(n.startsWith(jn)){const t=n.slice(jn.length).trim();return t.length>0?t:null}return Jp[n]??null}function tr(n){if(en(n))return Qo[n];const t=ee(n);return t?`"${t}", "Segoe UI", system-ui, sans-serif`:Qo["default-sans"]}function Zp(n){const t=ee(n);if(!t)return null;const e=t.trim().replaceAll(/\s+/g,"+");return `${Yp}?family=${e}&display=swap`}function Qp(n,t){const e=`vot-google-font-${n}`;if(document.getElementById(e))return;const i=globalThis.GM_addStyle,o=typeof i=="function"?i(t):document.createElement("style");o instanceof HTMLElement&&(o.id=e,o.textContent||(o.textContent=t),o.parentElement||(document.head||document.documentElement).appendChild(o));}async function tg(n,t={}){if(Pn.has(n))return;const e=Vn.get(n);if(e!==void 0){await e;return}const i=Zp(n);if(!i){Pn.add(n);return}const o=ee(n),r=(async()=>{try{const s=await Q(i,{timeout:1e4,forceGmXhr:t.forceGmXhr??!0,headers:{Accept:"text/css,*/*;q=0.1"}});if(!s.ok)throw new Error(`Google Fonts CSS request failed with ${s.status}`);const a=await s.text();if(!a.trim())throw new Error("Google Fonts CSS response is empty");Qp(n,a),Pn.add(n),document.fonts&&o&&await document.fonts.load(`500 20px "${o}"`),t.onLoaded?.();}catch{}finally{Vn.delete(n);}})();Vn.set(n,r),await r;}async function eg(){return de!==null?await de:(de=(async()=>{const n=await Q(Xp,{timeout:15e3,forceGmXhr:xe});if(!n.ok)throw new Error(`Google Fonts metadata request failed with ${n.status}`);const e=(await n.text()).replace(/^\)\]\}'\n?/,""),o=JSON.parse(e).familyMetadataList?.map(r=>r.family?.trim()??"").filter(r=>r.length>0);return Array.from(new Set(o)).sort((r,s)=>r.localeCompare(s))})().catch(n=>(de=null,[])),await de)}class ng{video;container;fullscreenLayer=null;constructor({video:t,container:e}){this.video=t,this.container=e;}updateContainer(t){this.container=t;}getWidgetParentElement(){return this.shouldUseFullscreenViewportLayer()?this.ensureFullscreenLayer():this.container}getLayoutRootElement(){return this.fullscreenLayer?.isConnected?this.fullscreenLayer:this.container}syncWidgetContainer(t){const e=this.getWidgetParentElement();e===this.container&&getComputedStyle(this.container).position==="static"&&(this.container.style.position="relative"),t&&t.parentElement!==e&&e.appendChild(t),e===this.container&&this.fullscreenLayer?.parentElement&&(this.fullscreenLayer.remove(),this.fullscreenLayer=null);}release(){this.fullscreenLayer&&(this.fullscreenLayer.remove(),this.fullscreenLayer=null);}getActiveFullscreenElement(){const t=document,e=t.fullscreenElement??t.webkitFullscreenElement;return mi(e,[this.container,this.video],{allowDocumentViewport:true})}isCurrentVideoInFullscreenSession(){const t=this.getActiveFullscreenElement();return t?t===this.container||t.contains(this.container)||this.container.contains(t)?true:!!(this.video&&(t===this.video||t.contains(this.video)||this.video.contains(t))):false}shouldUseFullscreenViewportLayer(){return this.isCurrentVideoInFullscreenSession()}ensureFullscreenLayer(){if(!this.fullscreenLayer){const t=document.createElement("vot-block");t.classList.add("vot-subtitles-layer"),this.fullscreenLayer=t;}return this.fullscreenLayer.parentElement!==this.container&&this.container.appendChild(this.fullscreenLayer),this.fullscreenLayer}}function ig(n,t){return n>=t.startMs&&n>1,r=t[o];if(n=r.startMs+r.durationMs)e=o+1;else return o}return -1}function xt(n,t,e){return Math.max(t,Math.min(n,e))}function rg(n,t,e,i,o){const r=e-n,s=i-t;return r*r+s*s>=o*o}function er({anchorX:n,anchorY:t,elementWidth:e,elementHeight:i,boxWidth:o,boxHeight:r,bottomInset:s}){let a=n,l=t;const c=Math.max(0,r-s),u=i||0;if(e){let d=a-e/2;const h=o-e;h>=0?d=xt(d,0,h):d=h/2,a=d+e/2;}return l=xt(l,u,c),{anchorX:a,anchorY:l}}function nr({current:n,candidates:t,thresholdPx:e}){let i=n,o=Number.POSITIVE_INFINITY;for(const r of t){const s=Math.abs(r-n);se?{snapped:false,value:n}:{snapped:true,value:i}}const ir=(n,t)=>!!n?.italic==!!t?.italic&&!!n?.bold==!!t?.bold&&!!n?.underline==!!t?.underline;function sg(n,t,e){const i=[];let o="",r;for(let s=0;s<=t;){const a=n[s],l=a?.text??"";if(!l){s+=1;continue}if(l===` -`){o&&(i.push({kind:"text",text:o,style:r}),o="",r=void 0),i.push({kind:"break"}),s+=1;continue}if(a.isWordLike){let d=o+l;const h=o?r:a.style;o="",r=void 0;let f=s,y=!!e?.has(s)?s:null;for(;y===null&&f+1<=t;){const w=n[f+1];if(!w||w.isWordLike||w.text===` -`||!ir(w.style,h))break;if(d+=w.text,f+=1,e?.has(f)){y=f;break}}if(y!==null){for(i.push({kind:"word",text:d,style:h},{kind:"break"}),s=y+1;s<=t&&!n[s]?.isWordLike&&!n[s]?.text.trim();)s+=1;continue}i.push({kind:"word",text:d,style:h}),s=f+1;continue}const c=!!e?.has(s);if(!(l.trim().length===0)){c?(i.push({kind:"text",text:o+l,style:o?r:a.style},{kind:"break"}),o="",r=void 0):(o&&!ir(r,a.style)&&(i.push({kind:"text",text:o,style:r}),o=""),o+=l,r=a.style),s+=1;continue}o&&(i.push({kind:"text",text:o,style:r}),o="",r=void 0),c?i.push({kind:"text",text:l,style:a.style},{kind:"break"}):i.push({kind:"text",text:l,style:a.style}),s+=1;}return o&&i.push({kind:"text",text:o,style:r}),i}const ag=.55;function lg(n,t,e){return Number.isNaN(n)?t:Math.min(e,Math.max(t,n))}function cg(n){return n<1?28:n<1.4?32:42}function ug(n,t){const e=Math.max(1,n.w),i=Math.max(1,n.h),o=e/i;let r=cg(o);if(t){const{fontSizePx:a,maxWidthPx:l}=t;if(Number.isFinite(a)&&Number.isFinite(l)&&a>0&&l>0){const c=a*ag;c>0&&(r=l/c);}}return {maxLength:lg(Math.round(r*2),50,180)}}const or=n=>!!(n?.isWordLike&&n.text?.trim()),Nt=(n,t,e)=>Math.min(e,Math.max(t,n)),$s=(n,t,e)=>e$s(n.prefixWidths,t,e)-(n.trailingGapWidths[e]??0);function pg(n,t,e){if(e$s(n.prefixChars,t,e)-(n.trailingGapChars[e]??0);function Hs(n,t,e,i){if(e{const s=(n+t)/2,a=e-n,l=e-t,c=a*a+l*l,u=n-s,d=t-s,h=u*u+d*d,g=r>=4&&(i<=1||o<=1)?1e9:0,w=r>=6&&(i<=2||o<=2)?2e7:0;let x=c+h+g+w;return n>t&&(x+=(n-t)/Math.max(1,e)*.15),x};function Us(n,t,e,i){if(i<=0||!n.widths.length)return null;const o=Nt(t,0,n.widths.length-1),r=Nt(e,0,n.widths.length-1);if(r<=o)return null;const s=n.prefixWidths,a=n.trailingGapWidths,l=s[o],c=s[r+1],u=a[r]??0;let d=null,h=Number.POSITIVE_INFINITY;const f=r-o+1;for(let g=o;gi||x>i)continue;const m=g-o+1,k=r-g,P=mg(w,x,i,m,k,f);P>1;Hs(n,0,s,t)?(r=s,i=s+1):o=s-1;}return r}function bg(n,t){if(t<=0)return {breakAfterWordIndices:[],truncateAfterWordIndex:null};const e=n.widths.length;if(e<=1)return {breakAfterWordIndices:[],truncateAfterWordIndex:null};if(Si(n,0,e-1)<=t)return {breakAfterWordIndices:[],truncateAfterWordIndex:null};const o=zs(n,t);if(o.length)return {breakAfterWordIndices:o,truncateAfterWordIndex:null};const s=vg(n,t)??0;if(s>=e-1)return {breakAfterWordIndices:[],truncateAfterWordIndex:null};const a=Us(n,0,s,t);return {breakAfterWordIndices:a===null?[]:[a],truncateAfterWordIndex:s}}const xi=n=>n.endWord-n.startWord+1,yg=(n,t)=>{const e=[];let i=0;for(;i{if(t<=0)return false;const i=n[t],o=n[t-1];if(!i||!o||xi(o)<3)return false;const r=o.endWord,s={startWord:o.startWord,endWord:r-1},a={startWord:r,endWord:i.endWord};return !e(s.startWord,s.endWord)||!e(a.startWord,a.endWord)?false:(o.endWord=s.endWord,i.startWord=a.startWord,true)},Sg=(n,t,e)=>{if(t>=n.length-1)return false;const i=n[t],o=n[t+1];if(!i||!o||xi(o)<3)return false;const r=o.startWord,s={startWord:i.startWord,endWord:r},a={startWord:r+1,endWord:o.endWord};return !e(s.startWord,s.endWord)||!e(a.startWord,a.endWord)?false:(i.endWord=s.endWord,o.startWord=a.startWord,true)},xg=(n,t)=>{for(let e=n.length-1;e>=0;e-=1)xi(n[e])===1&&(wg(n,e,t)||Sg(n,e,t));},kg=(n,t,e)=>{const i=t[e];if(!i)return "";let o="";for(let r=i.tokenIndex;r<=i.breakAfterTokenIndex;r+=1)o+=n[r]?.text??"";return o.trimEnd()},Tg=(n,t,e,i)=>{for(let o=0;o=l;g-=1)if(dg.test(kg(t,e,g))){a=g;break}if(a{const i=t[e];if(!i)return 0;const o=e>0?t[e-1]?.breakAfterTokenIndex??i.tokenIndex-1:-1;let r=Nt(o+1,0,i.tokenIndex);for(;r{const i=[];for(const o of n){const r=t[o.startWord],s=t[o.endWord];if(!r||!s)continue;const a=Lg(e,t,o.startWord),l=s.breakAfterTokenIndex+1,c=e[r.tokenIndex]?.startMs??e[a]?.startMs??0,u=e[s.tokenIndex]?.startMs??c,d=e[s.tokenIndex]?.durationMs??0,h=t[o.endWord+1],g=(h?e[h.tokenIndex]?.startMs:void 0)??u+d;i.push({startToken:a,endToken:l,startMs:c,endMs:g});}return i};function Ig(n,t,e,i,o){const r=t.length;if(r===0)return [];const s=Number.isFinite(o)&&o>0?o:null,a=(c,u)=>us?false:Hs(e,c,u,i),l=yg(r,a);return xg(l,a),Tg(l,n,t,a),Ag(l,t,n)}const Cg=8,Eg=.97,Pg=24;function rr(n){if(!Number.isFinite(n)||n<=0)return 0;const t=n-Cg,e=n*Eg,i=Math.min(t,e);return Math.max(Pg,i)}class Vg{video;container;fullscreenLayerController;tooltipLayoutRoot;subtitlesContainer=null;subtitlesBlock=null;renderedTokenEls=[];passedFlagsBuffer=[];subtitles=null;subtitleLang;lastRenderKey=null;lastActiveLineIndex=null;highlightWords=false;fontSize=20;fontSizeOverridden=false;fontFamily="default-sans";manualMaxLength=300;smartLayoutEnabled=true;smartFontSizePx=0;smartMaxWidthPx=0;smartMaxLength=0;smartAnchorWidthPx=0;smartAnchorHeightPx=0;lastSmartLayoutKey=null;lastSmartLayoutCheckTs=0;opacity="0.2";maxLength=300;repositionPending=false;positionRefreshPending=false;updatePending=false;lastUpdateRequestTs=0;updateMinIntervalMs=100;updateMinIntervalHighlightMs=33;useVideoFrameCallbacks;videoFrameRequestId=null;dragDocListenersAttached=false;lastPositionRefreshTs=0;positionRefreshIntervalMs=250;subtitleMaxWidthPx=0;breakAfterTokenIndices=[];breakAfterTokenIndexSet=null;smartTruncateAfterTokenIndex=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};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(t){return t.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;onTimeUpdateBound;onPlaybackStateChangeBound;onVisualViewportChangeBound;constructor(t,e,i,o=void 0){this.video=t,this.container=e,this.fullscreenLayerController=new ng({video:t,container:e}),this.intervalIdleChecker=i,this.tooltipLayoutRoot=o,this.useVideoFrameCallbacks=!!this.video&&typeof this.video.requestVideoFrameCallback=="function",this.onPointerDownBound=r=>this.onPointerDown(r),this.onPointerUpBound=r=>this.onPointerUp(r),this.onPointerMoveBound=r=>this.onPointerMove(r),this.onTimeUpdateBound=()=>this.requestUpdate(),this.onPlaybackStateChangeBound=()=>this.handlePlaybackStateChange(),this.onVisualViewportChangeBound=()=>this.scheduleReposition(),this.checkerUnsubscribe=this.intervalIdleChecker.subscribe(()=>{this.onCheckerTick();}),this.bindEvents();}updateMount({container:t,tooltipLayoutRoot:e}){const i=this.container!==t,o=this.tooltipLayoutRoot!==e;this.container=t,this.fullscreenLayerController.updateContainer(t),this.tooltipLayoutRoot=e,this.syncWidgetMount(),(i||o)&&this.tokenTooltip?.updateMount({parentElement:this.getTokenTooltipParentElement(),layoutRoot:this.tooltipLayoutRoot??document.documentElement}),this.subtitles&&(this.insetCacheReady=false,this.lastAppliedLeftPct=null,this.lastAppliedTopPct=null,this.updateContainerRect(),this.requestUpdate());}resetTranslationContext(t=false){this.strTranslatedTokens="",t&&this.releaseTooltip();}resetSegmentationMemo(){this.tokenProcessingMemo=null,this.tokenPrecomputeMemo=null,this.lineMeasureMemo=null,this.lastSegmentIndex=0;}resetWrapMemo(){this.setBreakAfterTokenIndices([]),this.smartTruncateAfterTokenIndex=null,this.lastWrapKey=null;}resetRenderMemo(){this.lastRenderKey=null;}computeAnchorBoxLayout(t){const e={left:0,top:0,w:t.w,h:t.h},i=this.video;if(!i)return e;const o=i.getBoundingClientRect();if(!(o.width>0&&o.height>0))return e;const r=t.rect;if(!(o.right>r.left&&o.leftr.top&&o.top0&&l>0))return e;const c=(o.left-r.left)/t.scaleX,u=(o.top-r.top)/t.scaleY,d=t.w-a,h=t.h-l,f=d>=0?xt(c,0,d):(t.w-a)/2,g=h>=0?xt(u,0,h):(t.h-l)/2;return {left:f,top:g,w:a,h:l}}readSmartCssMetrics(){const t=this.subtitlesBlock;if(!t)return null;const e=getComputedStyle(t),i=Number.parseFloat(e.fontSize),o=Number.parseFloat(e.maxWidth);if(!Number.isFinite(i)||!Number.isFinite(o)||i<=0||o<=0)return null;this.subtitleMaxWidthPx=o;const r=Number.parseFloat(e.paddingLeft)||0,s=Number.parseFloat(e.paddingRight)||0,a=Math.max(0,o-r-s);return a<=0?null:{fontSizePx:i,maxWidthPx:a}}ensureSmartLayout(t){if(!this.smartLayoutEnabled)return this.maxLength=this.manualMaxLength,null;const e=this.readSmartCssMetrics(),i=e?.fontSizePx??this.smartFontSizePx,o=e?.maxWidthPx??this.smartMaxWidthPx,r=ug(t,e),s=`${Math.round(i)}|${Math.round(o)}|${r.maxLength}`,a=Math.abs(i-this.smartFontSizePx)>.5,l=Math.abs(o-this.smartMaxWidthPx)>.5,c=r.maxLength!==this.smartMaxLength;return s!==this.lastSmartLayoutKey&&(this.lastSmartLayoutKey=s,this.smartFontSizePx=i,this.smartMaxWidthPx=o,this.smartMaxLength=r.maxLength),c&&(this.maxLength=r.maxLength,this.resetRenderMemo(),this.resetSegmentationMemo()),(a||l)&&this.lastWrapTokens&&(this.lastWrapKey=null,this.scheduleWrapRecompute(),this.resetSegmentationMemo()),r}scheduleReposition(){this.abortController.signal.aborted||this.subtitles&&(this.repositionPending=true,this.intervalIdleChecker.markActivity("subtitles-reposition"),this.intervalIdleChecker.requestImmediateTick());}setSubtitlesContainerVar(t,e){const i=this.subtitlesContainer;if(i){if(e===null){i.style.removeProperty(t);return}i.style.setProperty(t,e);}}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 t=this.fontFamily;this.setSubtitlesContainerVar("--vot-subtitles-font-family-custom",tr(t)),tg(t,{forceGmXhr:true,onLoaded:()=>{this.fontFamily===t&&(this.lastWrapKey=null,this.resetSegmentationMemo(),this.scheduleWrapRecompute(),this.scheduleReposition());}});}syncVisualStyleVars(){this.applyOpacityStyle(),this.applyManualFontSizeStyle(),this.applyFontFamilyStyle();}ensureGuidesLayer(){if(this.guidesLayer)return this.guidesLayer;const t=document.createElement("vot-block");t.classList.add("vot-subtitles-guides");const e=document.createElement("vot-block");e.classList.add("vot-subtitles-guide","vot-subtitles-guide--vertical");const i=document.createElement("vot-block");return i.classList.add("vot-subtitles-guide","vot-subtitles-guide--horizontal"),t.append(e,i),this.guidesLayer=t,this.verticalGuide=e,this.horizontalGuide=i,this.hideSnapGuides(),t}hideSnapGuides(){this.verticalGuide?.removeAttribute("data-visible"),this.horizontalGuide?.removeAttribute("data-visible");}updateSnapGuides(t,e){const{showVerticalCenter:i=false,showHorizontalCenter:o=false}=e;this.ensureGuidesLayer().isConnected||this.syncGuideLayerMount(),this.verticalGuide&&(this.verticalGuide.style.left=`${t.left+t.w/2}px`,this.verticalGuide.style.top=`${t.top}px`,this.verticalGuide.style.height=`${t.h}px`,i?this.verticalGuide.dataset.visible="true":delete this.verticalGuide.dataset.visible),this.horizontalGuide&&(this.horizontalGuide.style.left=`${t.left}px`,this.horizontalGuide.style.top=`${t.top+t.h/2}px`,this.horizontalGuide.style.width=`${t.w}px`,o?this.horizontalGuide.dataset.visible="true":delete this.horizontalGuide.dataset.visible);}syncGuideLayerMount(){const t=this.fullscreenLayerController.getWidgetParentElement(),e=this.ensureGuidesLayer();e.parentElement!==t&&t.appendChild(e);}syncWidgetMount(){this.fullscreenLayerController.syncWidgetContainer(this.subtitlesContainer),this.syncGuideLayerMount();}getTokenTooltipParentElement(){const t=this.fullscreenLayerController.getWidgetParentElement();return t===this.container?document.documentElement:t}createSubtitlesContainer(){if(this.subtitlesContainer)return this.subtitlesContainer;const t=document.createElement("vot-block");return t.classList.add("vot-subtitles-widget"),this.subtitlesContainer=t,this.syncWidgetMount(),t.addEventListener("pointerdown",this.onPointerDownBound,{signal:this.abortController.signal,passive:true}),this.syncVisualStyleVars(),this.insetCacheReady=false,this.updateContainerRect(),t}bindEvents(){const{signal:t}=this.abortController,e={signal:t};this.video?.addEventListener("play",this.onPlaybackStateChangeBound,e),this.video?.addEventListener("pause",this.onPlaybackStateChangeBound,e),this.video?.addEventListener("seeking",this.onPlaybackStateChangeBound,e),this.video?.addEventListener("seeked",this.onPlaybackStateChangeBound,e),this.video?.addEventListener("ended",this.onPlaybackStateChangeBound,e),this.resizeObserver=new ResizeObserver(()=>this.onResize()),this.resizeObserver.observe(this.container),this.video&&this.resizeObserver.observe(this.video),globalThis.visualViewport?.addEventListener("resize",this.onVisualViewportChangeBound,e),globalThis.visualViewport?.addEventListener("scroll",this.onVisualViewportChangeBound,e);}getUpdateMinIntervalMs(){return this.highlightWords?this.updateMinIntervalHighlightMs:this.updateMinIntervalMs}requestUpdate(t=performance.now()){if(this.abortController.signal.aborted||!this.subtitles)return;const e=this.getUpdateMinIntervalMs();t-this.lastUpdateRequestTs{if(this.videoFrameRequestId=null,this.abortController.signal.aborted)return;const i=this.video;!i||i.paused||i.ended||this.subtitles&&(this.requestUpdate(t),this.startVideoFrameLoop());};onCheckerTick(){this.abortController.signal.aborted||(this.repositionPending&&(this.repositionPending=false,this.updateContainerRect(),this.updatePending=true),this.wrapPending&&(this.wrapPending=false,this.recomputeWrapNow()),this.positionRefreshPending&&(this.positionRefreshPending=false,this.applySubtitlePosition()),this.updatePending&&(this.updatePending=false,this.update()));}attachDragDocumentListeners(){this.dragDocListenersAttached||(this.dragDocListenersAttached=true,document.addEventListener("pointermove",this.onPointerMoveBound,{passive:false}),document.addEventListener("pointerup",this.onPointerUpBound),document.addEventListener("pointercancel",this.onPointerUpBound));}detachDragDocumentListeners(){this.dragDocListenersAttached&&(this.dragDocListenersAttached=false,document.removeEventListener("pointermove",this.onPointerMoveBound),document.removeEventListener("pointerup",this.onPointerUpBound),document.removeEventListener("pointercancel",this.onPointerUpBound));}onResize(){this.syncWidgetMount(),this.scheduleReposition();}updateContainerRect(){const t=this.getLayoutSize();if(!t.w||!t.h)return;const e=this.computeAnchorBoxLayout(t);!e.w||!e.h||(this.refreshBottomInsetNow(t,e),this.applySubtitlePositionWithLayout(t,e));}getLayoutSize(){const t=this.fullscreenLayerController.getLayoutRootElement(),e=t.getBoundingClientRect(),i=t.clientWidth||e.width,o=t.clientHeight||e.height,r=e.width&&i?e.width/i:1,s=e.height&&o?e.height/o:1;return {w:i,h:o,rect:e,scaleX:r,scaleY:s}}ensureSafeAreaProbe(){if(this.safeAreaProbeEl)return;const t=document.createElement("div");t.style.position="fixed",t.style.left="0",t.style.right="0",t.style.bottom="0",t.style.height="env(safe-area-inset-bottom, 0px)",t.style.pointerEvents="none",t.style.opacity="0",t.style.zIndex="-1",document.documentElement.appendChild(t),this.safeAreaProbeEl=t;}getSafeAreaBottomInsetPx(){return this.ensureSafeAreaProbe(),this.safeAreaProbeEl&&this.safeAreaProbeEl.offsetHeight||0}refreshInsetCache(){const t=this.fullscreenLayerController.getLayoutRootElement();this.safeAreaBottomInsetCachedPx=this.getSafeAreaBottomInsetPx(),this.containerPaddingBottomCachedPx=Number.parseFloat(getComputedStyle(t).paddingBottom||"0")||0,this.insetCacheReady=true;}isMobileViewport(){return typeof globalThis.matchMedia!="function"?false:globalThis.matchMedia("(max-width: 900px) and (pointer: coarse)").matches}getBottomInsetPreset(){const t=document,e=t.fullscreenElement??t.webkitFullscreenElement;if(!(e instanceof Element))return this.bottomInsetByMode.normal;const{container:i,video:o}=this;return e===i||e.contains(i)||i.contains(e)?this.bottomInsetByMode.fullscreen:o&&(e===o||e.contains(o)||o.contains(e))?this.bottomInsetByMode.fullscreen:this.bottomInsetByMode.normal}computeReservedBottomInsetPx(t,e=this.getBottomInsetPreset()){const i=t*e.ratio;return xt(i,e.minPx,e.maxPx)}refreshBottomInsetNow(t,e){this.refreshInsetCache();const i=e?.h??this.computeAnchorBoxLayout(t??this.getLayoutSize()).h;if(!i){this.bottomInsetCachedPx=0;return}const o=this.getBottomInsetPreset();this.bottomInsetCachedPx=this.computeReservedBottomInsetPx(i,o);}getBottomInsetPx(t,e){this.insetCacheReady||this.refreshInsetCache();const i=this.getBottomInsetPreset(),o=this.safeAreaBottomInsetCachedPx,r=this.containerPaddingBottomCachedPx;if(this.isMobileViewport())return Math.max(r,o);const s=e?.h??this.computeAnchorBoxLayout(t??this.getLayoutSize()).h,a=s?this.computeReservedBottomInsetPx(s,i):i.minPx,l=Math.max(this.bottomInsetCachedPx,a);return Math.max(r,o,l)+i.gapPx}onPointerDown(t){const e=this.subtitlesContainer;if(!e)return;const i=t.target;if(!(i instanceof Node)||!e.contains(i)||!t.isPrimary||t.pointerType==="mouse"&&t.button!==0)return;const o=this.getLayoutSize(),{rect:r,w:s,h:a,scaleX:l,scaleY:c}=o;if(!s||!a)return;const u=this.computeAnchorBoxLayout(o);if(!u.w||!u.h)return;this.lastPositionRefreshTs=performance.now();const d=e.getBoundingClientRect(),h=(t.clientX-r.left)/l-u.left,f=(t.clientY-r.top)/c-u.top,g=(d.left-r.left+d.width/2)/l-u.left,y=(d.top-r.top+d.height)/c-u.top;this.dragging.pointerId=t.pointerId,this.dragging.candidate=true,this.dragging.active=false,this.dragging.moved=false,this.dragging.startClientX=t.clientX,this.dragging.startClientY=t.clientY,this.dragging.offset.x=g-h,this.dragging.offset.y=y-f,this.hideSnapGuides(),this.attachDragDocumentListeners();const w=this.subtitlesBlock??(i instanceof Element?i:null);try{w?.setPointerCapture(t.pointerId);}catch{}}onPointerUp(t){this.dragging.pointerId!==null&&t.pointerId===this.dragging.pointerId&&(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(t){if(!this.dragging.candidate||this.dragging.pointerId===null||t.pointerId!==this.dragging.pointerId)return;if(this.dragging.active)this.dragging.moved=true;else {if(!rg(this.dragging.startClientX,this.dragging.startClientY,t.clientX,t.clientY,this.dragStartThresholdPx))return;this.dragging.active=true,this.dragging.moved=true,this.suppressTokenClicksUntil=performance.now()+450,this.releaseTooltip();}t.preventDefault(),t.stopPropagation();const e=this.getLayoutSize(),{rect:i,w:o,h:r,scaleX:s,scaleY:a}=e;if(!o||!r)return;const l=this.computeAnchorBoxLayout(e);if(!l.w||!l.h)return;const c=(t.clientX-i.left)/s-l.left,u=(t.clientY-i.top)/a-l.top;let d=c+this.dragging.offset.x,h=u+this.dragging.offset.y;const f=this.subtitlesContainer?.offsetWidth??0,g=this.subtitlesContainer?.offsetHeight??0,y=this.getBottomInsetPx(e,l),w=nr({current:d,candidates:[l.w/2],thresholdPx:this.snapThresholdPx});w.snapped&&(d=w.value);const x=l.h/2+g/2,m=nr({current:h,candidates:[x],thresholdPx:this.snapThresholdPx});m.snapped&&(h=m.value),{anchorX:d,anchorY:h}=er({anchorX:d,anchorY:h,elementWidth:f,elementHeight:g,boxWidth:l.w,boxHeight:l.h,bottomInset:y}),this.positionPreset="custom",this.position.left=d/l.w*100,this.position.top=h/l.h*100,this.updateSnapGuides(l,{showVerticalCenter:w.snapped,showHorizontalCenter:m.snapped}),this.applySubtitlePositionWithLayout(e,l);}applySubtitlePosition(){if(!this.subtitlesContainer)return;const e=this.getLayoutSize();if(!e.w||!e.h)return;const i=this.computeAnchorBoxLayout(e);!i.w||!i.h||this.applySubtitlePositionWithLayout(e,i);}applySubtitlePositionWithLayout(t,e){const i=this.subtitlesContainer;if(!i)return;const o=Math.min(t.scaleX||1,t.scaleY||1),r=o>0&&o<.999?Math.min(1/o,3):1;Math.abs(r-1)<.001?i.style.removeProperty("--vot-subtitles-scale-compensation"):i.style.setProperty("--vot-subtitles-scale-compensation",r.toFixed(3));const s=Math.max(1,Math.round(e.w)),a=Math.max(1,Math.round(e.h));(s!==this.smartAnchorWidthPx||a!==this.smartAnchorHeightPx)&&(this.smartAnchorWidthPx=s,this.smartAnchorHeightPx=a,i.style.setProperty("--vot-subtitles-anchor-width",`${s}px`),i.style.setProperty("--vot-subtitles-anchor-height",`${a}px`),this.lastWrapTokens&&(this.lastWrapKey=null,this.resetSegmentationMemo(),this.scheduleWrapRecompute())),this.smartLayoutEnabled&&this.ensureSmartLayout(e);const c=i.offsetWidth,u=i.offsetHeight,d=this.getBottomInsetPx(t,e);let h=this.position.left/100*e.w,f=this.position.top/100*e.h;if(this.positionPreset!=="custom"){const F=this.resolvePresetAnchorPosition({preset:this.positionPreset,anchorBox:e,elementWidth:c,elementHeight:u,bottomInset:d});h=F.anchorX,f=F.anchorY,e.w>0&&(this.position.left=h/e.w*100),e.h>0&&(this.position.top=f/e.h*100);}let g=h-c/2,y=f-u;const w=e.w-c,x=e.h-d-u;w>=0?g=xt(g,0,w):g=w/2,x>=0?y=xt(y,0,x):y=0,h=g+c/2,f=y+u;const m=e.left+h,k=e.top+f,P=m/t.w*100,D=k/t.h*100;(this.lastAppliedLeftPct===null||Math.abs(P-this.lastAppliedLeftPct)>=.01)&&(i.style.left=`${P}%`,this.lastAppliedLeftPct=P),(this.lastAppliedTopPct===null||Math.abs(D-this.lastAppliedTopPct)>=.01)&&(i.style.top=`${D}%`,this.lastAppliedTopPct=D),this.tokenTooltip?.updatePos();}resolvePresetAnchorPosition({preset:t,anchorBox:e,elementWidth:i,elementHeight:o,bottomInset:r}){let s=e.w/2,a=e.h-r;switch(t){case "top-center":a=o;break;case "center":a=e.h/2+o/2;break;case "bottom-left":s=i/2;break;case "bottom-right":s=e.w-i/2;break}return er({anchorX:s,anchorY:a,elementWidth:i,elementHeight:o,boxWidth:e.w,boxHeight:e.h,bottomInset:r})}applyPositionAfterContentRender(){const t=this.getLayoutSize();if(t.w&&t.h){const e=this.computeAnchorBoxLayout(t);if(e.w&&e.h){this.refreshBottomInsetNow(t,e),this.applySubtitlePositionWithLayout(t,e);return}this.refreshBottomInsetNow(t),this.applySubtitlePosition();return}this.refreshBottomInsetNow(),this.applySubtitlePosition();}trimEdgeWhitespaceTokens(t){if(!t.length)return t;let e=0,i=t.length;for(;ee&&!t[i-1]?.text.trim();)i-=1;return e===0&&i===t.length?t:e>=i?[]:t.slice(e,i)}selectTokensByMaxLength(t,e){if(!t.length)return t;let i=0,o=0,r=false,s=0,a=t.length,l=false,c=false;const u=(d,h)=>{if(h<=d||(l||(s=d,a=h,l=true),c))return;const f=t[d],g=t[h-1];if(!f||!g)return;const w=(hthis.maxLength&&d>i){r=true,u(i,d),i=d,o=h.text.length;continue}o=f;}return r?(u(i,t.length),this.trimEdgeWhitespaceTokens(t.slice(s,a))):this.trimEdgeWhitespaceTokens(t)}buildTokenPrecomputeInput(t){const e=this.tokenPrecomputeMemo;if(e?.tokens===t)return e.value;const{slices:i,key:o}=hg(t),s={words:i.map(a=>({tokenIndex:a.tokenIndex,breakAfterTokenIndex:a.breakAfterTokenIndex})),wordSlices:i,normalizedWordsKey:o};return this.tokenPrecomputeMemo={tokens:t,value:s},s}getTokenLayoutInputs(t){const e=this.subtitlesBlock;if(e){const c=getComputedStyle(e),u=`${c.fontStyle} ${c.fontVariant} ${c.fontWeight} ${c.fontSize} ${c.fontFamily}`;t.font=u;const d=Number.parseFloat(c.maxWidth),h=Number.parseFloat(c.paddingLeft)||0,f=Number.parseFloat(c.paddingRight)||0,g=Number.isFinite(d)?d:this.subtitleMaxWidthPx||globalThis.innerWidth*.8;return Number.isFinite(g)&&g>0&&(this.subtitleMaxWidthPx=g),{fontKey:u,maxWidthPx:Math.max(0,g-h-f)}}const i=Number.parseFloat(getComputedStyle(document.documentElement).fontSize)||16,s=Math.min(i*52,this.subtitleMaxWidthPx||globalThis.innerWidth*.8),a=this.fontSizeOverridden?this.fontSize:Math.min(24,Math.max(14,globalThis.innerWidth*.016)),l=`normal normal 500 ${a}px ${tr(this.fontFamily)}`;return t.font=l,{fontKey:l,maxWidthPx:Math.max(0,s-a)}}getActiveLineKey(t){return this.lastActiveLineIndex!==null?`${this.lastActiveLineIndex}`:`${t[0]?.startMs??0}:${t[0]?.durationMs??0}:${t.length}`}getLineMeasureMemo(t,e){const{words:i,wordSlices:o,normalizedWordsKey:r}=this.buildTokenPrecomputeInput(t);if(!i.length)return null;const s=this.getMeasureContext();if(!s)return null;const{fontKey:a,maxWidthPx:l}=this.getTokenLayoutInputs(s);if(!Number.isFinite(l)||l<24)return null;const c=`${e}|${a}|${Math.round(l)}|${r}`;if(this.lineMeasureMemo?.key===c)return this.lineMeasureMemo;const u=fg(o,h=>s.measureText(h).width),d={key:c,words:i,metrics:u,maxWidthPx:l};return this.lineMeasureMemo=d,d}buildTokenProcessingMemo(t,e){const i=this.getLineMeasureMemo(t,e);if(!i)return null;const o=`${i.key}|${this.maxLength}`;if(this.tokenProcessingMemo?.key===o)return this.tokenProcessingMemo;const r=rr(i.maxWidthPx),s=Ig(t,i.words,i.metrics,r,this.maxLength),a={key:o,segmentRanges:s};return this.tokenProcessingMemo=a,this.lastSegmentIndex=0,a}selectSegmentIndexFromRanges(t,e){if(!t.length)return -1;let i=this.lastSegmentIndex;for(i>=t.length&&(i=0);i=t[i].endMs;)i+=1;for(;i>0&&e=t[i].startMs&&ee>=r.startMs&&e=0?i=o:i=e{if(performance.now()r[s];return o.length=r.length,o}renderTokens(t){const e=this.breakAfterTokenIndexSet,i=[],o=sg(t,t.length-1,e);for(const r of o){if(r.kind==="word"){i.push(fe`${r.text}`);continue}if(r.kind==="break"){i.push(fe`
`);continue}if(r.style){i.push(fe`${r.text}`);continue}i.push(r.text);}return i}updatePassedClasses(t){const e=this.renderedTokenEls,i=Math.min(e.length,t.length);for(let o=0;oo[y].breakAfterTokenIndex);else if(this.smartLayoutEnabled){const y=bg(r,a);c=y.breakAfterWordIndices.map(w=>o[w].breakAfterTokenIndex),y.truncateAfterWordIndex!==null&&(u=o[y.truncateAfterWordIndex]?.breakAfterTokenIndex??null);}}const h=!this.arraysEqual(c,this.breakAfterTokenIndices),f=u!==this.smartTruncateAfterTokenIndex;(h||f)&&(this.setBreakAfterTokenIndices(c),this.smartTruncateAfterTokenIndex=u,this.resetRenderMemo(),this.update());}setContent(t,e=void 0){if(this.releaseTooltip(),this.subtitleLang=e,!t||!this.video){this.clearRenderedContent(),this.subtitles=null,this.clearPendingSchedulerState(),this.video?.removeEventListener("timeupdate",this.onTimeUpdateBound),this.stopVideoFrameLoop(),this.detachDragDocumentListeners();return}this.createSubtitlesContainer(),this.subtitles=t,this.lastActiveLineIndex=null,this.useVideoFrameCallbacks||this.video.addEventListener("timeupdate",this.onTimeUpdateBound,{signal:this.abortController.signal}),this.syncVideoFrameLoop(),this.updateContainerRect(),this.update(),this.intervalIdleChecker.requestImmediateTick();}setMaxLength(t){typeof t=="number"&&t>0&&(this.manualMaxLength=t,this.smartLayoutEnabled||(this.maxLength=t,this.resetSegmentationMemo(),this.update(),this.scheduleReposition()));}setHighlightWords(t){const e=this.highlightWords;this.highlightWords=!!t,e&&!this.highlightWords&&this.clearPassedClasses(),this.update();}setSmartLayout(t){const e=t!==false;e!==this.smartLayoutEnabled&&(this.smartLayoutEnabled=e,this.subtitlesContainer?.style.removeProperty("--vot-subtitles-max-width"),this.lastSmartLayoutKey=null,this.resetWrapMemo(),this.resetRenderMemo(),this.resetSegmentationMemo(),this.smartLayoutEnabled||(this.maxLength=this.manualMaxLength),this.applyManualFontSizeStyle(),this.update(),this.scheduleWrapRecompute(),this.scheduleReposition());}setFontSize(t){this.fontSize=t,this.fontSizeOverridden=true,this.smartLayoutEnabled||(this.applyManualFontSizeStyle(),this.lastWrapKey=null,this.resetSegmentationMemo(),this.scheduleWrapRecompute(),this.scheduleReposition());}setFontFamily(t){this.fontFamily=t,this.applyFontFamilyStyle(),this.lastWrapKey=null,this.resetSegmentationMemo(),this.scheduleWrapRecompute(),this.scheduleReposition();}setOpacity(t){const e=Number(t),i=Number.isFinite(e)?xt(e,0,100):0;this.opacity=((100-i)/100).toFixed(2),this.applyOpacityStyle();}stringifyTokens(t){let e="";for(const i of t)e+=i.text;return e}update(){if(!this.video||!this.subtitles)return;const t=this.video.currentTime*1e3,e=this.subtitles.subtitles;let i,o=-1;const r=this.lastActiveLineIndex;if(typeof r=="number"&&r>=0&&r500){this.lastSmartLayoutCheckTs=w;const x=this.getLayoutSize();if(x.w&&x.h){const m=this.computeAnchorBoxLayout(x);m.w&&m.h&&this.ensureSmartLayout(m);}}}else this.maxLength=this.manualMaxLength;const s=this.processTokens(i.tokens,t);this.lastWrapTokens=s;const a=this.smartLayoutEnabled&&typeof this.smartTruncateAfterTokenIndex=="number"&&this.smartTruncateAfterTokenIndex>=0&&this.smartTruncateAfterTokenIndex video`)?.src;if(!e)throw new N(`Failed to find video URL`);return{url:e}}catch(t){T.error(`Failed to get Bitview data by videoId: ${e}`,t.message);return}}async getVideoId(e){return e.searchParams.get(`v`)}},gn=class extends P{async getVideoData(e){let t=document.querySelector(`#player > source[type="video/mp4"]`)?.src;if(t)return{url:t}}async getVideoId(e){return/\/f\/([^/]+)/.exec(e.pathname)?.[1]}},_n=class extends P{async getVideoId(e){return e.pathname+e.search}},vn=class extends P{async getVideoId(e){return e.pathname+e.search}},yn=class extends P{API_ORIGIN=this.origin??`https://coursehunter.net`;async getCourseId(){let e=window.course_id;return e===void 0?document.querySelector(`input[name="course_id"]`)?.value:String(e)}async getLessonsData(e){let t=window.lessons;if(t?.length)return t;try{return await(await this.fetch(`${this.API_ORIGIN}/api/v1/course/${e}/lessons`)).json()}catch(t){T.error(`Failed to get CoursehunterLike lessons data by courseId: ${e}, because ${t.message}`);return}}getLessondId(e){let t=e.split(`?lesson=`)?.[1];return t||(t=document.querySelector(`.lessons-item_active`)?.dataset?.index,t)?+t:1}async getVideoData(e){let t=await this.getCourseId();if(!t)return;let n=await this.getLessonsData(t);if(!n)return;let r=this.getLessondId(e),{file:i,duration:a,title:o}=n?.[r-1];if(i)return{url:D(i),duration:a,title:o}}async getVideoId(e){let t=/course\/([^/]+)/.exec(e.pathname)?.[0];return t?t+e.search:void 0}},bn=[`auto`,`ru`,`en`,`zh`,`ko`,`lt`,`lv`,`ar`,`fr`,`it`,`es`,`de`,`ja`],xn=[`ru`,`en`,`kk`],Sn=class e extends P{SUBTITLE_SOURCE=`videojs`;SUBTITLE_FORMAT=`vtt`;static getPlayer(){let e=window.videojs,t=document.querySelector(`video.vjs-tech[id], video[id$='_html5_api']`),n=t?.id?.endsWith(`_html5_api`)?t.id.slice(0,-10):void 0;if(e?.getPlayer){if(n){let t=e.getPlayer(n);if(t)return t}if(t){let n=e.getPlayer(t);if(n)return n}}let r=(typeof e?.getPlayers==`function`?e.getPlayers():e?.players)??{};for(let e of Object.values(r)){let r=e,i=(typeof r.el==`function`?r.el():null)?.querySelector?.(`video.vjs-tech, video`)??null;if(i&&t&&i===t||n&&typeof r.id==`function`&&r.id()===n)return e}}getVideoDataByPlayer(t){try{let n=e.getPlayer(),r=document.querySelector(`video.vjs-tech, video[id$='_html5_api'], video[src]`);if(!n&&!r)throw Error(`Video player/video element not found, videoId ${t}`);let i=n?.duration?.()??r?.duration,a;if(n){let e=typeof n.currentSources==`function`?n.currentSources():n.getCache?.()?.sources;a=(Array.isArray(e)?e.find(e=>e?.type===`video/mp4`||e?.type===`video/webm`||e?.src):void 0)?.src}if(a??=r?.currentSrc||r?.src||r?.getAttribute?.(`src`)||void 0,!a)throw Error(`Failed to find video url for videoID ${t}`);let o=(r?Array.from(r.querySelectorAll(`track[src]`)):[]).filter(e=>e.kind!==`metadata`).flatMap(e=>{let t=e.getAttribute(`src`);if(!t)return[];let n=new URL(t,window.location.href).toString();return[{language:E(e.srclang||``),source:this.SUBTITLE_SOURCE,format:this.SUBTITLE_FORMAT,url:n}]});return{url:a,duration:i,subtitles:o}}catch(e){T.error(`Failed to get videojs video data`,e.message);return}}},Cn=class e extends Sn{API_ORIGIN=`https://www.coursera.org/api`;SUBTITLE_SOURCE=`coursera`;async getCourseData(e){try{return(await(await this.fetch(`${this.API_ORIGIN}/onDemandCourses.v1/${e}`)).json())?.elements?.[0]}catch(t){T.error(`Failed to get course data by courseId: ${e}`,t.message);return}}static getPlayer(){return Sn.getPlayer()}async getVideoData(t){let n=this.getVideoDataByPlayer(t);if(!n)return;let{options_:r}=e.getPlayer()??{};!n.subtitles?.length&&r&&(n.subtitles=r.tracks.map(e=>({url:e.src,language:E(e.srclang),source:this.SUBTITLE_SOURCE,format:this.SUBTITLE_FORMAT})));let i=r?.courseId;if(!i)return n;let a=`en`,o=await this.getCourseData(i);if(o){let{primaryLanguageCodes:[e]}=o;a=e?E(e):`en`}bn.includes(a)||(a=`en`);let s=(n.subtitles.find(e=>e.language===a)??n.subtitles?.[0])?.url;s||T.warn(`Failed to find any subtitle file`);let{url:c,duration:l}=n,u=s?[{target:`subtitles_file_url`,targetUrl:s},{target:`video_file_url`,targetUrl:c}]:null;return{...s?{url:this.service?.url+t,translationHelp:u}:{url:c,translationHelp:u},detectedLanguage:a,duration:l}}async getVideoId(e){return(/learn\/([^/]+)\/lecture\/([^/]+)/.exec(e.pathname)??/lecture\/([^/]+)\/([^/]+)/.exec(e.pathname))?.[0]}},wn=class extends P{getVideoIdFromUrl(e){let t=e.searchParams.get(`video`);if(t)return t}resolveVideoIdViaPostMessage(){return new Promise(e=>{let t=`https://www.dailymotion.com`,n=setTimeout(()=>{window.removeEventListener(`message`,r),e(void 0)},3e3),r=i=>{i.origin===t&&typeof i.data==`string`&&i.data.startsWith(`getVideoId:`)&&(clearTimeout(n),window.removeEventListener(`message`,r),e(i.data.replace(`getVideoId:`,``)))};window.addEventListener(`message`,r),window.top?.postMessage(`getVideoId:`,t)})}async getVideoId(e){return window.self===window.top?this.getVideoIdFromUrl(e):await this.resolveVideoIdViaPostMessage()}},Tn=class extends Sn{SUBTITLE_SOURCE=`datacamp`;getVideoDataFromInput(){try{let e=document.getElementById(`videoData`);return!e||!(e instanceof HTMLInputElement)&&!(e instanceof HTMLTextAreaElement)||!e.value?null:JSON.parse(e.value)}catch(e){return T.error(`Failed to parse DataCamp videoData input`,e instanceof Error?e.message:String(e)),null}}async getVideoData(e){let t=this.getVideoDataByPlayer(e);if(!t)return;let n=this.getVideoDataFromInput(),r=n?.video_url??n?.plain_video_mp4_link??n?.plain_video_hls_link??n?.video_mp4_link??n?.video_hls_link??t.url;if(r)return{url:e,video_url:r,translationHelp:[{target:`video_file_url`,targetUrl:r}]}}async getVideoId(e){return e.href}},En=class extends P{async getVideoData(e){if(!this.video)return;let t=this.video.querySelector(`source[type="application/x-mpegurl"]`)?.src;if(t)return{url:t}}async getVideoId(e){return/courses\/(([^/]+)\/lesson\/([^/]+)\/([^/]+))/.exec(e.pathname)?.[1]}},Dn=class e extends P{static getPlayer(){if(!(typeof player>`u`))return player}async getVideoData(t){let n=e.getPlayer();if(!n)return;let{config:{url:r,duration:i,lang:a,isLive:o}}=n;if(!r)return;let s=r.find(e=>e.src.includes(`www.douyin.com/aweme/v1/play/`));if(s)return{url:D(s.src),duration:i,isStream:o,...bn.includes(a)?{detectedLanguage:a}:{}}}async getVideoId(t){return/video\/([\d]+)/.exec(t.pathname)?.[0]||e.getPlayer()?.config.vid}},On=class extends P{async getVideoId(e){return/video\/watch\/([^/]+)/.exec(e.pathname)?.[1]}},kn=class extends P{async getVideoId(e){return e.pathname.slice(1)}},An=class extends P{API_ORIGIN=`https://dev.epicgames.com/community/api/learning`;async getPostInfo(e){try{return await(await this.fetch(`${this.API_ORIGIN}/post.json?hash_id=${e}`)).json()}catch(t){return T.error(`Failed to get epicgames post info by videoId: ${e}.`,t.message),!1}}getVideoBlock(){let e=/videoUrl\s?=\s"([^"]+)"?/,t=Array.from(document.body.querySelectorAll(`script`)).find(t=>e.exec(t.innerHTML));if(!t)return;let n=t.innerHTML.trim(),r=e.exec(n)?.[1]?.replace(`qsep://`,`https://`);if(!r)return;let i=/sources\s?=\s(\[([^\]]+)\])?/.exec(n)?.[1];if(!i)return{playlistUrl:r,subtitles:[]};try{return i=`${i.replace(/src:(\s)+?(videoUrl)/g,`src:"removed"`).substring(0,i.lastIndexOf(`},`))}]`.split(` +`).map(e=>e.replace(/([^\s]+):\s?(?!.*\1)/,`"$1":`)).join(` +`),{playlistUrl:r,subtitles:JSON.parse(i).filter(e=>e.type===`captions`)}}catch{return{playlistUrl:r,subtitles:[]}}}async getVideoData(e){let t=e.split(`:`)?.[1],n=await this.getPostInfo(t);if(!n)return;let r=this.getVideoBlock();if(!r)return;let{playlistUrl:i,subtitles:a}=r,{title:o,description:s}=n;return{url:i,title:o,description:s,subtitles:a.map(e=>({language:E(e.srclang),source:`epicgames`,format:`vtt`,url:e.src}))}}async getVideoId(e){return new Promise(e=>{let t=`https://dev.epicgames.com`,n=btoa(window.location.href);window.addEventListener(`message`,n=>{if(n.origin===t&&typeof n.data==`string`&&n.data.startsWith(`getVideoId:`))return e(n.data.replace(`getVideoId:`,``))}),window.top?.postMessage(`getVideoId:${n}`,t)})}},jn=class extends P{async getVideoId(e){return/video-([^/]+)\/([^/]+)/.exec(e.pathname)?.[0]}},Mn=class extends P{async getVideoId(e){return e.pathname.slice(1)}},Nn=class extends P{getPlayerData(){return document.querySelector(`#movie_player`)?.getVideoData?.()??void 0}async getVideoId(e){return this.getPlayerData()?.video_id}},Pn=class extends P{getVideoDataBySource(e){let t=document.querySelector(`.icms.video > source[type="video/mp4"][data-quality="360"]`)?.src;return t?{url:D(t)}:this.returnBaseData(e)}getVideoDataByNext(e){try{let e=document.getElementById(`__NEXT_DATA__`)?.textContent;if(!e)throw new en(`Not found __NEXT_DATA__ content`);let{props:{pageProps:{page:{description:t,title:n,video:{videoMetadata:{duration:r},assets:i}}}}}=JSON.parse(e),a=i.find(e=>e.height===360&&e.url.includes(`.mp4`))?.url;if(!a)throw new en(`Not found video URL in assets`);return{url:D(a),duration:r,title:n,description:t}}catch(t){return T.warn(`Failed to get ign video data by video ID: ${e}, because ${t.message}. Using clear link instead...`),this.returnBaseData(e)}}async getVideoData(e){return document.getElementById(`__NEXT_DATA__`)?this.getVideoDataByNext(e):this.getVideoDataBySource(e)}async getVideoId(e){return/([^/]+)\/([\d]+)\/video\/([^/]+)/.exec(e.pathname)?.[0]??/\/videos\/([^/]+)/.exec(e.pathname)?.[0]}},Fn=class extends P{async getVideoId(e){return/video\/([^/]+)/.exec(e.pathname)?.[1]}},In=class extends P{async getVideoData(e){try{let e=document.querySelector(`#incflix-stream source:first-of-type`);if(!e)throw new N(`Failed to find source element`);let t=e.getAttribute(`src`);if(!t)throw new N(`Failed to find source link`);let n=new URL(t.startsWith(`//`)?`https:${t}`:t);return n.searchParams.append(`media-proxy`,`video.mp4`),{url:D(n)}}catch(t){T.error(`Failed to get Incestflix data by videoId: ${e}`,t.message);return}}async getVideoId(e){return/\/watch\/([^/]+)/.exec(e.pathname)?.[1]}},Ln=class extends P{async getVideoId(e){let t=/^\/(?:[a-z]{2}\/)?v\/(?\d+)\/(?[^/?#]+)\/?$/i.exec(e.pathname)?.groups;if(!t)return;let{id:n,slug:r}=t;return`v/${n}/${r}`}},Rn=class extends P{API_ORIGIN=`https://kick.com/api`;async getClipInfo(e){try{let{clip_url:t,duration:n,title:r}=(await(await this.fetch(`${this.API_ORIGIN}/v2/clips/${e}`)).json()).clip;return{url:t,duration:n,title:r}}catch(t){T.error(`Failed to get kick clip info by clipId: ${e}.`,t.message);return}}async getVideoInfo(e){try{let{source:t,livestream:n}=await(await this.fetch(`${this.API_ORIGIN}/v1/video/${e}`)).json(),{session_title:r,duration:i}=n;return{url:t,duration:Math.round(i/1e3),title:r}}catch(t){T.error(`Failed to get kick video info by videoId: ${e}.`,t.message);return}}async getVideoData(e){return e.startsWith(`videos`)?await this.getVideoInfo(e.replace(`videos/`,``)):await this.getClipInfo(e.replace(`clips/`,``))}async getVideoId(e){return/([^/]+)\/((videos|clips)\/([^/]+))/.exec(e.pathname)?.[2]}},zn=class extends P{async getVideoData(e){try{let e=document.querySelector(`.ksr-video-player > video`),t=e?.querySelector(`source[type^='video/mp4']`)?.src;if(!t)throw new N(`Failed to find video URL`);let n=e?.querySelectorAll(`track`)??[];return{url:t,subtitles:Array.from(n).reduce((e,t)=>{let n=t.getAttribute(`srclang`),r=t.getAttribute(`src`);return!n||!r||e.push({language:E(n),url:r,format:`vtt`,source:`kickstarter`}),e},[])}}catch(t){T.error(`Failed to get Kickstarter data by videoId: ${e}`,t.message);return}}async getVideoId(e){return e.pathname.slice(1)}},Bn=class extends P{API_ORIGIN=window.location.origin;getSecureData(e){try{let[t,n,r]=e.split(`/`).filter(e=>e),i=Array.from(document.getElementsByTagName(`script`)),a=i.filter(e=>e.innerHTML.includes(`videoId = "${n}"`)||e.innerHTML.includes(`serialId = Number(${n})`));if(!a.length)throw new N(`Failed to find secure script`);let o=a[0]?.textContent?.trim();if(!o)throw new N(`Secure script content is empty`);let s=/'{[^']+}'/.exec(o)?.[0];if(!s)throw new N(`Secure json wasn't found in secure script`);let c=JSON.parse(s.replaceAll(`'`,``));if(t!==`serial`)return{videoType:t,videoId:n,hash:r,...c};let l=i.find(e=>e.innerHTML.includes(`var videoInfo = {}`))?.textContent?.trim();if(!l)throw new N(`Failed to find videoInfo content`);let u=/videoInfo\.type\s+?=\s+?'([^']+)'/.exec(l)?.[1],d=/videoInfo\.id\s+?=\s+?'([^']+)'/.exec(l)?.[1],f=/videoInfo\.hash\s+?=\s+?'([^']+)'/.exec(l)?.[1];if(!u||!d||!f)throw new N(`Failed to parse videoInfo content`);return{videoType:u,videoId:d,hash:f,...c}}catch(t){return T.error(`Failed to get kodik secure data by videoPath: ${e}.`,t.message),!1}}async getFtor(e){let{videoType:t,videoId:n,hash:r,d:i,d_sign:a,pd:o,pd_sign:s,ref:c,ref_sign:l}=e;try{return await(await this.fetch(`${this.API_ORIGIN}/ftor`,{method:`POST`,headers:{"User-Agent":f.userAgent,Origin:this.API_ORIGIN,Referer:`${this.API_ORIGIN}/${t}/${n}/${r}/360p`},body:new URLSearchParams({d:i,d_sign:a,pd:o,pd_sign:s,ref:decodeURIComponent(c),ref_sign:l,bad_user:`false`,cdn_is_working:`true`,info:`{}`,type:t,hash:r,id:n})})).json()}catch(e){return T.error(`Failed to get kodik video data (type: ${t}, id: ${n}, hash: ${r})`,e.message),!1}}decryptUrl(e){return`https:${atob(e.replace(/[a-zA-Z]/g,e=>{let t=e.charCodeAt(0)+18,n=e<=`Z`?90:122;return String.fromCharCode(n>=t?t:t-26)}))}`}async getVideoData(e){let t=this.getSecureData(e);if(!t)return;let n=await this.getFtor(t);if(!n)return;let r=Object.entries(n.links[n.default.toString()]).find(([,e])=>e.type===`application/x-mpegURL`)?.[1];if(r)return{url:r.src.startsWith(`//`)?`https:${r.src}`:this.decryptUrl(r.src)}}async getVideoId(e){return/\/(uv|video|seria|episode|season|serial)\/([^/]+)\/([^/]+)\/([\d]+)p/.exec(e.pathname)?.[0]}},Vn=class extends Sn{SUBTITLE_SOURCE=`linkedin`;async getVideoData(e){let t=this.getVideoDataByPlayer(e);if(!t)return;let{url:n,duration:r,subtitles:i}=t;return{url:D(new URL(n)),duration:r,subtitles:i}}async getVideoId(e){return/\/learning\/(([^/]+)\/([^/]+))/.exec(e.pathname)?.[1]}},Hn;(function(e){e.Channel=`Channel`,e.Video=`Video`})(Hn||={});var Un=class extends P{getClientVersion(){if(!(typeof SENTRY_RELEASE>`u`))return SENTRY_RELEASE.id}async getVideoData(e){try{let t=this.getClientVersion();if(!t)throw new N(`Failed to get client version`);let n=await this.fetch(`https://www.loom.com/graphql`,{headers:{"User-Agent":f.userAgent,"content-type":`application/json`,"x-loom-request-source":`loom_web_${t}`,"apollographql-client-name":`web`,"apollographql-client-version":t,"Alt-Used":`www.loom.com`},body:`{"operationName":"FetchCaptions","variables":{"videoId":"${e}"},"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(n.status!==200)throw new N(`Failed to get data from graphql`);let r=(await n.json()).data.fetchVideoTranscript;if(r.__typename===`GenericError`)throw new N(r.message);return{url:this.service?.url+e,subtitles:[{format:`vtt`,language:E(r.language),source:`loom`,url:r.captions_source_url}]}}catch(t){return T.error(`Failed to get Loom video data, because: ${t.message}`),this.returnBaseData(e)}}async getVideoId(e){return/(embed|share)\/([^/]+)?/.exec(e.pathname)?.[2]}},Wn=class extends P{API_ORIGIN=`https://my.mail.ru`;async getVideoMeta(e){try{return await(await this.fetch(`${this.API_ORIGIN}/+/video/meta/${e}?xemail=&ajax_call=1&func_name=&mna=&mnb=&ext=1&_=${Date.now()}`)).json()}catch(e){T.error(`Failed to get mail.ru video data`,e.message);return}}async getVideoId(e){let t=e.pathname;if(/\/(v|mail|bk|inbox)\//.exec(t))return t.slice(1);let n=/video\/embed\/([^/]+)/.exec(t)?.[1];if(!n)return;let r=await this.getVideoMeta(n);if(r)return r.meta.url.replace(`//my.mail.ru/`,``)}},Gn=class extends P{DEFAULT_SITE_ORIGIN=`https://mediafile.cc`;SITE_ORIGIN=this.service?.url?.slice(0,-1)??this.DEFAULT_SITE_ORIGIN;getVideoSrc(){let e=this.video instanceof HTMLVideoElement?this.video:document.querySelector(`video`);return e?.src||e?.currentSrc||e?.querySelector(`source[src]`)?.src||void 0}async getVideoData(e){let t=this.getVideoSrc();if(t)return{url:`${this.SITE_ORIGIN}/${e}`,video_url:t,translationHelp:[{target:`video_file_url`,targetUrl:t}]}}async getVideoId(e){return e.pathname.replace(/^\/+/,``)||void 0}},Kn=class extends Sn{SUBTITLE_SOURCE=`netacad`;async getVideoData(e){let t=this.getVideoDataByPlayer(e);if(!t)return;let{url:n,duration:r,subtitles:i}=t;return{url:D(new URL(n)),duration:r,subtitles:i}}async getVideoId(e){return e.pathname+e.search}},qn=class extends P{async getVideoId(e){return/([^/]+)\/(view)\/([^/]+)/.exec(e.pathname)?.[0]}},Jn=class extends P{async getVideoId(e){return e.hostname===`nico.ms`?e.pathname.replace(/^\//,``).split(`/`)[0]||void 0:/\/watch\/([^/?#]+)/.exec(e.pathname)?.[1]}},Yn=class extends P{async getVideoData(e){let t=this.returnBaseData(e);if(!t)return t;try{if(!this.video)throw Error(`Video element not found`);let e=this.video.querySelector(`source[type^="video/mp4"], source[type^="video/webm"]`)?.src;if(!e||!/^https?:\/\//.test(e))throw Error(`Video source not found`);return{...t,translationHelp:[{target:`video_file_url`,targetUrl:e}]}}catch{return t}}async getVideoId(e){return/gag\/([^/]+)/.exec(e.pathname)?.[1]}},Xn=class extends P{API_ORIGIN=`https://odysee.com`;async getVideoData(e){try{let t=await(await this.fetch(`${this.API_ORIGIN}/${e}`)).text(),n=/"contentUrl":(\s)?"([^"]+)"/.exec(t)?.[2];if(!n)throw new N(`Odysee url doesn't parsed`);return{url:n}}catch(t){T.error(`Failed to get odysee video data by video ID: ${e}`,t.message);return}}async getVideoId(e){return e.pathname.slice(1)}},Zn=class extends P{async getVideoId(e){return/\/video\/(\d+)/.exec(e.pathname)?.[1]}},Qn=class extends P{async getVideoId(e){return/\/([a-z]{2}\/(?:[a-z0-9-]+\/)?(?:replay|videos?|original-series\/episode)\/[\w-]+)\/?$/i.exec(e.pathname)?.[1]}},$n=class extends Sn{SUBTITLE_SOURCE=`oraclelearn`;async getVideoData(e){let t=this.getVideoDataByPlayer(e);if(!t)return;let{url:n,duration:r,subtitles:i}=t,a=this.returnBaseData(e),o=D(new URL(n));return a?{url:a.url,duration:r,subtitles:i,translationHelp:[{target:`video_file_url`,targetUrl:o}]}:{url:o,duration:r,subtitles:i}}async getVideoId(e){return/\/ou\/course\/(([^/]+)\/(\d+)\/(\d+))/.exec(e.pathname)?.[1]}},er=class extends P{API_ORIGIN=`https://www.patreon.com/api`;async getPosts(e){try{return await(await this.fetch(`${this.API_ORIGIN}/posts/${e}?json-api-use-default-includes=false`)).json()}catch(t){return T.error(`Failed to get patreon posts by postId: ${e}.`,t.message),!1}}async getVideoData(e){let t=await this.getPosts(e);if(!t)return;let n=t.data.attributes.post_file.url;if(n)return{url:n}}async getVideoId(e){let t=/posts\/([^/]+)/.exec(e.pathname)?.[1];if(t)return t.replace(/[^\d.]/g,``)}},tr=class extends P{async getVideoId(e){let t=e.pathname.replace(/\/+$/,``),n=/\/videos\/watch\/([^/]+)/.exec(t)?.[1];if(n)return`/videos/watch/${n}`;let r=/\/w\/([^/]+)/.exec(t)?.[1];if(r)return`/videos/watch/${r}`}},nr=class extends P{async getVideoId(e){return/\/((?:videopopout|[^/]+(?:\/profile)?\/videos)\/[^/?#&/]+)\/?$/.exec(e.pathname)?.[1]??/^\/([^/#?]+)\/?$/.exec(e.pathname)?.[1]}},rr=class extends P{async getVideoId(e){return e.searchParams.get(`viewkey`)??/embed\/([^/]+)/.exec(e.pathname)?.[1]}},ir=class extends P{async getVideoData(e){try{if(typeof flashvars>`u`)return;let{rnd:e,video_url:t,video_title:n}=flashvars;if(!t||!e)throw new N(`Failed to find video source or rnd`);let r=new URL(t);r.searchParams.append(`rnd`,e),T.log(`PornTN get_file link`,r.href);let i=await this.fetch(r.href,{method:`head`}),a=new URL(i.url);return T.log(`PornTN cdn link`,a.href),{url:D(a),title:n}}catch(t){T.error(`Failed to get PornTN data by videoId: ${e}`,t.message);return}}async getVideoId(e){return/\/videos\/(([^/]+)\/([^/]+))/.exec(e.pathname)?.[1]}},ar=class extends P{async getVideoData(e){try{if(!e)throw new N(`Failed to find PreserveTube video ID`);return{url:`https://s3.archive.party/preservetube/${e}.mp4`}}catch(t){T.error(`Failed to get PreserveTube data by videoId: ${e}`,t.message);return}}async getVideoId(e){return e.searchParams.get(`v`)??void 0}},or=class extends P{API_ORIGIN=`https://www.reddit.com`;async getContentUrl(e){if(this.service?.additionalData!==`old`){let e=document.querySelector(`shreddit-player-2, shreddit-player`);return(e?.getAttribute(`src`)??e?.querySelector(`source[type="application/vnd.apple.mpegURL"]`)?.getAttribute(`src`))?.replaceAll(`&`,`&`)}return document.querySelector(`[data-hls-url]`)?.dataset.hlsUrl?.replaceAll(`&`,`&`)}async getVideoData(e){try{let t=await this.getContentUrl(e);if(!t)throw new N(`Failed to find content url`);return{url:decodeURIComponent(t)}}catch(t){T.error(`Failed to get reddit video data by video ID: ${e}`,t.message);return}}async getVideoId(e){return/\/r\/(([^/]+)\/([^/]+)\/([^/]+)\/([^/]+))/.exec(e.pathname)?.[1]}},sr=class extends P{async getVideoData(e){let t=document.querySelector(`.jw-video, .media__video_noscript`);if(!t)return;let n=t.getAttribute(`src`);if(n)return n.endsWith(`.MP4`)&&(n=D(n)),{videoId:e,url:n}}async getVideoId(e){return e.pathname.slice(1)}},cr=class extends P{async getVideoId(e){let t=/\/videos?\/(\d+)(?:\/(.+))?\/?$/.exec(e.pathname);if(!t)return;let[,n,r]=t;return r?`${n}/${r.replace(/\/+$/,``)}/`:n}},lr=class extends P{async getVideoId(e){return e.pathname.slice(1)}},ur=class extends P{async getVideoId(e){return/(?:video|embed)\/([^/]+)/.exec(e.pathname)?.[1]}},dr=class extends P{API_ORIGIN=`https://learning.sap.com/`;async requestKaltura(e,t,n){try{return await(await this.fetch(`https://${e}/api_v3/service/multirequest`,{method:`POST`,body:JSON.stringify({1:{service:`session`,action:`startWidgetSession`,widgetId:`_${t}`},2:{service:`baseEntry`,action:`list`,ks:`{1:result:ks}`,filter:{redirectFromEntryId:n},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:`3.3.0`,format:1,ks:``,clientTag:`html5:v3.17.22`,partnerId:t}),headers:{"Content-Type":`application/json`}})).json()}catch(e){T.error(`Failed to request kaltura data`,e.message);return}}async getKalturaData(e){try{let t=document.querySelector(`script[data-nscript="beforeInteractive"]`);if(!t)throw new N(`Failed to find script element`);let n=/https:\/\/([^"]+)\/p\/([^"]+)\/embedPlaykitJs\/uiconf_id\/([^"]+)/.exec(t?.src);if(!n)throw new N(`Failed to get sap data for videoId: ${e}`);let[,r,i]=n,a=document.querySelector(`#shadow`)?.firstChild?.getAttribute(`id`);if(!a){let e=document.querySelector(`#__NEXT_DATA__`);if(!e)throw new N(`Failed to find next data element`);a=/"sourceId":\s?"([^"]+)"/.exec(e.innerText)?.[1]}if(!r||Number.isNaN(+i)||!a)throw new N(`One of the necessary parameters for getting a link to a sap video in wasn't found for ${e}. Params: kalturaDomain = ${r}, partnerId = ${i}, entryId = ${a}`);return await this.requestKaltura(r,i,a)}catch(e){T.error(`Failed to get kaltura data`,e.message);return}}async getVideoData(e){let t=await this.getKalturaData(e);if(!t)return;let[,n,r]=t,{duration:i}=n.objects[0],a=r.sources.find(e=>e.format===`url`&&e.protocols===`http,https`&&e.url.includes(`.mp4`))?.url;if(a)return{url:a,subtitles:r.playbackCaptions.map(e=>({language:E(e.languageCode),source:`sap`,format:`vtt`,url:e.webVttUrl,isAutoGenerated:e.label.includes(`auto-generated`)})),duration:i}}async getVideoId(e){return/((courses|learning-journeys)\/([^/]+)(\/[^/]+)?)/.exec(e.pathname)?.[1]}},fr=class extends P{async getVideoId(e){return/\/([\da-z]+\/(?:video|play|embed)(?:\/[^/]+)?)\/?$/i.exec(e.pathname)?.[1]??/\/([\da-z]+-[\da-z]+\/playlist\/[^/]+)\/?$/i.exec(e.pathname)?.[1]}},pr=class e extends P{static getMediaViewer(){if(!(typeof appMediaViewer>`u`))return appMediaViewer}async getVideoId(t){let n=e.getMediaViewer();if(!n||n.live)return;let r=n.target.message;if(r.peer_id._!==`peerChannel`)return;let i=r.media;if(i._!==`messageMediaDocument`||i.document.type!==`video`)return;let a=r.mid&4294967295;return`${await n.managers.appPeersManager.getPeerUsername(r.peerId)}/${a}`}},mr=class extends P{async getVideoId(e){return/(videos|embed)\/[^/]+/.exec(e.pathname)?.[0]}},hr=class extends P{async getVideoId(e){return/([^/]+)\/video\/([^/]+)/.exec(e.pathname)?.[0]}},gr=class extends P{async getVideoId(e){let t=e.searchParams.get(`vid`),n=/([^/]+)\/([\d]+)/.exec(e.pathname)?.[0];if(!(!t||!n))return`${n}?vid=${t}`}},_r=class extends P{API_ORIGIN=`https://clips.twitch.tv`;async getClipLink(e,t){let n=document.querySelector(`script[type='application/ld+json']`),r=e.slice(1);if(n){let e=JSON.parse(n.innerText)[`@graph`].find(e=>e[`@type`]===`VideoObject`)?.creator.url;if(!e)throw new N(`Failed to find channel link`);return`${e.replace(`https://www.twitch.tv/`,``)}/clip/${r}`}let i=r===`embed`,a=document.querySelector(i?`.tw-link[data-test-selector='stream-info-card-component__stream-avatar-link']`:`.clips-player a:not([class])`);if(a)return`${a.href.replace(`https://www.twitch.tv/`,``)}/clip/${i?t:r}`}async getVideoData(e){let t=document.querySelector(`[data-a-target="stream-title"], [data-test-selector="stream-info-card-component__subtitle"]`)?.innerText,n=!!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+e,isStream:n,title:t}}async getVideoId(e){let t=e.pathname;if(/^m\.twitch\.tv$/.test(t))return/videos\/([^/]+)/.exec(e.href)?.[0]??t.slice(1);if(/^player\.twitch\.tv$/.test(e.hostname))return`videos/${e.searchParams.get(`video`)}`;let n=/([^/]+)\/(?:clip)\/([^/]+)/.exec(t);if(n)return n[0];if(/^clips\.twitch\.tv$/.test(e.hostname))return await this.getClipLink(t,e.searchParams.get(`clip`));let r=/(?:videos)\/([^/]+)/.exec(t);if(r)return r[0];let i=document.querySelector(`.home-offline-hero .tw-link`);if(i?.href){let e=new URL(i.href);return/(?:videos)\/([^/]+)/.exec(e.pathname)?.[0]}return document.querySelector(`.persistent-player`)?t:void 0}},vr=class extends P{async getVideoId(e){let t=/status\/([^/]+)/.exec(e.pathname)?.[1];if(t)return t;let n=(this.video?.closest(`[data-testid="tweet"]`))?.querySelector(`a[role="link"][aria-label]`)?.href;return n?/status\/([^/]+)/.exec(n)?.[1]:void 0}};function yr(e){return typeof e==`object`&&!!e}function br(e){return typeof e==`object`&&!!e}function xr(e){if(Array.isArray(e))return e.filter(br);if(typeof e!=`object`||!e)return[];let t=e;return(Array.isArray(t.Video)?t.Video:Array.isArray(t.video)?t.video:[]).filter(br)}function Sr(e){if(!yr(e))return[];let t=[];for(let[n,r]of Object.entries(e))!yr(r)||typeof r.url!=`string`||t.push({src:r.url,type:typeof r.type==`string`?r.type:void 0,label:typeof r.height==`number`||typeof r.height==`string`?r.height:n});return t}function Cr(e){if(typeof e==`number`&&Number.isFinite(e))return e;let t=String(e??``).match(/(\d{3,4})/);return Number(t?.[1]??0)}function wr(e){if(typeof e.file==`string`)return e.file;if(typeof e.src==`string`)return e.src}function Tr(e,t){return e.includes(`mpegurl`)||/\.m3u8(?:$|[?#])/i.test(t)}function Er(e,t){return e.includes(`dash`)||/\.mpd(?:$|[?#])/i.test(t)}var Dr=class extends P{API_ORIGIN=`${window.location.origin}/api-2.0`;getModuleData(){let e=(document.querySelector(`.ud-app-loader[data-module-id='course-taking']`)??document.querySelector(`[data-module-id='course-taking']`))?.dataset?.moduleArgs;if(e)try{return JSON.parse(e)}catch{return}}getLectureId(e){let t=/(?:\/learn\/(?:v4\/t\/)?lecture\/|#\/?lecture\/)(\d+)/i,n=/\/lecture\/view\/\?/i,r=window.location.href,i=n.test(r)?new URL(r).searchParams:void 0,a=i?(()=>{for(let[e,t]of i){let n=e.toLowerCase();if(n===`lectureid`||n===`lecture_id`)return t}})():void 0;return t.exec(r)?.[1]??a??(e?t.exec(`/${e}`)?.[1]:void 0)}getCourseId(e){let t=e,n=this.normalizeId(t?.courseId??t?.course_id??t?.course?.id);if(n)return n;let r=this.normalizeId(document.querySelector(`[data-course-id]`)?.getAttribute(`data-course-id`));if(r)return r;let i=document.documentElement?.innerHTML??``;return/data-course-id=["'](\d+)/i.exec(i)?.[1]??/"courseId"\s*:\s*(\d+)/i.exec(i)?.[1]??/"courseId"\s*:\s*(\d+)/i.exec(i)?.[1]}normalizeId(e){if(typeof e==`number`&&Number.isFinite(e))return String(e);if(typeof e==`string`)return/^\d+$/.test(e)?e:void 0}parseJson(e){try{return JSON.parse(e)}catch{let t=e.replaceAll(`"`,`"`).replaceAll(`"`,`"`).replaceAll(`'`,`'`).replaceAll(`'`,`'`);try{return JSON.parse(t)}catch{return}}}getViewHtmlCandidates(e){if(typeof e!=`string`||!e.trim())return[];let t=new DOMParser().parseFromString(e,`text/html`),n=[];for(let e of Array.from(t.querySelectorAll(`source`))){let t=e.getAttribute(`src`);t&&n.push({src:t,type:e.getAttribute(`type`)??void 0,label:e.getAttribute(`data-res`)??void 0})}for(let e of Array.from(t.querySelectorAll(`[videojs-setup-data]`))){let t=e.getAttribute(`videojs-setup-data`);if(!t)continue;let r=this.parseJson(t);r&&n.push(...xr(r.sources))}return n}isErrorData(e){return Object.hasOwn(e,`error`)||Object.hasOwn(e,`detail`)&&!Object.hasOwn(e,`_class`)}async getLectureData(e,t){try{let n=await(await this.fetch(`${this.API_ORIGIN}/users/me/subscribed-courses/${e}/lectures/${t}/?`+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(n))throw new N(n.detail??`unknown error`);return n}catch(n){T.error(`Failed to get lecture data by courseId: ${e} and lectureId: ${t}`,n.message);return}}async getCourseLang(e){try{let t=await(await this.fetch(`${this.API_ORIGIN}/users/me/subscribed-courses/${e}?`+new URLSearchParams({"fields[course]":`locale`}).toString())).json();if(!this.isErrorData(t))return t;let n=await(await this.fetch(`${this.API_ORIGIN}/courses/${e}/?`+new URLSearchParams({"fields[course]":`locale`}).toString())).json();if(this.isErrorData(n))throw new N(n.detail??`unknown error`);return n}catch(t){T.error(`Failed to get course lang by courseId: ${e}`,t.message);return}}findVideoUrl(e,t,n,r,i,a,o){let s=[],c=Array.isArray(e)?e:[];for(let e of c)s.push({src:e.src,type:e.type,label:e.label});s.push(...xr(t)),s.push(...xr(n)),s.push(...Sr(a)),typeof o==`string`&&s.push(...this.getViewHtmlCandidates(o)),typeof r==`string`&&s.push({src:r}),typeof i==`string`&&s.push({src:i});let l=this.video?.currentSrc||this.video?.src;typeof l==`string`&&l&&s.push({src:l});let u=new Map;for(let e of s){let t=wr(e);if(!t||/^javascript:/i.test(t))continue;let n=Cr(e.label??e.quality??e.height),r=String(e.type??``).toLowerCase(),i=u.get(t);(!i||n>i.quality)&&u.set(t,{url:t,type:r,quality:n,isYouTubeWatch:/:\/\/(?:www\.)?youtube\.com\/watch\?/i.test(t)})}let d=Array.from(u.values());if(!d.length)return;let f=d.filter(e=>e.type.includes(`mp4`)||/\.mp4(?:$|[?#])/i.test(e.url));return f.length?(f.sort((e,t)=>t.quality-e.quality),f[0]?.url):d.find(e=>Tr(e.type,e.url))?.url||d.find(e=>Er(e.type,e.url))?.url||d.find(e=>!e.isYouTubeWatch)?.url||d[0]?.url}getCaptionLocale(e){let t=typeof e.locale_id==`string`?e.locale_id:typeof e.locale?.locale==`string`?e.locale.locale:void 0;return t?E(t):void 0}findSubtitleUrl(e,t){if(!Array.isArray(e))return;let n=e.filter(e=>yr(e)&&(typeof e.url==`string`||typeof e.download_url==`string`)),r=n.find(e=>this.getCaptionLocale(e)===t)??n.find(e=>this.getCaptionLocale(e)===`en`)??n[0];return r?.url??r?.download_url}async getVideoData(e){let t=this.getModuleData(),n=this.getCourseId(t),r=this.getLectureId(e);if(T.log(`[Udemy] courseId: ${n}, lectureId: ${r}`),!r||!n)return;let i=await this.getLectureData(n,r);if(!i)return;let{title:a,description:o,asset:s,view_html:c}=i,{length:l,media_sources:u,captions:d}=s,f=s,p=f.stream_urls,m=f.download_urls,h=this.findVideoUrl(u,p,m,f.stream_url??f.streamUrl,f.external_url,f.data?.outputs,c);if(!h){T.log(`Failed to find video file in asset sources`,s);return}let g=`en`,_=(await this.getCourseLang(n))?.locale?.locale;typeof _==`string`&&(g=E(_)),bn.includes(g)||(g=`en`);let v=this.findSubtitleUrl(d,g);return v||T.log(`Failed to find subtitle file in captions`,d),{...v?{url:this.service?.url+e,translationHelp:[{target:`subtitles_file_url`,targetUrl:v},{target:`video_file_url`,targetUrl:h}],detectedLanguage:g}:{url:h,translationHelp:null},duration:l,title:a,description:o}}async getVideoId(e){return e.pathname.slice(1)}},Or=class extends P{API_KEY=``;DEFAULT_SITE_ORIGIN=`https://vimeo.com`;SITE_ORIGIN=this.service?.url?.slice(0,-1)??this.DEFAULT_SITE_ORIGIN;isErrorData(e){return Object.hasOwn(e,`error`)}isPrivatePlayer(){return this.referer&&!this.referer.includes(`vimeo.com`)&&this.origin.endsWith(`player.vimeo.com`)}toPublicUrl(e){let[t,n]=e.split(`:`,2);return n?`${this.DEFAULT_SITE_ORIGIN}/${t}/${n}`:`${this.DEFAULT_SITE_ORIGIN}/${t}`}returnPublicBaseData(e){let t=this.returnBaseData(e);if(t)return{...t,url:this.toPublicUrl(e)}}normalizePublicVideoUrl(e,t){try{let n=new URL(e);if(n.hostname===`player.vimeo.com`)return this.toPublicUrl(t);if(n.hostname.endsWith(`vimeo.com`)){let e=/^\/(\d+):([a-z0-9]+)$/i.exec(n.pathname);if(e)return`${this.DEFAULT_SITE_ORIGIN}/${e[1]}/${e[2]}`}}catch{}return e}async getViewerData(){try{let e=await(await this.fetch(`https://vimeo.com/_next/viewer`)).json(),{apiUrl:t,jwt:n}=e;return this.API_ORIGIN=`https://${t}`,this.API_KEY=`jwt ${n}`,e}catch(e){return T.error(`Failed to get default viewer data.`,e.message),!1}}async getVideoInfo(e){try{let t=new URLSearchParams({fields:`name,link,description,duration`}).toString(),n=await(await this.fetch(`${this.API_ORIGIN}/videos/${e}?${t}`,{headers:{Authorization:this.API_KEY}})).json();if(this.isErrorData(n))throw Error(n.developer_message??n.error);return n}catch(t){return T.error(`Failed to get video info by video ID: ${e}`,t.message),!1}}async getPrivateVideoSource(e){try{let{default_cdn:t,cdns:n}=e.dash,r=n[t].url,i=await this.fetch(r);if(i.status!==200)throw new N(await i.text());let a=await i.json(),o=new URL(a.base_url,r),s=a.audio.find(e=>e.mime_type===`audio/mp4`&&e.format===`dash`);if(!s)throw new N(`Failed to find video data`);let c=s.segments?.[0]?.url;if(!c)throw new N(`Failed to find first segment url`);let[l,u]=c.split(`?`,2),d=new URLSearchParams(u);return d.delete(`range`),new URL(`${s.base_url}${l}?${d.toString()}`,o).href}catch(e){return T.error(`Failed to get private video source`,e.message),!1}}async getPrivateVideoInfo(e){try{if(typeof playerConfig>`u`)return;let t=await this.getPrivateVideoSource(playerConfig.request.files);if(!t)throw new N(`Failed to get private video source`);let{video:{title:n,duration:r},request:{text_tracks:i}}=playerConfig;return{url:`${this.SITE_ORIGIN}/${e}`,video_url:t,title:n,duration:r,subs:i}}catch(t){return T.error(`Failed to get private video info by video ID: ${e}`,t.message),!1}}async getSubsInfo(e){try{let t=new URLSearchParams({per_page:`100`,fields:`language,type,link`}).toString(),n=await(await this.fetch(`${this.API_ORIGIN}/videos/${e}/texttracks?${t}`,{headers:{Authorization:this.API_KEY}})).json();if(this.isErrorData(n))throw Error(n.developer_message??n.error);return n.data}catch(t){return T.error(`Failed to get subtitles info by video ID: ${e}`,t.message),[]}}async getVideoData(e){if(this.isPrivatePlayer()){let t=await this.getPrivateVideoInfo(e);if(!t)return;let{url:n,subs:r,video_url:i,title:a,duration:o}=t,s=r.map(e=>({language:E(e.lang),source:`vimeo`,format:`vtt`,url:new URL(e.url,this.SITE_ORIGIN).href,isAutoGenerated:e.lang.includes(`autogenerated`)})),c=s.length?[{target:`video_file_url`,targetUrl:i},{target:`subtitles_file_url`,targetUrl:s[0].url}]:null;return{...c?{url:n,translationHelp:c}:{url:i},subtitles:s,title:a,duration:o}}if(!this.extraInfo||(e.includes(`/`)&&(e=e.replace(`/`,`:`)),!await this.getViewerData()))return this.returnPublicBaseData(e);let t=await this.getVideoInfo(e);if(!t)return this.returnPublicBaseData(e);let n=(await this.getSubsInfo(e)).map(e=>({language:E(e.language),source:`vimeo`,format:`vtt`,url:e.link,isAutoGenerated:e.language.includes(`autogen`)})),{link:r,duration:i,name:a,description:o}=t;return{url:this.normalizePublicVideoUrl(r,e),title:a,description:o,subtitles:n,duration:i}}async getVideoId(e){let t=e.pathname.replace(/\/+$/,``),n=/video\/[^/]+$/.exec(t)?.[0];if(this.isPrivatePlayer())return n;if(n){let t=e.searchParams.get(`h`),r=n.replace(`video/`,``);return t?`${r}/${t}`:r}return(/channels\/[^/]+\/([^/]+)/.exec(t)?.[1]??/groups\/[^/]+\/videos\/([^/]+)/.exec(t)?.[1]??/(showcase|album)\/[^/]+\/video\/([^/]+)/.exec(t)?.[2])||/([^/]+\/)?[^/]+$/.exec(t)?.[0]}},kr=class e extends P{static getPlayer(){if(!(typeof Videoview>`u`))try{return Videoview?.getPlayerObject?.()}catch{return}}async getVideoData(t){let n=new URL(window.location.href),r=e.getPlayer();if(!r){let e=this.returnBaseData(t);return e&&{...e,url:At(t,n)}}try{let{description:e,duration:i,md_title:a}=r.vars,o=new DOMParser().parseFromString(e,`text/html`),s=Array.from(o.body.childNodes).filter(e=>e.nodeName!==`BR`).map(e=>e.textContent).join(` +`),c;return Object.hasOwn(r.vars,`subs`)&&(c=r.vars.subs.map(e=>({language:E(e.lang),source:`vk`,format:`vtt`,url:e.url,isAutoGenerated:!!e.is_auto}))),{url:At(t,n),title:a,description:s,duration:i,subtitles:c}}catch(e){T.error(`Failed to get VK video data, because: ${e.message}`);let r=this.returnBaseData(t);return r&&{...r,url:At(t,n)}}}async getVideoId(e){let t=/^\/((?:video|clip)-?\d+_\d+)(?:\/)?$/.exec(e.pathname);if(t)return t[1];let n=/\/playlist\/[^/]+\/(video-?\d+_\d+)/.exec(e.pathname);if(n)return n[1];let r=e.searchParams.get(`z`);if(r)return r.split(`/`)[0];let i=e.searchParams.get(`oid`),a=e.searchParams.get(`id`);if(i&&a){let e=Math.abs(Number.parseInt(i,10));if(!Number.isNaN(e))return`video-${e}_${a}`}}},Ar=class extends P{async getVideoId(e){return/(video|embed)\/(\d+)(\/[^/]+\/)?/.exec(e.pathname)?.[0]}},jr=/^\d+:(?:[\da-f]{32}|\d{16,})$/i,Mr=/^[A-Za-z0-9]+$/,Nr=/^(?:www\.)?weibo\.com$/,Pr=/^\/newlogin\/?$/,Fr=class extends P{async getVideoId(e){if(e.hostname===`video.weibo.com`){let t=e.searchParams.get(`fid`);return!t||!jr.test(t)?void 0:`tv/show/${t}`}if(Nr.test(e.host)&&Pr.test(e.pathname)){let t=e.searchParams.get(`url`);if(t)try{let n=new URL(t,e.origin);if(n.href!==e.href){let e=await this.getVideoId(n);if(e)return e}}catch{}let n=e.searchParams.get(`layerid`);if(n&&Mr.test(n))return`0/${n}`}let t=e.pathname.replace(/\/+$/,``);if(/^\/\d+\/[A-Za-z0-9]+$/.test(t)||/^\/0\/[A-Za-z0-9]+$/.test(t)||/^\/tv\/show\/\d+:(?:[\da-f]{32}|\d{16,})$/i.test(t))return t.slice(1)}},Ir=class extends P{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(e){let t=Date.now(),n=e.substring(0,Math.min(255,e.length))+t,r=await Tt(this.API_HMAC_KEY,n);if(!r)throw new N(`Failed to get weverse HMAC signature`);return{wmsgpad:t.toString(),wmd:r}}async getHashURLParams(e){let t=await this.createHash(e);return new URLSearchParams(t).toString()}async getPostPreview(e){let t=`/post/v1.0/post-${e}/preview?`+new URLSearchParams({fieldSet:`postForPreview`,...this.getURLData()}).toString();try{let e=await this.getHashURLParams(t);return await(await this.fetch(`${this.API_ORIGIN+t}&${e}`,{headers:this.HEADERS})).json()}catch(t){return T.error(`Failed to get weverse post preview by postId: ${e}`,t.message),!1}}async getVideoInKey(e){let t=`/video/v1.1/vod/${e}/inKey?`+new URLSearchParams({gcc:`RU`,...this.getURLData()}).toString();try{let e=await this.getHashURLParams(t);return await(await this.fetch(`${this.API_ORIGIN+t}&${e}`,{method:`POST`,headers:this.HEADERS})).json()}catch(t){return T.error(`Failed to get weverse InKey by videoId: ${e}`,t.message),!1}}async getVideoInfo(e,t,n){let r=Date.now();try{let i=new URLSearchParams({key:t,sid:n,nonce:r.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/${e}?`+i,{headers:this.HEADERS})).json()}catch(r){return T.error(`Failed to get weverse video info (infraVideoId: ${e}, inkey: ${t}, serviceId: ${n}`,r.message),!1}}extractVideoInfo(e){return e.find(e=>e.useP2P===!1&&e.source.includes(`.mp4`))}async getVideoData(e){let t=await this.getPostPreview(e);if(!t)return;let{videoId:n,serviceId:r,infraVideoId:i}=t.extension.video;if(!(n&&r&&i))return;let a=await this.getVideoInKey(n);if(!a)return;let o=await this.getVideoInfo(i,a.inKey,r);if(!o)return;let s=this.extractVideoInfo(o.videos.list);if(s)return{url:s.source,duration:s.duration}}async getVideoId(e){return/([^/]+)\/(live|media)\/([^/]+)/.exec(e.pathname)?.[3]}},Lr=class extends P{async getVideoId(e){return/\/(videos\/[^/]+-[\dA-Za-z]+)\/?$/.exec(e.pathname)?.[1]}},Rr=class extends P{async getVideoId(e){return/[^/]+\/[^/]+$/.exec(e.pathname)?.[0]}},zr=class extends P{API_ORIGIN=window.location.origin;CLIENT_PREFIX=`/client/disk`;INLINE_PREFIX=`/i/`;DISK_PREFIX=`/d/`;isErrorData(e){return Object.hasOwn(e,`error`)}async getClientVideoData(e){let t=new URL(window.location.href).searchParams.get(`idDialog`);if(!t)return;let n=document.querySelector(`#preloaded-data`);if(n)try{let{idClient:e,sk:r}=JSON.parse(n.innerText).config,i=await(await this.fetch(`${this.API_ORIGIN}/models-v2?m=mpfs/info`,{method:`POST`,body:JSON.stringify({apiMethod:`mpfs/info`,connection_id:e,requestParams:{path:t},sk:r}),headers:{"Content-Type":`application/json`}})).json();if(this.isErrorData(i))throw new N(i.error?.message??i.error?.code);if(i?.type!==`file`)throw new N(`Failed to get resource info`);let{meta:{short_url:a,video_info:o},name:s}=i;if(!o)throw new N(`There's no video open right now`);if(!a)throw new N(`Access to the video is limited`);return{url:a,title:this.clearTitle(s),duration:Math.round(o.duration/1e3)}}catch(t){T.error(`Failed to get yandex disk video data by video ID: ${e}, because ${t.message}`);return}}clearTitle(e){return e.replace(/(\.[^.]+)$/,``)}getBodyHash(e,t){let n=JSON.stringify({hash:e,sk:t});return encodeURIComponent(n)}async fetchList(e,t){let n=this.getBodyHash(e,t),r=await(await this.fetch(`${this.API_ORIGIN}/public/api/fetch-list`,{method:`POST`,body:n})).json();if(Object.hasOwn(r,`error`))throw new N(`Failed to fetch folder list`);return r.resources}async getDownloadUrl(e,t){let n=this.getBodyHash(e,t),r=await(await this.fetch(`${this.API_ORIGIN}/public/api/download-url`,{method:`POST`,body:n})).json();if(r.error)throw new N(`Failed to get download url`);return r.data.url}async getDiskVideoData(e){try{let t=document.getElementById(`store-prefetch`);if(!t)throw new N(`Failed to get prefetch data`);let n=e.split(`/`).slice(3);if(!n.length)throw new N(`Failed to find video file path`);let{resources:r,rootResourceId:i,environment:{sk:a}}=JSON.parse(t.innerText),o=r[i],s=n.length-1,c=n.filter((e,t)=>t!==s).join(`/`),l=Object.values(r);c.includes(`/`)&&(l=await this.fetchList(`${o.hash}:/${c}`,a));let u=l.find(e=>e.name===n[s]);if(!u)throw new N(`Failed to find resource`);if(u&&u.type===`dir`)throw new N(`Path is dir, but expected file`);let{meta:{short_url:d,mediatype:f,videoDuration:p},path:m,name:h}=u;if(f!==`video`)throw new N(`Resource isn't a video`);let g=this.clearTitle(h),_=Math.round(p/1e3);if(d)return{url:d,duration:_,title:g};let v=await this.getDownloadUrl(m,a);return{url:D(new URL(v)),duration:_,title:g}}catch(t){T.error(`Failed to get yandex disk video data by disk video ID: ${e}`,t.message);return}}async getVideoData(e){return e.startsWith(this.INLINE_PREFIX)||/^\/d\/([^/]+)$/.exec(e)?{url:this.service?.url+e.slice(1)}:(e=decodeURIComponent(e),e.startsWith(this.CLIENT_PREFIX)?await this.getClientVideoData(e):await this.getDiskVideoData(e))}async getVideoId(e){return e.pathname.startsWith(this.CLIENT_PREFIX)?e.pathname+e.search:/\/i\/([^/]+)/.exec(e.pathname)?.[0]||(/\/d\/([^/]+)/.exec(e.pathname)?e.pathname:void 0)}},Br=class extends P{async getVideoId(e){return/v_show\/id_[\w=]+/.exec(e.pathname)?.[0]}},F=class e extends P{static isMobile(){return/^m\.youtube\.com$/.test(window.location.hostname)}static extractVideoId(t){let n=t.hash.replace(/^#/,``);if(n){let r=n.startsWith(`!`)?n.slice(1):n,i=r;try{i=decodeURIComponent(r)}catch{}try{let n=i.startsWith(`http`)?new URL(i):new URL(i.startsWith(`/`)?i:`/${i}`,t.origin),r=e.extractVideoId(n);if(r)return r}catch{let e=/(?:^|[?&#])v=([^&#]+)/.exec(i)?.[1];if(e)return e}}return t.hostname===`youtu.be`?t.pathname.replace(/^\/+/,``).split(`/`)[0]||void 0:(/\/(?:watch|embed|shorts|live|v|e)\/([^/?#]+)/.exec(t.pathname)?.[1]??void 0)||(t.searchParams.get(`v`)??void 0)}static getPlayer(){return window.location.pathname.startsWith(`/shorts/`)&&!e.isMobile()?document.querySelector(`#shorts-player`):document.querySelector(`#movie_player`)}static getPlayerResponse(){return e.getPlayer()?.getPlayerResponse?.call(void 0)}static getPlayerData(){return e.getPlayer()?.getVideoData?.call(void 0)}static getVolume(){let t=e.getPlayer();return t?.getVolume?t.getVolume()/100:1}static setVolume(t){let n=e.getPlayer();return n?.setVolume?(n.setVolume(Math.round(t*100)),!0):!1}static isMuted(){let t=e.getPlayer();return t?.isMuted?t.isMuted():!1}static videoSeek(t,n){T.log(`videoSeek`,n),t.currentTime=(e.getPlayer()?.getProgressState()?.seekableEnd??t.currentTime)-n}static getPoToken(){let t=e.getPlayer();if(!t)return;let n=t.getAudioTrack?.call(void 0);if(!n?.captionTracks?.length)return;let r=n.captionTracks.find(e=>e.url.includes(`&pot=`));if(r)return/&pot=([^&]+)/.exec(r.url)?.[1]}static getGlobalConfig(){return typeof yt<`u`?yt?.config_:typeof ytcfg<`u`?ytcfg?.data_:void 0}static getDeviceParams(){let t=e.getGlobalConfig();if(!t)return`c=WEB`;let n=t.INNERTUBE_CONTEXT?.client,r=new URLSearchParams(t.DEVICE);return r.delete(`ceng`),r.delete(`cengver`),r.set(`c`,n?.clientName??t.INNERTUBE_CLIENT_NAME),r.set(`cver`,n?.clientVersion??t.INNERTUBE_CLIENT_VERSION),r.set(`cplayer`,`UNIPLAYER`),r.toString()}static getSubtitles(t){let n=e.getPlayerResponse()?.captions?.playerCaptionsTracklistRenderer;if(!n)return[];let r=n.captionTracks??[],i=(n.translationLanguages??[]).find(e=>e.languageCode===t),a=r.find(e=>e?.kind===`asr`)?.languageCode??`en`,o=r.reduce((e,n)=>{if(!(`languageCode`in n))return e;let r=n.languageCode?E(n.languageCode):void 0,o=n.baseUrl;if(!r||!o)return e;let s=`${o.startsWith(`http`)?o:`${window.location.origin}/${o}`}&fmt=json3`;return e.push({source:`youtube`,format:`json`,language:r,isAutoGenerated:n?.kind===`asr`,url:s}),i&&n.isTranslatable&&n.languageCode===a&&t!==r&&e.push({source:`youtube`,format:`json`,language:t,isAutoGenerated:n?.kind===`asr`,translatedFromLanguage:r,url:`${s}&tlang=${t}`}),e},[]);return T.log(`youtube subtitles:`,o),o}static getLanguage(){if(!e.isMobile()){let t=e.getPlayer()?.getAudioTrack?.call(void 0)?.getLanguageInfo();if(t&&t.id!==`und`)return E(t.id.split(`.`)[0])}let t=e.getPlayerResponse()?.captions?.playerCaptionsTracklistRenderer.captionTracks.find(e=>e.kind===`asr`&&e.languageCode);return t?E(t.languageCode):void 0}async getVideoData(t){let{title:n}=e.getPlayerData()??{},{shortDescription:r,isLive:i,title:a}=e.getPlayerResponse()?.videoDetails??{},o=e.getSubtitles(this.language),s=e.getLanguage();s&&!bn.includes(s)&&(s=void 0);let c=e.getPlayer()?.getDuration?.call(void 0)??void 0;return{url:this.service?.url+t,isStream:i,title:a,localizedTitle:n,detectedLanguage:s,description:r,subtitles:o,duration:c}}async getVideoId(t){if(t.searchParams.has(`enablejsapi`)){let n=e.getPlayer()?.getVideoUrl();t=n?new URL(n):t}return e.extractVideoId(t)}},Vr=/^\/play\/([^/?#]+)\/([^/?#]+)\/([^/?#]+)\/?$/i,Hr=class extends P{async getVideoId(e){let t=Vr.exec(e.pathname);if(!t)return;let[,n,r,i]=t;return`${n}/${r}/${i}`}},Ur={[k.mailru]:Wn,[k.weverse]:Ir,[k.weibo]:Fr,[k.kodik]:Bn,[k.patreon]:er,[k.reddit]:or,[k.bannedvideo]:fn,[k.kick]:Rn,[k.appledeveloper]:ln,[k.epicgames]:An,[k.odysee]:Xn,[k.coursehunterLike]:yn,[k.twitch]:_r,[k.sap]:dr,[k.jove]:Ln,[k.linkedin]:Vn,[k.vimeo]:Or,[k.yandexdisk]:zr,[k.vk]:kr,[k.trovo]:gr,[k.incestflix]:In,[k.porntn]:ir,[k.googledrive]:Nn,[k.bilibili]:pn,[k.xvideos]:Rr,[k.xhamster]:Lr,[k.spankbang]:fr,[k.rule34video]:cr,[k.picarto]:nr,[k.olympicsreplay]:Qn,[k.watchpornto]:Ar,[k.archive]:un,[k.dailymotion]:wn,[k.youku]:Br,[k.egghead]:kn,[k.newgrounds]:qn,[k.okru]:Zn,[k.peertube]:tr,[k.eporner]:jn,[k.bitchute]:mn,[k.rutube]:ur,[k.facebook]:Mn,[k.rumble]:lr,[k.twitter]:vr,[k.pornhub]:rr,[k.tiktok]:hr,[k.proxitok]:hr,[k.nine_gag]:Yn,[k.youtube]:F,[k.preservetube]:ar,[k.invidious]:F,[k.piped]:F,[k.zdf]:Hr,[k.dzen]:On,[k.bunnystream]:_n,[k.cloudflarestream]:vn,[k.loom]:Un,[k.rtnews]:sr,[k.bitview]:hn,[k.thisvid]:mr,[k.ign]:Pn,[k.bunkr]:gn,[k.imdb]:Fn,[k.telegram]:pr,[k.niconico]:Jn,[j.udemy]:Dr,[j.coursera]:Cn,[j.douyin]:Dn,[j.artstation]:dn,[j.kickstarter]:zn,[j.datacamp]:Tn,[j.oraclelearn]:$n,[j.deeplearningai]:En,[j.netacad]:Kn,[j.mediafile]:Gn},Wr=class{helpersData;constructor(e={}){this.helpersData=e}getHelper(e){return new Ur[e](this.helpersData)}};function Gr(e){return e in Ur}function Kr(e,t){let n=e.replace(/^\/+/,``),r=new URL(`https://vk.com/video`);r.searchParams.set(`z`,n);for(let e of[`list`,`access_key`]){let n=t.searchParams.get(e);n&&r.searchParams.set(e,n)}return r.toString()}function qr(){if(tn.exec(window.location.href))return[];let e=window.location.hostname,t=new URL(window.location.href),n=n=>n instanceof RegExp?n.test(e):typeof n==`string`?e.includes(n):typeof n==`function`?n(t):!1;return cn.filter(e=>!!e.match&&(Array.isArray(e.match)?e.match.some(n):n(e.match))&&e.host&&e.url)}async function Jr(e,t={}){let n=new URL(window.location.href),r=e.host;return Gr(r)?await new Wr(t).getHelper(r).getVideoId(n):r===k.custom?n.href:void 0}async function Yr(e,t={}){let n=new URL(window.location.href),r=await Jr(e,t);if(!r)throw new en(`Entered unsupported link: "${e.host}"`);let i=n.origin;if([k.peertube,k.coursehunterLike,k.bunnystream,k.cloudflarestream].includes(e.host)&&(e.url=i),e.rawResult)return{url:r,videoId:r,host:e.host,duration:void 0};if(!e.needExtraData)return e.host===k.vk?{url:Kr(r,n),videoId:r,host:e.host,duration:void 0}:{url:e.url+r,videoId:r,host:e.host,duration:void 0};if(!Gr(e.host))throw new en(`No helper is available for "${e.host}"`);let a=await new Wr({...t,service:e,origin:i}).getHelper(e.host).getVideoData(r);if(!a)throw new en(`Failed to get video raw url for ${e.host}`);return{...a,url:e.host===k.vk?Kr(r,n):a.url,videoId:r,host:e.host}}var Xr={version:`1.0.6`,debug:!1,fetchFn:(...e)=>{if(typeof globalThis.fetch!=`function`)throw Error(`Fetch API is not available in this environment`);return globalThis.fetch(...e)}},I={log:(...e)=>{if(Xr.debug)return console.log(`%c✦ chaimu.js v${Xr.version} ✦`,`background: #000; color: #fff; padding: 0 8px`,...e)}},Zr=[`playing`,`ratechange`,`play`,`waiting`,`stalled`,`seeking`,`pause`,`ended`,`seeked`];function Qr(){let e=window.AudioContext??window.webkitAudioContext;return e?new e:void 0}var $r=1e4,ei=class{static name=`BasePlayer`;chaimu;fetch;isBuffering=!1;_src;fetchOpts;constructor(e,t){this.chaimu=e,this._src=t,this.fetch=this.chaimu.fetchFn,this.fetchOpts=this.chaimu.fetchOpts}async init(){return this}async clear(){return this}lipSync(e=!1){return this}handleVideoEvent=e=>(I.log(`handle video ${e.type}`),this.lipSync(e.type),this);isPlaybackBlocked(){let e=this.chaimu.video;return this.isBuffering||!e||e.ended||e.seeking||e.readyState{I.log(`[AudioPlayer] idle suspend`),await this.suspendAudioContext()},$r)}cancelSuspend(){this.suspendTimer!==void 0&&(clearTimeout(this.suspendTimer),this.suspendTimer=void 0)}updateAudio(){return this.audio=new Audio(this.src),this.audio.crossOrigin=`anonymous`,this}async init(){return this.updateAudio(),this.initAudioBooster(),this}async resumeAndPlayAudio(){if(!(!this.audio||this.isPlaybackBlocked())&&(this.cancelSuspend(),!(!await this.resumeAudioContext()||this.isPlaybackBlocked())))try{await this.audio.play()}catch(e){this.handlePlaybackError(`play audio element`,e)}}lipSync(e=!1){if(I.log(`[AudioPlayer] lipsync video`,this.chaimu.video),!this.chaimu.video||(this.audio.currentTime=this.chaimu.video.currentTime,this.audio.playbackRate=this.chaimu.video.playbackRate,!e))return this;switch(e){case`play`:case`playing`:case`seeked`:return this.isBuffering=!1,this.chaimu.video.paused||this.syncPlay(),this;case`waiting`:case`stalled`:case`seeking`:return this.isBuffering=!0,this.audio.pause(),this;case`pause`:case`ended`:return this.isBuffering=!1,this.pause(),this;default:return this}}async clear(){return this.cancelSuspend(),this.audio.pause(),this.audio.src=``,this.audio.removeAttribute(`src`),this.disconnectAudioNodes(),await this.suspendAudioContext(),this}syncPlay(){return I.log(`[AudioPlayer] sync play called`),this.resumeAndPlayAudio(),this}async play(){return I.log(`[AudioPlayer] play called`),await this.resumeAndPlayAudio(),this}async pause(){return I.log(`[AudioPlayer] pause called`),this.isBuffering=!1,this.audio&&this.audio.pause(),this.scheduleSuspend(),this}set src(e){if(this._src=e,!e){this.clear();return}this.audio.src=e}get src(){return this._src}get currentSrc(){return this.audio.currentSrc}set volume(e){if(this.gainNode){this.gainNode.gain.value=e;return}this.audio.volume=e}get volume(){return this.gainNode?this.gainNode.gain.value:this.audio.volume}get playbackRate(){return this.audio.playbackRate}set playbackRate(e){this.audio.playbackRate=e}get currentTime(){return this.audio.currentTime}},ni=class extends ei{static name=`ChaimuPlayer`;audioBuffer;audioElement;mediaElementSource;gainNode;blobUrl;isClearing=!1;isInitializing=!1;clearingPromise;suspendTimer;async fetchAudio(){if(!this._src)throw Error(`No audio source provided`);if(!this.chaimu.audioContext)throw Error(`No audio context available`);I.log(`[ChaimuPlayer] Fetching audio from ${this._src}...`);let e;try{let t=await this.fetch(this._src,this.fetchOpts);if(!t.ok)throw Error(`Response status: ${t.status}`);I.log(`[ChaimuPlayer] Decoding fetched audio...`);let n=await t.arrayBuffer(),r=new Blob([n]);e=URL.createObjectURL(r),this.audioBuffer=await this.chaimu.audioContext.decodeAudioData(n),this.blobUrl&&URL.revokeObjectURL(this.blobUrl),this.blobUrl=e,e=void 0}catch(t){throw e&&URL.revokeObjectURL(e),Error(`Failed to fetch audio file, because ${t.message}`)}return this}initAudioBooster(){return this.chaimu.audioContext?(this.disconnectAudioNodes(),this.gainNode=this.chaimu.audioContext.createGain(),this):this}disconnectAudioNodes(){this.mediaElementSource&&=(this.mediaElementSource.disconnect(),void 0),this.gainNode&&=(this.gainNode.disconnect(),void 0)}scheduleSuspend(){this.cancelSuspend(),this.suspendTimer=setTimeout(async()=>{I.log(`[ChaimuPlayer] idle suspend`),await this.suspendAudioContext()},$r)}cancelSuspend(){this.suspendTimer!==void 0&&(clearTimeout(this.suspendTimer),this.suspendTimer=void 0)}async init(){if(this.isInitializing)throw Error(`Initialization already in progress`);this.isInitializing=!0;try{return await this.fetchAudio(),this.initAudioBooster(),this.createAudioElement(),this}finally{this.isInitializing=!1}}createAudioElement(){if(!this.chaimu.audioContext)throw Error(`No audio context available`);if(!this.blobUrl)throw Error(`No blob URL available.`);let e=new Audio(this.blobUrl);e.crossOrigin=`anonymous`,`preservesPitch`in e&&(e.preservesPitch=!0,`mozPreservesPitch`in e&&(e.mozPreservesPitch=!0),`webkitPreservesPitch`in e&&(e.webkitPreservesPitch=!0)),this.audioElement=e;let t=this.gainNode;if(!t)throw Error(`Audio gain node is missing`);this.mediaElementSource=this.chaimu.audioContext.createMediaElementSource(e),this.mediaElementSource.connect(t),t.connect(this.chaimu.audioContext.destination)}lipSync(e=!1){if(I.log(`[ChaimuPlayer] lipsync video`,this.chaimu.video,this),!this.chaimu.video||!e)return this;switch(e){case`play`:case`playing`:case`ratechange`:case`seeked`:return this.isBuffering=!1,this.chaimu.video.paused||this.start(),this;case`waiting`:case`stalled`:case`seeking`:return this.isBuffering=!0,this.audioElement&&this.audioElement.pause(),this;case`pause`:case`ended`:return this.isBuffering=!1,this.pause(),this;default:return this}}async reopenCtx(){if(!this.chaimu.audioContext)throw Error(`No audio context available`);try{this.chaimu.audioContext.state!==`closed`&&await this.chaimu.audioContext.close()}catch(e){I.log(`[ChaimuPlayer] Failed to close audio context:`,e)}return this.chaimu.audioContext=Qr(),this}async clear(){if(this.isClearing&&this.clearingPromise)return this.clearingPromise;if(!this.chaimu.audioContext)throw Error(`No audio context available`);return I.log(`clear audio context`),this.cancelSuspend(),this.isClearing=!0,this.clearingPromise=(async()=>{try{this.audioElement&&=(this.audioElement.pause(),void 0),this.blobUrl&&=(URL.revokeObjectURL(this.blobUrl),void 0);let e=this.gainNode?this.gainNode.gain.value:1;return this.disconnectAudioNodes(),await this.reopenCtx(),this.chaimu.audioContext&&(this.initAudioBooster(),this.volume=e,await this.suspendAudioContext()),this}finally{this.isClearing=!1,this.clearingPromise=void 0}})(),this.clearingPromise}async start(){if(!this.chaimu.audioContext)throw Error(`No audio context available`);if(!this.audioElement)throw Error(`Audio element is missing`);if(this.isClearing&&this.clearingPromise&&(I.log(`The other cleaner is still running, waiting...`),await this.clearingPromise),this.cancelSuspend(),this.isPlaybackBlocked()||(I.log(`starting audio via HTMLAudioElement`),!await this.resumeAudioContext())||this.isPlaybackBlocked())return this;this.chaimu.video&&(this.audioElement.currentTime=this.chaimu.video.currentTime,this.audioElement.playbackRate=this.chaimu.video.playbackRate);try{await this.audioElement.play()}catch(e){this.handlePlaybackError(`play audio element`,e)}return this}async pause(){if(!this.chaimu.audioContext)throw Error(`No audio context available`);return this.isBuffering=!1,this.audioElement&&this.audioElement.pause(),this.scheduleSuspend(),this}async play(){if(!this.chaimu.audioContext)throw Error(`No audio context available`);return this.cancelSuspend(),await this.resumeAudioContext(),this}set src(e){this._src=e}get src(){return this._src}get currentSrc(){return this._src}set volume(e){this.gainNode&&(this.gainNode.gain.value=e)}get volume(){return this.gainNode?this.gainNode.gain.value:0}set playbackRate(e){this.audioElement&&(this.audioElement.playbackRate=e)}get playbackRate(){return this.audioElement?this.audioElement.playbackRate:this.chaimu.video?.playbackRate??1}get currentTime(){return this.chaimu.video?.currentTime??0}},ri=class{_debug=!1;audioContext;player;video;fetchFn;fetchOpts;constructor({url:e,video:t,debug:n=!1,fetchFn:r=Xr.fetchFn,fetchOpts:i={},preferAudio:a=!1}){this._debug=Xr.debug=n,this.fetchFn=r,this.fetchOpts=i,this.audioContext=Qr(),this.player=this.audioContext&&!a?new ni(this,e):new ti(this,e),this.video=t}async init(){await this.player.init(),this.player.addVideoEvents(),this.video.paused||this.video.ended||this.video.seeking||this.video.readyStatetypeof e==`string`&&e.startsWith(`getVideoId:`),extractVideoId:e=>e.pathname.split(`/`).at(-2)??null,responseFormatter:(e,t)=>`${typeof t==`string`?t:``}:${e}`},"https://www.dailymotion.com":{targetOrigin:`https://geo.dailymotion.com`,dataFilter:e=>typeof e==`string`&&e.startsWith(`getVideoId:`),extractVideoId:e=>/(?:^|\/)video\/([^/]+)/.exec(e.pathname)?.[1],responseFormatter:e=>`getVideoId:${e}`}}).find(([e])=>globalThis.location.origin===e)?.[1];e&&globalThis.addEventListener(`message`,t=>{try{if(t.origin!==e.targetOrigin||!e.dataFilter(t.data))return;let n=e.extractVideoId(new URL(globalThis.location.href));if(!n)return;let r=e.responseFormatter(n,t.data);t.source&&`postMessage`in t.source&&t.source.postMessage(r,e.targetOrigin)}catch(e){console.error(`Iframe communication error:`,e)}})}var ui=`api.browser.yandex.ru`,di=`media-proxy.toil.cc/v1/proxy/m3u8`,fi=`vot-new.toil-dump.workers.dev`,pi=`vot-worker.kload.workers.dev`,mi=`https://vot.toil.cc/v1`,hi=`https://translate-backend.transly.workers.dev/v2`,gi=`https://rust-server-531j.onrender.com/detect`,_i=`https://rust-server-531j.onrender.com`,vi=`https://avatars.mds.yandex.net/get-yapic`,yi=`ilyhalight/voice-over-translation`,bi=`https://raw.githubusercontent.com/${yi}`,xi=`https://github.com/${yi}`,Si=`yandexbrowser`,Ci=`yandexbrowser`,wi=[`UA`,`LV`,`LT`],Ti=1e3,Ei=`2025-05-09`,Di=[`auto`,`original`],Oi=`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`.split(`.`),ki=()=>{},L={log:ki,warn:ki,error:ki};function Ai(){return navigator.language?.substring(0,2).toLowerCase()||`en`}var ji=new Set([`uk`,`be`,`bg`,`mk`,`sr`,`bs`,`hr`,`sl`,`pl`,`sk`,`cs`]);function Mi(e){return xn.includes(e)?e:ji.has(e)?`ru`:`en`}var Ni=Ai(),Pi=Mi(Ni),Fi=3e4,Ii=/\p{Cc}/gu,Li=/[\\/:*?"'<>|]+/g,Ri=/^https?:\/\//i,zi=/-{2,}/g,Bi=/^[.\s-]+|[.\s-]+$/g;function Vi(){return new Date().toISOString().slice(0,10)}function Hi(e){return e.replace(Ii,``)}function Ui(e){let t=new WeakSet;return JSON.stringify(e,(e,n)=>{if(n&&typeof n==`object`){if(t.has(n))return`[Circular]`;if(t.add(n),Array.isArray(n))return n;let e={},r=Object.keys(n).sort();for(let t of r)e[t]=n[t];return e}return n})}function Wi(e){let t=2166136261,n=0;for(;n65535?2:1}return(t>>>0).toString(36)}var Gi=()=>!!document.pictureInPictureEnabled;async function Ki(e,t){try{let n=await e.createWritable();return await n.write(t),await n.close(),!0}catch{return!1}}async function qi(e,t){let n=typeof navigator>`u`?void 0:navigator;if(!n?.share||typeof File>`u`)return`unsupported`;let r;try{r=new File([e],t,{type:e.type||`application/octet-stream`})}catch{return`unsupported`}if(typeof n.canShare==`function`&&!n.canShare({files:[r]}))return`unsupported`;try{return await n.share({files:[r],title:t}),`shared`}catch(e){return e instanceof DOMException&&e.name===`AbortError`?`shared`:`error`}}function Ji(e,t){let n=URL.createObjectURL(e),r=document.createElement(`a`);r.href=n,r.download=t,r.rel=`noopener noreferrer`,r.target=`_blank`,r.style.position=`fixed`,r.style.left=`-9999px`,r.style.top=`0`,(document.body??document.documentElement).append(r);try{return r.click(),!0}catch{return!1}finally{r.remove(),Xi(n)}}async function Yi(e,t,n={}){return n.fileHandle&&await Ki(n.fileHandle,e)?!0:n.preferShare?await qi(e,t)===`shared`:Ji(e,t)}function Xi(e,t=Fi){let n=Number.isFinite(t)&&t>=0?t:Fi;globalThis.setTimeout(()=>URL.revokeObjectURL(e),n)}function Zi(e){let t=e.trim();return t&&Hi(t).replace(Ri,``).replace(Li,`-`).replace(zi,`-`).replace(Bi,``)||Vi()}var Qi=()=>Math.floor(Date.now()/1e3),$i=e=>e?Object.fromEntries(new Headers(e)):{};function R(e,t=0,n=100){return Math.min(Math.max(e,Math.min(t,n)),Math.max(t,n))}function ea(e){let t={},n=Object.entries(e);for(;n.length;){let e=n.pop();if(!e)continue;let[r,i]=e;if(i!==void 0){if(!(typeof i==`object`&&i&&!Array.isArray(i))){t[r]=i;continue}for(let[e,t]of Object.entries(i))n.push([`${r}.${e}`,t])}}return t}var ta=7200*1e3,na=`x-vot-cache-created-at`,ra=`x-vot-cache-key`,ia=`vot-http-cache-v1`,aa=500,oa=`VOTSession`,sa={translation:`translationExpiresAt`,subtitles:`subtitlesExpiresAt`};function ca(){return Math.floor(Date.now()/1e3)}function la(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.expires==`number`&&Number.isFinite(t.expires)&&typeof t.timestamp==`number`&&Number.isFinite(t.timestamp)&&typeof t.uuid==`string`&&t.uuid.length>0&&typeof t.secretKey==`string`&&t.secretKey.length>0}function ua(e){if(!e||typeof e!=`object`)return{};let t=ca(),n=Object.entries(e).flatMap(([e,n])=>!la(n)||n.timestamp+n.expires<=t?[]:[[e,n]]);return Object.fromEntries(n)}function da(e){return Object.keys(e).length>0}var fa=class{constructor(e=B){this.storage=e}getStorageKey(){return oa}async restore(e,t={}){let n=this.getStorageKey(),r=await this.storage.getRaw(n),i=ua(r);return da(i)?{...t,...i}:(r!==void 0&&await this.storage.deleteRaw(n),t)}async persist(e,t){let n=this.getStorageKey(),r=ua(t);if(!da(r)){await this.storage.deleteRaw(n);return}await this.storage.setRaw(n,r)}},pa=class{cache=new Map;clear(){this.cache.clear()}getTranslation(e){return this.getValue(e,`translation`)}setTranslation(e,t){this.setValue(e,`translation`,t)}getSubtitles(e){return this.getValue(e,`subtitles`)}setSubtitles(e,t){this.setValue(e,`subtitles`,t)}deleteSubtitles(e){this.deleteValue(e,`subtitles`)}getValue(e,t){let n=Date.now(),r=this.cache.get(e);if(!r)return;let i=sa[t],a=r[i];if(a!==void 0&&a<=n){r[t]=void 0,r[i]=void 0,this.evictIfEmpty(e,r);return}return r[t]}setValue(e,t,n){let r=Date.now(),i=this.getOrCreateEntry(e),a=r+ta,o=sa[t];i[t]=n,i[o]=a}deleteValue(e,t){let n=this.cache.get(e);if(!n)return;let r=sa[t];n[t]=void 0,n[r]=void 0,this.evictIfEmpty(e,n)}evictIfEmpty(e,t){t.translation===void 0&&t.subtitles===void 0&&this.cache.delete(e)}getOrCreateEntry(e){let t=this.cache.get(e);if(t)return t;let n={};return this.cache.set(e,n),n}},ma=new class{memoryCache=new Map;inFlightRequests=new Map;async execute(e,t,n){if(!t||t.ttlMs<=0)return n();let r=t.key??this.buildDefaultCacheKey(e);if(!r)return n();let i=this.normalizeMethod(e.method),a=t.ttlMs,o=t.cacheName||ia,s=t.useMemory!==!1,c=t.useCacheApi!==!1&&i===`GET`&&this.supportsCacheApi(),l=c?Wi(r):``,u=t.dedupe!==!1,d=t.allowStaleOnError!==!1,f=Date.now(),p=await this.readCachedResponse({key:r,nowMs:f,useMemory:s,useCacheApi:c,cacheName:o,url:e.url,cacheApiKey:l,ttlMs:a,allowStaleOnError:d});if(p.fresh)return p.fresh;if(!u)return await this.runNetworkRequestWithFallback({key:r,cacheName:o,url:e.url,cacheApiKey:l,ttlMs:a,useMemory:s,useCacheApi:c},n,d?p.stale:void 0);let m=this.inFlightRequests.get(r);if(m!==void 0)return(await m).clone();let h=this.runNetworkRequestWithFallback({key:r,cacheName:o,url:e.url,cacheApiKey:l,ttlMs:a,useMemory:s,useCacheApi:c},n,d?p.stale?.clone():void 0);this.inFlightRequests.set(r,h);try{return(await h).clone()}finally{this.inFlightRequests.delete(r)}}async readCachedResponse({key:e,nowMs:t,useMemory:n,useCacheApi:r,cacheName:i,url:a,cacheApiKey:o,ttlMs:s,allowStaleOnError:c}){let l;if(n){let n=this.readMemoryCache(e,t);if(n.fresh)return{fresh:n.fresh};l=n.stale}if(!r)return{stale:l};let u=await this.readCacheApi(i,a,o,s,t,c);return u.fresh?(n&&this.writeMemoryCache(e,u.fresh.clone(),u.expiresAt??t+s),{fresh:u.fresh}):{stale:l??u.stale}}async runNetworkRequestWithFallback(e,t,n){try{return await this.runNetworkRequest(e,t)}catch(e){if(n)return n;throw e}}async runNetworkRequest({key:e,cacheName:t,url:n,cacheApiKey:r,ttlMs:i,useMemory:a,useCacheApi:o},s){let c=await s();if(!c.ok)return c;let l=Date.now(),u=this.computeExpiresAt(l,i);if(a&&this.writeMemoryCache(e,c.clone(),u),o){let e=this.toStorableResponse(c.clone(),l);await this.writeCacheApi(t,n,r,e)}return c}computeExpiresAt(e,t){return!Number.isFinite(t)||t<=0?e:t>=2**53-1-e?2**53-1:e+t}normalizeMethod(e){return(e||`GET`).toUpperCase()}resolveBodyKey(e){if(e==null)return``;if(typeof e==`string`)return e;if(e instanceof URLSearchParams)return e.toString()}buildDefaultCacheKey(e){let t=this.normalizeMethod(e.method);if(t===`GET`)return`${t}:${e.url}`;let n=this.resolveBodyKey(e.body);if(n!==void 0)return`${t}:${e.url}#${Wi(n)}`}supportsCacheApi(){return typeof caches<`u`&&typeof caches.open==`function`}readCreatedAtMs(e){let t=e.headers.get(na);if(!t)return null;let n=Number(t);return Number.isFinite(n)?n:null}ensureVaryByCacheKey(e){let t=e.get(`vary`);if(!t){e.set(`vary`,ra);return}let n=new Set(t.split(`,`).map(e=>e.trim().toLowerCase()));!n.has(`*`)&&!n.has(ra)&&e.set(`vary`,`${t}, ${ra}`)}toStorableResponse(e,t){let n=new Headers(e.headers);return n.set(na,String(t)),this.ensureVaryByCacheKey(n),new Response(e.body,{status:e.status,statusText:e.statusText,headers:n})}readMemoryCache(e,t){let n=this.memoryCache.get(e);return n?n.expiresAt>t?(this.touchMemoryCache(e,n),{fresh:n.response.clone(),expiresAt:n.expiresAt}):(this.memoryCache.delete(e),{stale:n.response.clone(),expiresAt:n.expiresAt}):{}}touchMemoryCache(e,t){this.memoryCache.delete(e),this.memoryCache.set(e,t)}trimMemoryCache(){for(;this.memoryCache.size>aa;){let e=this.memoryCache.keys().next().value;if(typeof e!=`string`)break;this.memoryCache.delete(e)}}writeMemoryCache(e,t,n){this.memoryCache.has(e)&&this.memoryCache.delete(e),this.memoryCache.set(e,{response:t,expiresAt:n}),this.trimMemoryCache()}async readCacheApi(e,t,n,r,i,a){try{let o=new Request(t,{method:`GET`,headers:{[ra]:n}}),s=await caches.open(e),c=await s.match(o);if(!c)return{};let l=this.readCreatedAtMs(c);if(l===null)return await s.delete(o),{};let u=this.computeExpiresAt(l,r);if(u>i)return{fresh:c.clone(),expiresAt:u};if(!a)return await s.delete(o),{};let d=c.clone();return await s.delete(o),{stale:d,expiresAt:u}}catch{return{}}}async writeCacheApi(e,t,n,r){try{let i=new Request(t,{method:`GET`,headers:{[ra]:n}});await(await caches.open(e)).put(i,r)}catch{}}};async function ha(e,t,n){return ma.execute(e,t,n)}function ga(e){let t=new WeakSet;try{return JSON.stringify(e,(e,n)=>typeof n!=`object`||!n?n:t.has(n)?`[Circular]`:(t.add(n),n))??null}catch{return null}}function _a(e){let t=[e?.data?.message,e?.error?.message,e?.message];for(let e of t)if(typeof e==`string`&&e)return e;return null}function va(e,t){let n=_a(e);if(n)return n;let r=ga(e);if(r&&r!==`{}`)return r;let i=e.constructor?.name;return i?`[${i}]`:t}function ya(e,t){return typeof e==`number`||typeof e==`boolean`||typeof e==`bigint`?`${e}`:typeof e==`symbol`?e.description?`Symbol(${e.description})`:`Symbol`:typeof e==`function`?e.name?`[Function ${e.name}]`:`[Function]`:t}function ba(e,t=`Unknown error`){return e instanceof Error?e.message||t:typeof e==`string`?e||t:e==null?t:typeof e==`object`?va(e,t):ya(e,t)}function xa(e){return ba(e,``)}function Sa(e){let t=e;return typeof DOMException<`u`&&t instanceof DOMException&&t.name===`AbortError`||t instanceof Error&&t.name===`AbortError`||t?.message===`AbortError`}function Ca(e=`Aborted`){try{return new DOMException(e,`AbortError`)}catch{let t=Error(e);return t.name=`AbortError`,t}}var wa=new AbortController().signal;function Ta(e){let t=e.throwIfAborted;if(typeof t==`function`)try{t.call(e);return}catch(t){throw e.aborted||Sa(t)?Ca():t instanceof Error?t:Error(String(t))}if(e.aborted)throw Ca()}function Ea(e,t){if(!(Number.isFinite(e)&&e>0))return{signal:t??wa,cleanup:()=>{}};let n=new AbortController,r,i=()=>{r!==void 0&&(clearTimeout(r),r=void 0),n.abort(t?.reason)};return t&&(t.addEventListener(`abort`,i,{once:!0}),t.aborted&&i()),n.signal.aborted||(r=setTimeout(()=>{n.abort(Ca(`Timeout`)),r=void 0},e)),{signal:n.signal,cleanup:()=>{r!==void 0&&(clearTimeout(r),r=void 0),t?.removeEventListener(`abort`,i)}}}var Da=`api.browser.yandex.ru`,Oa=`googlevideo.com`,ka=/^([\w-]+):\s*(.+)$/,Aa=/^[a-zA-Z][a-zA-Z\d+.-]*:/,ja=typeof GM_info>`u`?void 0:GM_info?.scriptHandler;function Ma(){let e=typeof GM_xmlhttpRequest>`u`?globalThis.GM_xmlhttpRequest:GM_xmlhttpRequest;return typeof e==`function`?e:void 0}function Na(){let e=typeof GM>`u`?globalThis.GM:GM,t=e?.xmlHttpRequest??e?.xmlhttpRequest;return typeof t==`function`?t.bind(e):void 0}function Pa(){return!!(Ma()||Na())}var Fa=!(typeof IS_EXTENSION<`u`&&IS_EXTENSION)&&!!ja&&!Pa(),Ia=typeof GM<`u`||globalThis.GM!==void 0,La=Pa();function Ra(e){let t=e.trim();try{return new URL(t).hostname.toLowerCase()}catch{if(!Aa.test(t))try{return new URL(`https://${t}`).hostname.toLowerCase()}catch{}return}}function za(e,t){return e===t||e.endsWith(`.${t}`)}function Ba(e,t,n=!1){if(n)return!0;if(!e){let e=t.toLowerCase();return e.includes(Da)||e.includes(Oa)}return za(e,Da)||za(e,Oa)}function Va(e){return typeof e==`string`?e:e instanceof URL?e.href:e.url}function Ha(e,t){return t?t.toUpperCase():e instanceof Request?(e.method||`GET`).toUpperCase():`GET`}function Ua(e){return typeof e!=`string`||e.length===0?{}:e.split(/\r?\n/).reduce((e,t)=>{let n=ka.exec(t);if(!n)return e;let[,r,i]=n;return e[r]=i,e},{})}function Wa(e){let t=e;return typeof t?.error==`string`?t.error:typeof t?.statusText==`string`?t.statusText:xa(e)||`Unknown error`}async function Ga(e,t,n){let r=$i(n.headers),i=Ma(),a=Na();if(i)return await new Promise((a,o)=>{let s=!1,c,l=()=>{c&&n.signal?.removeEventListener(`abort`,c)},u=e=>{s||(s=!0,l(),o(e))},d=i({method:n.method||`GET`,url:e,responseType:`blob`,data:n.body,timeout:t,headers:r,onload:t=>{if(s)return;s=!0,l();let n=Ua(t.responseHeaders),r=new Response(t.response,{status:t.status,statusText:typeof t.statusText==`string`?t.statusText:``,headers:n});Object.defineProperty(r,`url`,{value:t.finalUrl??e}),a(r)},ontimeout:()=>u(Error(`Timeout`)),onerror:e=>u(Error(Wa(e))),onabort:()=>u(Ca())});if(c=()=>{try{d?.abort?.()}catch{}u(Ca())},n.signal&&(n.signal.addEventListener(`abort`,c,{once:!0}),n.signal.aborted)){c();return}});if(!a)throw TypeError(`GM_xmlhttpRequest is not available`);let o=a({method:n.method||`GET`,url:e,responseType:`blob`,data:n.body,timeout:t,headers:r}),s;try{let t=new Promise((e,t)=>{n.signal&&(s=()=>{try{o.abort?.()}catch{}t(Ca())},n.signal.addEventListener(`abort`,s,{once:!0}),n.signal.aborted&&s())}),r=await Promise.race([o,t]),i=Ua(r.responseHeaders),a=new Response(r.response,{status:r.status,statusText:typeof r.statusText==`string`?r.statusText:``,headers:i});return Object.defineProperty(a,`url`,{value:r.finalUrl??e}),a}finally{s&&n.signal?.removeEventListener(`abort`,s)}}async function z(e,t={}){let{timeout:n=15e3,forceGmXhr:r=!1,responseCache:i,...a}=t,o=Va(e),s=Ra(o),c=Ha(e,a.method),l=async()=>{if(Ba(s,o,r))return L.log(`GM_fetch: routing request via GM_xmlhttpRequest`,{host:s??`unknown`,reason:r?`forced`:`host-policy`,url:o}),await Ga(o,n,a);let{signal:t,cleanup:i}=Ea(n,a.signal);try{return await fetch(e,{...a,signal:t})}catch(e){if(t.aborted||Sa(e))throw e;return L.log(`GM_fetch preventing CORS by GM_xmlhttpRequest`,xa(e)||`Unknown error`),await Ga(o,n,a)}finally{i()}};return i?await ha({url:o,method:c,body:a.body},i,l):await l()}var Ka=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(([e,t])=>t.map(([t,n])=>({category:e,oldKey:t,newKey:n??t,shouldDeleteOldKey:!!n}))),qa=new Map(Ka.map(e=>[e.oldKey,e])),Ja=Array.from(new Set(Ka.map(e=>e.oldKey)));function Ya(e){let t={};for(let n of e)t[n]=void 0;return t}function Xa(e,t){switch(e){case`numToBool`:case`number`:return typeof t==`number`;case`array`:return Array.isArray(t);case`string`:return typeof t==`string`||t===null;default:return!1}}function Za(e,t){switch(e){case`string`:case`array`:case`number`:return t;default:return!!t}}function Qa(e,t){let n=Za(e.category,t);return e.oldKey===`autoVolume`&&typeof t==`number`&&t<1&&(n=Math.round(t*100)),n}function $a(e,t){return Array.isArray(e)&&Array.isArray(t)?e.length===t.length&&e.every((e,n)=>Object.is(e,t[n])):Object.is(e,t)}function eo(e){if(e!==null)try{return JSON.parse(e)}catch{return}}async function to(e){if(e.compatVersion===`2025-05-09`)return e;let t=new Set([...Object.keys(e),...Ja]),n=await B.getValues(Ya(t)),r={...e},i=[],a=[];for(let[e,t]of Object.entries(n)){if(t===void 0)continue;let o=qa.get(e);if(!o||!Xa(o.category,t))continue;let s=Qa(o,t);r[o.newKey]=s;let c=n[o.newKey];(o.shouldDeleteOldKey||!$a(c,s))&&i.push(B.set(o.newKey,s)),o.shouldDeleteOldKey&&a.push(B.delete(o.oldKey))}return await Promise.all([...i,...a]),{...r,compatVersion:Ei}}var no=class{support=null;localStorageListeners=new Map;shouldUseSyntheticListeners(e){return!e.promiseAddValueChangeListener&&!e.legacyAddValueChangeListener}getGMRuntime(){return typeof GM<`u`?GM:globalThis.GM}resolveSupport(){if(this.support)return this.support;let e=this.getGMRuntime(),t={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:Ia&&typeof e?.getValue==`function`,promiseGetValues:Ia&&typeof e?.getValues==`function`,promiseSet:Ia&&typeof e?.setValue==`function`,promiseDelete:Ia&&typeof e?.deleteValue==`function`,promiseList:Ia&&typeof e?.listValues==`function`,promiseAddValueChangeListener:Ia&&typeof e?.addValueChangeListener==`function`,promiseRemoveValueChangeListener:Ia&&typeof e?.removeValueChangeListener==`function`};return this.support=t,L.log(`[VOT Storage] GM Promises: ${t.promiseGet} | GM legacy: ${t.legacyGet}`),t}get isSupportOnlyLS(){let e=this.resolveSupport();return!e.legacyGet&&!e.legacySet&&!e.legacyDelete&&!e.legacyList&&!e.promiseGet&&!e.promiseGetValues&&!e.promiseSet&&!e.promiseDelete&&!e.promiseList}syncGetByName(e,t,n){if(n.legacyGet)return GM_getValue(e,t);let r=globalThis.localStorage.getItem(e);if(r===null)return t;try{return JSON.parse(r)}catch{return t}}async getRaw(e,t){let n=this.resolveSupport();return n.promiseGet&&GM.getValue?await GM.getValue(e,t):this.syncGetByName(e,t,n)}async get(e,t){return this.getRaw(e,t)}async getValues(e){let t=this.resolveSupport();if(t.promiseGetValues&&GM.getValues)return await GM.getValues(e);let n=Object.entries(e);if(t.promiseGet&&GM.getValue){let e=await Promise.all(n.map(async([e,t])=>[e,await GM.getValue(e,t)]));return Object.fromEntries(e)}return Object.fromEntries(n.map(([e,n])=>[e,this.syncGetByName(e,n,t)]))}syncSetByName(e,t,n){return n.legacySet?GM_setValue(e,t):globalThis.localStorage.setItem(e,JSON.stringify(t))}async setRaw(e,t){let n=this.resolveSupport(),r=e,i=this.shouldUseSyntheticListeners(n),a=i?await this.getRaw(e):void 0;if(n.promiseSet&&GM.setValue){await GM.setValue(e,t),i&&this.notifyLocalStorageListeners(r,a,t,!1);return}let o=this.syncSetByName(e,t,n);return this.notifyLocalStorageListeners(r,a,t,!1),o}async set(e,t){return this.setRaw(e,t)}syncDeleteByName(e,t){return t.legacyDelete?GM_deleteValue(e):globalThis.localStorage.removeItem(e)}async deleteRaw(e){let t=this.resolveSupport(),n=e,r=this.shouldUseSyntheticListeners(t),i=r?await this.getRaw(e):void 0;if(t.promiseDelete&&GM.deleteValue){await GM.deleteValue(e),r&&this.notifyLocalStorageListeners(n,i,void 0,!1);return}let a=this.syncDeleteByName(e,t);return this.notifyLocalStorageListeners(n,i,void 0,!1),a}async delete(e){return this.deleteRaw(e)}addValueChangeListener(e,t){let n=this.resolveSupport(),r=this.getGMRuntime();if(n.promiseAddValueChangeListener){let i=r?.addValueChangeListener,a=n.promiseRemoveValueChangeListener?r?.removeValueChangeListener:void 0;if(typeof i==`function`){let n=i(e,this.createTypedListener(t));return()=>{typeof a==`function`&&a(n)}}}if(n.legacyAddValueChangeListener){let r=globalThis.GM_addValueChangeListener,i=n.legacyRemoveValueChangeListener?globalThis.GM_removeValueChangeListener:void 0;if(typeof r==`function`){let n=r(e,this.createTypedListener(t));return()=>{typeof i==`function`&&i(n)}}}let i=this.getLocalStorageListeners(e),a=t;i.add(a);let o=t=>{t.storageArea!==globalThis.localStorage||t.key!==e||a(e,eo(t.oldValue),eo(t.newValue),!0)};return globalThis.addEventListener(`storage`,o),()=>{i.delete(a),i.size===0&&this.localStorageListeners.delete(e),globalThis.removeEventListener(`storage`,o)}}createTypedListener(e){return(t,n,r,i)=>{e(t,n,r,i)}}getLocalStorageListeners(e){let t=this.localStorageListeners.get(e);if(t)return t;let n=new Set;return this.localStorageListeners.set(e,n),n}notifyLocalStorageListeners(e,t,n,r){let i=this.localStorageListeners.get(e);if(!(!i||i.size===0))for(let a of i)a(e,t,n,r)}syncList(e){return e.legacyList?GM_listValues():Oi}async list(){let e=this.resolveSupport();return e.promiseList&&GM.listValues?await GM.listValues():this.syncList(e)}},ro=`__VOT_STORAGE_SINGLETON__`,B=(()=>{let e=globalThis,t=e[ro];if(t instanceof no)return t;let n=new no;return e[ro]=n,n})(),io=`vot-auth`,ao=`account-updated`;function oo(){return{source:io,type:ao}}function so(e){if(!e||typeof e!=`object`)return!1;let t=e;return t.source===`vot-auth`&&t.type===`account-updated`}function co(e=globalThis.opener){!e||typeof e.postMessage!=`function`||e.postMessage(oo(),`*`)}function lo(){let e=globalThis._userData;if(!e||typeof e!=`object`)return null;let t=e;return typeof t.avatar_id!=`string`||typeof t.username!=`string`||t.avatar_id.length===0||t.username.length===0?null:{avatar_id:t.avatar_id,username:t.username}}async function uo(){let{access_token:e,expires_in:t}=Object.fromEntries(new URLSearchParams(globalThis.location.hash.slice(1)));if(!e||!t)throw Error(`[VOT] Invalid token response`);let n=Number.parseInt(t,10);if(Number.isNaN(n))throw TypeError(`[VOT] Invalid expires_in value`);await B.set(`account`,{token:e,expires:Date.now()+n*1e3,username:void 0,avatarId:void 0}),co()}async function fo(){let e=lo();if(!e)throw Error(`[VOT] Invalid user data`);let{avatar_id:t,username:n}=e,r=await B.get(`account`);if(!r)throw Error(`[VOT] No account data found`);await B.set(`account`,{...r,username:n,avatarId:t}),co()}async function po(){if(globalThis.location.pathname===`/auth/callback`)return uo();if(globalThis.location.pathname===`/my/profile`)return fo()}var mo={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`},ho=[`localePhrases`,`localeLang`,`localeHash`,`localeVersion`,`localeUpdatedAt`,`localeLangOverride`],go=ea(mo),_o=`master`,vo=(()=>{let e=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`.split(`.`))?`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`.split(`.`):[`en`];return e.includes(`auto`)?e:[`auto`,...e]})();function yo(e,t){return e||t||`unknown`}function bo(){return yo(`1.11.4`,typeof GM_info<`u`?String(GM_info?.script?.version||``):``)}var V=new class{lang;locale;defaultLocale=go;localesUrl=`${bi}/${_o}/src/localization/locales`;hashesUrl=`${bi}/${_o}/src/localization/hashes.json`;warnedMissingKeys=new Set;_langOverride=`auto`;constructor(){this.lang=this.getLang(),this.locale={}}async init(){let[e,t]=await Promise.all([B.get(`localeLangOverride`,`auto`),B.get(`localePhrases`,``)]);return this._langOverride=e,this.lang=this.getLang(),this.setLocaleFromJsonString(t),this}get langOverride(){return this._langOverride}getLang(){return this.langOverride===`auto`?Ni:this.langOverride}getAvailableLangs(){return[...vo]}async reset(){return await Promise.all(ho.map(e=>B.delete(e))),this}buildUrl(e,t=``,n=!1){return`${e}${t}${n?`?timestamp=${Qi()}`:``}`}async changeLang(e){return this.langOverride===e?!1:(await B.set(`localeLangOverride`,e),this._langOverride=e,this.lang=this.getLang(),await this.update(!0),!0)}async checkUpdates(e=!1){L.log(`Check locale updates...`);try{let t=await z(this.buildUrl(this.hashesUrl,``,e));if(!t.ok)throw t.status;let n=await t.json();if(!n||typeof n!=`object`)throw Error(`Invalid locale hashes payload`);let r=n[this.lang];return typeof r!=`string`||!r||await B.get(`localeHash`,``)===r?!1:r}catch(e){return console.error(`[VOT] [localizationProvider] Failed to get locales hash:`,e),null}}async update(e=!1){let t=bo(),n=await B.get(`localeVersion`,``),r=await this.checkUpdates(e);if(r===null)return this;if(!r)return n!==t&&await B.set(`localeVersion`,t),this;let i=Qi();L.log(`Updating locale...`);try{let n=await z(this.buildUrl(this.localesUrl,`/${this.lang}.json`,e));if(!n.ok)throw n.status;let a=await n.text();this.setLocaleFromJsonString(a),await Promise.all([B.set(`localePhrases`,a),B.set(`localeHash`,r),B.set(`localeLang`,this.lang),B.set(`localeVersion`,t),B.set(`localeUpdatedAt`,i)])}catch(e){console.error(`[VOT] [localizationProvider] Failed to get locale:`,e),this.setLocaleFromJsonString(await B.get(`localePhrases`,``))}return this}setLocaleFromJsonString(e){let t=e.trim();if(!t)return this.locale={},this.warnedMissingKeys.clear(),this;try{let e=JSON.parse(t);if(!e||typeof e!=`object`||Array.isArray(e))throw Error(`Locale payload should be a JSON object`);this.locale=ea(e)}catch(e){console.error(`[VOT] [localizationProvider]`,e),this.locale={}}return this.warnedMissingKeys.clear(),this}getFromLocale(e,t,n=`locale`){return e[t]??this.warnMissingKey(e,t,n)}warnMissingKey(e,t,n){let r=`${n}:${t}`;this.warnedMissingKeys.has(r)||(this.warnedMissingKeys.add(r),console.warn(`[VOT] [localizationProvider] locale`,e,`doesn't contain key`,t))}getDefault(e){return this.getFromLocale(this.defaultLocale,e,`default`)??e}get(e){return this.getFromLocale(this.locale,e)??this.getDefault(e)}getLangLabel(e){let t=`langs.${e}`;if(t in this.defaultLocale){let e=this.get(t);if(e)return e}return e.toUpperCase()}},xo=null;function So(){return xo??=V.init(),xo}var Co=()=>globalThis.self!==globalThis.top,wo=!1,To=null;async function Eo(e,t){if(!wo){if(To!==null){await To;return}To=(async()=>{if(t(`Activating runtime`,{reason:e}),globalThis.location.origin===`https://rust-server-531j.onrender.com`){await po(),wo=!0;return}await So(),Co()||await V.update(),L.log(`Selected menu language: ${V.lang}`),wo=!0})();try{await To}finally{To=null}}}var Do=new WeakSet;function Oo(e){let{videoObserver:t,videosWrappers:n,ensureRuntimeActivated:r,getServicesCached:i,findContainer:a,createVideoHandler:o}=e;if(Do.has(t))return;Do.add(t);let s=new WeakSet,c=new WeakMap,l=new WeakMap,u=new WeakMap,d=e=>{let t=l.get(e);return t&&c.get(t)===e&&c.delete(t),l.delete(e),t??void 0},f=e=>{e&&u.delete(e)},p=async(e,t)=>{let r=n.get(e);if(r)try{await r.release()}catch(e){console.error(`[VOT] Failed to release videoHandler (${t})`,e)}finally{n.delete(e)}},m=e=>{for(let t of i()){let n=a(t,e);if(n)return{site:t,container:n}}return null},h=e=>{let t=String(e.host);return t===`peertube`||t===`directlink`?{...e,url:globalThis.location.origin}:e},g=async e=>{if(!e)return;let t=u.get(e);t&&(u.delete(e),!(!t.isConnected||n.has(t)||s.has(t))&&await _(t))},_=async e=>{if(!(n.has(e)||s.has(e))){s.add(e);try{try{await r(`video-detected`)}catch(e){console.error(`[VOT] Failed to activate runtime`,e);return}let t=m(e);if(!t)return;let{site:i,container:a}=t,s=c.get(a);if(s&&s!==e){if(s.isConnected){u.set(a,e);return}await p(s,`stale container`),d(s)}let _=o(e,a,h(i));n.set(e,_),l.set(e,a),c.set(a,e);try{if(await _.init(),n.get(e)!==_)return;try{await _.setCanPlay()}catch(e){console.error(`[VOT] Failed to get video data`,e)}}catch(t){if(n.get(e)===_){await p(e,`init failed`);let t=d(e);f(t),await g(t)}console.error(`[VOT] Failed to initialize videoHandler`,t)}}finally{s.delete(e)}}};t.onVideoAdded.addListener(_),t.onVideoRemoved.addListener(async e=>{let t=d(e);await p(e,`video removed`),s.delete(e),t&&u.get(t)===e&&f(t),await g(t)})}function ko(e){return e.isIframe?e.href===`about:blank`||e.href.startsWith(`about:srcdoc`)||e.origin===`null`:!1}function Ao(e){return ko(e)?`skip`:!e.isIframe&&e.origin===e.authOrigin?`auth-eager`:`lazy`}function jo(e){return e?typeof ShadowRoot<`u`&&e instanceof ShadowRoot?e.host:e.parentNode??null:null}function Mo(e,t){let n=t;for(;n;){if(n===e)return!0;n=jo(n)}return!1}function No(e){return e===document.body||e===document.documentElement}function Po(e,t,n={}){let{allowDocumentViewport:r=!1}=n;if(!(e instanceof HTMLElement)||No(e)&&!r)return null;for(let n of t)if(n&&(Mo(e,n)||Mo(n,e)))return e;return null}function Fo(e,t){return!e||!t?null:Ro(e,t,e instanceof Document?null:e)}function Io(e,t,n){if(!n)return e.querySelector(t);let r=e.querySelectorAll(t);for(let e of r)if(Mo(e,n))return e;return null}function Lo(e){let t=e.getRootNode();if(t instanceof ShadowRoot)return t.host;if(t instanceof Document)return t;if(t!==e){let n=jo(t);if(n&&n!==e&&n instanceof Element)return n}return null}function Ro(e,t,n){return e?e instanceof Document?Io(e,t,n):e.closest(t)||Ro(Lo(e),t,n):null}function zo(e,t){if(!t)return null;let n=Fo(e,t);return n instanceof HTMLElement&&n.isConnected&&Mo(n,e)?n:null}function Bo(e,t){return t.host===`youtube`&&t.additionalData!==`mobile`?e.parentElement??e:e}function Vo(e){let t=Bo(e.container,e.site),n=e.fullscreenRoot??t;return{base:t,root:n,portalContainer:t,subtitlesMountContainer:n}}var H=class{listeners=new Set;get size(){return this.listeners.size}addListener(e){return this.listeners.add(e),this}removeListener(e){return this.listeners.delete(e),this}dispatch(...e){for(let t of this.listeners)try{t(...e)}catch(e){console.error(`[VOT]`,e)}}async dispatchAsync(...e){let t=[];for(let n of this.listeners)try{let r=n(...e);r&&typeof r.then==`function`&&t.push(Promise.resolve(r))}catch(e){console.error(`[VOT]`,e)}if(!t.length)return;let n=await Promise.allSettled(t);for(let e of n)e.status===`rejected`&&console.error(`[VOT]`,e.reason)}clear(){this.listeners.clear()}};function Ho(e,t,n,r){return JSON.stringify({downloadType:e,itag:t,minChunkSize:r,fileSize:n})}function Uo(e){return e?.toLowerCase()??``}function Wo(e){let t=Uo(e);return t.includes(`audio/`)&&!t.includes(`video/`)}function Go(e){let t=Uo(e);return t.includes(`audio/mp4`)&&t.includes(`mp4a.`)}function Ko(e){let t=Uo(e);return t.includes(`audio/webm`)&&t.includes(`opus`)}function qo(e){if(!e)return`mp4a.40.2`;let t=/codecs="([^"]+)"/i.exec(e);if(!t?.[1])return`mp4a.40.2`;let n=t[1].split(`,`).map(e=>e.trim());return n.find(e=>e.toLowerCase().startsWith(`mp4a.`))??n[0]??`mp4a.40.2`}function Jo(e,t){let n=null,r=t===`max`?-1/0:1/0;for(let i of e){let e=i.bitrate??0;(t===`max`&&e>r||t===`min`&&eWo(e.mimeType));if(!n.length)throw Error(`No adaptive audio formats were found in player response`);let r=t===`bestefficiency`?`min`:`max`,i=t===`bestefficiency`?[n.filter(e=>Ko(e.mimeType)),n]:[n.filter(e=>Go(e.mimeType)),n.filter(e=>Ko(e.mimeType)),n];for(let e of i){if(!e.length)continue;let t=Jo(e,r);if(t)return t}throw Error(`No adaptive audio formats were found in player response`)}var Xo=class extends Error{status;constructor(e=403){super(`Failed to load watch page: ${e}`),this.name=`YtWatchContextForbiddenError`,this.status=e}},Zo=/^[a-zA-Z0-9_-]{11}$/,Qo=`https://www.youtube.com`,$o=`19.44.38`,es=`1.60.19`,ts=`19.45.4`,ns=[`ANDROID_VR`,`ANDROID`,`IOS`,`WEB`,`MWEB`],rs={accept:`*/*`,origin:Qo,referer:`${Qo}/`},is=256*1024;function as(e){return e?{signal:e}:{}}function os(e,t,n){switch(e){case`ANDROID`:case`YTMUSIC_ANDROID`:case`YTSTUDIO_ANDROID`:return{clientName:`ANDROID`,clientVersion:$o,hl:`en`,gl:`US`,androidSdkVersion:34,osName:`Android`,osVersion:`14`,platform:`MOBILE`};case`ANDROID_VR`:return{clientName:`ANDROID_VR`,clientVersion:es,hl:`en`,gl:`US`,androidSdkVersion:31,osName:`Android`,osVersion:`12`,platform:`MOBILE`};case`IOS`:return{clientName:`IOS`,clientVersion:ts,hl:`en`,gl:`US`,platform:`MOBILE`,osName:`iPhone`,osVersion:`18.0.0.22A3354`,deviceMake:`Apple`,deviceModel:`iPhone16,2`};case`MWEB`:return{clientName:`MWEB`,clientVersion:t.clientVersion,hl:`en`,gl:`US`,originalUrl:`${Qo}/watch?v=${n}`};default:return{clientName:`WEB`,clientVersion:t.clientVersion,hl:`en`,gl:`US`,utcOffsetMinutes:0,originalUrl:`${Qo}/watch?v=${n}`}}}function ss(e){let t=e.trim();if(Zo.test(t))return t;let n;try{n=new URL(t)}catch{throw Error(`Cannot extract YouTube video id from: ${e}`)}let r=n.hostname.toLowerCase();if(r===`youtu.be`||r.endsWith(`.youtu.be`))return cs(n.pathname.split(`/`).find(Boolean),e);let i=n.searchParams.get(`v`);if(i&&Zo.test(i))return i;let a=ls(n.pathname.split(`/`).filter(Boolean));if(a)return a;throw Error(`Cannot extract YouTube video id from: ${e}`)}function cs(e,t){if(e&&Zo.test(e))return e;throw Error(`Cannot extract YouTube video id from: ${t}`)}function ls(e){for(let t of[`shorts`,`embed`]){let n=e.indexOf(t);if(n===-1)continue;let r=e[n+1];if(r&&Zo.test(r))return r}return null}function us(e){return e.replaceAll(`\\u0026`,`&`).replaceAll(`\\/`,`/`)}function ds(e){let t=e.videoId??e.videoUrl;if(!t)throw Error(`Either videoId or videoUrl is required`);return ss(t)}function fs(e,t){for(let n of t){let t=n.exec(e)?.[1];if(t)return t}}async function ps(e){return new Uint8Array(await e.arrayBuffer())}function ms(e=16){let t=`abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_`,n=``;if(typeof crypto<`u`&&typeof crypto.getRandomValues==`function`){let r=new Uint8Array(e);crypto.getRandomValues(r);for(let e of r)n+=t[e%64]??`a`;return n}for(let r=0;ri)return!1;let a=_s(e.headers.get(`content-range`));return a?a.start===n&&a.end===n+t.byteLength-1:e.status===206?t.byteLength===i:e.status===200?n===0&&t.byteLength===i:!1}function bs(e,t){let n=e.headers.get(`content-range`)??`none`,r=e.headers.get(`content-length`)??`none`;return`status=${e.status}; bytes=${t.byteLength}; content-range=${n}; content-length=${r}`}function xs(e){let t=e?.toLowerCase()??``;return t.includes(`audio/webm`)?`audio/webm`:t.includes(`audio/mp4`)?`audio/mp4`:`audio/aac`}function Ss(e){let t=e?[e,...ns]:[...ns],n=new Set;return t.filter(e=>n.has(e)?!1:(n.add(e),!0))}var Cs=class{fetchFn;constructor(e={}){this.fetchFn=e.fetchImplementation??fetch}async fetchRangeChunk(e,t,n,r){let i=`bytes=${t}-${n}`,a=await this.fetchFn(e,{headers:{...rs,range:i},...as(r)});if(!a.ok)throw Error(`Failed to download stream chunk: ${a.status}`);let o=await ps(a);if(!ys(a,o,t,n))throw Error(`Received unexpected stream chunk payload: ${bs(a,o)}`);return o}async downloadStreamByRanges(e,t,n){let r=await this.resolveStreamContentLength(e,t,n,!0),i=new Uint8Array(r),a=0;for(let t=0;ti.byteLength)throw Error(`Downloaded stream chunk exceeds probed stream content length`);i.set(s,a),a+=s.byteLength}return a===i.byteLength?i:i.slice(0,a)}async downloadAudioToChunkStream(e,t){if(t.chunkSize<=0)throw RangeError(`Audio downloader. ytAudio. chunkSize must be > 0`);return this.withResolvedPlayableAudioFormat(e,e.videoQuality??`best`,`Chunk mode requires an adaptive audio stream format`,`Unable to resolve streamable format for chunk mode`,async({resolved:e,signal:n})=>{let r=await this.resolveStreamContentLength(e.streamUrl,e.chosenFormat.contentLength,n,!0),i=Math.max(1,Math.ceil(r/t.chunkSize));return{videoId:e.videoId,fileSize:r,itag:e.chosenFormat.itag??0,mediaPartsLength:i,getMediaBuffers:async function*(){for(let a=0;a{let r=await this.downloadStreamByRanges(e.streamUrl,e.chosenFormat.contentLength,n),i=this.getExtractionHints(e.chosenFormat);return await t.write(r),{videoId:e.videoId,bytesWritten:r.byteLength,mimeType:xs(e.chosenFormat.mimeType),codec:i.codec,sampleRate:i.sampleRate,channels:i.channels}})}async withResolvedPlayableAudioFormat(e,t,n,r,i){let a=ds(e),{signal:o}=e,s=await this.fetchWatchContext(a,o),c=Ss(e.client),l=[];for(let e of c)try{let r=await this.resolvePlayableFormatForClient({videoId:a,watchContext:s,client:e,quality:t,signal:o});if(!Wo(r.chosenFormat.mimeType))throw Error(n);return await i({resolved:r,signal:o})}catch(t){let n=t instanceof Error?t.message:String(t);l.push(`${e}: ${n}`)}throw Error(`${r}. Attempts: ${l.join(` | `)}`)}async resolvePlayableFormatForClient({videoId:e,watchContext:t,client:n,quality:r,signal:i}){let a=((await this.fetchPlayerResponse(e,t,n,i)).streamingData?.adaptiveFormats??[]).filter(e=>!!e.url);if(!a.length)throw Error(`Player response did not contain direct adaptive audio stream URLs`);let o=Yo(a,r);return{videoId:e,chosenFormat:o,streamUrl:this.resolveFormatUrl(o,t.clientVersion)}}async resolveStreamContentLength(e,t,n,r=!1){let i=hs(t);if(i!==null&&!r)return i;let a;try{a=await this.fetchFn(e,{headers:{...rs,range:`bytes=0-0`},...as(n)})}catch(e){if(i!==null)return i;let t=e instanceof Error?e.message:String(e);throw Error(`Failed to probe stream content length: ${t}`)}if(!a.ok){if(i!==null)return i;throw Error(`Failed to probe stream content length: ${a.status}`)}let o=gs(a.headers.get(`content-range`)),s=hs(a.headers.get(`x-goog-stored-content-length`)),c=hs(a.headers.get(`content-length`));if(typeof a.body?.cancel==`function`)try{await a.body.cancel()}catch{}return o??s??i??c??(()=>{throw Error(`Failed to resolve stream content length`)})()}getExtractionHints(e){let t=qo(e.mimeType),n=Number.parseInt(e.audioSampleRate??``,10);return{codec:t,sampleRate:Number.isFinite(n)&&n>0?n:44100,channels:e.audioChannels&&e.audioChannels>0?e.audioChannels:2}}resolveFormatUrl(e,t){if(!e.url)throw Error(`Selected format does not contain a direct stream URL`);let n=new URL(e.url);return n.searchParams.get(`c`)===`WEB`&&n.searchParams.set(`cver`,t),n.searchParams.set(`cpn`,ms()),n.toString()}async fetchWatchContext(e,t){let n=`${Qo}/watch?v=${encodeURIComponent(e)}&hl=en`,r=await this.fetchFn(n,{headers:rs,...as(t)});if(!r.ok)throw r.status===403?new Xo(r.status):Error(`Failed to load watch page: ${r.status}`);let i=await r.text(),a=fs(i,[/"INNERTUBE_API_KEY":"([^"]+)"/,/["']INNERTUBE_API_KEY["']\s*:\s*"([^"]+)"/]),o=fs(i,[/"INNERTUBE_CLIENT_VERSION":"([^"]+)"/,/["']INNERTUBE_CLIENT_VERSION["']\s*:\s*"([^"]+)"/]),s=fs(i,[/"STS":(\d+)/,/["']STS["']\s*:\s*(\d+)/]),c=fs(i,[/"VISITOR_DATA":"([^"]+)"/,/"visitorData":"([^"]+)"/,/["'](?:VISITOR_DATA|visitorData)["']\s*:\s*"([^"]+)"/]);if(!a||!o)throw Error(`Failed to extract required player context from watch page`);let l=s?Number.parseInt(s,10):void 0,u={apiKey:a,clientVersion:o};return typeof l==`number`&&Number.isFinite(l)&&(u.signatureTimestamp=l),c&&(u.visitorData=us(c)),u}async fetchPlayerResponse(e,t,n,r){let i=os(n,t,e);t.visitorData&&(i.visitorData=t.visitorData);let a={context:{client:i},videoId:e,contentCheckOk:!0,racyCheckOk:!0};t.signatureTimestamp&&(a.playbackContext={contentPlaybackContext:{signatureTimestamp:t.signatureTimestamp}});let o=`${Qo}/youtubei/v1/player?key=${encodeURIComponent(t.apiKey)}`,s=await this.fetchFn(o,{method:`POST`,headers:{...rs,"content-type":`application/json`,...t.visitorData?{"x-goog-visitor-id":t.visitorData}:{}},body:JSON.stringify(a),...as(r)});if(!s.ok)throw Error(`Player API request failed with status ${s.status}`);let c=await s.json(),l=!!c.streamingData?.formats?.length,u=!!c.streamingData?.adaptiveFormats?.length;if(!l&&!u)throw Error(`Player response did not contain streaming formats`);return c}},ws=`bestefficiency`,Ts=3e4;function Es(e){if(e<=0)throw RangeError(`Audio downloader. ytAudio. chunkSize must be > 0`)}function Ds({signal:e,timeoutMs:t}){return async(n,r={})=>await z(n,{...r,signal:r.signal??e,forceGmXhr:!0,timeout:t})}function Os(e){return e instanceof Xo?!0:e instanceof Error&&/failed to load watch page:\s*403/i.test(e.message)}async function ks({videoId:e,signal:t},n={}){let r=n.chunkSize??f.minChunkSize;Es(r);let i=Ds({signal:t,timeoutMs:n.fetchTimeoutMs??Ts}),a=n.createDownloader?.(i)??new Cs({fetchImplementation:i});try{let n=await a.downloadAudioToChunkStream({videoId:e,videoQuality:ws,signal:t},{chunkSize:r});return{fileId:Ho(qt.WEB_API_STEAL_SIG_AND_N,n.itag,String(n.fileSize),r),mediaPartsLength:n.mediaPartsLength,getMediaBuffers:n.getMediaBuffers}}catch(e){if(Os(e))throw e;console.warn(`[VOT] ytAudio streaming mode failed, falling back to buffered mode`,e)}let o=(await a.downloadAudioToUint8Array({videoId:e,videoQuality:ws,signal:t})).bytes;if(!o||o.byteLength===0)throw Error(`Audio downloader. ytAudio. Empty audio`);let s=Math.max(1,Math.ceil(o.byteLength/r));return{fileId:Ho(qt.WEB_API_STEAL_SIG_AND_N,0,String(o.byteLength),r),mediaPartsLength:s,async*getMediaBuffers(){for(let e=0;e=Is&&(n+=1),n>=60)return t(`translationTakeMoreThanHour`);if(n<=1)return t(`translationTakeAboutMinute`);let r=String(n);return n!==11&&n%10==1?t(`translationTakeApproximatelyMinute2`).replace(`{0}`,r):![12,13,14].includes(n)&&[2,3,4].includes(n%10)?t(`translationTakeApproximatelyMinute`).replace(`{0}`,r):t(`translationTakeApproximatelyMinutes`).replace(`{0}`,r)}var U=class extends Error{name=`VOTLocalizedError`;unlocalizedMessage;localizedMessage;constructor(e){super(V.getDefault(e)),this.unlocalizedMessage=e,this.localizedMessage=V.get(e)}};function Rs(e){return e??null}async function zs(e,t){let n=await e.translateVideoImpl(t.videoData,t.requestLang,t.responseLang,Rs(t.translationHelp),!t.useAudioDownload,t.signal);return n?.url?{url:n.url,usedLivelyVoice:!!n.usedLivelyVoice}:null}function Bs(e){return{videoId:e.videoId,from:e.requestLang,to:e.responseLang,url:e.downloadTranslationUrl??e.fallbackUrl,useLivelyVoice:e.usedLivelyVoice}}async function Vs(e){return!(e.isActionStale(e.actionContext)||(await e.updateTranslation(e.url,e.actionContext),e.isActionStale(e.actionContext)))}async function Hs(e){let t=await zs(e.requester,{videoData:e.request.videoData,requestLang:e.request.requestLang,responseLang:e.request.responseLang,translationHelp:e.request.translationHelp,useAudioDownload:e.request.useAudioDownload,signal:e.request.signal});return!t||!await Vs({url:t.url,actionContext:e.actionContext,isActionStale:e.isActionStale,updateTranslation:e.updateTranslation})||e.isActionStale(e.actionContext)?null:t}function Us(e){e.setTranslation(e.cacheKey,Bs({videoId:e.videoId,requestLang:e.requestLang,responseLang:e.responseLang,fallbackUrl:e.fallbackUrl,downloadTranslationUrl:e.downloadTranslationUrl,usedLivelyVoice:e.usedLivelyVoice}))}function Ws(e){return e.aborted||!e.translateApiErrorsEnabled||!e.hadAsyncWait?e.hadAsyncWait:(e.notify({videoId:e.videoId,message:e.error}),!1)}function Gs(e){if(!e||typeof e!=`object`)return null;let t=e,n=t.data&&typeof t.data==`object`?t.data:void 0;return{name:t.name,message:t.message,data:n}}function Ks(e){let t=Gs(e)?.data?.message;return typeof t==`string`&&t.length>0?t:void 0}function qs(e){let t=Gs(e);if(!t||t.name!==`VOTJSError`)return e;let n=typeof t.message==`string`?t.message:``,r=typeof t.data?.message==`string`&&t.data.message.length>0;return n===`Yandex couldn't translate video`&&!r||n===`Failed to request video translation`?new U(`requestTranslationFailed`):n===`Audio link wasn't received`||n===`Audio link wasn't received from VOT response`?new U(`audioNotReceived`):e}var Js=class{videoHandler;audioDownloader;downloading;downloadWaiters=new Set;requestedFailAudio=new Set;constructor(e){this.videoHandler=e,this.audioDownloader=new Fs,this.downloading=!1,this.audioDownloader.addEventListener(`downloadedAudio`,this.onDownloadedAudio).addEventListener(`downloadedPartialAudio`,this.onDownloadedPartialAudio).addEventListener(`downloadAudioError`,this.onDownloadAudioError)}onDownloadedAudio=async(e,t)=>{if(L.log(`downloadedAudio`,t),!this.downloading){L.log(`skip downloadedAudio`);return}let{videoId:n,fileId:r,audioData:i}=t,a=this.getCanonicalUrl(n);try{await this.videoHandler.votClient.requestVtransAudio(a,e,{audioFile:i,fileId:r})}catch(e){L.error(`Failed to upload downloaded audio`,e),this.finishDownloadFailure(Error(`Audio downloader failed while uploading full audio`));return}this.finishDownloadSuccess()};onDownloadedPartialAudio=async(e,t)=>{if(L.log(`downloadedPartialAudio`,t),!this.downloading){L.log(`skip downloadedPartialAudio`);return}let{audioData:n,fileId:r,videoId:i,amount:a,version:o,index:s}=t,c=this.getCanonicalUrl(i);try{await this.videoHandler.votClient.requestVtransAudio(c,e,{audioFile:n,chunkId:s},{audioPartsLength:a,fileId:r,version:o})}catch(e){L.error(`Failed to upload downloaded audio chunk`,e),this.finishDownloadFailure(Error(`Audio downloader failed while uploading chunk`));return}s===a-1&&this.finishDownloadSuccess()};onDownloadAudioError=async e=>{if(!this.downloading){L.log(`skip downloadAudioError`);return}L.log(`Failed to download audio ${e}`);let t=this.getCanonicalUrl(e);if(!(this.videoHandler.site.host===`youtube`&&this.videoHandler.data?.useAudioDownload)){this.finishDownloadFailure(new U(`VOTFailedDownloadAudio`));return}try{this.requestedFailAudio.has(t)?L.log(`fail-audio-js request already sent for this video`):(L.log(`Sending fail-audio-js request`),await this.videoHandler.votClient.requestVtransFailAudio(t),this.requestedFailAudio.add(t)),this.finishDownloadSuccess()}catch(e){L.error(`fail-audio-js request failed`,e),this.finishDownloadFailure(new U(`VOTFailedDownloadAudio`))}};finishDownloadSuccess(){this.downloading=!1,this.resolveDownloadWaiters()}finishDownloadFailure(e){this.downloading=!1,this.rejectDownloadWaiters(e)}getCanonicalUrl(e){return`https://youtu.be/${e}`}isLivelyVoiceUnavailableError(e){let t=xa(e);return!!t&&t.toLowerCase().includes(`обычная озвучка`)}scheduleRetry(e,t,n){return new Promise((r,i)=>{let a=null,o=()=>{a!==null&&clearTimeout(a),n.removeEventListener(`abort`,s)},s=()=>{o(),i(Ca())};if(n.addEventListener(`abort`,s,{once:!0}),n.aborted){s();return}a=setTimeout(async()=>{if(n.aborted){s();return}o();try{r(await e())}catch(e){i(e)}},t),a!==null&&(this.videoHandler.autoRetry=a)})}getVideoTranslationRetryDelayMs(e,t){return e>0?25e3:t<=600?6e4:75e3}async translateVideoImpl(e,t,n,r=null,i=!1,a=wa,o={}){let{disableLivelyVoice:s=!1,retryAttempt:c=0}=o;clearTimeout(this.videoHandler.autoRetry),this.finishDownloadSuccess();let l=this.videoHandler.getRequestLangForTranslation(t,n);L.log(e,`Translate video (requestLang: ${t}, requestLangForApi: ${l}, responseLang: ${n})`);let u=s;try{Ta(a);let o=this.videoHandler.isLivelyVoiceAllowed(l,n),s=await this.requestTranslationWithLivelyFallback({videoData:e,requestLangForApi:l,responseLang:n,translationHelp:r,shouldSendFailedAudio:i,livelyDisabled:u,livelyVoiceAllowed:o});u=s.livelyDisabled;let d=s.useLivelyVoice,f=s.response;if(!f)throw Error(`Failed to get translation response`);if(L.log(`Translate video result`,f),Ta(a),f.translated&&f.remainingTime<1)return L.log(`Video translation finished with this data: `,f),{...f,usedLivelyVoice:d};let p=f.message??V.get(`translationTakeFewMinutes`);if(await this.videoHandler.updateTranslationErrorMsg(f.remainingTime>0?Ls(f.remainingTime,e=>V.get(e)):p,a),f.status===Kt.AUDIO_REQUESTED&&this.videoHandler.isYouTubeHosts())return this.videoHandler.hadAsyncWait=!0,L.log(`Start audio download`),this.downloading=!0,await this.audioDownloader.runAudioDownload(e.videoId,f.translationId,a),L.log(`waiting downloading finish`),await this.waitForAudioDownloadCompletion(a,15e3),await this.translateVideoImpl(e,t,n,r,!0,a,{disableLivelyVoice:u,retryAttempt:c})}catch(t){if(Sa(t))return L.log(`aborted video translation`),null;let n=qs(t);return await this.videoHandler.updateTranslationErrorMsg(Ks(n)??n,a),this.videoHandler.hadAsyncWait=Ws({aborted:!!this.videoHandler.actionsAbortController?.signal?.aborted,translateApiErrorsEnabled:!!this.videoHandler.data?.translateAPIErrors,hadAsyncWait:this.videoHandler.hadAsyncWait,videoId:e.videoId,error:t,notify:e=>this.videoHandler.notifier.translationFailed(e)}),console.error(`[VOT]`,t),null}return this.videoHandler.hadAsyncWait=!0,this.scheduleRetry(()=>this.translateVideoImpl(e,t,n,r,i,a,{disableLivelyVoice:u,retryAttempt:c+1}),this.getVideoTranslationRetryDelayMs(c,e.duration),a)}async requestTranslationWithLivelyFallback({videoData:e,requestLangForApi:t,responseLang:n,translationHelp:r,shouldSendFailedAudio:i,livelyDisabled:a,livelyVoiceAllowed:o}){let s=!a&&o&&!!this.videoHandler.data?.useLivelyVoice;for(;;){try{let o=await this.videoHandler.votClient.translateVideo({videoData:e,requestLang:t,responseLang:n,translationHelp:r,extraOpts:{useLivelyVoice:s,videoTitle:this.videoHandler.videoData?.title},shouldSendFailedAudio:i});if(!s||!this.isLivelyVoiceUnavailableError(o))return{response:o,useLivelyVoice:s,livelyDisabled:a};L.log(`[translateVideoImpl] Server responded that lively voices are unavailable. Falling back to standard translation.`,o)}catch(e){if(!s||!this.isLivelyVoiceUnavailableError(e))throw e;L.log(`[translateVideoImpl] Lively voices are unavailable. Falling back to standard translation.`,e)}a=!0,s=!1}}waitForAudioDownloadCompletion(e,t){return this.downloading?new Promise((n,r)=>{let i,a=()=>{s(),r(Ca())},o=setTimeout(()=>{s(),n()},t),s=()=>{clearTimeout(o),e.removeEventListener(`abort`,a),this.downloadWaiters.delete(i)};i={resolve:()=>{s(),n()},reject:e=>{s(),r(e)}},this.downloadWaiters.add(i),e.addEventListener(`abort`,a,{once:!0}),e.aborted&&a()}):Promise.resolve()}resolveDownloadWaiters(){this.forEachDownloadWaiter(e=>e.resolve())}rejectDownloadWaiters(e){this.forEachDownloadWaiter(t=>t.reject(e))}forEachDownloadWaiter(e){if(!this.downloadWaiters.size)return;let t=Array.from(this.downloadWaiters);this.downloadWaiters.clear();for(let n of t)e(n)}},Ys=class{state={status:`idle`};deps;constructor(e){this.deps=e}get currentState(){return this.state}setState(e){this.state=e,L.log(`[TranslationOrchestrator] state`,e)}reset(){this.setState({status:`idle`})}async runAutoTranslationIfEligible(){if(this.state.status===`idle`&&this.deps.isFirstPlay()&&this.deps.isAutoTranslateEnabled()&&this.deps.getVideoId()){if(this.deps.isMobileYouTubeMuted?.()){L.log(`[TranslationOrchestrator] Mobile YouTube video is muted, deferring auto-translate`),this.setState({status:`deferred`,reason:`muted`}),this.deps.setMuteWatcher?.(()=>{L.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(!1),this.reset()}catch(e){throw this.setState({status:`error`,message:e}),e}}}};function Xs(e,t={}){let{requireVideoData:n=!1,clearVideoData:r=!1}=t;n&&!e.videoData||(r&&(e.videoData=void 0),e.stopTranslation(),e.resetSubtitlesWidget())}function Zs(e,t={}){let{hideMenu:n=!1}=t;e?.votButton?.container&&(e.votButton.container.hidden=!0),n&&e?.votMenu&&(e.votMenu.hidden=!0)}function Qs(e,t,n={}){let{requireVideoData:r,clearVideoData:i,hideMenu:a}=n;Xs(e,{requireVideoData:r,clearVideoData:i}),Zs(t,{hideMenu:a})}var $s=class{host;lifecycleGeneration=0;lastSetCanPlaySourceKey=``;activeSetCanPlaySourceKey=``;setCanPlayRequested=!1;setCanPlayLoopPromise;constructor(e){this.host=e}isStale(e){return e!==this.lifecycleGeneration}resetActions(e){if(typeof this.host.resetActionsAbortController==`function`){this.host.resetActionsAbortController(e);return}this.host.actionsAbortController?.abort(e)}invalidateActiveSession(e){this.lifecycleGeneration!==0&&(this.lifecycleGeneration+=1,this.resetActions(`[VideoLifecycle] ${e}`),L.log(`[VideoLifecycle] cancelled active session (active: ${this.lifecycleGeneration})`,{reason:e}))}startSession(e){this.lifecycleGeneration+=1;let t=this.lifecycleGeneration;return this.resetActions(`[VideoLifecycle][session:${t}] ${e}`),L.log(`[VideoLifecycle][session:${t}] started`,{reason:e}),t}shouldAbortHandleSrcChanged(e,t){return this.isStale(e)?(L.log(`[VideoLifecycle][session:${e}] handleSrcChanged aborted at ${t} (active: ${this.lifecycleGeneration})`),!0):!1}showOverlayButton(e){e.votButton.container.hidden=!1,e.votButton.opacity=1,this.host.queueOverlayAutoHide?.()}teardown(){this.setCanPlayRequested=!1,this.invalidateActiveSession(`teardown`)}getCurrentSourceKey(){let e=this.host.video.srcObject?`1`:`0`;if(this.host.site.host===`youtube`){let t=globalThis.location.pathname;return`${`${globalThis.location.origin}${t}${globalThis.location.search}`}||${e}`}let t=this.host.video.currentSrc||this.host.video.src||``;return`${globalThis.location.href}||${t}||${e}`}resolveContainer(){let{site:e,video:t,container:n}=this.host;return e.selector?zo(t,e.selector)||(n.isConnected&&Mo(n,t)?n:t.parentElement??n):t.parentElement??n}async setCanPlay(){if(this.setCanPlayRequested=!0,this.setCanPlayLoopPromise!==void 0){let e=this.getCurrentSourceKey();return this.activeSetCanPlaySourceKey&&e!==this.activeSetCanPlaySourceKey?this.invalidateActiveSession(`setCanPlay source changed while previous trigger is running`):L.log(`[VideoLifecycle] setCanPlay deduplicated for same source`,{sourceKey:e}),await this.setCanPlayLoopPromise}let e=(async()=>{for(;this.setCanPlayRequested;)this.setCanPlayRequested=!1,await this.runSetCanPlayOnce()})();this.setCanPlayLoopPromise=e;try{await e}finally{this.setCanPlayLoopPromise===e&&(this.setCanPlayLoopPromise=void 0)}}async runSetCanPlayOnce(){let e=this.getCurrentSourceKey();if(this.host.videoData?.videoId&&e===this.lastSetCanPlaySourceKey){L.log(`[VideoLifecycle] setCanPlay deduplicated for same source`,{sourceKey:e});return}let t;try{t=await this.host.getVideoData()}catch(t){L.log(`[VideoLifecycle] getVideoData failed for source ${e}`,t),this.host.videoData=void 0,Zs(this.host.uiManager.votOverlayView,{hideMenu:!0});return}if(this.getCurrentSourceKey()!==e){L.log(`[VideoLifecycle] discarded stale getVideoData result after source change`,{sourceKey:e});return}this.host.videoData=t,this.activeSetCanPlaySourceKey=e;let n=this.startSession(`setCanPlay (source: ${e})`);L.log(`[VideoLifecycle][session:${n}] setCanPlay started`,{sourceKey:e});try{if(await this.handleSrcChanged(n,e),this.isStale(n)){L.log(`[VideoLifecycle][session:${n}] setCanPlay aborted after src change (active: ${this.lifecycleGeneration})`);return}let t=this.runAutoSubtitlesIfEnabled(n);if(await this.host.translationOrchestrator.runAutoTranslationIfEligible(),this.isStale(n)){L.log(`[VideoLifecycle][session:${n}] auto-translation result ignored (stale session)`);return}if(await t,this.isStale(n)){L.log(`[VideoLifecycle][session:${n}] auto-subtitles result ignored (stale session)`);return}L.log(`[VideoLifecycle][session:${n}] setCanPlay finished`)}finally{this.activeSetCanPlaySourceKey===e&&(this.activeSetCanPlaySourceKey=``)}}async runAutoSubtitlesIfEnabled(e){if(!(!this.host.data.autoSubtitles||!this.host.videoData?.videoId))try{await this.host.enableSubtitlesForCurrentLangPair()}catch(t){L.log(`[VideoLifecycle][session:${e}] auto-subtitles failed`,t)}}async handleSrcChanged(e,t){let n=typeof e==`number`?e:this.startSession(`manual handleSrcChanged`),r=typeof t==`string`&&t.length>0?t:this.getCurrentSourceKey();if(this.shouldAbortHandleSrcChanged(n,`before start`))return;L.log(`[VideoLifecycle][session:${n}] src changed`,{sourceKey:r}),this.host.translationOrchestrator.reset(),this.host.firstPlay=!0;let i=this.host.uiManager.votOverlayView;Qs(this.host,i,{requireVideoData:!0}),!this.host.video.src&&!this.host.video.currentSrc&&!this.host.video.srcObject&&Zs(i,{hideMenu:!0});let a=this.resolveContainer();if(a!==this.host.container&&(this.host.container=a),this.shouldAbortHandleSrcChanged(n,`before getVideoData`)||(this.showOverlayButton(i),this.shouldAbortHandleSrcChanged(n,`after getVideoData`)))return;if(!this.host.videoData?.videoId){L.log(`[VideoLifecycle][session:${n}] No videoId resolved, hiding overlay`),Zs(i,{hideMenu:!0});return}let o=this.host.getPreferredSubtitlesLanguage(this.host.videoData.detectedLanguage,this.host.videoData.responseLanguage);if(o){let e=this.host.getSubtitlesCacheKey(this.host.videoData.videoId,this.host.videoData.detectedLanguage,o),t=this.host.cacheManager.getSubtitles(e);this.host.subtitles=t??[],this.host.subtitlesCacheKey=t===void 0?null:e}else this.host.subtitles=[],this.host.subtitlesCacheKey=null;await this.host.updateSubtitlesLangSelect(),!this.shouldAbortHandleSrcChanged(n,`after subtitles update`)&&(this.host.translateToLang=this.host.data.responseLanguage??`ru`,this.host.setSelectMenuValues(this.host.videoData.detectedLanguage,this.host.videoData.responseLanguage),this.showOverlayButton(i),this.lastSetCanPlaySourceKey=r,L.log(`[VideoLifecycle][session:${n}] src handling finished`))}};function ec(e,t){let n=()=>e;return{get video(){return n().video},get site(){return n().site},get container(){return n().container},set container(e){n().container!==e&&(n().container=e,n().uiManager.updateMount(t(e)))},get firstPlay(){return n().firstPlay},set firstPlay(e){n().firstPlay=e},stopTranslation:()=>e.stopTranslation(),get uiManager(){return n().uiManager},getVideoData:()=>e.getVideoData(),cacheManager:{getSubtitles:e=>n().cacheManager.getSubtitles(e)},getSubtitlesCacheKey:(t,n,r)=>e.getSubtitlesCacheKey(t,n,r),getPreferredSubtitlesLanguage:(t,n)=>e.getPreferredSubtitlesLanguage(t,n),updateSubtitlesLangSelect:()=>e.updateSubtitlesLangSelect(),enableSubtitlesForCurrentLangPair:()=>e.enableSubtitlesForCurrentLangPair(),setSelectMenuValues:(t,n)=>e.setSelectMenuValues(t,n),get translateToLang(){return n().translateToLang},set translateToLang(e){n().translateToLang=e},get data(){return n().data??{}},get subtitles(){return n().subtitles},set subtitles(e){n().subtitles=e},get subtitlesCacheKey(){return n().subtitlesCacheKey},set subtitlesCacheKey(e){n().subtitlesCacheKey=e},get videoData(){return n().videoData},set videoData(e){n().videoData=e},get actionsAbortController(){return n().actionsAbortController},set actionsAbortController(e){n().actionsAbortController=e},resetActionsAbortController:t=>e.resetActionsAbortController(t),translationOrchestrator:e.translationOrchestrator,resetSubtitlesWidget:()=>e.resetSubtitlesWidget(),queueOverlayAutoHide:()=>e.overlayVisibility?.queueAutoHide()}}var tc=450,nc=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`),rc=/[\p{N}\p{P}\p{S}]+/gu,ic=/\s+/g,ac=/\p{L}/u;function oc(e,t){return e.length<=t?e:e.slice(0,t).trimEnd()}function sc(e,t){let n=`${e??``} ${t??``}`.trim();if(!n)return``;let r=n.normalize(`NFKC`).replace(nc,` `).replace(rc,` `).replace(ic,` `).trim();return ac.test(r)?oc(r,tc):``}var cc=5e3,lc=2**53-1,uc=null,dc=0,fc=null,pc=0;async function mc(){let e=Date.now();if(uc&&e-dci){let t=jc(e,`up`,r);return Math.min(a,t)}return jc(e,`nearest`,r)}var Nc=new Set([`youtube`,`googledrive`]),Pc=Nc,Fc=new Set([`rutube`,`ok`]),Ic=new Set([`youtube`,`invidious`,`piped`]);function Lc(e){return Nc.has(e)}function Rc(e){return Pc.has(e)}function zc(e){return Fc.has(e)}function Bc(e){return Rc(e.host)&&e.additionalData!==`mobile`}function Vc(e){return Ic.has(e)}var Hc={rutube:`ru`,"ok.ru":`ru`,mail_ru:`ru`,weverse:`ko`,niconico:`ja`,youku:`zh`,bilibili:`zh`,weibo:`zh`,zdf:`de`},Uc=`.ytp-volume-panel [aria-valuenow]`,Wc=35,Gc=500,Kc=new Set(bn),qc=new Map;function Jc(e){let t=qc.get(e);if(t)return t;let n={};for(qc.set(e,n);qc.size>Gc;){let e=qc.keys().next().value;if(typeof e!=`string`)break;qc.delete(e)}return n}function Yc(e){if(typeof e!=`string`)return;let t=e.toLowerCase().split(/[-_]/)[0];return Kc.has(t)?t:void 0}function Xc(e){return!!(e&&e!==`auto`)}function Zc(e,t){return sc(typeof e==`string`?e:``,typeof t==`string`?t:void 0)}function Qc(e){let t=Hc[e];if(t)return t;if(e===`vk`){let e=document.getElementsByTagName(`track`)?.[0]?.srclang;return Yc(e)}}function $c(e){if(!Array.isArray(e)||e.length===0)return;let t=t=>{for(let n of e){if(!n||typeof n!=`object`)continue;let e=n;if(e.source!==`youtube`||typeof e.translatedFromLanguage==`string`||t&&e.isAutoGenerated===!0)continue;let r=Yc(e.language);if(Xc(r))return r}};return t(!0)??t(!1)}async function el(e){if(e.isStream)return{detectedLanguage:`auto`};if(e.userOverrideLanguage)return{detectedLanguage:e.userOverrideLanguage};let t=Qc(e.host);if(Xc(t))return{detectedLanguage:t,cacheLanguage:t};let n=Yc(e.possibleLanguage);if(Xc(n))return{detectedLanguage:n,cacheLanguage:n};let r=e.host===`youtube`?$c(e.subtitles):void 0;if(Xc(r))return{detectedLanguage:r,cacheLanguage:r};if(e.cachedDetectedLanguage)return{detectedLanguage:e.cachedDetectedLanguage};if(!e.allowTextLanguageDetection)return{detectedLanguage:`auto`};let i=Zc(e.title,e.description);if(!i||i.length0?i/a*100:i):null}var nl=class{videoHandler;constructor(e){this.videoHandler=e}setDetectedLanguageCache(e,t){Jc(e).detectedLanguage=t}rememberUserLanguageSelection(e,t){let n=Yc(t);if(!Xc(n)){let t=qc.get(e);t&&delete t.userLanguageOverride;return}let r=Jc(e);r.userLanguageOverride=n,r.detectedLanguage=n}rememberDetectedLanguage(e,t){let n=Yc(t);Xc(n)&&(this.setDetectedLanguageCache(e,n),this.videoHandler.videoData?.videoId===e&&(this.videoHandler.videoData.detectedLanguage=n))}async detectLanguageSingleFlight(e,t){let n=Jc(e),r=n.detectInFlight;if(r!==void 0)return r;let i=(async()=>{L.log(`Detecting language text: ${t}`);let e=Yc(await bc(t));return Xc(e)?e:void 0})();n.detectInFlight=i;try{return await i}finally{n.detectInFlight===i&&delete n.detectInFlight}}async ensureDetectedLanguageForTranslation(e){if(!e?.videoId||e.detectedLanguage!==`auto`)return;let t=Jc(e.videoId),{detectedLanguage:n,cacheLanguage:r}=await el({isStream:e.isStream,host:this.videoHandler.site.host,possibleLanguage:e.detectedLanguage,subtitles:e.subtitles,userOverrideLanguage:t.userLanguageOverride,cachedDetectedLanguage:t.detectedLanguage,title:e.title,description:e.description,allowTextLanguageDetection:!0,detectLanguage:async t=>await this.detectLanguageSingleFlight(e.videoId,t)});r&&this.setDetectedLanguageCache(e.videoId,r),!(!n||n===`auto`)&&this.videoHandler.setSelectMenuValues(n,this.videoHandler.translateToLang)}async getVideoData(){let{duration:e,url:t,videoId:n,host:r,title:i,translationHelp:a=null,localizedTitle:o,description:s,detectedLanguage:c,subtitles:l,isStream:u=!1}=await Yr(this.videoHandler.site,{fetchFn:z,video:this.videoHandler.video,language:V.lang}),d=Jc(n),{detectedLanguage:p,cacheLanguage:m}=await el({isStream:u,host:this.videoHandler.site.host,possibleLanguage:c,subtitles:l,userOverrideLanguage:d.userLanguageOverride,cachedDetectedLanguage:d.detectedLanguage,title:i,description:s,allowTextLanguageDetection:!1,detectLanguage:async e=>await this.detectLanguageSingleFlight(n,e)});m&&this.setDetectedLanguageCache(n,m);let h={translationHelp:a,isStream:u,duration:e||this.videoHandler.video?.duration||f.defaultDuration,videoId:n,url:t,host:r,detectedLanguage:p,responseLanguage:this.videoHandler.translateToLang,subtitles:l,title:i,localizedTitle:o,description:s,downloadTitle:o??i??document.title??n};return d.lastLoggedDetectedLanguage!==p&&(console.log(`[VOT] Detected language:`,p),d.lastLoggedDetectedLanguage=p),h}async videoValidator(){let e=this.videoHandler.videoData,t=this.videoHandler.data;if(!e||!t)throw new U(`VOTNoVideoIDFound`);if(L.log(`VideoValidator videoData: `,this.videoHandler.videoData),this.videoHandler.data.enabledDontTranslateLanguages&&this.videoHandler.data.dontTranslateLanguages?.includes(this.videoHandler.videoData.detectedLanguage))throw new U(`VOTDisableFromYourLang`);if(this.videoHandler.videoData.isStream)throw new U(`VOTStreamNotAvailable`);if(this.videoHandler.videoData.duration>14400)throw new U(`VOTVideoIsTooLong`);return!0}getVideoVolume(){let e=this.videoHandler.video;if(e){if(Lc(this.videoHandler.site.host)){let e=tl(Uc);if(e!=null)return kc(e);let t=F.getVolume();if(typeof t==`number`&&Number.isFinite(t))return jc(t)}return jc(e.volume)}}setVideoVolume(e){let t=jc(e),n=t>0;if(!Lc(this.videoHandler.site.host))return n&&(this.videoHandler.video.muted=!1),this.videoHandler.video.volume=t,this;try{let e=F.getPlayer(),r=F.setVolume(t);if(n&&(e?.unMute?.(),this.videoHandler.video.muted=!1),typeof r==`boolean`&&r||typeof r==`number`&&Number.isFinite(r))return this}catch{}return n&&(this.videoHandler.video.muted=!1),this.videoHandler.video.volume=t,this}isMuted(){return Lc(this.videoHandler.site.host)?F.isMuted():this.videoHandler.video?.muted}syncVideoVolumeSlider(){let e=this.videoHandler.uiManager.votOverlayView;if(!e?.isInitialized())return this;let t=Lc(this.videoHandler.site.host)?tl(Uc):null,n=this.isMuted()?0:t??Oc(this.getVideoVolume()??0);return e.videoVolumeSlider.value=n,this.videoHandler.onVideoVolumeSliderSynced?.(n),this}setSelectMenuValues(e,t){let n=this.videoHandler.videoData;if(!n)return this;let r=Yc(e)??`auto`,i=`${r}->${t}`,a=Jc(n.videoId);a.lastLoggedLangPair!==i&&(console.log(`[VOT] Set translation from ${r} to ${t}`),a.lastLoggedLangPair=i),n.detectedLanguage=r,n.responseLanguage=t,this.videoHandler.translateFromLang=r,this.videoHandler.translateToLang=t;let o=this.videoHandler.uiManager.votOverlayView;return o?.isInitialized()?(o.languagePairSelect.fromSelect.selectTitle=V.getLangLabel(r),o.languagePairSelect.toSelect.selectTitle=V.getLangLabel(t),o.languagePairSelect.fromSelect.setSelectedValue(r),o.languagePairSelect.toSelect.setSelectedValue(t),this):this}},rl=globalThis,il=e=>e,al=rl.trustedTypes,ol=al?al.createPolicy(`lit-html`,{createHTML:e=>e}):void 0,sl=`$lit$`,cl=`lit$${Math.random().toFixed(9).slice(2)}$`,ll=`?`+cl,ul=`<${ll}>`,dl=document,fl=()=>dl.createComment(``),pl=e=>e===null||typeof e!=`object`&&typeof e!=`function`,ml=Array.isArray,hl=e=>ml(e)||typeof e?.[Symbol.iterator]==`function`,gl=`[ +\f\r]`,_l=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,vl=/-->/g,yl=/>/g,bl=RegExp(`>|${gl}(?:([^\\s"'>=/]+)(${gl}*=${gl}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,`g`),xl=/'/g,Sl=/"/g,Cl=/^(?:script|style|textarea|title)$/i,wl=e=>(t,...n)=>({_$litType$:e,strings:t,values:n}),Tl=wl(1),G=wl(2),El=Symbol.for(`lit-noChange`),K=Symbol.for(`lit-nothing`),Dl=new WeakMap,Ol=dl.createTreeWalker(dl,129);function kl(e,t){if(!ml(e)||!e.hasOwnProperty(`raw`))throw Error(`invalid template strings array`);return ol===void 0?t:ol.createHTML(t)}var Al=(e,t)=>{let n=e.length-1,r=[],i,a=t===2?``:t===3?``:``,o=_l;for(let t=0;t`?(o=i??_l,l=-1):c[1]===void 0?l=-2:(l=o.lastIndex-c[2].length,s=c[1],o=c[3]===void 0?bl:c[3]===`"`?Sl:xl):o===Sl||o===xl?o=bl:o===vl||o===yl?o=_l:(o=bl,i=void 0);let d=o===bl&&e[t+1].startsWith(`/>`)?` `:``;a+=o===_l?n+ul:l>=0?(r.push(s),n.slice(0,l)+sl+n.slice(l)+cl+d):n+cl+(l===-2?t:d)}return[kl(e,a+(e[n]||``)+(t===2?``:t===3?``:``)),r]},jl=class e{constructor({strings:t,_$litType$:n},r){let i;this.parts=[];let a=0,o=0,s=t.length-1,c=this.parts,[l,u]=Al(t,n);if(this.el=e.createElement(l,r),Ol.currentNode=this.el.content,n===2||n===3){let e=this.el.content.firstChild;e.replaceWith(...e.childNodes)}for(;(i=Ol.nextNode())!==null&&c.length0){i.textContent=al?al.emptyScript:``;for(let n=0;n2||n[0]!==``||n[1]!==``?(this._$AH=Array(n.length-1).fill(new String),this.strings=n):this._$AH=K}_$AI(e,t=this,n,r){let i=this.strings,a=!1;if(i===void 0)e=Ml(this,e,t,0),a=!pl(e)||e!==this._$AH&&e!==El,a&&(this._$AH=e);else{let r=e,o,s;for(e=i[0],o=0;o{let r=n?.renderBefore??t,i=r._$litPart$;if(i===void 0){let e=n?.renderBefore??null;r._$litPart$=i=new Pl(t.insertBefore(fl(),e),e,void 0,n??{})}return i._$AI(e),i},Vl=`.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:"";inherits:true;initial-value:.8}@property --vot-subtitles-scale-compensation{syntax:"";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}`;function Hl(e){let t=globalThis.GM_addStyle;if(typeof t==`function`)return t(e);let n=document.createElement(`style`);return n.textContent=e,(document.head||document.documentElement).appendChild(n),n}Hl(Vl);function Ul(){if(globalThis.__votKeyboardNavInitialized)return;globalThis.__votKeyboardNavInitialized=!0;let e=document.documentElement,t=`vot-keyboard-nav`,n=()=>e.classList.add(t),r=()=>e.classList.remove(t);globalThis.addEventListener(`keydown`,e=>{e.key===`Tab`&&n()},!0);for(let e of[`pointerdown`,`mousedown`,`touchstart`])globalThis.addEventListener(e,r,{capture:!0,passive:!0})}Ul();var J={makeButtonLike(e,{ariaLabel:t}={}){e.setAttribute(`role`,`button`),e.hasAttribute(`tabindex`)||(e.tabIndex=0);let n=e.tabIndex,r=()=>{e.getAttribute(`disabled`)===`true`?(e.setAttribute(`aria-disabled`,`true`),e.tabIndex=-1):(e.removeAttribute(`aria-disabled`),e.tabIndex=n)};return r(),new MutationObserver(()=>r()).observe(e,{attributes:!0,attributeFilter:[`disabled`]}),t&&e.setAttribute(`aria-label`,t),e.addEventListener(`keydown`,t=>{e.getAttribute(`disabled`)===`true`||e.getAttribute(`aria-disabled`)===`true`||(t.key===`Enter`||t.key===` `)&&(t.preventDefault(),e.click())}),e},createEl(e,t=[],n=null){let r=document.createElement(e);return t.length&&r.classList.add(...t),n!==null&&r.append(n),r},createHeader(e,t=4){return J.createEl(`vot-block`,[`vot-header`,`vot-header-level-${t}`],e)},createInformation(e,t){let n=J.createEl(`vot-block`,[`vot-info`]),r=J.createEl(`vot-block`);q(e,r);let i=J.createEl(`vot-block`);return q(t,i),n.append(r,i),{container:n,header:r,value:i}},createButton(e){let t=J.createEl(`vot-block`,[`vot-button`],e);return J.makeButtonLike(t)},createTextButton(e){let t=J.createEl(`vot-block`,[`vot-text-button`],e);return J.makeButtonLike(t)},createOutlinedButton(e){let t=J.createEl(`vot-block`,[`vot-outlined-button`],e);return J.makeButtonLike(t)},createIconButton(e,t={}){let n=J.createEl(`vot-block`,[`vot-icon-button`]);return q(e,n),J.makeButtonLike(n,t)},createInlineLoader(){return J.createEl(`vot-block`,[`vot-inline-loader`])},createPortal(e=!1){return J.createEl(`vot-block`,[`vot-portal${e?`-local`:``}`])},createSubtitleInfo(e,t,n){let r=J.createEl(`vot-block`,[`vot-subtitles-info`]);r.id=`vot-subtitles-info`;let i=J.createEl(`vot-block`,[`vot-subtitles-info-service`],V.get(`VOTTranslatedBy`).replace(`{0}`,n)),a=J.createEl(`vot-block`,[`vot-subtitles-info-header`],e),o=J.createEl(`vot-block`,[`vot-subtitles-info-context`],t);return r.append(i,a,o),{container:r,translatedWith:i,header:a,context:o}}},Wl=[`left`,`top`,`right`,`bottom`],Gl=[`hover`,`click`],Y=class e{showed=!1;target;anchor;content;position;preferredPosition;trigger;parentElement;layoutRoot;offsetX;offsetY;_hidden;autoLayout;pageWidth;pageHeight;globalOffsetX;globalOffsetY;maxWidth;backgroundColor;borderRadius;_bordered;container;onResizeObserver;intersectionObserver;scrollListening=!1;positionRafId=null;destroyFallbackTimerId;static DESTROY_FALLBACK_MS=700;tooltipId=typeof crypto<`u`&&`randomUUID`in crypto?crypto.randomUUID():`vot-tooltip-${Math.random().toString(36).slice(2)}`;prevAriaDescribedBy=null;constructor({target:t,anchor:n=void 0,content:r=``,position:i=`top`,trigger:a=`hover`,offset:o=4,maxWidth:s=void 0,hidden:c=!1,autoLayout:l=!0,backgroundColor:u=void 0,borderRadius:d=void 0,bordered:f=!0,parentElement:p=document.body,layoutRoot:m=document.documentElement}){if(!(t instanceof HTMLElement))throw TypeError(`target must be a valid HTMLElement`);this.target=t,this.anchor=n instanceof HTMLElement?n:t,this.content=r,typeof o==`number`?this.offsetY=this.offsetX=o:(this.offsetX=o.x,this.offsetY=o.y),this._hidden=c,this.autoLayout=l,this.trigger=e.validateTrigger(a)?a:`hover`,this.position=e.validatePos(i)?i:`top`,this.preferredPosition=this.position,this.parentElement=p,this.layoutRoot=m,this.borderRadius=d,this._bordered=f,this.maxWidth=s,this.backgroundColor=u,this.updatePageSize(),this.init()}static validatePos(e){return Wl.includes(e)}static validateTrigger(e){return Gl.includes(e)}setPosition(t){return this.preferredPosition=e.validatePos(t)?t:`top`,this.position=this.preferredPosition,this.schedulePositionUpdate(),this}setContent(e){return this.content=e,this.container?(this.container.replaceChildren(),typeof e==`string`?this.container.textContent=e:this.container.append(e),this.schedulePositionUpdate(),this):this}updateMount({parentElement:e,layoutRoot:t}){return e&&this.parentElement!==e&&(this.parentElement=e,this.container?.isConnected&&e.appendChild(this.container)),t&&this.layoutRoot!==t&&(this.layoutRoot=t),this.schedulePositionUpdate(),this}onResize=()=>{this.schedulePositionUpdate()};onClick=()=>{this.showed?this.destroy():this.create()};onTargetKeyDown=e=>{e.key!==`Escape`||!this.showed||this.destroy()};onScroll=()=>{this.schedulePositionUpdate()};onHoverPointerDown=e=>{e.pointerType!==`mouse`&&this.create()};onHoverPointerUp=e=>{e.pointerType!==`mouse`&&this.destroy()};onMouseEnter=()=>{this.create()};onMouseLeave=e=>{this.isInTooltipContext(e.relatedTarget)||this.destroy()};onTooltipMouseLeave=e=>{this.isInTooltipContext(e.relatedTarget)||this.destroy()};isInTooltipContext(e){return e instanceof Node?this.target.contains(e)||this.container?.contains(e):!1}updatePageSize(){if(this.layoutRoot===document.documentElement)this.globalOffsetX=0,this.globalOffsetY=0;else{let{left:e,top:t}=this.layoutRoot.getBoundingClientRect();this.globalOffsetX=e,this.globalOffsetY=t}return this.pageWidth=this.layoutRoot.clientWidth||document.documentElement.clientWidth,this.pageHeight=this.layoutRoot.clientHeight||document.documentElement.clientHeight,this}onIntersect=([e])=>{if(!e.isIntersecting)return this.destroy(!0)};init(){return this.onResizeObserver=new ResizeObserver(this.onResize),this.intersectionObserver=new IntersectionObserver(this.onIntersect),this.target.addEventListener(`keydown`,this.onTargetKeyDown),this.trigger===`click`?(this.target.addEventListener(`pointerdown`,this.onClick),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),this)}release(){return this.destroy(!0),this.detachScrollListener(),this.target.removeEventListener(`keydown`,this.onTargetKeyDown),this.trigger===`click`?(this.target.removeEventListener(`pointerdown`,this.onClick),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),this)}schedulePositionUpdate(){this.container&&this.positionRafId===null&&(this.positionRafId=requestAnimationFrame(()=>{this.positionRafId=null,this.updatePageSize(),this.updatePos()}))}cancelPositionUpdate(){this.positionRafId!==null&&(cancelAnimationFrame(this.positionRafId),this.positionRafId=null)}clearDestroyFallbackTimer(){this.destroyFallbackTimerId!==void 0&&(globalThis.clearTimeout(this.destroyFallbackTimerId),this.destroyFallbackTimerId=void 0)}create(){return this.destroy(!0),this.showed=!0,this.container=J.createEl(`vot-block`,[`vot-tooltip`],this.content),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(),this.backgroundColor!==void 0&&(this.container.style.backgroundColor=this.backgroundColor),this.borderRadius!==void 0&&(this.container.style.borderRadius=`${this.borderRadius}px`),this.hidden?this.container.hidden=!0:this.syncAriaDescribedBy(!0),this.container.style.opacity=`1`,this.trigger===`hover`&&this.container.addEventListener(`mouseleave`,this.onTooltipMouseLeave),this.attachScrollListener(),this.onResizeObserver?.observe(this.layoutRoot),this.anchor!==this.layoutRoot&&this.onResizeObserver?.observe(this.anchor),this.intersectionObserver?.observe(this.target),this}updatePos(){if(!this.container)return this;let{top:e,left:t}=this.calcPos(this.autoLayout,this.preferredPosition),n=Math.max(0,this.pageWidth-this.offsetX*2),r=R(this.maxWidth??n,0,n);return this.container.style.transform=`translate(${t}px, ${e}px)`,this.container.dataset.position=this.position,this.container.style.maxWidth=`${r}px`,this}calcPos(e=!0,t=this.preferredPosition){if(!this.container)return{top:0,left:0};let{left:n,right:r,top:i,bottom:a,width:o,height:s}=this.anchor.getBoundingClientRect(),{width:c,height:l}=this.container.getBoundingClientRect(),u=R(c,0,this.pageWidth),d=R(l,0,this.pageHeight),f={left:n-this.globalOffsetX,right:r-this.globalOffsetX,top:i-this.globalOffsetY,bottom:a-this.globalOffsetY,anchorWidth:o,anchorHeight:s},p={width:u,height:d},m=this.resolveTooltipPosition(f,p,t,e),h=this.getTooltipCoordinates(f,p,m);return this.position=m,{top:h.top+this.globalOffsetY,left:h.left+this.globalOffsetX}}resolveTooltipPosition(e,t,n,r){if(!r)return n;switch(n){case`top`:return R(e.top-t.height-this.offsetY,0,this.pageHeight)+this.offsetYthis.pageWidth-this.offsetX?`left`:`right`;case`bottom`:return R(e.bottom+this.offsetY,0,this.pageHeight-t.height)+t.height>this.pageHeight-this.offsetY?`top`:`bottom`;case`left`:return Math.max(0,e.left-t.width-this.offsetX)+t.width>e.left-this.offsetX?`right`:`left`;default:return n}}getTooltipCoordinates(e,t,n){switch(n){case`top`:return{top:R(e.top-t.height-this.offsetY,0,this.pageHeight),left:R(e.left-t.width/2+e.anchorWidth/2,this.offsetX,this.pageWidth-t.width-this.offsetX)};case`right`:return{top:R(e.top+(e.anchorHeight-t.height)/2,this.offsetY,this.pageHeight-t.height-this.offsetY),left:R(e.right+this.offsetX,0,this.pageWidth-t.width)};case`bottom`:return{top:R(e.bottom+this.offsetY,0,this.pageHeight-t.height),left:R(e.left-t.width/2+e.anchorWidth/2,this.offsetX,this.pageWidth-t.width-this.offsetX)};case`left`:return{top:R(e.top+(e.anchorHeight-t.height)/2,this.offsetY,this.pageHeight-t.height-this.offsetY),left:Math.max(0,e.left-t.width-this.offsetX)};default:return{top:0,left:0}}}destroy(t=!1){if(!this.container)return this;let n=this.container;if(this.cancelPositionUpdate(),this.clearDestroyFallbackTimer(),this.showed=!1,this.syncAriaDescribedBy(!1),this.onResizeObserver?.disconnect(),this.intersectionObserver?.disconnect(),this.detachScrollListener(),t)return n.remove(),this.container=void 0,this;n.removeEventListener(`mouseleave`,this.onTooltipMouseLeave),n.style.pointerEvents=`none`,n.style.opacity=`0`;let r=()=>{this.clearDestroyFallbackTimer(),n?.remove(),this.container===n&&(this.container=void 0)};return n.addEventListener(`transitionend`,r,{once:!0}),n.addEventListener(`transitioncancel`,r,{once:!0}),this.destroyFallbackTimerId=globalThis.setTimeout(r,e.DESTROY_FALLBACK_MS),this}syncAriaDescribedBy(e){let t=this.target.getAttribute(`aria-describedby`);if(this.prevAriaDescribedBy??=t,!e){this.prevAriaDescribedBy===null?this.target.removeAttribute(`aria-describedby`):this.target.setAttribute(`aria-describedby`,this.prevAriaDescribedBy),this.prevAriaDescribedBy=null;return}let n=new Set((t??``).split(/\s+/).filter(Boolean));n.add(this.tooltipId),this.target.setAttribute(`aria-describedby`,Array.from(n).join(` `))}set bordered(e){this._bordered=e,this.container?.classList.toggle(`vot-tooltip-bordered`,e)}get bordered(){return this._bordered}set hidden(e){this._hidden=e,this.container&&(this.container.hidden=e),this.showed&&this.syncAriaDescribedBy(!e)}get hidden(){return this._hidden}attachScrollListener(){this.scrollListening||(this.scrollListening=!0,document.addEventListener(`scroll`,this.onScroll,{passive:!0,capture:!0}))}detachScrollListener(){this.scrollListening&&(this.scrollListening=!1,document.removeEventListener(`scroll`,this.onScroll,{capture:!0}))}};function Kl(e,t){return e>=t.startMs&&e{if(e.tokens.length)return e.tokens;let t=e.text.trim();return t?[{text:t,startMs:e.startMs,durationMs:e.durationMs,isWordLike:!!t}]:[]},Jl=e=>(e.text||ql(e).map(e=>e.text).join(``)).replaceAll(/\s+/gu,` `).trim(),Yl=(e,t)=>{let n=e.startMs+Math.max(0,e.durationMs),r=t.startMs+Math.max(0,t.durationMs);return e.startMs{let t=[];for(let n of e){let e=Jl(n.line);e&&(t.some(t=>e===Jl(t.line)&&t.line.speakerId===n.line.speakerId&&Yl(t.line,n.line))||t.push(n))}return t},Zl=(e,t)=>{let n=0,r=t.length-1,i=-1;for(;n<=r;){let a=n+r>>1;if(t[a].startMs<=e){i=a,n=a+1;continue}r=a-1}return i},Ql=(e,t,n=1/0)=>{let r=Zl(e,t);if(r<0)return[];let i=Number.isFinite(n)?Math.max(0,e-Math.max(0,n)):-1/0,a=[];for(let n=r;n>=0;--n){let r=t[n];if(r.startMs{let r=Ql(e,t,n);if(!r.length)return null;let i=Xl(r.map(e=>({index:e,line:t[e]})));if(!i.length)return null;if(i.length===1){let[e]=i;return{line:e.line,lineKey:`${e.index}`}}let a=[],o=[],s=[],c=[],l=1/0,u=0;for(let e of i){let t=ql(e.line);if(t.length){if(a.length>0){let t=Math.max(l,e.line.startMs);a.push({text:` +`,startMs:t,durationMs:0,isWordLike:!1})}a.push(...t),o.push(e.line.text||t.map(e=>e.text).join(``)),s.push(e.line.metadata?.rawText??e.line.text),c.push(`${e.index}`),l=Math.min(l,e.line.startMs),u=Math.max(u,e.line.startMs+Math.max(0,e.line.durationMs))}}return!a.length||!c.length?null:{line:{text:o.join(` +`),startMs:l,durationMs:Math.max(0,u-l),speakerId:i[0]?.line.speakerId??`0`,tokens:a,metadata:s.length?{rawText:s.join(` +`)}:void 0},lineKey:c.join(`,`)}},eu=[`srt`,`vtt`,`ass`,`json`],tu=[`default-sans`,`arial`,`helvetica`,`roboto`,`verdana`,`open-sans`,`poppins`,`lato`,`montserrat`,`barlow`],nu={"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`},ru=new Set(eu);function iu(e){return typeof e==`string`&&ru.has(e)}function au(e){return typeof e==`object`&&!!e}function ou(e){if(!au(e))return null;let t=e.format;return typeof e.source!=`string`||typeof e.language!=`string`||typeof e.url!=`string`||!iu(t)?null:{source:e.source,format:t,language:e.language,url:e.url,translatedFromLanguage:typeof e.translatedFromLanguage==`string`?e.translatedFromLanguage:void 0,isAutoGenerated:typeof e.isAutoGenerated==`boolean`?e.isAutoGenerated:void 0}}function su(e){return tu.includes(e)}var cu=`google:`,lu=`https://fonts.googleapis.com/css2`,uu=`https://fonts.google.com/metadata/fonts`,du={roboto:`Roboto`,"open-sans":`Open Sans`,poppins:`Poppins`,lato:`Lato`,montserrat:`Montserrat`,barlow:`Barlow`},fu=new Set,pu=new Map,mu=null;function hu(e){return`${cu}${e}`}function gu(e){if(e.startsWith(cu)){let t=e.slice(7).trim();return t.length>0?t:null}return du[e]??null}function _u(e){if(su(e))return nu[e];let t=gu(e);return t?`"${t}", "Segoe UI", system-ui, sans-serif`:nu[`default-sans`]}function vu(e){let t=gu(e);return t?`${lu}?family=${t.trim().replaceAll(/\s+/g,`+`)}&display=swap`:null}function yu(e,t){let n=`vot-google-font-${e}`;if(document.getElementById(n))return;let r=globalThis.GM_addStyle,i=typeof r==`function`?r(t):document.createElement(`style`);i instanceof HTMLElement&&(i.id=n,i.textContent||=t,i.parentElement||(document.head||document.documentElement).appendChild(i))}async function bu(e,t={}){if(fu.has(e))return;let n=pu.get(e);if(n!==void 0){await n;return}let r=vu(e);if(!r){fu.add(e);return}let i=gu(e),a=(async()=>{try{let n=await z(r,{timeout:1e4,forceGmXhr:t.forceGmXhr??!0,headers:{Accept:`text/css,*/*;q=0.1`}});if(!n.ok)throw Error(`Google Fonts CSS request failed with ${n.status}`);let a=await n.text();if(!a.trim())throw Error(`Google Fonts CSS response is empty`);yu(e,a),fu.add(e),document.fonts&&i&&await document.fonts.load(`500 20px "${i}"`),t.onLoaded?.()}catch(t){L.log(`Failed to load Google Font for subtitles`,{fontFamily:e,error:t})}finally{pu.delete(e)}})();pu.set(e,a),await a}async function xu(){return mu===null&&(mu=(async()=>{let e=await z(uu,{timeout:15e3,forceGmXhr:La});if(!e.ok)throw Error(`Google Fonts metadata request failed with ${e.status}`);let t=(await e.text()).replace(/^\)\]\}'\n?/,``),n=JSON.parse(t).familyMetadataList?.map(e=>e.family?.trim()??``).filter(e=>e.length>0);return Array.from(new Set(n)).sort((e,t)=>e.localeCompare(t))})().catch(e=>(mu=null,L.log(`Failed to load Google Fonts catalog`,e),[]))),await mu}var Su=class{video;container;fullscreenLayer=null;constructor({video:e,container:t}){this.video=e,this.container=t}updateContainer(e){this.container=e}getWidgetParentElement(){return this.shouldUseFullscreenViewportLayer()?this.ensureFullscreenLayer():this.container}getLayoutRootElement(){return this.fullscreenLayer?.isConnected?this.fullscreenLayer:this.container}syncWidgetContainer(e){let t=this.getWidgetParentElement();t===this.container&&getComputedStyle(this.container).position===`static`&&(this.container.style.position=`relative`),e&&e.parentElement!==t&&t.appendChild(e),t===this.container&&this.fullscreenLayer?.parentElement&&(this.fullscreenLayer.remove(),this.fullscreenLayer=null)}release(){this.fullscreenLayer&&=(this.fullscreenLayer.remove(),null)}getActiveFullscreenElement(){let e=document;return Po(e.fullscreenElement??e.webkitFullscreenElement,[this.container,this.video],{allowDocumentViewport:!0})}isCurrentVideoInFullscreenSession(){let e=this.getActiveFullscreenElement();return e?e===this.container||e.contains(this.container)||this.container.contains(e)?!0:!!(this.video&&(e===this.video||e.contains(this.video)||this.video.contains(e))):!1}shouldUseFullscreenViewportLayer(){return this.isCurrentVideoInFullscreenSession()}ensureFullscreenLayer(){if(!this.fullscreenLayer){let e=document.createElement(`vot-block`);e.classList.add(`vot-subtitles-layer`),this.fullscreenLayer=e}return this.fullscreenLayer.parentElement!==this.container&&this.container.appendChild(this.fullscreenLayer),this.fullscreenLayer}},Cu=/^[a-z]+$/iu,wu=/^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/iu,Tu=/^(?:rgb|rgba|hsl|hsla)\(\s*[0-9.,%\s/+-]+\)$/iu,Eu=/^[a-z0-9_-]+$/iu,Du=e=>{if(!e?.length)return;let t=Array.from(new Set(e.map(e=>e.trim()).filter(e=>e&&Eu.test(e)))).sort((e,t)=>e.localeCompare(t));return t.length?t:void 0},Ou=e=>{let t=e.trim();if(t){if(wu.test(t)||Cu.test(t))return t.toLowerCase();if(Tu.test(t))return t}},ku=e=>{if(!e)return;let t={};e.italic&&(t.italic=!0),e.bold&&(t.bold=!0),e.underline&&(t.underline=!0);let n=typeof e.color==`string`?Ou(e.color):void 0;n&&(t.color=n);let r=Du(e.classes);return r&&(t.classes=r),Object.keys(t).length?t:void 0},Au=e=>{if(!e||typeof e!=`object`)return;let t=e;return ku({italic:t.italic===!0,bold:t.bold===!0,underline:t.underline===!0,color:typeof t.color==`string`?t.color:void 0,classes:Array.isArray(t.classes)?t.classes.filter(e=>typeof e==`string`):void 0})},ju=(e,t)=>{let n=ku(e),r=ku(t),i=n?.classes??[],a=r?.classes??[];return!!n?.italic==!!r?.italic&&!!n?.bold==!!r?.bold&&!!n?.underline==!!r?.underline&&(n?.color??``)===(r?.color??``)&&i.length===a.length&&i.every((e,t)=>e===a[t])},Mu=e=>{let t=ku(e);return t?.color?`--vot-subtitles-inline-color:${t.color};`:``};function Nu(e,t,n){return Math.max(t,Math.min(e,n))}function Pu(e,t,n,r,i){let a=n-e,o=r-t;return a*a+o*o>=i*i}function Fu({elementHeight:e,boxHeight:t,bottomInset:n}){let r=Math.max(0,e||0),i=Math.max(r,t-n);return{minAnchorY:r,baselineAnchorY:i,travelPx:Math.max(0,i-r)}}function Iu({anchorY:e,elementHeight:t,boxHeight:n,bottomInset:r}){let{minAnchorY:i,baselineAnchorY:a,travelPx:o}=Fu({elementHeight:t,boxHeight:n,bottomInset:r});return{offsetFromBaselinePx:Nu(e,i,a)-a,travelPx:o}}function Lu({state:e,elementHeight:t,boxHeight:n,bottomInset:r}){let{minAnchorY:i,baselineAnchorY:a,travelPx:o}=Fu({elementHeight:t,boxHeight:n,bottomInset:r});if(!e||o<=0)return a;let s=Math.max(0,e.travelPx||0),c=Math.max(0,-(e.offsetFromBaselinePx||0));if(s<=0||c<=0)return a;let l=c/s*o;return Nu(a-Math.min(o>=s?c:o,l),i,a)}function Ru({anchorX:e,anchorY:t,elementWidth:n,elementHeight:r,boxWidth:i,boxHeight:a,bottomInset:o}){let s=e,c=t,l=Math.max(0,a-o),u=r||0;if(n){let e=s-n/2,t=i-n;e=t>=0?Nu(e,0,t):t/2,s=e+n/2}return c=Nu(c,u,l),{anchorX:s,anchorY:c}}function zu({current:e,candidates:t,thresholdPx:n}){let r=e,i=1/0;for(let n of t){let t=Math.abs(n-e);tn?{snapped:!1,value:e}:{snapped:!0,value:r}}var Bu=/^[\p{P}\p{S}]+/u,Vu=/[\p{P}\p{S}]+$/u,Hu=/^[\p{P}\p{S}]+$/u,Uu=/\s+|[\p{P}\p{S}]+|[^\s\p{P}\p{S}]+/gu,Wu=(e,t,n,r={})=>{e.push({kind:`text`,text:t,style:n,highlightIndex:r.highlightIndex}),r.withBreak&&e.push({kind:`break`})},Gu=(e,t,n)=>{let r=t;for(;r<=n&&!e[r]?.isWordLike&&!e[r]?.text.trim();)r+=1;return r},Ku=(e,t,n)=>{for(let r=t;r<=n;r+=1){let t=e[r]?.text??``;if(!(!e[r]?.isWordLike||!t.trim())&&t.trimStart().replace(Bu,``).replace(Vu,``))return!0}return!1},qu=(e,t,n,r,i,a)=>{let o=t[n],s=/^\s+/u.exec(o.text)?.[0]??``,c=o.text.slice(s.length);s&&Wu(e,s,o.style);let l=Bu.exec(c)?.[0]??``,u=c.slice(l.length),d=Vu.exec(u)?.[0]??``,f=d?u.slice(0,u.length-d.length):u;return f?(l&&Wu(e,l,o.style,{highlightIndex:a}),e.push({kind:`word`,text:f,style:o.style,highlightIndex:a}),d&&Wu(e,d,o.style,{highlightIndex:a}),i?.has(n)?(e.push({kind:`break`}),{consumedWord:!0,nextTokenIndex:Gu(t,n+1,r)}):{consumedWord:!0,nextTokenIndex:n+1}):(c&&Wu(e,c,o.style),i?.has(n)?(e.push({kind:`break`}),{consumedWord:!1,nextTokenIndex:Gu(t,n+1,r)}):{consumedWord:!1,nextTokenIndex:n+1})},Ju=(e,t,n,r,i,a,o,s,c)=>{let l=s??(Ku(n,t+1,r)?c:void 0),u=a.match(Uu)??[a];for(let t of u)Wu(e,t,i.style,{highlightIndex:Hu.test(t)?l:void 0});return o?(e.push({kind:`break`}),Gu(n,t+1,r)):t+1};function Yu(e,t,n){let r=[],i=0,a=null;for(let o=0;o<=t;){let s=e[o],c=s?.text??``;if(!c){o+=1;continue}if(c===` +`){r.push({kind:`break`}),o+=1;continue}if(s.isWordLike){let s=qu(r,e,o,t,n,i);o=s.nextTokenIndex,s.consumedWord&&(a=i,i+=1);continue}let l=!!n?.has(o);o=Ju(r,o,e,t,s,c,l,a,i)}return r}var Xu=(e,t,n)=>Math.min(n,Math.max(t,e)),Zu=e=>Math.round(e),Qu=e=>e<.8?{widthRatio:.9,charsPerLine:27,fontHeightRatio:.03}:e<1.1?{widthRatio:.84,charsPerLine:31,fontHeightRatio:.031}:e<1.5?{widthRatio:.76,charsPerLine:36,fontHeightRatio:.033}:e<1.95?{widthRatio:.72,charsPerLine:40,fontHeightRatio:.034}:{widthRatio:.68,charsPerLine:44,fontHeightRatio:.035},$u=e=>e>=1920?{extraChars:4,widthScale:1.04}:e>=1440?{extraChars:3,widthScale:1.03}:e>=960?{extraChars:2,widthScale:1.02}:e>=640?{extraChars:1,widthScale:1.01}:{extraChars:0,widthScale:1},ed=e=>Math.max(7,e*.56);function td(e,t=null){let n=Number.isFinite(e.w)?Math.max(0,e.w):0,r=Number.isFinite(e.h)?Math.max(0,e.h):0;if(n<=0||r<=0)return{fontSizePx:t?.fontSizePx??20,maxWidthPx:t?.maxWidthPx??null};let{widthRatio:i,charsPerLine:a,fontHeightRatio:o}=Qu(n/r),{extraChars:s,widthScale:c}=$u(n),l=Xu(r*o,16,42),u=t?.fontSizePx??l,d=ed(u),f=n*Math.min(.92,i),p=n*Xu(i*c,.66,.92);return{fontSizePx:u,maxWidthPx:Zu(Xu(Xu(a+s,25,48)*d,f,p))}}var nd=/[.!?…:;][)"'\]»”]*\s*$/u,rd=/[,،、][)"'\]»”]*\s*$/u,id=/^\s*[\p{Pe}\p{Pf},.;:!?%‰…]/u,ad=/\s*[\p{Ps}\p{Pi}¿¡([{«“"'`-]\s*$/u,od=e=>e.replaceAll(/\s+/gu,` `).trim(),sd=e=>nd.test(e)?`strong`:rd.test(e)?`soft`:`neutral`,cd=e=>id.test(e),ld=e=>ad.test(e),ud=e=>!!(e?.isWordLike&&e.text.trim()),dd=e=>e&&Number.isFinite(e.startMs)?e.startMs:0,fd=e=>e?dd(e)+Math.max(0,e.durationMs):0,pd=(e,t,n)=>{for(let r=t;r{for(let r=n-1;r>=t;--r){let t=e[r];if(ud(t))return fd(t)}return fd(e[n-1])},hd=(e,t)=>{let n=e[t],r=dd(n);return{text:` +`,tokenIndex:t,breakAfterTokenIndex:t,startToken:t,endToken:t+1,charLength:0,startMs:r,endMs:r,boundary:`strong`,forcesLineBreak:!0}},gd=(e,t)=>{let n=t;for(;n>0&&e[n-1]?.text!==` +`&&!ud(e[n-1]);)--n;let r=t+1;for(;re.text).join(``);return{text:i,tokenIndex:t,breakAfterTokenIndex:r-1,startToken:n,endToken:r,charLength:od(i).length,startMs:pd(e,n,r),endMs:md(e,n,r),boundary:sd(i),forcesLineBreak:!1}};function _d(e){let t=[],n=[],r=0;for(;re.text).join(``);t.push({text:r,tokenIndex:0,breakAfterTokenIndex:e.length-1,startToken:0,endToken:e.length,charLength:od(r).length,startMs:pd(e,0,e.length),endMs:md(e,0,e.length),boundary:sd(r),forcesLineBreak:!1}),n.push(od(r))}return{slices:t,key:n.join(`|`)}}function vd(e,t){return e.map(e=>({...e,width:e.forcesLineBreak?0:t(e.text)}))}var yd=(e,t)=>t<=0?0:fd(e[t-1]),bd=(e,t,n,r)=>{if(r<=n)return;let i=pd(t,n,r),a=yd(t,r);e.push({startToken:n,endToken:r,startMs:i,endMs:Math.max(i,a)})};function xd(e,t,n,r){if(!t.length||!e.length)return[];let i=Math.max(1,n),a=Math.max(1,r),o=[],s=t[0].startToken,c=0,l=0,u=1,d=s;for(let n of t){if(n.forcesLineBreak){if(u+=1,l=0,d=n.endToken,u>2){let t=Math.max(n.startToken,n.tokenIndex);bd(o,e,s,t),s=t,c=0,d=s}continue}let t=c+n.charLength;if((l===0||l+n.width<=i)&&t<=a){l+=n.width,c=t,d=n.endToken;continue}if(u===1){u=2,l=n.width,c=t,d=n.endToken;continue}let r=Math.max(n.startToken,n.tokenIndex);bd(o,e,s,r),s=r,c=n.charLength,l=n.width,u=1,d=n.endToken}if(bd(o,e,s,d),!o.length)return[{startToken:0,endToken:e.length,startMs:pd(e,0,e.length),endMs:md(e,0,e.length)}];for(let e=0;et.startMs&&(t.endMs=n.startMs)}let f=o.at(-1);return f&&(f.endMs=Math.max(f.endMs,md(e,f.startToken,f.endToken))),o.filter(e=>e.endToken>e.startToken)}var Sd=(e,t,n,r)=>n<=t?0:r(e.slice(t,n).map(e=>e.text).join(``)),Cd=(e,t)=>{let n=t;for(;n+1{let r=null,i=1/0;for(let a=0;a=e.length-1)continue;let l=c+1,u=l,d=Sd(e,0,l,t),f=Sd(e,u,e.length,t),p=e.slice(0,l).map(e=>e.text).join(``),m=e.slice(u).map(e=>e.text).join(``),h=Math.max(0,d-n)*12+Math.max(0,f-n)*12+Math.abs(f-d)*.4+(cd(m)?260:0)+(ld(p)?70:0);h{let c=Math.max(0,e-o)*12+Math.max(0,t-o)*12,l=Math.abs(t/Math.max(e,1)-1.08)*120,u=i<2?80:0,d=a<2?80:0,f=cd(r)?260:0,p=ld(n)?70:0,m=s===`strong`?-28:s===`soft`?-14:0;return c+l+u+d+f+p+m};function Ed(e,t,n){if(!e.length||e.reduce((e,t)=>e+Number(t.text===` +`),0)>0)return{breakAfterTokenIndices:[]};let{slices:r}=_d(e),i=r.filter(e=>!e.forcesLineBreak);if(!i.length||Sd(e,0,e.length,t)<=n)return{breakAfterTokenIndices:[]};let a=null,o=1/0;for(let r=0;re.text).join(``),secondText:e.slice(d).map(e=>e.text).join(``),firstWordCount:r+1,secondWordCount:i.length-(r+1),maxWidthPx:n,boundary:s.boundary});fthis.onPointerDown(e),this.onPointerUpBound=e=>this.onPointerUp(e),this.onPointerMoveBound=e=>this.onPointerMove(e),this.onPlaybackStateChangeBound=()=>this.handlePlaybackStateChange(),this.onVisualViewportChangeBound=()=>this.scheduleReposition(),this.checkerUnsubscribe=this.intervalIdleChecker.subscribe(()=>{this.onCheckerTick()}),this.bindEvents()}updateMount({container:e,tooltipLayoutRoot:t}){let n=this.container!==e,r=this.tooltipLayoutRoot!==t;this.container=e,this.fullscreenLayerController.updateContainer(e),this.tooltipLayoutRoot=t,this.syncWidgetMount(),(n||r)&&this.tokenTooltip?.updateMount({parentElement:this.getTokenTooltipParentElement(),layoutRoot:this.tooltipLayoutRoot??document.documentElement}),this.subtitles&&(this.insetCacheReady=!1,this.lastAppliedLeftPct=null,this.lastAppliedTopPct=null,this.updateContainerRect(),this.requestUpdate())}resetTranslationContext(e=!1){this.strTranslatedTokens=``,e&&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(e){let t={left:0,top:0,w:e.w,h:e.h},n=this.video;if(!n)return t;let r=n.getBoundingClientRect();if(!(r.width>0&&r.height>0))return t;let i=e.rect;if(!(r.right>i.left&&r.lefti.top&&r.top0&&o>0))return t;let s=(r.left-i.left)/e.scaleX,c=(r.top-i.top)/e.scaleY,l=e.w-a,u=e.h-o;return{left:l>=0?Nu(s,0,l):(e.w-a)/2,top:u>=0?Nu(c,0,u):(e.h-o)/2,w:a,h:o}}readSmartCssMetrics(){let e=this.subtitlesBlock;if(!e)return null;let t=getComputedStyle(e),n=Number.parseFloat(t.fontSize),r=Number.parseFloat(t.maxWidth);if(!Number.isFinite(n)||!Number.isFinite(r)||n<=0||r<=0)return null;this.subtitleMaxWidthPx=r;let i=Number.parseFloat(t.paddingLeft)||0,a=Number.parseFloat(t.paddingRight)||0,o=Math.max(0,r-i-a);return o<=0?null:{fontSizePx:n,maxWidthPx:o}}ensureSmartLayout(e){if(!this.smartLayoutEnabled)return null;let t=this.readSmartCssMetrics(),n=t?.fontSizePx??this.smartFontSizePx,r=td(e,t),i=r.maxWidthPx??this.smartMaxWidthPx,a=`${Math.round(n)}|${Math.round(i)}|${Math.round(r.maxWidthPx??0)}`,o=Math.abs(n-this.smartFontSizePx)>.5,s=Math.abs(i-this.smartMaxWidthPx)>.5;return a!==this.lastSmartLayoutKey&&(this.lastSmartLayoutKey=a,this.smartFontSizePx=n,this.smartMaxWidthPx=i,this.resetRenderMemo()),this.setSubtitlesContainerVar(`--vot-subtitles-max-width`,r.maxWidthPx&&r.maxWidthPx>0?`${r.maxWidthPx}px`:null),(o||s)&&this.lastWrapTokens&&(this.lastWrapKey=null,this.resetSegmentationMemo(),this.scheduleWrapRecompute()),r}scheduleReposition(){this.abortController.signal.aborted||this.subtitles&&(this.repositionPending=!0,this.intervalIdleChecker.markActivity(`subtitles-reposition`),this.intervalIdleChecker.requestImmediateTick())}setSubtitlesContainerVar(e,t){let n=this.subtitlesContainer;if(n){if(t===null){n.style.removeProperty(e);return}n.style.setProperty(e,t)}}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(){let e=this.fontFamily;this.setSubtitlesContainerVar(`--vot-subtitles-font-family-custom`,_u(e)),bu(e,{forceGmXhr:!0,onLoaded:()=>{this.fontFamily===e&&(this.lastWrapKey=null,this.resetSegmentationMemo(),this.scheduleWrapRecompute(),this.scheduleReposition())}})}syncVisualStyleVars(){this.applyOpacityStyle(),this.applyManualFontSizeStyle(),this.applyFontFamilyStyle()}ensureGuidesLayer(){if(this.guidesLayer)return this.guidesLayer;let e=document.createElement(`vot-block`);e.classList.add(`vot-subtitles-guides`);let t=document.createElement(`vot-block`);t.classList.add(`vot-subtitles-guide`,`vot-subtitles-guide--vertical`);let n=document.createElement(`vot-block`);return n.classList.add(`vot-subtitles-guide`,`vot-subtitles-guide--horizontal`),e.append(t,n),this.guidesLayer=e,this.verticalGuide=t,this.horizontalGuide=n,this.hideSnapGuides(),e}hideSnapGuides(){this.verticalGuide?.removeAttribute(`data-visible`),this.horizontalGuide?.removeAttribute(`data-visible`)}updateSnapGuides(e,t){let{showVerticalCenter:n=!1,showHorizontalCenter:r=!1}=t;this.ensureGuidesLayer().isConnected||this.syncGuideLayerMount(),this.verticalGuide&&(this.verticalGuide.style.left=`${e.left+e.w/2}px`,this.verticalGuide.style.top=`${e.top}px`,this.verticalGuide.style.height=`${e.h}px`,n?this.verticalGuide.dataset.visible=`true`:delete this.verticalGuide.dataset.visible),this.horizontalGuide&&(this.horizontalGuide.style.left=`${e.left}px`,this.horizontalGuide.style.top=`${e.top+e.h/2}px`,this.horizontalGuide.style.width=`${e.w}px`,r?this.horizontalGuide.dataset.visible=`true`:delete this.horizontalGuide.dataset.visible)}syncGuideLayerMount(){let e=this.fullscreenLayerController.getWidgetParentElement(),t=this.ensureGuidesLayer();t.parentElement!==e&&e.appendChild(t)}syncWidgetMount(){this.fullscreenLayerController.syncWidgetContainer(this.subtitlesContainer),this.syncGuideLayerMount()}getTokenTooltipParentElement(){let e=this.fullscreenLayerController.getWidgetParentElement();return e===this.container?document.documentElement:e}createSubtitlesContainer(){if(this.subtitlesContainer)return this.subtitlesContainer;let e=document.createElement(`vot-block`);return e.classList.add(`vot-subtitles-widget`),this.subtitlesContainer=e,this.syncWidgetMount(),e.addEventListener(`pointerdown`,this.onPointerDownBound,{signal:this.abortController.signal,passive:!0}),this.syncVisualStyleVars(),this.insetCacheReady=!1,this.updateContainerRect(),e}bindEvents(){let{signal:e}=this.abortController,t={signal:e};this.video?.addEventListener(`play`,this.onPlaybackStateChangeBound,t),this.video?.addEventListener(`pause`,this.onPlaybackStateChangeBound,t),this.video?.addEventListener(`seeking`,this.onPlaybackStateChangeBound,t),this.video?.addEventListener(`seeked`,this.onPlaybackStateChangeBound,t),this.video?.addEventListener(`ended`,this.onPlaybackStateChangeBound,t),this.resizeObserver=new ResizeObserver(()=>this.onResize()),this.resizeObserver.observe(this.container),this.video&&this.resizeObserver.observe(this.video),globalThis.visualViewport?.addEventListener(`resize`,this.onVisualViewportChangeBound,t),globalThis.visualViewport?.addEventListener(`scroll`,this.onVisualViewportChangeBound,t)}getUpdateMinIntervalMs(){return this.highlightWords?this.updateMinIntervalHighlightMs:this.updateMinIntervalMs}requestUpdate(e,t=performance.now()){if(this.abortController.signal.aborted||!this.subtitles)return;typeof e==`number`&&Number.isFinite(e)?this.lastPlaybackTimeMs=Math.max(0,e):this.video&&(this.lastPlaybackTimeMs=Math.max(0,this.video.currentTime*1e3));let n=this.getUpdateMinIntervalMs();t-this.lastUpdateRequestTs{if(this.videoFrameRequestId=null,this.abortController.signal.aborted)return;let n=this.video;if(!n||n.paused||n.ended||!this.subtitles)return;let r=typeof t.mediaTime==`number`&&Number.isFinite(t.mediaTime)?t.mediaTime*1e3:void 0;this.requestUpdate(r,e),this.startVideoFrameLoop()};onCheckerTick(){this.abortController.signal.aborted||(this.repositionPending&&(this.repositionPending=!1,this.updateContainerRect(),this.updatePending=!0),this.wrapPending&&(this.wrapPending=!1,this.recomputeWrapNow()),this.positionRefreshPending&&(this.positionRefreshPending=!1,this.applySubtitlePosition()),this.updatePending&&(this.updatePending=!1,this.update()))}attachDragDocumentListeners(){this.dragDocListenersAttached||(this.dragDocListenersAttached=!0,document.addEventListener(`pointermove`,this.onPointerMoveBound,{passive:!1}),document.addEventListener(`pointerup`,this.onPointerUpBound),document.addEventListener(`pointercancel`,this.onPointerUpBound))}detachDragDocumentListeners(){this.dragDocListenersAttached&&(this.dragDocListenersAttached=!1,document.removeEventListener(`pointermove`,this.onPointerMoveBound),document.removeEventListener(`pointerup`,this.onPointerUpBound),document.removeEventListener(`pointercancel`,this.onPointerUpBound))}onResize(){this.syncWidgetMount(),this.scheduleReposition()}updateContainerRect(){let e=this.getLayoutSize();if(!e.w||!e.h)return;let t=this.computeAnchorBoxLayout(e);!t.w||!t.h||(this.refreshBottomInsetNow(e,t),this.applySubtitlePositionWithLayout(e,t))}getLayoutSize(){let e=this.fullscreenLayerController.getLayoutRootElement(),t=e.getBoundingClientRect(),n=e.clientWidth||t.width,r=e.clientHeight||t.height;return{w:n,h:r,rect:t,scaleX:t.width&&n?t.width/n:1,scaleY:t.height&&r?t.height/r:1}}ensureSafeAreaProbe(){if(this.safeAreaProbeEl)return;let e=document.createElement(`div`);e.style.position=`fixed`,e.style.left=`0`,e.style.right=`0`,e.style.bottom=`0`,e.style.height=`env(safe-area-inset-bottom, 0px)`,e.style.pointerEvents=`none`,e.style.opacity=`0`,e.style.zIndex=`-1`,document.documentElement.appendChild(e),this.safeAreaProbeEl=e}getSafeAreaBottomInsetPx(){return this.ensureSafeAreaProbe(),this.safeAreaProbeEl&&this.safeAreaProbeEl.offsetHeight||0}refreshInsetCache(){let e=this.fullscreenLayerController.getLayoutRootElement();this.safeAreaBottomInsetCachedPx=this.getSafeAreaBottomInsetPx(),this.containerPaddingBottomCachedPx=Number.parseFloat(getComputedStyle(e).paddingBottom||`0`)||0,this.insetCacheReady=!0}isMobileViewport(){return typeof globalThis.matchMedia==`function`?globalThis.matchMedia(`(max-width: 900px) and (pointer: coarse)`).matches:!1}getBottomInsetPreset(){let e=document,t=e.fullscreenElement??e.webkitFullscreenElement;if(!(t instanceof Element))return this.bottomInsetByMode.normal;let{container:n,video:r}=this;return t===n||t.contains(n)||n.contains(t)||r&&(t===r||t.contains(r)||r.contains(t))?this.bottomInsetByMode.fullscreen:this.bottomInsetByMode.normal}computeReservedBottomInsetPx(e,t=this.getBottomInsetPreset()){return Nu(e*t.ratio,t.minPx,t.maxPx)}refreshBottomInsetNow(e,t){this.refreshInsetCache();let n=t?.h??this.computeAnchorBoxLayout(e??this.getLayoutSize()).h;if(!n){this.bottomInsetCachedPx=0;return}let r=this.getBottomInsetPreset();this.bottomInsetCachedPx=this.computeReservedBottomInsetPx(n,r)}getBottomInsetPx(e,t){this.insetCacheReady||this.refreshInsetCache();let n=this.getBottomInsetPreset(),r=this.safeAreaBottomInsetCachedPx,i=this.containerPaddingBottomCachedPx;if(this.isMobileViewport())return Math.max(i,r);let a=t?.h??this.computeAnchorBoxLayout(e??this.getLayoutSize()).h,o=a?this.computeReservedBottomInsetPx(a,n):n.minPx,s=Math.max(this.bottomInsetCachedPx,o);return Math.max(i,r,s)+n.gapPx}onPointerDown(e){let t=this.subtitlesContainer;if(!t)return;let n=e.target;if(!(n instanceof Node)||!t.contains(n)||!e.isPrimary||e.pointerType===`mouse`&&e.button!==0)return;let r=this.getLayoutSize(),{rect:i,w:a,h:o,scaleX:s,scaleY:c}=r;if(!a||!o)return;let l=this.computeAnchorBoxLayout(r);if(!l.w||!l.h)return;this.lastPositionRefreshTs=performance.now();let u=t.getBoundingClientRect(),d=(e.clientX-i.left)/s-l.left,f=(e.clientY-i.top)/c-l.top,p=(u.left-i.left+u.width/2)/s-l.left,m=(u.top-i.top+u.height)/c-l.top;this.dragging.pointerId=e.pointerId,this.dragging.candidate=!0,this.dragging.active=!1,this.dragging.moved=!1,this.dragging.startClientX=e.clientX,this.dragging.startClientY=e.clientY,this.dragging.offset.x=p-d,this.dragging.offset.y=m-f,this.hideSnapGuides(),this.attachDragDocumentListeners();let h=this.subtitlesBlock??(n instanceof Element?n:null);try{h?.setPointerCapture(e.pointerId)}catch{}}onPointerUp(e){this.dragging.pointerId!==null&&e.pointerId===this.dragging.pointerId&&(this.dragging.moved&&(this.suppressTokenClicksUntil=performance.now()+450),this.dragging.pointerId=null,this.dragging.candidate=!1,this.dragging.active=!1,this.dragging.moved=!1,this.hideSnapGuides(),this.detachDragDocumentListeners())}onPointerMove(e){if(!this.dragging.candidate||this.dragging.pointerId===null||e.pointerId!==this.dragging.pointerId)return;if(this.dragging.active)this.dragging.moved=!0;else{if(!Pu(this.dragging.startClientX,this.dragging.startClientY,e.clientX,e.clientY,this.dragStartThresholdPx))return;this.dragging.active=!0,this.dragging.moved=!0,this.suppressTokenClicksUntil=performance.now()+450,this.releaseTooltip()}e.preventDefault(),e.stopPropagation();let t=this.getLayoutSize(),{rect:n,w:r,h:i,scaleX:a,scaleY:o}=t;if(!r||!i)return;let s=this.computeAnchorBoxLayout(t);if(!s.w||!s.h)return;let c=(e.clientX-n.left)/a-s.left,l=(e.clientY-n.top)/o-s.top,u=c+this.dragging.offset.x,d=l+this.dragging.offset.y,f=this.subtitlesContainer?.offsetWidth??0,p=this.subtitlesContainer?.offsetHeight??0,m=this.getBottomInsetPx(t,s),h=zu({current:u,candidates:[s.w/2],thresholdPx:this.snapThresholdPx});h.snapped&&(u=h.value);let g=s.h/2+p/2,_=zu({current:d,candidates:[g],thresholdPx:this.snapThresholdPx});_.snapped&&(d=_.value),{anchorX:u,anchorY:d}=Ru({anchorX:u,anchorY:d,elementWidth:f,elementHeight:p,boxWidth:s.w,boxHeight:s.h,bottomInset:m}),this.positionPreset=`custom`,this.customVerticalAnchorState=Iu({anchorY:d,elementHeight:p,boxHeight:s.h,bottomInset:m}),this.position.left=u/s.w*100,this.position.top=d/s.h*100,this.updateSnapGuides(s,{showVerticalCenter:h.snapped,showHorizontalCenter:_.snapped}),this.applySubtitlePositionWithLayout(t,s)}applySubtitlePosition(){if(!this.subtitlesContainer)return;let e=this.getLayoutSize();if(!e.w||!e.h)return;let t=this.computeAnchorBoxLayout(e);!t.w||!t.h||this.applySubtitlePositionWithLayout(e,t)}applySubtitlePositionWithLayout(e,t){let n=this.subtitlesContainer;if(!n)return;this.applyScaleCompensation(n,e),this.syncAnchorDimensions(n,t),this.smartLayoutEnabled&&this.ensureSmartLayout(t);let r=n.offsetWidth,i=n.offsetHeight,a=this.getBottomInsetPx(e,t),o=this.resolveCurrentAnchorPosition(t,r,i,a),s=this.clampContainerPosition(t,o.anchorX,o.anchorY,r,i,a),c=s.anchorX,l=s.anchorY,u=t.left+c,d=t.top+l,f=u/e.w*100,p=d/e.h*100;this.updateContainerPosition(n,f,p),this.tokenTooltip?.updatePos()}applyScaleCompensation(e,t){let n=Math.min(t.scaleX||1,t.scaleY||1),r=n>0&&n<.999?Math.min(1/n,3):1;if(Math.abs(r-1)<.001){e.style.removeProperty(`--vot-subtitles-scale-compensation`);return}e.style.setProperty(`--vot-subtitles-scale-compensation`,r.toFixed(3))}syncAnchorDimensions(e,t){let n=Math.max(1,Math.round(t.w)),r=Math.max(1,Math.round(t.h));(n!==this.smartAnchorWidthPx||r!==this.smartAnchorHeightPx)&&(this.smartAnchorWidthPx=n,this.smartAnchorHeightPx=r,e.style.setProperty(`--vot-subtitles-anchor-width`,`${n}px`),e.style.setProperty(`--vot-subtitles-anchor-height`,`${r}px`),this.lastWrapTokens&&(this.lastWrapKey=null,this.resetSegmentationMemo(),this.scheduleWrapRecompute()))}resolveCurrentAnchorPosition(e,t,n,r){let i=this.position.left/100*e.w,a=this.position.top/100*e.h;if(this.positionPreset===`custom`)return a=Lu({state:this.customVerticalAnchorState,elementHeight:n,boxHeight:e.h,bottomInset:r}),{anchorX:i,anchorY:a};let o=this.resolvePresetAnchorPosition({preset:this.positionPreset,anchorBox:e,elementWidth:t,elementHeight:n,bottomInset:r});return i=o.anchorX,a=o.anchorY,e.w>0&&(this.position.left=i/e.w*100),e.h>0&&(this.position.top=a/e.h*100),{anchorX:i,anchorY:a}}clampContainerPosition(e,t,n,r,i,a){let o=t-r/2,s=n-i,c=e.w-r,l=e.h-a-i;return o=c>=0?Nu(o,0,c):c/2,s=l>=0?Nu(s,0,l):0,{anchorX:o+r/2,anchorY:s+i}}updateContainerPosition(e,t,n){(this.lastAppliedLeftPct===null||Math.abs(t-this.lastAppliedLeftPct)>=.01)&&(e.style.left=`${t}%`,this.lastAppliedLeftPct=t),(this.lastAppliedTopPct===null||Math.abs(n-this.lastAppliedTopPct)>=.01)&&(e.style.top=`${n}%`,this.lastAppliedTopPct=n)}resolvePresetAnchorPosition({preset:e,anchorBox:t,elementWidth:n,elementHeight:r,bottomInset:i}){let a=t.w/2,o=t.h-i;switch(e){case`top-center`:o=r;break;case`center`:o=t.h/2+r/2;break;case`bottom-left`:a=n/2;break;case`bottom-right`:a=t.w-n/2;break;case`bottom-center`:case`custom`:break}return Ru({anchorX:a,anchorY:o,elementWidth:n,elementHeight:r,boxWidth:t.w,boxHeight:t.h,bottomInset:i})}applyPositionAfterContentRender(){let e=this.getLayoutSize();if(e.w&&e.h){let t=this.computeAnchorBoxLayout(e);if(t.w&&t.h){this.refreshBottomInsetNow(e,t),this.applySubtitlePositionWithLayout(e,t);return}this.refreshBottomInsetNow(e),this.applySubtitlePosition();return}this.refreshBottomInsetNow(),this.applySubtitlePosition()}trimEdgeWhitespaceTokens(e){if(!e.length)return e;let t=0,n=e.length;for(;tt&&!e[n-1]?.text.trim();)--n;return t===0&&n===e.length?e:t>=n?[]:e.slice(t,n)}selectTokensByMaxLength(e,t){if(!e.length)return e;let n=0,r=0,i=!1,a=0,o=e.length,s=!1,c=!1,l=(n,r)=>{if(r<=n||(s||=(a=n,o=r,!0),c))return;let i=e[n],l=e[r-1];if(!i||!l)return;let u=(rthis.maxLength&&t>n){i=!0,l(n,t),n=t,r=a.text.length;continue}r=e}return i?(l(n,e.length),this.trimEdgeWhitespaceTokens(e.slice(a,o))):this.trimEdgeWhitespaceTokens(e)}buildTokenPrecomputeInput(e){let t=this.tokenPrecomputeMemo;if(t?.tokens===e)return t.value;let{slices:n,key:r}=_d(e),i={wordSlices:n,normalizedWordsKey:r};return this.tokenPrecomputeMemo={tokens:e,value:i},i}getTokenLayoutInputs(e){let t=this.subtitlesBlock;if(t){let n=getComputedStyle(t),r=`${n.fontStyle} ${n.fontVariant} ${n.fontWeight} ${n.fontSize} ${n.fontFamily}`;e.font=r;let i=Number.parseFloat(n.maxWidth),a=Number.parseFloat(n.paddingLeft)||0,o=Number.parseFloat(n.paddingRight)||0,s=Number.isFinite(i)?i:this.subtitleMaxWidthPx||globalThis.innerWidth*.8;return Number.isFinite(s)&&s>0&&(this.subtitleMaxWidthPx=s),{fontKey:r,maxWidthPx:Math.max(0,s-a-o)}}let n=Number.parseFloat(getComputedStyle(document.documentElement).fontSize)||16,r=Math.min(n*52,this.subtitleMaxWidthPx||globalThis.innerWidth*.8),i=this.fontSizeOverridden?this.fontSize:Math.min(24,Math.max(14,globalThis.innerWidth*.016)),a=`normal normal 500 ${i}px ${_u(this.fontFamily)}`;return e.font=a,{fontKey:a,maxWidthPx:Math.max(0,r-i)}}getActiveLineKey(e){return this.lastActiveLineKey===null?`${e[0]?.startMs??0}:${e[0]?.durationMs??0}:${e.length}`:this.lastActiveLineKey}getLineMeasureMemo(e,t){let{wordSlices:n,normalizedWordsKey:r}=this.buildTokenPrecomputeInput(e);if(!n.length)return null;let i=this.getMeasureContext();if(!i)return null;let{fontKey:a,maxWidthPx:o}=this.getTokenLayoutInputs(i);if(!Number.isFinite(o)||o<24)return null;let s=`${t}|${a}|${Math.round(o)}|${r}`;if(this.lineMeasureMemo?.key===s)return this.lineMeasureMemo;let c={key:s,metrics:vd(n,e=>i.measureText(e).width),maxWidthPx:o};return this.lineMeasureMemo=c,c}buildTokenProcessingMemo(e,t){let n=this.getLineMeasureMemo(e,t);if(!n)return null;let r=`${n.key}|${this.maxLength}`;if(this.tokenProcessingMemo?.key===r)return this.tokenProcessingMemo;let i=Ad(n.maxWidthPx),a={key:r,segmentRanges:xd(e,n.metrics,i,this.maxLength)};return this.tokenProcessingMemo=a,this.lastSegmentIndex=0,a}selectSegmentIndexFromRanges(e,t){if(!e.length)return-1;let n=this.lastSegmentIndex;for(n>=e.length&&(n=0);n=e[n].endMs;)n+=1;for(;n>0&&t=e[n].startMs&&tt>=e.startMs&&t=0?r:t{if(performance.now()i[e];return r.length=i.length,r}renderTokens(e){return Yu(e,e.length-1,this.breakAfterTokenIndexSet).map(e=>this.renderPlanPart(e))}renderStyledSpan(e,t,n=!1,r){return!t&&!n&&r===void 0?e:Tl`${e}`}renderPlanPart(e){return e.kind===`break`?Tl`
`:this.renderStyledSpan(e.text,e.style,e.kind===`word`,e.highlightIndex)}updatePassedClasses(e){for(let t of this.renderedHighlightEls){let n=Number.parseInt(t.dataset.votHighlightIndex??``,10),r=Number.isInteger(n)&&n>=0&&nn.measureText(e).width,a);this.arraysEqual(s.breakAfterTokenIndices,this.breakAfterTokenIndices)||(this.setBreakAfterTokenIndices(s.breakAfterTokenIndices),this.resetRenderMemo(),this.update())}setContent(e,t=void 0){if(this.releaseTooltip(),this.subtitleLang=t,!e||!this.video){this.clearRenderedContent(),this.subtitles=null,this.maxActiveCueLookbackMs=0,this.lastPlaybackTimeMs=null,this.clearPendingSchedulerState(),this.stopVideoFrameLoop(),this.detachDragDocumentListeners();return}this.createSubtitlesContainer(),this.subtitles=e,this.maxActiveCueLookbackMs=e.subtitles.reduce((e,t)=>Math.max(e,Math.max(0,t.durationMs)),0),this.lastPlaybackTimeMs=Math.max(0,this.video.currentTime*1e3),this.lastActiveLineKey=null,this.syncVideoFrameLoop(),this.updateContainerRect(),this.update(),this.intervalIdleChecker.requestImmediateTick()}setMaxLength(e){typeof e==`number`&&e>0&&(this.maxLength=e,this.resetSegmentationMemo(),this.update(),this.scheduleReposition())}setHighlightWords(e){let t=this.highlightWords;this.highlightWords=!!e,t&&!this.highlightWords&&this.clearPassedClasses(),this.update()}setSmartLayout(e){let t=e!==!1;t!==this.smartLayoutEnabled&&(this.smartLayoutEnabled=t,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(e){this.fontSize=e,this.fontSizeOverridden=!0,this.smartLayoutEnabled||(this.applyManualFontSizeStyle(),this.lastWrapKey=null,this.resetSegmentationMemo(),this.scheduleWrapRecompute(),this.scheduleReposition())}setFontFamily(e){this.fontFamily=e,this.applyFontFamilyStyle(),this.lastWrapKey=null,this.resetSegmentationMemo(),this.scheduleWrapRecompute(),this.scheduleReposition()}setOpacity(e){let t=Number(e);this.opacity=((100-(Number.isFinite(t)?Nu(t,0,100):0))/100).toFixed(2),this.applyOpacityStyle()}stringifyTokens(e){let t=``;for(let n of e)t+=n.text;return t}resolveActiveLine(e,t){return $l(e,t,this.maxActiveCueLookbackMs)}clearInactiveLineState(){if(this.lastActiveLineKey=null,this.subtitlesBlock||this.lastRenderKey!==null||this.strTokens){this.clearRenderedContent({releaseTooltip:!0});return}this.releaseTooltip()}refreshSmartLayoutIfNeeded(){if(!this.smartLayoutEnabled)return;let e=performance.now();if(this.lastSmartLayoutKey!==null&&e-this.lastSmartLayoutCheckTs<=500)return;this.lastSmartLayoutCheckTs=e;let t=this.getLayoutSize();if(!t.w||!t.h)return;let n=this.computeAnchorBoxLayout(t);n.w&&n.h&&this.ensureSmartLayout(n)}getRenderState(e,t,n){let r=this.processTokens(e.tokens,n);this.lastWrapTokens=r;let i=this.stringifyTokens(r),a=i!==this.strTokens;a&&(this.releaseTooltip(),this.strTokens=i,this.resetTranslationContext(),this.resetWrapMemo());let o=`${t}:${i}`;return{tokens:r,tokensChanged:a,passedFlags:this.highlightWords?this.buildPassedState(r,n,o):null,renderKey:`${t}:${i}:${this.breakAfterTokenIndices.join(`,`)}`}}syncRenderedTokens(e){this.subtitlesContainer=this.subtitlesContainer??this.createSubtitlesContainer(),q(Tl` - ${this.renderTokens(s)} - `,this.subtitlesContainer);const y=this.subtitlesContainer.firstElementChild;this.subtitlesBlock=y instanceof HTMLElement&&y.classList.contains("vot-subtitles")?y:null,this.renderedTokenEls=this.subtitlesBlock?Array.from(this.subtitlesBlock.querySelectorAll('span[data-vot-token="1"]')):[],this.highlightWords&&d&&this.updatePassedClasses(d),c?(this.applyPositionAfterContentRender(),this.scheduleWrapRecompute(s),this.scheduleReposition()):this.maybeRefreshPosition();}release(){this.detachDragDocumentListeners(),this.stopVideoFrameLoop(),this.abortController.abort(),this.resizeObserver?.disconnect(),this.clearPendingSchedulerState(),this.checkerUnsubscribe?.(),this.checkerUnsubscribe=null,this.releaseTooltip(),this.subtitlesContainer&&(this.subtitlesContainer.remove(),this.subtitlesContainer=null),this.fullscreenLayerController.release(),this.safeAreaProbeEl&&(this.safeAreaProbeEl.remove(),this.safeAreaProbeEl=null),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;}}const Mg=/^<\s*(\/?)\s*([a-z0-9]+)(?:[.\s][^>]*)?>/iu,_g=/^\{([^}]*)\}/u,Og=/^(\s*)>>\s*/u,Dg=n=>{const t={};return n.italic&&(t.italic=true),n.bold&&(t.bold=true),n.underline&&(t.underline=true),Object.keys(t).length>0?t:void 0},Rg=(n,t)=>!!n?.italic==!!t?.italic&&!!n?.bold==!!t?.bold&&!!n?.underline==!!t?.underline,he=(n,t,e)=>{if(!t)return;const i=Dg(e),o=n.at(-1);if(o&&Rg(o.style,i)){o.text+=t;return}n.push({text:t,style:i});},Bg=(n,t)=>{const e=/^\\([ibu])([01])$/u.exec(n.trim());if(!e)return;const i=e[2]==="1";if(e[1]==="i"){t.italic=i;return}if(e[1]==="b"){t.bold=i;return}e[1]==="u"&&(t.underline=i);},Fg=n=>{for(const t of n){if(!t.text)continue;const e=t.text.replace(Og,"$1");if(t.text=e,e.length>0)break}for(;n[0]?.text==="";)n.shift();},_e=n=>{const t=[],e={italic:false,bold:false,underline:false};let i=0;for(;i({start:Math.max(0,h.start-a),end:Math.max(0,h.end-a),style:h.style})).filter(h=>h.end>h.start&&h.start({...h,end:Math.min(h.end,u.length)}));return {text:u,styledSpans:d}},sr=(n,t,e)=>!n?.length||e<=t?void 0:n.find(o=>to.start&&o.style)?.style,Ng="\uFEFF",$g=/\{[^}]*\}/gu,Ue=/^\s*(?\d{1,2}:\d{2}:\d{2}[,.]\d{1,3})\s*-->\s*(?\d{1,2}:\d{2}:\d{2}[,.]\d{1,3})\s*$/u,Mn=/^(?(?:\d{2}:)?\d{2}:\d{2}\.\d{3})\s+-->\s+(?(?:\d{2}:)?\d{2}:\d{2}\.\d{3})(?(?:[ \t]+.+)?)$/u,ki=n=>n.replace(Ng,"").replaceAll(/\r\n?/gu,` -`),Hg=n=>n.length>=3?n.slice(0,3):n.padEnd(3,"0"),nn=(n,t)=>{const e=n.trim().split(":");if(e.length<2||e.length>3)return null;const i=e.length===2?["00",...e]:e,[o,r,s]=i,[a,l="0"]=s.split(/[.,]/u);if(!/^\d+$/u.test(o)||!/^\d{2}$/u.test(r)||!/^\d{2}$/u.test(a)||!/^\d+$/u.test(l))return null;const c=Number(o),u=Number(r),d=Number(a),h=Number(Hg(l).slice(0,t));return !Number.isFinite(c)||!Number.isFinite(u)||!Number.isFinite(d)||u>59||d>59?null:((c*60+u)*60+d)*1e3+h},on=(n,{delimiter:t,allowOptionalHours:e,fractionDigits:i})=>{const o=Math.max(0,Math.round(n)),r=Math.floor(o/36e5),s=Math.floor(o%36e5/6e4),a=Math.floor(o%6e4/1e3),c=(o%1e3).toString().padStart(3,"0").slice(0,i);return `${e&&r===0?"":`${r.toString().padStart(2,"0")}:`}${s.toString().padStart(2,"0")}:${a.toString().padStart(2,"0")}${t}${c}`},ar=n=>{const t=Math.max(0,Math.round(n)),e=Math.floor(t/36e5),i=Math.floor(t%36e5/6e4),o=Math.floor(t%6e4/1e3),r=Math.floor(t%1e3/10);return `${e}:${i.toString().padStart(2,"0")}:${o.toString().padStart(2,"0")}.${r.toString().padStart(2,"0")}`},lr=n=>{const t=/^(?\d+):(?\d{2}):(?\d{2})\.(?\d{2})$/u.exec(n.trim());if(!t?.groups)return null;const e=Number(t.groups.hours),i=Number(t.groups.minutes),o=Number(t.groups.seconds),r=Number(t.groups.centiseconds);return i>59||o>59||!Number.isFinite(e)||!Number.isFinite(r)?null:((e*60+i)*60+o)*1e3+r*10},ne=n=>_e(n).text,bn=n=>n.sort((t,e)=>{const i=t.line.startMs-e.line.startMs;if(i!==0)return i;const o=t.line.startMs+Math.max(0,t.line.durationMs)-(e.line.startMs+Math.max(0,e.line.durationMs));return o!==0?o:t.index-e.index}).map(({line:t})=>t),Ug=n=>{let t=0,e=n.length;for(;tt&&n[e-1]==="";)e-=1;return n.slice(t,e)},zg=n=>{const t=n.trim();if(!t)return;const e={};for(const i of t.split(/\s+/u)){const o=i.indexOf(":");o<=0||(e[i.slice(0,o)]=i.slice(o+1));}return {raw:t,values:e}},Wg=n=>{const e=(/^\s*]+)?(?:\s+([^>]*?))?>/iu.exec(n)??/^\s*]*?)>/iu.exec(n))?.[1]?.trim();return e||void 0},qg=n=>ne(n),Gg=n=>ne(n),Kg=n=>ne(n),Yg=n=>{const e=ki(n).split(` -`),i=[],o=a=>{const l=e[a]?.trim()??"",c=e[a+1]?.trim()??"";return Ue.test(l)||/^\d+$/u.test(l)&&Ue.test(c)};let r=0,s=0;for(;r=e.length)break;let a=r;/^\d+$/u.test(e[r].trim())&&Ue.test(e[r+1]?.trim()??"")&&(a=r+1);const l=Ue.exec(e[a]?.trim()??"");if(!l?.groups){r+=1;continue}const c=nn(l.groups.start,3),u=nn(l.groups.end,3);if(c==null||u==null||u0&&o(r))break;d.push(e[r]),r+=1;}const h=Ug(d).join(` -`),f=_e(h);i.push({index:s,line:{text:qg(h),startMs:c,durationMs:Math.max(0,u-c),speakerId:"0",tokens:[],metadata:{rawText:h,styledSpans:f.styledSpans}}}),s+=1;}return {format:"srt",subtitles:bn(i)}},Ws=(n,t,e)=>n.format===e?t.metadata?.rawText??t.text:e==="ass"?t.text.replaceAll(` -`,"\\N"):t.text,Xg=n=>n.subtitles.map((t,e)=>{const i=Ws(n,t,"srt"),o=t.startMs+Math.max(0,t.durationMs);return [String(e+1),`${on(t.startMs,{delimiter:",",allowOptionalHours:false,fractionDigits:3})} --> ${on(o,{delimiter:",",allowOptionalHours:false,fractionDigits:3})}`,i].join(` + ${this.renderTokens(e)} + `,this.subtitlesContainer);let t=this.subtitlesContainer.firstElementChild;this.subtitlesBlock=t instanceof HTMLElement&&t.classList.contains(`vot-subtitles`)?t:null,this.renderedHighlightEls=this.subtitlesBlock?Array.from(this.subtitlesBlock.querySelectorAll(`span[data-vot-highlight-index]`)):[]}update(){if(!this.video||!this.subtitles)return;let e=this.resolvePlaybackTimeMs(),t=this.subtitles.subtitles,n=this.resolveActiveLine(e,t);if(!n){this.clearInactiveLineState();return}this.lastActiveLineKey=n.lineKey,this.refreshSmartLayoutIfNeeded();let{tokens:r,tokensChanged:i,passedFlags:a,renderKey:o}=this.getRenderState(n.line,n.lineKey,e);if(o===this.lastRenderKey){this.highlightWords&&!i&&a&&this.updatePassedClasses(a),this.maybeRefreshPosition();return}this.lastRenderKey=o,this.syncRenderedTokens(r),this.highlightWords&&a&&this.updatePassedClasses(a),i?(this.applyPositionAfterContentRender(),this.scheduleWrapRecompute(r),this.scheduleReposition()):this.maybeRefreshPosition()}release(){this.detachDragDocumentListeners(),this.stopVideoFrameLoop(),this.abortController.abort(),this.resizeObserver?.disconnect(),this.clearPendingSchedulerState(),this.checkerUnsubscribe?.(),this.checkerUnsubscribe=null,this.releaseTooltip(),this.subtitlesContainer&&=(this.subtitlesContainer.remove(),null),this.fullscreenLayerController.release(),this.safeAreaProbeEl&&=(this.safeAreaProbeEl.remove(),null),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=!1}},Md=/^<\s*(\/?)\s*([a-z0-9]+)([^>]*)>/iu,Nd=/^\{([^}]*)\}/u,Pd=/^(\s*)>>\s*/u,Fd=/(\d{1,2}:\d{2}(?::\d{2})?)(?=[\p{L}\p{M}])/gu,Id=/([\p{L}\p{M}]+)(\d+)|(\d+)([\p{L}\p{M}]+)/gu,Ld=/\\[^\\]+/gu,Rd=/^\\([ibu])([01])$/u,zd=/^\\(?:1?c|c)&H([0-9a-f]{6,8})&$/iu,Bd=/^\\r(?:[^\\}]*)?$/u,Vd=e=>({italic:e.italic,bold:e.bold,underline:e.underline,color:e.color,classes:[...e.classes]}),Hd=(e,t)=>{e.italic=t.italic,e.bold=t.bold,e.underline=t.underline,e.color=t.color,e.classes=[...t.classes]},Ud=e=>{e.italic=!1,e.bold=!1,e.underline=!1,e.color=void 0,e.classes=[]},Wd=e=>ku({italic:e.italic,bold:e.bold,underline:e.underline,color:e.color,classes:e.classes}),Gd=(e,t,n)=>{if(!t)return;let r=Wd(n),i=e.at(-1);if(i&&ju(i.style,r)){i.text+=t;return}e.push({text:t,style:r})},Kd=e=>{let t=e.trim();if(!/^[0-9a-f]{6,8}$/iu.test(t))return;let n=t.slice(-6),r=n.slice(0,2),i=n.slice(2,4);return Ou(`#${n.slice(4,6)}${i}${r}`)},qd=(e,t)=>{let n=Rd.exec(e.trim());if(n){let e=n[2]===`1`;if(n[1]===`i`){t.italic=e;return}if(n[1]===`b`){t.bold=e;return}if(n[1]===`u`){t.underline=e;return}}if(Bd.test(e.trim())){Ud(t);return}let r=zd.exec(e.trim());r&&(t.color=Kd(r[1]))},Jd=(e,t)=>{let n=e.match(Ld)??[];for(let e of n)qd(e,t)},Yd=e=>{for(let t of e){if(!t.text)continue;let e=t.text.replace(Pd,`$1`);if(t.text=e,e.length>0)break}for(;e[0]?.text===``;)e.shift()},Xd=e=>{for(let t of e)t.text&&=t.text.replaceAll(Fd,`$1 `)},Zd=e=>{for(let t of e)t.text&&=t.text.replaceAll(Id,(e,t,n,r,i)=>{let a=t??i??``,o=n??r??``;return/^[A-Za-z]{1,3}$/u.test(a)||a.length===1&&a===a.toLocaleUpperCase()&&a!==a.toLocaleLowerCase()?e:t?`${a} ${o}`:`${o} ${a}`})},Qd=e=>{let t=e.trim();if(!t.startsWith(`.`))return;let n=t.split(/\s+/u,1)[0].split(`.`).filter(Boolean);return n.length?n:void 0},$d=e=>{let t=/\bcolor\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s>]+))/iu.exec(e),n=t?.[1]??t?.[2]??t?.[3];return n?Ou(n):void 0},ef=(e,t,n)=>{for(let r=t.length-1;r>=0;--r){if(t[r].tagName!==e)continue;let[i]=t.splice(r,1);if(!i)return;Hd(n,i.previousStyle);return}if(e===`b`){n.bold=!1;return}if(e===`i`){n.italic=!1;return}if(e===`u`){n.underline=!1;return}if(e===`font`){n.color=void 0;return}e===`c`&&(n.classes=[])},tf=(e,t,n,r)=>{let i=e[1]===`/`,a=e[2].toLowerCase(),o=e[3]??``;if(a===`br`){Gd(t,` +`,n);return}if(i){ef(a,r,n);return}if([`b`,`i`,`u`,`font`,`c`].includes(a)){if(r.push({tagName:a,previousStyle:Vd(n)}),a===`b`){n.bold=!0;return}if(a===`i`){n.italic=!0;return}if(a===`u`){n.underline=!0;return}if(a===`font`){let e=$d(o);e&&(n.color=e);return}n.classes=Qd(o)??[]}},nf=(e,t,n,r,i)=>{let a=e.slice(t);if(a.startsWith(`\\N`)||a.startsWith(`\\n`))return Gd(n,` +`,r),t+2;if(a.startsWith(`\\h`))return Gd(n,` `,r),t+2;if(a[0]===` +`)return Gd(n,` +`,r),t+1;let o=Nd.exec(a);if(o)return Jd(o[1],r),t+o[0].length;let s=Md.exec(a);return s?(tf(s,n,r,i),t+s[0].length):null},rf=e=>{let t=[],n=``;for(let r of e){if(!r.text)continue;let e=n.length;n+=r.text,t.push({start:e,end:n.length,style:r.style})}return{text:n,styledSpans:t}},af=(e,t)=>{let n=e.replaceAll(`\xA0`,` `),r=/^\s*/u.exec(n)?.[0].length??0,i=/\s*$/u.exec(n)?.[0].length??0,a=Math.max(r,n.length-i),o=n.slice(r,a);return{text:o,styledSpans:t.map(e=>({start:Math.max(0,e.start-r),end:Math.max(0,e.end-r),style:e.style})).filter(e=>e.end>e.start&&e.start({...e,end:Math.min(e.end,o.length)}))}},of=e=>{let t=[],n={italic:!1,bold:!1,underline:!1,color:void 0,classes:[]},r=[],i=0;for(;i{if(!(!e?.length||n<=t))return e.find(e=>te.start&&e.style)?.style},cf=``,lf=/\{[^}]*\}/gu,uf=/^\s*(?\d{1,2}:\d{2}:\d{2}[,.]\d{1,3})\s*-->\s*(?\d{1,2}:\d{2}:\d{2}[,.]\d{1,3})\s*$/u,df=/^(?(?:\d{2}:)?\d{2}:\d{2}\.\d{3})\s+-->\s+(?(?:\d{2}:)?\d{2}:\d{2}\.\d{3})(?(?:[ \t]+.+)?)$/u,ff=e=>e.replace(cf,``).replaceAll(/\r\n?/gu,` +`),pf=e=>e.length>=3?e.slice(0,3):e.padEnd(3,`0`),mf=(e,t)=>{let n=e.trim().split(`:`);if(n.length<2||n.length>3)return null;let[r,i,a]=n.length===2?[`00`,...n]:n,[o,s=`0`]=a.split(/[.,]/u);if(!/^\d+$/u.test(r)||!/^\d{2}$/u.test(i)||!/^\d{2}$/u.test(o)||!/^\d+$/u.test(s))return null;let c=Number(r),l=Number(i),u=Number(o),d=Number(pf(s).slice(0,t));return!Number.isFinite(c)||!Number.isFinite(l)||!Number.isFinite(u)||l>59||u>59?null:((c*60+l)*60+u)*1e3+d},hf=(e,{delimiter:t,allowOptionalHours:n,fractionDigits:r})=>{let{hours:i,minutes:a,seconds:o,milliseconds:s}=gf(e,Math.round),c=s.toString().padStart(3,`0`).slice(0,r);return`${n&&i===0?``:`${i.toString().padStart(2,`0`)}:`}${a.toString().padStart(2,`0`)}:${o.toString().padStart(2,`0`)}${t}${c}`},gf=(e,t)=>{let n=Math.max(0,t(e));return{hours:Math.floor(n/36e5),minutes:Math.floor(n%36e5/6e4),seconds:Math.floor(n%6e4/1e3),milliseconds:n%1e3}},_f=e=>{let{hours:t,minutes:n,seconds:r,milliseconds:i}=gf(e,Math.round),a=Math.floor(i/10);return`${t}:${n.toString().padStart(2,`0`)}:${r.toString().padStart(2,`0`)}.${a.toString().padStart(2,`0`)}`},vf=e=>{let t=/^(?\d+):(?\d{2}):(?\d{2})\.(?\d{2})$/u.exec(e.trim());if(!t?.groups)return null;let n=Number(t.groups.hours),r=Number(t.groups.minutes),i=Number(t.groups.seconds),a=Number(t.groups.centiseconds);return r>59||i>59||!Number.isFinite(n)||!Number.isFinite(a)?null:((n*60+r)*60+i)*1e3+a*10},yf=e=>of(e).text,bf=e=>e.startMs+Math.max(0,e.durationMs),xf=(e,t)=>Math.max(0,t-e),Sf=e=>e.sort((e,t)=>{let n=e.line.startMs-t.line.startMs;if(n!==0)return n;let r=bf(e.line)-bf(t.line);return r===0?e.index-t.index:r}).map(({line:e})=>e),Cf=e=>{let t=0,n=e.length;for(;tt&&e[n-1]===``;)--n;return e.slice(t,n)},wf=e=>{let t=e.trim();if(!t)return;let n={};for(let e of t.split(/\s+/u)){let t=e.indexOf(`:`);t<=0||(n[e.slice(0,t)]=e.slice(t+1))}return{raw:t,values:n}},Tf=e=>(/^\s*]+)?(?:\s+([^>]*?))?>/iu.exec(e)??/^\s*]*?)>/iu.exec(e))?.[1]?.trim()||void 0,Ef=(e,t)=>{let n=e[t]?.trim()??``,r=e[t+1]?.trim()??``;return uf.test(n)||/^\d+$/u.test(n)&&uf.test(r)},Df=(e,t)=>/^\d+$/u.test(e[t]?.trim()??``)&&uf.test(e[t+1]?.trim()??``)?t+1:t,Of=(e,t)=>{let n=uf.exec(e[t]?.trim()??``);if(!n?.groups)return null;let r=mf(n.groups.start,3),i=mf(n.groups.end,3);return r==null||i==null||i{let n=t,r=[];for(;n0&&Ef(e,n))break;r.push(e[n]),n+=1}return{rawText:Cf(r).join(` +`),nextCursor:n}},Af=(e,{rawText:t,startMs:n,endMs:r,speakerId:i,displayModel:a,metadata:o})=>({index:e,line:{text:a.text,startMs:n,durationMs:xf(n,r),speakerId:i,tokens:[],metadata:{rawText:t,styledSpans:a.styledSpans,...o}}}),jf=()=>({format:`vtt`,subtitles:[],metadata:{vtt:{headerText:``,blocks:[]}}}),Mf=e=>e.startsWith(`NOTE`)||e===`STYLE`||e===`REGION`,Nf=(e,t)=>{let n=[],r=t;for(;r!df.test(e[t]??``)&&df.test(e[t+1]??``)?{cueId:e[t],timingCursor:t+1}:{cueId:void 0,timingCursor:t},Ff=e=>{let t=df.exec(e);if(!t?.groups)return null;let n=mf(t.groups.start,3),r=mf(t.groups.end,3);return n==null||r==null||r{let n=[],r=t;for(;re.slice(7).split(`,`).map(e=>e.trim()),Rf=(e,t)=>e.length||!t.eventFormat?e:Lf(t.eventFormat),zf=(e,t)=>{let n=Xf(t,e.length);return Object.fromEntries(e.map((e,t)=>[e,n[t]??``]))},Bf=(e,t)=>{if(t.startsWith(`Format:`)){e.styleFormat=t;return}if(t.startsWith(`Style:`)){e.styleLines.push(t);return}e.preEventLines.push(t)},Vf=(e,t)=>{let n=vf(t.Start??``),r=vf(t.End??``);if(n==null||r==null||r{if(t.startsWith(`Format:`))return{eventFields:Lf(t),cue:null};if(!t.includes(`:`))return e.preEventLines.push(t),{eventFields:r,cue:null};let i=t.indexOf(`:`),a=t.slice(0,i).trim(),o=t.slice(i+1).trim(),s=Rf(r,e),c=zf(s,o);return a===`Comment`?(e.commentLines.push(t),{eventFields:s,cue:null}):a===`Dialogue`?{eventFields:s,cue:Vf(n,c)}:(e.preEventLines.push(t),{eventFields:s,cue:null})},Uf=e=>{let t=ff(e).split(` +`),n=[],r=0,i=0;for(;r=t.length)break;let e=Df(t,r),a=Of(t,e);if(!a){r+=1;continue}let o=kf(t,e+1);r=o.nextCursor;let s=o.rawText,c=of(s);n.push(Af(i,{rawText:s,startMs:a.startMs,endMs:a.endMs,speakerId:`0`,displayModel:c})),i+=1}return{format:`srt`,subtitles:Sf(n)}},Wf=(e,t,n)=>e.format===n?t.metadata?.rawText??t.text:n===`ass`?t.text.replaceAll(` +`,`\\N`):t.text,Gf=e=>e.subtitles.map((t,n)=>{let r=Wf(e,t,`srt`),i=bf(t);return[String(n+1),`${hf(t.startMs,{delimiter:`,`,allowOptionalHours:!1,fractionDigits:3})} --> ${hf(i,{delimiter:`,`,allowOptionalHours:!1,fractionDigits:3})}`,r].join(` `)}).join(` -`),Jg=(n,t,e)=>{n.push({cueIndex:t,lines:e});},jg=n=>{const e=ki(n).split(` -`),i=e[0]??"";if(!i.startsWith("WEBVTT"))return {format:"vtt",subtitles:[],metadata:{vtt:{headerText:"",blocks:[]}}};const o={headerText:i.slice(6).trim(),blocks:[]},r=[];let s=1;for(;s=e.length)break;if(e[s].startsWith("NOTE")||e[s]==="STYLE"||e[s]==="REGION"){const y=[];for(;s{const t=n.startMs+Math.max(0,n.durationMs),e=n.metadata?.vtt?.settings?.raw;return `${on(n.startMs,{delimiter:".",allowOptionalHours:true,fractionDigits:3})} --> ${on(t,{delimiter:".",allowOptionalHours:true,fractionDigits:3})}${e?` ${e}`:""}`},Qg=n=>{const t=n.metadata?.vtt,e=[`WEBVTT${t?.headerText?` ${t.headerText}`:""}`],i=new Map;for(const r of t?.blocks??[]){const s=i.get(r.cueIndex)??[];s.push(r.lines),i.set(r.cueIndex,s);}const o=r=>{for(const s of i.get(r)??[])e.push(s.join(` -`));};return o(0),n.subtitles.forEach((r,s)=>{const a=r.metadata?.vtt?.cueId,l=[...a?[a]:[],Zg(r),(n.format==="vtt"?r.metadata?.vtt?.rawPayload:r.text.split(` +`),Kf=(e,t,n)=>{e.push({cueIndex:t,lines:n})},qf=e=>{let t=ff(e).split(` +`),n=t[0]??``;if(!n.startsWith(`WEBVTT`))return jf();let r={headerText:n.slice(6).trim(),blocks:[]},i=[],a=1;for(;a=t.length)break;if(Mf(t[a])){let e=Nf(t,a);a=e.nextCursor,Kf(r.blocks,i.length,e.blockLines);continue}let e=Pf(t,a),n=Ff(t[e.timingCursor]??``);if(!n){a+=1;continue}let o=If(t,e.timingCursor+1);a=o.nextCursor;let s=o.payloadLines,c=s.join(` +`),l=of(c),u=Tf(c);i.push({index:i.length,line:Af(i.length,{rawText:c,startMs:n.startMs,endMs:n.endMs,speakerId:u??`0`,displayModel:l,metadata:{vtt:{cueId:e.cueId,settings:wf(n.settingsRaw),voice:u,rawPayload:s}}}).line})}return{format:`vtt`,subtitles:Sf(i),metadata:{vtt:r}}},Jf=e=>{let t=bf(e),n=e.metadata?.vtt?.settings?.raw,r=n?` ${n}`:``;return`${hf(e.startMs,{delimiter:`.`,allowOptionalHours:!0,fractionDigits:3})} --> ${hf(t,{delimiter:`.`,allowOptionalHours:!0,fractionDigits:3})}${r}`},Yf=e=>{let t=e.metadata?.vtt,n=[`WEBVTT${t?.headerText?` ${t.headerText}`:``}`],r=new Map;for(let e of t?.blocks??[]){let t=r.get(e.cueIndex)??[];t.push(e.lines),r.set(e.cueIndex,t)}let i=e=>{for(let t of r.get(e)??[])n.push(t.join(` +`))};return i(0),e.subtitles.forEach((t,r)=>{let a=t.metadata?.vtt?.cueId,o=[...a?[a]:[],Jf(t),(e.format===`vtt`?t.metadata?.vtt?.rawPayload:t.text.split(` `))?.join(` -`)??r.text];e.push(l.join(` -`)),o(s+1);}),e.filter(Boolean).join(` +`)??t.text];n.push(o.join(` +`)),i(r+1)}),n.filter(Boolean).join(` -`)},tm=(n,t)=>{if(t<=1)return [n];const e=[];let i=0;for(let o=0;o({scriptInfoLines:[],styleFormat:"",styleLines:[],eventFormat:"",preEventLines:[],commentLines:[]}),nm=(n="Exported subtitles")=>({scriptInfoLines:[`Title: ${n}`,"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:[]}),im=n=>{const e=ki(n).split(` -`),i=em(),o=[];let r="",s=[];return e.forEach((a,l)=>{const c=a.trim();if(!c)return;const u=/^\[(.+)\]$/u.exec(c);if(u){r=u[1];return}if(r==="Script Info"){i.scriptInfoLines.push(a);return}if(r==="V4+ Styles"||r==="V4 Styles"){if(a.startsWith("Format:")){i.styleFormat=a;return}if(a.startsWith("Style:")){i.styleLines.push(a);return}i.preEventLines.push(a);return}if(r==="Events"){if(a.startsWith("Format:")){i.eventFormat=a,s=a.slice(7).split(",").map(D=>D.trim());return}if(!a.includes(":")){i.preEventLines.push(a);return}const d=a.indexOf(":"),h=a.slice(0,d).trim(),f=a.slice(d+1).trim();!s.length&&i.eventFormat&&(s=i.eventFormat.slice(7).split(",").map(D=>D.trim()));const g=tm(f,s.length),y=Object.fromEntries(s.map((D,F)=>[D,g[F]??""]));if(h==="Comment"){i.commentLines.push(a);return}if(h!=="Dialogue"){i.preEventLines.push(a);return}const w=lr(y.Start??""),x=lr(y.End??"");if(w==null||x==null||x{const t=n.metadata?.ass,e=n.startMs+Math.max(0,n.durationMs),i=t?.rawText??n.metadata?.rawText??n.text.replaceAll(` -`,"\\N");return [t?.kind==="comment"?"Comment":"Dialogue",": ",[t?.layer??"0",ar(n.startMs),ar(e),t?.style??"Default",t?.name??"",t?.marginL??"0",t?.marginR??"0",t?.marginV??"0",t?.effect??"",i].join(",")].join("")},rm=(n,t)=>{if(!t)return n;const e=`Title: ${t}`,i=[...n.scriptInfoLines],o=i.findIndex(r=>r.startsWith("Title:"));return o>=0?i[o]==="Title: Exported subtitles"&&(i[o]=e):i.unshift(e),{...n,scriptInfoLines:i}},sm=(n,t)=>{const e=n.metadata?.ass,i=e&&e.scriptInfoLines.length>0&&e.styleFormat&&e.styleLines.length>0&&e.eventFormat?e:nm(t?.assTitle),o=rm(i,t?.assTitle);return ["[Script Info]",...o.scriptInfoLines,"","[V4+ Styles]",o.styleFormat,...o.styleLines,...o.preEventLines,"","[Events]",o.eventFormat,...o.commentLines,...n.subtitles.map(s=>om({...s,metadata:n.format==="ass"?s.metadata:{...s.metadata,rawText:Ws(n,s,"ass")}}))].join(` -`).trim()},am=(n,t)=>t==="srt"?Yg(n):t==="vtt"?jg(n):im(n),cr=n=>({...n,subtitles:bn(n.subtitles.map((t,e)=>({index:e,line:t})))}),lm=n=>{const t=n.subtitles.map(e=>({text:e.text,startMs:e.startMs,durationMs:e.durationMs,speakerId:e.speakerId,tokens:e.tokens.map(i=>({text:i.text,startMs:i.startMs,durationMs:i.durationMs}))}));return {containsTokens:t.some(e=>e.tokens.length>0),subtitles:t}},cm=(n,t,e)=>t==="json"?lm(n):t==="srt"?Xg(n):t==="vtt"?Qg(n):sm(n,e);function um(n){return new Uint8Array([n>>>24&255,n>>>16&255,n>>>8&255,n&255])}function dm(n){return new Uint8Array([n>>>21&127,n>>>14&127,n>>>7&127,n&127])}function hm(n,t){const e=new TextEncoder().encode(t),i=new Uint8Array(e.length+1);i[0]=3,i.set(e,1);const o=new Uint8Array(10+i.length);o.set([84,73,84,50],0),o.set(um(i.length),4),o.set(i,10);const r=new Uint8Array(10);r.set([73,68,51,3,0,0],0),r.set(dm(o.length),6);const s=new Uint8Array(n),a=new Uint8Array(r.length+o.length+s.length);return a.set(r,0),a.set(o,r.length),a.set(s,r.length+o.length),new Blob([a],{type:"audio/mpeg"})}async function fm(n,t){const e=Number(n.headers.get("Content-Length")??0);if(!n.body)return n.arrayBuffer();const i=n.body.getReader();let o=0,r=e>0?new Uint8Array(e):null;const s=[];for(;;){const{done:c,value:u}=await i.read();if(c)break;if(!(!u||u.byteLength===0)){if(r){const d=o+u.byteLength;if(d>r.length){const h=new Uint8Array(Math.max(d,r.length*2));h.set(r.subarray(0,o)),r=h;}r.set(u,o),o=d;}else s.push(u),o+=u.byteLength;e>0&&t($(Math.round(o/e*100)));}}if(r)return r.buffer.slice(0,o);const a=new Uint8Array(o);let l=0;for(const c of s)a.set(c,l),l+=c.byteLength;return a.buffer}async function pm(n,t,e=()=>{},i={}){const o=await gm(n,t,e);return await ds(o,`${t}.mp3`,i)}async function gm(n,t,e=()=>{}){const i=await fm(n,e);return e(100),hm(i,t)}function qs(n,t){return n.root===t.root&&n.portalContainer===t.portalContainer&&n.subtitlesMountContainer===t.subtitlesMountContainer&&n.tooltipLayoutRoot===t.tooltipLayoutRoot}function mm(n,t,e){return qs(n,t)?n:(e(t),t)}function vm(n,t){return n.root!==t.root||n.tooltipLayoutRoot!==t.tooltipLayoutRoot}const bm=rt` +`)},Xf=(e,t)=>{if(t<=1)return[e];let n=[],r=0;for(let i=0;i({scriptInfoLines:[],styleFormat:``,styleLines:[],eventFormat:``,preEventLines:[],commentLines:[]}),Qf=(e=`Exported subtitles`)=>({scriptInfoLines:[`Title: ${e}`,`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:[]}),$f=e=>{let t=ff(e).split(` +`),n=Zf(),r=[],i=``,a=[];return t.forEach((e,t)=>{let o=e.trim();if(!o)return;let s=/^\[(.+)\]$/u.exec(o);if(s){i=s[1];return}if(i===`Script Info`){n.scriptInfoLines.push(e);return}if(i===`V4+ Styles`||i===`V4 Styles`){Bf(n,e);return}if(i===`Events`){e.startsWith(`Format:`)&&(n.eventFormat=e);let i=Hf(n,e,t,a);a=i.eventFields,i.cue&&r.push(i.cue)}}),{format:`ass`,subtitles:Sf(r),metadata:{ass:n}}},ep=e=>{let t=e.metadata?.ass,n=bf(e),r=t?.rawText??e.metadata?.rawText??e.text.replaceAll(` +`,`\\N`);return[t?.kind===`comment`?`Comment`:`Dialogue`,`: `,[t?.layer??`0`,_f(e.startMs),_f(n),t?.style??`Default`,t?.name??``,t?.marginL??`0`,t?.marginR??`0`,t?.marginV??`0`,t?.effect??``,r].join(`,`)].join(``)},tp=(e,t)=>{if(!t)return e;let n=`Title: ${t}`,r=[...e.scriptInfoLines],i=r.findIndex(e=>e.startsWith(`Title:`));return i>=0?r[i]===`Title: Exported subtitles`&&(r[i]=n):r.unshift(n),{...e,scriptInfoLines:r}},np=(e,t)=>{let n=e.metadata?.ass,r=tp(n&&n.scriptInfoLines.length>0&&n.styleFormat&&n.styleLines.length>0&&n.eventFormat?n:Qf(t?.assTitle),t?.assTitle);return[`[Script Info]`,...r.scriptInfoLines,``,`[V4+ Styles]`,r.styleFormat,...r.styleLines,...r.preEventLines,``,`[Events]`,r.eventFormat,...r.commentLines,...e.subtitles.map(t=>ep({...t,metadata:e.format===`ass`?t.metadata:{...t.metadata,rawText:Wf(e,t,`ass`)}}))].join(` +`).trim()},rp=(e,t)=>t===`srt`?Uf(e):t===`vtt`?qf(e):$f(e),ip=e=>({...e,subtitles:Sf(e.subtitles.map((e,t)=>({index:t,line:e})))}),ap=e=>{let t=e.subtitles.map(e=>({text:e.text,startMs:e.startMs,durationMs:e.durationMs,speakerId:e.speakerId,tokens:e.tokens.map(e=>({text:e.text,startMs:e.startMs,durationMs:e.durationMs}))}));return{containsTokens:t.some(e=>e.tokens.length>0),subtitles:t}},op=(e,t,n)=>t===`json`?ap(e):t===`srt`?Gf(e):t===`vtt`?Yf(e):np(e,n);function sp(e){return new Uint8Array([e>>>24&255,e>>>16&255,e>>>8&255,e&255])}function cp(e){return new Uint8Array([e>>>21&127,e>>>14&127,e>>>7&127,e&127])}function lp(e,t){let n=new TextEncoder().encode(t),r=new Uint8Array(n.length+1);r[0]=3,r.set(n,1);let i=new Uint8Array(10+r.length);i.set([84,73,84,50],0),i.set(sp(r.length),4),i.set(r,10);let a=new Uint8Array(10);a.set([73,68,51,3,0,0],0),a.set(cp(i.length),6);let o=new Uint8Array(e),s=new Uint8Array(a.length+i.length+o.length);return s.set(a,0),s.set(i,a.length),s.set(o,a.length+i.length),new Blob([s],{type:`audio/mpeg`})}function up(e,t,n){let r=n+t.byteLength,i=e;if(r>i.length){let e=new Uint8Array(Math.max(r,i.length*2));e.set(i.subarray(0,n)),i=e}return i.set(t,n),{out:i,loaded:r}}function dp(e,t){let n=new Uint8Array(t),r=0;for(let t of e)n.set(t,r),r+=t.byteLength;return n.buffer}async function fp(e,t){let n=Number(e.headers.get(`Content-Length`)??0);if(!e.body)return e.arrayBuffer();let r=e.body.getReader(),i=0,a=n>0?new Uint8Array(n):null,o=[];for(;;){let{done:e,value:s}=await r.read();if(e)break;if(!(!s||s.byteLength===0)){if(a){let e=up(a,s,i);a=e.out,i=e.loaded}else o.push(s),i+=s.byteLength;n>0&&t(R(Math.round(i/n*100)))}}return a?a.buffer.slice(0,i):dp(o,i)}async function pp(e,t,n=()=>{},r={}){return await Yi(await mp(e,t,n),`${t}.mp3`,r)}async function mp(e,t,n=()=>{}){let r=await fp(e,n);return n(100),lp(r,t)}function hp(e,t){return e.root===t.root&&e.portalContainer===t.portalContainer&&e.subtitlesMountContainer===t.subtitlesMountContainer&&e.tooltipLayoutRoot===t.tooltipLayoutRoot}function gp(e,t,n){return hp(e,t)?e:(n(t),t)}function _p(e,t){return e.root!==t.root||e.tooltipLayoutRoot!==t.tooltipLayoutRoot}function vp(e){return e instanceof Error&&e.name===`AbortError`}async function yp(e){if(!e.videoData?.videoId||(bp(e)&&(e.videoData=await e.getVideoData()),!e.videoData?.videoId))throw new U(`VOTNoVideoIDFound`);return e.videoData}function bp(e){return e.site.host===`vk`&&e.site.additionalData===`clips`||e.site.host===`douyin`}async function xp(e){let t=e.videoHandler;if(t){if(L.log(`[handleTranslationBtnClick] click translationBtn`),t.hasActiveSource()){L.log(`[handleTranslationBtnClick] video has active source`),await t.stopTranslation();return}if(e.currentStatus===`error`&&!e.currentLoading&&e.transformBtn(`none`,V.get(`translateVideo`)),e.currentStatus!==`none`||e.currentLoading){L.log(`[handleTranslationBtnClick] translationBtn isn't in none state`),t.actionsAbortController.abort(),await t.stopTranslation();return}try{L.log(`[handleTranslationBtnClick] trying execute translation`);let e=await yp(t);await t.videoManager.ensureDetectedLanguageForTranslation(e),L.log(`[handleTranslationBtnClick] Run translateFunc`,e.videoId),await t.translateFunc(e.videoId,e.isStream,e.detectedLanguage,e.responseLanguage,e.translationHelp)}catch(t){if(vp(t)){e.transformBtn(`none`,V.get(`translateVideo`));return}if(console.error(`[VOT]`,t),!(t instanceof Error)){e.transformBtn(`error`,String(t));return}let n=t.name===`VOTLocalizedError`?t.localizedMessage:t.message;e.transformBtn(`error`,n)}}}var Sp=G` -`,ym=rt` +`,Cp=G` -`,wm=rt` +`,wp=G` -`,Sm=rt` +`,Tp=G` -`,xm=rt` +`,Ep=G` -`,km=rt` +`,Dp=G` -`,Gs=rt` +`,Op=G` -`,Tm=rt` +`,kp=G` -`,Lm=rt` +`,Ap=G` -`,ur=rt` +`,jp=G` -`,dr=rt` +`,Mp=G` -`,Am=rt` +`,Np=G` -`,Im=rt` +`,Pp=G` - `;function At(n,t,e){n[t].addListener(e);}function It(n,t,e){n[t].removeListener(e);}function st(n,t){n.hidden=t;}function at(n){return n.hidden}class Cm{button;loaderMain;loaderCircle;onClick=new N;events={click:this.onClick};_progress=0;constructor(){const t=this.createElements();this.button=t.button,this.loaderMain=t.loaderMain,this.loaderCircle=t.loaderCircle,this.progress=0;}createElements(){const t=S.createIconButton(Sm,{ariaLabel:"Download translation"}),e=t.querySelector(".vot-loader-main");if(!e)throw new Error("[VOT] DownloadButton loader main element not found");const i=t.querySelector(".vot-loader-progress");if(!i)throw new Error("[VOT] DownloadButton loader circle element not found");return t.addEventListener("click",()=>{this.onClick.dispatch();}),{button:t,loaderMain:e,loaderCircle:i}}addEventListener(t,e){return At(this.events,"click",e),this}removeEventListener(t,e){return It(this.events,"click",e),this}get progress(){return this._progress}set progress(t){const e=Em(t);this._progress=e;const i=this.getCircleCircumference();this.loaderCircle.style.strokeDasharray=`${i}`;const o=i*(1-e/100);this.loaderCircle.style.strokeDashoffset=`${o}`,this.loaderMain.style.opacity=e===0?"1":"0",this.loaderCircle.style.opacity=e===0?"0":"1";}getCircleCircumference(){const t=this.loaderCircle.r?.baseVal?.value??0;return 2*Math.PI*t}set hidden(t){st(this.button,t);}get hidden(){return at(this.button)}}function Em(n){if(!Number.isFinite(n))return 0;const t=n<1?n*100:n;return Math.max(0,Math.min(100,Math.round(t)))}class pt{container;icon;text;_labelText;_icon;constructor({labelText:t,icon:e}){this._labelText=t,this._icon=e;const i=this.createElements();this.container=i.container,this.icon=i.icon,this.text=i.text;}createElements(){const t=S.createEl("vot-block",["vot-label"]),e=S.createEl("span",["vot-label-text"]);e.textContent=this._labelText;const i=S.createEl("span",["vot-label-icon"]);return this._icon?it(this._icon,i):i.hidden=true,t.append(e,i),{container:t,icon:i,text:e}}set hidden(t){st(this.container,t);}get hidden(){return at(this.container)}}class Zn{container;backdrop;box;contentWrapper;headerContainer;titleContainer;title;closeButton;bodyContainer;footerContainer;onClose=new N;events={close:this.onClose};previouslyFocused=null;keydownListener;adaptiveAlignObserver;adaptiveAlignRaf=null;handleViewportChange=()=>{this.scheduleAdaptiveVerticalAlign();};titleId=typeof crypto<"u"&&"randomUUID"in crypto?crypto.randomUUID():`vot-dialog-title-${Math.random().toString(36).slice(2)}`;_titleHtml;_isTemp;constructor({titleHtml:t,isTemp:e=false}){this._titleHtml=t,this._isTemp=e;const i=this.createElements();this.container=i.container,this.backdrop=i.backdrop,this.box=i.box,this.contentWrapper=i.contentWrapper,this.headerContainer=i.headerContainer,this.titleContainer=i.titleContainer,this.title=i.title,this.closeButton=i.closeButton,this.bodyContainer=i.bodyContainer,this.footerContainer=i.footerContainer;}createElements(){const t=S.createEl("vot-block",["vot-dialog-container"]);this._isTemp&&t.classList.add("vot-dialog-temp"),t.hidden=!this._isTemp,t.setAttribute("aria-hidden",t.hidden?"true":"false"),t.toggleAttribute("inert",t.hidden);const e=S.createEl("vot-block",["vot-dialog-backdrop"]),i=S.createEl("vot-block",["vot-dialog"]);i.dataset.verticalAlign="center",i.setAttribute("role","dialog"),i.setAttribute("aria-modal","true"),i.tabIndex=-1;const o=S.createEl("vot-block",["vot-dialog-content-wrapper"]),r=S.createEl("vot-block",["vot-dialog-header-container"]),s=S.createEl("vot-block",["vot-dialog-title-container"]),a=S.createEl("vot-block",["vot-dialog-title"]);a.id=this.titleId,a.append(this._titleHtml),s.appendChild(a),i.setAttribute("aria-labelledby",this.titleId);const l=S.createIconButton(Lm,{ariaLabel:"Close"});l.classList.add("vot-dialog-close-button"),e.addEventListener("click",()=>{this.close();}),l.addEventListener("click",()=>{this.close();}),r.append(s,l);const c=S.createEl("vot-block",["vot-dialog-body-container"]),u=S.createEl("vot-block",["vot-dialog-footer-container"]);return o.append(r,c,u),i.appendChild(o),t.append(e,i),i.addEventListener("click",d=>{d.stopPropagation();}),{container:t,backdrop:e,box:i,contentWrapper:o,headerContainer:r,titleContainer:s,title:a,closeButton:l,bodyContainer:c,footerContainer:u}}addEventListener(t,e){return At(this.events,"close",e),this}removeEventListener(t,e){return It(this.events,"close",e),this}open(){return this.previouslyFocused??=document.activeElement,this.hidden=false,this.attachKeydownTrap(),this.attachAdaptiveVerticalAlign(),queueMicrotask(()=>this.focusFirst()),this}remove(){return this.detachAdaptiveVerticalAlign(),this.detachKeydownTrap(),this.container.remove(),this.restoreFocus(),this.onClose.dispatch(),this}close(){return this._isTemp?this.remove():(this.detachAdaptiveVerticalAlign(),this.detachKeydownTrap(),this.hidden=true,this.restoreFocus(),this.onClose.dispatch(),this)}attachAdaptiveVerticalAlign(){if(this.adaptiveAlignObserver){this.scheduleAdaptiveVerticalAlign();return}typeof ResizeObserver<"u"&&(this.adaptiveAlignObserver=new ResizeObserver(()=>{this.scheduleAdaptiveVerticalAlign();}),this.adaptiveAlignObserver.observe(this.contentWrapper)),globalThis.addEventListener("resize",this.handleViewportChange,{passive:true}),globalThis.visualViewport&&(globalThis.visualViewport.addEventListener("resize",this.handleViewportChange,{passive:true}),globalThis.visualViewport.addEventListener("scroll",this.handleViewportChange,{passive:true})),this.scheduleAdaptiveVerticalAlign();}detachAdaptiveVerticalAlign(){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),this.adaptiveAlignRaf!==null&&(cancelAnimationFrame(this.adaptiveAlignRaf),this.adaptiveAlignRaf=null);}scheduleAdaptiveVerticalAlign(){this.adaptiveAlignRaf!==null&&cancelAnimationFrame(this.adaptiveAlignRaf),this.adaptiveAlignRaf=requestAnimationFrame(()=>{this.adaptiveAlignRaf=null,this.updateAdaptiveVerticalAlign();});}updateAdaptiveVerticalAlign(){const t=globalThis.visualViewport?.height??globalThis.innerHeight;if(!t||t<=0)return;const e=16,i=Math.max(160,Math.round(t*.75)),o=Math.max(160,Math.round(t-e*2)),r=this.contentWrapper.scrollHeight,s=this.box.dataset.verticalAlign==="top",a=i-8,l=Math.round(t*.6);(s?r>l:r>=a)?(this.box.dataset.verticalAlign="top",this.box.style.setProperty("--vot-dialog-max-height",`${o}px`)):(this.box.dataset.verticalAlign="center",this.box.style.setProperty("--vot-dialog-max-height",`${i}px`));}restoreFocus(){const t=this.previouslyFocused;this.previouslyFocused=null,t&&t instanceof HTMLElement&&document.contains(t)&&t.focus();}getFocusableElements(){const t=["button:not([disabled])","[href]","input:not([disabled])","select:not([disabled])","textarea:not([disabled])","[tabindex]:not([tabindex='-1'])","[role='button']:not([aria-disabled='true'])"];return Array.from(this.container.querySelectorAll(t.join(","))).filter(e=>!e.hidden&&e.getClientRects().length>0)}focusFirst(){(this.getFocusableElements()[0]??this.closeButton??this.box).focus?.();}attachKeydownTrap(){this.keydownListener||(this.keydownListener=t=>{if(t.key==="Escape"){t.preventDefault(),this.close();return}if(t.key!=="Tab")return;const e=this.getFocusableElements();if(!e.length){t.preventDefault(),this.box.focus();return}const i=e[0],o=e.at(-1)??i,r=document.activeElement;t.shiftKey?(r===i||r===this.box)&&(t.preventDefault(),o.focus()):r===o&&(t.preventDefault(),i.focus());},this.container.addEventListener("keydown",this.keydownListener));}detachKeydownTrap(){this.keydownListener&&(this.container.removeEventListener("keydown",this.keydownListener),this.keydownListener=void 0);}set hidden(t){st(this.container,t),this.container.setAttribute("aria-hidden",t?"true":"false"),this.container.toggleAttribute("inert",t);}get hidden(){return at(this.container)}get isDialogOpen(){return !this.container.hidden}}class Qn{container;input;label;onInput=new N;onChange=new N;events={input:this.onInput,change:this.onChange};_labelHtml;_multiline;_placeholder;_value;constructor({labelHtml:t="",placeholder:e="",value:i="",multiline:o=false}){this._labelHtml=t,this._multiline=o,this._placeholder=e,this._value=i;const r=this.createElements();this.container=r.container,this.input=r.input,this.label=r.label;}createElements(){const t=S.createEl("vot-block",["vot-textfield"]),e=document.createElement(this._multiline?"textarea":"input");this._labelHtml||e.classList.add("vot-show-placeholer","vot-show-placeholder"),e.placeholder=this._placeholder,e.value=this._value;const i=S.createEl("span");return i.append(this._labelHtml),t.append(e,i),e.addEventListener("input",()=>{this._value=this.input.value,this.onInput.dispatch(this._value);}),e.addEventListener("change",()=>{this._value=this.input.value,this.onChange.dispatch(this._value);}),{container:t,label:i,input:e}}addEventListener(t,e){return At(this.events,t,e),this}removeEventListener(t,e){return It(this.events,t,e),this}get value(){return this._value}set value(t){this._value!==t&&(this.input.value=this._value=t,this.onChange.dispatch(this._value));}get placeholder(){return this._placeholder}set placeholder(t){this.input.placeholder=this._placeholder=t;}get disabled(){return this.input.disabled}set disabled(t){this.input.disabled=t;}set hidden(t){st(this.container,t);}get hidden(){return at(this.container)}}class Z{container;outer;arrowIcon;title;dialogParent;labelElement;_selectTitle;_dialogTitle;multiSelect;baseItems;_items;searchItemsProvider;isLoading=false;isDialogOpen=false;searchRequestId=0;onSelectItem=new N;onBeforeOpen=new N;events={selectItem:this.onSelectItem,beforeOpen:this.onBeforeOpen};contentList;contentItemSearchDatasetKey="votSearchLabel";contentItemIndexDatasetKey="votIndex";selectedItems=[];selectedValues;constructor({selectTitle:t,dialogTitle:e,items:i,searchItemsProvider:o,labelElement:r,dialogParent:s=document.documentElement,multiSelect:a}){this._selectTitle=t,this._dialogTitle=e,this.baseItems=this.cloneItems(i),this._items=this.cloneItems(i),this.searchItemsProvider=o,this.multiSelect=a??false,this.labelElement=r,this.dialogParent=s,this.selectedValues=this.calcSelectedValues();const l=this.createElements();this.container=l.container,this.outer=l.outer,this.arrowIcon=l.arrowIcon,this.title=l.title;}cloneItems(t){return t.map(e=>({...e}))}static genLanguageItems(t,e){return t.map(i=>{const o=`langs.${i}`,r=v.get(o);return {label:r===o?i.toUpperCase():r,value:i,selected:e===i}})}multiSelectItemHandle=t=>{const e=t.value;this.selectedValues.has(e)&&this.selectedValues.size>1?this.selectedValues.delete(e):this.selectedValues.add(e),this.syncItemsSelectionState(),this.syncItemsSelectionState(this.baseItems),this.updateSelectedState(),this.onSelectItem.dispatch(Array.from(this.selectedValues));};singleSelectItemHandle=t=>{const e=t.value;this.selectedValues=new Set([e]),this.syncItemsSelectionState(),this.syncItemsSelectionState(this.baseItems),this.updateSelectedState(),this.onSelectItem.dispatch(e);};onContentItemClick=t=>{if(!(t.target instanceof HTMLElement))return;const e=t.target.closest(".vot-select-content-item");if(!e||e.inert||!this.contentList?.contains(e))return;const i=e.dataset[this.contentItemIndexDatasetKey];if(!i)return;const o=this._items[Number(i)];if(o){if(this.multiSelect){this.multiSelectItemHandle(o);return}this.singleSelectItemHandle(o);}};syncItemsSelectionState(t=this._items){for(const e of t)e.selected=this.selectedValues.has(e.value);}restoreBaseItems(){this._items=this.cloneItems(this.baseItems),this.syncItemsSelectionState(),this.updateSelectedState();}createDialogContentList(){const t=S.createEl("vot-block",["vot-select-content-list"]);for(const[e,i]of this._items.entries()){const o=S.createEl("vot-block",["vot-select-content-item"]);o.textContent=i.label,o.dataset.votSelected=i.selected===true?"true":"false",o.dataset.votValue=i.value,o.dataset[this.contentItemSearchDatasetKey]=i.label.toLowerCase(),o.dataset[this.contentItemIndexDatasetKey]=String(e),i.disabled&&(o.inert=true),t.appendChild(o);}return t.addEventListener("click",this.onContentItemClick),this.selectedItems=Array.from(t.children),t}createElements(){const t=S.createEl("vot-block",["vot-select"]);this.labelElement?(t.classList.add("vot-select--labeled"),t.append(this.labelElement)):t.classList.add("vot-select--control-only");const e=S.createEl("vot-block",["vot-select-outer"]);S.makeButtonLike(e),e.setAttribute("aria-haspopup","dialog"),e.setAttribute("aria-expanded","false");const i=S.createEl("vot-block",["vot-select-title"]);i.textContent=this.visibleText;const o=S.createEl("vot-block",["vot-select-arrow-icon"]);return it(Gs,o),e.append(i,o),e.addEventListener("click",()=>{if(!this.disabled&&!(this.isLoading||this.isDialogOpen))try{this.isLoading=!0;const r=new Zn({titleHtml:this._dialogTitle,isTemp:!0});this.onBeforeOpen.dispatch(r),this.dialogParent.appendChild(r.container),this.isDialogOpen=!0,e.setAttribute("aria-expanded","true");const s=new Qn({labelHtml:v.get("searchField")});s.addEventListener("input",async a=>{const l=++this.searchRequestId;if(this.searchItemsProvider){const u=await this.searchItemsProvider(a);if(l!==this.searchRequestId)return;this.updateItems(u,{persist:!1});}const c=a.toLowerCase();for(const u of this.selectedItems){const d=u.dataset[this.contentItemSearchDatasetKey]??"";u.hidden=!d.includes(c);}}),this.contentList=this.createDialogContentList(),r.bodyContainer.append(s.container,this.contentList),r.addEventListener("close",()=>{this.isDialogOpen=!1,this.restoreBaseItems(),this.selectedItems=[],this.contentList=void 0,e.setAttribute("aria-expanded","false");}),r.open();}finally{this.isLoading=false;}}),t.appendChild(e),{container:t,outer:e,arrowIcon:o,title:i}}calcSelectedValues(){return new Set(this._items.filter(t=>t.selected).map(t=>t.value))}addEventListener(t,e){return At(this.events,t,e),this}removeEventListener(t,e){return It(this.events,t,e),this}updateTitle(){return this.title.textContent=this.visibleText,this}updateSelectedState(){if(this.selectedItems.length>0)for(const t of this.selectedItems){const e=t.dataset.votValue;e!==void 0&&(t.dataset.votSelected=this.selectedValues.has(e).toString());}return this.updateTitle(),this}setSelectedValue(t){const e=Array.isArray(t)?t:[t];let i;return this.multiSelect?i=e:i=e.length>0?[e[0]]:[],this.selectedValues=new Set(i),this.syncItemsSelectionState(),this.syncItemsSelectionState(this.baseItems),this.updateSelectedState(),this}updateItems(t,e={}){const{persist:i=true}=e,o=this.cloneItems(t);i&&(this.baseItems=this.cloneItems(o)),this._items=o,this.selectedValues=this.calcSelectedValues(),this.updateSelectedState();const r=this.contentList?.parentElement;if(!this.contentList||!r)return this;const s=this.contentList;return this.contentList=this.createDialogContentList(),r.replaceChild(this.contentList,s),this}get visibleText(){return this.multiSelect?this._items.filter(t=>this.selectedValues.has(t.value)).map(t=>t.label).join(", ")||this._selectTitle:this._items.find(t=>t.selected)?.label??this._selectTitle}set selectTitle(t){this._selectTitle=t,this.updateTitle();}set hidden(t){st(this.container,t);}get hidden(){return at(this.container)}get disabled(){return this.outer.getAttribute("disabled")==="true"||this.outer.getAttribute("aria-disabled")==="true"}set disabled(t){if(t){this.outer.setAttribute("disabled","true");return}this.outer.removeAttribute("disabled");}}class Pm{container;fromSelect;directionIcon;toSelect;dialogParent;_fromSelectTitle;_fromDialogTitle;_fromItems;_toSelectTitle;_toDialogTitle;_toItems;constructor({from:{selectTitle:t=v.get("videoLanguage"),dialogTitle:e=v.get("videoLanguage"),items:i},to:{selectTitle:o=v.get("translationLanguage"),dialogTitle:r=v.get("translationLanguage"),items:s},dialogParent:a=document.documentElement}){this._fromSelectTitle=t,this._fromDialogTitle=e,this._fromItems=i,this._toSelectTitle=o,this._toDialogTitle=r,this._toItems=s,this.dialogParent=a;const l=this.createElements();this.container=l.container,this.fromSelect=l.fromSelect,this.directionIcon=l.directionIcon,this.toSelect=l.toSelect;}createElements(){const t=S.createEl("vot-block",["vot-lang-select"]),e=new Z({selectTitle:this._fromSelectTitle,dialogTitle:this._fromDialogTitle,items:this._fromItems,dialogParent:this.dialogParent}),i=S.createEl("vot-block",["vot-lang-select-icon"]);it(Tm,i);const o=new Z({selectTitle:this._toSelectTitle,dialogTitle:this._toDialogTitle,items:this._toItems,dialogParent:this.dialogParent});return t.append(e.container,i,o.container),{container:t,fromSelect:e,directionIcon:i,toSelect:o}}setSelectedValues(t,e){return this.fromSelect.setSelectedValue(t),this.toSelect.setSelectedValue(e),this}updateItems(t,e){return this._fromItems=t,this._toItems=e,this.fromSelect=this.fromSelect.updateItems(t),this.toSelect=this.toSelect.updateItems(e),this}}class Et{container;input;label;onInput=new N;_labelHtml;_value;_min;_max;_step;constructor({labelHtml:t,value:e=50,min:i=0,max:o=100,step:r=1}){this._labelHtml=t,this._value=e,this._min=i,this._max=o,this._step=r;const s=this.createElements();this.container=s.container,this.input=s.input,this.label=s.label,this.update();}updateProgress(){const t=this._max-this._min,e=t<=0?0:(this._value-this._min)/t,i=Math.max(0,Math.min(1,e));return this.container.style.setProperty("--vot-progress",i.toString()),this}update(){return this._value=this.input.valueAsNumber,this._min=+this.input.min,this._max=+this.input.max,this.updateProgress(),this}createElements(){const t=S.createEl("vot-block",["vot-slider"]),e=document.createElement("input");e.type="range",e.min=this._min.toString(),e.max=this._max.toString(),e.step=this._step.toString(),e.value=this._value.toString();const i=S.createEl("span");return it(this._labelHtml,i),t.append(e,i),e.addEventListener("input",()=>{this.update(),this.onInput.dispatch(this._value,false);}),{container:t,label:i,input:e}}addEventListener(t,e){return this.onInput.addListener(e),this}removeEventListener(t,e){return this.onInput.removeListener(e),this}get value(){return this._value}set value(t){this._value=_n(t,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(t){this._min=t,this.input.min=this._min.toString(),this._value=_n(this._value,this._min,this._max),this.input.value=this._value.toString(),this.updateProgress();}get max(){return this._max}set max(t){this._max=t,this.input.max=this._max.toString(),this._value=_n(this._value,this._min,this._max),this.input.value=this._value.toString(),this.updateProgress();}get step(){return this._step}set step(t){this._step=t,this.input.step=this._step.toString();}get disabled(){return this.input.disabled}set disabled(t){this.input.disabled=t;}set hidden(t){st(this.container,t);}get hidden(){return at(this.container)}}function _n(n,t,e){return !Number.isFinite(n)||e=66?"right":"default":"default"}static calcDirection(t){return ["default","top"].includes(t)?"row":"column"}createElements(){const t=S.createEl("vot-block",["vot-segmented-button"]);t.dataset.position=this._position,t.dataset.direction=this._direction,t.dataset.status=this._status;const e=S.createEl("vot-block",["vot-segment","vot-translate-button"]);e.setAttribute("role","button"),e.tabIndex=0,e.setAttribute("aria-label",this._labelText||"Translate"),it(bm,e);const i=S.createEl("span",["vot-segment-label"]);i.textContent=this._labelText,e.appendChild(i);const o=S.createEl("vot-block",["vot-separator"]),r=S.createEl("vot-block",["vot-segment-only-icon"]);r.setAttribute("role","button"),r.tabIndex=0,r.setAttribute("aria-label","Picture in picture"),it(ym,r);const s=S.createEl("vot-block",["vot-separator"]),a=S.createEl("vot-block",["vot-segment-only-icon"]);return a.setAttribute("role","button"),a.tabIndex=0,a.setAttribute("aria-label","Menu"),a.setAttribute("aria-haspopup","dialog"),a.setAttribute("aria-expanded","false"),it(wm,a),t.append(e,o,r,s,a),{container:t,translateButton:e,separator:o,pipButton:r,separator2:s,menuButton:a,label:i}}showPiPButton(t){return this.separator2.hidden=this.pipButton.hidden=!t,this}setText(t){return this._labelText=t,this.label.textContent=t,this.translateButton.setAttribute("aria-label",t||"Translate"),this}remove(){return this.container.remove(),this}get tooltipPos(){switch(this.position){case "left":return "right";case "right":return "left";default:return "bottom"}}set status(t){this._status=this.container.dataset.status=t;}get status(){return this._status}set loading(t){this.container.dataset.loading=t.toString();}get loading(){return this.container.dataset.loading==="true"}set hidden(t){st(this.container,t);}get hidden(){return at(this.container)}get position(){return this._position}set position(t){this._position=this.container.dataset.position=t;}get direction(){return this._direction}set direction(t){this._direction=this.container.dataset.direction=t;}set opacity(t){const e=Number.isFinite(t)?t:1;this._opacity=e;const i=e<=.01;this.container.classList.toggle("vot-segmented-button--hidden",i);}get opacity(){return this._opacity}}class Vm{container;contentWrapper;headerContainer;bodyContainer;footerContainer;titleContainer;title;_position;_titleHtml;menuId=typeof crypto<"u"&&"randomUUID"in crypto?`vot-menu-${crypto.randomUUID()}`:`vot-menu-${Math.random().toString(36).slice(2)}`;titleId=typeof crypto<"u"&&"randomUUID"in crypto?`vot-menu-title-${crypto.randomUUID()}`:`vot-menu-title-${Math.random().toString(36).slice(2)}`;constructor({position:t="default",titleHtml:e=""}){this._position=t,this._titleHtml=e;const i=this.createElements();this.container=i.container,this.contentWrapper=i.contentWrapper,this.headerContainer=i.headerContainer,this.bodyContainer=i.bodyContainer,this.footerContainer=i.footerContainer,this.titleContainer=i.titleContainer,this.title=i.title;}createElements(){const t=S.createEl("vot-block",["vot-menu"]);t.hidden=true,t.id=this.menuId,t.dataset.position=this._position,t.setAttribute("role","dialog"),t.setAttribute("aria-modal","false"),t.setAttribute("aria-hidden","true"),t.toggleAttribute("inert",true);const e=S.createEl("vot-block",["vot-menu-content-wrapper"]);t.appendChild(e);const i=S.createEl("vot-block",["vot-menu-header-container"]),o=S.createEl("vot-block",["vot-menu-title-container"]);i.appendChild(o);const r=S.createEl("vot-block",["vot-menu-title"]);r.id=this.titleId,r.append(this._titleHtml),o.appendChild(r),t.setAttribute("aria-labelledby",this.titleId);const s=S.createEl("vot-block",["vot-menu-body-container"]),a=S.createEl("vot-block",["vot-menu-footer-container"]);return e.append(i,s,a),{container:t,contentWrapper:e,headerContainer:i,bodyContainer:s,footerContainer:a,titleContainer:o,title:r}}setText(t){return this._titleHtml=this.title.textContent=t,this}remove(){return this.container.remove(),this}set hidden(t){st(this.container,t),this.container.setAttribute("aria-hidden",t?"true":"false"),this.container.toggleAttribute("inert",t);}get hidden(){return at(this.container)}get position(){return this._position}set position(t){this._position=this.container.dataset.position=t;}}class ve{static BIG_CONTAINER_WIDTH_PX=550;mount;globalPortal;abortController=null;defaultVolumePersistTimer;defaultVolumePersistDelayMs=250;dragging=false;dragCandidate=false;dragDirty=false;dragStartX=0;dragStartY=0;currentClientX=0;dragThresholdPx=6;containerRect=null;dragIsBigContainer=null;checkerUnsubscribe=null;initialized=false;data;videoHandler;intervalIdleChecker;events={"click:settings":new N,"click:pip":new N,"click:downloadTranslation":new N,"click:downloadSubtitles":new N,"click:translate":new N,"input:videoVolume":new N,"input:translationVolume":new N,"select:fromLanguage":new N,"select:toLanguage":new N,"select:subtitles":new N};votButton;votButtonTooltip;votMenu;downloadTranslationButton;downloadSubtitlesButton;openSettingsButton;languagePairSelect;subtitlesSelectLabel;subtitlesSelect;videoVolumeSliderLabel;videoVolumeSlider;translationVolumeSliderLabel;translationVolumeSlider;constructor({mount:t,globalPortal:e,data:i={},videoHandler:o,intervalIdleChecker:r}){this.mount=t,this.globalPortal=e,this.data=i,this.videoHandler=o,this.intervalIdleChecker=r;}get root(){return this.mount.root}get portalContainer(){return this.mount.portalContainer}get tooltipLayoutRoot(){return this.mount.tooltipLayoutRoot}updateMount(t){const e=this.mount.root,i=t.root,o=this.mount.tooltipLayoutRoot,r=t.tooltipLayoutRoot;return this.mount=t,this.isInitialized()?(e!==i&&(this.votButton&&i.appendChild(this.votButton.container),this.votMenu&&i.appendChild(this.votMenu.container)),this.votButtonTooltip&&vm({root:e,portalContainer:this.mount.portalContainer,subtitlesMountContainer:this.mount.subtitlesMountContainer,tooltipLayoutRoot:o},t)&&this.votButtonTooltip.updateMount({layoutRoot:r??document.documentElement}),this):this}isInitialized(){return this.initialized}calcButtonLayout(t){return this.isBigContainer&&Mm(t)?{direction:"column",position:t}:{direction:"row",position:"default"}}addEventListener(t,e){return this.events[t].addListener(e),this}removeEventListener(t,e){return this.events[t].removeListener(e),this}scheduleDefaultVolumePersist(){this.defaultVolumePersistTimer!==void 0&&globalThis.clearTimeout(this.defaultVolumePersistTimer),this.defaultVolumePersistTimer=globalThis.setTimeout(()=>{this.defaultVolumePersistTimer=void 0,this.flushDefaultVolumePersist();},this.defaultVolumePersistDelayMs);}flushDefaultVolumePersist(){this.defaultVolumePersistTimer!==void 0&&(globalThis.clearTimeout(this.defaultVolumePersistTimer),this.defaultVolumePersistTimer=void 0),typeof this.data.defaultVolume=="number"&&I.set("defaultVolume",this.data.defaultVolume);}initUI(t="default"){if(this.isInitialized())throw new Error("[VOT] OverlayView is already initialized");this.initialized=true;const{position:e,direction:i}=this.calcButtonLayout(t);this.votButton=new On({position:e,direction:i,status:"none",labelHtml:v.get("translateVideo")}),this.votButton.opacity=0,this.pipButtonVisible||this.votButton.showPiPButton(false),this.root.appendChild(this.votButton.container),this.votButtonTooltip=new K({target:this.votButton.translateButton,content:v.get("translateVideo"),position:this.votButton.tooltipPos,autoLayout:false,hidden:i==="row",bordered:false,parentElement:this.globalPortal,layoutRoot:this.tooltipLayoutRoot}),this.votMenu=new Vm({titleHtml:v.get("VOTSettings"),position:e}),this.root.appendChild(this.votMenu.container),this.votButton.menuButton.setAttribute("aria-controls",this.votMenu.container.id),this.downloadTranslationButton=new Cm,this.downloadTranslationButton.hidden=true,this.downloadSubtitlesButton=S.createIconButton(xm,{ariaLabel:"Download subtitles"}),this.downloadSubtitlesButton.hidden=true,this.openSettingsButton=S.createIconButton(km,{ariaLabel:v.get("VOTSettings")}),this.votMenu.headerContainer.append(this.downloadTranslationButton.button,this.downloadSubtitlesButton,this.openSettingsButton);const o=this.videoHandler?.videoData?.detectedLanguage??"en",r=this.data.responseLanguage??"ru";this.languagePairSelect=new Pm({from:{selectTitle:v.get(`langs.${o}`),items:Z.genLanguageItems(Lt,o)},to:{selectTitle:v.get(`langs.${r}`),items:Z.genLanguageItems(si,r)},dialogParent:this.globalPortal}),this.subtitlesSelectLabel=new pt({labelText:v.get("VOTSubtitles")}),this.subtitlesSelect=new Z({selectTitle:v.get("VOTSubtitlesDisabled"),dialogTitle:v.get("VOTSubtitles"),labelElement:this.subtitlesSelectLabel.container,dialogParent:this.globalPortal,items:[{label:v.get("VOTSubtitlesDisabled"),value:"disabled",selected:true}]});const s=this.videoHandler?this.videoHandler.getVideoVolume()*100:100;this.videoVolumeSliderLabel=new Pt({labelText:v.get("VOTVolume"),value:s}),this.videoVolumeSlider=new Et({labelHtml:this.videoVolumeSliderLabel.container,value:s}),this.videoVolumeSlider.hidden=!this.data.showVideoSlider||this.votButton.status!=="success";const a=this.data.defaultVolume??100;return this.translationVolumeSliderLabel=new Pt({labelText:v.get("VOTVolumeTranslation"),value:a}),this.translationVolumeSlider=new Et({labelHtml:this.translationVolumeSliderLabel.container,value:a,max:this.data.audioBooster?ls:100}),this.translationVolumeSlider.hidden=this.votButton.status!=="success",this.votMenu.bodyContainer.append(this.languagePairSelect.container,this.subtitlesSelect.container,this.videoVolumeSlider.container,this.translationVolumeSlider.container),this}initUIEvents(){if(!this.isInitialized())throw new Error("[VOT] OverlayView isn't initialized");this.abortController=new AbortController;const t=this.abortController.signal;this.checkerUnsubscribe?.(),this.checkerUnsubscribe=this.intervalIdleChecker.subscribe(()=>{this.onCheckerTick();}),this.votButton.container.addEventListener("click",l=>{l.preventDefault(),l.stopPropagation(),l.stopImmediatePropagation();},{signal:t});const e=l=>c=>{(c.key==="Enter"||c.key===" ")&&(c.preventDefault(),l());},i=l=>l.isPrimary&&l.button===0,o=(l,{returnFocusToToggle:c=false}={})=>{this.isInitialized()&&(this.votMenu.hidden=!l,this.votButton.menuButton.setAttribute("aria-expanded",l.toString()),this.votButtonTooltip&&(this.votButtonTooltip.hidden=l||this.votButton.direction==="row"),l?queueMicrotask(()=>this.openSettingsButton?.focus?.()):c?queueMicrotask(()=>this.votButton.menuButton.focus?.()):this.votButton.menuButton.blur());},r=()=>o(this.votMenu.hidden),s=(l=false)=>o(false,{returnFocusToToggle:l});this.votButton.translateButton.addEventListener("pointerdown",l=>{i(l)&&(s(),this.events["click:translate"].dispatch());},{signal:t}),this.votButton.translateButton.addEventListener("keydown",e(()=>{s(),this.events["click:translate"].dispatch();}),{signal:t}),this.votButton.pipButton.addEventListener("pointerdown",l=>{i(l)&&(s(),this.events["click:pip"].dispatch());},{signal:t}),this.votButton.pipButton.addEventListener("keydown",e(()=>{s(),this.events["click:pip"].dispatch();}),{signal:t}),this.votButton.menuButton.addEventListener("pointerdown",l=>{i(l)&&(l.preventDefault(),r());},{signal:t}),this.votButton.menuButton.addEventListener("keydown",e(r),{signal:t});const a="none";this.votButton.container.style.touchAction=a,this.votButton.translateButton.style.touchAction=a,this.votButton.pipButton.style.touchAction=a,this.votButton.menuButton.style.touchAction=a,this.votButton.container.addEventListener("pointerdown",this.onDragStart,{signal:t}),this.votButton.container.addEventListener("touchstart",this.onTouchDragStart,{signal:t,passive:false}),this.votMenu.container.addEventListener("click",l=>{l.preventDefault(),l.stopPropagation(),l.stopImmediatePropagation();},{signal:t});for(const l of ["pointerdown","mousedown"])this.votMenu.container.addEventListener(l,c=>{c.stopImmediatePropagation();},{signal:t});return document.addEventListener("pointerdown",l=>{if(this.votMenu.hidden)return;const c=l.target,u=typeof l.composedPath=="function"?l.composedPath():[],d=c&&this.votMenu.container.contains(c)||u.includes(this.votMenu.container),h=c&&this.votButton.menuButton.contains(c)||u.includes(this.votButton.menuButton),f=c&&this.votButton.container.contains(c)||u.includes(this.votButton.container),g=c instanceof HTMLElement&&!!c.closest(".vot-dialog-container");d||h||f||g||s(false);},{signal:t,capture:true,passive:true}),this.votMenu.container.addEventListener("keydown",l=>{if(l.key!=="Escape")return;const c=document.documentElement.classList.contains("vot-keyboard-nav");l.preventDefault(),l.stopPropagation(),s(c),this.votButton.container.matches(":hover")||this.votMenu.container.matches(":hover")||this.videoHandler?.overlayVisibility?.queueAutoHide?.();},{signal:t}),this.downloadTranslationButton.addEventListener("click",()=>{this.events["click:downloadTranslation"].dispatch();}),this.downloadSubtitlesButton.addEventListener("click",()=>{this.events["click:downloadSubtitles"].dispatch();},{signal:t}),this.openSettingsButton.addEventListener("click",()=>{s(),this.events["click:settings"].dispatch();},{signal:t}),this.languagePairSelect.fromSelect.addEventListener("selectItem",l=>{this.videoHandler?.videoData&&(this.videoHandler.videoData.detectedLanguage=l,this.videoHandler.videoManager.rememberUserLanguageSelection(this.videoHandler.videoData.videoId,l)),this.events["select:fromLanguage"].dispatch(l);}),this.languagePairSelect.toSelect.addEventListener("selectItem",async l=>{this.videoHandler?.videoData&&(this.videoHandler.translateToLang=this.videoHandler.videoData.responseLanguage=l);const c=this.data.responseLanguage;c!==l&&(this.data.responseLanguage=l,await I.set("responseLanguage",this.data.responseLanguage)),this.data.enabledDontTranslateLanguages&&Array.isArray(this.data.dontTranslateLanguages)&&this.data.dontTranslateLanguages.length===1&&c!==l&&typeof c=="string"&&this.data.dontTranslateLanguages[0]===c&&(this.data.dontTranslateLanguages=[l],await I.set("dontTranslateLanguages",this.data.dontTranslateLanguages)),this.events["select:toLanguage"].dispatch(l);}),this.subtitlesSelect.addEventListener("beforeOpen",async l=>{if(!this.videoHandler?.videoData)return;const c=this.videoHandler.getSubtitlesCacheKey(this.videoHandler.videoData.videoId,this.videoHandler.videoData.detectedLanguage,this.videoHandler.videoData.responseLanguage);if(this.videoHandler.subtitlesCacheKey===c)return;if(this.videoHandler.cacheManager.getSubtitles(c)!==void 0){await this.videoHandler.ensureSubtitlesForCurrentLangPair();return}const u=this.votButton?.loading??false;this.votButton&&(this.votButton.loading=true);const d=S.createInlineLoader();d.style.margin="0 auto",l.footerContainer.appendChild(d);try{await this.videoHandler.ensureSubtitlesForCurrentLangPair();}finally{d.remove(),this.votButton&&(this.votButton.loading=u);}}),this.subtitlesSelect.addEventListener("selectItem",l=>{this.events["select:subtitles"].dispatch(l);}),this.videoVolumeSlider.addEventListener("input",(l,c)=>{this.videoVolumeSliderLabel&&(this.videoVolumeSliderLabel.value=l),!c&&this.events["input:videoVolume"].dispatch(l);}),this.translationVolumeSlider.addEventListener("input",(l,c)=>{this.translationVolumeSliderLabel&&(this.translationVolumeSliderLabel.value=l),this.data.defaultVolume!==l&&(this.data.defaultVolume=l,this.scheduleDefaultVolumePersist()),!c&&this.events["input:translationVolume"].dispatch(l);}),this}updateButtonLayout(t,e){return this.isInitialized()?(this.votMenu.position=t,this.votButton.position=t,this.votButton.direction=e,this.votButtonTooltip.hidden=e==="row",this.votButtonTooltip.setPosition(this.votButton.tooltipPos),this):this}moveButton(t){if(!this.isInitialized())return this;const e=this.dragIsBigContainer??this.isBigContainer,i=On.calcPosition(t,e);if(i===this.votButton.position)return this;const o=On.calcDirection(i);return this.data.buttonPos=i,this.updateButtonLayout(i,o),this}startDragSession(t,e,i){this.dragCandidate=true,this.dragging=false,this.dragStartX=t,this.dragStartY=e,this.currentClientX=t,this.containerRect=this.root.getBoundingClientRect(),this.dragIsBigContainer=this.isBigContainer,this.dragDirty=false,this.intervalIdleChecker.markActivity(i),this.intervalIdleChecker.requestImmediateTick();}queueDragTick(t){this.dragDirty||(this.dragDirty=true,this.intervalIdleChecker.markActivity(t),this.intervalIdleChecker.requestImmediateTick());}updateDragFromMove(t,e,i){if(this.currentClientX=t,!!this.dragCandidate){if(!this.dragging){const o=Math.abs(this.currentClientX-this.dragStartX),r=Math.abs(e-this.dragStartY);o+r>=this.dragThresholdPx&&(this.dragging=true);}this.dragging&&this.queueDragTick(i);}}onDragStart=t=>{!t.isPrimary||t.button!==0||t.pointerType!=="touch"&&(t.preventDefault(),this.startDragSession(t.clientX,t.clientY,"overlay-pointer-down"),document.addEventListener("pointermove",this.onGlobalPointerMove,{passive:true}),document.addEventListener("pointerup",this.onDragEnd),document.addEventListener("pointercancel",this.onDragEnd));};onTouchDragStart=t=>{if(!t.touches||t.touches.length===0)return;const e=t.touches[0];this.startDragSession(e.clientX,e.clientY,"overlay-touch-start"),document.addEventListener("touchmove",this.onGlobalTouchMove,{passive:false}),document.addEventListener("touchend",this.onDragEnd),document.addEventListener("touchcancel",this.onDragEnd);};onGlobalTouchMove=t=>{if(!t.touches||t.touches.length===0)return;const e=t.touches[0];this.updateDragFromMove(e.clientX,e.clientY,"overlay-touch-move"),this.dragging&&t.preventDefault();};onGlobalPointerMove=t=>{this.updateDragFromMove(t.clientX,t.clientY,"overlay-pointer-move");};applyDragFromState=()=>{if(!this.dragging||!this.dragDirty||!this.containerRect)return;const t=this.containerRect.width;if(!(t>0&&Number.isFinite(t)))return;this.dragDirty=false;const e=this.currentClientX-this.containerRect.left,o=Math.max(0,Math.min(e,t))/t*100;this.moveButton(o);};onCheckerTick=()=>{this.applyDragFromState();};onDragEnd=()=>{document.removeEventListener("pointermove",this.onGlobalPointerMove),document.removeEventListener("pointerup",this.onDragEnd),document.removeEventListener("pointercancel",this.onDragEnd),document.removeEventListener("touchmove",this.onGlobalTouchMove),document.removeEventListener("touchend",this.onDragEnd),document.removeEventListener("touchcancel",this.onDragEnd),this.applyDragFromState();const t=this.dragIsBigContainer??this.isBigContainer;this.dragging&&t&&this.data.buttonPos&&I.set("buttonPos",this.data.buttonPos),this.dragging=false,this.dragCandidate=false,this.dragDirty=false,this.containerRect=null,this.dragIsBigContainer=null;};updateButtonOpacity(t){return !this.isInitialized()||!this.votMenu.hidden?this:(Math.abs(this.votButton.opacity-t)>.01&&(this.votButton.opacity=t),this)}doReleaseUI(){this.votButton?.remove(),this.votMenu?.remove(),this.votButtonTooltip?.release();}doReleaseUIEvents(){this.abortController?.abort(),this.abortController=null,this.checkerUnsubscribe?.(),this.checkerUnsubscribe=null,this.onDragEnd(),this.flushDefaultVolumePersist();for(const t of Object.values(this.events))t.clear();}release(){return this.isInitialized()?(this.doReleaseUIEvents(),this.doReleaseUI(),this.initialized=false,this):this}get isBigContainer(){const t=this.videoHandler?.video?.getBoundingClientRect?.().width;if(typeof t=="number"&&Number.isFinite(t))return t>ve.BIG_CONTAINER_WIDTH_PX;const e=this.videoHandler?.container?.getBoundingClientRect?.().width;return typeof e=="number"&&Number.isFinite(e)?e>ve.BIG_CONTAINER_WIDTH_PX:this.root.clientWidth>ve.BIG_CONTAINER_WIDTH_PX}get pipButtonVisible(){return us()&&!!this.data.showPiPButton}}function Mm(n){return n==="left"||n==="right"}const _m=["default","top","left","right"],Om={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"},Ks={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"},A={bot:"bot",desktop:"desktop",mobile:"mobile",tablet:"tablet",tv:"tv"},X={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"},wt={Blink:"Blink",EdgeHTML:"EdgeHTML",Gecko:"Gecko",Presto:"Presto",Trident:"Trident",WebKit:"WebKit"};class p{static getFirstMatch(t,e){const i=e.match(t);return i&&i.length>0&&i[1]||""}static getSecondMatch(t,e){const i=e.match(t);return i&&i.length>1&&i[2]||""}static matchAndReturnConst(t,e,i){if(t.test(e))return i}static getWindowsVersionName(t){switch(t){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}}static getMacOSVersionName(t){const e=t.split(".").splice(0,2).map(r=>parseInt(r,10)||0);e.push(0);const i=e[0],o=e[1];if(i===10)switch(o){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(i){case 11:return "Big Sur";case 12:return "Monterey";case 13:return "Ventura";case 14:return "Sonoma";case 15:return "Sequoia";default:return}}static getAndroidVersionName(t){const e=t.split(".").splice(0,2).map(i=>parseInt(i,10)||0);if(e.push(0),!(e[0]===1&&e[1]<5)){if(e[0]===1&&e[1]<6)return "Cupcake";if(e[0]===1&&e[1]>=6)return "Donut";if(e[0]===2&&e[1]<2)return "Eclair";if(e[0]===2&&e[1]===2)return "Froyo";if(e[0]===2&&e[1]>2)return "Gingerbread";if(e[0]===3)return "Honeycomb";if(e[0]===4&&e[1]<1)return "Ice Cream Sandwich";if(e[0]===4&&e[1]<4)return "Jelly Bean";if(e[0]===4&&e[1]>=4)return "KitKat";if(e[0]===5)return "Lollipop";if(e[0]===6)return "Marshmallow";if(e[0]===7)return "Nougat";if(e[0]===8)return "Oreo";if(e[0]===9)return "Pie"}}static getVersionPrecision(t){return t.split(".").length}static compareVersions(t,e,i=false){const o=p.getVersionPrecision(t),r=p.getVersionPrecision(e);let s=Math.max(o,r),a=0;const l=p.map([t,e],c=>{const u=s-p.getVersionPrecision(c),d=c+new Array(u+1).join(".0");return p.map(d.split("."),h=>new Array(20-h.length).join("0")+h).reverse()});for(i&&(a=s-Math.min(o,r)),s-=1;s>=a;){if(l[0][s]>l[1][s])return 1;if(l[0][s]===l[1][s]){if(s===a)return 0;s-=1;}else if(l[0][s]{i[l]=s[l];});}return t}static getBrowserAlias(t){return Om[t]}static getBrowserTypeByAlias(t){return Ks[t]||""}}const C=/version\/(\d+(\.?_?\d+)+)/i,Dm=[{test:[/gptbot/i],describe(n){const t={name:"GPTBot"},e=p.getFirstMatch(/gptbot\/(\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/chatgpt-user/i],describe(n){const t={name:"ChatGPT-User"},e=p.getFirstMatch(/chatgpt-user\/(\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/oai-searchbot/i],describe(n){const t={name:"OAI-SearchBot"},e=p.getFirstMatch(/oai-searchbot\/(\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/claudebot/i,/claude-web/i,/claude-user/i,/claude-searchbot/i],describe(n){const t={name:"ClaudeBot"},e=p.getFirstMatch(/(?:claudebot|claude-web|claude-user|claude-searchbot)\/(\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/omgilibot/i,/webzio-extended/i],describe(n){const t={name:"Omgilibot"},e=p.getFirstMatch(/(?:omgilibot|webzio-extended)\/(\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/diffbot/i],describe(n){const t={name:"Diffbot"},e=p.getFirstMatch(/diffbot\/(\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/perplexitybot/i],describe(n){const t={name:"PerplexityBot"},e=p.getFirstMatch(/perplexitybot\/(\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/perplexity-user/i],describe(n){const t={name:"Perplexity-User"},e=p.getFirstMatch(/perplexity-user\/(\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/youbot/i],describe(n){const t={name:"YouBot"},e=p.getFirstMatch(/youbot\/(\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/meta-webindexer/i],describe(n){const t={name:"Meta-WebIndexer"},e=p.getFirstMatch(/meta-webindexer\/(\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/meta-externalads/i],describe(n){const t={name:"Meta-ExternalAds"},e=p.getFirstMatch(/meta-externalads\/(\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/meta-externalagent/i],describe(n){const t={name:"Meta-ExternalAgent"},e=p.getFirstMatch(/meta-externalagent\/(\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/meta-externalfetcher/i],describe(n){const t={name:"Meta-ExternalFetcher"},e=p.getFirstMatch(/meta-externalfetcher\/(\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/googlebot/i],describe(n){const t={name:"Googlebot"},e=p.getFirstMatch(/googlebot\/(\d+(\.\d+))/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/linespider/i],describe(n){const t={name:"Linespider"},e=p.getFirstMatch(/(?:linespider)(?:-[-\w]+)?[\s/](\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/amazonbot/i],describe(n){const t={name:"AmazonBot"},e=p.getFirstMatch(/amazonbot\/(\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/bingbot/i],describe(n){const t={name:"BingCrawler"},e=p.getFirstMatch(/bingbot\/(\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/baiduspider/i],describe(n){const t={name:"BaiduSpider"},e=p.getFirstMatch(/baiduspider\/(\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/duckduckbot/i],describe(n){const t={name:"DuckDuckBot"},e=p.getFirstMatch(/duckduckbot\/(\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/ia_archiver/i],describe(n){const t={name:"InternetArchiveCrawler"},e=p.getFirstMatch(/ia_archiver\/(\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/facebookexternalhit/i,/facebookcatalog/i],describe(){return {name:"FacebookExternalHit"}}},{test:[/slackbot/i,/slack-imgProxy/i],describe(n){const t={name:"SlackBot"},e=p.getFirstMatch(/(?:slackbot|slack-imgproxy)(?:-[-\w]+)?[\s/](\d+(\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/yahoo!?[\s/]*slurp/i],describe(){return {name:"YahooSlurp"}}},{test:[/yandexbot/i,/yandexmobilebot/i],describe(){return {name:"YandexBot"}}},{test:[/pingdom/i],describe(){return {name:"PingdomBot"}}},{test:[/opera/i],describe(n){const t={name:"Opera"},e=p.getFirstMatch(C,n)||p.getFirstMatch(/(?:opera)[\s/](\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/opr\/|opios/i],describe(n){const t={name:"Opera"},e=p.getFirstMatch(/(?:opr|opios)[\s/](\S+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/SamsungBrowser/i],describe(n){const t={name:"Samsung Internet for Android"},e=p.getFirstMatch(C,n)||p.getFirstMatch(/(?:SamsungBrowser)[\s/](\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/Whale/i],describe(n){const t={name:"NAVER Whale Browser"},e=p.getFirstMatch(C,n)||p.getFirstMatch(/(?:whale)[\s/](\d+(?:\.\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/PaleMoon/i],describe(n){const t={name:"Pale Moon"},e=p.getFirstMatch(C,n)||p.getFirstMatch(/(?:PaleMoon)[\s/](\d+(?:\.\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/MZBrowser/i],describe(n){const t={name:"MZ Browser"},e=p.getFirstMatch(/(?:MZBrowser)[\s/](\d+(?:\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/focus/i],describe(n){const t={name:"Focus"},e=p.getFirstMatch(/(?:focus)[\s/](\d+(?:\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/swing/i],describe(n){const t={name:"Swing"},e=p.getFirstMatch(/(?:swing)[\s/](\d+(?:\.\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/coast/i],describe(n){const t={name:"Opera Coast"},e=p.getFirstMatch(C,n)||p.getFirstMatch(/(?:coast)[\s/](\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/opt\/\d+(?:.?_?\d+)+/i],describe(n){const t={name:"Opera Touch"},e=p.getFirstMatch(/(?:opt)[\s/](\d+(\.?_?\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/yabrowser/i],describe(n){const t={name:"Yandex Browser"},e=p.getFirstMatch(/(?:yabrowser)[\s/](\d+(\.?_?\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/ucbrowser/i],describe(n){const t={name:"UC Browser"},e=p.getFirstMatch(C,n)||p.getFirstMatch(/(?:ucbrowser)[\s/](\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/Maxthon|mxios/i],describe(n){const t={name:"Maxthon"},e=p.getFirstMatch(C,n)||p.getFirstMatch(/(?:Maxthon|mxios)[\s/](\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/epiphany/i],describe(n){const t={name:"Epiphany"},e=p.getFirstMatch(C,n)||p.getFirstMatch(/(?:epiphany)[\s/](\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/puffin/i],describe(n){const t={name:"Puffin"},e=p.getFirstMatch(C,n)||p.getFirstMatch(/(?:puffin)[\s/](\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/sleipnir/i],describe(n){const t={name:"Sleipnir"},e=p.getFirstMatch(C,n)||p.getFirstMatch(/(?:sleipnir)[\s/](\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/k-meleon/i],describe(n){const t={name:"K-Meleon"},e=p.getFirstMatch(C,n)||p.getFirstMatch(/(?:k-meleon)[\s/](\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/micromessenger/i],describe(n){const t={name:"WeChat"},e=p.getFirstMatch(/(?:micromessenger)[\s/](\d+(\.?_?\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/qqbrowser/i],describe(n){const t={name:/qqbrowserlite/i.test(n)?"QQ Browser Lite":"QQ Browser"},e=p.getFirstMatch(/(?:qqbrowserlite|qqbrowser)[/](\d+(\.?_?\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/msie|trident/i],describe(n){const t={name:"Internet Explorer"},e=p.getFirstMatch(/(?:msie |rv:)(\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/\sedg\//i],describe(n){const t={name:"Microsoft Edge"},e=p.getFirstMatch(/\sedg\/(\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/edg([ea]|ios)/i],describe(n){const t={name:"Microsoft Edge"},e=p.getSecondMatch(/edg([ea]|ios)\/(\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/vivaldi/i],describe(n){const t={name:"Vivaldi"},e=p.getFirstMatch(/vivaldi\/(\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/seamonkey/i],describe(n){const t={name:"SeaMonkey"},e=p.getFirstMatch(/seamonkey\/(\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/sailfish/i],describe(n){const t={name:"Sailfish"},e=p.getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i,n);return e&&(t.version=e),t}},{test:[/silk/i],describe(n){const t={name:"Amazon Silk"},e=p.getFirstMatch(/silk\/(\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/phantom/i],describe(n){const t={name:"PhantomJS"},e=p.getFirstMatch(/phantomjs\/(\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/slimerjs/i],describe(n){const t={name:"SlimerJS"},e=p.getFirstMatch(/slimerjs\/(\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/blackberry|\bbb\d+/i,/rim\stablet/i],describe(n){const t={name:"BlackBerry"},e=p.getFirstMatch(C,n)||p.getFirstMatch(/blackberry[\d]+\/(\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/(web|hpw)[o0]s/i],describe(n){const t={name:"WebOS Browser"},e=p.getFirstMatch(C,n)||p.getFirstMatch(/w(?:eb)?[o0]sbrowser\/(\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/bada/i],describe(n){const t={name:"Bada"},e=p.getFirstMatch(/dolfin\/(\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/tizen/i],describe(n){const t={name:"Tizen"},e=p.getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.?_?\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/qupzilla/i],describe(n){const t={name:"QupZilla"},e=p.getFirstMatch(/(?:qupzilla)[\s/](\d+(\.?_?\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/librewolf/i],describe(n){const t={name:"LibreWolf"},e=p.getFirstMatch(/(?:librewolf)[\s/](\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/firefox|iceweasel|fxios/i],describe(n){const t={name:"Firefox"},e=p.getFirstMatch(/(?:firefox|iceweasel|fxios)[\s/](\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/electron/i],describe(n){const t={name:"Electron"},e=p.getFirstMatch(/(?:electron)\/(\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/sogoumobilebrowser/i,/metasr/i,/se 2\.[x]/i],describe(n){const t={name:"Sogou Browser"},e=p.getFirstMatch(/(?:sogoumobilebrowser)[\s/](\d+(\.?_?\d+)+)/i,n),i=p.getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.?_?\d+)+)/i,n),o=p.getFirstMatch(/se ([\d.]+)x/i,n),r=e||i||o;return r&&(t.version=r),t}},{test:[/MiuiBrowser/i],describe(n){const t={name:"Miui"},e=p.getFirstMatch(/(?:MiuiBrowser)[\s/](\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test(n){return n.hasBrand("DuckDuckGo")?true:n.test(/\sDdg\/[\d.]+$/i)},describe(n,t){const e={name:"DuckDuckGo"};if(t){const o=t.getBrandVersion("DuckDuckGo");if(o)return e.version=o,e}const i=p.getFirstMatch(/\sDdg\/([\d.]+)$/i,n);return i&&(e.version=i),e}},{test(n){return n.hasBrand("Brave")},describe(n,t){const e={name:"Brave"};if(t){const i=t.getBrandVersion("Brave");if(i)return e.version=i,e}return e}},{test:[/chromium/i],describe(n){const t={name:"Chromium"},e=p.getFirstMatch(/(?:chromium)[\s/](\d+(\.?_?\d+)+)/i,n)||p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/chrome|crios|crmo/i],describe(n){const t={name:"Chrome"},e=p.getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/GSA/i],describe(n){const t={name:"Google Search"},e=p.getFirstMatch(/(?:GSA)\/(\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test(n){const t=!n.test(/like android/i),e=n.test(/android/i);return t&&e},describe(n){const t={name:"Android Browser"},e=p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/playstation 4/i],describe(n){const t={name:"PlayStation 4"},e=p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/safari|applewebkit/i],describe(n){const t={name:"Safari"},e=p.getFirstMatch(C,n);return e&&(t.version=e),t}},{test:[/.*/i],describe(n){const t=/^(.*)\/(.*) /,e=/^(.*)\/(.*)[ \t]\((.*)/,o=n.search("\\(")!==-1?e:t;return {name:p.getFirstMatch(o,n),version:p.getSecondMatch(o,n)}}}],Rm=[{test:[/Roku\/DVP/],describe(n){const t=p.getFirstMatch(/Roku\/DVP-(\d+\.\d+)/i,n);return {name:X.Roku,version:t}}},{test:[/windows phone/i],describe(n){const t=p.getFirstMatch(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i,n);return {name:X.WindowsPhone,version:t}}},{test:[/windows /i],describe(n){const t=p.getFirstMatch(/Windows ((NT|XP)( \d\d?.\d)?)/i,n),e=p.getWindowsVersionName(t);return {name:X.Windows,version:t,versionName:e}}},{test:[/Macintosh(.*?) FxiOS(.*?)\//],describe(n){const t={name:X.iOS},e=p.getSecondMatch(/(Version\/)(\d[\d.]+)/,n);return e&&(t.version=e),t}},{test:[/macintosh/i],describe(n){const t=p.getFirstMatch(/mac os x (\d+(\.?_?\d+)+)/i,n).replace(/[_\s]/g,"."),e=p.getMacOSVersionName(t),i={name:X.MacOS,version:t};return e&&(i.versionName=e),i}},{test:[/(ipod|iphone|ipad)/i],describe(n){const t=p.getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i,n).replace(/[_\s]/g,".");return {name:X.iOS,version:t}}},{test:[/OpenHarmony/i],describe(n){const t=p.getFirstMatch(/OpenHarmony\s+(\d+(\.\d+)*)/i,n);return {name:X.HarmonyOS,version:t}}},{test(n){const t=!n.test(/like android/i),e=n.test(/android/i);return t&&e},describe(n){const t=p.getFirstMatch(/android[\s/-](\d+(\.\d+)*)/i,n),e=p.getAndroidVersionName(t),i={name:X.Android,version:t};return e&&(i.versionName=e),i}},{test:[/(web|hpw)[o0]s/i],describe(n){const t=p.getFirstMatch(/(?:web|hpw)[o0]s\/(\d+(\.\d+)*)/i,n),e={name:X.WebOS};return t&&t.length&&(e.version=t),e}},{test:[/blackberry|\bbb\d+/i,/rim\stablet/i],describe(n){const t=p.getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i,n)||p.getFirstMatch(/blackberry\d+\/(\d+([_\s]\d+)*)/i,n)||p.getFirstMatch(/\bbb(\d+)/i,n);return {name:X.BlackBerry,version:t}}},{test:[/bada/i],describe(n){const t=p.getFirstMatch(/bada\/(\d+(\.\d+)*)/i,n);return {name:X.Bada,version:t}}},{test:[/tizen/i],describe(n){const t=p.getFirstMatch(/tizen[/\s](\d+(\.\d+)*)/i,n);return {name:X.Tizen,version:t}}},{test:[/linux/i],describe(){return {name:X.Linux}}},{test:[/CrOS/],describe(){return {name:X.ChromeOS}}},{test:[/PlayStation 4/],describe(n){const t=p.getFirstMatch(/PlayStation 4[/\s](\d+(\.\d+)*)/i,n);return {name:X.PlayStation4,version:t}}}],Bm=[{test:[/googlebot/i],describe(){return {type:A.bot,vendor:"Google"}}},{test:[/linespider/i],describe(){return {type:A.bot,vendor:"Line"}}},{test:[/amazonbot/i],describe(){return {type:A.bot,vendor:"Amazon"}}},{test:[/gptbot/i],describe(){return {type:A.bot,vendor:"OpenAI"}}},{test:[/chatgpt-user/i],describe(){return {type:A.bot,vendor:"OpenAI"}}},{test:[/oai-searchbot/i],describe(){return {type:A.bot,vendor:"OpenAI"}}},{test:[/baiduspider/i],describe(){return {type:A.bot,vendor:"Baidu"}}},{test:[/bingbot/i],describe(){return {type:A.bot,vendor:"Bing"}}},{test:[/duckduckbot/i],describe(){return {type:A.bot,vendor:"DuckDuckGo"}}},{test:[/claudebot/i,/claude-web/i,/claude-user/i,/claude-searchbot/i],describe(){return {type:A.bot,vendor:"Anthropic"}}},{test:[/omgilibot/i,/webzio-extended/i],describe(){return {type:A.bot,vendor:"Webz.io"}}},{test:[/diffbot/i],describe(){return {type:A.bot,vendor:"Diffbot"}}},{test:[/perplexitybot/i],describe(){return {type:A.bot,vendor:"Perplexity AI"}}},{test:[/perplexity-user/i],describe(){return {type:A.bot,vendor:"Perplexity AI"}}},{test:[/youbot/i],describe(){return {type:A.bot,vendor:"You.com"}}},{test:[/ia_archiver/i],describe(){return {type:A.bot,vendor:"Internet Archive"}}},{test:[/meta-webindexer/i],describe(){return {type:A.bot,vendor:"Meta"}}},{test:[/meta-externalads/i],describe(){return {type:A.bot,vendor:"Meta"}}},{test:[/meta-externalagent/i],describe(){return {type:A.bot,vendor:"Meta"}}},{test:[/meta-externalfetcher/i],describe(){return {type:A.bot,vendor:"Meta"}}},{test:[/facebookexternalhit/i,/facebookcatalog/i],describe(){return {type:A.bot,vendor:"Meta"}}},{test:[/slackbot/i,/slack-imgProxy/i],describe(){return {type:A.bot,vendor:"Slack"}}},{test:[/yahoo/i],describe(){return {type:A.bot,vendor:"Yahoo"}}},{test:[/yandexbot/i,/yandexmobilebot/i],describe(){return {type:A.bot,vendor:"Yandex"}}},{test:[/pingdom/i],describe(){return {type:A.bot,vendor:"Pingdom"}}},{test:[/huawei/i],describe(n){const t=p.getFirstMatch(/(can-l01)/i,n)&&"Nova",e={type:A.mobile,vendor:"Huawei"};return t&&(e.model=t),e}},{test:[/nexus\s*(?:7|8|9|10).*/i],describe(){return {type:A.tablet,vendor:"Nexus"}}},{test:[/ipad/i],describe(){return {type:A.tablet,vendor:"Apple",model:"iPad"}}},{test:[/Macintosh(.*?) FxiOS(.*?)\//],describe(){return {type:A.tablet,vendor:"Apple",model:"iPad"}}},{test:[/kftt build/i],describe(){return {type:A.tablet,vendor:"Amazon",model:"Kindle Fire HD 7"}}},{test:[/silk/i],describe(){return {type:A.tablet,vendor:"Amazon"}}},{test:[/tablet(?! pc)/i],describe(){return {type:A.tablet}}},{test(n){const t=n.test(/ipod|iphone/i),e=n.test(/like (ipod|iphone)/i);return t&&!e},describe(n){const t=p.getFirstMatch(/(ipod|iphone)/i,n);return {type:A.mobile,vendor:"Apple",model:t}}},{test:[/nexus\s*[0-6].*/i,/galaxy nexus/i],describe(){return {type:A.mobile,vendor:"Nexus"}}},{test:[/Nokia/i],describe(n){const t=p.getFirstMatch(/Nokia\s+([0-9]+(\.[0-9]+)?)/i,n),e={type:A.mobile,vendor:"Nokia"};return t&&(e.model=t),e}},{test:[/[^-]mobi/i],describe(){return {type:A.mobile}}},{test(n){return n.getBrowserName(true)==="blackberry"},describe(){return {type:A.mobile,vendor:"BlackBerry"}}},{test(n){return n.getBrowserName(true)==="bada"},describe(){return {type:A.mobile}}},{test(n){return n.getBrowserName()==="windows phone"},describe(){return {type:A.mobile,vendor:"Microsoft"}}},{test(n){const t=Number(String(n.getOSVersion()).split(".")[0]);return n.getOSName(true)==="android"&&t>=3},describe(){return {type:A.tablet}}},{test(n){return n.getOSName(true)==="android"},describe(){return {type:A.mobile}}},{test:[/smart-?tv|smarttv/i],describe(){return {type:A.tv}}},{test:[/netcast/i],describe(){return {type:A.tv}}},{test(n){return n.getOSName(true)==="macos"},describe(){return {type:A.desktop,vendor:"Apple"}}},{test(n){return n.getOSName(true)==="windows"},describe(){return {type:A.desktop}}},{test(n){return n.getOSName(true)==="linux"},describe(){return {type:A.desktop}}},{test(n){return n.getOSName(true)==="playstation 4"},describe(){return {type:A.tv}}},{test(n){return n.getOSName(true)==="roku"},describe(){return {type:A.tv}}}],Fm=[{test(n){return n.getBrowserName(true)==="microsoft edge"},describe(n){if(/\sedg\//i.test(n))return {name:wt.Blink};const e=p.getFirstMatch(/edge\/(\d+(\.?_?\d+)+)/i,n);return {name:wt.EdgeHTML,version:e}}},{test:[/trident/i],describe(n){const t={name:wt.Trident},e=p.getFirstMatch(/trident\/(\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test(n){return n.test(/presto/i)},describe(n){const t={name:wt.Presto},e=p.getFirstMatch(/presto\/(\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test(n){const t=n.test(/gecko/i),e=n.test(/like gecko/i);return t&&!e},describe(n){const t={name:wt.Gecko},e=p.getFirstMatch(/gecko\/(\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}},{test:[/(apple)?webkit\/537\.36/i],describe(){return {name:wt.Blink}}},{test:[/(apple)?webkit/i],describe(n){const t={name:wt.WebKit},e=p.getFirstMatch(/webkit\/(\d+(\.?_?\d+)+)/i,n);return e&&(t.version=e),t}}];class hr{constructor(t,e=false,i=null){if(t==null||t==="")throw new Error("UserAgent parameter can't be empty");this._ua=t;let o=false;typeof e=="boolean"?(o=e,this._hints=i):e!=null&&typeof e=="object"?this._hints=e:this._hints=null,this.parsedResult={},o!==true&&this.parse();}getHints(){return this._hints}hasBrand(t){if(!this._hints||!Array.isArray(this._hints.brands))return false;const e=t.toLowerCase();return this._hints.brands.some(i=>i.brand&&i.brand.toLowerCase()===e)}getBrandVersion(t){if(!this._hints||!Array.isArray(this._hints.brands))return;const e=t.toLowerCase(),i=this._hints.brands.find(o=>o.brand&&o.brand.toLowerCase()===e);return i?i.version:void 0}getUA(){return this._ua}test(t){return t.test(this._ua)}parseBrowser(){this.parsedResult.browser={};const t=p.find(Dm,e=>{if(typeof e.test=="function")return e.test(this);if(Array.isArray(e.test))return e.test.some(i=>this.test(i));throw new Error("Browser's test function is not valid")});return t&&(this.parsedResult.browser=t.describe(this.getUA(),this)),this.parsedResult.browser}getBrowser(){return this.parsedResult.browser?this.parsedResult.browser:this.parseBrowser()}getBrowserName(t){return t?String(this.getBrowser().name).toLowerCase()||"":this.getBrowser().name||""}getBrowserVersion(){return this.getBrowser().version}getOS(){return this.parsedResult.os?this.parsedResult.os:this.parseOS()}parseOS(){this.parsedResult.os={};const t=p.find(Rm,e=>{if(typeof e.test=="function")return e.test(this);if(Array.isArray(e.test))return e.test.some(i=>this.test(i));throw new Error("Browser's test function is not valid")});return t&&(this.parsedResult.os=t.describe(this.getUA())),this.parsedResult.os}getOSName(t){const{name:e}=this.getOS();return t?String(e).toLowerCase()||"":e||""}getOSVersion(){return this.getOS().version}getPlatform(){return this.parsedResult.platform?this.parsedResult.platform:this.parsePlatform()}getPlatformType(t=false){const{type:e}=this.getPlatform();return t?String(e).toLowerCase()||"":e||""}parsePlatform(){this.parsedResult.platform={};const t=p.find(Bm,e=>{if(typeof e.test=="function")return e.test(this);if(Array.isArray(e.test))return e.test.some(i=>this.test(i));throw new Error("Browser's test function is not valid")});return t&&(this.parsedResult.platform=t.describe(this.getUA())),this.parsedResult.platform}getEngine(){return this.parsedResult.engine?this.parsedResult.engine:this.parseEngine()}getEngineName(t){return t?String(this.getEngine().name).toLowerCase()||"":this.getEngine().name||""}parseEngine(){this.parsedResult.engine={};const t=p.find(Fm,e=>{if(typeof e.test=="function")return e.test(this);if(Array.isArray(e.test))return e.test.some(i=>this.test(i));throw new Error("Browser's test function is not valid")});return t&&(this.parsedResult.engine=t.describe(this.getUA())),this.parsedResult.engine}parse(){return this.parseBrowser(),this.parseOS(),this.parsePlatform(),this.parseEngine(),this}getResult(){return p.assign({},this.parsedResult)}satisfies(t){const e={};let i=0;const o={};let r=0;if(Object.keys(t).forEach(a=>{const l=t[a];typeof l=="string"?(o[a]=l,r+=1):typeof l=="object"&&(e[a]=l,i+=1);}),i>0){const a=Object.keys(e),l=p.find(a,u=>this.isOS(u));if(l){const u=this.satisfies(e[l]);if(u!==void 0)return u}const c=p.find(a,u=>this.isPlatform(u));if(c){const u=this.satisfies(e[c]);if(u!==void 0)return u}}if(r>0){const a=Object.keys(o),l=p.find(a,c=>this.isBrowser(c,true));if(l!==void 0)return this.compareVersion(o[l])}}isBrowser(t,e=false){const i=this.getBrowserName().toLowerCase();let o=t.toLowerCase();const r=p.getBrowserTypeByAlias(o);return e&&r&&(o=r.toLowerCase()),o===i}compareVersion(t){let e=[0],i=t,o=false;const r=this.getBrowserVersion();if(typeof r=="string")return t[0]===">"||t[0]==="<"?(i=t.substr(1),t[1]==="="?(o=true,i=t.substr(2)):e=[],t[0]===">"?e.push(1):e.push(-1)):t[0]==="="?i=t.substr(1):t[0]==="~"&&(o=true,i=t.substr(1)),e.indexOf(p.compareVersions(r,i,o))>-1}isOS(t){return this.getOSName(true)===String(t).toLowerCase()}isPlatform(t){return this.getPlatformType(true)===String(t).toLowerCase()}isEngine(t){return this.getEngineName(true)===String(t).toLowerCase()}is(t,e=false){return this.isBrowser(t,e)||this.isOS(t)||this.isPlatform(t)}some(t=[]){return t.some(e=>this.is(e))}}class Nm{static getParser(t,e=false,i=null){if(typeof t!="string")throw new Error("UserAgent should be a string");return new hr(t,e,i)}static parse(t,e=null){return new hr(t,e).getResult()}static get BROWSER_MAP(){return Ks}static get ENGINE_MAP(){return wt}static get OS_MAP(){return X}static get PLATFORMS_MAP(){return A}}const ze=Nm.getParser(globalThis.navigator.userAgent).getResult(),pe="unknown",fr=(...n)=>n.filter(Boolean).join(" ").trim()||pe;function Ys(){const n=fr(ze.os?.name,ze.os?.version),t=fr(ze.browser?.name,ze.browser?.version),e=(()=>{const s=GM_info?.scriptHandler,a=GM_info?.version;return s&&a?`${s} v${a}`:s||a||pe})(),i=GM_info?.script?.version??pe,o=GM_info?.script?.name??pe,r=globalThis?.location?.href??pe;return {os:n,browser:t,loader:e,scriptVersion:i,scriptName:o,url:r}}class $m{container;accountWrapper;buttons;usernameEl;avatarEl;avatarImg;actionButton;refreshButton;tokenButton;onClick=new N;onRefresh=new N;onClickSecret=new N;events={click:this.onClick,"click:secret":this.onClickSecret,refresh:this.onRefresh};_loggedIn;_username;_avatarId;constructor({loggedIn:t=false,username:e="unnamed",avatarId:i="0/0-0"}={}){this._loggedIn=t,this._username=e,this._avatarId=i;const o=this.createElements();this.container=o.container,this.accountWrapper=o.accountWrapper,this.buttons=o.buttons,this.usernameEl=o.usernameEl,this.avatarEl=o.avatarEl,this.avatarImg=o.avatarImg,this.actionButton=o.actionButton,this.refreshButton=o.refreshButton,this.tokenButton=o.tokenButton;}createElements(){const t=S.createEl("vot-block",["vot-account"]),e=S.createEl("vot-block",["vot-account-wrapper"]);e.hidden=!this._loggedIn;const i=S.createEl("img",["vot-account-avatar-img"]);i.src=`${po}/${this._avatarId}/islands-retina-middle`,i.loading="lazy",i.alt="user avatar";const o=S.createEl("vot-block",["vot-account-avatar"],i),r=S.createEl("vot-block",["vot-account-username"]);r.textContent=this._username,e.append(o,r);const s=S.createEl("vot-block",["vot-account-buttons"]),a=S.createOutlinedButton(this.buttonText);a.addEventListener("click",()=>{this.onClick.dispatch();});const l=S.createIconButton(Im,{ariaLabel:v.get("VOTLoginViaToken")});l.hidden=this._loggedIn,l.addEventListener("click",()=>{this.onClickSecret.dispatch();});const c=S.createIconButton(Am,{ariaLabel:v.get("VOTRefresh")});return c.addEventListener("click",()=>{this.onRefresh.dispatch();}),s.append(a,l,c),t.append(e,s),{container:t,accountWrapper:e,buttons:s,usernameEl:r,avatarImg:i,avatarEl:o,actionButton:a,refreshButton:c,tokenButton:l}}addEventListener(t,e){return At(this.events,t,e),this}removeEventListener(t,e){return It(this.events,t,e),this}get buttonText(){return this._loggedIn?v.get("VOTLogout"):v.get("VOTLogin")}get loggedIn(){return this._loggedIn}set loggedIn(t){this._loggedIn=t,this.accountWrapper.hidden=!this._loggedIn,this.actionButton.textContent=this.buttonText,this.tokenButton.hidden=this._loggedIn;}get avatarId(){return this._avatarId}set avatarId(t){this._avatarId=t??"0/0-0",this.avatarImg.src=`${po}/${this._avatarId}/islands-retina-middle`;}get username(){return this._username}set username(t){this._username=t??"unnamed",this.usernameEl.textContent=this._username;}set hidden(t){st(this.container,t);}get hidden(){return at(this.container)}}class W{container;input;label;onChange=new N;events={change:this.onChange};_labelHtml;_checked;_isSubCheckbox;constructor({labelHtml:t,checked:e=false,isSubCheckbox:i=false}){this._labelHtml=t,this._checked=e,this._isSubCheckbox=i;const o=this.createElements();this.container=o.container,this.input=o.input,this.label=o.label;}createElements(){const t=S.createEl("label",["vot-checkbox"]);this._isSubCheckbox&&t.classList.add("vot-checkbox-sub");const e=document.createElement("input");e.type="checkbox",e.checked=this._checked,e.addEventListener("change",()=>{this._checked=e.checked,this.onChange.dispatch(this._checked);});const i=S.createEl("span");return it(this._labelHtml,i),t.append(e,i),{container:t,input:e,label:i}}addEventListener(t,e){return At(this.events,"change",e),this}removeEventListener(t,e){return It(this.events,"change",e),this}set hidden(t){st(this.container,t);}get hidden(){return at(this.container)}get disabled(){return this.input.disabled}set disabled(t){this.input.disabled=t;}get checked(){return this._checked}set checked(t){this._checked!==t&&(this._checked=this.input.checked=t,this.onChange.dispatch(this._checked));}}class Hm{container;header;arrowIcon;onClick=new N;events={click:this.onClick};_titleHtml;constructor({titleHtml:t}){this._titleHtml=t;const e=this.createElements();this.container=e.container,this.header=e.header,this.arrowIcon=e.arrowIcon;}createElements(){const t=S.createEl("vot-block",["vot-details"]);S.makeButtonLike(t);const e=S.createEl("vot-block");e.append(this._titleHtml);const i=S.createEl("vot-block",["vot-details-arrow-icon"]);return it(Gs,i),t.append(e,i),t.addEventListener("click",()=>{this.onClick.dispatch();}),{container:t,header:e,arrowIcon:i}}addEventListener(t,e){return At(this.events,"click",e),this}removeEventListener(t,e){return It(this.events,"click",e),this}set hidden(t){st(this.container,t);}get hidden(){return at(this.container)}}class pr{container;button;onChange=new N;events={change:this.onChange};_labelHtml;_key;pressedKeys;comboKeys;recording=false;constructor({labelHtml:t,key:e=null}){this._labelHtml=t,this._key=e,this.pressedKeys=new Set,this.comboKeys=new Set;const i=this.createElements();this.container=i.container,this.button=i.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=t=>{if(!(!this.recording||t.repeat)){if(t.preventDefault(),t.code==="Escape"){this.key=null,this.button.textContent=this.keyText,this.stopRecordingKeys();return}this.pressedKeys.add(t.code),this.comboKeys.add(t.code),this.button.textContent=We(this.pressedKeys);}};keyupOrBlurHandle=t=>{this.recording&&(t&&(this.pressedKeys.delete(t.code),this.button.textContent=this.pressedKeys.size?We(this.pressedKeys):We(this.comboKeys),this.pressedKeys.size)||(this.key=this.comboKeys.size?Um(this.comboKeys):null,this.stopRecordingKeys()));};blurHandle=()=>{this.keyupOrBlurHandle();};createElements(){const t=S.createEl("vot-block",["vot-hotkey"]),e=S.createEl("vot-block",["vot-hotkey-label"]);e.textContent=this._labelHtml;const i=S.createEl("vot-block",["vot-hotkey-button"]);return S.makeButtonLike(i),i.textContent=this.keyText,i.addEventListener("click",()=>{if(this.recording){this.stopRecordingKeys(),this.button.textContent=this.keyText;return}i.dataset.status="active",this.recording=true,this.pressedKeys.clear(),this.comboKeys.clear(),this.button.textContent=v.get("PressTheKeyCombination"),document.addEventListener("keydown",this.keydownHandle),document.addEventListener("keyup",this.keyupOrBlurHandle),globalThis.addEventListener("blur",this.blurHandle);}),t.append(e,i),{container:t,button:i,label:e}}addEventListener(t,e){return At(this.events,"change",e),this}removeEventListener(t,e){return It(this.events,"change",e),this}set hidden(t){st(this.container,t);}get hidden(){return at(this.container)}get key(){return this._key}get keyText(){return this._key?We(this._key):v.get("None")}set key(t){this._key!==t&&(this._key=t,this.button.textContent=this.keyText,this.onChange.dispatch(this._key));}}function Um(n){return (Array.isArray(n)?n:Array.from(n)).map(e=>e.replace("Key","").replace("Digit","")).join("+")}function We(n){let t;typeof n=="string"?t=n.split("+").filter(Boolean):Array.isArray(n)?t=n:t=Array.from(n);const e=o=>{switch(o){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 o.replace("Key","").replace("Digit","")}},i=o=>{const r=e(o);return r==="Ctrl"?0:r==="Alt"?1:r==="Shift"?2:r==="Meta"?3:10};return t.slice().sort((o,r)=>i(o)-i(r)).map(e).join("+")}const zm=["click:bugReport","click:resetSettings","update:account","change:autoTranslate","change:autoSubtitles","change:showVideoVolume","change:audioBooster","change:syncVolume","change:useLivelyVoice","change:subtitlesHighlightWords","change:subtitlesSmartLayout","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 Wm(){const n={};for(const t of zm)n[t]=new N;return n}const qm=30,Xs={"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 gr(n){return en(n)?Xs[n]:ee(n)??"Default Sans"}class Ti{static PERSIST_DELAY_MS=250;globalPortal;initialized=false;data;videoHandler;suppressSubtitlesSmartLayoutCheckboxChange=false;events=Wm();persistTimerIds={};dialog;accountButton;accountButtonRefreshTooltip;accountButtonTokenTooltip;autoTranslateCheckbox;autoSubtitlesCheckbox;dontTranslateLanguagesCheckbox;dontTranslateLanguagesSelect;autoSetVolumeSliderLabel;autoSetVolumeCheckbox;smartDuckingCheckbox;autoSetVolumeSlider;showVideoVolumeSliderCheckbox;audioBoosterCheckbox;audioBoosterTooltip;syncVolumeCheckbox;downloadWithNameCheckbox;sendNotifyOnCompleteCheckbox;useLivelyVoiceCheckbox;useLivelyVoiceTooltip;useAudioDownloadCheckbox;useAudioDownloadCheckboxLabel;useAudioDownloadCheckboxTooltip;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:t,data:e={},videoHandler:i}){this.globalPortal=t,this.data=e,this.videoHandler=i;}isInitialized(){return this.initialized}createAccordionSection(t,e={}){const i=S.createEl("vot-block",["vot-settings-section"]),o=new Hm({titleHtml:t});o.container.classList.add("vot-settings-section-header");const r=typeof crypto<"u"&&"randomUUID"in crypto?crypto.randomUUID():`${Date.now()}-${Math.random().toString(16).slice(2)}`,s=`vot-settings-section-header-${r}`,a=`vot-settings-section-content-${r}`;o.container.id=s;const l=S.createEl("vot-block",["vot-settings-section-content"]);l.id=a,l.setAttribute("role","region"),l.setAttribute("aria-labelledby",s),o.container.setAttribute("aria-controls",a);const c=d=>{o.container.dataset.open=d?"true":"false",o.container.setAttribute("aria-expanded",d?"true":"false"),l.hidden=!d;},u=()=>o.container.dataset.open==="true";return c(!!e.open),o.addEventListener("click",()=>{const d=o.container.dataset.open==="true";c(!d);}),i.append(o.container,l),{title:t,container:i,header:o.container,content:l,setOpen:c,getOpen:u}}setSubtitlesSmartLayout(t){this.data.subtitlesSmartLayout=t,I.set("subtitlesSmartLayout",t),this.subtitlesSmartLayoutCheckbox?.checked!==t&&(this.suppressSubtitlesSmartLayoutCheckboxChange=true,this.subtitlesSmartLayoutCheckbox.checked=t,this.suppressSubtitlesSmartLayoutCheckboxChange=false),this.events["change:subtitlesSmartLayout"].dispatch(t);}scheduleStoragePersist(t,e){const i=this.persistTimerIds[t];i!==void 0&&globalThis.clearTimeout(i),this.persistTimerIds[t]=globalThis.setTimeout(()=>{this.persistTimerIds[t]=void 0,I.set(t,e);},Ti.PERSIST_DELAY_MS);}flushStoragePersists(){for(const t of Object.keys(this.persistTimerIds)){const e=this.persistTimerIds[t];if(e===void 0)continue;globalThis.clearTimeout(e),this.persistTimerIds[t]=void 0;const i=this.data[t];typeof i=="number"&&I.set(t,i);}}bindPersistedSetting({control:t,event:e,apply:i,storageKey:o,readPersistedValue:r,logLabel:s,dispatch:a,afterPersist:l}){t.addEventListener(e,async c=>{i(c),await I.set(o,r()),l&&await l(c),a?.(c);});}initUI(){if(this.isInitialized())throw new Error("[VOT] SettingsView is already initialized");this.dialog=new Zn({titleHtml:v.get("VOTSettings")}),this.globalPortal.appendChild(this.dialog.container);const t=this.createAccordionSection(v.get("VOTMyAccount"),{open:true}),e=this.createAccordionSection(v.get("translationSettings"),{open:true}),i=this.createAccordionSection(v.get("subtitlesSettings")),o=this.createAccordionSection(v.get("hotkeysSettings")),r=this.createAccordionSection(v.get("proxySettings")),s=this.createAccordionSection(v.get("miscSettings")),a=this.createAccordionSection(v.get("appearance")),l=this.createAccordionSection(v.get("aboutExtension")),c=[t,e,i,o,r,s,a,l];this.dialog.bodyContainer.append(...c.map(O=>O.container)),this.accountButton=new $m({avatarId:this.data.account?.avatarId,username:this.data.account?.username,loggedIn:!!this.data.account?.token}),I.isSupportOnlyLS?(this.accountButton.refreshButton.setAttribute("disabled","true"),this.accountButton.actionButton.setAttribute("disabled","true")):this.accountButtonRefreshTooltip=new K({target:this.accountButton.refreshButton,content:v.get("VOTRefresh"),position:"bottom",backgroundColor:"var(--vot-helper-ondialog)",parentElement:this.globalPortal}),this.accountButtonTokenTooltip=new K({target:this.accountButton.tokenButton,content:v.get("VOTLoginViaToken"),position:"bottom",backgroundColor:"var(--vot-helper-ondialog)",parentElement:this.globalPortal}),this.autoTranslateCheckbox=new W({labelHtml:v.get("VOTAutoTranslate"),checked:this.data.autoTranslate}),this.autoSubtitlesCheckbox=new W({labelHtml:v.get("VOTAutoSubtitles"),checked:this.data.autoSubtitles});const u=this.data.dontTranslateLanguages??[];this.dontTranslateLanguagesCheckbox=new W({labelHtml:v.get("DontTranslateSelectedLanguages"),checked:this.data.enabledDontTranslateLanguages}),this.dontTranslateLanguagesSelect=new Z({dialogParent:this.globalPortal,dialogTitle:v.get("DontTranslateSelectedLanguages"),selectTitle:u.map(O=>v.get(`langs.${O}`)).join(", ")||v.get("DontTranslateSelectedLanguages"),items:Z.genLanguageItems(Lt).map(O=>({...O,selected:u.includes(O.value)})),multiSelect:true,labelElement:this.dontTranslateLanguagesCheckbox.container}),this.dontTranslateLanguagesSelect.disabled=!this.dontTranslateLanguagesCheckbox.checked;const d=this.data.autoVolume??fn;this.autoSetVolumeSliderLabel=new Pt({labelText:v.get("VOTAutoSetVolume"),value:d}),this.autoSetVolumeCheckbox=new W({labelHtml:this.autoSetVolumeSliderLabel.container,checked:this.data.enabledAutoVolume??true}),this.autoSetVolumeSlider=new Et({labelHtml:this.autoSetVolumeCheckbox.container,value:d,min:0});const h=!!this.data.syncVolume;this.autoSetVolumeSlider.disabled=!this.autoSetVolumeCheckbox.checked,this.smartDuckingCheckbox=new W({labelHtml:v.get("smartDucking"),checked:this.data.enabledSmartDucking??true}),this.smartDuckingCheckbox.disabled=h||!this.autoSetVolumeCheckbox.checked,this.showVideoVolumeSliderCheckbox=new W({labelHtml:v.get("showVideoVolumeSlider"),checked:this.data.showVideoSlider}),this.audioBoosterCheckbox=new W({labelHtml:v.get("VOTAudioBooster"),checked:this.data.audioBooster}),this.videoHandler?.isAudioContextSupported||(this.audioBoosterCheckbox.disabled=true,this.audioBoosterTooltip=new K({target:this.audioBoosterCheckbox.container,content:v.get("VOTNeedWebAudioAPI"),position:"bottom",backgroundColor:"var(--vot-helper-ondialog)",parentElement:this.globalPortal})),this.syncVolumeCheckbox=new W({labelHtml:v.get("VOTSyncVolume"),checked:this.data.syncVolume}),this.downloadWithNameCheckbox=new W({labelHtml:v.get("VOTDownloadWithName"),checked:this.data.downloadWithName}),this.downloadWithNameCheckbox.disabled=!xe,this.sendNotifyOnCompleteCheckbox=new W({labelHtml:v.get("VOTSendNotifyOnComplete"),checked:this.data.sendNotifyOnComplete}),this.useLivelyVoiceCheckbox=new W({labelHtml:v.get("VOTUseLivelyVoice"),checked:this.data.useLivelyVoice}),this.useLivelyVoiceTooltip=new K({target:this.useLivelyVoiceCheckbox.container,content:v.get("VOTAccountRequired"),position:"bottom",backgroundColor:"var(--vot-helper-ondialog)",parentElement:this.globalPortal,hidden:!!this.data.account?.token}),this.data.account?.token||(this.useLivelyVoiceCheckbox.disabled=true),this.useAudioDownloadCheckboxLabel=new pt({labelText:v.get("VOTUseAudioDownload"),icon:ur}),this.useAudioDownloadCheckbox=new W({labelHtml:this.useAudioDownloadCheckboxLabel.container,checked:this.data.useAudioDownload}),xe||(this.useAudioDownloadCheckbox.disabled=true),this.useAudioDownloadCheckboxTooltip=new K({target:this.useAudioDownloadCheckboxLabel.container,content:v.get("VOTUseAudioDownloadWarning"),position:"bottom",backgroundColor:"var(--vot-helper-ondialog)",parentElement:this.globalPortal}),t.content.append(this.accountButton.container),e.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 pt({labelText:v.get("VOTSubtitlesDownloadFormat")}),this.subtitlesDownloadFormatSelect=new Z({selectTitle:this.data.subtitlesDownloadFormat??v.get("VOTSubtitlesDownloadFormat"),dialogTitle:v.get("VOTSubtitlesDownloadFormat"),dialogParent:this.globalPortal,labelElement:this.subtitlesDownloadFormatSelectLabel.container,items:Kp.map(O=>({label:O.toUpperCase(),value:O,selected:O===this.data.subtitlesDownloadFormat}))}),this.subtitlesHighlightWordsCheckbox=new W({labelHtml:v.get("VOTHighlightWords"),checked:this.data.highlightWords});const f=this.data.subtitlesSmartLayout??true;this.subtitlesSmartLayoutCheckbox=new W({labelHtml:v.get("subtitlesSmartLayout"),checked:f});const g=this.data.subtitlesMaxLength??300;this.subtitlesMaxLengthSliderLabel=new Pt({labelText:v.get("VOTSubtitlesMaxLength"),labelEOL:":",value:g,symbol:""}),this.subtitlesMaxLengthSlider=new Et({labelHtml:this.subtitlesMaxLengthSliderLabel.container,value:g,min:50,max:300});const y=this.data.subtitlesFontSize??20;this.subtitlesFontSizeSliderLabel=new Pt({labelText:v.get("VOTSubtitlesFontSize"),labelEOL:":",value:y,symbol:"px"}),this.subtitlesFontSizeSlider=new Et({labelHtml:this.subtitlesFontSizeSliderLabel.container,value:y,min:8,max:50});const w=typeof this.data.subtitlesFontFamily=="string"?this.data.subtitlesFontFamily:void 0,x=w&&(en(w)||ee(w))?w:"default-sans",m=(O,mt=[])=>{const re=Ns.map(j=>({label:Xs[j],value:j,selected:j===O})),De=mt.filter(j=>{const $t=j.toLowerCase();return !re.some(xa=>xa.label.toLowerCase()===$t)}).map(j=>{const $t=jp(j);return {label:j,value:$t,selected:$t===O}});if(!en(O)&&!De.some(j=>j.value===O)){const j=ee(O);j&&De.unshift({label:j,value:O,selected:true});}return [...re,...De]};this.subtitlesFontFamilySelectLabel=new pt({labelText:v.get("VOTSubtitlesFont")}),this.subtitlesFontFamilySelect=new Z({selectTitle:gr(x),dialogTitle:v.get("VOTSubtitlesFont"),dialogParent:this.globalPortal,labelElement:this.subtitlesFontFamilySelectLabel.container,items:m(x),searchItemsProvider:async O=>{const mt=Array.from(this.subtitlesFontFamilySelect?.selectedValues??[])[0]??x,re=O.trim().toLowerCase();if(!re)return m(mt);const j=(await eg()).filter($t=>$t.toLowerCase().includes(re)).slice(0,qm);return m(mt,j)}}),this.subtitlesFontFamilySelect.addEventListener("selectItem",O=>{this.subtitlesFontFamilySelect&&(this.subtitlesFontFamilySelect.updateItems(m(O)),this.subtitlesFontFamilySelect.selectTitle=gr(O));});const k=this.data.subtitlesOpacity??20;this.subtitlesBackgroundOpacitySliderLabel=new Pt({labelText:v.get("VOTSubtitlesOpacity"),labelEOL:":",value:k,symbol:"%"}),this.subtitlesBackgroundOpacitySlider=new Et({labelHtml:this.subtitlesBackgroundOpacitySliderLabel.container,value:k,min:0,max:100}),i.content.append(this.subtitlesDownloadFormatSelect.container,this.subtitlesFontFamilySelect.container,this.subtitlesHighlightWordsCheckbox.container,this.subtitlesSmartLayoutCheckbox.container,this.subtitlesMaxLengthSlider.container,this.subtitlesFontSizeSlider.container,this.subtitlesBackgroundOpacitySlider.container),this.translateHotkeyButton=new pr({labelHtml:v.get("translateVideo"),key:this.data.translationHotkey}),this.subtitlesHotkeyButton=new pr({labelHtml:v.get("VOTSubtitles"),key:this.data.subtitlesHotkey}),o.content.append(this.translateHotkeyButton.container,this.subtitlesHotkeyButton.container),this.proxyWorkerHostTextfield=new Qn({labelHtml:v.get("VOTProxyWorkerHost"),value:this.data.proxyWorkerHost,placeholder:we});const P=[v.get("VOTTranslateProxyDisabled"),v.get("VOTTranslateProxyEnabled"),v.get("VOTTranslateProxyEverything")],D=this.data.translateProxyEnabled??0,F=ie&&cs.includes(ie);this.proxyTranslationStatusSelectLabel=new pt({icon:F?ur:void 0,labelText:v.get("VOTTranslateProxyStatus")}),F&&(this.proxyTranslationStatusSelectTooltip=new K({target:this.proxyTranslationStatusSelectLabel.icon,content:v.get("VOTTranslateProxyStatusDefault"),position:"bottom",backgroundColor:"var(--vot-helper-ondialog)",parentElement:this.globalPortal})),this.proxyTranslationStatusSelect=new Z({selectTitle:P[D],dialogTitle:v.get("VOTTranslateProxyStatus"),dialogParent:this.globalPortal,labelElement:this.proxyTranslationStatusSelectLabel.container,items:P.map((O,mt)=>({label:O,value:mt.toString(),selected:mt===D,disabled:mt===0&&ms}))}),r.content.append(this.proxyWorkerHostTextfield.container,this.proxyTranslationStatusSelect.container),this.translateAPIErrorsCheckbox=new W({labelHtml:v.get("VOTTranslateAPIErrors"),checked:this.data.translateAPIErrors??true}),this.translateAPIErrorsCheckbox.hidden=v.lang==="ru",this.useNewAudioPlayerCheckbox=new W({labelHtml:v.get("VOTNewAudioPlayer"),checked:this.data.newAudioPlayer}),this.videoHandler?.isAudioContextSupported||(this.useNewAudioPlayerCheckbox.disabled=true,this.useNewAudioPlayerTooltip=new K({target:this.useNewAudioPlayerCheckbox.container,content:v.get("VOTNeedWebAudioAPI"),position:"bottom",backgroundColor:"var(--vot-helper-ondialog)",parentElement:this.globalPortal}));const lt=this.videoHandler?.site.needBypassCSP?`${v.get("VOTOnlyBypassMediaCSP")} (${v.get("VOTMediaCSPEnabledOnSite")})`:v.get("VOTOnlyBypassMediaCSP");this.onlyBypassMediaCSPCheckbox=new W({labelHtml:lt,checked:this.data.onlyBypassMediaCSP,isSubCheckbox:true}),this.videoHandler?.isAudioContextSupported||(this.onlyBypassMediaCSPTooltip=new K({target:this.onlyBypassMediaCSPCheckbox.container,content:v.get("VOTNeedWebAudioAPI"),position:"bottom",backgroundColor:"var(--vot-helper-ondialog)",parentElement:this.globalPortal})),this.onlyBypassMediaCSPCheckbox.disabled=!this.data.newAudioPlayer&&!!this.videoHandler?.isAudioContextSupported,this.data.newAudioPlayer||(this.onlyBypassMediaCSPCheckbox.hidden=true),this.translationTextServiceLabel=new pt({labelText:v.get("VOTTranslationTextService"),icon:dr});const tt=this.data.translationService??pn;this.translationTextServiceSelect=new Z({selectTitle:v.get(`services.${tt}`),dialogTitle:v.get("VOTTranslationTextService"),dialogParent:this.globalPortal,labelElement:this.translationTextServiceLabel.container,items:Es.map(O=>({label:v.get(`services.${O}`),value:O,selected:O===tt}))}),this.translationTextServiceTooltip=new K({target:this.translationTextServiceLabel.icon,content:v.get("VOTNotAffectToVoice"),position:"bottom",backgroundColor:"var(--vot-helper-ondialog)",parentElement:this.globalPortal}),this.detectServiceLabel=new pt({labelText:v.get("VOTDetectService")});const gt=this.data.detectService??di;this.detectServiceSelect=new Z({selectTitle:v.get(`services.${gt}`),dialogTitle:v.get("VOTDetectService"),dialogParent:this.globalPortal,labelElement:this.detectServiceLabel.container,items:gp.map(O=>({label:v.get(`services.${O}`),value:O,selected:O===gt}))}),this.showPiPButtonCheckbox=new W({labelHtml:v.get("VOTShowPiPButton"),checked:this.data.showPiPButton}),this.showPiPButtonCheckbox.hidden=!us();const Ei=Math.round((this.data.autoHideButtonDelay??hi)/1e3*10)/10;this.autoHideButtonDelaySliderLabel=new Pt({labelText:v.get("autoHideButtonDelay"),labelEOL:":",value:Ei,symbol:` ${v.get("secs")}`}),this.autoHideButtonDelaySlider=new Et({labelHtml:this.autoHideButtonDelaySliderLabel.container,value:Ei,min:.1,max:3,step:.1}),this.buttonPositionSelectLabel=new pt({labelText:v.get("buttonPosition"),icon:dr});const Pi=this.data.buttonPos??"default";this.buttonPositionSelect=new Z({selectTitle:v.get(`position.${Pi}`),dialogTitle:v.get("buttonPosition"),labelElement:this.buttonPositionSelectLabel.container,dialogParent:this.globalPortal,items:_m.map(O=>({label:v.get(`position.${O}`),value:O,selected:O===Pi}))}),this.buttonPositionTooltip=new K({target:this.buttonPositionSelectLabel.icon,content:v.get("minButtonPositionContainer"),position:"bottom",backgroundColor:"var(--vot-helper-ondialog)",parentElement:this.globalPortal}),this.menuLanguageSelectLabel=new pt({labelText:v.get("VOTMenuLanguage")}),this.menuLanguageSelect=new Z({selectTitle:v.get(`langs.${v.langOverride}`),dialogTitle:v.get("VOTMenuLanguage"),labelElement:this.menuLanguageSelectLabel.container,dialogParent:this.globalPortal,items:Z.genLanguageItems(v.getAvailableLangs(),v.langOverride)}),this.bugReportButton=S.createOutlinedButton(v.get("VOTBugReport")),this.resetSettingsButton=S.createButton(v.get("resetSettings")),s.content.append(this.translateAPIErrorsCheckbox.container,this.useNewAudioPlayerCheckbox.container,this.onlyBypassMediaCSPCheckbox.container),e.content.append(this.translationTextServiceSelect.container,this.detectServiceSelect.container),a.content.append(this.showPiPButtonCheckbox.container,this.autoHideButtonDelaySlider.container,this.buttonPositionSelect.container,this.menuLanguageSelect.container);const Oe=Ys(),pa=S.createInformation(`${v.get("VOTVersion")}:`,Oe.scriptVersion||GM_info.script.version||v.get("notFound")),ga=S.createInformation(`${v.get("VOTAuthors")}:`,GM_info.script.author||"Toil, SashaXser, MrSoczekXD, mynovelhost, sodapng"),ma=S.createInformation(`${v.get("VOTLoader")}:`,Oe.loader),va=S.createInformation(`${v.get("VOTBrowser")}:`,`${Oe.browser} (${Oe.os})`),ba=new Date((this.data.localeUpdatedAt??0)*1e3).toLocaleString(),ya=this.data.localeHash??v.get("notFound"),wa=fe`${ya}
(${v.get("VOTUpdatedAt")} - ${ba})`,Sa=S.createInformation(`${v.get("VOTLocaleHash")}:`,wa),Vi=S.createOutlinedButton(v.get("VOTUpdateLocaleFiles"));return Vi.addEventListener("click",async()=>{await I.set("localeHash",""),await v.update(true),globalThis.location.reload();}),l.content.append(pa.container,ga.container,ma.container,va.container,Sa.container,Vi),this.dialog.footerContainer.append(this.bugReportButton,this.resetSettingsButton),this.initialized=true,this}initUIEvents(){if(!this.isInitialized())throw new Error("[VOT] SettingsView isn't initialized");return this.accountButton.addEventListener("click",async()=>{if(!I.isSupportOnlyLS){if(this.accountButton.loggedIn)return await I.delete("account"),this.data.account={},this.updateAccountInfo();globalThis.open(ss,"_blank")?.focus();}}),this.accountButton.addEventListener("click:secret",async()=>{const t=new Zn({titleHtml:v.get("VOTLoginViaToken"),isTemp:true});this.globalPortal.appendChild(t.container);const e=S.createEl("vot-block",void 0,v.get("VOTYandexTokenInfo")),i=new Qn({labelHtml:v.get("VOTYandexToken"),value:this.data.account?.token});i.addEventListener("change",async o=>{this.data.account=o?{expires:Date.now()+3153418e4,token:o}:{},await I.set("account",this.data.account),this.updateAccountInfo();}),t.bodyContainer.append(e,i.container),t.open();}),this.accountButton.addEventListener("refresh",async()=>{I.isSupportOnlyLS||(this.data.account=await I.get("account",{}),this.updateAccountInfo());}),this.bindPersistedSetting({control:this.autoTranslateCheckbox,event:"change",apply:t=>{this.data.autoTranslate=t;},storageKey:"autoTranslate",readPersistedValue:()=>this.data.autoTranslate,logLabel:"autoTranslate",dispatch:t=>this.events["change:autoTranslate"].dispatch(t)}),this.bindPersistedSetting({control:this.autoSubtitlesCheckbox,event:"change",apply:t=>{this.data.autoSubtitles=t;},storageKey:"autoSubtitles",readPersistedValue:()=>this.data.autoSubtitles,logLabel:"autoSubtitles",dispatch:t=>this.events["change:autoSubtitles"].dispatch(t)}),this.dontTranslateLanguagesCheckbox.addEventListener("change",async t=>{this.data.enabledDontTranslateLanguages=t,this.dontTranslateLanguagesSelect.disabled=!t,await I.set("enabledDontTranslateLanguages",this.data.enabledDontTranslateLanguages);}),this.dontTranslateLanguagesSelect.addEventListener("selectItem",async t=>{this.data.dontTranslateLanguages=t,await I.set("dontTranslateLanguages",this.data.dontTranslateLanguages);}),this.bindPersistedSetting({control:this.autoSetVolumeCheckbox,event:"change",apply:t=>{this.data.enabledAutoVolume=t,this.autoSetVolumeSlider.disabled=!t,this.smartDuckingCheckbox.disabled=!t||!!this.syncVolumeCheckbox?.checked;},storageKey:"enabledAutoVolume",readPersistedValue:()=>this.data.enabledAutoVolume,logLabel:"enabledAutoVolume",afterPersist:async()=>this.videoHandler?.setupAudioSettings?.()}),this.bindPersistedSetting({control:this.smartDuckingCheckbox,event:"change",apply:t=>{this.data.enabledSmartDucking=t;},storageKey:"enabledSmartDucking",readPersistedValue:()=>this.data.enabledSmartDucking,logLabel:"enabledSmartDucking",afterPersist:async()=>this.videoHandler?.setupAudioSettings?.()}),this.bindPersistedSetting({control:this.autoSetVolumeSlider,event:"input",apply:t=>{this.data.autoVolume=this.autoSetVolumeSliderLabel.value=t;},storageKey:"autoVolume",readPersistedValue:()=>this.data.autoVolume,logLabel:"autoVolume"}),this.bindPersistedSetting({control:this.showVideoVolumeSliderCheckbox,event:"change",apply:t=>{this.data.showVideoSlider=t;},storageKey:"showVideoSlider",readPersistedValue:()=>this.data.showVideoSlider,logLabel:"showVideoVolumeSlider",dispatch:t=>this.events["change:showVideoVolume"].dispatch(t)}),this.bindPersistedSetting({control:this.audioBoosterCheckbox,event:"change",apply:t=>{this.data.audioBooster=t;},storageKey:"audioBooster",readPersistedValue:()=>this.data.audioBooster,logLabel:"audioBooster",dispatch:t=>this.events["change:audioBooster"].dispatch(t)}),this.bindPersistedSetting({control:this.syncVolumeCheckbox,event:"change",apply:t=>{this.data.syncVolume=t,this.autoSetVolumeSlider.disabled=!this.autoSetVolumeCheckbox?.checked,this.smartDuckingCheckbox.disabled=t||!this.autoSetVolumeCheckbox?.checked,t&&this.smartDuckingCheckbox?.checked&&(this.smartDuckingCheckbox.checked=false);},storageKey:"syncVolume",readPersistedValue:()=>this.data.syncVolume,logLabel:"syncVolume",dispatch:t=>this.events["change:syncVolume"].dispatch(t)}),this.bindPersistedSetting({control:this.downloadWithNameCheckbox,event:"change",apply:t=>{this.data.downloadWithName=t;},storageKey:"downloadWithName",readPersistedValue:()=>this.data.downloadWithName,logLabel:"downloadWithName"}),this.bindPersistedSetting({control:this.sendNotifyOnCompleteCheckbox,event:"change",apply:t=>{this.data.sendNotifyOnComplete=t;},storageKey:"sendNotifyOnComplete",readPersistedValue:()=>this.data.sendNotifyOnComplete,logLabel:"sendNotifyOnComplete"}),this.bindPersistedSetting({control:this.useLivelyVoiceCheckbox,event:"change",apply:t=>{this.data.useLivelyVoice=t;},storageKey:"useLivelyVoice",readPersistedValue:()=>this.data.useLivelyVoice,logLabel:"useLivelyVoice",dispatch:t=>this.events["change:useLivelyVoice"].dispatch(t)}),this.bindPersistedSetting({control:this.useAudioDownloadCheckbox,event:"change",apply:t=>{this.data.useAudioDownload=t;},storageKey:"useAudioDownload",readPersistedValue:()=>this.data.useAudioDownload,logLabel:"useAudioDownload"}),this.bindPersistedSetting({control:this.subtitlesDownloadFormatSelect,event:"selectItem",apply:t=>{this.data.subtitlesDownloadFormat=t;},storageKey:"subtitlesDownloadFormat",readPersistedValue:()=>this.data.subtitlesDownloadFormat,logLabel:"subtitlesDownloadFormat"}),this.bindPersistedSetting({control:this.subtitlesHighlightWordsCheckbox,event:"change",apply:t=>{this.data.highlightWords=t;},storageKey:"highlightWords",readPersistedValue:()=>this.data.highlightWords,logLabel:"highlightWords",dispatch:t=>this.events["change:subtitlesHighlightWords"].dispatch(t)}),this.subtitlesSmartLayoutCheckbox?.addEventListener("change",t=>{this.suppressSubtitlesSmartLayoutCheckboxChange||this.setSubtitlesSmartLayout(t);}),this.subtitlesMaxLengthSlider.addEventListener("input",t=>{this.subtitlesMaxLengthSliderLabel.value=t,(this.data.subtitlesSmartLayout??true)===true&&this.setSubtitlesSmartLayout(false),this.data.subtitlesMaxLength=t,this.scheduleStoragePersist("subtitlesMaxLength",this.data.subtitlesMaxLength),this.events["input:subtitlesMaxLength"].dispatch(t);}),this.subtitlesFontSizeSlider.addEventListener("input",t=>{this.subtitlesFontSizeSliderLabel.value=t,(this.data.subtitlesSmartLayout??true)===true&&this.setSubtitlesSmartLayout(false),this.data.subtitlesFontSize=t,this.scheduleStoragePersist("subtitlesFontSize",this.data.subtitlesFontSize),this.events["input:subtitlesFontSize"].dispatch(t);}),this.subtitlesBackgroundOpacitySlider.addEventListener("input",t=>{this.subtitlesBackgroundOpacitySliderLabel.value=t,this.data.subtitlesOpacity=t,this.scheduleStoragePersist("subtitlesOpacity",this.data.subtitlesOpacity),this.events["input:subtitlesBackgroundOpacity"].dispatch(t);}),this.bindPersistedSetting({control:this.subtitlesFontFamilySelect,event:"selectItem",apply:t=>{this.data.subtitlesFontFamily=t;},storageKey:"subtitlesFontFamily",readPersistedValue:()=>this.data.subtitlesFontFamily,logLabel:"subtitlesFontFamily",dispatch:t=>this.events["select:subtitlesFontFamily"].dispatch(t)}),this.bindPersistedSetting({control:this.translateHotkeyButton,event:"change",apply:t=>{this.data.translationHotkey=t;},storageKey:"translationHotkey",readPersistedValue:()=>this.data.translationHotkey,logLabel:"translationHotkey"}),this.bindPersistedSetting({control:this.subtitlesHotkeyButton,event:"change",apply:t=>{this.data.subtitlesHotkey=t;},storageKey:"subtitlesHotkey",readPersistedValue:()=>this.data.subtitlesHotkey,logLabel:"subtitlesHotkey"}),this.proxyWorkerHostTextfield.addEventListener("change",async t=>{this.data.proxyWorkerHost=t||we,await I.set("proxyWorkerHost",this.data.proxyWorkerHost),V.log("proxyWorkerHost value changed. New value:",this.data.proxyWorkerHost),this.events["change:proxyWorkerHost"].dispatch(t);}),this.proxyTranslationStatusSelect.addEventListener("selectItem",async t=>{this.data.translateProxyEnabled=Number.parseInt(t,10),await I.set("translateProxyEnabled",this.data.translateProxyEnabled),await I.set("translateProxyEnabledDefault",false),V.log("translateProxyEnabled value changed. New value:",this.data.translateProxyEnabled),this.events["select:proxyTranslationStatus"].dispatch(t);}),this.bindPersistedSetting({control:this.translateAPIErrorsCheckbox,event:"change",apply:t=>{this.data.translateAPIErrors=t;},storageKey:"translateAPIErrors",readPersistedValue:()=>this.data.translateAPIErrors,logLabel:"translateAPIErrors"}),this.bindPersistedSetting({control:this.useNewAudioPlayerCheckbox,event:"change",apply:t=>{this.data.newAudioPlayer=t,this.onlyBypassMediaCSPCheckbox.disabled=this.onlyBypassMediaCSPCheckbox.hidden=!t;},storageKey:"newAudioPlayer",readPersistedValue:()=>this.data.newAudioPlayer,logLabel:"newAudioPlayer",dispatch:t=>this.events["change:useNewAudioPlayer"].dispatch(t)}),this.bindPersistedSetting({control:this.onlyBypassMediaCSPCheckbox,event:"change",apply:t=>{this.data.onlyBypassMediaCSP=t;},storageKey:"onlyBypassMediaCSP",readPersistedValue:()=>this.data.onlyBypassMediaCSP,logLabel:"onlyBypassMediaCSP",dispatch:t=>this.events["change:onlyBypassMediaCSP"].dispatch(t)}),this.bindPersistedSetting({control:this.translationTextServiceSelect,event:"selectItem",apply:t=>{this.data.translationService=t;},storageKey:"translationService",readPersistedValue:()=>this.data.translationService,logLabel:"translationService",dispatch:t=>this.events["select:translationTextService"].dispatch(t)}),this.bindPersistedSetting({control:this.detectServiceSelect,event:"selectItem",apply:t=>{this.data.detectService=t;},storageKey:"detectService",readPersistedValue:()=>this.data.detectService,logLabel:"detectService"}),this.bindPersistedSetting({control:this.showPiPButtonCheckbox,event:"change",apply:t=>{this.data.showPiPButton=t;},storageKey:"showPiPButton",readPersistedValue:()=>this.data.showPiPButton,logLabel:"showPiPButton",dispatch:t=>this.events["change:showPiPButton"].dispatch(t)}),this.autoHideButtonDelaySlider.addEventListener("input",t=>{this.autoHideButtonDelaySliderLabel.value=t;const e=Math.round(t*1e3);this.data.autoHideButtonDelay=e,this.scheduleStoragePersist("autoHideButtonDelay",this.data.autoHideButtonDelay),this.events["input:autoHideButtonDelay"].dispatch(t);}),this.bindPersistedSetting({control:this.buttonPositionSelect,event:"selectItem",apply:t=>{this.data.buttonPos=t;},storageKey:"buttonPos",readPersistedValue:()=>this.data.buttonPos,logLabel:"buttonPos",dispatch:t=>this.events["select:buttonPosition"].dispatch(t)}),this.menuLanguageSelect.addEventListener("selectItem",async t=>{await v.changeLang(t)&&(this.data.localeUpdatedAt=await I.get("localeUpdatedAt",0),this.events["select:menuLanguage"].dispatch(t));}),this.bugReportButton.addEventListener("click",()=>this.events["click:bugReport"].dispatch()),this.resetSettingsButton.addEventListener("click",()=>this.events["click:resetSettings"].dispatch()),this}addEventListener(t,e){return this.events[t].addListener(e),this}removeEventListener(t,e){return this.events[t].removeListener(e),this}doReleaseUI(){this.dialog?.remove();for(const t of [this.accountButtonRefreshTooltip,this.accountButtonTokenTooltip,this.audioBoosterTooltip,this.useLivelyVoiceTooltip,this.useAudioDownloadCheckboxTooltip,this.useNewAudioPlayerTooltip,this.onlyBypassMediaCSPTooltip,this.translationTextServiceTooltip,this.proxyTranslationStatusSelectTooltip,this.buttonPositionTooltip])t?.release();}doReleaseUIEvents(){this.flushStoragePersists();for(const t of Object.values(this.events))t.clear();}release(){return this.isInitialized()?(this.doReleaseUIEvents(),this.doReleaseUI(),this.initialized=false,this):this}updateAccountInfo(){if(!this.isInitialized())throw new Error("[VOT] SettingsView isn't initialized");const t=!!this.data.account?.token;return this.accountButton.avatarId=this.data.account?.avatarId,this.useLivelyVoiceTooltip.hidden=this.accountButton.loggedIn=t,this.accountButton.username=this.data.account?.username,this.useLivelyVoiceCheckbox.disabled=!t,this.events["update:account"].dispatch(this.data.account),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()}}class Gm{mount;initialized=false;videoHandler;intervalIdleChecker;data;votGlobalPortal;votOverlayView;votSettingsView;constructor({mount:t,data:e={},videoHandler:i,intervalIdleChecker:o}){this.mount=t,this.videoHandler=i,this.data=e,this.intervalIdleChecker=o;}get root(){return this.mount.root}get portalContainer(){return this.mount.portalContainer}get tooltipLayoutRoot(){return this.mount.tooltipLayoutRoot}isInitialized(){return this.initialized}initUI(){if(this.isInitialized())throw new Error("[VOT] UIManager is already initialized");return this.initialized=true,this.votGlobalPortal=S.createPortal(),this.getGlobalPortalHost(this.mount).appendChild(this.votGlobalPortal),this.votOverlayView=new ve({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 Ti({globalPortal:this.votGlobalPortal,data:this.data,videoHandler:this.videoHandler}),this.votSettingsView.initUI(),this}updateMount(t){const e=this.getGlobalPortalHost(t);return this.votGlobalPortal?.parentElement!==e&&e.appendChild(this.votGlobalPortal),this.mount=mm(this.mount,t,i=>{this.votOverlayView?.updateMount(i);}),this.videoHandler?.subtitlesWidget?.updateMount({container:t.subtitlesMountContainer,tooltipLayoutRoot:t.tooltipLayoutRoot}),this}getGlobalPortalHost(t){const e=document,i=e.fullscreenElement??e.webkitFullscreenElement;return !!mi(i,[t.root],{allowDocumentViewport:true})?t.root: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 t=this.votOverlayView;t&&t.addEventListener("click:translate",async()=>{await this.handleTranslationBtnClick();}).addEventListener("click:pip",async()=>{if(this.videoHandler)try{await(this.videoHandler.video===document.pictureInPictureElement?document.exitPictureInPicture():this.videoHandler.video.requestPictureInPicture());}catch{}}).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",e=>{if(this.videoHandler){if(this.videoHandler.setVideoVolume(e/100),!this.data.syncVolume){this.videoHandler.onVideoVolumeSliderSynced(e);return}this.videoHandler.syncVolumeWrapper("video",e);}}).addEventListener("input:translationVolume",e=>{if(!this.videoHandler)return;const i=e??this.data.defaultVolume??100;if(this.videoHandler.audioPlayer.player.volume=i/100,!this.data.syncVolume){this.videoHandler.onTranslationVolumeSliderSynced(i);return}this.videoHandler.syncVolumeWrapper("translation",i);}).addEventListener("select:subtitles",e=>{this.videoHandler&&this.runDetached(this.videoHandler.changeSubtitlesLang(e),"Failed to change subtitles language");});}bindSettingsViewEvents(){const t=this.votSettingsView;t&&t.addEventListener("update:account",async e=>{this.videoHandler&&(this.videoHandler.votClient.apiToken=e?.token);}).addEventListener("change:autoTranslate",async e=>{const i=this.videoHandler;e&&i&&!i.hasActiveSource()&&await this.handleTranslationBtnClick();}).addEventListener("change:autoSubtitles",async e=>{!e||!this.videoHandler?.videoData?.videoId||await this.videoHandler.enableSubtitlesForCurrentLangPair();}).addEventListener("change:showVideoVolume",()=>{this.withInitializedOverlayView(e=>{!e.videoVolumeSlider||!e.votButton||(e.videoVolumeSlider.container.hidden=!this.data.showVideoSlider||e.votButton.status!=="success");});}).addEventListener("change:audioBooster",async()=>{this.withInitializedOverlayView(e=>{if(!e.translationVolumeSlider)return;const i=e.translationVolumeSlider.value,o=this.data.audioBooster?ls:100;e.translationVolumeSlider.max=o;const r=$(i,0,o);e.translationVolumeSlider.value=r,this.videoHandler?.onTranslationVolumeSliderSynced(r);});}).addEventListener("change:syncVolume",e=>{this.videoHandler&&(this.videoHandler.setupAudioSettings(),e&&this.withInitializedOverlayView(i=>{const o=i.videoVolumeSlider,r=i.translationVolumeSlider;!o||!r||this.videoHandler.resetVolumeLinkState(Number(o.value),Number(r.value));}));}).addEventListener("change:useLivelyVoice",()=>{this.videoHandler&&this.runDetached(this.videoHandler.stopTranslate(),"Failed to stop translation after voice mode change");}).addEventListener("change:subtitlesHighlightWords",e=>{this.updateSubtitlesWidgetSetting(e,this.data.highlightWords,(i,o)=>{i.setHighlightWords(o);});}).addEventListener("change:subtitlesSmartLayout",e=>{this.updateSubtitlesWidgetSetting(e,this.data.subtitlesSmartLayout,(i,o)=>{i.setSmartLayout(o);});}).addEventListener("input:subtitlesMaxLength",e=>{this.updateSubtitlesWidgetSetting(e,this.data.subtitlesMaxLength,(i,o)=>{i.setMaxLength(o);});}).addEventListener("input:subtitlesFontSize",e=>{this.updateSubtitlesWidgetSetting(e,this.data.subtitlesFontSize,(i,o)=>{i.setFontSize(o);});}).addEventListener("select:subtitlesFontFamily",e=>{this.updateSubtitlesWidgetSetting(e,this.data.subtitlesFontFamily,(i,o)=>{i.setFontFamily(o);});}).addEventListener("input:subtitlesBackgroundOpacity",e=>{this.updateSubtitlesWidgetSetting(e,this.data.subtitlesOpacity,(i,o)=>{i.setOpacity(o);});}).addEventListener("change:proxyWorkerHost",e=>{this.videoHandler&&this.runDetached(this.videoHandler.handleProxySettingsChanged("proxyWorkerHost"),"Failed to apply proxyWorkerHost change");}).addEventListener("select:proxyTranslationStatus",()=>{this.videoHandler&&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(e=>{e.resetTranslationContext(true);});}).addEventListener("change:showPiPButton",()=>{this.withInitializedOverlayView(e=>{e.votButton&&(e.votButton.pipButton.hidden=e.votButton.separator2.hidden=!e.pipButtonVisible);});}).addEventListener("select:buttonPosition",e=>{this.withInitializedOverlayView(i=>{const o=this.data.buttonPos??e,{position:r,direction:s}=i.calcButtonLayout(o);i.updateButtonLayout(r,s);});}).addEventListener("select:menuLanguage",async()=>{await this.reloadMenu();}).addEventListener("click:bugReport",()=>{if(!this.videoHandler)return;const e=new URLSearchParams(this.videoHandler.collectReportInfo()).toString();globalThis.open(`${jc}/issues/new?${e}`,"_blank")?.focus();}).addEventListener("click:resetSettings",async()=>{const e=await I.list();await Promise.all(e.map(i=>I.delete(i))),await I.set("compatVersion",Se),globalThis.location.reload();});}async handleDownloadTranslationClick(){const t=this.votOverlayView,e=this.videoHandler;if(!t?.isInitialized()||!e?.downloadTranslationUrl||!e.videoData)return;const i=t.downloadTranslationButton,o=e.downloadTranslationUrl,r=this.data.downloadWithName?bo(e.videoData.downloadTitle):`translation_${e.videoData.videoId}`,a={preferShare:this.isLikelyMobileDownloadContext()},l=c=>{i&&(i.progress=c);};l(0);try{await this.downloadTranslationAudio(o,r,l,a);}catch(c){console.error("[VOT] Download translation failed:",c),this.triggerUrlDownload(o,`${r}.mp3`)||globalThis.open(o,"_blank")?.focus();}finally{l(0);}}async downloadTranslationAudio(t,e,i,o){const r=await Q(t,{timeout:0});if(!r.ok)throw new Error(`HTTP ${r.status}`);await pm(r,e,i,o);}async handleDownloadSubtitlesClick(){const t=this.videoHandler;if(!t?.yandexSubtitles||!t.videoData)return;const e=this.data.subtitlesDownloadFormat??"json",i=cm(t.yandexSubtitles,e,{assTitle:t.videoData.localizedTitle??t.videoData.title??t.videoData.downloadTitle}),o=new Blob([e==="json"?JSON.stringify(i):i],{type:"text/plain"}),s=`${this.data.downloadWithName?bo(t.videoData.downloadTitle):`subtitles_${t.videoData.videoId}`}.${e}`,l={preferShare:this.isLikelyMobileDownloadContext()};await ds(o,s,l);}async reloadMenu(){if(!this.votOverlayView?.isInitialized())throw new Error("[VOT] OverlayView isn't initialized");const t=this.votOverlayView.votButton.opacity,e=this.votOverlayView.votButton.container.hidden,i=this.votOverlayView.votMenu.hidden,o=this.data.buttonPos??"default",r=this.votSettingsView?.dialog?.container?.hidden===false;if(await this.videoHandler?.stopTranslation(),this.release(),this.initUI(),this.initUIEvents(),!this.videoHandler)return this;try{const{position:a,direction:l}=this.votOverlayView.calcButtonLayout(o);this.votOverlayView.updateButtonLayout(a,l),this.votOverlayView.votMenu.hidden=i,this.votOverlayView.votButton.container.hidden=e,this.votOverlayView.votButton.opacity=t;}catch{}try{this.videoHandler.rebindOverlayVisibilityTargets();}catch{}if(r)try{this.votSettingsView?.open();}catch{}await this.videoHandler.updateSubtitlesLangSelect();const s=this.videoHandler.subtitlesWidget;return s&&s.resetTranslationContext(true),this}async handleTranslationBtnClick(){if(!this.votOverlayView?.isInitialized())throw new Error("[VOT] OverlayView isn't initialized");const t=this.videoHandler;if(!t)return this;if(t.hasActiveSource())return await t.stopTranslation(),this;if(this.votOverlayView.votButton.status==="error"&&!this.votOverlayView.votButton.loading&&this.transformBtn("none",v.get("translateVideo")),this.votOverlayView.votButton.status!=="none"||this.votOverlayView.votButton.loading)return t.actionsAbortController.abort(),await t.stopTranslation(),this;try{V.log("[handleTranslationBtnClick] trying execute translation");const e=await this.getVideoDataForTranslation(t);await t.videoManager.ensureDetectedLanguageForTranslation(e),V.log("[handleTranslationBtnClick] Run translateFunc",e.videoId),await t.translateFunc(e.videoId,e.isStream,e.detectedLanguage,e.responseLanguage,e.translationHelp);}catch(e){if(this.isAbortError(e))return this.transformBtn("none",v.get("translateVideo")),this;if(console.error("[VOT]",e),!(e instanceof Error))return this.transformBtn("error",String(e)),this;const i=e.name==="VOTLocalizedError"?e.localizedMessage:e.message;this.transformBtn("error",i);}return this}async getVideoDataForTranslation(t){if(!t.videoData?.videoId)throw new nt("VOTNoVideoIDFound");if(this.shouldRefreshVideoDataBeforeTranslation(t)&&(t.videoData=await t.getVideoData()),!t.videoData?.videoId)throw new nt("VOTNoVideoIDFound");return t.videoData}shouldRefreshVideoDataBeforeTranslation(t){return t.site.host==="vk"&&t.site.additionalData==="clips"||t.site.host==="douyin"}isAbortError(t){return t instanceof Error&&t.name==="AbortError"}isLoadingText(t){const e=v.get("TranslationDelayed");return typeof t=="string"&&(t.includes(v.get("translationTake"))||(e?t.includes(e):false))}transformBtn(t,e){if(!this.votOverlayView?.isInitialized())throw new Error("[VOT] OverlayView isn't initialized");return this.votOverlayView.votButton.status=t,this.votOverlayView.votButton.loading=t==="error"&&this.isLoadingText(e),this.votOverlayView.votButton.setText(e),this.votOverlayView.votButtonTooltip.setContent(e),this}release(){return this.isInitialized()?(this.votOverlayView.release(),this.votSettingsView.release(),this.votGlobalPortal.remove(),this.initialized=false,this):this}withInitializedOverlayView(t){this.votOverlayView?.isInitialized()&&t(this.votOverlayView);}withSubtitlesWidget(t){const e=this.videoHandler?.subtitlesWidget;e&&t(e);}updateSubtitlesWidgetSetting(t,e,i){this.withSubtitlesWidget(o=>{i(o,e??t);});}runDetached(t,e){t.catch(i=>{});}triggerUrlDownload(t,e){try{const i=document.createElement("a");return i.href=t,i.download=e,i.target="_blank",i.rel="noopener noreferrer",i.style.display="none",document.body.appendChild(i),i.click(),i.remove(),!0}catch{return false}}isLikelyMobileDownloadContext(){return this.videoHandler?.site.additionalData==="mobile"?true:typeof matchMedia=="function"&&matchMedia("(pointer: coarse)").matches}restartAudioPlayer(){this.restartAudioPlayerSafely();}async restartAudioPlayerSafely(){const t=this.videoHandler;if(t)try{await t.stopTranslate(),t.createPlayer();}catch{}}}class Km{deps;hideDeadlineMs=0;hideArmed=false;unsubscribeChecker;constructor(t){this.deps=t,this.unsubscribeChecker=this.deps.checker.subscribe(()=>{this.onCheckerTick();});}show(){const t=this.getView();return t?(t.updateButtonOpacity(1),t):null}cancel(){this.hideDeadlineMs=0,this.hideArmed=false;}release(){this.cancel(),this.unsubscribeChecker();}queueAutoHide(){if(!this.show())return;const t=this.deps.getAutoHideDelay();this.hideDeadlineMs=this.nowMs()+Math.max(0,t),this.hideArmed=true,this.deps.checker.markActivity("overlay-queue-hide"),this.deps.checker.requestImmediateTick();}handleOverlayInteraction(t){const e=t?.type;if(e){if(e==="focusin"){this.handleFocusIn();return}if(e.startsWith("pointer")){this.cancel(),this.show(),this.deps.checker.markActivity("overlay-interaction"),t.stopPropagation?.();return}this.handleHostInteraction(t);}}handleHostInteraction(t){const e=t?.type;if(e){if(e==="focusin"){this.handleFocusIn();return}if(e.startsWith("pointer")){const i=t.target;this.deps.isInteractiveNode(i)&&t.stopPropagation?.(),this.deps.checker.markActivity("overlay-host-pointer");}this.queueAutoHide();}}scheduleHide(t){if(!this.getView())return;const e=t?.currentTarget;let i=t?.relatedTarget??null;!i&&typeof t?.composedPath=="function"&&(i=t.composedPath()[1]??null);const o=i instanceof Node?i:null,r=e instanceof Node?e:null;o&&(r?.contains(o)||this.deps.isInteractiveNode(o))||this.queueAutoHide();}onCheckerTick(){if(!this.hideArmed||this.hideDeadlineMs<=0||this.nowMs()+2typeof performance<"u"&&typeof performance.now=="function"?performance.now():Date.now(),setInterval:globalThis.setInterval.bind(globalThis),clearInterval:globalThis.clearInterval.bind(globalThis),queueMicrotask:n=>{if(typeof globalThis.queueMicrotask=="function"){globalThis.queueMicrotask(n);return}Promise.resolve().then(n);},isDocumentHidden:()=>typeof document<"u"&&typeof document.hidden=="boolean"?document.hidden:false,onVisibilityChange:n=>typeof document>"u"||typeof document.addEventListener!="function"?()=>{}:(document.addEventListener("visibilitychange",n),()=>{typeof document.removeEventListener=="function"&&document.removeEventListener("visibilitychange",n);})}}class Zm{profile;runtime;subscribers=new Set;intervalId=null;unsubscribeVisibilityChange=null;running=false;destroyed=false;immediateQueued=false;currentMode="active";lastActivityAt;onVisibilityChangeHandler=()=>{this.destroyed||!this.running||(this.runtime.isDocumentHidden()?this.clearIntervalTimer():this.armInterval(),this.requestImmediateTick());};constructor(t={}){this.profile=Jm(t.profile),this.runtime={...jm(),...t.runtime},this.lastActivityAt=this.runtime.nowMs();}start(){this.destroyed||this.running||(this.running=true,this.lastActivityAt=this.runtime.nowMs(),this.subscribeVisibilityChange(),this.armInterval(),this.runTick("start"));}stop(){this.running&&(this.running=false,this.clearIntervalTimer(),this.immediateQueued=false,this.unsubscribeFromVisibilityChange());}destroy(){this.destroyed||(this.stop(),this.subscribers.clear(),this.destroyed=true);}subscribe(t){return this.destroyed?()=>{}:(this.subscribers.add(t),()=>{this.subscribers.delete(t);})}markActivity(t){if(this.destroyed||(this.lastActivityAt=this.runtime.nowMs(),!this.running))return;const e=this.resolveMode(this.lastActivityAt);e!==this.currentMode&&(this.currentMode=e);}requestImmediateTick(){this.destroyed||!this.running||this.immediateQueued||(this.immediateQueued=true,this.runtime.queueMicrotask(()=>{this.immediateQueued=false,!(this.destroyed||!this.running)&&this.runTick("immediate");}));}resolveMode(t){return this.runtime.isDocumentHidden()?"hidden":t-this.lastActivityAt>=this.profile.idleAfterMs?"idle":"active"}clearIntervalTimer(){this.intervalId!==null&&(this.runtime.clearInterval(this.intervalId),this.intervalId=null);}armInterval(){this.intervalId===null&&(this.intervalId=this.runtime.setInterval(()=>{this.runTick("interval");},this.profile.checkIntervalMs));}runTick(t){if(this.destroyed||!this.running||this.subscribers.size===0)return;const e=this.runtime.nowMs(),i=this.resolveMode(e);i!==this.currentMode&&(this.currentMode=i);const o={nowMs:e,mode:i,source:t};for(const r of this.subscribers)try{r(o);}catch{}}subscribeVisibilityChange(){this.unsubscribeVisibilityChange===null&&(this.unsubscribeVisibilityChange=this.runtime.onVisibilityChange(this.onVisibilityChangeHandler));}unsubscribeFromVisibilityChange(){this.unsubscribeVisibilityChange!==null&&(this.unsubscribeVisibilityChange(),this.unsubscribeVisibilityChange=null);}}function Li(n){return new Zm({profile:n})}const Js=()=>Date.now();function Dn(){return GM_info?.script?.name||"VOT"}function js(n,t){try{return v?.get?.(n)||t}catch{return t}}function Qm(n,t,e){if(!e)return true;const i=n.get(t)??0;return Js()-i>=e}function tv(n,t){n.set(t,Js());}function Rn(n){const t=n.trim();if(!t)return null;try{const e=v.get(t),i=v.getDefault(t);return e!==t||i!==t?e||i||t:null}catch{return null}}function ev(n){if(n&&typeof n=="object"){const e=n;if(e.name==="VOTLocalizedError"){if(typeof e.localizedMessage=="string"&&e.localizedMessage.trim())return e.localizedMessage;if(typeof e.unlocalizedMessage=="string"){const i=Rn(e.unlocalizedMessage);if(i)return i}}}if(typeof n=="string"){const e=Rn(n);if(e)return e}const t=gn(n);return t?Rn(t)||t:js("requestTranslationFailed","Translation failed")}function nv(n){try{if(typeof GM_notification=="function")return GM_notification(n),!0;const t=globalThis.GM;if(t!==void 0&&typeof t.notification=="function"){const e={text:n.text,title:n.title,image:n.image,onclick:n.onclick,ondone:n.ondone};return t.notification(e),!0}}catch{}return false}class iv{lastSentAt=new Map;send(t,e={}){try{const i=e.key||t.tag||`${t.title??""}|${t.text??""}`,o=e.cooldownMs??0;if(!Qm(this.lastSentAt,i,o))return;const r={...t,title:t.title??Dn()};nv(r)?tv(this.lastSentAt,i):V.log("[notify] unavailable",r);}catch{}}translationCompleted(t){const e=js("VOTTranslationCompletedNotify","The translation on the {0} has been completed!").replace("{0}",t);this.send({text:e,title:Dn(),timeout:5e3,silent:true,tag:"VOTTranslationCompleted",onclick:()=>{try{globalThis.focus();}catch{}}},{key:`translation_completed_${t}`,cooldownMs:1e4});}translationFailed(t){const{videoId:e,message:i}=t;if(mn(i))return;const o=ev(i),r=Dn();this.send({text:o,title:r,timeout:8e3,silent:true,tag:`VOTtranslationFailed_${e||"unknown"}`,onclick:()=>{try{globalThis.focus();}catch{}}},{key:`translation_failed_${e||"unknown"}`,cooldownMs:3e4});}}const ov=["class","id","title"],rv=["advertise","advertisement","promo","sponsor","banner","commercial","preroll","midroll","postroll","ad-container","sponsored"],sv=new RegExp(rv.map(n=>n.replaceAll(/[.*+?^${}()|[\]\\]/g,String.raw`\$&`)).join("|")),rn=Symbol.for("vot.attachShadowHook");function av(){const n=Object.getOwnPropertyDescriptor(Element.prototype,"attachShadow");return !n||typeof n.value!="function"?null:n}function lv(){const n=globalThis,t=n[rn];if(t?.descriptor&&t.subscribers instanceof Set)return t;const e=av();if(!e)return null;const i=e.value,o={descriptor:e,subscribers:new Set},r=function(s){const a=i.call(this,s);for(const l of o.subscribers)try{l(a);}catch{}return a};try{Object.defineProperty(Element.prototype,"attachShadow",{...e,value:r});}catch{return null}return n[rn]=o,o}function cv(n){const t=globalThis,e=t[rn];if(e&&(e.subscribers.delete(n),!(e.subscribers.size>0))){try{Object.defineProperty(Element.prototype,"attachShadow",e.descriptor);}catch{const i=e.descriptor.value;typeof i=="function"&&(Element.prototype.attachShadow=i);}delete t[rn];}}class be{seenVideos=new WeakSet;activeVideos=new WeakSet;observedRoots=new WeakSet;videoListenerControllers=new Map;pendingAdded=new Set;pendingRemoved=new Set;flushPending=false;static MAX_FLUSH_BUDGET_MS=6;static MAX_NODES_PER_SLICE=120;onVideoAdded=new N;onVideoRemoved=new N;observer=new MutationObserver(t=>this.onMutations(t));intervalIdleChecker;checkerUnsubscribe=null;enabled=false;attachShadowSubscriber=null;onDocumentReady=null;onPageShow=()=>{const t=document.documentElement;t&&(this.pendingAdded.add(t),this.scheduleFlush());};constructor(t=Li()){this.intervalIdleChecker=t;}static containsAdKeyword(t){return t.length>0&&sv.test(t)}isAdRelated(t){for(const e of ov){const i=t.getAttribute(e);if(i&&be.containsAdKeyword(i.toLowerCase()))return true}return false}isInsideAd(t){for(let e=t.parentElement;e;e=e.parentElement)if(this.isAdRelated(e))return true;return false}getCapturedAudioTrackCount(t){const e=t,i=e.captureStream??e.mozCaptureStream;if(typeof i!="function")return null;try{return i.call(t).getAudioTracks().length}catch{return null}}isLikelySilentDecorativeVideo(t){if(!(t.muted||t.defaultMuted)||!t.autoplay||!t.loop||t.controls)return false;const e=t;if(typeof e.mozHasAudio=="boolean")return !e.mozHasAudio;if("audioTracks"in e&&typeof e.audioTracks?.length=="number"){if(e.audioTracks.length>0)return false;const o=this.getCapturedAudioTrackCount(t);return o!==null?o===0:true}const i=this.getCapturedAudioTrackCount(t);return i!==null?i===0:false}hasAudio(t){const e=t;return t.srcObject instanceof MediaStream?t.srcObject.getAudioTracks().length>0:typeof e.mozHasAudio=="boolean"?e.mozHasAudio:typeof e.webkitAudioDecodedByteCount=="number"&&e.webkitAudioDecodedByteCount>0||"audioTracks"in e&&typeof e.audioTracks?.length=="number"&&e.audioTracks.length>0?true:!this.isLikelySilentDecorativeVideo(t)}isValidVideo(t){return !(this.isAdRelated(t)||this.isInsideAd(t)||!this.hasAudio(t))}observeRoot(t){this.observedRoots.has(t)||(this.observedRoots.add(t),this.observer.observe(t,{childList:true,subtree:true}));}scan(t){if(t instanceof HTMLVideoElement){this.trackVideo(t);return}if(t.nodeType!==Node.ELEMENT_NODE&&t.nodeType!==Node.DOCUMENT_FRAGMENT_NODE&&t.nodeType!==Node.DOCUMENT_NODE)return;const e=document.createTreeWalker(t,NodeFilter.SHOW_ELEMENT,{acceptNode:i=>{const o=i,r=o.tagName==="VIDEO",s=!!o.shadowRoot;return r||s?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP}});for(;e.nextNode();){const i=e.currentNode;if(i instanceof HTMLVideoElement){this.trackVideo(i);continue}const o=i.shadowRoot;o&&(this.observeRoot(o),this.scan(o));}}getVideoListenerSignal(t){const e=this.videoListenerControllers.get(t);e&&e.abort();const i=new AbortController;return this.videoListenerControllers.set(t,i),i.signal}cleanupVideoListeners(t){const e=this.videoListenerControllers.get(t);e&&(e.abort(),this.videoListenerControllers.delete(t));}cleanupAllVideoListeners(){for(const t of this.videoListenerControllers.values())t.abort();this.videoListenerControllers.clear();}trackVideo(t){if(this.seenVideos.has(t))return;this.seenVideos.add(t);const e=this.getVideoListenerSignal(t),i=()=>{this.isValidVideo(t)&&(this.activeVideos.has(t)||(this.activeVideos.add(t),this.onVideoAdded.dispatch(t)));};if(t.readyState>=HTMLMediaElement.HAVE_CURRENT_DATA)i();else {t.addEventListener("loadeddata",i,{once:true,signal:e});const o=()=>{t.readyState>=HTMLMediaElement.HAVE_CURRENT_DATA&&i();};t.addEventListener("play",o,{once:true,passive:true,signal:e});}t.addEventListener("emptied",()=>{t.isConnected||this.untrackVideo(t);},{passive:true,signal:e});}untrackVideo(t){this.cleanupVideoListeners(t),this.activeVideos.has(t)&&(this.onVideoRemoved.dispatch(t),this.activeVideos.delete(t)),this.seenVideos.delete(t);}collectVideos(t){const e=new Set,i=o=>{for(const r of o)e.add(r);};if(t instanceof HTMLVideoElement&&e.add(t),(t instanceof Document||t instanceof DocumentFragment||t instanceof Element)&&i(t.querySelectorAll("video")),t instanceof Element){const o=t.shadowRoot;o&&i(o.querySelectorAll("video"));}return Array.from(e)}getNowMs(){return typeof performance<"u"&&typeof performance.now=="function"?performance.now():Date.now()}isSliceBudgetReached(t,e){return e>=be.MAX_NODES_PER_SLICE?true:this.getNowMs()-t>=be.MAX_FLUSH_BUDGET_MS}processPendingAdded(t){let e=0;for(;this.pendingAdded.size>0;){const i=this.pendingAdded.values().next();if(i.done||(this.pendingAdded.delete(i.value),this.scan(i.value),e+=1,this.isSliceBudgetReached(t,e)))break}return e}processPendingRemoved(t,e){let i=e;for(;this.pendingRemoved.size>0&&!this.isSliceBudgetReached(t,i);){const o=this.pendingRemoved.values().next();if(o.done)break;this.pendingRemoved.delete(o.value);for(const r of this.collectVideos(o.value))r.isConnected||this.untrackVideo(r);i+=1;}return i}flushSlice=()=>{if(!this.enabled){this.pendingAdded.clear(),this.pendingRemoved.clear(),this.flushPending=false;return}const t=this.getNowMs(),e=this.processPendingAdded(t);this.processPendingRemoved(t,e),this.flushPending=this.pendingAdded.size>0||this.pendingRemoved.size>0,this.flushPending&&this.intervalIdleChecker.requestImmediateTick();};onCheckerTick=()=>{this.flushPending&&this.flushSlice();};scheduleFlush=()=>{this.enabled&&(this.flushPending=true,this.intervalIdleChecker.requestImmediateTick());};installAttachShadowHook(){if(this.attachShadowSubscriber)return;const t=lv();if(!t)return;const e=i=>{this.enabled&&(this.observeRoot(i),this.pendingAdded.add(i),this.scheduleFlush());};t.subscribers.add(e),this.attachShadowSubscriber=e;}uninstallAttachShadowHook(){this.attachShadowSubscriber&&(cv(this.attachShadowSubscriber),this.attachShadowSubscriber=null);}enqueueAddedNode(t){if(t.nodeType===Node.ELEMENT_NODE){const e=t.shadowRoot;e&&this.observeRoot(e);}this.pendingAdded.add(t);}enqueueMutation(t){for(const e of t.addedNodes)this.enqueueAddedNode(e);for(const e of t.removedNodes)this.pendingRemoved.add(e);}onMutations(t){for(const e of t)e.type==="childList"&&this.enqueueMutation(e);(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 t=document.documentElement;if(t){this.observeRoot(t),this.scan(t);return}const e=()=>{const i=document.documentElement;i&&(document.removeEventListener("readystatechange",e),this.onDocumentReady=null,this.enabled&&(this.observeRoot(i),this.scan(i)));};this.onDocumentReady=e,document.addEventListener("readystatechange",e),typeof queueMicrotask=="function"?queueMicrotask(e):Promise.resolve().then(e);}disable(){this.enabled&&(this.enabled=false,globalThis.removeEventListener("pageshow",this.onPageShow),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=new WeakSet,this.activeVideos=new WeakSet,this.observedRoots=new WeakSet);}}function Bn(n,t){const e=ft(t);n.lastVideoPercent=e;}function Fn(n,t){const e=Number(t);if(!Number.isFinite(e))return;const i=Math.max(0,Math.round(e));n.lastTranslationPercent=i;}function uv({state:n,fromType:t,newVolume:e,currentVideo:i,currentTranslation:o,translationMin:r,translationMax:s}){if(n.initialized||(n.lastVideoPercent=Number(i),n.lastTranslationPercent=Number(o),n.initialized=true),t==="video"){const u=ft(e),d=u-ft(n.lastVideoPercent);n.lastVideoPercent=u;const h=Jn(n.lastTranslationPercent+d,r,s);return n.lastTranslationPercent=h,{nextTranslation:h}}const a=Jn(Number.isFinite(e)?e:o,r,s),l=a-n.lastTranslationPercent;n.lastTranslationPercent=a;const c=ft(n.lastVideoPercent+l);return n.lastVideoPercent=c,{nextVideo:c}}const vr={allowTouchMoveHandler:true,disableContainerDrag:false},dv={xvideos:{allowTouchMoveHandler:false},youtube:{disableContainerDrag:true}};function hv(n){if(!n)return vr;const t=dv[n]??{};return {...vr,...t}}function fv(n,t){if(!t||t===n||n.aborted)return n;if(t.aborted)return t;if(typeof AbortSignal<"u"&&"any"in AbortSignal)return AbortSignal.any([n,t]);const i=new AbortController,o=()=>{n.removeEventListener("abort",r),t.removeEventListener("abort",s);},r=()=>{o(),i.abort(n.reason);},s=()=>{o(),i.abort(t.reason);};return n.addEventListener("abort",r,{once:true}),t.addEventListener("abort",s,{once:true}),i.signal}function Zs(n){const t=(i,o,r,s)=>{const a=fv(n,s?.signal);if(!s){i.addEventListener(o,r,{signal:a});return}const{signal:l,...c}=s;i.addEventListener(o,r,{...c,signal:a});};return {add:t,addMany:(i,o,r,s)=>{for(const a of o)t(i,a,r,s);}}}function br(n,t,e){n(t,["pointerenter","focusin"],i=>e.handleOverlayInteraction(i)),n(t,["pointermove"],i=>e.handleOverlayInteraction(i),{passive:true}),n(t,["pointerleave","focusout"],i=>e.scheduleHide(i));}function ti(n,t=0){const e=typeof n=="number"?n:Number(n);return Number.isFinite(e)?ft(e):t}function Qs(n,t,e={}){e.skipYouTubeLikeHosts&&Ms(n.site.host)||n.smartVolumeDuckingInterval===void 0&&(!n.data?.syncVolume||!n.audioPlayer?.player?.src||n.isLikelyInternalVideoVolumeChange(t)||n.syncVolumeWrapper("video",t));}function yr(n,t,e){const i=t.votMenu?.container;if(i){const s=e??n.video.getBoundingClientRect().height;i.style.setProperty("--vot-container-height",`${s}px`);}const{position:o,direction:r}=t.calcButtonLayout(n.data?.buttonPos??"default");t.updateButtonLayout(o,r);}function ta(n){return n.replace("Key","").replace("Digit","")}function pv(n){const t=new Set;for(const e of n)t.add(ta(e));return t}function wr(n,t){if(!n)return null;const e=t.get(n);if(e)return e;const i=n.split("+").filter(Boolean).map(ta),o={parts:i,partsSet:new Set(i)};return t.set(n,o),o}function Sr(n,t){if(!t||n.size!==t.parts.length)return false;for(const e of t.partsSet)if(!n.has(e))return false;return true}function gv(n){const{self:t,overlayView:e,addMany:i}=n,o=()=>{t.refreshOverlayMount(),yr(t,e);};t.resizeObserver=new ResizeObserver(r=>{for(const s of r)yr(t,e,s.contentRect.height);}),t.resizeObserver.observe(t.video),o(),i(document,["fullscreenchange","webkitfullscreenchange"],()=>o()),i(t.video,["webkitbeginfullscreen","webkitendfullscreen"],()=>o());}function mv(n){const{self:t}=n;if(!_s(t.site))return;t.syncVolumeObserver=new MutationObserver(i=>{if(!t.audioPlayer?.player?.src)return;let o=false,r=null;for(const a of i){if(a.type!=="attributes"||a.attributeName!=="aria-valuenow")continue;o=true;const l=a.target instanceof Element?a.target.getAttribute("aria-valuenow"):null,c=l!=null?Number.parseFloat(l):Number.NaN;Number.isFinite(c)&&(r=c);}if(!o)return;let s;if(r!=null)s=ti(r);else {const a=t.isMuted()?0:t.getVideoVolume();s=ti(a*100);}t.syncVideoVolumeSlider(),Qs(t,s);});const e=document.querySelector(".ytp-volume-panel");e&&t.syncVolumeObserver.observe(e,{attributes:true,subtree:true,attributeFilter:["aria-valuenow"]});}function vv(n){const{self:t}=n;if(t.site.host!=="youtube"||t.site.additionalData==="mobile")return;const e=async()=>{try{if(!t.videoData)return;const r=B.getPlayer(),s=r?.getAvailableAudioTracks?.()??null;if(!Array.isArray(s)||s.length<=1)return;const l=r?.getAudioTrack?.()?.getLanguageInfo?.()?.id,c=l&&l!=="und"?l.toLowerCase().split(/[-_.]/)[0]:void 0;if(!c||!Lt.includes(c))return;const u=c;if(u===t.videoData.detectedLanguage)return;if(t.videoManager.rememberDetectedLanguage(t.videoData.videoId,u),t.setSelectMenuValues(u,t.videoData.responseLanguage),t.data?.autoTranslate&&u!==t.videoData.responseLanguage){V.log(`[VOT] Audio track language changed to ${u}, triggering auto-translation`);try{await t.uiManager.handleTranslationBtnClick();}catch(d){V.log("[VOT] Failed to trigger auto-translation on audio track change:",d);}}}catch{}},i=B.getPlayer(),o=["onApiChange","onStateChange"];if(i?.addEventListener)for(const r of o)try{i.addEventListener(r,e);}catch{}e(),t.abortController.signal.addEventListener("abort",()=>{if(i?.removeEventListener)for(const r of o)try{i.removeEventListener(r,e);}catch{}},{once:true});}function bv(n){const{self:t,overlayView:e,add:i,addMany:o,platformConfig:r}=n;i(document,"click",d=>{const h=d.target,f=e.votButton?.container,g=e.votMenu?.container,y=t.uiManager.votSettingsView?.dialog?.container,w=h&&f?f.contains(h):false,x=h&&g?g.contains(h):false,m=h?t.container.contains(h):false,k=h&&y?y.contains(h):false,P=h instanceof Element&&h.closest(".vot-dialog-temp")instanceof Element;w||x||k||P||(m||e.updateButtonOpacity(0),g&&!g.hidden&&(g.hidden=true,t.overlayVisibility?.queueAutoHide()));});const s=new Set,a=new Map,l=()=>s.clear(),c=(d,h)=>{d().catch(f=>{});};i(document,"keydown",d=>{const h=d;if(h.repeat)return;s.add(h.code);const f=document.activeElement,g=f?.tagName?.toLowerCase?.()??"";if(["input","textarea"].includes(g)||!!f?.isContentEditable)return;const w=pv(s);if(Sr(w,wr(t.data?.translationHotkey,a))){l(),c(()=>t.uiManager.handleTranslationBtnClick());return}Sr(w,wr(t.data?.subtitlesHotkey,a))&&(l(),c(()=>t.toggleSubtitlesForCurrentLangPair()));}),i(document,"keyup",d=>s.delete(d.code)),i(document,"blur",l),i(document,"visibilitychange",()=>{document.hidden&&l();}),i(globalThis,"blur",l);const u=t.getEventContainer();u&&(o(u,["pointerenter","pointerdown"],d=>t.overlayVisibility.handleHostInteraction(d)),i(u,"pointermove",d=>t.overlayVisibility.handleHostInteraction(d),{passive:true}),i(u,"pointerleave",d=>t.overlayVisibility.scheduleHide(d))),t.rebindOverlayVisibilityTargets(),r.allowTouchMoveHandler&&i(document,"touchmove",d=>t.overlayVisibility.handleHostInteraction(d),{passive:true}),r.disableContainerDrag&&(t.container.draggable=false);}function yv(n){const{self:t,overlayView:e,add:i}=n,o=async()=>{try{await t.setCanPlay();}catch{}};let r=false;const s=()=>{r||(r=true,queueMicrotask(async()=>{r=false,await o();}));};i(t.video,"canplay",()=>{t.site.host==="rutube"&&t.video.src||s();});const a=async()=>{let l;try{l=await os(t.site,{fetchFn:Q,video:t.video});}catch{}t.videoData&&l&&l===t.videoData.videoId||As(t,e,{clearVideoData:true,hideMenu:true});};i(t.video,"emptied",()=>{a().catch(l=>{});}),Tp(t.site.host)||i(t.video,"volumechange",()=>{t.syncVideoVolumeSlider();const l=t.uiManager.votOverlayView;if(!l?.isInitialized())return;const c=ti(l.videoVolumeSlider.value);Qs(t,c,{skipYouTubeLikeHosts:true});}),t.site.host==="youtube"&&!t.site.additionalData&&i(document,"yt-page-data-updated",()=>{globalThis.location.pathname.startsWith("/shorts/")&&s();});}function wv(){const n=this.uiManager.votOverlayView;if(!n?.subtitlesSelect)return;const{add:t,addMany:e}=Zs(this.abortController.signal),i={self:this,overlayView:n,platformConfig:hv(this.site.host),add:t,addMany:e};gv(i),mv(i),vv(i),bv(i),yv(i);}function Sv(){this.overlayVisibilityTargetsAbortController?.abort(),this.overlayVisibilityTargetsAbortController=new AbortController;const{signal:n}=this.overlayVisibilityTargetsAbortController,t=this.uiManager?.votOverlayView?.votButton?.container,e=this.uiManager?.votOverlayView?.votMenu?.container;if(!t||!e||!this.overlayVisibility)return;const i=this.overlayVisibility,{addMany:o}=Zs(n);br(o,t,i),br(o,e,i);}function xv(n){if(!(n instanceof Node))return false;const t=this.uiManager?.votOverlayView,e=t?.votButton?.container,i=t?.votMenu?.container;return e instanceof Node&&e.contains(n)||i instanceof Node&&i.contains(n)}function kv(){const n=this.data?.autoHideButtonDelay;return typeof n=="number"&&Number.isFinite(n)?n:hi}function Tv(){this.resizeObserver?.disconnect(),this.overlayVisibilityTargetsAbortController?.abort(),this.overlayVisibilityTargetsAbortController=void 0,_s(this.site)&&this.syncVolumeObserver?.disconnect();}let ie;function Lv(n){ie=n;}let Nn=null;async function Av(){ie||(Nn??=(async()=>{try{const e=(await(await Q("https://cloudflare-dns.com/cdn-cgi/trace",{timeout:7e3})).text()).split(` -`).find(i=>i.startsWith("loc="));Lv(e?.slice(4,6).toUpperCase());}catch(n){console.error("[VOT] Error getting country:",n);}})().finally(()=>{Nn=null;}),await Nn);}async function Iv(){if(this.initialized)return;const n=this.isAudioContextSupported;this.data=await I.getValues({autoTranslate:false,autoSubtitles:false,dontTranslateLanguages:[Ke],enabledDontTranslateLanguages:true,enabledAutoVolume:true,enabledSmartDucking:true,autoVolume:fn,buttonPos:"default",showVideoSlider:true,syncVolume:false,downloadWithName:xe,sendNotifyOnComplete:false,subtitlesMaxLength:300,subtitlesSmartLayout:true,highlightWords:false,subtitlesFontSize:20,subtitlesFontFamily:"default-sans",subtitlesOpacity:20,subtitlesDownloadFormat:"srt",responseLanguage:Ke,defaultVolume:100,onlyBypassMediaCSP:n,newAudioPlayer:n,showPiPButton:false,translateAPIErrors:true,translationService:pn,detectService:di,translationHotkey:null,subtitlesHotkey:null,m3u8ProxyHost:Kc,proxyWorkerHost:we,translateProxyEnabled:0,translateProxyEnabledDefault:true,audioBooster:false,useLivelyVoice:false,autoHideButtonDelay:hi,useAudioDownload:xe,compatVersion:"",account:{},localeHash:"",localeUpdatedAt:0}),this.data.compatVersion!==Se&&(this.data=await Yu(this.data),await I.set("compatVersion",Se));try{if(Ke==="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 t=this.data.responseLanguage;this.data.dontTranslateLanguages=[t],await I.set("dontTranslateLanguages",this.data.dontTranslateLanguages);}}catch{}this.uiManager.data=this.data,console.log("[VOT] data from db:",this.data),!this.data.translateProxyEnabled&&ms&&(this.data.translateProxyEnabled=1),await Av(),ie&&cs.includes(ie)&&this.data.translateProxyEnabledDefault&&(this.data.translateProxyEnabled=2),V.log("translateProxyEnabled",this.data.translateProxyEnabled,this.data.translateProxyEnabledDefault),await this.initVOTClient(),this.uiManager.initUI(),this.uiManager.initUIEvents(),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;}function Cv(n,t="Operation timed out"){return new Promise((e,i)=>setTimeout(()=>i(new Error(t)),n))}const Ev="und",Pv=new Map,Vv=new Map,Mv=n=>{if(n)try{return Intl.getCanonicalLocales(n)[0]}catch{return}},_v=n=>{const t=Mv(n);if(t)return Intl.Segmenter.supportedLocalesOf([t])[0]},ea=(n,t)=>{const e=_v(n),i=`${t}:${e??Ev}`,o=t==="sentence"?Vv:Pv,r=o.get(i);if(r)return r;const s=new Intl.Segmenter(e,{granularity:t});return o.set(i,s),s},na=(n,t)=>n?Array.from(ea(t,"word").segment(n),e=>({text:e.segment,index:e.index,isWordLike:!!e.isWordLike})):[],Ov=(n,t)=>n?Array.from(ea(t,"sentence").segment(n),e=>({text:e.segment,index:e.index})):[],sn=/\s/u,Dv=/^[,.:;!?%)\]}>В»]/u,Rv=/[([{'"«„-]$/u,xr=/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u,ia=n=>!!n.trim(),Bv=(n,t)=>{const e=n.at(-1)??"",i=t[0]??"";return !(!e||!i||sn.test(e)||sn.test(i)||Rv.test(n)||Dv.test(t)||xr.test(e)&&xr.test(i))},Fv=n=>{let t="",e="";const i=[];for(const o of n){const r=o.text.trim();if(!r)continue;t&&Bv(e,r)&&(t+=" ");const s=t.length;t+=r;const a=t.length;i.push({line:{...o,text:r},text:r,start:s,end:a}),e=r;}return {streamText:t,spans:i}},Nv=(n,t,e)=>{let i=t,o=e;for(;ii&&sn.test(n[o-1]??"");)o-=1;return i>=o?null:{start:i,end:o}},$v=(n,t,e)=>{const i=Math.max(t,n.start),o=Math.min(e,n.end);if(i>=o)return null;const r=i-n.start,s=o-n.start,a=n.text.slice(r,s);if(!a)return null;const l=Math.max(n.text.length,1),c=n.line.startMs+Math.round(n.line.durationMs*r/l),u=n.line.startMs+Math.round(n.line.durationMs*s/l);return {text:a,startMs:c,durationMs:Math.max(0,u-c),isWordLike:ia(a)}},Hv=(n,t,e)=>{const i=e.index,o=i+e.text.length,r=Nv(n,i,o);if(!r)return null;const s=n.slice(r.start,r.end);if(!ia(s))return null;const a=[];let l="0";for(const d of t){const h=$v(d,r.start,r.end);h&&(a.length===0&&(l=d.line.speakerId),a.push(h));}if(!a.length)return null;const c=Math.min(...a.map(d=>d.startMs)),u=Math.max(...a.map(d=>d.startMs+Math.max(0,d.durationMs)));return {text:s,startMs:c,durationMs:Math.max(0,u-c),speakerId:l,tokens:a}},Uv=(n,t)=>{const{streamText:e,spans:i}=Fv(n);if(!e||!i.length)return [];const r=Ov(e,t).map(s=>Hv(e,i,s)).filter(s=>s!==null);return r.length?r:i.map(({line:s})=>s)},zv=n=>n==="json"||n==="srt"||n==="vtt"||n==="ass",Wv=n=>!!(n&&typeof n=="object"),qv=n=>typeof n=="number"&&Number.isFinite(n)?n:0,an=n=>Math.max(0,qv(n)),Gv=n=>Wv(n)?typeof n.source=="string"&&zv(n.format)&&typeof n.language=="string"&&typeof n.url=="string"&&(n.translatedFromLanguage===void 0||typeof n.translatedFromLanguage=="string")&&(n.isAutoGenerated===void 0||typeof n.isAutoGenerated=="boolean"):false,Kv=(n,t,e)=>{const i=n.subtitles;if(!Array.isArray(i)||i.length===0)return null;const o=t??e;if(o){const r=i.find(a=>a.language===o&&typeof a.translatedFromLanguage=="string");if(r)return r;const s=i.find(a=>a.language===o);if(s)return s}return i[0]??null},Yv=n=>{const t=B.getPoToken();if(!t)return n;let e=t;for(let r=0;r<10;r+=1){let s;try{s=decodeURIComponent(e);}catch{break}if(s===e)break;e=s;}const i=B.getDeviceParams(),o=typeof i=="string"?i.replace(/^[?&]+/u,""):"";try{const r=new URL(n);if(r.searchParams.set("potc","1"),r.searchParams.set("pot",e),o){const s=new URLSearchParams(o);for(const[a,l]of s.entries())r.searchParams.set(a,l);}return r.toString()}catch{const r=n.includes("?")?"&":"?",s=o?`&${o}`:"";return `${n}${r}potc=1&pot=${encodeURIComponent(e)}${s}`}},kr=(n,t)=>n-t,qe=(n,t)=>nt?1:0,Xv=(n,t)=>{const e=Math.min(n.length,t.length);for(let i=0;in?t===0?e?1:0:e?0:1:0,jv=(n,t,e,i)=>!n||!t||e===i?0:1,Zv=(n,t)=>{const e=n.source==="yandex",i=e?0:1,o=n.language===pi?0:1,r=!!n.translatedFromLanguage,s=t&&n.language===t?0:1,a=Jv(e,s,r),l=jv(e,r,n.translatedFromLanguage,t),c=e&&!r?s:0,u=e?0:+!!n.isAutoGenerated;return [i,o,a,l,c,u]},Qv=(n,t,e)=>({descriptor:n,index:t,rank:Zv(n,e),language:n.language,translatedFromLanguage:n.translatedFromLanguage??"",source:n.source,url:n.url,isAutoGenerated:+!!n.isAutoGenerated}),tb=(n,t)=>{const e=n.map((i,o)=>Qv(i,o,t));return e.sort((i,o)=>{const r=Xv(i.rank,o.rank);if(r!==0)return r;const s=qe(i.language,o.language)||qe(i.translatedFromLanguage,o.translatedFromLanguage)||qe(i.source,o.source)||qe(i.url,o.url)||kr(i.isAutoGenerated,o.isAutoGenerated);return s!==0?s:kr(i.index,o.index)}),e.map(i=>i.descriptor)},eb=(n,t)=>typeof n=="boolean"?n:!!t.trim(),nb=n=>{if(!n||typeof n!="object")return {text:"",startMs:0,durationMs:0,isWordLike:false};const t=n,e=typeof t.text=="string"?t.text:"";return {text:e,startMs:an(t.startMs),durationMs:an(t.durationMs),isWordLike:eb(t.isWordLike,e)}},ib=n=>{if(!n||typeof n!="object")return {text:"",startMs:0,durationMs:0,speakerId:"0",tokens:[]};const t=n,e=Array.isArray(t.tokens)?t.tokens.map(nb):[];return {text:typeof t.text=="string"?t.text:"",startMs:an(t.startMs),durationMs:an(t.durationMs),speakerId:typeof t.speakerId=="string"?t.speakerId:"0",tokens:e}},Tr=n=>{if(!n||typeof n!="object")return {format:"json",subtitles:[]};const t=n,e=Array.isArray(t.subtitles)?t.subtitles.map(ib):[];return {format:t.format??"json",subtitles:e}},ob=/<(?:\d{1,2}:)?\d{2}:\d{2}(?:[.,]\d{1,3})?>/gu,rb=120,sb=/\s+([,.:;!?])/gu,ab=/[ \t]*\n[ \t]*/gu,lb=/[ \t]{2,}/gu,cb=/\n{3,}/gu,ub=/\s+/gu;let $n=null;const db=n=>{if(!n.includes("<"))return n;if(typeof document<"u"){$n||($n=document.createElement("template"));const t=$n;return t.innerHTML=n,t.content.textContent??""}return n.replaceAll(/<\/?[^>]+>/gu,"")},Lr=n=>{let t=n;return t.includes("<")&&(t=db(t.replaceAll(ob,""))),t.replaceAll(sb,"$1").replaceAll(ab,` -`).replaceAll(lb," ").replaceAll(cb,` - -`).trim()},hb=n=>n.toLowerCase().replaceAll(ub," ").trim(),fb=(n,t)=>!t||n.tStartMs+n.dDurationMs<=t.tStartMs?Math.max(0,n.dDurationMs):Math.max(0,t.tStartMs-n.tStartMs),pb=(n,t,e)=>{const i=[];let o="",r=e;for(let s=0;sn.durationMs>0,mb=n=>n.text?ne(n.text):n.tokens.length?ne(n.tokens.map(t=>t.text).join("")):"",ei=(n,t,e)=>{if(!n.length)return [];const i=Math.max(0,e),o=n.map(l=>Math.max(l.length,1)),r=o.reduce((l,c)=>l+c,0),s=new Array(o.length+1).fill(0);for(let l=0;l{const t=[];let e=0;for(let i=0;i{const e=[];for(const i of n){const o=ne(i.text);if(!o||!gb(i))continue;const r=na(o,t).filter(a=>a.isWordLike&&a.text.trim());if(!r.length)continue;const s=ei(r.map(a=>a.text),i.startMs,i.durationMs);e.push(...s);}return e},yb=(n,t,e)=>{if(!e)return [];const i=t.language,o=n.metadata?.styledSpans??_e(n.metadata?.rawText??n.text??e).styledSpans,r=na(e,i);if(!r.length)return [];const s=ei(r.map(d=>d.text),n.startMs,n.durationMs),a=[];for(let d=0;dx.text===` -`?"":x.text),f,g);for(let x=0;x(h.isWordLike&&h.text.trim()&&d.push(f),d),[]);if(!c.length)return a;const u=c.length;for(let d=0;d{const e=await Q(n,{timeout:7e3});if(t==="vtt"||t==="srt"||t==="ass"){const i=await e.text();return am(i,t)}return e.json()},Sb=(n,t)=>{if(t.source==="youtube")return oe.formatYoutubeSubtitles(n,!!t.isAutoGenerated);if(t.format==="srt"||t.format==="vtt"||t.format==="ass")return cr(Tr(n));const e=Tr(n);return t.source==="vk"?oe.cleanJsonSubtitles(e):cr(e)},xb=(n,t)=>{const e=oe.autoMerge(n,t);return {...e,subtitles:oe.processTokens(e,t)}},kb=n=>{const t=[],e=new Set;for(const i of n.subtitles??[])i.language&&!e.has(i.language)&&(e.add(i.language),t.push({source:"yandex",format:"json",language:i.language,url:i.url})),i.translatedLanguage&&t.push({source:"yandex",format:"json",language:i.translatedLanguage,translatedFromLanguage:i.language,url:i.translatedUrl??i.url});return t},oe={processTokens(n,t){const e=[];for(const i of n.subtitles){const o=mb(i),r=yb(i,t,o);e.push({...i,text:o,tokens:r});}return e},formatYoutubeSubtitles(n,t=false){const e=n.events??[];if(!e.length)return console.error("[VOT] Invalid YouTube subtitles format:",n),{format:"json",subtitles:[]};const i=[];for(let o=0;oe.tokens.length>0)?n:{...n,subtitles:Uv(n.subtitles,t.language)}},cleanJsonSubtitles(n){const t=r=>r.startMs+Math.max(0,r.durationMs),e=r=>{let s=0;for(const a of r)s+=a.text.length;return s},i=[];for(const r of n.subtitles){const s=Lr(r.text);if(!s)continue;const a=[];for(const l of r.tokens){const c=Lr(l.text);c&&a.push({...l,text:c});}i.push({line:{...r,text:s,tokens:a},comparableText:hb(s),tokensTextLength:e(a)});}const o=[];for(const r of i){const{line:s,comparableText:a,tokensTextLength:l}=r;if(!a)continue;const c=o.at(-1);if(!c){o.push(r);continue}const u=t(c.line),d=t(s),h=c.comparableText===a,f=s.startMs<=u+rb;if(!h||!f){o.push(r);continue}const g=Math.min(c.line.startMs,s.startMs),y=Math.max(u,d),w=l>=c.tokensTextLength?s.tokens:c.line.tokens;c.line={...c.line,startMs:g,durationMs:Math.max(0,y-g),tokens:w},c.tokensTextLength=Math.max(c.tokensTextLength,l);}return {...n,subtitles:o.map(({line:r})=>r)}},async fetchSubtitles(n,t,e){const i=Gv(n)?n:Kv(n,t,e);if(!i)return {format:"json",subtitles:[]};const{source:o,format:r}=i;let{url:s}=i;o==="youtube"&&(s=Yv(s));try{const a=await wb(s,r),l=Sb(a,i),c=xb(l,i);return V.log("[VOT] Processed subtitles:",c),c}catch(a){return console.error("[VOT] Failed to process subtitles:",a),{format:"json",subtitles:[]}}},async getSubtitles(n,t){const{host:e,url:i,detectedLanguage:o,videoId:r,duration:s,subtitles:a=[]}=t;try{const l={videoData:{host:e,url:i,videoId:r,duration:s},requestLang:o},u=await Promise.race([n.getSubtitles(l),Cv(5e3,"Timeout")]);V.log("[VOT] Subtitles response:",u),u.waiting&&console.error("[VOT] Failed to get Yandex subtitles");const h=[...kb(u),...a];return tb(h,o)}catch(l){let c="Error in getSubtitles function";throw l instanceof Error&&l.message==="Timeout"&&(c="Failed to get Yandex subtitles: timeout"),console.error(`[VOT] ${c}`,l),l}}},ni="https://vtrans.s3-private.mds.yandex.net/tts/prod/",ln="/video-translation/audio-proxy/",Ar="https://brosubs.s3-private.mds.yandex.net/vtrans/",Tb="/video-subtitles/subtitles-proxy/";function Ai(n){return n??we}function oa(n){return n.translateProxyEnabled===2}function Lb(n,t){return !oa(t)||!n.startsWith(ni)?n:n.replace(ni,`https://${Ai(t.proxyWorkerHost)}${ln}`)}function Ab(n){const t=String(n||"");if(!t)return t;try{const e=new URL(t);return e.pathname.startsWith(ln)?(e.host="vtrans.s3-private.mds.yandex.net",e.pathname=`/tts/prod/${e.pathname.slice(ln.length).replace(/^\/+/,"")}`,e.protocol="https:",e.toString()):t}catch{return t}}function Ib(n,t){return n.startsWith(ni)||n.startsWith(`https://${Ai(t.proxyWorkerHost)}${ln}`)}function Cb(n,t){if(!oa(t)||!n.startsWith(Ar))return n;const e=n.slice(Ar.length);return `https://${Ai(t.proxyWorkerHost)}${Tb}${e}`}const Ce="disabled",Eb=new Set(["srt","vtt","ass","json"]),Pb=/^\d+$/u,ii=new WeakMap;function Vb(n){const t=n.videoData;return t?.videoId?n.getSubtitlesCacheKey(t.videoId,t.detectedLanguage,t.responseLanguage):null}function Mb(n){return n!==null&&typeof n=="object"}function ra(n){if(!Mb(n))return null;const t=n,e=t.format;return typeof t.source!="string"||typeof t.language!="string"||typeof t.url!="string"||typeof e!="string"||!Eb.has(e)?null:{source:t.source,format:e,language:t.language,url:t.url,translatedFromLanguage:typeof t.translatedFromLanguage=="string"?t.translatedFromLanguage:void 0,isAutoGenerated:typeof t.isAutoGenerated=="boolean"?t.isAutoGenerated:void 0}}function sa(n){const t=[];for(let e=0;e=n.length?null:ra(n[t])}function Db(n){const t=(ii.get(n)??0)+1;return ii.set(n,t),t}function Rb(n,t){return ii.get(n)===t}function Bb(){return {label:v.get("VOTSubtitlesDisabled"),value:Ce,selected:true,disabled:false}}function Fb(n){const t=[Bb()];for(const{descriptor:e,index:i}of n)t.push({label:Nb(e),value:String(i),selected:false,disabled:false});return t}function aa(n){const e=n[Symbol.iterator]().next();return e.done?void 0:e.value}function Nb(n){const t=v.getLangLabel(n.language),e=n.translatedFromLanguage?` ${v.get("VOTTranslatedFrom")} ${v.getLangLabel(n.translatedFromLanguage)}`:"",i=n.source==="yandex"?"":`, ${globalThis.location.hostname}`,o=n.isAutoGenerated?` (${v.get("VOTAutogenerated")})`:"";return `${t}${e}${i}${o}`}function Ee(n){return (n??"").toLowerCase()}function cn(n){return Ee(n).split(/[-_]/)[0]}function vt(n,t){if(!n||!t)return false;const e=Ee(n),i=Ee(t);return e===i||cn(e)===cn(i)}function $b(n,t,e){if(!n.length)return null;const i=Ee(t),o=Ee(e),r=i==="auto"||i==="",s=cn(i),a=cn(o),l=m=>m.source==="yandex",c=m=>!!m.isAutoGenerated,u=(m,k,P)=>vt(m.language,P)?r?true:vt(m.translatedFromLanguage,k):false,d=(m,k)=>vt(m.language,k)?m.translatedFromLanguage?vt(m.translatedFromLanguage,k):true:false,h=m=>n.find(({descriptor:k})=>m(k))?.index??null,f=()=>{const m=h(k=>!l(k)&&vt(k.language,o)&&!c(k));return m??h(k=>!l(k)&&vt(k.language,o)&&c(k))},g=h(m=>l(m)&&u(m,i,o));if(g!=null)return g;if(!r&&s&&a&&s===a){const m=h(F=>d(F,o)&&!c(F));if(m!=null)return m;const k=h(F=>d(F,o)&&c(F));if(k!=null)return k;const P=f();if(P!=null)return P;const D=h(F=>l(F)&&vt(F.language,o));if(D!=null)return D}const y=h(m=>l(m)&&vt(m.language,o));if(y!=null)return y;const w=h(m=>!l(m)&&u(m,i,o));if(w!=null)return w;const x=f();return x??null}async function Hb(n){const t=Db(this),e=this.uiManager.votOverlayView;if(!e?.subtitlesSelect||!e.downloadSubtitlesButton)return this;if(e.subtitlesSelect.setSelectedValue(n),n===Ce)return this.hasSubtitlesWidget()&&this.subtitlesWidget?.setContent(null),e.downloadSubtitlesButton.hidden=true,this.yandexSubtitles=null,this;const i=_b(n);if(i==null)return this.hasSubtitlesWidget()&&this.subtitlesWidget?.setContent(null),e.downloadSubtitlesButton.hidden=true,this.yandexSubtitles=null,this;const o=Ob(this.subtitles,i);if(!o)return this.hasSubtitlesWidget()&&this.subtitlesWidget?.setContent(null),e.downloadSubtitlesButton.hidden=true,this.yandexSubtitles=null,this;let r={...o};const s=Cb(r.url,{translateProxyEnabled:this.data?.translateProxyEnabled,proxyWorkerHost:this.data?.proxyWorkerHost});s!==r.url&&(r={...r,url:s},console.log(`[VOT] Subs proxied via ${r.url}`));const a=await oe.fetchSubtitles(r);return Rb(this,t)?(this.yandexSubtitles=a,this.getSubtitlesWidget().setContent(this.yandexSubtitles,r.language),e.downloadSubtitlesButton.hidden=false,this):this}async function Ub(){const n=this.uiManager.votOverlayView;if(!n?.subtitlesSelect)return;const t=sa(this.subtitles),e=Fb(t);n.subtitlesSelect.updateItems(e),await this.changeSubtitlesLang(Ce);}async function la(){const n=Vb(this);if(!n)return (this.subtitlesCacheKey!==null||this.subtitles.length>0)&&(this.subtitles=[],this.subtitlesCacheKey=null,await this.updateSubtitlesLangSelect()),this;if(this.subtitlesCacheKey===n){const e=this.cacheManager.getSubtitles(n)!==void 0;if(this.subtitles.length>0||e)return this}const t=this.cacheManager.getSubtitles(n);return t!==void 0?(this.subtitles=Array.isArray(t)?t:[],this.subtitlesCacheKey=n,await this.updateSubtitlesLangSelect(),this):(await this.loadSubtitles(),this)}async function zb(){const n=this.uiManager.votOverlayView;if(!n?.subtitlesSelect)return this;try{await la.call(this);}catch{return this}const t=this.videoData?.detectedLanguage??this.translateFromLang,e=this.videoData?.responseLanguage??this.translateToLang,i=$b(sa(this.subtitles),t,e);return i==null?this:aa(n.subtitlesSelect.selectedValues)===String(i)?this:(await this.changeSubtitlesLang(String(i)),this)}async function Wb(){const n=this.uiManager.votOverlayView;if(!n?.subtitlesSelect)return this;const t=aa(n.subtitlesSelect.selectedValues);return t&&t!==Ce?(await this.changeSubtitlesLang(Ce),this):(await this.enableSubtitlesForCurrentLangPair(),this)}async function qb(){if(!this.videoData?.videoId){console.error(`[VOT] ${v.getDefault("VOTNoVideoIDFound")}`),this.subtitles=[],this.subtitlesCacheKey=null;return}const n=this.getSubtitlesCacheKey(this.videoData.videoId,this.videoData.detectedLanguage,this.videoData.responseLanguage);try{let t=this.cacheManager.getSubtitles(n);if(!t){let e=this.subtitlesLoadPromises.get(n);e===void 0&&(e=oe.getSubtitles(this.votClient,this.videoData),this.subtitlesLoadPromises.set(n,e));try{t=await e,t=Array.isArray(t)?t:[],this.cacheManager.setSubtitles(n,t);}finally{this.subtitlesLoadPromises.get(n)===e&&this.subtitlesLoadPromises.delete(n);}}this.subtitles=Array.isArray(t)?t:[],this.subtitlesCacheKey=n;}catch(t){console.error("[VOT] Failed to load subtitles:",t),this.subtitles=[],this.subtitlesCacheKey=null;}await this.updateSubtitlesLangSelect();}const Je=0,je=1,yn=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:Qe,applyDeltaThreshold01:Qe/2});function Ii(n){return {isDucked:false,speechGateOpen:false,rmsEnvelope:0,baseline:Dt(n),lastApplied:void 0,lastTickAt:0,lastSoundAt:0,rmsMissingSinceAt:null}}function Gb(){return Ii()}function Kb(n,t,e=yn){const i=Yb(t),o=Dt(n.volumeOnStart);if(!n.translationActive)return {kind:"stop",runtime:i,restoreVolume:i.baseline??o};const r=Number.isFinite(n.nowMs)?n.nowMs:Date.now(),s=i.lastTickAt||r,a=$(r-s,0,e.maxDtMs),l=a/1e3;i.lastTickAt=r;const c=Tt(n.rms),u=c?$(n.rms,Je,je):0,d=i.rmsEnvelope,h=u>d?e.rmsAttackTauMs:e.rmsReleaseTauMs,f=h>0?-Math.expm1(-a/h):1;i.rmsEnvelope=$(d+(u-d)*f,Je,je);let g=i.speechGateOpen;n.audioIsPlaying&&!c?(i.rmsMissingSinceAt??=r,g&&(i.lastSoundAt=r),i.rmsMissingSinceAt!==null&&r-i.rmsMissingSinceAt>=e.rmsMissingGraceMs&&(g=true,i.lastSoundAt=r)):(i.rmsMissingSinceAt=null,g?n.audioIsPlaying&&i.rmsEnvelope>=e.thresholdOffRms?i.lastSoundAt=r:r-i.lastSoundAt>e.holdMs&&(g=false):n.audioIsPlaying&&i.rmsEnvelope>=e.thresholdOnRms&&(g=true,i.lastSoundAt=r)),i.speechGateOpen=g;const y=Dt(n.currentVideoVolume);if(!Tt(y))return {kind:"noop",runtime:i};i.isDucked&&Tt(i.lastApplied)&&Math.abs(y-i.lastApplied)>e.externalBaselineDelta01&&(i.baseline=y),i.isDucked||(i.baseline=y);const w=i.baseline??o??y;if(i.baseline=w,!n.hostVideoActive)return i.lastApplied=y,{kind:"noop",runtime:i};const x=Dt(n.duckingTarget01)??w,m=Math.min(w,x);let k=w;g?(i.isDucked||(i.isDucked=true),k=m):i.isDucked&&Math.abs(w-y)0?-Math.expm1(-a/P):1;let F=y+(k-y)*D;const lt=(k0&&(F=$(F,y-lt,y+lt)),F=$(F,Je,je);const tt=wp(F,y,k,e.volumeStep01),gt=e.applyDeltaThreshold01;return Math.abs(tt-y)=gt?(i.lastApplied=tt,{kind:"apply",runtime:i,volume01:tt}):{kind:"noop",runtime:i}}function Yb(n){return {isDucked:!!n.isDucked,speechGateOpen:!!n.speechGateOpen,rmsEnvelope:Dt(n.rmsEnvelope)??0,baseline:Dt(n.baseline),lastApplied:Dt(n.lastApplied),lastTickAt:Tt(n.lastTickAt)?n.lastTickAt:0,lastSoundAt:Tt(n.lastSoundAt)?n.lastSoundAt:0,rmsMissingSinceAt:Tt(n.rmsMissingSinceAt)?n.rmsMissingSinceAt:null}}function Dt(n){if(Tt(n))return $(n,Je,je)}function Tt(n){return typeof n=="number"&&Number.isFinite(n)}const Xb=yn.tickMs,Ir=5e3,Jb=150,Cr=2,un=new WeakMap;function Hn(n){if(!n||typeof n!="object")return false;const t=n;return typeof t.connect=="function"&&typeof t.disconnect=="function"}function jb(n){return n?.audio??n?.audioElement}function ca(){return typeof performance<"u"&&typeof performance.now=="function"?performance.now():Date.now()}function Ci(n){return n.data?.syncVolume||!n.data?.enabledAutoVolume?"off":n.data?.enabledSmartDucking??true?"smart":"classic"}async function Zb(n){const t=n.audioPlayer?.audioContext;if(!t||t.state!=="suspended")return "not-needed";const e=1500,i=(async()=>{try{return await t.resume(),"resumed"}catch{return "failed"}})();let o;const r=new Promise(a=>{o=setTimeout(()=>a("timeout"),e);}),s=await Promise.race([i,r]);return o!==void 0&&clearTimeout(o),s}async function dn(n,t){if(!t||!n.audioPlayer)return;const e=n.audioPlayer.player,i=String(e.currentSrc||e.src||""),o=n.proxifyAudio(n.unproxifyAudio(i)),r=n.proxifyAudio(n.unproxifyAudio(t));if(o===r)try{await e.clear(),e.src="",V.log("[updateTranslation] cleared stale partially-applied source");}catch{}}function Qb(n){return n.audioPlayer?.audioContext??n.audioContext}function ua(n){if(n.connectedInputNode&&n.analyser)try{n.connectedInputNode.disconnect(n.analyser);}catch{}if(n.connectedInputNode=void 0,n.createdMediaSource)try{n.createdMediaSource.disconnect();}catch{}if(n.createdMediaSource=void 0,n.analyser)try{n.analyser.disconnect();}catch{}n.analyser=void 0,n.analyserFloatData=void 0,n.analyserData=void 0,n.mediaElement=void 0,n.audioContext=void 0,n.mediaSourceCreationFailed=false;}function ty(n){const t=un.get(n);t&&(ua(t),un.delete(n));}function ey(n,t,e,i){if(Hn(n?.gainNode))return n.gainNode;if(Hn(n?.audioSource))return n.audioSource;if(Hn(n?.mediaElementSource))return n.mediaElementSource;if(!(i.mediaSourceCreationFailed&&i.mediaElement===t&&i.audioContext===e)){if(i.createdMediaSource&&i.mediaElement===t&&i.audioContext===e)return i.createdMediaSource;try{const o=e.createMediaElementSource(t);return i.createdMediaSource=o,i.mediaSourceCreationFailed=!1,o}catch{i.mediaSourceCreationFailed=true;return}}}function ny(n,t,e){const i=Qb(n);if(!i)return;let o=un.get(n);if(o||(o={},un.set(n,o)),(o.mediaElement&&o.mediaElement!==e||o.audioContext&&o.audioContext!==i)&&ua(o),o.mediaElement=e,o.audioContext=i,!o.analyser){const a=i.createAnalyser();a.fftSize=512,o.analyser=a;}const r=ey(t,e,i,o),s=o.analyser;if(!(!r||!s)){if(o.connectedInputNode!==r){if(o.connectedInputNode)try{o.connectedInputNode.disconnect(s);}catch{}try{r.connect(s),o.connectedInputNode=r;}catch{return}}return {analyser:s,state:o}}}function iy(n){return {isDucked:n.smartVolumeIsDucked,speechGateOpen:n.smartVolumeSpeechGateOpen,rmsEnvelope:n.smartVolumeRmsEnvelope,baseline:n.smartVolumeDuckingBaseline,lastApplied:n.smartVolumeLastApplied,lastTickAt:n.smartVolumeLastTickAt,lastSoundAt:n.smartVolumeLastSoundAt,rmsMissingSinceAt:n.smartVolumeRmsMissingSinceAt}}function Pe(n,t){n.smartVolumeIsDucked=t.isDucked,n.smartVolumeSpeechGateOpen=t.speechGateOpen,n.smartVolumeRmsEnvelope=t.rmsEnvelope,n.smartVolumeDuckingBaseline=t.baseline,n.smartVolumeLastApplied=t.lastApplied,n.smartVolumeLastTickAt=t.lastTickAt,n.smartVolumeLastSoundAt=t.lastSoundAt,n.smartVolumeRmsMissingSinceAt=t.rmsMissingSinceAt;}function wn(n,t={}){const{restoreVolume:e}=t;n.smartVolumeDuckingInterval!==void 0&&(clearTimeout(n.smartVolumeDuckingInterval),n.smartVolumeDuckingInterval=void 0);const i=typeof e=="number"?e:n.smartVolumeDuckingBaseline??n.volumeOnStart;if(typeof i=="number"&&(typeof e=="number"||n.smartVolumeIsDucked))try{n.setVideoVolume(i);}catch{}ty(n),Pe(n,Gb());}function da(n){typeof globalThis>"u"||n.smartVolumeDuckingInterval!==void 0&&(n.smartVolumeDuckingInterval=globalThis.setTimeout(()=>{if(n.smartVolumeDuckingInterval!==void 0){try{sy(n);}catch{wn(n);return}n.smartVolumeDuckingInterval!==void 0&&da(n);}},Xb));}function oy(n){if(typeof globalThis>"u"||n.smartVolumeDuckingInterval!==void 0||Ci(n)!=="smart")return;const t=n.getVideoVolume(),e=typeof n.smartVolumeDuckingBaseline=="number"?n.smartVolumeDuckingBaseline:t,i=Ii(e);if(Number.isFinite(t)&&Number.isFinite(e)&&t{},0),clearTimeout(n.smartVolumeDuckingInterval),da(n);}function ry(n,t){const e=n.audioPlayer?.player,i=ny(n,e,t);if(!i)return;const{analyser:o,state:r}=i;try{if(typeof o.getFloatTimeDomainData=="function"){let l=r.analyserFloatData;l?.length!==o.fftSize&&(l=new Float32Array(o.fftSize),r.analyserFloatData=l),o.getFloatTimeDomainData(l);let c=0;for(const u of l)c+=u*u;return $(Math.sqrt(c/l.length),0,1)}let s=r.analyserData;s?.length!==o.fftSize&&(s=new Uint8Array(o.fftSize),r.analyserData=s),o.getByteTimeDomainData(s);let a=0;for(const l of s){const c=(l-128)/128;a+=c*c;}return $(Math.sqrt(a/s.length),0,1)}catch{return}}function sy(n){if(Ci(n)!=="smart"){fa.call(n);return}const t=n.audioPlayer?.player,e=jb(t),i=!!e&&!e.paused&&!e.muted&&(e.volume??1)>.001,o=ca(),r=n.getVideoVolume(),s=n.video,a=!(s&&(s.paused||s.ended)),l=$(n.data?.autoVolume??fn,0,100)/100;n.smartVolumeDuckingTarget=l;const c=i&&e?ry(n,e):0,u=Kb({nowMs:o,translationActive:n.hasActiveSource(),audioIsPlaying:i,rms:c,currentVideoVolume:r,hostVideoActive:a,duckingTarget01:l,volumeOnStart:n.volumeOnStart},iy(n),yn);switch(u.kind){case "stop":wn(n,{restoreVolume:u.restoreVolume});return;case "apply":n.setVideoVolume(u.volume01),Pe(n,u.runtime);return;case "noop":Pe(n,u.runtime);return;default:throw new TypeError("Unhandled smart ducking decision")}}function ay(n,t){return t.aborted?Promise.resolve():new Promise(e=>{const i=setTimeout(()=>{t.removeEventListener("abort",o),e();},n),o=()=>{clearTimeout(i),t.removeEventListener("abort",o),e();};t.addEventListener("abort",o,{once:true});})}async function Er(n,t,e){const i=n.actionsAbortController.signal,o=n.isMultiMethodS3(t)?{method:"HEAD",signal:i,timeout:Ir}:{headers:{range:"bytes=0-0"},signal:i,timeout:Ir};for(let r=1;r<=Cr;r++){if(n.isActionStale(e))return false;try{const s=await Q(t,o);if(n.isActionStale(e))return !1;if(V.log("[validateAudioUrl] probe response",{audioUrl:t,attempt:r,ok:s.ok,status:s.status}),s.ok)return !0}catch{if(n.isActionStale(e)||i.aborted)return false}if(r{this.refreshTranslationAudio().catch(t=>{});},n);}async function ha(n,t){const e=await Jf({requester:n.translationHandler,request:{videoData:t.videoData,requestLang:t.requestLang,responseLang:t.responseLang,translationHelp:t.translationHelp,useAudioDownload:!!n.data?.useAudioDownload,signal:n.actionsAbortController.signal},actionContext:t.actionContext,isActionStale:i=>n.isActionStale(i),updateTranslation:(i,o)=>n.updateTranslation(i,o),scheduleTranslationRefresh:()=>n.scheduleTranslationRefresh()});return e?(t.onBeforeCache&&await t.onBeforeCache(e),jf({cacheKey:t.cacheKey,setTranslation:(i,o)=>n.cacheManager.setTranslation(i,o),videoId:t.cacheVideoId,requestLang:t.cacheRequestLang,responseLang:t.cacheResponseLang,fallbackUrl:e.url,downloadTranslationUrl:n.downloadTranslationUrl,usedLivelyVoice:e.usedLivelyVoice}),e):null}async function uy(){if(!this.videoData||this.videoData.isStream||!this.hasActiveSource()||this.isRefreshingTranslation)return;const n=this.videoData.videoId;if(!n)return;this.actionsAbortController?.signal?.aborted&&this.resetActionsAbortController("refreshTranslationAudio"),this.isRefreshingTranslation=true;const t={gen:this.actionsGeneration,videoId:n},e=bi(this.videoData.translationHelp);try{if(!await ha(this,{videoData:this.videoData,requestLang:this.translateFromLang,responseLang:this.translateToLang,translationHelp:e,actionContext:t,cacheKey:this.getTranslationCacheKey(n,this.translateFromLang,this.translateToLang,e),cacheVideoId:n,cacheRequestLang:this.translateFromLang,cacheResponseLang:this.translateToLang}))return}finally{this.isRefreshingTranslation=false;}}function dy(n){return Lb(n,{translateProxyEnabled:this.data?.translateProxyEnabled,proxyWorkerHost:this.data?.proxyWorkerHost})}function hy(n){return Ab(n)}async function fy(n="proxySettingsChanged"){try{this.cacheManager.clear(),this.activeTranslation=null;}catch{}try{await this.stopTranslation();}catch{}await this.initVOTClient();}function py(n){return Ib(n,{proxyWorkerHost:this.data?.proxyWorkerHost})}function Pr(n,t){return n.proxifyAudio(n.unproxifyAudio(t))}async function Vr(n,t,e){const i=n.audioPlayer.player.src!==t;let o=null;i&&(n.audioPlayer.player.src=t,o=t);try{if(i&&await n.audioPlayer.init(),n.isActionStale(e))return await dn(n,o),{status:"stale",didSetSource:i,appliedSourceUrl:o};const r=await Zb(n);return r==="timeout"?V.log("[updateTranslation] continuing after AudioContext resume timeout"):r==="failed"&&V.log("[updateTranslation] AudioContext resume failed, continue without deferred resume"),n.isActionStale(e)?(await dn(n,o),{status:"stale",didSetSource:i,appliedSourceUrl:o}):(!n.video.paused&&n.audioPlayer.player.src&&n.audioPlayer.player.lipSync("play"),{status:"success",didSetSource:i,appliedSourceUrl:o})}catch(r){return {status:"error",didSetSource:i,appliedSourceUrl:o,error:r}}}async function gy(n,t){if(await this.waitForPendingStopTranslate(),this.isActionStale(t))return;this.audioPlayer||this.createPlayer(),this.audioPlayer.audioContext?.state==="closed"&&this.createPlayer();const e=Pr(this,n),i=this.audioPlayer.player.currentSrc||this.audioPlayer.player.src||"",o=Pr(this,i);let r=e;if(e!==o&&(r=await this.validateAudioUrl(e,t)),this.isActionStale(t))return;let s=await Vr(this,r,t),a=s.appliedSourceUrl;if(s.status==="error"&&s.didSetSource&&!this.isActionStale(t)){const l=this.unproxifyAudio(r);if(l!==r)try{V.log("[updateTranslation] proxied audio init failed, retrying direct URL");const c=await this.validateAudioUrl(l,t);if(this.isActionStale(t)){await dn(this,a);return}r=c,s=await Vr(this,c,t),a=s.appliedSourceUrl;}catch(c){s={status:"error",didSetSource:true,appliedSourceUrl:a,error:c};}}if(s.status!=="stale"){if(s.status==="error"){V.log("this.audioPlayer.init() error",s.error),await dn(this,a);const l=ps(s.error);this.transformBtn("error",l);return}this.setupAudioSettings(),this.transformBtn("success",v.get("disableTranslate")),this.afterUpdateTranslation(r);}}async function my(n,t,e,i,o){await this.waitForPendingStopTranslate(),await this.videoValidator(),this.actionsAbortController?.signal?.aborted&&this.resetActionsAbortController("translateFunc");const r=this.uiManager.votOverlayView;if(!r?.votButton)return;if(r.votButton.loading=true,this.hadAsyncWait=false,this.volumeOnStart=this.getVideoVolume(),!n){await this.updateTranslationErrorMsg(new nt("VOTNoVideoIDFound"),this.actionsAbortController.signal);return}const s=this.videoData;if(!s){await this.updateTranslationErrorMsg(new nt("VOTNoVideoIDFound"),this.actionsAbortController.signal);return}const a=bi(o),l=this.getTranslationCacheKey(n,e,i,a),c=`video_${l}`;if(this.activeTranslation?.key===c){await this.activeTranslation.promise;return}const u={gen:this.actionsGeneration,videoId:n},d=(async()=>{if(this.isActionStale(u))return;const h=e,f=i,g=async x=>await ks({url:x,actionContext:u,isActionStale:m=>this.isActionStale(m),updateTranslation:(m,k)=>this.updateTranslation(m,k),scheduleTranslationRefresh:()=>this.scheduleTranslationRefresh()}),y=this.cacheManager.getTranslation(l);if(y?.url)return await g(y.url),void 0;await ha(this,{videoData:s,requestLang:h,responseLang:f,translationHelp:a,actionContext:u,cacheKey:l,cacheVideoId:n,cacheRequestLang:e,cacheResponseLang:i,onBeforeCache:async()=>{const x=this.videoData?this.getSubtitlesCacheKey(n,this.videoData.detectedLanguage,this.videoData.responseLanguage):null;(x?this.cacheManager.getSubtitles(x):null)?.some(k=>k.source==="yandex"&&k.translatedFromLanguage===s.detectedLanguage&&k.language===s.responseLanguage)||(x&&this.cacheManager.deleteSubtitles(x),this.subtitles=[],this.subtitlesCacheKey=null);}});})();this.activeTranslation={key:c,promise:d};try{return await d}catch(h){throw this.hadAsyncWait=Ts({aborted:this.actionsAbortController.signal.aborted,translateApiErrorsEnabled:!!this.data?.translateAPIErrors,hadAsyncWait:this.hadAsyncWait,videoId:n,error:h,notify:f=>this.notifier.translationFailed(f)}),h}finally{this.activeTranslation?.promise===d&&(this.activeTranslation=null);const h=this.uiManager.votOverlayView?.votButton;!this.activeTranslation&&h?.loading&&!this.hasActiveSource()&&this.transformBtn("none",v.get("translateVideo"));}}function vy(){return Lp(this.site.host)}function fa(){typeof this.data?.defaultVolume=="number"&&(this.audioPlayer.player.volume=this.data.defaultVolume/100);const n=Ci(this);if(n==="off"){wn(this,{restoreVolume:this.smartVolumeDuckingBaseline??this.volumeOnStart});return}const t=$(this.data.autoVolume??fn,0,100)/100;if(this.smartVolumeDuckingTarget=t,!this.hasActiveSource())return;if(n==="smart"){oy(this);return}this.smartVolumeDuckingInterval!==void 0&&(clearTimeout(this.smartVolumeDuckingInterval),this.smartVolumeDuckingInterval=void 0),typeof this.smartVolumeDuckingBaseline!="number"&&(this.smartVolumeDuckingBaseline=this.getVideoVolume());const e=this.smartVolumeDuckingBaseline??this.getVideoVolume();this.setVideoVolume(Math.min(e,t)),Pe(this,Ii(this.smartVolumeDuckingBaseline)),this.smartVolumeIsDucked=true;}const by=new Set(si),yy=n=>by.has(n),wy=Promise.resolve();class Sy{video;container;site;translateFromLang="auto";translateToLang=Ke;data;videoData;firstPlay=true;audioContext;votClient;audioPlayer;abortController;actionsAbortController;actionsGeneration=0;notifier=new iv;cacheManager;votSessionStorage=new Lu;subtitlesLoadPromises=new Map;downloadTranslationUrl=null;translationRefreshTimeout;isRefreshingTranslation=false;autoRetry;votOpts;volumeOnStart;volumeLinkState={initialized:false,lastVideoPercent:0,lastTranslationPercent:0};internalVideoVolumeSetAt=0;internalVideoVolumeSetPercent=null;internalVideoVolumeSuppressionMs=250;internalVideoVolumeSetHistory=[];internalVideoVolumeSetHistoryLimit=48;smartVolumeDuckingInterval;smartVolumeDuckingTarget=.2;smartVolumeDuckingBaseline;smartVolumeLastApplied;smartVolumeLastTickAt=0;smartVolumeLastSoundAt=0;smartVolumeRmsMissingSinceAt=null;smartVolumeRmsEnvelope=0;smartVolumeSpeechGateOpen=false;smartVolumeIsDucked=false;longWaitingResCount=0;hadAsyncWait=false;subtitles=[];subtitlesCacheKey=null;subtitlesWidget;activeTranslation=null;stopTranslatePromise=null;interactionChecker;uiManager;overlayVisibility;overlayVisibilityTargetsAbortController;translationOrchestrator;lifecycleController;translationHandler;videoManager;yandexSubtitles=null;resizeObserver;syncVolumeObserver;initialized=false;mountCache;errorTranslationCache=new Map;getFullscreenOverlayRoot(){const t=document,e=t.fullscreenElement??t.webkitFullscreenElement;return mi(e,[this.container])}getOverlayMountPoints(t=this.container){const e=this.getFullscreenOverlayRoot(),{base:i,root:o,portalContainer:r,subtitlesMountContainer:s}=gf({container:t,site:this.site,fullscreenRoot:e}),a=this.mountCache;return a?.container===t&&a.base===i&&a.subtitlesMountContainer===s&&a.fullscreenRoot===e&&(a.root.isConnected??document.documentElement.contains(a.root))?{root:a.root,portalContainer:a.portalContainer,subtitlesMountContainer:a.subtitlesMountContainer,fullscreenRoot:a.fullscreenRoot}:(this.mountCache={container:t,base:i,root:o,portalContainer:r,subtitlesMountContainer:s,fullscreenRoot:e},{root:o,portalContainer:r,subtitlesMountContainer:s,fullscreenRoot:e})}getOverlayMount(t=this.container){const{root:e,portalContainer:i,subtitlesMountContainer:o,fullscreenRoot:r}=this.getOverlayMountPoints(t);return {root:e,portalContainer:i,subtitlesMountContainer:o,tooltipLayoutRoot:r??this.tooltipLayoutRoot}}getTranslationCacheKey(t,e,i,o){const r=this.getRequestLangForTranslation(e,i),s=this.isLivelyVoiceAllowed(r,i)&&this.data?.useLivelyVoice,a=o==null?"":hu(o),l=a?Kn(a):"0";return `${t}_${r}_${i}_${s}_${l}`}getSubtitlesCacheKey(t,e,i){return `${t}_${e}_${i}_${!!this.data?.useLivelyVoice}`}isActionStale(t){return t?this.actionsGeneration!==t.gen||this.videoData?.videoId!==t.videoId:false}updateVOTClientRequestSignal(){this.votClient&&(this.votClient.fetchOpts={...this.votClient.fetchOpts??{},signal:this.actionsAbortController.signal});}resetActionsAbortController(t){try{this.actionsAbortController?.abort(t);}catch{}this.actionsAbortController=new AbortController,this.actionsGeneration++,this.updateVOTClientRequestSignal();}constructor(t,e,i){this.video=t,this.container=e,this.site=i,this.abortController=new AbortController,this.actionsAbortController=new AbortController,this.cacheManager=new Au,this.interactionChecker=Li(),this.interactionChecker.start();const o=()=>this,r=this.getOverlayMount(e);this.uiManager=new Gm({mount:r,data:this.data,videoHandler:this,intervalIdleChecker:this.interactionChecker}),this.overlayVisibility=new Km({checker:this.interactionChecker,getOverlayView:()=>this.uiManager.votOverlayView??null,getAutoHideDelay:()=>this.getAutoHideDelay(),isInteractiveNode:a=>this.isOverlayInteractiveNode(a)}),this.translationOrchestrator=new ep({isFirstPlay:()=>this.firstPlay,setFirstPlay:a=>{this.firstPlay=a;},isAutoTranslateEnabled:()=>!!this.data?.autoTranslate,getVideoId:()=>this.videoData?.videoId,scheduleAutoTranslate:()=>this.runAutoTranslate(),isMobileYouTubeMuted:()=>this.site.host==="youtube"&&this.site.additionalData==="mobile"&&this.video.muted,setMuteWatcher:a=>{let l=false;const c=()=>{l||(l=true,this.video.removeEventListener("volumechange",u),a());},u=()=>{this.video.muted||c();};this.video.addEventListener("volumechange",u,{signal:this.abortController.signal}),queueMicrotask(()=>{this.video.muted||c();});}});const s={get video(){return o().video},get site(){return o().site},get container(){return o().container},set container(a){o().container!==a&&(o().container=a,o().uiManager.updateMount(o().getOverlayMount(a)));},get firstPlay(){return o().firstPlay},set firstPlay(a){o().firstPlay=a;},stopTranslation:()=>this.stopTranslation(),get uiManager(){return o().uiManager},getVideoData:()=>this.getVideoData(),cacheManager:{getSubtitles:a=>o().cacheManager.getSubtitles(a)},getSubtitlesCacheKey:(a,l,c)=>this.getSubtitlesCacheKey(a,l,c),updateSubtitlesLangSelect:()=>this.updateSubtitlesLangSelect(),enableSubtitlesForCurrentLangPair:()=>this.enableSubtitlesForCurrentLangPair(),setSelectMenuValues:(a,l)=>this.setSelectMenuValues(a,l),get translateToLang(){return o().translateToLang},set translateToLang(a){yy(a)&&(o().translateToLang=a);},get data(){return o().data??{}},get subtitles(){return o().subtitles},set subtitles(a){o().subtitles=a;},get subtitlesCacheKey(){return o().subtitlesCacheKey},set subtitlesCacheKey(a){o().subtitlesCacheKey=a;},get videoData(){return o().videoData},set videoData(a){o().videoData=a;},get actionsAbortController(){return o().actionsAbortController},set actionsAbortController(a){o().actionsAbortController=a;},resetActionsAbortController:a=>this.resetActionsAbortController(a),initVOTClient:()=>this.initVOTClient(),translationOrchestrator:this.translationOrchestrator,resetSubtitlesWidget:()=>this.resetSubtitlesWidget(),queueOverlayAutoHide:()=>this.overlayVisibility?.queueAutoHide()};this.lifecycleController=new ip(s),this.translationHandler=new tp(this),this.videoManager=new _p(this);}getSubtitlesWidget(){if(!this.subtitlesWidget){const{subtitlesMountContainer:t}=this.getOverlayMountPoints();this.subtitlesWidget=new Vg(this.video,t,this.interactionChecker,this.tooltipLayoutRoot),this.data&&(this.subtitlesWidget.setSmartLayout(typeof this.data.subtitlesSmartLayout=="boolean"?this.data.subtitlesSmartLayout:true),typeof this.data.subtitlesMaxLength=="number"&&this.subtitlesWidget.setMaxLength(this.data.subtitlesMaxLength),typeof this.data.highlightWords=="boolean"&&this.subtitlesWidget.setHighlightWords(this.data.highlightWords),typeof this.data.subtitlesFontSize=="number"&&this.subtitlesWidget.setFontSize(this.data.subtitlesFontSize),typeof this.data.subtitlesFontFamily=="string"&&this.subtitlesWidget.setFontFamily(this.data.subtitlesFontFamily),typeof this.data.subtitlesOpacity=="number"&&this.subtitlesWidget.setOpacity(this.data.subtitlesOpacity));}return this.subtitlesWidget}hasSubtitlesWidget(){return !!this.subtitlesWidget}resetSubtitlesWidget(){this.hasSubtitlesWidget()&&(this.subtitlesWidget?.release(),this.subtitlesWidget=void 0);}get uiRoot(){return this.getOverlayMountPoints().root}get portalContainer(){return this.getOverlayMountPoints().portalContainer}get tooltipLayoutRoot(){switch(this.site.host){case "kickstarter":return document.getElementById("react-project-header")??void 0;case "custom":return;default:return this.container}}getEventContainer(){return this.site.eventSelector?document.querySelector(this.site.eventSelector)??this.container:this.container}async runAutoTranslate(){await this.videoManager.videoValidator(),await this.uiManager.handleTranslationBtnClick();}getAudioContext(){if(this.audioContext)return this.audioContext;if(this.isAudioContextSupported)try{return this.audioContext=ui(),this.audioContext}catch(t){console.warn("[VOT] Failed to init AudioContext, falling back:",t);return}}get isAudioContextSupported(){return globalThis.AudioContext!==void 0||globalThis.webkitAudioContext!==void 0}getPreferAudio(){return !this.getAudioContext()||!this.data||!this.data.newAudioPlayer||this.videoData?.isStream?true:this.data.newAudioPlayer&&!this.data.onlyBypassMediaCSP?false:!this.site.needBypassCSP}createPlayer(){const t=this.getPreferAudio();return this.audioPlayer=new Hc({video:this.video,debug:false,fetchFn:Q,fetchOpts:{timeout:0},preferAudio:t}),this}isLikelyInternalVideoVolumeChange(t){const e=Date.now(),i=this.internalVideoVolumeSetHistory;if(i.length>0){let r=0,s=false;for(const a of i)e-a.at>a.suppressMs||(i[r++]=a,!s&&Math.abs(t-a.percent)<=1&&(s=true));return i.length=r,s}return this.internalVideoVolumeSetPercent===null||e-this.internalVideoVolumeSetAt>this.internalVideoVolumeSuppressionMs?false:Math.abs(t-this.internalVideoVolumeSetPercent)<=1}callModule(t,...e){return t.call(this,...e)}callModuleAsync(t,...e){return t.call(this,...e)}init(){return Iv.call(this)}async initVOTClient(){const t=this.data?.translateProxyEnabled?this.data?.proxyWorkerHost??we:Gc;this.votOpts={fetchFn:Q,fetchOpts:{signal:this.actionsAbortController.signal},apiToken:this.data?.account?.token,hostVOT:Yc,host:t},this.votClient=new(this.data?.translateProxyEnabled?dl:ul)(this.votOpts),this.votClient.sessions=await this.votSessionStorage.restore(t,this.votClient.sessions);const e=this.votClient.getSession.bind(this.votClient);return this.votClient.getSession=async i=>{const o=await e(i);return await this.votSessionStorage.persist(t,this.votClient.sessions),o},this}transformBtn(t,e){return this.uiManager.transformBtn(t,e),this}hasActiveSource(){return !!this.audioPlayer?.player?.src}initExtraEvents(){return this.callModule(wv)}refreshOverlayMount(){this.mountCache=void 0;const t=this.getOverlayMount(this.container),e=!qs(this.uiManager.mount,t);this.uiManager.updateMount(t),e&&this.rebindOverlayVisibilityTargets();}rebindOverlayVisibilityTargets=Sv;setCanPlay(){return this.lifecycleController.setCanPlay()}isOverlayInteractiveNode(t){return this.callModule(xv,t)}getAutoHideDelay(){return this.callModule(kv)}changeSubtitlesLang=Hb;updateSubtitlesLangSelect=Ub;ensureSubtitlesForCurrentLangPair=la;loadSubtitles=qb;enableSubtitlesForCurrentLangPair(){return this.callModuleAsync(zb)}toggleSubtitlesForCurrentLangPair(){return this.callModuleAsync(Wb)}getRequestLangForTranslation(t,e){return this.data?.useLivelyVoice&&this.data?.account?.token&&e==="ru"?"en":t}isLivelyVoiceAllowed(t=this.videoData?.detectedLanguage??"auto",e=this.videoData?.responseLanguage??this.translateToLang){return !(this.getRequestLangForTranslation(t,e)!=="en"||e!=="ru"||!this.data?.account?.token)}getVideoVolume(){return this.videoManager.getVideoVolume()}setVideoVolume(t,e={}){const i=_t(t),o=typeof e.suppressSyncMs=="number"&&Number.isFinite(e.suppressSyncMs)?Math.max(0,e.suppressSyncMs):this.internalVideoVolumeSuppressionMs,r=Date.now(),s=Ps(i);return this.internalVideoVolumeSetAt=r,this.internalVideoVolumeSetPercent=s,this.internalVideoVolumeSetHistory.push({at:r,percent:s,suppressMs:o}),this.internalVideoVolumeSetHistory.length>this.internalVideoVolumeSetHistoryLimit&&this.internalVideoVolumeSetHistory.splice(0,this.internalVideoVolumeSetHistory.length-this.internalVideoVolumeSetHistoryLimit),this.videoManager.setVideoVolume(i),this}onVideoVolumeSliderSynced(t){const e=ft(t);if(!this.volumeLinkState.initialized){Bn(this.volumeLinkState,e);return}this.data?.syncVolume&&this.hasActiveSource()&&!this.isLikelyInternalVideoVolumeChange(e)||Bn(this.volumeLinkState,e);}onTranslationVolumeSliderSynced(t){if(!this.volumeLinkState.initialized){Fn(this.volumeLinkState,t);return}Fn(this.volumeLinkState,t);}resetVolumeLinkState(t,e){Bn(this.volumeLinkState,t),Fn(this.volumeLinkState,e),this.volumeLinkState.initialized=true;}isMuted(){return this.videoManager.isMuted()}syncVideoVolumeSlider(){this.videoManager.syncVideoVolumeSlider();}setSelectMenuValues(t,e){this.videoManager.setSelectMenuValues(t,e);}syncVolumeWrapper(t,e){const i=this.uiManager.votOverlayView;if(!i?.isInitialized())return;const o=i.videoVolumeSlider,r=i.translationVolumeSlider;if(!o||!r)return;const{nextVideo:s,nextTranslation:a}=uv({state:this.volumeLinkState,fromType:t,newVolume:e,currentVideo:Number(o.value),currentTranslation:Number(r.value),translationMin:r.min,translationMax:r.max});if(typeof a=="number"){r.value=a,this.audioPlayer?.player&&(this.audioPlayer.player.volume=a/100);return}typeof s=="number"&&(o.value=s,this.setVideoVolume(s/100));}getVideoData(){return this.videoManager.getVideoData()}videoValidator(){return this.videoManager.videoValidator()}stopTranslate(){if(this.stopTranslatePromise!==null)return this.stopTranslatePromise;const e=(async()=>{if(this.audioPlayer?.player){try{this.audioPlayer.player.removeVideoEvents(),this.audioPlayer.player.src="",await this.audioPlayer.player.clear();}catch{}V.log("audioPlayer after stopTranslate",this.audioPlayer);}this.activeTranslation=null;const i=this.uiManager.votOverlayView;i&&(i.videoVolumeSlider&&(i.videoVolumeSlider.hidden=true),i.translationVolumeSlider&&(i.translationVolumeSlider.hidden=true),i.downloadTranslationButton&&(i.downloadTranslationButton.hidden=true)),this.downloadTranslationUrl=null,this.longWaitingResCount=0,this.hadAsyncWait=false,this.transformBtn("none",v.get("translateVideo")),V.log(`Volume on start: ${this.volumeOnStart}`);const o=typeof this.smartVolumeDuckingBaseline=="number"?this.smartVolumeDuckingBaseline:this.volumeOnStart;wn(this,{restoreVolume:o}),this.volumeOnStart=void 0,this.autoRetry!==void 0&&(clearTimeout(this.autoRetry),this.autoRetry=void 0),this.translationRefreshTimeout!==void 0&&(clearTimeout(this.translationRefreshTimeout),this.translationRefreshTimeout=void 0),this.resetActionsAbortController("stopTranslate");})().finally(()=>{this.stopTranslatePromise===e&&(this.stopTranslatePromise=null);});return this.stopTranslatePromise=e,e}waitForPendingStopTranslate(){return this.stopTranslatePromise??wy}async updateTranslationErrorMsg(t,e){if(e?.aborted)return;const i=v.get("translationTake"),o=v.lang;if(this.longWaitingResCount=t===v.get("translationTakeAboutMinute")?this.longWaitingResCount+1:0,V.log("longWaitingResCount",this.longWaitingResCount),this.longWaitingResCount>Zc&&(t=new nt("TranslationDelayed")),t?.name==="VOTLocalizedError")this.transformBtn("error",t.localizedMessage);else if(t instanceof Error)this.transformBtn("error",t?.message);else if(this.data?.translateAPIErrors&&o!=="ru"&&!t?.includes(i)){const r=this.uiManager.votOverlayView;if(!r?.votButton)return;const s=Array.isArray(t)?t.join(" "):String(t),a=`${o}:${s}`,l=this.errorTranslationCache.get(a);if(l)this.transformBtn("error",l);else {r.votButton.loading=true;const c=await Xn(s,"ru",o),u=Array.isArray(c)?c.join(` -`):String(c);if(e?.aborted)return;if(this.errorTranslationCache.set(a,u),this.errorTranslationCache.size>50){const d=this.errorTranslationCache.keys().next().value;d&&this.errorTranslationCache.delete(d);}this.transformBtn("error",u);}if(e?.aborted)return}else {const r=Array.isArray(t)?t.join(` -`):String(t??"");this.transformBtn("error",r);}e?.aborted||["Подготавливаем перевод","Видео передано в обработку","Ожидаем перевод видео","Загружаем переведенное аудио"].includes(t)&&this.uiManager.votOverlayView?.votButton&&(this.uiManager.votOverlayView.votButton.loading=true);}afterUpdateTranslation(t){const e=this.uiManager.votOverlayView;if(!e?.votButton)return;const i=e.votButton.container.dataset.status==="success";e.videoVolumeSlider&&(e.videoVolumeSlider.hidden=!this.data?.showVideoSlider||!i),e.translationVolumeSlider&&(e.translationVolumeSlider.hidden=!i),e.videoVolumeSlider&&e.translationVolumeSlider?(this.volumeLinkState.lastVideoPercent=Number(e.videoVolumeSlider.value),this.volumeLinkState.lastTranslationPercent=Number(e.translationVolumeSlider.value),this.volumeLinkState.initialized=true):this.volumeLinkState.initialized=false,this.videoData&&!this.videoData.isStream&&(e.downloadTranslationButton&&(e.downloadTranslationButton.hidden=false),this.downloadTranslationUrl=t),V.log("afterUpdateTranslation downloadTranslationUrl",this.downloadTranslationUrl),this.data?.sendNotifyOnComplete&&this.hadAsyncWait&&i&&(this.notifier.translationCompleted(globalThis.location.hostname),this.hadAsyncWait=false);}validateAudioUrl(t,e){return this.callModuleAsync(ly,t,e)}scheduleTranslationRefresh(){this.callModule(cy);}refreshTranslationAudio=uy;proxifyAudio(t){return this.callModule(dy,t)}unproxifyAudio(t){return this.callModule(hy,t)}handleProxySettingsChanged=fy;isMultiMethodS3(t){return this.callModule(py,t)}updateTranslation=gy;translateFunc(t,e,i,o,r){return my.call(this,t,e,i,o,r)}isYouTubeHosts(){return this.callModule(vy)}setupAudioSettings(){return this.callModule(fa)}stopTranslation=async()=>{this.translationOrchestrator?.reset(),this.overlayVisibility?.cancel(),await this.stopTranslate(),this.syncVideoVolumeSlider();};handleSrcChanged(){return this.lifecycleController.handleSrcChanged()}async release(){this.initialized=false;try{await this.stopTranslation();}catch{}this.lifecycleController?.teardown(),this.abortController?.abort(),this.abortController=new AbortController,this.overlayVisibility?.release(),this.releaseExtraEvents(),this.hasSubtitlesWidget()&&(this.subtitlesWidget?.release(),this.subtitlesWidget=void 0),this.interactionChecker?.destroy(),this.uiManager.release();}collectReportInfo(){const t=Ys(),e=this.videoData?.detectedLanguage??"unknown",i=this.videoData?.responseLanguage??"unknown",o=`
+ `;function Fp(e,t,n){e[t].addListener(n)}function Ip(e,t,n){e[t].removeListener(n)}function X(e,t){e.hidden=t}function Z(e){return e.hidden}var Lp=class{button;loaderMain;loaderCircle;onClick=new H;events={click:this.onClick};_progress=0;constructor(){let e=this.createElements();this.button=e.button,this.loaderMain=e.loaderMain,this.loaderCircle=e.loaderCircle,this.progress=0}createElements(){let e=J.createIconButton(Tp,{ariaLabel:`Download translation`}),t=e.querySelector(`.vot-loader-main`);if(!t)throw Error(`[VOT] DownloadButton loader main element not found`);let n=e.querySelector(`.vot-loader-progress`);if(!n)throw Error(`[VOT] DownloadButton loader circle element not found`);return e.addEventListener(`click`,()=>{this.onClick.dispatch()}),{button:e,loaderMain:t,loaderCircle:n}}addEventListener(e,t){return Fp(this.events,`click`,t),this}removeEventListener(e,t){return Ip(this.events,`click`,t),this}get progress(){return this._progress}set progress(e){let t=Rp(e);this._progress=t;let n=this.getCircleCircumference();this.loaderCircle.style.strokeDasharray=`${n}`;let r=n*(1-t/100);this.loaderCircle.style.strokeDashoffset=`${r}`,this.loaderMain.style.opacity=t===0?`1`:`0`,this.loaderCircle.style.opacity=t===0?`0`:`1`}getCircleCircumference(){let e=this.loaderCircle.r?.baseVal?.value??0;return 2*Math.PI*e}set hidden(e){X(this.button,e)}get hidden(){return Z(this.button)}};function Rp(e){if(!Number.isFinite(e))return 0;let t=e<1?e*100:e;return Math.max(0,Math.min(100,Math.round(t)))}var zp=class{container;icon;text;_labelText;_icon;constructor({labelText:e,icon:t}){this._labelText=e,this._icon=t;let n=this.createElements();this.container=n.container,this.icon=n.icon,this.text=n.text}createElements(){let e=J.createEl(`vot-block`,[`vot-label`]),t=J.createEl(`span`,[`vot-label-text`]);t.textContent=this._labelText;let n=J.createEl(`span`,[`vot-label-icon`]);return this._icon?q(this._icon,n):n.hidden=!0,e.append(t,n),{container:e,icon:n,text:t}}set hidden(e){X(this.container,e)}get hidden(){return Z(this.container)}},Bp=class{container;backdrop;box;contentWrapper;headerContainer;titleContainer;title;closeButton;bodyContainer;footerContainer;onClose=new H;events={close:this.onClose};previouslyFocused=null;keydownListener;adaptiveAlignObserver;adaptiveAlignRaf=null;handleViewportChange=()=>{this.scheduleAdaptiveVerticalAlign()};titleId=typeof crypto<`u`&&`randomUUID`in crypto?crypto.randomUUID():`vot-dialog-title-${Math.random().toString(36).slice(2)}`;_titleHtml;_isTemp;constructor({titleHtml:e,isTemp:t=!1}){this._titleHtml=e,this._isTemp=t;let n=this.createElements();this.container=n.container,this.backdrop=n.backdrop,this.box=n.box,this.contentWrapper=n.contentWrapper,this.headerContainer=n.headerContainer,this.titleContainer=n.titleContainer,this.title=n.title,this.closeButton=n.closeButton,this.bodyContainer=n.bodyContainer,this.footerContainer=n.footerContainer}createElements(){let e=J.createEl(`vot-block`,[`vot-dialog-container`]);this._isTemp&&e.classList.add(`vot-dialog-temp`),e.hidden=!this._isTemp,e.setAttribute(`aria-hidden`,e.hidden?`true`:`false`),e.toggleAttribute(`inert`,e.hidden);let t=J.createEl(`vot-block`,[`vot-dialog-backdrop`]),n=J.createEl(`vot-block`,[`vot-dialog`]);n.dataset.verticalAlign=`center`,n.setAttribute(`role`,`dialog`),n.setAttribute(`aria-modal`,`true`),n.tabIndex=-1;let r=J.createEl(`vot-block`,[`vot-dialog-content-wrapper`]),i=J.createEl(`vot-block`,[`vot-dialog-header-container`]),a=J.createEl(`vot-block`,[`vot-dialog-title-container`]),o=J.createEl(`vot-block`,[`vot-dialog-title`]);o.id=this.titleId,o.append(this._titleHtml),a.appendChild(o),n.setAttribute(`aria-labelledby`,this.titleId);let s=J.createIconButton(Ap,{ariaLabel:`Close`});s.classList.add(`vot-dialog-close-button`),t.addEventListener(`click`,()=>{this.close()}),s.addEventListener(`click`,()=>{this.close()}),i.append(a,s);let c=J.createEl(`vot-block`,[`vot-dialog-body-container`]),l=J.createEl(`vot-block`,[`vot-dialog-footer-container`]);return r.append(i,c,l),n.appendChild(r),e.append(t,n),n.addEventListener(`click`,e=>{e.stopPropagation()}),{container:e,backdrop:t,box:n,contentWrapper:r,headerContainer:i,titleContainer:a,title:o,closeButton:s,bodyContainer:c,footerContainer:l}}addEventListener(e,t){return Fp(this.events,`close`,t),this}removeEventListener(e,t){return Ip(this.events,`close`,t),this}open(){return this.previouslyFocused??=document.activeElement,this.hidden=!1,this.attachKeydownTrap(),this.attachAdaptiveVerticalAlign(),queueMicrotask(()=>this.focusFirst()),this}remove(){return this.detachAdaptiveVerticalAlign(),this.detachKeydownTrap(),this.container.remove(),this.restoreFocus(),this.onClose.dispatch(),this}close(){return this._isTemp?this.remove():(this.detachAdaptiveVerticalAlign(),this.detachKeydownTrap(),this.hidden=!0,this.restoreFocus(),this.onClose.dispatch(),this)}attachAdaptiveVerticalAlign(){if(this.adaptiveAlignObserver){this.scheduleAdaptiveVerticalAlign();return}typeof ResizeObserver<`u`&&(this.adaptiveAlignObserver=new ResizeObserver(()=>{this.scheduleAdaptiveVerticalAlign()}),this.adaptiveAlignObserver.observe(this.contentWrapper)),globalThis.addEventListener(`resize`,this.handleViewportChange,{passive:!0}),globalThis.visualViewport&&(globalThis.visualViewport.addEventListener(`resize`,this.handleViewportChange,{passive:!0}),globalThis.visualViewport.addEventListener(`scroll`,this.handleViewportChange,{passive:!0})),this.scheduleAdaptiveVerticalAlign()}detachAdaptiveVerticalAlign(){this.adaptiveAlignObserver&&=(this.adaptiveAlignObserver.disconnect(),void 0),globalThis.removeEventListener(`resize`,this.handleViewportChange),globalThis.visualViewport?.removeEventListener(`resize`,this.handleViewportChange),globalThis.visualViewport?.removeEventListener(`scroll`,this.handleViewportChange),this.adaptiveAlignRaf!==null&&(cancelAnimationFrame(this.adaptiveAlignRaf),this.adaptiveAlignRaf=null)}scheduleAdaptiveVerticalAlign(){this.adaptiveAlignRaf!==null&&cancelAnimationFrame(this.adaptiveAlignRaf),this.adaptiveAlignRaf=requestAnimationFrame(()=>{this.adaptiveAlignRaf=null,this.updateAdaptiveVerticalAlign()})}updateAdaptiveVerticalAlign(){let e=globalThis.visualViewport?.height??globalThis.innerHeight;if(!e||e<=0)return;let t=Math.max(160,Math.round(e*.75)),n=Math.max(160,Math.round(e-32)),r=this.contentWrapper.scrollHeight,i=this.box.dataset.verticalAlign===`top`,a=t-8,o=Math.round(e*.6);(i?r>o:r>=a)?(this.box.dataset.verticalAlign=`top`,this.box.style.setProperty(`--vot-dialog-max-height`,`${n}px`)):(this.box.dataset.verticalAlign=`center`,this.box.style.setProperty(`--vot-dialog-max-height`,`${t}px`))}restoreFocus(){let e=this.previouslyFocused;this.previouslyFocused=null,e&&e instanceof HTMLElement&&document.contains(e)&&e.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(e=>!e.hidden&&e.getClientRects().length>0)}focusFirst(){(this.getFocusableElements()[0]??this.closeButton??this.box).focus?.()}attachKeydownTrap(){this.keydownListener||(this.keydownListener=e=>{if(e.key===`Escape`){e.preventDefault(),this.close();return}if(e.key!==`Tab`)return;let t=this.getFocusableElements();if(!t.length){e.preventDefault(),this.box.focus();return}let n=t[0],r=t.at(-1)??n,i=document.activeElement;e.shiftKey?(i===n||i===this.box)&&(e.preventDefault(),r.focus()):i===r&&(e.preventDefault(),n.focus())},this.container.addEventListener(`keydown`,this.keydownListener))}detachKeydownTrap(){this.keydownListener&&=(this.container.removeEventListener(`keydown`,this.keydownListener),void 0)}set hidden(e){X(this.container,e),this.container.setAttribute(`aria-hidden`,e?`true`:`false`),this.container.toggleAttribute(`inert`,e)}get hidden(){return Z(this.container)}get isDialogOpen(){return!this.container.hidden}},Vp=class{container;input;label;onInput=new H;onChange=new H;events={input:this.onInput,change:this.onChange};_labelHtml;_multiline;_placeholder;_value;constructor({labelHtml:e=``,placeholder:t=``,value:n=``,multiline:r=!1}){this._labelHtml=e,this._multiline=r,this._placeholder=t,this._value=n;let i=this.createElements();this.container=i.container,this.input=i.input,this.label=i.label}createElements(){let e=J.createEl(`vot-block`,[`vot-textfield`]),t=document.createElement(this._multiline?`textarea`:`input`);this._labelHtml||t.classList.add(`vot-show-placeholer`,`vot-show-placeholder`),t.placeholder=this._placeholder,t.value=this._value;let n=J.createEl(`span`);return n.append(this._labelHtml),e.append(t,n),t.addEventListener(`input`,()=>{this._value=this.input.value,this.onInput.dispatch(this._value)}),t.addEventListener(`change`,()=>{this._value=this.input.value,this.onChange.dispatch(this._value)}),{container:e,label:n,input:t}}addEventListener(e,t){return Fp(this.events,e,t),this}removeEventListener(e,t){return Ip(this.events,e,t),this}get value(){return this._value}set value(e){this._value!==e&&(this.input.value=this._value=e,this.onChange.dispatch(this._value))}get placeholder(){return this._placeholder}set placeholder(e){this.input.placeholder=this._placeholder=e}get disabled(){return this.input.disabled}set disabled(e){this.input.disabled=e}set hidden(e){X(this.container,e)}get hidden(){return Z(this.container)}},Q=class{container;outer;arrowIcon;title;dialogParent;labelElement;_selectTitle;_dialogTitle;multiSelect;baseItems;_items;searchItemsProvider;isLoading=!1;isDialogOpen=!1;searchRequestId=0;onSelectItem=new H;onBeforeOpen=new H;events={selectItem:this.onSelectItem,beforeOpen:this.onBeforeOpen};contentList;contentItemSearchDatasetKey=`votSearchLabel`;contentItemIndexDatasetKey=`votIndex`;selectedItems=[];selectedValues;constructor({selectTitle:e,dialogTitle:t,items:n,searchItemsProvider:r,labelElement:i,dialogParent:a=document.documentElement,multiSelect:o}){this._selectTitle=e,this._dialogTitle=t,this.baseItems=this.cloneItems(n),this._items=this.cloneItems(n),this.searchItemsProvider=r,this.multiSelect=o??!1,this.labelElement=i,this.dialogParent=a,this.selectedValues=this.calcSelectedValues();let s=this.createElements();this.container=s.container,this.outer=s.outer,this.arrowIcon=s.arrowIcon,this.title=s.title}cloneItems(e){return e.map(e=>({...e}))}static genLanguageItems(e,t){return e.map(e=>{let n=`langs.${e}`,r=V.get(n);return{label:r===n?e.toUpperCase():r,value:e,selected:t===e}})}multiSelectItemHandle=e=>{let t=e.value;this.selectedValues.has(t)&&this.selectedValues.size>1?this.selectedValues.delete(t):this.selectedValues.add(t),this.syncItemsSelectionState(),this.syncItemsSelectionState(this.baseItems),this.updateSelectedState(),this.onSelectItem.dispatch(Array.from(this.selectedValues))};singleSelectItemHandle=e=>{let t=e.value;this.selectedValues=new Set([t]),this.syncItemsSelectionState(),this.syncItemsSelectionState(this.baseItems),this.updateSelectedState(),this.onSelectItem.dispatch(t)};onContentItemClick=e=>{if(!(e.target instanceof HTMLElement))return;let t=e.target.closest(`.vot-select-content-item`);if(!t||t.inert||!this.contentList?.contains(t))return;let n=t.dataset[this.contentItemIndexDatasetKey];if(!n)return;let r=this._items[Number(n)];if(r){if(this.multiSelect){this.multiSelectItemHandle(r);return}this.singleSelectItemHandle(r)}};syncItemsSelectionState(e=this._items){for(let t of e)t.selected=this.selectedValues.has(t.value)}restoreBaseItems(){this._items=this.cloneItems(this.baseItems),this.syncItemsSelectionState(),this.updateSelectedState()}createDialogContentList(){let e=J.createEl(`vot-block`,[`vot-select-content-list`]);for(let[t,n]of this._items.entries()){let r=J.createEl(`vot-block`,[`vot-select-content-item`]);r.textContent=n.label,r.dataset.votSelected=n.selected===!0?`true`:`false`,r.dataset.votValue=n.value,r.dataset[this.contentItemSearchDatasetKey]=n.label.toLowerCase(),r.dataset[this.contentItemIndexDatasetKey]=String(t),n.disabled&&(r.inert=!0),e.appendChild(r)}return e.addEventListener(`click`,this.onContentItemClick),this.selectedItems=Array.from(e.children),e}createElements(){let e=J.createEl(`vot-block`,[`vot-select`]);this.labelElement?(e.classList.add(`vot-select--labeled`),e.append(this.labelElement)):e.classList.add(`vot-select--control-only`);let t=J.createEl(`vot-block`,[`vot-select-outer`]);J.makeButtonLike(t),t.setAttribute(`aria-haspopup`,`dialog`),t.setAttribute(`aria-expanded`,`false`);let n=J.createEl(`vot-block`,[`vot-select-title`]);n.textContent=this.visibleText;let r=J.createEl(`vot-block`,[`vot-select-arrow-icon`]);return q(Op,r),t.append(n,r),t.addEventListener(`click`,()=>{if(!this.disabled&&!(this.isLoading||this.isDialogOpen))try{this.isLoading=!0;let e=new Bp({titleHtml:this._dialogTitle,isTemp:!0});this.onBeforeOpen.dispatch(e),this.dialogParent.appendChild(e.container),this.isDialogOpen=!0,t.setAttribute(`aria-expanded`,`true`);let n=new Vp({labelHtml:V.get(`searchField`)});n.addEventListener(`input`,async e=>{let t=++this.searchRequestId;if(this.searchItemsProvider){let n=await this.searchItemsProvider(e);if(t!==this.searchRequestId)return;this.updateItems(n,{persist:!1})}let n=e.toLowerCase();for(let e of this.selectedItems)e.hidden=!(e.dataset[this.contentItemSearchDatasetKey]??``).includes(n)}),this.contentList=this.createDialogContentList(),e.bodyContainer.append(n.container,this.contentList),e.addEventListener(`close`,()=>{this.isDialogOpen=!1,this.restoreBaseItems(),this.selectedItems=[],this.contentList=void 0,t.setAttribute(`aria-expanded`,`false`)}),e.open()}finally{this.isLoading=!1}}),e.appendChild(t),{container:e,outer:t,arrowIcon:r,title:n}}calcSelectedValues(){return new Set(this._items.filter(e=>e.selected).map(e=>e.value))}addEventListener(e,t){return Fp(this.events,e,t),this}removeEventListener(e,t){return Ip(this.events,e,t),this}updateTitle(){return this.title.textContent=this.visibleText,this}updateSelectedState(){if(this.selectedItems.length>0)for(let e of this.selectedItems){let t=e.dataset.votValue;t!==void 0&&(e.dataset.votSelected=this.selectedValues.has(t).toString())}return this.updateTitle(),this}setSelectedValue(e){let t=Array.isArray(e)?e:[e],n;return n=this.multiSelect?t:t.length>0?[t[0]]:[],this.selectedValues=new Set(n),this.syncItemsSelectionState(),this.syncItemsSelectionState(this.baseItems),this.updateSelectedState(),this}updateItems(e,t={}){let{persist:n=!0}=t,r=this.cloneItems(e);n&&(this.baseItems=this.cloneItems(r)),this._items=r,this.selectedValues=this.calcSelectedValues(),this.updateSelectedState();let i=this.contentList?.parentElement;if(!this.contentList||!i)return this;let a=this.contentList;return this.contentList=this.createDialogContentList(),i.replaceChild(this.contentList,a),this}get visibleText(){return this.multiSelect?this._items.filter(e=>this.selectedValues.has(e.value)).map(e=>e.label).join(`, `)||this._selectTitle:this._items.find(e=>e.selected)?.label??this._selectTitle}set selectTitle(e){this._selectTitle=e,this.updateTitle()}set hidden(e){X(this.container,e)}get hidden(){return Z(this.container)}get disabled(){return this.outer.getAttribute(`disabled`)===`true`||this.outer.getAttribute(`aria-disabled`)===`true`}set disabled(e){if(e){this.outer.setAttribute(`disabled`,`true`);return}this.outer.removeAttribute(`disabled`)}},Hp=class{container;fromSelect;directionIcon;toSelect;dialogParent;_fromSelectTitle;_fromDialogTitle;_fromItems;_toSelectTitle;_toDialogTitle;_toItems;constructor({from:{selectTitle:e=V.get(`videoLanguage`),dialogTitle:t=V.get(`videoLanguage`),items:n},to:{selectTitle:r=V.get(`translationLanguage`),dialogTitle:i=V.get(`translationLanguage`),items:a},dialogParent:o=document.documentElement}){this._fromSelectTitle=e,this._fromDialogTitle=t,this._fromItems=n,this._toSelectTitle=r,this._toDialogTitle=i,this._toItems=a,this.dialogParent=o;let s=this.createElements();this.container=s.container,this.fromSelect=s.fromSelect,this.directionIcon=s.directionIcon,this.toSelect=s.toSelect}createElements(){let e=J.createEl(`vot-block`,[`vot-lang-select`]),t=new Q({selectTitle:this._fromSelectTitle,dialogTitle:this._fromDialogTitle,items:this._fromItems,dialogParent:this.dialogParent}),n=J.createEl(`vot-block`,[`vot-lang-select-icon`]);q(kp,n);let r=new Q({selectTitle:this._toSelectTitle,dialogTitle:this._toDialogTitle,items:this._toItems,dialogParent:this.dialogParent});return e.append(t.container,n,r.container),{container:e,fromSelect:t,directionIcon:n,toSelect:r}}setSelectedValues(e,t){return this.fromSelect.setSelectedValue(e),this.toSelect.setSelectedValue(t),this}updateItems(e,t){return this._fromItems=e,this._toItems=t,this.fromSelect=this.fromSelect.updateItems(e),this.toSelect=this.toSelect.updateItems(t),this}},Up=class{container;input;label;onInput=new H;_labelHtml;_value;_min;_max;_step;constructor({labelHtml:e,value:t=50,min:n=0,max:r=100,step:i=1}){this._labelHtml=e,this._value=t,this._min=n,this._max=r,this._step=i;let a=this.createElements();this.container=a.container,this.input=a.input,this.label=a.label,this.update()}updateProgress(){let e=this._max-this._min,t=e<=0?0:(this._value-this._min)/e,n=Math.max(0,Math.min(1,t));return this.container.style.setProperty(`--vot-progress`,n.toString()),this}update(){return this._value=this.input.valueAsNumber,this._min=+this.input.min,this._max=+this.input.max,this.updateProgress(),this}createElements(){let e=J.createEl(`vot-block`,[`vot-slider`]),t=document.createElement(`input`);t.type=`range`,t.min=this._min.toString(),t.max=this._max.toString(),t.step=this._step.toString(),t.value=this._value.toString();let n=J.createEl(`span`);return q(this._labelHtml,n),e.append(t,n),t.addEventListener(`input`,()=>{this.update(),this.onInput.dispatch(this._value,!1)}),{container:e,label:n,input:t}}addEventListener(e,t){return this.onInput.addListener(t),this}removeEventListener(e,t){return this.onInput.removeListener(t),this}get value(){return this._value}set value(e){this._value=Wp(e,this._min,this._max),this.input.value=this._value.toString(),this.updateProgress(),this.onInput.dispatch(this._value,!0)}get min(){return this._min}set min(e){this._min=e,this.input.min=this._min.toString(),this._value=Wp(this._value,this._min,this._max),this.input.value=this._value.toString(),this.updateProgress()}get max(){return this._max}set max(e){this._max=e,this.input.max=this._max.toString(),this._value=Wp(this._value,this._min,this._max),this.input.value=this._value.toString(),this.updateProgress()}get step(){return this._step}set step(e){this._step=e,this.input.step=this._step.toString()}get disabled(){return this.input.disabled}set disabled(e){this.input.disabled=e}set hidden(e){X(this.container,e)}get hidden(){return Z(this.container)}};function Wp(e,t,n){return!Number.isFinite(e)||n=66?`right`:`default`:`default`}static calcDirection(e){return[`default`,`top`].includes(e)?`row`:`column`}createElements(){let e=J.createEl(`vot-block`,[`vot-segmented-button`]);e.dataset.position=this._position,e.dataset.direction=this._direction,e.dataset.status=this._status;let t=J.createEl(`vot-block`,[`vot-segment`,`vot-translate-button`]);t.setAttribute(`role`,`button`),t.tabIndex=0,t.setAttribute(`aria-label`,this._labelText||`Translate`),q(Sp,t);let n=J.createEl(`span`,[`vot-segment-label`]);n.textContent=this._labelText,t.appendChild(n);let r=J.createEl(`vot-block`,[`vot-separator`]),i=J.createEl(`vot-block`,[`vot-segment-only-icon`]);i.setAttribute(`role`,`button`),i.tabIndex=0,i.setAttribute(`aria-label`,`Picture in picture`),q(Cp,i);let a=J.createEl(`vot-block`,[`vot-separator`]),o=J.createEl(`vot-block`,[`vot-segment-only-icon`]);return o.setAttribute(`role`,`button`),o.tabIndex=0,o.setAttribute(`aria-label`,`Menu`),o.setAttribute(`aria-haspopup`,`dialog`),o.setAttribute(`aria-expanded`,`false`),q(wp,o),e.append(t,r,i,a,o),{container:e,translateButton:t,separator:r,pipButton:i,separator2:a,menuButton:o,label:n}}showPiPButton(e){return this.separator2.hidden=this.pipButton.hidden=!e,this}setText(e){return this._labelText=e,this.label.textContent=e,this.translateButton.setAttribute(`aria-label`,e||`Translate`),this}remove(){return this.container.remove(),this}get tooltipPos(){switch(this.position){case`left`:return`right`;case`right`:return`left`;default:return`bottom`}}set status(e){this._status=this.container.dataset.status=e}get status(){return this._status}set loading(e){this.container.dataset.loading=e.toString()}get loading(){return this.container.dataset.loading===`true`}set hidden(e){X(this.container,e)}get hidden(){return Z(this.container)}get position(){return this._position}set position(e){this._position=this.container.dataset.position=e}get direction(){return this._direction}set direction(e){this._direction=this.container.dataset.direction=e}set opacity(e){let t=Number.isFinite(e)?e:1;this._opacity=t;let n=t<=.01;this.container.classList.toggle(`vot-segmented-button--hidden`,n)}get opacity(){return this._opacity}},qp=class{container;contentWrapper;headerContainer;bodyContainer;footerContainer;titleContainer;title;_position;_titleHtml;menuId=typeof crypto<`u`&&`randomUUID`in crypto?`vot-menu-${crypto.randomUUID()}`:`vot-menu-${Math.random().toString(36).slice(2)}`;titleId=typeof crypto<`u`&&`randomUUID`in crypto?`vot-menu-title-${crypto.randomUUID()}`:`vot-menu-title-${Math.random().toString(36).slice(2)}`;constructor({position:e=`default`,titleHtml:t=``}){this._position=e,this._titleHtml=t;let n=this.createElements();this.container=n.container,this.contentWrapper=n.contentWrapper,this.headerContainer=n.headerContainer,this.bodyContainer=n.bodyContainer,this.footerContainer=n.footerContainer,this.titleContainer=n.titleContainer,this.title=n.title}createElements(){let e=J.createEl(`vot-block`,[`vot-menu`]);e.hidden=!0,e.id=this.menuId,e.dataset.position=this._position,e.setAttribute(`role`,`dialog`),e.setAttribute(`aria-modal`,`false`),e.setAttribute(`aria-hidden`,`true`),e.toggleAttribute(`inert`,!0);let t=J.createEl(`vot-block`,[`vot-menu-content-wrapper`]);e.appendChild(t);let n=J.createEl(`vot-block`,[`vot-menu-header-container`]),r=J.createEl(`vot-block`,[`vot-menu-title-container`]);n.appendChild(r);let i=J.createEl(`vot-block`,[`vot-menu-title`]);i.id=this.titleId,i.append(this._titleHtml),r.appendChild(i),e.setAttribute(`aria-labelledby`,this.titleId);let a=J.createEl(`vot-block`,[`vot-menu-body-container`]),o=J.createEl(`vot-block`,[`vot-menu-footer-container`]);return t.append(n,a,o),{container:e,contentWrapper:t,headerContainer:n,bodyContainer:a,footerContainer:o,titleContainer:r,title:i}}setText(e){return this._titleHtml=this.title.textContent=e,this}remove(){return this.container.remove(),this}set hidden(e){X(this.container,e),this.container.setAttribute(`aria-hidden`,e?`true`:`false`),this.container.toggleAttribute(`inert`,e)}get hidden(){return Z(this.container)}get position(){return this._position}set position(e){this._position=this.container.dataset.position=e}},Jp=class e{static BIG_CONTAINER_WIDTH_PX=550;mount;globalPortal;abortController=null;defaultVolumePersistTimer;defaultVolumePersistDelayMs=250;dragging=!1;dragCandidate=!1;dragDirty=!1;dragStartX=0;dragStartY=0;currentClientX=0;activePointerId=null;dragThresholdPx=6;containerRect=null;dragIsBigContainer=null;checkerUnsubscribe=null;initialized=!1;data;videoHandler;intervalIdleChecker;events={"click:settings":new H,"click:pip":new H,"click:downloadTranslation":new H,"click:downloadSubtitles":new H,"click:translate":new H,"input:videoVolume":new H,"input:translationVolume":new H,"select:fromLanguage":new H,"select:toLanguage":new H,"select:subtitles":new H};votButton;votButtonTooltip;votMenu;downloadTranslationButton;downloadSubtitlesButton;openSettingsButton;languagePairSelect;subtitlesSelectLabel;subtitlesSelect;videoVolumeSliderLabel;videoVolumeSlider;translationVolumeSliderLabel;translationVolumeSlider;constructor({mount:e,globalPortal:t,data:n={},videoHandler:r,intervalIdleChecker:i}){this.mount=e,this.globalPortal=t,this.data=n,this.videoHandler=r,this.intervalIdleChecker=i}get root(){return this.mount.root}get portalContainer(){return this.mount.portalContainer}get tooltipLayoutRoot(){return this.mount.tooltipLayoutRoot}updateMount(e){let t=this.mount.root,n=e.root,r=this.mount.tooltipLayoutRoot,i=e.tooltipLayoutRoot;return this.mount=e,this.isInitialized()?(t!==n&&(this.votButton&&n.appendChild(this.votButton.container),this.votMenu&&n.appendChild(this.votMenu.container)),this.votButtonTooltip&&_p({root:t,portalContainer:this.mount.portalContainer,subtitlesMountContainer:this.mount.subtitlesMountContainer,tooltipLayoutRoot:r},e)&&this.votButtonTooltip.updateMount({layoutRoot:i??document.documentElement}),this):this}isInitialized(){return this.initialized}calcButtonLayout(e){return this.isBigContainer&&Yp(e)?{direction:`column`,position:e}:{direction:`row`,position:`default`}}addEventListener(e,t){return this.events[e].addListener(t),this}removeEventListener(e,t){return this.events[e].removeListener(t),this}scheduleDefaultVolumePersist(){this.defaultVolumePersistTimer!==void 0&&globalThis.clearTimeout(this.defaultVolumePersistTimer),this.defaultVolumePersistTimer=globalThis.setTimeout(()=>{this.defaultVolumePersistTimer=void 0,this.flushDefaultVolumePersist()},this.defaultVolumePersistDelayMs)}flushDefaultVolumePersist(){this.defaultVolumePersistTimer!==void 0&&(globalThis.clearTimeout(this.defaultVolumePersistTimer),this.defaultVolumePersistTimer=void 0),typeof this.data.defaultVolume==`number`&&B.set(`defaultVolume`,this.data.defaultVolume)}initUI(e=`default`){if(this.isInitialized())throw Error(`[VOT] OverlayView is already initialized`);this.initialized=!0;let{position:t,direction:n}=this.calcButtonLayout(e);this.votButton=new Kp({position:t,direction:n,status:`none`,labelHtml:V.get(`translateVideo`)}),this.votButton.opacity=0,this.pipButtonVisible||this.votButton.showPiPButton(!1),this.root.appendChild(this.votButton.container),this.votButtonTooltip=new Y({target:this.votButton.translateButton,content:V.get(`translateVideo`),position:this.votButton.tooltipPos,autoLayout:!1,hidden:n===`row`,bordered:!1,parentElement:this.globalPortal,layoutRoot:this.tooltipLayoutRoot}),this.votMenu=new qp({titleHtml:V.get(`VOTSettings`),position:t}),this.root.appendChild(this.votMenu.container),this.votButton.menuButton.setAttribute(`aria-controls`,this.votMenu.container.id),this.downloadTranslationButton=new Lp,this.downloadTranslationButton.hidden=!0,this.downloadSubtitlesButton=J.createIconButton(Ep,{ariaLabel:`Download subtitles`}),this.downloadSubtitlesButton.hidden=!0,this.openSettingsButton=J.createIconButton(Dp,{ariaLabel:V.get(`VOTSettings`)}),this.votMenu.headerContainer.append(this.downloadTranslationButton.button,this.downloadSubtitlesButton,this.openSettingsButton);let r=this.videoHandler?.videoData?.detectedLanguage??`en`,i=this.data.responseLanguage??`ru`;this.languagePairSelect=new Hp({from:{selectTitle:V.get(`langs.${r}`),items:Q.genLanguageItems(bn,r)},to:{selectTitle:V.get(`langs.${i}`),items:Q.genLanguageItems(xn,i)},dialogParent:this.globalPortal}),this.subtitlesSelectLabel=new zp({labelText:V.get(`VOTSubtitles`)}),this.subtitlesSelect=new Q({selectTitle:V.get(`VOTSubtitlesDisabled`),dialogTitle:V.get(`VOTSubtitles`),labelElement:this.subtitlesSelectLabel.container,dialogParent:this.globalPortal,items:[{label:V.get(`VOTSubtitlesDisabled`),value:`disabled`,selected:!0}]});let a=this.videoHandler?this.videoHandler.getVideoVolume()*100:100;this.videoVolumeSliderLabel=new Gp({labelText:V.get(`VOTVolume`),value:a}),this.videoVolumeSlider=new Up({labelHtml:this.videoVolumeSliderLabel.container,value:a}),this.videoVolumeSlider.hidden=!this.data.showVideoSlider||this.votButton.status!==`success`;let o=this.data.defaultVolume??100;return this.translationVolumeSliderLabel=new Gp({labelText:V.get(`VOTVolumeTranslation`),value:o}),this.translationVolumeSlider=new Up({labelHtml:this.translationVolumeSliderLabel.container,value:o,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),this}initUIEvents(){if(!this.isInitialized())throw Error(`[VOT] OverlayView isn't initialized`);this.abortController=new AbortController;let e=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:e});let t=e=>t=>{(t.key===`Enter`||t.key===` `)&&(t.preventDefault(),e())},n=e=>e.isPrimary&&e.button===0,r=(e,{returnFocusToToggle:t=!1}={})=>{this.isInitialized()&&(this.votMenu.hidden=!e,this.votButton.menuButton.setAttribute(`aria-expanded`,e.toString()),this.votButtonTooltip&&(this.votButtonTooltip.hidden=e||this.votButton.direction===`row`),e?queueMicrotask(()=>this.openSettingsButton?.focus?.()):t?queueMicrotask(()=>this.votButton.menuButton.focus?.()):this.votButton.menuButton.blur())},i=()=>r(this.votMenu.hidden),a=(e=!1)=>r(!1,{returnFocusToToggle:e});this.votButton.translateButton.addEventListener(`pointerdown`,e=>{n(e)&&(a(),this.events[`click:translate`].dispatch())},{signal:e}),this.votButton.translateButton.addEventListener(`keydown`,t(()=>{a(),this.events[`click:translate`].dispatch()}),{signal:e}),this.votButton.pipButton.addEventListener(`pointerdown`,e=>{n(e)&&(a(),this.events[`click:pip`].dispatch())},{signal:e}),this.votButton.pipButton.addEventListener(`keydown`,t(()=>{a(),this.events[`click:pip`].dispatch()}),{signal:e}),this.votButton.menuButton.addEventListener(`pointerdown`,e=>{n(e)&&(e.preventDefault(),i())},{signal:e}),this.votButton.menuButton.addEventListener(`keydown`,t(i),{signal:e});let o=`none`;this.votButton.container.style.touchAction=o,this.votButton.translateButton.style.touchAction=o,this.votButton.pipButton.style.touchAction=o,this.votButton.menuButton.style.touchAction=o,this.votButton.container.addEventListener(`pointerdown`,this.onDragStart,{signal:e}),this.votButton.container.addEventListener(`pointermove`,this.onPointerMove,{signal:e}),this.votButton.container.addEventListener(`pointerup`,this.onDragEnd,{signal:e}),this.votButton.container.addEventListener(`pointercancel`,this.onDragEnd,{signal:e}),this.votButton.container.addEventListener(`lostpointercapture`,this.onDragEnd,{signal:e}),this.votMenu.container.addEventListener(`click`,e=>{e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation()},{signal:e});for(let t of[`pointerdown`,`mousedown`])this.votMenu.container.addEventListener(t,e=>{e.stopImmediatePropagation()},{signal:e});return document.addEventListener(`pointerdown`,e=>{if(this.votMenu.hidden)return;let t=e.target,n=typeof e.composedPath==`function`?e.composedPath():[],r=t&&this.votMenu.container.contains(t)||n.includes(this.votMenu.container),i=t&&this.votButton.menuButton.contains(t)||n.includes(this.votButton.menuButton),o=t&&this.votButton.container.contains(t)||n.includes(this.votButton.container),s=t instanceof HTMLElement&&!!t.closest(`.vot-dialog-container`);r||i||o||s||a(!1)},{signal:e,capture:!0,passive:!0}),this.votMenu.container.addEventListener(`keydown`,e=>{if(e.key!==`Escape`)return;let t=document.documentElement.classList.contains(`vot-keyboard-nav`);e.preventDefault(),e.stopPropagation(),a(t),this.votButton.container.matches(`:hover`)||this.votMenu.container.matches(`:hover`)||this.videoHandler?.overlayVisibility?.queueAutoHide?.()},{signal:e}),this.downloadTranslationButton.addEventListener(`click`,()=>{this.events[`click:downloadTranslation`].dispatch()}),this.downloadSubtitlesButton.addEventListener(`click`,()=>{this.events[`click:downloadSubtitles`].dispatch()},{signal:e}),this.openSettingsButton.addEventListener(`click`,()=>{a(),this.events[`click:settings`].dispatch()},{signal:e}),this.languagePairSelect.fromSelect.addEventListener(`selectItem`,e=>{this.videoHandler?.videoData&&(this.videoHandler.videoData.detectedLanguage=e,this.videoHandler.videoManager.rememberUserLanguageSelection(this.videoHandler.videoData.videoId,e)),this.events[`select:fromLanguage`].dispatch(e)}),this.languagePairSelect.toSelect.addEventListener(`selectItem`,async e=>{this.videoHandler?.videoData&&(this.videoHandler.translateToLang=this.videoHandler.videoData.responseLanguage=e);let t=this.data.responseLanguage;t!==e&&(this.data.responseLanguage=e,await B.set(`responseLanguage`,this.data.responseLanguage)),this.data.enabledDontTranslateLanguages&&Array.isArray(this.data.dontTranslateLanguages)&&this.data.dontTranslateLanguages.length===1&&t!==e&&typeof t==`string`&&this.data.dontTranslateLanguages[0]===t&&(this.data.dontTranslateLanguages=[e],await B.set(`dontTranslateLanguages`,this.data.dontTranslateLanguages)),this.events[`select:toLanguage`].dispatch(e)}),this.subtitlesSelect.addEventListener(`beforeOpen`,async e=>{if(!this.videoHandler?.videoData)return;let t=this.videoHandler.getPreferredSubtitlesLanguage(this.videoHandler.videoData.detectedLanguage,this.videoHandler.videoData.responseLanguage);if(!t)return;let n=this.videoHandler.getSubtitlesCacheKey(this.videoHandler.videoData.videoId,this.videoHandler.videoData.detectedLanguage,t);if(this.videoHandler.subtitlesCacheKey===n)return;if(this.videoHandler.cacheManager.getSubtitles(n)!==void 0){await this.videoHandler.ensureSubtitlesForCurrentLangPair();return}let r=this.votButton?.loading??!1;this.votButton&&(this.votButton.loading=!0);let i=J.createInlineLoader();i.style.margin=`0 auto`,e.footerContainer.appendChild(i);try{await this.videoHandler.ensureSubtitlesForCurrentLangPair()}finally{i.remove(),this.votButton&&(this.votButton.loading=r)}}),this.subtitlesSelect.addEventListener(`selectItem`,e=>{this.events[`select:subtitles`].dispatch(e)}),this.videoVolumeSlider.addEventListener(`input`,(e,t)=>{this.videoVolumeSliderLabel&&(this.videoVolumeSliderLabel.value=e),!t&&this.events[`input:videoVolume`].dispatch(e)}),this.translationVolumeSlider.addEventListener(`input`,(e,t)=>{this.translationVolumeSliderLabel&&(this.translationVolumeSliderLabel.value=e),this.data.defaultVolume!==e&&(this.data.defaultVolume=e,this.scheduleDefaultVolumePersist()),!t&&this.events[`input:translationVolume`].dispatch(e)}),this}updateButtonLayout(e,t){return this.isInitialized()?(this.votMenu.position=e,this.votButton.position=e,this.votButton.direction=t,this.votButtonTooltip.hidden=t===`row`,this.votButtonTooltip.setPosition(this.votButton.tooltipPos),this):this}moveButton(e){if(!this.isInitialized())return this;let t=this.dragIsBigContainer??this.isBigContainer,n=Kp.calcPosition(e,t);if(n===this.votButton.position)return this;let r=Kp.calcDirection(n);return this.data.buttonPos=n,this.updateButtonLayout(n,r),this}startDragSession(e,t,n){this.dragCandidate=!0,this.dragging=!1,this.dragStartX=e,this.dragStartY=t,this.currentClientX=e,this.containerRect=this.root.getBoundingClientRect(),this.dragIsBigContainer=this.isBigContainer,this.dragDirty=!1,this.intervalIdleChecker.markActivity(n),this.intervalIdleChecker.requestImmediateTick()}queueDragTick(e){this.dragDirty||(this.dragDirty=!0,this.intervalIdleChecker.markActivity(e),this.intervalIdleChecker.requestImmediateTick())}updateDragFromMove(e,t,n){this.currentClientX=e,this.dragCandidate&&(this.dragging||Math.abs(this.currentClientX-this.dragStartX)+Math.abs(t-this.dragStartY)>=this.dragThresholdPx&&(this.dragging=!0),this.dragging&&this.queueDragTick(n))}onDragStart=e=>{!e.isPrimary||e.button!==0||(e.preventDefault(),this.activePointerId=e.pointerId,this.startDragSession(e.clientX,e.clientY,`overlay-pointer-down`))};onPointerMove=e=>{if(this.activePointerId!==e.pointerId)return;let t=this.dragging;if(this.updateDragFromMove(e.clientX,e.clientY,`overlay-pointer-move`),!t&&this.dragging)try{this.votButton?.container.setPointerCapture(e.pointerId)}catch{}this.dragging&&e.preventDefault()};applyDragFromState=()=>{if(!this.dragging||!this.dragDirty||!this.containerRect)return;let e=this.containerRect.width;if(!(e>0&&Number.isFinite(e)))return;this.dragDirty=!1;let t=this.currentClientX-this.containerRect.left,n=Math.max(0,Math.min(t,e))/e*100;this.moveButton(n)};onCheckerTick=()=>{this.applyDragFromState()};onDragEnd=e=>{if(e&&this.activePointerId!==null&&e.pointerId!==this.activePointerId)return;let t=this.activePointerId;if(t!==null)try{this.votButton?.container.hasPointerCapture(t)&&this.votButton.container.releasePointerCapture(t)}catch{}this.applyDragFromState();let n=this.dragIsBigContainer??this.isBigContainer;this.dragging&&n&&this.data.buttonPos&&B.set(`buttonPos`,this.data.buttonPos),this.dragging=!1,this.dragCandidate=!1,this.dragDirty=!1,this.containerRect=null,this.dragIsBigContainer=null,this.activePointerId=null};updateButtonOpacity(e){return!this.isInitialized()||!this.votMenu.hidden||Math.abs(this.votButton.opacity-e)>.01&&(this.votButton.opacity=e),this}doReleaseUI(){this.votButton?.remove(),this.votMenu?.remove(),this.votButtonTooltip?.release()}doReleaseUIEvents(){this.abortController?.abort(),this.abortController=null,this.checkerUnsubscribe?.(),this.checkerUnsubscribe=null,this.onDragEnd(),this.flushDefaultVolumePersist();for(let e of Object.values(this.events))e.clear()}release(){return this.isInitialized()?(this.doReleaseUIEvents(),this.doReleaseUI(),this.initialized=!1,this):this}get isBigContainer(){let t=this.videoHandler?.video?.getBoundingClientRect?.().width;if(typeof t==`number`&&Number.isFinite(t))return t>e.BIG_CONTAINER_WIDTH_PX;let n=this.videoHandler?.container?.getBoundingClientRect?.().width;return typeof n==`number`&&Number.isFinite(n)?n>e.BIG_CONTAINER_WIDTH_PX:this.root.clientWidth>e.BIG_CONTAINER_WIDTH_PX}get pipButtonVisible(){return Gi()&&!!this.data.showPiPButton}};function Yp(e){return e===`left`||e===`right`}var Xp=[`default`,`top`,`left`,`right`],Zp=d(c(((e,t)=>{(function(n,r){typeof e==`object`&&typeof t==`object`?t.exports=r():typeof define==`function`&&define.amd?define([],r):typeof e==`object`?e.bowser=r():n.bowser=r()})(e,(function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){typeof Symbol<`u`&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:`Module`}),Object.defineProperty(e,`__esModule`,{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t||4&t&&typeof e==`object`&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,`default`,{enumerable:!0,value:e}),2&t&&typeof e!=`string`)for(var i in e)n.d(r,i,function(t){return e[t]}.bind(null,i));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,`a`,t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p=``,n(n.s=90)}({17:function(e,t,n){"use strict";t.__esModule=!0,t.default=void 0;var r=n(18);t.default=function(){function e(){}return e.getFirstMatch=function(e,t){var n=t.match(e);return n&&n.length>0&&n[1]||``},e.getSecondMatch=function(e,t){var n=t.match(e);return n&&n.length>1&&n[2]||``},e.matchAndReturnConst=function(e,t,n){if(e.test(t))return n},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 n=t[0],r=t[1];if(n===10)switch(r){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(n){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),!(t[0]===1&&t[1]<5))return t[0]===1&&t[1]<6?`Cupcake`:t[0]===1&&t[1]>=6?`Donut`:t[0]===2&&t[1]<2?`Eclair`:t[0]===2&&t[1]===2?`Froyo`:t[0]===2&&t[1]>2?`Gingerbread`:t[0]===3?`Honeycomb`:t[0]===4&&t[1]<1?`Ice Cream Sandwich`:t[0]===4&&t[1]<4?`Jelly Bean`:t[0]===4&&t[1]>=4?`KitKat`:t[0]===5?`Lollipop`:t[0]===6?`Marshmallow`:t[0]===7?`Nougat`:t[0]===8?`Oreo`:t[0]===9?`Pie`:void 0},e.getVersionPrecision=function(e){return e.split(`.`).length},e.compareVersions=function(t,n,r){r===void 0&&(r=!1);var i=e.getVersionPrecision(t),a=e.getVersionPrecision(n),o=Math.max(i,a),s=0,c=e.map([t,n],(function(t){var n=o-e.getVersionPrecision(t),r=t+Array(n+1).join(`.0`);return e.map(r.split(`.`),(function(e){return Array(20-e.length).join(`0`)+e})).reverse()}));for(r&&(s=o-Math.min(i,a)),--o;o>=s;){if(c[0][o]>c[1][o])return 1;if(c[0][o]===c[1][o]){if(o===s)return 0;--o}else if(c[0][o]1?i-1:0),o=1;o0){var o=Object.keys(n),c=s.default.find(o,(function(e){return t.isOS(e)}));if(c){var l=this.satisfies(n[c]);if(l!==void 0)return l}var u=s.default.find(o,(function(e){return t.isPlatform(e)}));if(u){var d=this.satisfies(n[u]);if(d!==void 0)return d}}if(a>0){var f=Object.keys(i),p=s.default.find(f,(function(e){return t.isBrowser(e,!0)}));if(p!==void 0)return this.compareVersion(i[p])}},t.isBrowser=function(e,t){t===void 0&&(t=!1);var n=this.getBrowserName().toLowerCase(),r=e.toLowerCase(),i=s.default.getBrowserTypeByAlias(r);return t&&i&&(r=i.toLowerCase()),r===n},t.compareVersion=function(e){var t=[0],n=e,r=!1,i=this.getBrowserVersion();if(typeof i==`string`)return e[0]===`>`||e[0]===`<`?(n=e.substr(1),e[1]===`=`?(r=!0,n=e.substr(2)):t=[],e[0]===`>`?t.push(1):t.push(-1)):e[0]===`=`?n=e.substr(1):e[0]===`~`&&(r=!0,n=e.substr(1)),t.indexOf(s.default.compareVersions(i,n,r))>-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 t===void 0&&(t=!1),this.isBrowser(e,t)||this.isOS(e)||this.isPlatform(e)},t.some=function(e){var t=this;return e===void 0&&(e=[]),e.some((function(e){return t.is(e)}))},e}(),e.exports=t.default},92:function(e,t,n){"use strict";t.__esModule=!0,t.default=void 0;var r,i=(r=n(17))&&r.__esModule?r:{default:r},a=/version\/(\d+(\.?_?\d+)+)/i;t.default=[{test:[/gptbot/i],describe:function(e){var t={name:`GPTBot`},n=i.default.getFirstMatch(/gptbot\/(\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/chatgpt-user/i],describe:function(e){var t={name:`ChatGPT-User`},n=i.default.getFirstMatch(/chatgpt-user\/(\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/oai-searchbot/i],describe:function(e){var t={name:`OAI-SearchBot`},n=i.default.getFirstMatch(/oai-searchbot\/(\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/claudebot/i,/claude-web/i,/claude-user/i,/claude-searchbot/i],describe:function(e){var t={name:`ClaudeBot`},n=i.default.getFirstMatch(/(?:claudebot|claude-web|claude-user|claude-searchbot)\/(\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/omgilibot/i,/webzio-extended/i],describe:function(e){var t={name:`Omgilibot`},n=i.default.getFirstMatch(/(?:omgilibot|webzio-extended)\/(\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/diffbot/i],describe:function(e){var t={name:`Diffbot`},n=i.default.getFirstMatch(/diffbot\/(\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/perplexitybot/i],describe:function(e){var t={name:`PerplexityBot`},n=i.default.getFirstMatch(/perplexitybot\/(\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/perplexity-user/i],describe:function(e){var t={name:`Perplexity-User`},n=i.default.getFirstMatch(/perplexity-user\/(\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/youbot/i],describe:function(e){var t={name:`YouBot`},n=i.default.getFirstMatch(/youbot\/(\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/meta-webindexer/i],describe:function(e){var t={name:`Meta-WebIndexer`},n=i.default.getFirstMatch(/meta-webindexer\/(\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/meta-externalads/i],describe:function(e){var t={name:`Meta-ExternalAds`},n=i.default.getFirstMatch(/meta-externalads\/(\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/meta-externalagent/i],describe:function(e){var t={name:`Meta-ExternalAgent`},n=i.default.getFirstMatch(/meta-externalagent\/(\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/meta-externalfetcher/i],describe:function(e){var t={name:`Meta-ExternalFetcher`},n=i.default.getFirstMatch(/meta-externalfetcher\/(\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/googlebot/i],describe:function(e){var t={name:`Googlebot`},n=i.default.getFirstMatch(/googlebot\/(\d+(\.\d+))/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/linespider/i],describe:function(e){var t={name:`Linespider`},n=i.default.getFirstMatch(/(?:linespider)(?:-[-\w]+)?[\s/](\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/amazonbot/i],describe:function(e){var t={name:`AmazonBot`},n=i.default.getFirstMatch(/amazonbot\/(\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/bingbot/i],describe:function(e){var t={name:`BingCrawler`},n=i.default.getFirstMatch(/bingbot\/(\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/baiduspider/i],describe:function(e){var t={name:`BaiduSpider`},n=i.default.getFirstMatch(/baiduspider\/(\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/duckduckbot/i],describe:function(e){var t={name:`DuckDuckBot`},n=i.default.getFirstMatch(/duckduckbot\/(\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/ia_archiver/i],describe:function(e){var t={name:`InternetArchiveCrawler`},n=i.default.getFirstMatch(/ia_archiver\/(\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/facebookexternalhit/i,/facebookcatalog/i],describe:function(){return{name:`FacebookExternalHit`}}},{test:[/slackbot/i,/slack-imgProxy/i],describe:function(e){var t={name:`SlackBot`},n=i.default.getFirstMatch(/(?:slackbot|slack-imgproxy)(?:-[-\w]+)?[\s/](\d+(\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),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`},n=i.default.getFirstMatch(a,e)||i.default.getFirstMatch(/(?:opera)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/opr\/|opios/i],describe:function(e){var t={name:`Opera`},n=i.default.getFirstMatch(/(?:opr|opios)[\s/](\S+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/SamsungBrowser/i],describe:function(e){var t={name:`Samsung Internet for Android`},n=i.default.getFirstMatch(a,e)||i.default.getFirstMatch(/(?:SamsungBrowser)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/Whale/i],describe:function(e){var t={name:`NAVER Whale Browser`},n=i.default.getFirstMatch(a,e)||i.default.getFirstMatch(/(?:whale)[\s/](\d+(?:\.\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/PaleMoon/i],describe:function(e){var t={name:`Pale Moon`},n=i.default.getFirstMatch(a,e)||i.default.getFirstMatch(/(?:PaleMoon)[\s/](\d+(?:\.\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/MZBrowser/i],describe:function(e){var t={name:`MZ Browser`},n=i.default.getFirstMatch(/(?:MZBrowser)[\s/](\d+(?:\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/focus/i],describe:function(e){var t={name:`Focus`},n=i.default.getFirstMatch(/(?:focus)[\s/](\d+(?:\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/swing/i],describe:function(e){var t={name:`Swing`},n=i.default.getFirstMatch(/(?:swing)[\s/](\d+(?:\.\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/coast/i],describe:function(e){var t={name:`Opera Coast`},n=i.default.getFirstMatch(a,e)||i.default.getFirstMatch(/(?:coast)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/opt\/\d+(?:.?_?\d+)+/i],describe:function(e){var t={name:`Opera Touch`},n=i.default.getFirstMatch(/(?:opt)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/yabrowser/i],describe:function(e){var t={name:`Yandex Browser`},n=i.default.getFirstMatch(/(?:yabrowser)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/ucbrowser/i],describe:function(e){var t={name:`UC Browser`},n=i.default.getFirstMatch(a,e)||i.default.getFirstMatch(/(?:ucbrowser)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/Maxthon|mxios/i],describe:function(e){var t={name:`Maxthon`},n=i.default.getFirstMatch(a,e)||i.default.getFirstMatch(/(?:Maxthon|mxios)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/epiphany/i],describe:function(e){var t={name:`Epiphany`},n=i.default.getFirstMatch(a,e)||i.default.getFirstMatch(/(?:epiphany)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/puffin/i],describe:function(e){var t={name:`Puffin`},n=i.default.getFirstMatch(a,e)||i.default.getFirstMatch(/(?:puffin)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/sleipnir/i],describe:function(e){var t={name:`Sleipnir`},n=i.default.getFirstMatch(a,e)||i.default.getFirstMatch(/(?:sleipnir)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/k-meleon/i],describe:function(e){var t={name:`K-Meleon`},n=i.default.getFirstMatch(a,e)||i.default.getFirstMatch(/(?:k-meleon)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/micromessenger/i],describe:function(e){var t={name:`WeChat`},n=i.default.getFirstMatch(/(?:micromessenger)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/qqbrowser/i],describe:function(e){var t={name:/qqbrowserlite/i.test(e)?`QQ Browser Lite`:`QQ Browser`},n=i.default.getFirstMatch(/(?:qqbrowserlite|qqbrowser)[/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/msie|trident/i],describe:function(e){var t={name:`Internet Explorer`},n=i.default.getFirstMatch(/(?:msie |rv:)(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/\sedg\//i],describe:function(e){var t={name:`Microsoft Edge`},n=i.default.getFirstMatch(/\sedg\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/edg([ea]|ios)/i],describe:function(e){var t={name:`Microsoft Edge`},n=i.default.getSecondMatch(/edg([ea]|ios)\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/vivaldi/i],describe:function(e){var t={name:`Vivaldi`},n=i.default.getFirstMatch(/vivaldi\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/seamonkey/i],describe:function(e){var t={name:`SeaMonkey`},n=i.default.getFirstMatch(/seamonkey\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/sailfish/i],describe:function(e){var t={name:`Sailfish`},n=i.default.getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i,e);return n&&(t.version=n),t}},{test:[/silk/i],describe:function(e){var t={name:`Amazon Silk`},n=i.default.getFirstMatch(/silk\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/phantom/i],describe:function(e){var t={name:`PhantomJS`},n=i.default.getFirstMatch(/phantomjs\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/slimerjs/i],describe:function(e){var t={name:`SlimerJS`},n=i.default.getFirstMatch(/slimerjs\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/blackberry|\bbb\d+/i,/rim\stablet/i],describe:function(e){var t={name:`BlackBerry`},n=i.default.getFirstMatch(a,e)||i.default.getFirstMatch(/blackberry[\d]+\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/(web|hpw)[o0]s/i],describe:function(e){var t={name:`WebOS Browser`},n=i.default.getFirstMatch(a,e)||i.default.getFirstMatch(/w(?:eb)?[o0]sbrowser\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/bada/i],describe:function(e){var t={name:`Bada`},n=i.default.getFirstMatch(/dolfin\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/tizen/i],describe:function(e){var t={name:`Tizen`},n=i.default.getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/qupzilla/i],describe:function(e){var t={name:`QupZilla`},n=i.default.getFirstMatch(/(?:qupzilla)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/librewolf/i],describe:function(e){var t={name:`LibreWolf`},n=i.default.getFirstMatch(/(?:librewolf)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/firefox|iceweasel|fxios/i],describe:function(e){var t={name:`Firefox`},n=i.default.getFirstMatch(/(?:firefox|iceweasel|fxios)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/electron/i],describe:function(e){var t={name:`Electron`},n=i.default.getFirstMatch(/(?:electron)\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/sogoumobilebrowser/i,/metasr/i,/se 2\.[x]/i],describe:function(e){var t={name:`Sogou Browser`},n=i.default.getFirstMatch(/(?:sogoumobilebrowser)[\s/](\d+(\.?_?\d+)+)/i,e),r=i.default.getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.?_?\d+)+)/i,e),a=i.default.getFirstMatch(/se ([\d.]+)x/i,e),o=n||r||a;return o&&(t.version=o),t}},{test:[/MiuiBrowser/i],describe:function(e){var t={name:`Miui`},n=i.default.getFirstMatch(/(?:MiuiBrowser)[\s/](\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:function(e){return!!e.hasBrand(`DuckDuckGo`)||e.test(/\sDdg\/[\d.]+$/i)},describe:function(e,t){var n={name:`DuckDuckGo`};if(t){var r=t.getBrandVersion(`DuckDuckGo`);if(r)return n.version=r,n}var a=i.default.getFirstMatch(/\sDdg\/([\d.]+)$/i,e);return a&&(n.version=a),n}},{test:function(e){return e.hasBrand(`Brave`)},describe:function(e,t){var n={name:`Brave`};if(t){var r=t.getBrandVersion(`Brave`);if(r)return n.version=r,n}return n}},{test:[/chromium/i],describe:function(e){var t={name:`Chromium`},n=i.default.getFirstMatch(/(?:chromium)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/chrome|crios|crmo/i],describe:function(e){var t={name:`Chrome`},n=i.default.getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:[/GSA/i],describe:function(e){var t={name:`Google Search`},n=i.default.getFirstMatch(/(?:GSA)\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:function(e){var t=!e.test(/like android/i),n=e.test(/android/i);return t&&n},describe:function(e){var t={name:`Android Browser`},n=i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/playstation 4/i],describe:function(e){var t={name:`PlayStation 4`},n=i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/safari|applewebkit/i],describe:function(e){var t={name:`Safari`},n=i.default.getFirstMatch(a,e);return n&&(t.version=n),t}},{test:[/.*/i],describe:function(e){var t=e.search(`\\(`)===-1?/^(.*)\/(.*) /:/^(.*)\/(.*)[ \t]\((.*)/;return{name:i.default.getFirstMatch(t,e),version:i.default.getSecondMatch(t,e)}}}],e.exports=t.default},93:function(e,t,n){"use strict";t.__esModule=!0,t.default=void 0;var r,i=(r=n(17))&&r.__esModule?r:{default:r},a=n(18);t.default=[{test:[/Roku\/DVP/],describe:function(e){var t=i.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=i.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=i.default.getFirstMatch(/Windows ((NT|XP)( \d\d?.\d)?)/i,e),n=i.default.getWindowsVersionName(t);return{name:a.OS_MAP.Windows,version:t,versionName:n}}},{test:[/Macintosh(.*?) FxiOS(.*?)\//],describe:function(e){var t={name:a.OS_MAP.iOS},n=i.default.getSecondMatch(/(Version\/)(\d[\d.]+)/,e);return n&&(t.version=n),t}},{test:[/macintosh/i],describe:function(e){var t=i.default.getFirstMatch(/mac os x (\d+(\.?_?\d+)+)/i,e).replace(/[_\s]/g,`.`),n=i.default.getMacOSVersionName(t),r={name:a.OS_MAP.MacOS,version:t};return n&&(r.versionName=n),r}},{test:[/(ipod|iphone|ipad)/i],describe:function(e){var t=i.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=i.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),n=e.test(/android/i);return t&&n},describe:function(e){var t=i.default.getFirstMatch(/android[\s/-](\d+(\.\d+)*)/i,e),n=i.default.getAndroidVersionName(t),r={name:a.OS_MAP.Android,version:t};return n&&(r.versionName=n),r}},{test:[/(web|hpw)[o0]s/i],describe:function(e){var t=i.default.getFirstMatch(/(?:web|hpw)[o0]s\/(\d+(\.\d+)*)/i,e),n={name:a.OS_MAP.WebOS};return t&&t.length&&(n.version=t),n}},{test:[/blackberry|\bbb\d+/i,/rim\stablet/i],describe:function(e){var t=i.default.getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i,e)||i.default.getFirstMatch(/blackberry\d+\/(\d+([_\s]\d+)*)/i,e)||i.default.getFirstMatch(/\bbb(\d+)/i,e);return{name:a.OS_MAP.BlackBerry,version:t}}},{test:[/bada/i],describe:function(e){var t=i.default.getFirstMatch(/bada\/(\d+(\.\d+)*)/i,e);return{name:a.OS_MAP.Bada,version:t}}},{test:[/tizen/i],describe:function(e){var t=i.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=i.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,n){"use strict";t.__esModule=!0,t.default=void 0;var r,i=(r=n(17))&&r.__esModule?r:{default:r},a=n(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=i.default.getFirstMatch(/(can-l01)/i,e)&&`Nova`,n={type:a.PLATFORMS_MAP.mobile,vendor:`Huawei`};return t&&(n.model=t),n}},{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),n=e.test(/like (ipod|iphone)/i);return t&&!n},describe:function(e){var t=i.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=i.default.getFirstMatch(/Nokia\s+([0-9]+(\.[0-9]+)?)/i,e),n={type:a.PLATFORMS_MAP.mobile,vendor:`Nokia`};return t&&(n.model=t),n}},{test:[/[^-]mobi/i],describe:function(){return{type:a.PLATFORMS_MAP.mobile}}},{test:function(e){return e.getBrowserName(!0)===`blackberry`},describe:function(){return{type:a.PLATFORMS_MAP.mobile,vendor:`BlackBerry`}}},{test:function(e){return e.getBrowserName(!0)===`bada`},describe:function(){return{type:a.PLATFORMS_MAP.mobile}}},{test:function(e){return e.getBrowserName()===`windows phone`},describe:function(){return{type:a.PLATFORMS_MAP.mobile,vendor:`Microsoft`}}},{test:function(e){var t=Number(String(e.getOSVersion()).split(`.`)[0]);return e.getOSName(!0)===`android`&&t>=3},describe:function(){return{type:a.PLATFORMS_MAP.tablet}}},{test:function(e){return e.getOSName(!0)===`android`},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 e.getOSName(!0)===`macos`},describe:function(){return{type:a.PLATFORMS_MAP.desktop,vendor:`Apple`}}},{test:function(e){return e.getOSName(!0)===`windows`},describe:function(){return{type:a.PLATFORMS_MAP.desktop}}},{test:function(e){return e.getOSName(!0)===`linux`},describe:function(){return{type:a.PLATFORMS_MAP.desktop}}},{test:function(e){return e.getOSName(!0)===`playstation 4`},describe:function(){return{type:a.PLATFORMS_MAP.tv}}},{test:function(e){return e.getOSName(!0)===`roku`},describe:function(){return{type:a.PLATFORMS_MAP.tv}}}],e.exports=t.default},95:function(e,t,n){"use strict";t.__esModule=!0,t.default=void 0;var r,i=(r=n(17))&&r.__esModule?r:{default:r},a=n(18);t.default=[{test:function(e){return e.getBrowserName(!0)===`microsoft edge`},describe:function(e){if(/\sedg\//i.test(e))return{name:a.ENGINE_MAP.Blink};var t=i.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},n=i.default.getFirstMatch(/trident\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:function(e){return e.test(/presto/i)},describe:function(e){var t={name:a.ENGINE_MAP.Presto},n=i.default.getFirstMatch(/presto\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}},{test:function(e){var t=e.test(/gecko/i),n=e.test(/like gecko/i);return t&&!n},describe:function(e){var t={name:a.ENGINE_MAP.Gecko},n=i.default.getFirstMatch(/gecko\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),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},n=i.default.getFirstMatch(/webkit\/(\d+(\.?_?\d+)+)/i,e);return n&&(t.version=n),t}}],e.exports=t.default}})}))}))(),1).default.getParser(globalThis.navigator.userAgent).getResult(),Qp=`unknown`,$p=(...e)=>e.filter(Boolean).join(` `).trim()||Qp;function em(){return typeof document<`u`&&document.hidden}function tm(){return{os:$p(Zp.os?.name,Zp.os?.version),browser:$p(Zp.browser?.name,Zp.browser?.version),loader:(()=>{let e=GM_info?.scriptHandler,t=GM_info?.version;return e&&t?`${e} v${t}`:e||t||Qp})(),scriptVersion:GM_info?.script?.version??Qp,scriptName:GM_info?.script?.name??Qp,url:globalThis?.location?.href??Qp}}var nm=class{container;accountWrapper;buttons;usernameEl;avatarEl;avatarImg;actionButton;refreshButton;tokenButton;onClick=new H;onRefresh=new H;onClickSecret=new H;events={click:this.onClick,"click:secret":this.onClickSecret,refresh:this.onRefresh};_loggedIn;_username;_avatarId;constructor({loggedIn:e=!1,username:t=`unnamed`,avatarId:n=`0/0-0`}={}){this._loggedIn=e,this._username=t,this._avatarId=n;let r=this.createElements();this.container=r.container,this.accountWrapper=r.accountWrapper,this.buttons=r.buttons,this.usernameEl=r.usernameEl,this.avatarEl=r.avatarEl,this.avatarImg=r.avatarImg,this.actionButton=r.actionButton,this.refreshButton=r.refreshButton,this.tokenButton=r.tokenButton}createElements(){let e=J.createEl(`vot-block`,[`vot-account`]),t=J.createEl(`vot-block`,[`vot-account-wrapper`]);t.hidden=!this._loggedIn;let n=J.createEl(`img`,[`vot-account-avatar-img`]);n.src=`${vi}/${this._avatarId}/islands-retina-middle`,n.loading=`lazy`,n.alt=`user avatar`;let r=J.createEl(`vot-block`,[`vot-account-avatar`],n),i=J.createEl(`vot-block`,[`vot-account-username`]);i.textContent=this._username,t.append(r,i);let a=J.createEl(`vot-block`,[`vot-account-buttons`]),o=J.createOutlinedButton(this.buttonText);o.addEventListener(`click`,()=>{this.onClick.dispatch()});let s=J.createIconButton(Pp,{ariaLabel:V.get(`VOTLoginViaToken`)});s.hidden=this._loggedIn,s.addEventListener(`click`,()=>{this.onClickSecret.dispatch()});let c=J.createIconButton(Np,{ariaLabel:V.get(`VOTRefresh`)});return c.addEventListener(`click`,()=>{this.onRefresh.dispatch()}),a.append(o,s,c),e.append(t,a),{container:e,accountWrapper:t,buttons:a,usernameEl:i,avatarImg:n,avatarEl:r,actionButton:o,refreshButton:c,tokenButton:s}}addEventListener(e,t){return Fp(this.events,e,t),this}removeEventListener(e,t){return Ip(this.events,e,t),this}get buttonText(){return this._loggedIn?V.get(`VOTLogout`):V.get(`VOTLogin`)}get loggedIn(){return this._loggedIn}set loggedIn(e){this._loggedIn=e,this.accountWrapper.hidden=!this._loggedIn,this.actionButton.textContent=this.buttonText,this.tokenButton.hidden=this._loggedIn}get avatarId(){return this._avatarId}set avatarId(e){this._avatarId=e??`0/0-0`,this.avatarImg.src=`${vi}/${this._avatarId}/islands-retina-middle`}get username(){return this._username}set username(e){this._username=e??`unnamed`,this.usernameEl.textContent=this._username}set hidden(e){X(this.container,e)}get hidden(){return Z(this.container)}},$=class{container;input;label;onChange=new H;events={change:this.onChange};_labelHtml;_checked;_isSubCheckbox;constructor({labelHtml:e,checked:t=!1,isSubCheckbox:n=!1}){this._labelHtml=e,this._checked=t,this._isSubCheckbox=n;let r=this.createElements();this.container=r.container,this.input=r.input,this.label=r.label}createElements(){let e=J.createEl(`label`,[`vot-checkbox`]);this._isSubCheckbox&&e.classList.add(`vot-checkbox-sub`);let t=document.createElement(`input`);t.type=`checkbox`,t.checked=this._checked,t.addEventListener(`change`,()=>{this._checked=t.checked,this.onChange.dispatch(this._checked)});let n=J.createEl(`span`);return q(this._labelHtml,n),e.append(t,n),{container:e,input:t,label:n}}addEventListener(e,t){return Fp(this.events,`change`,t),this}removeEventListener(e,t){return Ip(this.events,`change`,t),this}set hidden(e){X(this.container,e)}get hidden(){return Z(this.container)}get disabled(){return this.input.disabled}set disabled(e){this.input.disabled=e}get checked(){return this._checked}set checked(e){this._checked!==e&&(this._checked=this.input.checked=e,this.onChange.dispatch(this._checked))}},rm=class{container;header;arrowIcon;onClick=new H;events={click:this.onClick};_titleHtml;constructor({titleHtml:e}){this._titleHtml=e;let t=this.createElements();this.container=t.container,this.header=t.header,this.arrowIcon=t.arrowIcon}createElements(){let e=J.createEl(`vot-block`,[`vot-details`]);J.makeButtonLike(e);let t=J.createEl(`vot-block`);t.append(this._titleHtml);let n=J.createEl(`vot-block`,[`vot-details-arrow-icon`]);return q(Op,n),e.append(t,n),e.addEventListener(`click`,()=>{this.onClick.dispatch()}),{container:e,header:t,arrowIcon:n}}addEventListener(e,t){return Fp(this.events,`click`,t),this}removeEventListener(e,t){return Ip(this.events,`click`,t),this}set hidden(e){X(this.container,e)}get hidden(){return Z(this.container)}},im=class{container;button;onChange=new H;events={change:this.onChange};_labelHtml;_key;pressedKeys;comboKeys;recording=!1;constructor({labelHtml:e,key:t=null}){this._labelHtml=e,this._key=t,this.pressedKeys=new Set,this.comboKeys=new Set;let n=this.createElements();this.container=n.container,this.button=n.button}stopRecordingKeys(){this.recording=!1,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=e=>{if(!(!this.recording||e.repeat)){if(e.preventDefault(),e.code===`Escape`){this.key=null,this.button.textContent=this.keyText,this.stopRecordingKeys();return}this.pressedKeys.add(e.code),this.comboKeys.add(e.code),this.button.textContent=om(this.pressedKeys)}};keyupOrBlurHandle=e=>{this.recording&&(e&&(this.pressedKeys.delete(e.code),this.button.textContent=this.pressedKeys.size?om(this.pressedKeys):om(this.comboKeys),this.pressedKeys.size)||(this.key=this.comboKeys.size?am(this.comboKeys):null,this.stopRecordingKeys()))};blurHandle=()=>{this.keyupOrBlurHandle()};createElements(){let e=J.createEl(`vot-block`,[`vot-hotkey`]),t=J.createEl(`vot-block`,[`vot-hotkey-label`]);t.textContent=this._labelHtml;let n=J.createEl(`vot-block`,[`vot-hotkey-button`]);return J.makeButtonLike(n),n.textContent=this.keyText,n.addEventListener(`click`,()=>{if(this.recording){this.stopRecordingKeys(),this.button.textContent=this.keyText;return}n.dataset.status=`active`,this.recording=!0,this.pressedKeys.clear(),this.comboKeys.clear(),this.button.textContent=V.get(`PressTheKeyCombination`),document.addEventListener(`keydown`,this.keydownHandle),document.addEventListener(`keyup`,this.keyupOrBlurHandle),globalThis.addEventListener(`blur`,this.blurHandle)}),e.append(t,n),{container:e,button:n,label:t}}addEventListener(e,t){return Fp(this.events,`change`,t),this}removeEventListener(e,t){return Ip(this.events,`change`,t),this}set hidden(e){X(this.container,e)}get hidden(){return Z(this.container)}get key(){return this._key}get keyText(){return this._key?om(this._key):V.get(`None`)}set key(e){this._key!==e&&(this._key=e,this.button.textContent=this.keyText,this.onChange.dispatch(this._key))}};function am(e){return(Array.isArray(e)?e:Array.from(e)).map(e=>e.replace(`Key`,``).replace(`Digit`,``)).join(`+`)}function om(e){let t;t=typeof e==`string`?e.split(`+`).filter(Boolean):Array.isArray(e)?e:Array.from(e);let n=e=>{switch(e){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 e.replace(`Key`,``).replace(`Digit`,``)}},r=e=>{let t=n(e);return t===`Ctrl`?0:t===`Alt`?1:t===`Shift`?2:t===`Meta`?3:10};return t.slice().sort((e,t)=>r(e)-r(t)).map(n).join(`+`)}var sm=[`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 cm(){let e={};for(let t of sm)e[t]=new H;return e}var lm=30,[um,dm]=Di,fm={"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 pm(e){return su(e)?fm[e]:gu(e)??`Default Sans`}function mm(){return Object.keys(V.defaultLocale).filter(e=>e.startsWith(`langs.`)&&e!==`langs.auto`).map(e=>e.slice(6)).sort((e,t)=>V.getLangLabel(e).localeCompare(V.getLangLabel(t)))}function hm(e){return e===dm?V.get(`VOTOriginalVideoLanguage`):V.getLangLabel(e)}function gm(e){return[{label:hm(um),value:um,selected:e===um},{label:hm(dm),value:dm,selected:e===dm},...mm().map(t=>({label:hm(t),value:t,selected:e===t}))]}var _m=class e{static PERSIST_DELAY_MS=250;globalPortal;initialized=!1;data;videoHandler;suppressSubtitlesSmartLayoutCheckboxChange=!1;events=cm();persistTimerIds={};onAuthRefreshMessage=e=>{so(e.data)&&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:e,data:t={},videoHandler:n}){this.globalPortal=e,this.data=t,this.videoHandler=n}isInitialized(){return this.initialized}createAccordionSection(e,t={}){let n=J.createEl(`vot-block`,[`vot-settings-section`]),r=new rm({titleHtml:e});r.container.classList.add(`vot-settings-section-header`);let i=typeof crypto<`u`&&`randomUUID`in crypto?crypto.randomUUID():`${Date.now()}-${Math.random().toString(16).slice(2)}`,a=`vot-settings-section-header-${i}`,o=`vot-settings-section-content-${i}`;r.container.id=a;let s=J.createEl(`vot-block`,[`vot-settings-section-content`]);s.id=o,s.setAttribute(`role`,`region`),s.setAttribute(`aria-labelledby`,a),r.container.setAttribute(`aria-controls`,o);let c=e=>{r.container.dataset.open=e?`true`:`false`,r.container.setAttribute(`aria-expanded`,e?`true`:`false`),s.hidden=!e};return c(!!t.open),r.addEventListener(`click`,()=>{c(r.container.dataset.open!==`true`)}),n.append(r.container,s),{title:e,container:n,header:r.container,content:s,setOpen:c,getOpen:()=>r.container.dataset.open===`true`}}setSubtitlesSmartLayout(e){this.data.subtitlesSmartLayout=e,B.set(`subtitlesSmartLayout`,e),L.log(`subtitlesSmartLayout value changed. New value:`,e),this.subtitlesSmartLayoutCheckbox?.checked!==e&&(this.suppressSubtitlesSmartLayoutCheckboxChange=!0,this.subtitlesSmartLayoutCheckbox.checked=e,this.suppressSubtitlesSmartLayoutCheckboxChange=!1),this.events[`change:subtitlesSmartLayout`].dispatch(e)}scheduleStoragePersist(t,n){let r=this.persistTimerIds[t];r!==void 0&&globalThis.clearTimeout(r),this.persistTimerIds[t]=globalThis.setTimeout(()=>{this.persistTimerIds[t]=void 0,B.set(t,n)},e.PERSIST_DELAY_MS)}flushStoragePersists(){for(let e of Object.keys(this.persistTimerIds)){let t=this.persistTimerIds[e];if(t===void 0)continue;globalThis.clearTimeout(t),this.persistTimerIds[e]=void 0;let n=this.data[e];typeof n==`number`&&B.set(e,n)}}bindPersistedSetting({control:e,event:t,apply:n,storageKey:r,readPersistedValue:i,logLabel:a,dispatch:o,afterPersist:s}){e.addEventListener(t,async e=>{n(e),await B.set(r,i()),L.log(`${a} value changed. New value:`,e),s&&await s(e),o?.(e)})}createSettingsSections(){let e=[this.createAccordionSection(V.get(`VOTMyAccount`),{open:!0}),this.createAccordionSection(V.get(`translationSettings`),{open:!0}),this.createAccordionSection(V.get(`subtitlesSettings`)),this.createAccordionSection(V.get(`hotkeysSettings`)),this.createAccordionSection(V.get(`proxySettings`)),this.createAccordionSection(V.get(`miscSettings`)),this.createAccordionSection(V.get(`appearance`)),this.createAccordionSection(V.get(`aboutExtension`))];return{accountSection:e[0],translationSection:e[1],subtitlesSection:e[2],hotkeysSection:e[3],proxySection:e[4],miscSection:e[5],appearanceSection:e[6],aboutSection:e[7],sections:e}}initAccountControls(){this.accountButton=new nm({avatarId:this.data.account?.avatarId,username:this.data.account?.username,loggedIn:!!this.data.account?.token}),B.isSupportOnlyLS?(this.accountButton.refreshButton.setAttribute(`disabled`,`true`),this.accountButton.actionButton.setAttribute(`disabled`,`true`)):this.accountButtonRefreshTooltip=new Y({target:this.accountButton.refreshButton,content:V.get(`VOTRefresh`),position:`bottom`,backgroundColor:`var(--vot-helper-ondialog)`,parentElement:this.globalPortal}),this.accountButtonTokenTooltip=new Y({target:this.accountButton.tokenButton,content:V.get(`VOTLoginViaToken`),position:`bottom`,backgroundColor:`var(--vot-helper-ondialog)`,parentElement:this.globalPortal})}bindAccountStorageListener(){this.accountStorageListenerCleanup?.(),this.accountStorageListenerCleanup=B.addValueChangeListener(`account`,(e,t,n)=>{this.data.account=n??{},!(!this.isInitialized()||!this.accountButton)&&this.updateAccountInfo()})}async refreshAccountFromStorage(){B.isSupportOnlyLS||(this.data.account=await B.get(`account`,{}),!(!this.isInitialized()||!this.accountButton)&&this.updateAccountInfo())}buildSubtitleFontItems(e,t=[]){let n=tu.map(t=>({label:fm[t],value:t,selected:t===e})),r=t.filter(e=>{let t=e.toLowerCase();return!n.some(e=>e.label.toLowerCase()===t)}).map(t=>{let n=hu(t);return{label:t,value:n,selected:n===e}});if(!su(e)&&!r.some(t=>t.value===e)){let t=gu(e);t&&r.unshift({label:t,value:e,selected:!0})}return[...n,...r]}async searchSubtitleFontItems(e,t){let n=Array.from(this.subtitlesFontFamilySelect?.selectedValues??[])[0]??t,r=e.trim().toLowerCase();if(!r)return this.buildSubtitleFontItems(n);let i=(await xu()).filter(e=>e.toLowerCase().includes(r)).slice(0,lm);return this.buildSubtitleFontItems(n,i)}initUI(){if(this.isInitialized())throw Error(`[VOT] SettingsView is already initialized`);this.dialog=new Bp({titleHtml:V.get(`VOTSettings`)}),this.globalPortal.appendChild(this.dialog.container);let{accountSection:e,translationSection:t,subtitlesSection:n,hotkeysSection:r,proxySection:i,miscSection:a,appearanceSection:o,aboutSection:s,sections:c}=this.createSettingsSections();this.dialog.bodyContainer.append(...c.map(e=>e.container)),this.initAccountControls(),this.autoTranslateCheckbox=new $({labelHtml:V.get(`VOTAutoTranslate`),checked:this.data.autoTranslate}),this.autoSubtitlesCheckbox=new $({labelHtml:V.get(`VOTAutoSubtitles`),checked:this.data.autoSubtitles});let l=this.data.dontTranslateLanguages??[];this.dontTranslateLanguagesCheckbox=new $({labelHtml:V.get(`DontTranslateSelectedLanguages`),checked:this.data.enabledDontTranslateLanguages}),this.dontTranslateLanguagesSelect=new Q({dialogParent:this.globalPortal,dialogTitle:V.get(`DontTranslateSelectedLanguages`),selectTitle:l.map(e=>V.get(`langs.${e}`)).join(`, `)||V.get(`DontTranslateSelectedLanguages`),items:Q.genLanguageItems(bn).map(e=>({...e,selected:l.includes(e.value)})),multiSelect:!0,labelElement:this.dontTranslateLanguagesCheckbox.container}),this.dontTranslateLanguagesSelect.disabled=!this.dontTranslateLanguagesCheckbox.checked;let u=this.data.autoVolume??15;this.autoSetVolumeSliderLabel=new Gp({labelText:V.get(`VOTAutoSetVolume`),value:u}),this.autoSetVolumeCheckbox=new $({labelHtml:this.autoSetVolumeSliderLabel.container,checked:this.data.enabledAutoVolume??!0}),this.autoSetVolumeSlider=new Up({labelHtml:this.autoSetVolumeCheckbox.container,value:u,min:0});let d=!!this.data.syncVolume;this.autoSetVolumeSlider.disabled=!this.autoSetVolumeCheckbox.checked,this.smartDuckingCheckbox=new $({labelHtml:V.get(`smartDucking`),checked:this.data.enabledSmartDucking??!0}),this.smartDuckingCheckbox.disabled=d||!this.autoSetVolumeCheckbox.checked,this.showVideoVolumeSliderCheckbox=new $({labelHtml:V.get(`showVideoVolumeSlider`),checked:this.data.showVideoSlider}),this.audioBoosterCheckbox=new $({labelHtml:V.get(`VOTAudioBooster`),checked:this.data.audioBooster}),this.videoHandler?.isAudioContextSupported||(this.audioBoosterCheckbox.disabled=!0,this.audioBoosterTooltip=new Y({target:this.audioBoosterCheckbox.container,content:V.get(`VOTNeedWebAudioAPI`),position:`bottom`,backgroundColor:`var(--vot-helper-ondialog)`,parentElement:this.globalPortal})),this.syncVolumeCheckbox=new $({labelHtml:V.get(`VOTSyncVolume`),checked:this.data.syncVolume}),this.downloadWithNameCheckbox=new $({labelHtml:V.get(`VOTDownloadWithName`),checked:this.data.downloadWithName}),this.downloadWithNameCheckbox.disabled=!La,this.sendNotifyOnCompleteCheckbox=new $({labelHtml:V.get(`VOTSendNotifyOnComplete`),checked:this.data.sendNotifyOnComplete}),this.useLivelyVoiceCheckbox=new $({labelHtml:V.get(`VOTUseLivelyVoice`),checked:this.data.useLivelyVoice}),this.useLivelyVoiceTooltip=new Y({target:this.useLivelyVoiceCheckbox.container,content:V.get(`VOTAccountRequired`),position:`bottom`,backgroundColor:`var(--vot-helper-ondialog)`,parentElement:this.globalPortal,hidden:!!this.data.account?.token}),this.data.account?.token||(this.useLivelyVoiceCheckbox.disabled=!0),this.useAudioDownloadCheckboxLabel=new zp({labelText:V.get(`VOTUseAudioDownload`),icon:jp}),this.useAudioDownloadCheckbox=new $({labelHtml:this.useAudioDownloadCheckboxLabel.container,checked:this.data.useAudioDownload}),La||(this.useAudioDownloadCheckbox.disabled=!0),this.useAudioDownloadCheckboxTooltip=new Y({target:this.useAudioDownloadCheckboxLabel.container,content:V.get(`VOTUseAudioDownloadWarning`),position:`bottom`,backgroundColor:`var(--vot-helper-ondialog)`,parentElement:this.globalPortal}),e.content.append(this.accountButton.container),t.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 zp({labelText:V.get(`VOTSubtitlesDownloadFormat`)}),this.subtitlesDownloadFormatSelect=new Q({selectTitle:this.data.subtitlesDownloadFormat??V.get(`VOTSubtitlesDownloadFormat`),dialogTitle:V.get(`VOTSubtitlesDownloadFormat`),dialogParent:this.globalPortal,labelElement:this.subtitlesDownloadFormatSelectLabel.container,items:eu.map(e=>({label:e.toUpperCase(),value:e,selected:e===this.data.subtitlesDownloadFormat}))});let f=this.data.responseLanguageSubtitles??um;this.responseLanguageSubtitlesSelectLabel=new zp({labelText:V.get(`VOTDefaultSubtitlesLanguage`)}),this.responseLanguageSubtitlesSelect=new Q({selectTitle:hm(f),dialogTitle:V.get(`VOTDefaultSubtitlesLanguage`),dialogParent:this.globalPortal,labelElement:this.responseLanguageSubtitlesSelectLabel.container,items:gm(f)}),this.subtitlesHighlightWordsCheckbox=new $({labelHtml:V.get(`VOTHighlightWords`),checked:this.data.highlightWords});let p=this.data.subtitlesSmartLayout??!0;this.subtitlesSmartLayoutCheckbox=new $({labelHtml:V.get(`subtitlesSmartLayout`),checked:p});let m=this.data.subtitlesMaxLength??300;this.subtitlesMaxLengthSliderLabel=new Gp({labelText:V.get(`VOTSubtitlesMaxLength`),labelEOL:`:`,value:m,symbol:``}),this.subtitlesMaxLengthSlider=new Up({labelHtml:this.subtitlesMaxLengthSliderLabel.container,value:m,min:50,max:300});let h=this.data.subtitlesFontSize??20;this.subtitlesFontSizeSliderLabel=new Gp({labelText:V.get(`VOTSubtitlesFontSize`),labelEOL:`:`,value:h,symbol:`px`}),this.subtitlesFontSizeSlider=new Up({labelHtml:this.subtitlesFontSizeSliderLabel.container,value:h,min:8,max:50});let g=typeof this.data.subtitlesFontFamily==`string`?this.data.subtitlesFontFamily:void 0,_=g&&(su(g)||gu(g))?g:`default-sans`;this.subtitlesFontFamilySelectLabel=new zp({labelText:V.get(`VOTSubtitlesFont`)}),this.subtitlesFontFamilySelect=new Q({selectTitle:pm(_),dialogTitle:V.get(`VOTSubtitlesFont`),dialogParent:this.globalPortal,labelElement:this.subtitlesFontFamilySelectLabel.container,items:this.buildSubtitleFontItems(_),searchItemsProvider:e=>this.searchSubtitleFontItems(e,_)}),this.subtitlesFontFamilySelect.addEventListener(`selectItem`,e=>{this.subtitlesFontFamilySelect&&(this.subtitlesFontFamilySelect.updateItems(this.buildSubtitleFontItems(e)),this.subtitlesFontFamilySelect.selectTitle=pm(e))});let v=this.data.subtitlesOpacity??20;this.subtitlesBackgroundOpacitySliderLabel=new Gp({labelText:V.get(`VOTSubtitlesOpacity`),labelEOL:`:`,value:v,symbol:`%`}),this.subtitlesBackgroundOpacitySlider=new Up({labelHtml:this.subtitlesBackgroundOpacitySliderLabel.container,value:v,min:0,max:100}),n.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 im({labelHtml:V.get(`translateVideo`),key:this.data.translationHotkey}),this.subtitlesHotkeyButton=new im({labelHtml:V.get(`VOTSubtitles`),key:this.data.subtitlesHotkey}),r.content.append(this.translateHotkeyButton.container,this.subtitlesHotkeyButton.container),this.proxyWorkerHostTextfield=new Vp({labelHtml:V.get(`VOTProxyWorkerHost`),value:this.data.proxyWorkerHost,placeholder:pi});let ee=[V.get(`VOTTranslateProxyDisabled`),V.get(`VOTTranslateProxyEnabled`),V.get(`VOTTranslateProxyEverything`)],te=this.data.translateProxyEnabled??0,ne=$g&&wi.includes($g);this.proxyTranslationStatusSelectLabel=new zp({icon:ne?jp:void 0,labelText:V.get(`VOTTranslateProxyStatus`)}),ne&&(this.proxyTranslationStatusSelectTooltip=new Y({target:this.proxyTranslationStatusSelectLabel.icon,content:V.get(`VOTTranslateProxyStatusDefault`),position:`bottom`,backgroundColor:`var(--vot-helper-ondialog)`,parentElement:this.globalPortal})),this.proxyTranslationStatusSelect=new Q({selectTitle:ee[te],dialogTitle:V.get(`VOTTranslateProxyStatus`),dialogParent:this.globalPortal,labelElement:this.proxyTranslationStatusSelectLabel.container,items:ee.map((e,t)=>({label:e,value:t.toString(),selected:t===te,disabled:t===0&&Fa}))}),i.content.append(this.proxyWorkerHostTextfield.container,this.proxyTranslationStatusSelect.container),this.translateAPIErrorsCheckbox=new $({labelHtml:V.get(`VOTTranslateAPIErrors`),checked:this.data.translateAPIErrors??!0}),this.translateAPIErrorsCheckbox.hidden=V.lang===`ru`,this.useNewAudioPlayerCheckbox=new $({labelHtml:V.get(`VOTNewAudioPlayer`),checked:this.data.newAudioPlayer}),this.videoHandler?.isAudioContextSupported||(this.useNewAudioPlayerCheckbox.disabled=!0,this.useNewAudioPlayerTooltip=new Y({target:this.useNewAudioPlayerCheckbox.container,content:V.get(`VOTNeedWebAudioAPI`),position:`bottom`,backgroundColor:`var(--vot-helper-ondialog)`,parentElement:this.globalPortal})),this.onlyBypassMediaCSPCheckbox=new $({labelHtml:this.videoHandler?.site.needBypassCSP?`${V.get(`VOTOnlyBypassMediaCSP`)} (${V.get(`VOTMediaCSPEnabledOnSite`)})`:V.get(`VOTOnlyBypassMediaCSP`),checked:this.data.onlyBypassMediaCSP,isSubCheckbox:!0}),this.videoHandler?.isAudioContextSupported||(this.onlyBypassMediaCSPTooltip=new Y({target:this.onlyBypassMediaCSPCheckbox.container,content:V.get(`VOTNeedWebAudioAPI`),position:`bottom`,backgroundColor:`var(--vot-helper-ondialog)`,parentElement:this.globalPortal})),this.onlyBypassMediaCSPCheckbox.disabled=!this.data.newAudioPlayer&&!!this.videoHandler?.isAudioContextSupported,this.data.newAudioPlayer||(this.onlyBypassMediaCSPCheckbox.hidden=!0),this.translationTextServiceLabel=new zp({labelText:V.get(`VOTTranslationTextService`),icon:Mp});let re=this.data.translationService??`yandexbrowser`;this.translationTextServiceSelect=new Q({selectTitle:V.get(`services.${re}`),dialogTitle:V.get(`VOTTranslationTextService`),dialogParent:this.globalPortal,labelElement:this.translationTextServiceLabel.container,items:gc.map(e=>({label:V.get(`services.${e}`),value:e,selected:e===re}))}),this.translationTextServiceTooltip=new Y({target:this.translationTextServiceLabel.icon,content:V.get(`VOTNotAffectToVoice`),position:`bottom`,backgroundColor:`var(--vot-helper-ondialog)`,parentElement:this.globalPortal}),this.detectServiceLabel=new zp({labelText:V.get(`VOTDetectService`)});let ie=this.data.detectService??`yandexbrowser`;this.detectServiceSelect=new Q({selectTitle:V.get(`services.${ie}`),dialogTitle:V.get(`VOTDetectService`),dialogParent:this.globalPortal,labelElement:this.detectServiceLabel.container,items:xc.map(e=>({label:V.get(`services.${e}`),value:e,selected:e===ie}))}),this.showPiPButtonCheckbox=new $({labelHtml:V.get(`VOTShowPiPButton`),checked:this.data.showPiPButton}),this.showPiPButtonCheckbox.hidden=!Gi();let ae=Math.round((this.data.autoHideButtonDelay??1e3)/1e3*10)/10;this.autoHideButtonDelaySliderLabel=new Gp({labelText:V.get(`autoHideButtonDelay`),labelEOL:`:`,value:ae,symbol:` ${V.get(`secs`)}`}),this.autoHideButtonDelaySlider=new Up({labelHtml:this.autoHideButtonDelaySliderLabel.container,value:ae,min:.1,max:3,step:.1}),this.buttonPositionSelectLabel=new zp({labelText:V.get(`buttonPosition`),icon:Mp});let y=this.data.buttonPos??`default`;this.buttonPositionSelect=new Q({selectTitle:V.get(`position.${y}`),dialogTitle:V.get(`buttonPosition`),labelElement:this.buttonPositionSelectLabel.container,dialogParent:this.globalPortal,items:Xp.map(e=>({label:V.get(`position.${e}`),value:e,selected:e===y}))}),this.buttonPositionTooltip=new Y({target:this.buttonPositionSelectLabel.icon,content:V.get(`minButtonPositionContainer`),position:`bottom`,backgroundColor:`var(--vot-helper-ondialog)`,parentElement:this.globalPortal}),this.menuLanguageSelectLabel=new zp({labelText:V.get(`VOTMenuLanguage`)}),this.menuLanguageSelect=new Q({selectTitle:V.get(`langs.${V.langOverride}`),dialogTitle:V.get(`VOTMenuLanguage`),labelElement:this.menuLanguageSelectLabel.container,dialogParent:this.globalPortal,items:Q.genLanguageItems(V.getAvailableLangs(),V.langOverride)}),this.bugReportButton=J.createOutlinedButton(V.get(`VOTBugReport`)),this.resetSettingsButton=J.createButton(V.get(`resetSettings`)),a.content.append(this.translateAPIErrorsCheckbox.container,this.useNewAudioPlayerCheckbox.container,this.onlyBypassMediaCSPCheckbox.container),t.content.append(this.translationTextServiceSelect.container,this.detectServiceSelect.container),o.content.append(this.showPiPButtonCheckbox.container,this.autoHideButtonDelaySlider.container,this.buttonPositionSelect.container,this.menuLanguageSelect.container);let oe=tm(),se=J.createInformation(`${V.get(`VOTVersion`)}:`,oe.scriptVersion||GM_info.script.version||V.get(`notFound`)),ce=J.createInformation(`${V.get(`VOTAuthors`)}:`,GM_info.script.author||`Toil, SashaXser, MrSoczekXD, mynovelhost, sodapng`),le=J.createInformation(`${V.get(`VOTLoader`)}:`,oe.loader),ue=J.createInformation(`${V.get(`VOTBrowser`)}:`,`${oe.browser} (${oe.os})`),de=new Date((this.data.localeUpdatedAt??0)*1e3).toLocaleString(),b=Tl`${this.data.localeHash??V.get(`notFound`)}
(${V.get(`VOTUpdatedAt`)} + ${de})`,x=J.createInformation(`${V.get(`VOTLocaleHash`)}:`,b),fe=J.createOutlinedButton(V.get(`VOTUpdateLocaleFiles`));return fe.addEventListener(`click`,async()=>{await B.set(`localeHash`,``),await V.update(!0),globalThis.location.reload()}),s.content.append(se.container,ce.container,le.container,ue.container,x.container,fe),this.dialog.footerContainer.append(this.bugReportButton,this.resetSettingsButton),this.initialized=!0,this}initUIEvents(){if(!this.isInitialized())throw Error(`[VOT] SettingsView isn't initialized`);return globalThis.addEventListener(`message`,this.onAuthRefreshMessage),this.accountButton.addEventListener(`click`,async()=>{if(!B.isSupportOnlyLS){if(this.accountButton.loggedIn)return await B.delete(`account`),this.data.account={},this.updateAccountInfo();globalThis.open(_i,`_blank`)?.focus()}}),this.accountButton.addEventListener(`click:secret`,async()=>{let e=new Bp({titleHtml:V.get(`VOTLoginViaToken`),isTemp:!0});this.globalPortal.appendChild(e.container);let t=J.createEl(`vot-block`,void 0,V.get(`VOTYandexTokenInfo`)),n=new Vp({labelHtml:V.get(`VOTYandexToken`),value:this.data.account?.token});n.addEventListener(`change`,async e=>{this.data.account=e?{expires:Date.now()+3153418e4,token:e}:{},await B.set(`account`,this.data.account),this.updateAccountInfo()}),e.bodyContainer.append(t,n.container),e.open()}),this.accountButton.addEventListener(`refresh`,async()=>{await this.refreshAccountFromStorage()}),this.bindAccountStorageListener(),this.bindPersistedSetting({control:this.autoTranslateCheckbox,event:`change`,apply:e=>{this.data.autoTranslate=e},storageKey:`autoTranslate`,readPersistedValue:()=>this.data.autoTranslate,logLabel:`autoTranslate`,dispatch:e=>this.events[`change:autoTranslate`].dispatch(e)}),this.bindPersistedSetting({control:this.autoSubtitlesCheckbox,event:`change`,apply:e=>{this.data.autoSubtitles=e},storageKey:`autoSubtitles`,readPersistedValue:()=>this.data.autoSubtitles,logLabel:`autoSubtitles`,dispatch:e=>this.events[`change:autoSubtitles`].dispatch(e)}),this.dontTranslateLanguagesCheckbox.addEventListener(`change`,async e=>{this.data.enabledDontTranslateLanguages=e,this.dontTranslateLanguagesSelect.disabled=!e,await B.set(`enabledDontTranslateLanguages`,this.data.enabledDontTranslateLanguages),L.log(`enabledDontTranslateLanguages value changed. New value:`,e)}),this.dontTranslateLanguagesSelect.addEventListener(`selectItem`,async e=>{this.data.dontTranslateLanguages=e,await B.set(`dontTranslateLanguages`,this.data.dontTranslateLanguages),L.log(`dontTranslateLanguages value changed. New value:`,e)}),this.bindPersistedSetting({control:this.autoSetVolumeCheckbox,event:`change`,apply:e=>{this.data.enabledAutoVolume=e,this.autoSetVolumeSlider.disabled=!e,this.smartDuckingCheckbox.disabled=!e||!!this.syncVolumeCheckbox?.checked},storageKey:`enabledAutoVolume`,readPersistedValue:()=>this.data.enabledAutoVolume,logLabel:`enabledAutoVolume`,afterPersist:async()=>this.videoHandler?.setupAudioSettings?.()}),this.bindPersistedSetting({control:this.smartDuckingCheckbox,event:`change`,apply:e=>{this.data.enabledSmartDucking=e},storageKey:`enabledSmartDucking`,readPersistedValue:()=>this.data.enabledSmartDucking,logLabel:`enabledSmartDucking`,afterPersist:async()=>this.videoHandler?.setupAudioSettings?.()}),this.bindPersistedSetting({control:this.autoSetVolumeSlider,event:`input`,apply:e=>{this.data.autoVolume=this.autoSetVolumeSliderLabel.value=e},storageKey:`autoVolume`,readPersistedValue:()=>this.data.autoVolume,logLabel:`autoVolume`}),this.bindPersistedSetting({control:this.showVideoVolumeSliderCheckbox,event:`change`,apply:e=>{this.data.showVideoSlider=e},storageKey:`showVideoSlider`,readPersistedValue:()=>this.data.showVideoSlider,logLabel:`showVideoVolumeSlider`,dispatch:e=>this.events[`change:showVideoVolume`].dispatch(e)}),this.bindPersistedSetting({control:this.audioBoosterCheckbox,event:`change`,apply:e=>{this.data.audioBooster=e},storageKey:`audioBooster`,readPersistedValue:()=>this.data.audioBooster,logLabel:`audioBooster`,dispatch:e=>this.events[`change:audioBooster`].dispatch(e)}),this.bindPersistedSetting({control:this.syncVolumeCheckbox,event:`change`,apply:e=>{this.data.syncVolume=e,this.autoSetVolumeSlider.disabled=!this.autoSetVolumeCheckbox?.checked,this.smartDuckingCheckbox.disabled=e||!this.autoSetVolumeCheckbox?.checked,e&&this.smartDuckingCheckbox?.checked&&(this.smartDuckingCheckbox.checked=!1)},storageKey:`syncVolume`,readPersistedValue:()=>this.data.syncVolume,logLabel:`syncVolume`,dispatch:e=>this.events[`change:syncVolume`].dispatch(e)}),this.bindPersistedSetting({control:this.downloadWithNameCheckbox,event:`change`,apply:e=>{this.data.downloadWithName=e},storageKey:`downloadWithName`,readPersistedValue:()=>this.data.downloadWithName,logLabel:`downloadWithName`}),this.bindPersistedSetting({control:this.sendNotifyOnCompleteCheckbox,event:`change`,apply:e=>{this.data.sendNotifyOnComplete=e},storageKey:`sendNotifyOnComplete`,readPersistedValue:()=>this.data.sendNotifyOnComplete,logLabel:`sendNotifyOnComplete`}),this.bindPersistedSetting({control:this.useLivelyVoiceCheckbox,event:`change`,apply:e=>{this.data.useLivelyVoice=e},storageKey:`useLivelyVoice`,readPersistedValue:()=>this.data.useLivelyVoice,logLabel:`useLivelyVoice`,dispatch:e=>this.events[`change:useLivelyVoice`].dispatch(e)}),this.bindPersistedSetting({control:this.useAudioDownloadCheckbox,event:`change`,apply:e=>{this.data.useAudioDownload=e},storageKey:`useAudioDownload`,readPersistedValue:()=>this.data.useAudioDownload,logLabel:`useAudioDownload`}),this.bindPersistedSetting({control:this.responseLanguageSubtitlesSelect,event:`selectItem`,apply:e=>{this.data.responseLanguageSubtitles=e,this.responseLanguageSubtitlesSelect?.updateItems(gm(e)),this.responseLanguageSubtitlesSelect&&(this.responseLanguageSubtitlesSelect.selectTitle=hm(e))},storageKey:`responseLanguageSubtitles`,readPersistedValue:()=>this.data.responseLanguageSubtitles,logLabel:`responseLanguageSubtitles`,dispatch:e=>this.events[`select:responseLanguageSubtitles`].dispatch(e)}),this.bindPersistedSetting({control:this.subtitlesDownloadFormatSelect,event:`selectItem`,apply:e=>{this.data.subtitlesDownloadFormat=e},storageKey:`subtitlesDownloadFormat`,readPersistedValue:()=>this.data.subtitlesDownloadFormat,logLabel:`subtitlesDownloadFormat`}),this.bindPersistedSetting({control:this.subtitlesHighlightWordsCheckbox,event:`change`,apply:e=>{this.data.highlightWords=e},storageKey:`highlightWords`,readPersistedValue:()=>this.data.highlightWords,logLabel:`highlightWords`,dispatch:e=>this.events[`change:subtitlesHighlightWords`].dispatch(e)}),this.subtitlesSmartLayoutCheckbox?.addEventListener(`change`,e=>{this.suppressSubtitlesSmartLayoutCheckboxChange||this.setSubtitlesSmartLayout(e)}),this.subtitlesMaxLengthSlider.addEventListener(`input`,e=>{this.subtitlesMaxLengthSliderLabel.value=e,(this.data.subtitlesSmartLayout??!0)===!0&&this.setSubtitlesSmartLayout(!1),this.data.subtitlesMaxLength=e,this.scheduleStoragePersist(`subtitlesMaxLength`,this.data.subtitlesMaxLength),L.log(`subtitlesMaxLength value changed. New value:`,e),this.events[`input:subtitlesMaxLength`].dispatch(e)}),this.subtitlesFontSizeSlider.addEventListener(`input`,e=>{this.subtitlesFontSizeSliderLabel.value=e,(this.data.subtitlesSmartLayout??!0)===!0&&this.setSubtitlesSmartLayout(!1),this.data.subtitlesFontSize=e,this.scheduleStoragePersist(`subtitlesFontSize`,this.data.subtitlesFontSize),L.log(`subtitlesFontSize value changed. New value:`,e),this.events[`input:subtitlesFontSize`].dispatch(e)}),this.subtitlesBackgroundOpacitySlider.addEventListener(`input`,e=>{this.subtitlesBackgroundOpacitySliderLabel.value=e,this.data.subtitlesOpacity=e,this.scheduleStoragePersist(`subtitlesOpacity`,this.data.subtitlesOpacity),L.log(`subtitlesOpacity value changed. New value:`,e),this.events[`input:subtitlesBackgroundOpacity`].dispatch(e)}),this.bindPersistedSetting({control:this.subtitlesFontFamilySelect,event:`selectItem`,apply:e=>{this.data.subtitlesFontFamily=e},storageKey:`subtitlesFontFamily`,readPersistedValue:()=>this.data.subtitlesFontFamily,logLabel:`subtitlesFontFamily`,dispatch:e=>this.events[`select:subtitlesFontFamily`].dispatch(e)}),this.bindPersistedSetting({control:this.translateHotkeyButton,event:`change`,apply:e=>{this.data.translationHotkey=e},storageKey:`translationHotkey`,readPersistedValue:()=>this.data.translationHotkey,logLabel:`translationHotkey`}),this.bindPersistedSetting({control:this.subtitlesHotkeyButton,event:`change`,apply:e=>{this.data.subtitlesHotkey=e},storageKey:`subtitlesHotkey`,readPersistedValue:()=>this.data.subtitlesHotkey,logLabel:`subtitlesHotkey`}),this.proxyWorkerHostTextfield.addEventListener(`change`,async e=>{this.data.proxyWorkerHost=e||`vot-worker.kload.workers.dev`,await B.set(`proxyWorkerHost`,this.data.proxyWorkerHost),L.log(`proxyWorkerHost value changed. New value:`,this.data.proxyWorkerHost),this.events[`change:proxyWorkerHost`].dispatch(e)}),this.proxyTranslationStatusSelect.addEventListener(`selectItem`,async e=>{this.data.translateProxyEnabled=Number.parseInt(e,10),await B.set(`translateProxyEnabled`,this.data.translateProxyEnabled),await B.set(`translateProxyEnabledDefault`,!1),L.log(`translateProxyEnabled value changed. New value:`,this.data.translateProxyEnabled),this.events[`select:proxyTranslationStatus`].dispatch(e)}),this.bindPersistedSetting({control:this.translateAPIErrorsCheckbox,event:`change`,apply:e=>{this.data.translateAPIErrors=e},storageKey:`translateAPIErrors`,readPersistedValue:()=>this.data.translateAPIErrors,logLabel:`translateAPIErrors`}),this.bindPersistedSetting({control:this.useNewAudioPlayerCheckbox,event:`change`,apply:e=>{this.data.newAudioPlayer=e,this.onlyBypassMediaCSPCheckbox.disabled=this.onlyBypassMediaCSPCheckbox.hidden=!e},storageKey:`newAudioPlayer`,readPersistedValue:()=>this.data.newAudioPlayer,logLabel:`newAudioPlayer`,dispatch:e=>this.events[`change:useNewAudioPlayer`].dispatch(e)}),this.bindPersistedSetting({control:this.onlyBypassMediaCSPCheckbox,event:`change`,apply:e=>{this.data.onlyBypassMediaCSP=e},storageKey:`onlyBypassMediaCSP`,readPersistedValue:()=>this.data.onlyBypassMediaCSP,logLabel:`onlyBypassMediaCSP`,dispatch:e=>this.events[`change:onlyBypassMediaCSP`].dispatch(e)}),this.bindPersistedSetting({control:this.translationTextServiceSelect,event:`selectItem`,apply:e=>{this.data.translationService=e},storageKey:`translationService`,readPersistedValue:()=>this.data.translationService,logLabel:`translationService`,dispatch:e=>this.events[`select:translationTextService`].dispatch(e)}),this.bindPersistedSetting({control:this.detectServiceSelect,event:`selectItem`,apply:e=>{this.data.detectService=e},storageKey:`detectService`,readPersistedValue:()=>this.data.detectService,logLabel:`detectService`}),this.bindPersistedSetting({control:this.showPiPButtonCheckbox,event:`change`,apply:e=>{this.data.showPiPButton=e},storageKey:`showPiPButton`,readPersistedValue:()=>this.data.showPiPButton,logLabel:`showPiPButton`,dispatch:e=>this.events[`change:showPiPButton`].dispatch(e)}),this.autoHideButtonDelaySlider.addEventListener(`input`,e=>{this.autoHideButtonDelaySliderLabel.value=e;let t=Math.round(e*1e3);L.log(`autoHideButtonDelay value changed. New value:`,t),this.data.autoHideButtonDelay=t,this.scheduleStoragePersist(`autoHideButtonDelay`,this.data.autoHideButtonDelay),this.events[`input:autoHideButtonDelay`].dispatch(e)}),this.bindPersistedSetting({control:this.buttonPositionSelect,event:`selectItem`,apply:e=>{this.data.buttonPos=e},storageKey:`buttonPos`,readPersistedValue:()=>this.data.buttonPos,logLabel:`buttonPos`,dispatch:e=>this.events[`select:buttonPosition`].dispatch(e)}),this.menuLanguageSelect.addEventListener(`selectItem`,async e=>{await V.changeLang(e)&&(this.data.localeUpdatedAt=await B.get(`localeUpdatedAt`,0),this.events[`select:menuLanguage`].dispatch(e))}),this.bugReportButton.addEventListener(`click`,()=>this.events[`click:bugReport`].dispatch()),this.resetSettingsButton.addEventListener(`click`,()=>this.events[`click:resetSettings`].dispatch()),this}addEventListener(e,t){return this.events[e].addListener(t),this}removeEventListener(e,t){return this.events[e].removeListener(t),this}doReleaseUI(){this.dialog?.remove();for(let e of[this.accountButtonRefreshTooltip,this.accountButtonTokenTooltip,this.audioBoosterTooltip,this.useLivelyVoiceTooltip,this.useAudioDownloadCheckboxTooltip,this.useNewAudioPlayerTooltip,this.onlyBypassMediaCSPTooltip,this.translationTextServiceTooltip,this.proxyTranslationStatusSelectTooltip,this.buttonPositionTooltip])e?.release()}doReleaseUIEvents(){this.accountStorageListenerCleanup?.(),this.accountStorageListenerCleanup=void 0,globalThis.removeEventListener(`message`,this.onAuthRefreshMessage),this.flushStoragePersists();for(let e of Object.values(this.events))e.clear()}release(){return this.isInitialized()?(this.doReleaseUIEvents(),this.doReleaseUI(),this.initialized=!1,this):this}updateAccountInfo(){if(!this.isInitialized())throw Error(`[VOT] SettingsView isn't initialized`);let e=!!this.data.account?.token;return this.accountButton.avatarId=this.data.account?.avatarId,this.useLivelyVoiceTooltip.hidden=this.accountButton.loggedIn=e,this.accountButton.username=this.data.account?.username,this.useLivelyVoiceCheckbox.disabled=!e,this.events[`update:account`].dispatch(this.data.account),this}open(){if(!this.isInitialized())throw Error(`[VOT] SettingsView isn't initialized`);return this.dialog.open()}close(){if(!this.isInitialized())throw Error(`[VOT] SettingsView isn't initialized`);return this.dialog.close()}},vm=class{mount;initialized=!1;videoHandler;intervalIdleChecker;data;votGlobalPortal;votOverlayView;votSettingsView;constructor({mount:e,data:t={},videoHandler:n,intervalIdleChecker:r}){this.mount=e,this.videoHandler=n,this.data=t,this.intervalIdleChecker=r}get root(){return this.mount.root}get portalContainer(){return this.mount.portalContainer}get tooltipLayoutRoot(){return this.mount.tooltipLayoutRoot}isInitialized(){return this.initialized}initUI(){if(this.isInitialized())throw Error(`[VOT] UIManager is already initialized`);return this.initialized=!0,this.votGlobalPortal=J.createPortal(),this.getGlobalPortalHost(this.mount).appendChild(this.votGlobalPortal),this.votOverlayView=new Jp({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 _m({globalPortal:this.votGlobalPortal,data:this.data,videoHandler:this.videoHandler}),this.votSettingsView.initUI(),this}updateMount(e){let t=this.getGlobalPortalHost(e);return this.votGlobalPortal?.parentElement!==t&&t.appendChild(this.votGlobalPortal),this.mount=gp(this.mount,e,e=>{this.votOverlayView?.updateMount(e)}),this.videoHandler?.subtitlesWidget?.updateMount({container:e.subtitlesMountContainer,tooltipLayoutRoot:e.tooltipLayoutRoot}),this}getGlobalPortalHost(e){let t=document;return Po(t.fullscreenElement??t.webkitFullscreenElement,[e.root],{allowDocumentViewport:!0})?e.root:document.documentElement}initUIEvents(){if(!this.isInitialized())throw Error(`[VOT] UIManager isn't initialized`);this.votOverlayView.initUIEvents(),this.bindOverlayViewEvents(),this.votSettingsView.initUIEvents(),this.bindSettingsViewEvents()}bindOverlayViewEvents(){let e=this.votOverlayView;e&&e.addEventListener(`click:translate`,async()=>{await this.handleTranslationBtnClick()}).addEventListener(`click:pip`,async()=>{if(this.videoHandler)try{await(this.videoHandler.video===document.pictureInPictureElement?document.exitPictureInPicture():this.videoHandler.video.requestPictureInPicture())}catch(e){L.warn(`[VOT] Failed to toggle Picture-in-Picture`,e)}}).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`,e=>{if(!this.videoHandler)return;let t=e/100;if(this.videoHandler.setVideoVolume(t),this.videoHandler.applyManualVideoVolumeOverride(t),!this.data.syncVolume){this.videoHandler.onVideoVolumeSliderSynced(e);return}this.videoHandler.syncVolumeWrapper(`video`,e)}).addEventListener(`input:translationVolume`,e=>{if(!this.videoHandler)return;let t=e??this.data.defaultVolume??100;if(this.videoHandler.audioPlayer.player.volume=t/100,!this.data.syncVolume){this.videoHandler.onTranslationVolumeSliderSynced(t);return}this.videoHandler.syncVolumeWrapper(`translation`,t)}).addEventListener(`select:subtitles`,e=>{this.videoHandler&&this.runDetached(this.videoHandler.changeSubtitlesLang(e),`Failed to change subtitles language`)})}bindSettingsViewEvents(){let e=this.votSettingsView;e&&e.addEventListener(`update:account`,async e=>{this.videoHandler&&(this.videoHandler.votClient.apiToken=e?.token)}).addEventListener(`change:autoTranslate`,async e=>{let t=this.videoHandler;e&&t&&!t.hasActiveSource()&&await this.handleTranslationBtnClick()}).addEventListener(`change:autoSubtitles`,async e=>{!e||!this.videoHandler?.videoData?.videoId||await this.videoHandler.enableSubtitlesForCurrentLangPair()}).addEventListener(`select:responseLanguageSubtitles`,async()=>{!this.videoHandler?.data.autoSubtitles||!this.videoHandler.videoData||await this.videoHandler.enableSubtitlesForCurrentLangPair()}).addEventListener(`change:showVideoVolume`,()=>{this.withInitializedOverlayView(e=>{!e.videoVolumeSlider||!e.votButton||(e.videoVolumeSlider.container.hidden=!this.data.showVideoSlider||e.votButton.status!==`success`)})}).addEventListener(`change:audioBooster`,async()=>{this.withInitializedOverlayView(e=>{if(!e.translationVolumeSlider)return;let t=e.translationVolumeSlider.value,n=this.data.audioBooster&&!this.data.syncVolume?900:100;e.translationVolumeSlider.max=n;let r=R(t,0,n);e.translationVolumeSlider.value=r,this.videoHandler?.onTranslationVolumeSliderSynced(r)})}).addEventListener(`change:syncVolume`,e=>{this.videoHandler&&(this.videoHandler.setupAudioSettings(),this.withInitializedOverlayView(t=>{let n=t.videoVolumeSlider,r=t.translationVolumeSlider;if(!n||!r)return;let i=this.data.audioBooster&&!e?900:100;r.max=i;let a=R(r.value,0,i);r.value=a,this.videoHandler.onTranslationVolumeSliderSynced(a),e&&this.videoHandler.resetVolumeLinkState(Number(n.value),a)}))}).addEventListener(`change:useLivelyVoice`,()=>{this.videoHandler&&this.runDetached(this.videoHandler.stopTranslate(),`Failed to stop translation after voice mode change`)}).addEventListener(`change:subtitlesHighlightWords`,e=>{this.updateSubtitlesWidgetSetting(e,this.data.highlightWords,(e,t)=>{e.setHighlightWords(t)})}).addEventListener(`change:subtitlesSmartLayout`,e=>{this.updateSubtitlesWidgetSetting(e,this.data.subtitlesSmartLayout,(e,t)=>{e.setSmartLayout(t)})}).addEventListener(`input:subtitlesMaxLength`,e=>{this.updateSubtitlesWidgetSetting(e,this.data.subtitlesMaxLength,(e,t)=>{e.setMaxLength(t)})}).addEventListener(`input:subtitlesFontSize`,e=>{this.updateSubtitlesWidgetSetting(e,this.data.subtitlesFontSize,(e,t)=>{e.setFontSize(t)})}).addEventListener(`select:subtitlesFontFamily`,e=>{this.updateSubtitlesWidgetSetting(e,this.data.subtitlesFontFamily,(e,t)=>{e.setFontFamily(t)})}).addEventListener(`input:subtitlesBackgroundOpacity`,e=>{this.updateSubtitlesWidgetSetting(e,this.data.subtitlesOpacity,(e,t)=>{e.setOpacity(t)})}).addEventListener(`change:proxyWorkerHost`,e=>{this.videoHandler&&this.runDetached(this.videoHandler.handleProxySettingsChanged(`proxyWorkerHost`),`Failed to apply proxyWorkerHost change`)}).addEventListener(`select:proxyTranslationStatus`,()=>{this.videoHandler&&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(e=>{e.resetTranslationContext(!0)})}).addEventListener(`change:showPiPButton`,()=>{this.withInitializedOverlayView(e=>{e.votButton&&(e.votButton.pipButton.hidden=e.votButton.separator2.hidden=!e.pipButtonVisible)})}).addEventListener(`select:buttonPosition`,e=>{this.withInitializedOverlayView(t=>{let n=this.data.buttonPos??e,{position:r,direction:i}=t.calcButtonLayout(n);t.updateButtonLayout(r,i)})}).addEventListener(`select:menuLanguage`,async()=>{await this.reloadMenu()}).addEventListener(`click:bugReport`,()=>{if(!this.videoHandler)return;let e=new URLSearchParams(this.videoHandler.collectReportInfo()).toString();globalThis.open(`${xi}/issues/new?${e}`,`_blank`)?.focus()}).addEventListener(`click:resetSettings`,async()=>{let e=await B.list();await Promise.all(e.map(e=>B.delete(e))),await B.set(`compatVersion`,Ei),globalThis.location.reload()})}async handleDownloadTranslationClick(){let e=this.votOverlayView,t=this.videoHandler;if(!e?.isInitialized()||!t?.downloadTranslationUrl||!t.videoData)return;let n=e.downloadTranslationButton,r=t.downloadTranslationUrl,i=this.data.downloadWithName?Zi(t.videoData.downloadTitle):`translation_${t.videoData.videoId}`,a={preferShare:this.isLikelyMobileDownloadContext()},o=e=>{n&&(n.progress=e)};o(0);try{await this.downloadTranslationAudio(r,i,o,a)}catch(e){console.error(`[VOT] Download translation failed:`,e),this.triggerUrlDownload(r,`${i}.mp3`)||globalThis.open(r,`_blank`)?.focus()}finally{o(0)}}async downloadTranslationAudio(e,t,n,r){let i=await z(e,{timeout:0});if(!i.ok)throw Error(`HTTP ${i.status}`);await pp(i,t,n,r)}async handleDownloadSubtitlesClick(){let e=this.videoHandler;if(!e?.yandexSubtitles||!e.videoData)return;let t=this.data.subtitlesDownloadFormat??`json`,n=op(e.yandexSubtitles,t,{assTitle:e.videoData.localizedTitle??e.videoData.title??e.videoData.downloadTitle});await Yi(new Blob([t===`json`?JSON.stringify(n):n],{type:`text/plain`}),`${this.data.downloadWithName?Zi(e.videoData.downloadTitle):`subtitles_${e.videoData.videoId}`}.${t}`,{preferShare:this.isLikelyMobileDownloadContext()})}async reloadMenu(){if(!this.votOverlayView?.isInitialized())throw Error(`[VOT] OverlayView isn't initialized`);let e=this.votOverlayView.votButton.opacity,t=this.votOverlayView.votButton.container.hidden,n=this.votOverlayView.votMenu.hidden,r=this.data.buttonPos??`default`,i=this.votSettingsView?.dialog?.container?.hidden===!1;if(await this.videoHandler?.stopTranslation(),this.release(),this.initUI(),this.initUIEvents(),!this.videoHandler)return this;try{let{position:i,direction:a}=this.votOverlayView.calcButtonLayout(r);this.votOverlayView.updateButtonLayout(i,a),this.votOverlayView.votMenu.hidden=n,this.votOverlayView.votButton.container.hidden=t,this.votOverlayView.votButton.opacity=e}catch(e){L.warn(`[VOT] Failed to restore overlay state after menu reload`,e)}try{this.videoHandler.rebindOverlayVisibilityTargets()}catch(e){L.warn(`[VOT] Failed to rebind overlay visibility targets`,e)}if(i)try{this.votSettingsView?.open()}catch(e){L.warn(`[VOT] Failed to reopen settings after menu reload`,e)}await this.videoHandler.updateSubtitlesLangSelect();let a=this.videoHandler.subtitlesWidget;return a&&a.resetTranslationContext(!0),this}async handleTranslationBtnClick(){if(!this.votOverlayView?.isInitialized())throw Error(`[VOT] OverlayView isn't initialized`);return await xp({videoHandler:this.videoHandler,currentStatus:this.votOverlayView.votButton.status,currentLoading:this.votOverlayView.votButton.loading,transformBtn:(e,t)=>{this.transformBtn(e,t)}}),this}isLoadingText(e){let t=V.get(`TranslationDelayed`);return typeof e==`string`&&(e.includes(V.get(`translationTake`))||(t?e.includes(t):!1))}transformBtn(e,t){if(!this.votOverlayView?.isInitialized())throw Error(`[VOT] OverlayView isn't initialized`);return this.votOverlayView.votButton.status=e,this.votOverlayView.votButton.loading=e===`error`&&this.isLoadingText(t),this.votOverlayView.votButton.setText(t),this.votOverlayView.votButtonTooltip.setContent(t),this}release(){return this.isInitialized()?(this.votOverlayView.release(),this.votSettingsView.release(),this.votGlobalPortal.remove(),this.initialized=!1,this):this}withInitializedOverlayView(e){this.votOverlayView?.isInitialized()&&e(this.votOverlayView)}withSubtitlesWidget(e){let t=this.videoHandler?.subtitlesWidget;t&&e(t)}updateSubtitlesWidgetSetting(e,t,n){this.withSubtitlesWidget(r=>{n(r,t??e)})}runDetached(e,t){e.catch(e=>{L.warn(`[VOT] ${t}`,e)})}triggerUrlDownload(e,t){try{let n=document.createElement(`a`);return n.href=e,n.download=t,n.target=`_blank`,n.rel=`noopener noreferrer`,n.style.display=`none`,document.body.appendChild(n),n.click(),n.remove(),!0}catch{return!1}}isLikelyMobileDownloadContext(){return this.videoHandler?.site.additionalData===`mobile`?!0:typeof matchMedia==`function`&&matchMedia(`(pointer: coarse)`).matches}restartAudioPlayer(){this.restartAudioPlayerSafely()}async restartAudioPlayerSafely(){let e=this.videoHandler;if(e)try{await e.stopTranslate(),e.createPlayer()}catch(e){L.warn(`[VOT] Failed to restart audio player`,e)}}},ym=class{deps;hideDeadlineMs=0;hideArmed=!1;unsubscribeChecker;constructor(e){this.deps=e,this.unsubscribeChecker=this.deps.checker.subscribe(()=>{this.onCheckerTick()})}show(){let e=this.getView();return e?(e.updateButtonOpacity(1),e):null}cancel(){this.hideDeadlineMs=0,this.hideArmed=!1}release(){this.cancel(),this.unsubscribeChecker()}queueAutoHide(){if(!this.show())return;let e=this.deps.getAutoHideDelay();this.hideDeadlineMs=this.nowMs()+Math.max(0,e),this.hideArmed=!0,this.deps.checker.markActivity(`overlay-queue-hide`),this.deps.checker.requestImmediateTick()}handleOverlayInteraction(e){let t=e?.type;if(t){if(t===`focusin`){this.handleFocusIn();return}if(t.startsWith(`pointer`)){this.cancel(),this.show(),this.deps.checker.markActivity(`overlay-interaction`),e.stopPropagation?.();return}this.handleHostInteraction(e)}}handleHostInteraction(e){let t=e?.type;if(t){if(t===`focusin`){this.handleFocusIn();return}if(t.startsWith(`pointer`)){let t=e.target;this.deps.isInteractiveNode(t)&&e.stopPropagation?.(),this.deps.checker.markActivity(`overlay-host-pointer`)}this.queueAutoHide()}}scheduleHide(e){if(!this.getView())return;let t=e?.currentTarget,n=e?.relatedTarget??null;!n&&typeof e?.composedPath==`function`&&(n=e.composedPath()[1]??null);let r=n instanceof Node?n:null,i=t instanceof Node?t:null;r&&(i?.contains(r)||this.deps.isInteractiveNode(r))||this.queueAutoHide()}onCheckerTick(){if(!this.hideArmed||this.hideDeadlineMs<=0||this.nowMs()+2typeof performance<`u`&&typeof performance.now==`function`?performance.now():Date.now(),setInterval:globalThis.setInterval.bind(globalThis),clearInterval:globalThis.clearInterval.bind(globalThis),queueMicrotask:e=>{if(typeof globalThis.queueMicrotask==`function`){globalThis.queueMicrotask(e);return}Promise.resolve().then(e)},onVisibilityChange:e=>typeof document>`u`||typeof document.addEventListener!=`function`?()=>void 0:(document.addEventListener(`visibilitychange`,e),()=>{typeof document.removeEventListener==`function`&&document.removeEventListener(`visibilitychange`,e)})}}var Tm=class{profile;runtime;subscribers=new Set;intervalId=null;unsubscribeVisibilityChange=null;running=!1;destroyed=!1;immediateQueued=!1;currentMode=`active`;lastActivityAt;onVisibilityChangeHandler=()=>{this.destroyed||!this.running||(em()?this.clearIntervalTimer():this.armInterval(),this.requestImmediateTick())};constructor(e={}){this.profile=Cm(e.profile),this.runtime={...wm(),...e.runtime},this.lastActivityAt=this.runtime.nowMs()}start(){this.destroyed||this.running||(this.running=!0,this.lastActivityAt=this.runtime.nowMs(),this.subscribeVisibilityChange(),this.armInterval(),this.runTick(`start`))}stop(){this.running&&(this.running=!1,this.clearIntervalTimer(),this.immediateQueued=!1,this.unsubscribeFromVisibilityChange())}destroy(){this.destroyed||=(this.stop(),this.subscribers.clear(),!0)}subscribe(e){return this.destroyed?()=>void 0:(this.subscribers.add(e),()=>{this.subscribers.delete(e)})}markActivity(e){if(this.destroyed||(this.lastActivityAt=this.runtime.nowMs(),!this.running))return;let t=this.resolveMode(this.lastActivityAt);t!==this.currentMode&&(this.currentMode=t)}requestImmediateTick(){this.destroyed||!this.running||this.immediateQueued||(this.immediateQueued=!0,this.runtime.queueMicrotask(()=>{this.immediateQueued=!1,!(this.destroyed||!this.running)&&this.runTick(`immediate`)}))}resolveMode(e){return em()?`hidden`:e-this.lastActivityAt>=this.profile.idleAfterMs?`idle`:`active`}clearIntervalTimer(){this.intervalId!==null&&(this.runtime.clearInterval(this.intervalId),this.intervalId=null)}armInterval(){this.intervalId===null&&(this.intervalId=this.runtime.setInterval(()=>{this.runTick(`interval`)},this.profile.checkIntervalMs))}runTick(e){if(this.destroyed||!this.running||this.subscribers.size===0)return;let t=this.runtime.nowMs(),n=this.resolveMode(t);n!==this.currentMode&&(this.currentMode=n);let r={nowMs:t,mode:n,source:e};for(let e of this.subscribers)try{e(r)}catch{}}subscribeVisibilityChange(){this.unsubscribeVisibilityChange===null&&(this.unsubscribeVisibilityChange=this.runtime.onVisibilityChange(this.onVisibilityChangeHandler))}unsubscribeFromVisibilityChange(){this.unsubscribeVisibilityChange!==null&&(this.unsubscribeVisibilityChange(),this.unsubscribeVisibilityChange=null)}};function Em(e){return new Tm({profile:e})}var Dm=()=>Date.now();function Om(){return GM_info?.script?.name||`VOT`}function km(e,t){try{return V?.get?.(e)||t}catch{return t}}function Am(e,t,n){if(!n)return!0;let r=e.get(t)??0;return Dm()-r>=n}function jm(e,t){e.set(t,Dm())}function Mm(e){let t=e.trim();if(!t)return null;try{let e=V.get(t),n=V.getDefault(t);return e!==t||n!==t?e||n||t:null}catch{return null}}function Nm(e){if(!e||typeof e!=`object`)return null;let t=e;return t.name===`VOTLocalizedError`?typeof t.localizedMessage==`string`&&t.localizedMessage.trim()?t.localizedMessage:typeof t.unlocalizedMessage==`string`?Mm(t.unlocalizedMessage):null:null}function Pm(e){let t=xa(e);return t?Mm(t)||t:null}function Fm(e){let t=Nm(e);if(t)return t;if(typeof e==`string`){let t=Mm(e);if(t)return t}return Pm(e)||km(`requestTranslationFailed`,`Translation failed`)}function Im(e){try{if(typeof GM_notification==`function`)return GM_notification(e),!0;let t=globalThis.GM;if(t!==void 0&&typeof t.notification==`function`){let n={text:e.text,title:e.title,image:e.image,onclick:e.onclick,ondone:e.ondone};return t.notification(n),!0}}catch(e){L.log(`[notify] userscript api error`,e)}return!1}var Lm=class{lastSentAt=new Map;send(e,t={}){try{let n=t.key||e.tag||`${e.title??``}|${e.text??``}`,r=t.cooldownMs??0;if(!Am(this.lastSentAt,n,r))return;let i={...e,title:e.title??Om()};Im(i)?jm(this.lastSentAt,n):L.log(`[notify] unavailable`,i)}catch(e){L.log(`[notify] send error`,e)}}translationCompleted(e){let t=km(`VOTTranslationCompletedNotify`,`The translation on the {0} has been completed!`).replace(`{0}`,e);this.send({text:t,title:Om(),timeout:5e3,silent:!0,tag:`VOTTranslationCompleted`,onclick:()=>{try{globalThis.focus()}catch{}}},{key:`translation_completed_${e}`,cooldownMs:1e4})}translationFailed(e){let{videoId:t,message:n}=e;if(Sa(n))return;let r=Fm(n),i=Om();this.send({text:r,title:i,timeout:8e3,silent:!0,tag:`VOTtranslationFailed_${t||`unknown`}`,onclick:()=>{try{globalThis.focus()}catch{}}},{key:`translation_failed_${t||`unknown`}`,cooldownMs:3e4})}},Rm=[`class`,`id`,`title`],zm=new RegExp([`advertise`,`advertisement`,`promo`,`sponsor`,`banner`,`commercial`,`preroll`,`midroll`,`postroll`,`ad-container`,`sponsored`].map(e=>e.replaceAll(/[.*+?^${}()|[\]\\]/g,String.raw`\$&`)).join(`|`)),Bm=Symbol.for(`vot.attachShadowHook`);function Vm(){let e=Object.getOwnPropertyDescriptor(Element.prototype,`attachShadow`);return!e||typeof e.value!=`function`?null:e}function Hm(){let e=globalThis,t=e[Bm];if(t?.descriptor&&t.subscribers instanceof Set)return t;let n=Vm();if(!n)return null;let r=n.value,i={descriptor:n,subscribers:new Set},a=function(e){let t=r.call(this,e);for(let e of i.subscribers)try{e(t)}catch(e){L.error(`attachShadow subscriber failed`,e)}return t};try{Object.defineProperty(Element.prototype,`attachShadow`,{...n,value:a})}catch{return null}return e[Bm]=i,i}function Um(e){let t=globalThis,n=t[Bm];if(n&&(n.subscribers.delete(e),!(n.subscribers.size>0))){try{Object.defineProperty(Element.prototype,`attachShadow`,n.descriptor)}catch{let e=n.descriptor.value;typeof e==`function`&&(Element.prototype.attachShadow=e)}delete t[Bm]}}var Wm=class e{seenVideos=new WeakSet;activeVideos=new WeakSet;observedRoots=new WeakSet;videoListenerControllers=new Map;pendingAdded=new Set;pendingRemoved=new Set;flushPending=!1;static MAX_FLUSH_BUDGET_MS=6;static MAX_NODES_PER_SLICE=120;onVideoAdded=new H;onVideoRemoved=new H;observer=new MutationObserver(e=>this.onMutations(e));intervalIdleChecker;checkerUnsubscribe=null;enabled=!1;attachShadowSubscriber=null;onDocumentReady=null;onPageShow=()=>{let e=document.documentElement;e&&(this.pendingAdded.add(e),this.scheduleFlush())};constructor(e=Em()){this.intervalIdleChecker=e}static containsAdKeyword(e){return e.length>0&&zm.test(e)}isAdRelated(t){for(let n of Rm){let r=t.getAttribute(n);if(r&&e.containsAdKeyword(r.toLowerCase()))return!0}return!1}isInsideAd(e){for(let t=e.parentElement;t;t=t.parentElement)if(this.isAdRelated(t))return!0;return!1}getCapturedAudioTrackCount(e){let t=e,n=t.captureStream??t.mozCaptureStream;if(typeof n!=`function`)return null;try{return n.call(e).getAudioTracks().length}catch{return null}}isLikelySilentDecorativeVideo(e){if(!(e.muted||e.defaultMuted)||!e.autoplay||!e.loop||e.controls)return!1;let t=e;if(typeof t.mozHasAudio==`boolean`)return!t.mozHasAudio;if(`audioTracks`in t&&typeof t.audioTracks?.length==`number`){if(t.audioTracks.length>0)return!1;let n=this.getCapturedAudioTrackCount(e);return n===null?!0:n===0}let n=this.getCapturedAudioTrackCount(e);return n===null?!1:n===0}hasAudio(e){let t=e;return e.srcObject instanceof MediaStream?e.srcObject.getAudioTracks().length>0:typeof t.mozHasAudio==`boolean`?t.mozHasAudio:typeof t.webkitAudioDecodedByteCount==`number`&&t.webkitAudioDecodedByteCount>0||`audioTracks`in t&&typeof t.audioTracks?.length==`number`&&t.audioTracks.length>0?!0:!this.isLikelySilentDecorativeVideo(e)}isValidVideo(e){return this.isAdRelated(e)||this.isInsideAd(e)?!1:this.hasAudio(e)?!0:(L.log(`Ignoring video without audio:`,e),!1)}observeRoot(e){this.observedRoots.has(e)||(this.observedRoots.add(e),this.observer.observe(e,{childList:!0,subtree:!0}))}scan(e){if(e instanceof HTMLVideoElement){this.trackVideo(e);return}if(e.nodeType!==Node.ELEMENT_NODE&&e.nodeType!==Node.DOCUMENT_FRAGMENT_NODE&&e.nodeType!==Node.DOCUMENT_NODE)return;let t=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT,{acceptNode:e=>{let t=e,n=t.tagName===`VIDEO`,r=!!t.shadowRoot;return n||r?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP}});for(;t.nextNode();){let e=t.currentNode;if(e instanceof HTMLVideoElement){this.trackVideo(e);continue}let n=e.shadowRoot;n&&(this.observeRoot(n),this.scan(n))}}getVideoListenerSignal(e){let t=this.videoListenerControllers.get(e);t&&t.abort();let n=new AbortController;return this.videoListenerControllers.set(e,n),n.signal}cleanupVideoListeners(e){let t=this.videoListenerControllers.get(e);t&&(t.abort(),this.videoListenerControllers.delete(e))}cleanupAllVideoListeners(){for(let e of this.videoListenerControllers.values())e.abort();this.videoListenerControllers.clear()}trackVideo(e){if(this.seenVideos.has(e))return;this.seenVideos.add(e);let t=this.getVideoListenerSignal(e),n=()=>{this.isValidVideo(e)&&(this.activeVideos.has(e)||(this.activeVideos.add(e),this.onVideoAdded.dispatch(e)))};e.readyState>=HTMLMediaElement.HAVE_CURRENT_DATA?n():(e.addEventListener(`loadeddata`,n,{once:!0,signal:t}),e.addEventListener(`play`,()=>{e.readyState>=HTMLMediaElement.HAVE_CURRENT_DATA&&n()},{once:!0,passive:!0,signal:t})),e.addEventListener(`emptied`,()=>{e.isConnected||this.untrackVideo(e)},{passive:!0,signal:t})}untrackVideo(e){this.cleanupVideoListeners(e),this.activeVideos.has(e)&&(this.onVideoRemoved.dispatch(e),this.activeVideos.delete(e)),this.seenVideos.delete(e)}collectVideos(e){let t=new Set,n=e=>{for(let n of e)t.add(n)};if(e instanceof HTMLVideoElement&&t.add(e),(e instanceof Document||e instanceof DocumentFragment||e instanceof Element)&&n(e.querySelectorAll(`video`)),e instanceof Element){let t=e.shadowRoot;t&&n(t.querySelectorAll(`video`))}return Array.from(t)}getNowMs(){return typeof performance<`u`&&typeof performance.now==`function`?performance.now():Date.now()}isSliceBudgetReached(t,n){return n>=e.MAX_NODES_PER_SLICE?!0:this.getNowMs()-t>=e.MAX_FLUSH_BUDGET_MS}processPendingAdded(e){let t=0;for(;this.pendingAdded.size>0;){let n=this.pendingAdded.values().next();if(n.done||(this.pendingAdded.delete(n.value),this.scan(n.value),t+=1,this.isSliceBudgetReached(e,t)))break}return t}processPendingRemoved(e,t){let n=t;for(;this.pendingRemoved.size>0&&!this.isSliceBudgetReached(e,n);){let e=this.pendingRemoved.values().next();if(e.done)break;this.pendingRemoved.delete(e.value);for(let t of this.collectVideos(e.value))t.isConnected||this.untrackVideo(t);n+=1}return n}flushSlice=()=>{if(!this.enabled){this.pendingAdded.clear(),this.pendingRemoved.clear(),this.flushPending=!1;return}let e=this.getNowMs(),t=this.processPendingAdded(e);this.processPendingRemoved(e,t),this.flushPending=this.pendingAdded.size>0||this.pendingRemoved.size>0,this.flushPending&&this.intervalIdleChecker.requestImmediateTick()};onCheckerTick=()=>{this.flushPending&&this.flushSlice()};scheduleFlush=()=>{this.enabled&&(this.flushPending=!0,this.intervalIdleChecker.requestImmediateTick())};installAttachShadowHook(){if(this.attachShadowSubscriber)return;let e=Hm();if(!e)return;let t=e=>{this.enabled&&(this.observeRoot(e),this.pendingAdded.add(e),this.scheduleFlush())};e.subscribers.add(t),this.attachShadowSubscriber=t}uninstallAttachShadowHook(){this.attachShadowSubscriber&&=(Um(this.attachShadowSubscriber),null)}enqueueAddedNode(e){if(e.nodeType===Node.ELEMENT_NODE){let t=e.shadowRoot;t&&this.observeRoot(t)}this.pendingAdded.add(e)}enqueueMutation(e){for(let t of e.addedNodes)this.enqueueAddedNode(t);for(let t of e.removedNodes)this.pendingRemoved.add(t)}onMutations(e){for(let t of e)t.type===`childList`&&this.enqueueMutation(t);(this.pendingAdded.size>0||this.pendingRemoved.size>0)&&this.scheduleFlush()}enable(){if(this.enabled)return;this.enabled=!0,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:!0});let e=document.documentElement;if(e){this.observeRoot(e),this.scan(e);return}let t=()=>{let e=document.documentElement;e&&(document.removeEventListener(`readystatechange`,t),this.onDocumentReady=null,this.enabled&&(this.observeRoot(e),this.scan(e)))};this.onDocumentReady=t,document.addEventListener(`readystatechange`,t),typeof queueMicrotask==`function`?queueMicrotask(t):Promise.resolve().then(t)}disable(){this.enabled&&(this.enabled=!1,globalThis.removeEventListener(`pageshow`,this.onPageShow),this.onDocumentReady&&=(document.removeEventListener(`readystatechange`,this.onDocumentReady),null),this.uninstallAttachShadowHook(),this.observer.disconnect(),this.cleanupAllVideoListeners(),this.flushPending=!1,this.checkerUnsubscribe?.(),this.checkerUnsubscribe=null,this.intervalIdleChecker.stop(),this.pendingAdded.clear(),this.pendingRemoved.clear(),this.seenVideos=new WeakSet,this.activeVideos=new WeakSet,this.observedRoots=new WeakSet)}};function Gm(e,t){e.lastVideoPercent=W(t)}function Km(e,t){e.lastTranslationPercent=W(t)}function qm(e,t){let n=W(e);return{min:n,max:W(Math.min(100,t),n,100)}}function Jm({state:e,fromType:t,newVolume:n,currentVideo:r,currentTranslation:i,translationMin:a,translationMax:o}){let s=qm(a,o);if(e.initialized||=(e.lastVideoPercent=W(r),e.lastTranslationPercent=Dc(Number(i),s.min,s.max),!0),t===`video`){let t=W(n),r=t-W(e.lastVideoPercent);e.lastVideoPercent=t;let i=Dc(e.lastTranslationPercent+r,s.min,s.max);return e.lastTranslationPercent=i,{nextTranslation:i}}let c=Dc(Number.isFinite(n)?n:i,s.min,s.max),l=c-e.lastTranslationPercent;e.lastTranslationPercent=c;let u=W(e.lastVideoPercent+l);return e.lastVideoPercent=u,{nextVideo:u}}var Ym={allowTouchMoveHandler:!0,disableContainerDrag:!1},Xm={xvideos:{allowTouchMoveHandler:!1},youtube:{disableContainerDrag:!0}};function Zm(e){if(!e)return Ym;let t=Xm[e]??{};return{...Ym,...t}}var Qm=0,$m=1,eh=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:wc,applyDeltaThreshold01:wc/2});function th(e){return{isDucked:!1,speechGateOpen:!1,rmsEnvelope:0,baseline:uh(e),lastApplied:void 0,lastTickAt:0,lastSoundAt:0,rmsMissingSinceAt:null}}function nh(){return th()}function rh(e,t,n,r,i){let a=t.speechGateOpen;return e.smartEnabled?e.audioIsPlaying&&!i?(t.rmsMissingSinceAt??=r,a&&(t.lastSoundAt=r),t.rmsMissingSinceAt!==null&&r-t.rmsMissingSinceAt>=n.rmsMissingGraceMs?(t.lastSoundAt=r,!0):a):(t.rmsMissingSinceAt=null,e.audioIsPlaying&&(!a&&t.rmsEnvelope>=n.thresholdOnRms||a&&t.rmsEnvelope>=n.thresholdOffRms)?(t.lastSoundAt=r,!0):a&&r-t.lastSoundAt<=n.holdMs):(t.lastSoundAt=r,t.rmsMissingSinceAt=null,!0)}function ih(e,t,n,r){e.isDucked&&dh(e.lastApplied)&&Math.abs(t-e.lastApplied)>r.externalBaselineDelta01&&(e.baseline=t),e.isDucked||(e.baseline=t);let i=e.baseline??n??t;return e.baseline=i,i}function ah(e,t,n,r,i,a){let o=Math.min(r,i);return t?(e.isDucked=!0,o):(e.isDucked&&Math.abs(r-n)0?-Math.expm1(-n/a):1,s=t+(e-t)*o,c=(e0&&(s=R(s,t-c,t+c)),R(s,Qm,$m)}function sh(e,t,n,r){return Math.abs(n-t)=r?(e.lastApplied=n,{kind:`apply`,runtime:e,volume01:n}):{kind:`noop`,runtime:e}}function ch(e,t,n=eh){let r=lh(t),i=uh(e.volumeOnStart);if(!e.translationActive||!e.enabledAutoVolume)return{kind:`stop`,runtime:r,restoreVolume:r.baseline??i};let a=Number.isFinite(e.nowMs)?e.nowMs:Date.now(),o=R(a-(r.lastTickAt||a),0,n.maxDtMs),s=o/1e3;r.lastTickAt=a;let c=dh(e.rms),l=c?R(e.rms,Qm,$m):0,u=r.rmsEnvelope,d=l>u?n.rmsAttackTauMs:n.rmsReleaseTauMs,f=d>0?-Math.expm1(-o/d):1;r.rmsEnvelope=R(u+(l-u)*f,Qm,$m);let p=rh(e,r,n,a,c);r.speechGateOpen=p;let m=uh(e.currentVideoVolume);if(!dh(m))return{kind:`noop`,runtime:r};let h=ih(r,m,i,n);if(!e.hostVideoActive)return r.lastApplied=m,{kind:`noop`,runtime:r};let g=ah(r,p,m,h,uh(e.duckingTarget01)??h,n),_=Mc(oh(g,m,o,s,n),m,g,n.volumeStep01),v=n.applyDeltaThreshold01;return sh(r,m,_,v)}function lh(e){return{isDucked:!!e.isDucked,speechGateOpen:!!e.speechGateOpen,rmsEnvelope:uh(e.rmsEnvelope)??0,baseline:uh(e.baseline),lastApplied:uh(e.lastApplied),lastTickAt:dh(e.lastTickAt)?e.lastTickAt:0,lastSoundAt:dh(e.lastSoundAt)?e.lastSoundAt:0,rmsMissingSinceAt:dh(e.rmsMissingSinceAt)?e.rmsMissingSinceAt:null}}function uh(e){if(dh(e))return R(e,Qm,$m)}function dh(e){return typeof e==`number`&&Number.isFinite(e)}var fh=eh.tickMs,ph=new WeakMap;function mh(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.connect==`function`&&typeof t.disconnect==`function`}function hh(e){return e?.audio??e?.audioElement}function gh(){return typeof performance<`u`&&typeof performance.now==`function`?performance.now():Date.now()}function _h(e){return e.data?.enabledAutoVolume?e.data?.syncVolume?`classic`:e.data?.enabledSmartDucking??!0?`smart`:`classic`:`off`}function vh(e){return e.audioPlayer?.audioContext??e.audioContext}function yh(e){if(e.connectedInputNode&&e.analyser)try{e.connectedInputNode.disconnect(e.analyser)}catch{}if(e.connectedInputNode=void 0,e.createdMediaSource)try{e.createdMediaSource.disconnect()}catch{}if(e.createdMediaSource=void 0,e.analyser)try{e.analyser.disconnect()}catch{}e.analyser=void 0,e.analyserFloatData=void 0,e.analyserData=void 0,e.mediaElement=void 0,e.audioContext=void 0,e.mediaSourceCreationFailed=!1}function bh(e){let t=ph.get(e);t&&(yh(t),ph.delete(e))}function xh(e,t,n,r){if(mh(e?.gainNode))return e.gainNode;if(mh(e?.audioSource))return e.audioSource;if(mh(e?.mediaElementSource))return e.mediaElementSource;if(!(r.mediaSourceCreationFailed&&r.mediaElement===t&&r.audioContext===n)){if(r.createdMediaSource&&r.mediaElement===t&&r.audioContext===n)return r.createdMediaSource;try{let e=n.createMediaElementSource(t);return r.createdMediaSource=e,r.mediaSourceCreationFailed=!1,e}catch(e){r.mediaSourceCreationFailed=!0,L.log(`[SmartDucking] failed to create media source`,e);return}}}function Sh(e,t,n){let r=vh(e);if(!r)return;let i=ph.get(e);if(i||(i={},ph.set(e,i)),(i.mediaElement&&i.mediaElement!==n||i.audioContext&&i.audioContext!==r)&&yh(i),i.mediaElement=n,i.audioContext=r,!i.analyser){let e=r.createAnalyser();e.fftSize=512,i.analyser=e}let a=xh(t,n,r,i),o=i.analyser;if(!(!a||!o)){if(i.connectedInputNode!==a){if(i.connectedInputNode)try{i.connectedInputNode.disconnect(o)}catch{}try{a.connect(o),i.connectedInputNode=a}catch(e){L.log(`[SmartDucking] failed to connect analyser`,e);return}}return{analyser:o,state:i}}}function Ch(e){return{isDucked:e.smartVolumeIsDucked,speechGateOpen:e.smartVolumeSpeechGateOpen,rmsEnvelope:e.smartVolumeRmsEnvelope,baseline:e.smartVolumeDuckingBaseline,lastApplied:e.smartVolumeLastApplied,lastTickAt:e.smartVolumeLastTickAt,lastSoundAt:e.smartVolumeLastSoundAt,rmsMissingSinceAt:e.smartVolumeRmsMissingSinceAt}}function wh(e,t){e.smartVolumeIsDucked=t.isDucked,e.smartVolumeSpeechGateOpen=t.speechGateOpen,e.smartVolumeRmsEnvelope=t.rmsEnvelope,e.smartVolumeDuckingBaseline=t.baseline,e.smartVolumeLastApplied=t.lastApplied,e.smartVolumeLastTickAt=t.lastTickAt,e.smartVolumeLastSoundAt=t.lastSoundAt,e.smartVolumeRmsMissingSinceAt=t.rmsMissingSinceAt}function Th(e,t={}){let{restoreVolume:n}=t;e.smartVolumeDuckingInterval!==void 0&&(clearTimeout(e.smartVolumeDuckingInterval),e.smartVolumeDuckingInterval=void 0);let r=typeof n==`number`?n:e.smartVolumeDuckingBaseline??e.volumeOnStart;if(typeof r==`number`&&(typeof n==`number`||e.smartVolumeIsDucked))try{e.setVideoVolume(r)}catch{}bh(e),wh(e,nh())}function Eh(e){typeof globalThis>`u`||e.smartVolumeDuckingInterval!==void 0&&(e.smartVolumeDuckingInterval=globalThis.setTimeout(()=>{if(e.smartVolumeDuckingInterval!==void 0){try{kh(e)}catch(t){L.log(`[SmartDucking] tick failed, stopping smart ducking`,t),Th(e);return}e.smartVolumeDuckingInterval!==void 0&&Eh(e)}},fh))}function Dh(e){if(typeof globalThis>`u`||e.smartVolumeDuckingInterval!==void 0||_h(e)!==`smart`)return;let t=e.getVideoVolume(),n=typeof e.smartVolumeDuckingBaseline==`number`?e.smartVolumeDuckingBaseline:t,r=th(n);if(Number.isFinite(t)&&Number.isFinite(n)&&t{},0),clearTimeout(e.smartVolumeDuckingInterval),Eh(e)}function Oh(e,t){let n=e.audioPlayer?.player,r=Sh(e,n,t);if(!r)return;let{analyser:i,state:a}=r;try{if(typeof i.getFloatTimeDomainData==`function`){let e=a.analyserFloatData;e?.length!==i.fftSize&&(e=new Float32Array(i.fftSize),a.analyserFloatData=e),i.getFloatTimeDomainData(e);let t=0;for(let n of e)t+=n*n;return R(Math.sqrt(t/e.length),0,1)}let e=a.analyserData;e?.length!==i.fftSize&&(e=new Uint8Array(i.fftSize),a.analyserData=e),i.getByteTimeDomainData(e);let t=0;for(let n of e){let e=(n-128)/128;t+=e*e}return R(Math.sqrt(t/e.length),0,1)}catch{return}}function kh(e){if(_h(e)!==`smart`){Ah.call(e);return}let t=e.audioPlayer?.player,n=hh(t),r=!!n&&!n.paused&&!n.muted&&(n.volume??1)>.001,i=gh(),a=e.getVideoVolume(),o=e.video,s=!(o&&(o.paused||o.ended)),c=R(e.data?.autoVolume??15,0,100)/100;e.smartVolumeDuckingTarget=c;let l=r&&n?Oh(e,n):0,u=ch({nowMs:i,translationActive:e.hasActiveSource(),enabledAutoVolume:!0,smartEnabled:!0,audioIsPlaying:r,rms:l,currentVideoVolume:a,hostVideoActive:s,duckingTarget01:c,volumeOnStart:e.volumeOnStart},Ch(e),eh);switch(u.kind){case`stop`:Th(e,{restoreVolume:u.restoreVolume});return;case`apply`:e.setVideoVolume(u.volume01),wh(e,u.runtime);return;case`noop`:wh(e,u.runtime);return;default:throw TypeError(`Unhandled smart ducking decision`)}}function Ah(){typeof this.data?.defaultVolume==`number`&&(this.audioPlayer.player.volume=this.data.defaultVolume/100);let e=_h(this);if(e===`off`){Th(this,{restoreVolume:this.smartVolumeDuckingBaseline??this.volumeOnStart});return}let t=R(this.data.autoVolume??15,0,100)/100;if(this.smartVolumeDuckingTarget=t,!this.hasActiveSource())return;if(e===`smart`){Dh(this);return}this.smartVolumeDuckingInterval!==void 0&&(clearTimeout(this.smartVolumeDuckingInterval),this.smartVolumeDuckingInterval=void 0),typeof this.smartVolumeDuckingBaseline!=`number`&&(this.smartVolumeDuckingBaseline=this.getVideoVolume());let n=this.smartVolumeDuckingBaseline??this.getVideoVolume();this.setVideoVolume(Math.min(n,t)),wh(this,th(this.smartVolumeDuckingBaseline)),this.smartVolumeIsDucked=!0}function jh(e){if(!this.data?.enabledAutoVolume||!this.hasActiveSource())return;let t=jc(e);this.smartVolumeDuckingTarget=t,this.smartVolumeDuckingBaseline=t,this.smartVolumeLastApplied=t}var Mh=`https://vtrans.s3-private.mds.yandex.net/tts/prod/`,Nh=`/video-translation/audio-proxy/`,Ph=`https://brosubs.s3-private.mds.yandex.net/vtrans/`,Fh=`/video-subtitles/subtitles-proxy/`;function Ih(e){return e??`vot-worker.kload.workers.dev`}function Lh(e){return!!e.translateProxyEnabled}function Rh(e){return e.translateProxyEnabled===2}function zh(e){return!!(e.gmXhrSupported&&Lh(e))}function Bh(e,t){return!Rh(t)||!e.startsWith(Mh)?e:e.replace(Mh,`https://${Ih(t.proxyWorkerHost)}${Nh}`)}function Vh(e){let t=String(e||``);if(!t)return t;try{let e=new URL(t);return e.pathname.startsWith(Nh)?(e.host=`vtrans.s3-private.mds.yandex.net`,e.pathname=`/tts/prod/${e.pathname.slice(31).replace(/^\/+/,``)}`,e.protocol=`https:`,e.toString()):t}catch{return t}}function Hh(e,t){return e.startsWith(Mh)||e.startsWith(`https://${Ih(t.proxyWorkerHost)}${Nh}`)}function Uh(e,t){if(!Rh(t)||!e.startsWith(Ph))return e;let n=e.slice(49);return`https://${Ih(t.proxyWorkerHost)}${Fh}${n}`}var Wh=`disabled`,Gh=/^\d+$/u,[Kh,qh]=Di;function Jh(e){let t=[];for(let n=0;n=e.length?null:ou(e[t])}function Zh(){return{label:V.get(`VOTSubtitlesDisabled`),value:Wh,selected:!0,disabled:!1}}function Qh(e){return`${V.getLangLabel(e.language)}${e.translatedFromLanguage?` ${V.get(`VOTTranslatedFrom`)} ${V.getLangLabel(e.translatedFromLanguage)}`:``}${e.source===`yandex`?``:`, ${globalThis.location.hostname}`}${e.isAutoGenerated?` (${V.get(`VOTAutogenerated`)})`:``}`}function $h(e){let t=[Zh()];for(let{descriptor:n,index:r}of e)t.push({label:Qh(n),value:String(r),selected:!1,disabled:!1});return t}function eg(e){let t=e[Symbol.iterator]().next();return t.done?void 0:t.value}function tg(e){return(e??``).toLowerCase()}function ng(e){return tg(e).split(/[-_]/)[0]}function rg(e,t){if(!e||!t)return!1;let n=tg(e),r=tg(t);return n===r||ng(n)===ng(r)}function ig(e,t,n){if(e===qh){let e=tg(t);return e&&e!==Kh?e:void 0}return typeof e==`string`&&e&&e!==Kh?tg(e):tg(n)||tg(t)}function ag(e,t,n){if(!e.length)return null;let r=tg(t),i=tg(n),a=r===`auto`||r===``,o=ng(r),s=ng(i),c=e=>e.source===`yandex`,l=e=>!!e.isAutoGenerated,u=(e,t,n)=>rg(e.language,n)?a?!0:rg(e.translatedFromLanguage,t):!1,d=(e,t)=>rg(e.language,t)?e.translatedFromLanguage?rg(e.translatedFromLanguage,t):!0:!1,f=t=>e.find(({descriptor:e})=>t(e))?.index??null,p=()=>f(e=>!c(e)&&rg(e.language,i)&&!l(e))??f(e=>!c(e)&&rg(e.language,i)&&l(e));if(!a&&o&&s&&o===s){let e=f(e=>d(e,i)&&!l(e));if(e!=null)return e;let t=f(e=>d(e,i)&&l(e));if(t!=null)return t;let n=p();if(n!=null)return n;let r=f(e=>c(e)&&rg(e.language,i));if(r!=null)return r}return f(e=>c(e)&&u(e,r,i))??f(e=>c(e)&&rg(e.language,i))??f(e=>!c(e)&&u(e,r,i))??p()??null}var og=5e3,sg=150,cg=2;async function lg(e){let t=e.audioPlayer?.audioContext;if(!t||t.state!==`suspended`)return`not-needed`;let n=(async()=>{try{return await t.resume(),`resumed`}catch(e){return L.log(`[updateTranslation] Failed to resume AudioContext`,e),`failed`}})(),r,i=new Promise(e=>{r=setTimeout(()=>e(`timeout`),1500)}),a=await Promise.race([n,i]);return r!==void 0&&clearTimeout(r),a===`resumed`?L.log(`[updateTranslation] AudioContext resumed`):a===`timeout`&&L.log(`[updateTranslation] AudioContext resume timeout`),a}async function ug(e,t){if(!t||!e.audioPlayer)return;let n=e.audioPlayer.player,r=String(n.currentSrc||n.src||``);if(e.proxifyAudio(e.unproxifyAudio(r))===e.proxifyAudio(e.unproxifyAudio(t)))try{await n.clear(),n.src=``,L.log(`[updateTranslation] cleared stale partially-applied source`)}catch(e){L.log(`[updateTranslation] failed to clear stale source`,e)}}function dg(e,t){return e<=0||t.aborted?Promise.resolve():new Promise(n=>{let r=setTimeout(()=>{t.removeEventListener(`abort`,i),n()},e),i=()=>{clearTimeout(r),t.removeEventListener(`abort`,i),n()};t.addEventListener(`abort`,i,{once:!0})})}async function fg(e,t,n){let r=e.actionsAbortController.signal,i={headers:{range:`bytes=0-0`},signal:r,timeout:og};for(let a=1;a<=cg;a++){if(pg(e,n,r))return!1;try{let o=await z(t,i);if(pg(e,n,r))return!1;if(L.log(`[validateAudioUrl] probe response`,{audioUrl:t,attempt:a,ok:o.ok,status:o.status}),o.ok)return!0}catch(i){if(pg(e,n,r))return!1;L.log(`[validateAudioUrl] probe error`,{audioUrl:t,attempt:a,err:i})}if(!await mg(a,e,n,r))return!1}return!1}function pg(e,t,n){return e.isActionStale(t)||n.aborted}async function mg(e,t,n,r){return e>=cg?!0:pg(t,n,r)?!1:(await dg(sg,r),!pg(t,n,r))}async function hg(e,t){if(this.isActionStale(t)||await fg(this,e,t))return e;let n=this.unproxifyAudio(e);return n!==e&&await fg(this,n,t)?(L.log(`[validateAudioUrl] switching to direct audio URL after probe`),n):e}function gg(){!this.videoData||this.videoData.isStream||this.hasActiveSource()&&this.refreshTranslationAudio().catch(e=>{L.log(`[scheduleTranslationRefresh] refresh failed`,e)})}async function _g(){if(!this.videoData||this.videoData.isStream||!this.hasActiveSource())return;let e=this.videoData.videoId;if(!e)return;let t=Rs(this.videoData.translationHelp),n=this.getTranslationCacheKey(e,this.translateFromLang,this.translateToLang,t);this.cacheManager.getTranslation(n)?.url||(L.log(`[scheduleTranslationRefresh] translation cache expired after resume, refreshing now`),await this.refreshTranslationAudio())}async function vg(e,t){let n=await Hs({requester:e.translationHandler,request:{videoData:t.videoData,requestLang:t.requestLang,responseLang:t.responseLang,translationHelp:t.translationHelp,useAudioDownload:!!e.data?.useAudioDownload,signal:e.actionsAbortController.signal},actionContext:t.actionContext,isActionStale:t=>e.isActionStale(t),updateTranslation:(t,n)=>e.updateTranslation(t,n)});return n?(t.onBeforeCache&&await t.onBeforeCache(n),Us({cacheKey:t.cacheKey,setTranslation:(t,n)=>e.cacheManager.setTranslation(t,n),videoId:t.cacheVideoId,requestLang:t.cacheRequestLang,responseLang:t.cacheResponseLang,fallbackUrl:n.url,downloadTranslationUrl:e.downloadTranslationUrl,usedLivelyVoice:n.usedLivelyVoice}),n):null}async function yg(){if(!this.videoData||this.videoData.isStream||!this.hasActiveSource()||this.isRefreshingTranslation)return;let e=this.videoData.videoId;if(!e)return;this.actionsAbortController?.signal?.aborted&&this.resetActionsAbortController(`refreshTranslationAudio`),this.isRefreshingTranslation=!0;let t={gen:this.actionsGeneration,videoId:e},n=Rs(this.videoData.translationHelp);try{if(!await vg(this,{videoData:this.videoData,requestLang:this.translateFromLang,responseLang:this.translateToLang,translationHelp:n,actionContext:t,cacheKey:this.getTranslationCacheKey(e,this.translateFromLang,this.translateToLang,n),cacheVideoId:e,cacheRequestLang:this.translateFromLang,cacheResponseLang:this.translateToLang}))return}finally{this.isRefreshingTranslation=!1}}function bg(e){let t=Bh(e,{translateProxyEnabled:this.data?.translateProxyEnabled,proxyWorkerHost:this.data?.proxyWorkerHost});return t!==e&&L.log(`[VOT] Audio proxied via ${t}`),t}function xg(e){return Vh(e)}async function Sg(e=`proxySettingsChanged`){L.log(`[VOT] ${e}: clearing translation/subtitles cache`);try{this.cacheManager.clear(),this.activeTranslation=null}catch{}try{await this.stopTranslation()}catch{}await this.initVOTClient()}function Cg(e){return Hh(e,{proxyWorkerHost:this.data?.proxyWorkerHost})}function wg(e,t){return e.proxifyAudio(e.unproxifyAudio(t))}async function Tg(e,t,n){let r=e.audioPlayer.player.src!==t,i=null;r&&(e.audioPlayer.player.src=t,i=t);try{if(r&&await e.audioPlayer.init(),e.isActionStale(n))return await ug(e,i),{status:`stale`,didSetSource:r,appliedSourceUrl:i};let t=await lg(e);return t===`timeout`?L.log(`[updateTranslation] continuing after AudioContext resume timeout`):t===`failed`&&L.log(`[updateTranslation] AudioContext resume failed, continue without deferred resume`),e.isActionStale(n)?(await ug(e,i),{status:`stale`,didSetSource:r,appliedSourceUrl:i}):(!e.video.paused&&e.audioPlayer.player.src&&e.audioPlayer.player.lipSync(`play`),{status:`success`,didSetSource:r,appliedSourceUrl:i})}catch(e){return{status:`error`,didSetSource:r,appliedSourceUrl:i,error:e}}}async function Eg(e,t){if(await this.waitForPendingStopTranslate(),this.isActionStale(t))return;this.audioPlayer||this.createPlayer(),this.audioPlayer.audioContext?.state===`closed`&&(L.log(`[updateTranslation] AudioContext is closed, recreating player`),this.createPlayer());let n=wg(this,e),r=this.audioPlayer.player.currentSrc||this.audioPlayer.player.src||``,i=n===wg(this,r)?n:await this.validateAudioUrl(n,t);if(this.isActionStale(t))return;let a=await Dg(this,i,t),o=a.nextAudioUrl,s=a.applyResult,c=s.appliedSourceUrl;if(s.status!==`stale`){if(s.status===`error`){L.log(`this.audioPlayer.init() error`,s.error),await ug(this,c);let e=ba(s.error);this.transformBtn(`error`,e);return}this.clearVolumeLinkState(),this.setupAudioSettings(),this.transformBtn(`success`,V.get(`disableTranslate`)),this.afterUpdateTranslation(o)}}async function Dg(e,t,n){let r=t,i=await Tg(e,r,n);return Og(e,i,n,r)&&await kg(e,r,i.appliedSourceUrl,n)||{nextAudioUrl:r,applyResult:i}}function Og(e,t,n,r){return t.status===`error`&&t.didSetSource&&!e.isActionStale(n)&&e.unproxifyAudio(r)!==r}async function kg(e,t,n,r){let i=e.unproxifyAudio(t);L.log(`[updateTranslation] proxied audio init failed, retrying direct URL`);try{let t=await e.validateAudioUrl(i,r);return e.isActionStale(r)?(await ug(e,n),{nextAudioUrl:t,applyResult:{status:`stale`,didSetSource:!0,appliedSourceUrl:n}}):{nextAudioUrl:t,applyResult:await Tg(e,t,r)}}catch(e){return{nextAudioUrl:t,applyResult:{status:`error`,didSetSource:!0,appliedSourceUrl:n,error:e}}}}async function Ag(e,t,n,r,i){await this.waitForPendingStopTranslate(),L.log(`Run videoValidator`),await this.videoValidator(),this.actionsAbortController?.signal?.aborted&&this.resetActionsAbortController(`translateFunc`);let a=this.uiManager.votOverlayView;if(!a?.votButton){L.log(`[translateFunc] Overlay view missing, skipping translation`);return}if(a.votButton.loading=!0,this.hadAsyncWait=!1,this.volumeOnStart=this.getVideoVolume(),!e){L.log(`Skip translation - no VIDEO_ID resolved yet`),await this.updateTranslationErrorMsg(new U(`VOTNoVideoIDFound`),this.actionsAbortController.signal);return}let o=this.videoData;if(!o){await this.updateTranslationErrorMsg(new U(`VOTNoVideoIDFound`),this.actionsAbortController.signal);return}let s=Rs(i),c=this.getTranslationCacheKey(e,n,r,s),l=`video_${c}`;if(this.activeTranslation?.key===l){L.log(`[translateFunc] Reusing in-flight translation`),await this.activeTranslation.promise;return}let u={gen:this.actionsGeneration,videoId:e},d=(async()=>{if(this.isActionStale(u)){L.log(`[translateFunc] Stale translation task - skipping`);return}let t=n,i=r,a=async e=>await this.updateTranslation(e,u),l=this.cacheManager.getTranslation(c);if(l?.url){await a(l.url),L.log(`[translateFunc] Cached translation was received`);return}let d=await vg(this,{videoData:o,requestLang:t,responseLang:i,translationHelp:s,actionContext:u,cacheKey:c,cacheVideoId:e,cacheRequestLang:n,cacheResponseLang:r,onBeforeCache:async()=>{let t=this.getPreferredSubtitlesLanguage(o.detectedLanguage,o.responseLanguage);if(!t)return;let n=this.videoData?this.getSubtitlesCacheKey(e,this.videoData.detectedLanguage,t):null,r=n?this.cacheManager.getSubtitles(n):null;Array.isArray(r)&&ag(Jh(r),o.detectedLanguage,t)!=null||(n&&this.cacheManager.deleteSubtitles(n),this.subtitles=[],this.subtitlesCacheKey=null)}});L.log(`[translateRes]`,d),d||L.log(`Skip translation`)})();this.activeTranslation={key:l,promise:d};try{return await d}catch(t){throw this.hadAsyncWait=Ws({aborted:this.actionsAbortController.signal.aborted,translateApiErrorsEnabled:!!this.data?.translateAPIErrors,hadAsyncWait:this.hadAsyncWait,videoId:e,error:t,notify:e=>this.notifier.translationFailed(e)}),t}finally{this.activeTranslation?.promise===d&&(this.activeTranslation=null);let e=this.uiManager.votOverlayView?.votButton;!this.activeTranslation&&e?.loading&&!this.hasActiveSource()&&(L.log(`[translateFunc] clearing stale loading state`),this.transformBtn(`none`,V.get(`translateVideo`)))}}function jg(){return Vc(this.site.host)}function Mg(e,t){if(!t||t===e||e.aborted)return e;if(t.aborted)return t;if(typeof AbortSignal<`u`&&`any`in AbortSignal)return AbortSignal.any([e,t]);let n=new AbortController,r=()=>{e.removeEventListener(`abort`,i),t.removeEventListener(`abort`,a)},i=()=>{r(),n.abort(e.reason)},a=()=>{r(),n.abort(t.reason)};return e.addEventListener(`abort`,i,{once:!0}),t.addEventListener(`abort`,a,{once:!0}),n.signal}function Ng(e){let t=(t,n,r,i)=>{let a=Mg(e,i?.signal);if(!i){t.addEventListener(n,r,{signal:a});return}let{signal:o,...s}=i;t.addEventListener(n,r,{...s,signal:a})};return{add:t,addMany:(e,n,r,i)=>{for(let a of n)t(e,a,r,i)}}}function Pg(e,t,n){e(t,[`pointerenter`,`focusin`],e=>n.handleOverlayInteraction(e)),e(t,[`pointermove`],e=>n.handleOverlayInteraction(e),{passive:!0}),e(t,[`pointerleave`,`focusout`],e=>n.scheduleHide(e))}function Fg(e,t=0){let n=typeof e==`number`?e:Number(e);return Number.isFinite(n)?W(n):t}function Ig(e,t,n={}){n.skipYouTubeLikeHosts&&Rc(e.site.host)||e.smartVolumeDuckingInterval===void 0&&(!e.data?.syncVolume||!e.audioPlayer?.player?.src||e.isLikelyInternalVideoVolumeChange(t)||e.syncVolumeWrapper(`video`,t))}function Lg(e,t,n){let r=t.votMenu?.container;if(r){let t=n??e.video.getBoundingClientRect().height;r.style.setProperty(`--vot-container-height`,`${t}px`)}let{position:i,direction:a}=t.calcButtonLayout(e.data?.buttonPos??`default`);t.updateButtonLayout(i,a)}function Rg(e){return e.replace(`Key`,``).replace(`Digit`,``)}function zg(e){let t=new Set;for(let n of e)t.add(Rg(n));return t}function Bg(e,t){if(!e)return null;let n=t.get(e);if(n)return n;let r=e.split(`+`).filter(Boolean).map(Rg),i={parts:r,partsSet:new Set(r)};return t.set(e,i),i}function Vg(e,t){if(!t||e.size!==t.parts.length)return!1;for(let n of t.partsSet)if(!e.has(n))return!1;return!0}function Hg(e){let{self:t,overlayView:n,addMany:r}=e,i=()=>{t.refreshOverlayMount(),Lg(t,n)};t.resizeObserver=new ResizeObserver(e=>{for(let r of e)Lg(t,n,r.contentRect.height)}),t.resizeObserver.observe(t.video),i(),r(document,[`fullscreenchange`,`webkitfullscreenchange`],()=>i()),r(t.video,[`webkitbeginfullscreen`,`webkitendfullscreen`],()=>i())}function Ug(e){let{self:t}=e;if(!Bc(t.site))return;t.syncVolumeObserver=new MutationObserver(e=>{if(!t.audioPlayer?.player?.src)return;let n=!1,r=null;for(let t of e){if(t.type!==`attributes`||t.attributeName!==`aria-valuenow`)continue;n=!0;let e=t.target instanceof Element?t.target.getAttribute(`aria-valuenow`):null,i=e==null?NaN:Number.parseFloat(e);Number.isFinite(i)&&(r=i)}if(!n)return;let i;i=Fg(r??(t.isMuted()?0:t.getVideoVolume())*100),t.syncVideoVolumeSlider(),Ig(t,i)});let n=document.querySelector(`.ytp-volume-panel`);n&&t.syncVolumeObserver.observe(n,{attributes:!0,subtree:!0,attributeFilter:[`aria-valuenow`]})}function Wg(e){let{self:t}=e;if(t.site.host!==`youtube`||t.site.additionalData===`mobile`)return;let n=async()=>{try{if(!t.videoData)return;let e=F.getPlayer(),n=e?.getAvailableAudioTracks?.()??null;if(!Array.isArray(n)||n.length<=1)return;let r=e?.getAudioTrack?.()?.getLanguageInfo?.()?.id,i=r&&r!==`und`?r.toLowerCase().split(/[-_.]/)[0]:void 0;if(!i||!bn.includes(i))return;let a=i;if(a===t.videoData.detectedLanguage)return;if(t.videoManager.rememberDetectedLanguage(t.videoData.videoId,a),t.setSelectMenuValues(a,t.videoData.responseLanguage),t.data?.autoTranslate&&a!==t.videoData.responseLanguage){L.log(`[VOT] Audio track language changed to ${a}, triggering auto-translation`);try{await t.uiManager.handleTranslationBtnClick()}catch(e){L.log(`[VOT] Failed to trigger auto-translation on audio track change:`,e)}}}catch(e){L.log(`[VOT] Failed to sync audio track language`,e)}},r=F.getPlayer(),i=[`onApiChange`,`onStateChange`];if(r?.addEventListener)for(let e of i)try{r.addEventListener(e,n)}catch(t){L.log(`[VOT] Failed to bind ${e}`,t)}n(),t.abortController.signal.addEventListener(`abort`,()=>{if(r?.removeEventListener)for(let e of i)try{r.removeEventListener(e,n)}catch(t){L.log(`[VOT] Failed to unbind ${e}`,t)}},{once:!0})}function Gg(e){let{self:t,overlayView:n,add:r,addMany:i,platformConfig:a}=e;r(document,`click`,e=>{let r=e.target,i=n.votButton?.container,a=n.votMenu?.container,o=t.uiManager.votSettingsView?.dialog?.container,s=r&&i?i.contains(r):!1,c=r&&a?a.contains(r):!1,l=r?t.container.contains(r):!1,u=r&&o?o.contains(r):!1,d=r instanceof Element&&r.closest(`.vot-dialog-temp`)instanceof Element;L.log(`[document click] ${s} ${c} ${l} ${u} ${d}`),!(s||c||u||d)&&(l||n.updateButtonOpacity(0),a&&!a.hidden&&(a.hidden=!0,t.overlayVisibility?.queueAutoHide()))});let o=new Set,s=new Map,c=()=>o.clear(),l=(e,t)=>{e().catch(e=>{L.log(`[VOT] ${t} hotkey action failed`,e)})};r(document,`keydown`,e=>{let n=e;if(n.repeat)return;o.add(n.code);let r=document.activeElement,i=r?.tagName?.toLowerCase?.()??``;if([`input`,`textarea`].includes(i)||r?.isContentEditable)return;let a=zg(o);if(Vg(a,Bg(t.data?.translationHotkey,s))){c(),l(()=>t.uiManager.handleTranslationBtnClick(),`Translation`);return}Vg(a,Bg(t.data?.subtitlesHotkey,s))&&(c(),l(()=>t.toggleSubtitlesForCurrentLangPair(),`Subtitles`))}),r(document,`keyup`,e=>o.delete(e.code)),r(document,`blur`,c),r(document,`visibilitychange`,()=>{document.hidden&&c()}),r(globalThis,`blur`,c);let u=t.getEventContainer();u&&(i(u,[`pointerenter`,`pointerdown`],e=>t.overlayVisibility.handleHostInteraction(e)),r(u,`pointermove`,e=>t.overlayVisibility.handleHostInteraction(e),{passive:!0}),r(u,`pointerleave`,e=>t.overlayVisibility.scheduleHide(e))),t.rebindOverlayVisibilityTargets(),a.allowTouchMoveHandler&&r(document,`touchmove`,e=>t.overlayVisibility.handleHostInteraction(e),{passive:!0}),a.disableContainerDrag&&(t.container.draggable=!1)}function Kg(e){let{self:t,add:n}=e,r=!1,i=()=>{r=!1};n(t.video,`pause`,()=>{r=!0}),n(t.video,`playing`,()=>{r&&(r=!1,_g.call(t).catch(e=>{L.log(`[VOT] Failed to refresh translation after playback resumed`,e)}))}),n(t.video,`loadstart`,i),n(t.video,`emptied`,i)}function qg(e){let{self:t,overlayView:n,add:r}=e,i=async()=>{try{await t.setCanPlay()}catch(e){L.log(`[VOT] setCanPlay() failed`,e)}},a=!1,o=()=>{a||(a=!0,queueMicrotask(async()=>{a=!1,await i()}))};r(t.video,`canplay`,()=>{t.site.host===`rutube`&&t.video.src||o()});let s=async()=>{let e;try{e=await Jr(t.site,{fetchFn:z,video:t.video})}catch(e){L.log(`[VOT] Failed to resolve video id on emptied`,e)}t.videoData&&e&&e===t.videoData.videoId||(L.log(`lipsync mode is emptied`),Qs(t,n,{clearVideoData:!0,hideMenu:!0}))};r(t.video,`emptied`,()=>{s().catch(e=>{L.log(`[VOT] Failed to handle emptied lifecycle event`,e)})}),zc(t.site.host)||r(t.video,`volumechange`,()=>{t.syncVideoVolumeSlider();let e=t.uiManager.votOverlayView;e?.isInitialized()&&Ig(t,Fg(e.videoVolumeSlider.value),{skipYouTubeLikeHosts:!0})}),t.site.host===`youtube`&&!t.site.additionalData&&r(document,`yt-page-data-updated`,()=>{L.log(`yt-page-data-updated`),globalThis.location.pathname.startsWith(`/shorts/`)&&o()})}function Jg(){let e=this.uiManager.votOverlayView;if(!e?.subtitlesSelect)return;let{add:t,addMany:n}=Ng(this.abortController.signal),r={self:this,overlayView:e,platformConfig:Zm(this.site.host),add:t,addMany:n};Kg(r),Hg(r),Ug(r),Wg(r),Gg(r),qg(r)}function Yg(){this.overlayVisibilityTargetsAbortController?.abort(),this.overlayVisibilityTargetsAbortController=new AbortController;let{signal:e}=this.overlayVisibilityTargetsAbortController,t=this.uiManager?.votOverlayView?.votButton?.container,n=this.uiManager?.votOverlayView?.votMenu?.container;if(!t||!n||!this.overlayVisibility)return;let r=this.overlayVisibility,{addMany:i}=Ng(e);Pg(i,t,r),Pg(i,n,r)}function Xg(e){if(!(e instanceof Node))return!1;let t=this.uiManager?.votOverlayView,n=t?.votButton?.container,r=t?.votMenu?.container;return n instanceof Node&&n.contains(e)||r instanceof Node&&r.contains(e)}function Zg(){let e=this.data?.autoHideButtonDelay;return typeof e==`number`&&Number.isFinite(e)?e:Ti}function Qg(){this.resizeObserver?.disconnect(),this.overlayVisibilityTargetsAbortController?.abort(),this.overlayVisibilityTargetsAbortController=void 0,Bc(this.site)&&this.syncVolumeObserver?.disconnect()}var $g;function e_(e){$g=e}var t_=null;async function n_(){$g||(t_??=(async()=>{try{e_((await(await z(`https://cloudflare-dns.com/cdn-cgi/trace`,{timeout:7e3})).text()).split(` +`).find(e=>e.startsWith(`loc=`))?.slice(4,6).toUpperCase())}catch(e){console.error(`[VOT] Error getting country:`,e)}})().finally(()=>{t_=null}),await t_)}async function r_(){if(this.initialized)return;let e=this.isAudioContextSupported;this.data=await B.getValues({autoTranslate:!1,autoSubtitles:!1,dontTranslateLanguages:[Pi],enabledDontTranslateLanguages:!0,enabledAutoVolume:!0,enabledSmartDucking:!0,autoVolume:15,buttonPos:`default`,showVideoSlider:!0,syncVolume:!1,downloadWithName:La,sendNotifyOnComplete:!1,subtitlesMaxLength:300,subtitlesSmartLayout:!0,highlightWords:!1,subtitlesFontSize:20,subtitlesFontFamily:`default-sans`,subtitlesOpacity:20,subtitlesDownloadFormat:`srt`,responseLanguage:Pi,responseLanguageSubtitles:`auto`,defaultVolume:100,onlyBypassMediaCSP:e,newAudioPlayer:e,showPiPButton:!1,translateAPIErrors:!0,translationService:Si,detectService:Ci,translationHotkey:null,subtitlesHotkey:null,m3u8ProxyHost:di,proxyWorkerHost:pi,translateProxyEnabled:0,translateProxyEnabledDefault:!0,audioBooster:!1,useLivelyVoice:!1,autoHideButtonDelay:Ti,useAudioDownload:La,compatVersion:``,account:{},localeHash:``,localeUpdatedAt:0}),this.data.compatVersion!==`2025-05-09`&&(this.data=await to(this.data),await B.set(`compatVersion`,Ei));try{if(Pi===`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`){let e=this.data.responseLanguage;this.data.dontTranslateLanguages=[e],await B.set(`dontTranslateLanguages`,this.data.dontTranslateLanguages)}}catch{}this.uiManager.data=this.data,console.log(`[VOT] data from db:`,this.data),!this.data.translateProxyEnabled&&Fa&&(this.data.translateProxyEnabled=1),await n_(),$g&&wi.includes($g)&&this.data.translateProxyEnabledDefault&&(this.data.translateProxyEnabled=2),L.log(`translateProxyEnabled`,this.data.translateProxyEnabled,this.data.translateProxyEnabledDefault),L.log(`Extension compatibility passed...`),await this.initVOTClient(),this.uiManager.initUI(),this.uiManager.initUIEvents(),this.uiManager.votOverlayView?.votButton?.container&&(this.uiManager.votOverlayView.votButton.container.hidden=!0),this.createPlayer(),this.translateToLang=this.data.responseLanguage??`ru`,this.initExtraEvents(),this.initialized=!0}var i_=`und`,a_=new Map,o_=new Map,s_=e=>{if(e)try{return Intl.getCanonicalLocales(e)[0]}catch{return}},c_=e=>{let t=s_(e);if(t)return Intl.Segmenter.supportedLocalesOf([t])[0]},l_=(e,t)=>{let n=c_(e),r=`${t}:${n??i_}`,i=t===`sentence`?o_:a_,a=i.get(r);if(a)return a;let o=new Intl.Segmenter(n,{granularity:t});return i.set(r,o),o},u_=(e,t)=>e?Array.from(l_(t,`word`).segment(e),e=>({text:e.segment,index:e.index,isWordLike:!!e.isWordLike})):[],d_=(e,t)=>e?Array.from(l_(t,`sentence`).segment(e),e=>({text:e.segment,index:e.index})):[],f_=/\s/u,p_=/^[,.:;!?%)\]}>»]/u,m_=/[([{<«'"-]$/u,h_=/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u,g_=(e,t)=>{let n=e.at(-1)??``,r=t[0]??``;return!(!n||!r||f_.test(n)||f_.test(r)||m_.test(n)||p_.test(r)||h_.test(n)&&h_.test(r))},__=/\s/u,v_=e=>!!e.trim(),y_=e=>{let t=``,n=``,r=[];for(let i of e){let e=i.text.trim();if(!e)continue;t&&g_(n,e)&&(t+=` `);let a=t.length;t+=e;let o=t.length;r.push({line:{...i,text:e},text:e,start:a,end:o}),n=e}return{streamText:t,spans:r}},b_=(e,t,n)=>{let r=t,i=n;for(;rr&&__.test(e[i-1]??``);)--i;return r>=i?null:{start:r,end:i}},x_=(e,t,n)=>{let r=Math.max(t,e.start),i=Math.min(n,e.end);if(r>=i)return null;let a=r-e.start,o=i-e.start,s=e.text.slice(a,o);if(!s)return null;let c=Math.max(e.text.length,1),l=e.line.startMs+Math.round(e.line.durationMs*a/c),u=e.line.startMs+Math.round(e.line.durationMs*o/c);return{text:s,startMs:l,durationMs:Math.max(0,u-l),isWordLike:v_(s)}},S_=(e,t,n)=>{let r=n.index,i=b_(e,r,r+n.text.length);if(!i)return null;let a=e.slice(i.start,i.end);if(!v_(a))return null;let o=[],s=`0`;for(let e of t){let t=x_(e,i.start,i.end);t&&(o.length===0&&(s=e.line.speakerId),o.push(t))}if(!o.length)return null;let c=Math.min(...o.map(e=>e.startMs)),l=Math.max(...o.map(e=>e.startMs+Math.max(0,e.durationMs)));return{text:a,startMs:c,durationMs:Math.max(0,l-c),speakerId:s,tokens:o}},C_=(e,t)=>{let{streamText:n,spans:r}=y_(e);if(!n||!r.length)return[];let i=d_(n,t).map(e=>S_(n,r,e)).filter(e=>e!==null);return i.length?i:r.map(({line:e})=>e)},w_=e=>typeof e==`number`&&Number.isFinite(e)?e:0,T_=e=>Math.max(0,w_(e)),E_=(e,t,n)=>{let r=e.subtitles;if(!Array.isArray(r)||r.length===0)return null;let i=t??n;if(i){let e=r.find(e=>e.language===i&&typeof e.translatedFromLanguage==`string`);if(e)return e;let t=r.find(e=>e.language===i);if(t)return t}return r[0]??null},D_=e=>`host`in e&&`videoId`in e&&`detectedLanguage`in e&&`duration`in e&&typeof e.host==`string`&&typeof e.videoId==`string`&&typeof e.detectedLanguage==`string`&&typeof e.duration==`number`,O_=e=>{let t=F.getPoToken();if(!t)return e;let n=t;for(let e=0;e<10;e+=1){let e;try{e=decodeURIComponent(n)}catch{break}if(e===n)break;n=e}let r=F.getDeviceParams(),i=typeof r==`string`?r.replace(/^[?&]+/u,``):``;try{let t=new URL(e);if(t.searchParams.set(`potc`,`1`),t.searchParams.set(`pot`,n),i){let e=new URLSearchParams(i);for(let[n,r]of e.entries())t.searchParams.set(n,r)}return t.toString()}catch{let t=e.includes(`?`)?`&`:`?`,r=i?`&${i}`:``;return`${e}${t}potc=1&pot=${encodeURIComponent(n)}${r}`}},k_=(e,t)=>e-t,A_=(e,t)=>et?1:0,j_=(e,t)=>{let n=Math.min(e.length,t.length);for(let r=0;re?t===0?n?1:0:n?0:1:0,N_=(e,t,n,r)=>!e||!t||n===r?0:1,P_=(e,t)=>{let n=e.source===`yandex`,r=n?0:1,i=e.language===Ni?0:1,a=!!e.translatedFromLanguage,o=t&&e.language===t?0:1;return[r,i,M_(n,o,a),N_(n,a,e.translatedFromLanguage,t),n&&!a?o:0,n?0:Number(!!e.isAutoGenerated)]},F_=(e,t,n)=>({descriptor:e,index:t,rank:P_(e,n),language:e.language,translatedFromLanguage:e.translatedFromLanguage??``,source:e.source,url:e.url,isAutoGenerated:Number(!!e.isAutoGenerated)}),I_=(e,t)=>{let n=e.map((e,n)=>F_(e,n,t));return n.sort((e,t)=>{let n=j_(e.rank,t.rank);if(n!==0)return n;let r=A_(e.language,t.language)||A_(e.translatedFromLanguage,t.translatedFromLanguage)||A_(e.source,t.source)||A_(e.url,t.url)||k_(e.isAutoGenerated,t.isAutoGenerated);return r===0?k_(e.index,t.index):r}),n.map(e=>e.descriptor)},L_=(e,t)=>typeof e==`boolean`?e:!!t.trim(),R_=e=>{if(!e||typeof e!=`object`)return null;let t=e,n=T_(t.start),r=T_(t.end);return r<=n?null:{start:n,end:r,style:Au(t.style)}},z_=e=>{if(!e||typeof e!=`object`)return;let t=e,n={};typeof t.rawText==`string`&&(n.rawText=t.rawText);let r=Array.isArray(t.styledSpans)?t.styledSpans.map(R_).filter(e=>e!==null):[];return r.length&&(n.styledSpans=r),t.vtt&&typeof t.vtt==`object`&&(n.vtt=t.vtt),t.ass&&typeof t.ass==`object`&&(n.ass=t.ass),Object.keys(n).length?n:void 0},B_=e=>{if(!e||typeof e!=`object`)return{text:``,startMs:0,durationMs:0,isWordLike:!1};let t=e,n=typeof t.text==`string`?t.text:``;return{text:n,startMs:T_(t.startMs),durationMs:T_(t.durationMs),isWordLike:L_(t.isWordLike,n),style:Au(t.style)}},V_=e=>{if(!e||typeof e!=`object`)return{text:``,startMs:0,durationMs:0,speakerId:`0`,tokens:[]};let t=e,n=Array.isArray(t.tokens)?t.tokens.map(B_):[];return{text:typeof t.text==`string`?t.text:``,startMs:T_(t.startMs),durationMs:T_(t.durationMs),speakerId:typeof t.speakerId==`string`?t.speakerId:`0`,tokens:n,metadata:z_(t.metadata)}},H_=e=>{if(!e||typeof e!=`object`)return{format:`json`,subtitles:[]};let t=e,n=Array.isArray(t.subtitles)?t.subtitles.map(V_):[];return{format:t.format??`json`,subtitles:n}},U_=(e,t)=>!t||e.tStartMs+e.dDurationMs<=t.tStartMs?Math.max(0,e.dDurationMs):Math.max(0,t.tStartMs-e.tStartMs),W_=(e,t,n)=>{let r=[],i=``,a=n,o=``;for(let s=0;se.durationMs>0,K_=e=>e.text?yf(e.text):e.tokens.length?yf(e.tokens.map(e=>e.text).join(``)):``,q_=(e,t,n)=>{if(!e.length)return[];let r=Math.max(0,n),i=e.map(e=>Math.max(e.length,1)),a=i.reduce((e,t)=>e+t,0),o=Array(i.length+1).fill(0);for(let e=0;e{let t=[],n=0;for(let r=0;ryf(e.map(e=>e.text).join(``)),X_=e=>{let t=[];for(let n of e){if(!n.text.includes(` +`)){t.push(n);continue}let e=J_(n.text),r=q_(e.map(e=>e.text===` +`?``:e.text),n.startMs,n.durationMs);for(let i=0;i!n||!e.tokens.length||t.source===`youtube`||e.metadata?.styledSpans?.length&&!e.tokens.some(e=>e.style)?!1:Y_(e.tokens)===n,Q_=(e,t)=>{let n=[];for(let r of e){let e=yf(r.text);if(!e||!G_(r))continue;let i=u_(e,t).filter(e=>e.isWordLike&&e.text.trim());if(!i.length)continue;let a=q_(i.map(e=>e.text),r.startMs,r.durationMs);n.push(...a)}return n},$_=(e,t,n,r)=>({text:e.text,startMs:t,durationMs:n,isWordLike:e.isWordLike,style:sf(r,e.index,e.index+e.text.length)}),ev=(e,t,n,r)=>{let i=J_(e.text);if(i.length===1&&i[0].text===e.text)return[$_(e,t,n,r)];let a=q_(i.map(e=>e.text===` +`?``:e.text),t,n);return i.map((n,i)=>{let o=n.text===` +`;return{text:n.text,startMs:a[i]?.startMs??t,durationMs:o?0:a[i]?.durationMs??0,isWordLike:!o&&e.isWordLike,style:o?void 0:sf(r,e.index+n.startOffset,e.index+n.endOffset)}})},tv=e=>e.reduce((e,t,n)=>(t.isWordLike&&t.text.trim()&&e.push(n),e),[]),nv=(e,t)=>{if(!t.length)return e;let n=tv(e);if(!n.length)return e;let r=n.length;for(let i=0;i{if(!n)return[];if(Z_(e,t,n))return X_(e.tokens);let r=t.language,i=e.metadata?.styledSpans??of(e.metadata?.rawText??e.text??n).styledSpans,a=u_(n,r);if(!a.length)return[];let o=q_(a.map(e=>e.text),e.startMs,e.durationMs),s=[];for(let t=0;t{let n=await z(e,{timeout:7e3});return t===`vtt`||t===`srt`||t===`ass`?rp(await n.text(),t):n.json()},av=(e,t)=>t.source===`youtube`?cv.formatYoutubeSubtitles(e,!!t.isAutoGenerated):(t.format===`srt`||t.format===`vtt`||t.format,ip(H_(e))),ov=(e,t)=>{let n=cv.autoMerge(e,t);return{...n,subtitles:cv.processTokens(n,t)}},sv=e=>{let t=[],n=new Set;for(let r of e.subtitles??[])r.language&&!n.has(r.language)&&(n.add(r.language),t.push({source:`yandex`,format:`json`,language:r.language,url:r.url})),r.translatedLanguage&&t.push({source:`yandex`,format:`json`,language:r.translatedLanguage,translatedFromLanguage:r.language,url:r.translatedUrl??r.url});return t},cv={processTokens(e,t){let n=[];for(let r of e.subtitles){let e=K_(r),i=rv(r,t,e);n.push({...r,text:e,tokens:i})}return n},formatYoutubeSubtitles(e,t=!1){let n=e.events??[];if(!n.length)return console.error(`[VOT] Invalid YouTube subtitles format:`,e),{format:`json`,subtitles:[]};let r=[];for(let e=0;ee.tokens.length>0)?e:{...e,subtitles:C_(e.subtitles,t.language)}},async fetchSubtitles(e,t,n){let r=ou(e);if(!r&&D_(e)&&(r=E_(e,t,n)),!r)return{format:`json`,subtitles:[]};let{source:i,format:a}=r,{url:o}=r;i===`youtube`&&(o=O_(o));try{let e=ov(av(await iv(o,a),r),r);return L.log(`[VOT] Processed subtitles:`,e),e}catch(e){return console.error(`[VOT] Failed to process subtitles:`,e),{format:`json`,subtitles:[]}}},async getSubtitles(e,t){let{host:n,url:r,detectedLanguage:i,videoId:a,duration:o,subtitles:s=[]}=t;try{let t={videoData:{host:n,url:r,videoId:a,duration:o},requestLang:i},c=await Promise.race([e.getSubtitles(t),new Promise((e,t)=>{setTimeout(()=>t(Error(`Timeout`)),5e3)})]);return L.log(`[VOT] Subtitles response:`,c),c.waiting&&console.error(`[VOT] Failed to get Yandex subtitles`),I_([...sv(c),...s],i)}catch(e){let t=`Error in getSubtitles function`;throw e instanceof Error&&e.message===`Timeout`&&(t=`Failed to get Yandex subtitles: timeout`),console.error(`[VOT] ${t}`,e),e}}},lv=new WeakMap;function uv(e){let t=e.videoData;return e.getPreferredSubtitlesLanguage(t?.detectedLanguage,t?.responseLanguage)}function dv(e){let t=e.videoData;if(!t?.videoId)return null;let n=t.detectedLanguage?.toLowerCase();if(!n||n===`auto`)return null;let r=uv(e);return r?e.getSubtitlesCacheKey(t.videoId,n,r):null}async function fv(e){if(!e.videoData?.videoId)return!1;await e.videoManager.ensureDetectedLanguageForTranslation(e.videoData);let t=e.videoData.detectedLanguage?.toLowerCase();return!!(t&&t!==`auto`)}function pv(e){return[e.source,e.format,e.language,e.translatedFromLanguage??``,e.isAutoGenerated?`1`:`0`,e.url].join(`|`)}function mv(e){let t=new Set,n=[];for(let r of e){let e=pv(r);t.has(e)||(t.add(e),n.push(r))}return n}function hv(e,t){let n=e.videoData;if(!n)throw Error(`Video data is required to load subtitles`);if(e.site.host!==`youtube`||!t)return n;let r=F.getSubtitles(t);return r.length?{...n,subtitles:mv([...Array.isArray(n.subtitles)?n.subtitles:[],...r])}:n}function gv(e){let t=(lv.get(e)??0)+1;return lv.set(e,t),t}function _v(e,t){return lv.get(e)===t}function vv(e,t){return e.hasSubtitlesWidget()&&e.subtitlesWidget?.setContent(null),t.downloadSubtitlesButton.hidden=!0,e.yandexSubtitles=null,e}async function yv(e){L.log(`[onchange] subtitles`,e);let t=gv(this),n=this.uiManager.votOverlayView;if(!n?.subtitlesSelect||!n.downloadSubtitlesButton)return this;if(n.subtitlesSelect.setSelectedValue(e),e===`disabled`)return vv(this,n);let r=Yh(e);if(r==null)return vv(this,n);let i=Xh(this.subtitles,r);if(!i)return vv(this,n);let a={...i},o=Uh(a.url,{translateProxyEnabled:this.data?.translateProxyEnabled,proxyWorkerHost:this.data?.proxyWorkerHost});o!==a.url&&(a={...a,url:o},console.log(`[VOT] Subs proxied via ${a.url}`));let s=await cv.fetchSubtitles(a);return _v(this,t)?(this.yandexSubtitles=s,this.getSubtitlesWidget().setContent(this.yandexSubtitles,a.language),n.downloadSubtitlesButton.hidden=!1,this):this}async function bv(){let e=this.uiManager.votOverlayView;if(!e?.subtitlesSelect)return;let t=$h(Jh(this.subtitles));e.subtitlesSelect.updateItems(t),await this.changeSubtitlesLang(Wh)}async function xv(){if(!await fv(this))return(this.subtitlesCacheKey!==null||this.subtitles.length>0)&&(this.subtitles=[],this.subtitlesCacheKey=null,await this.updateSubtitlesLangSelect()),this;let e=dv(this);if(!e)return(this.subtitlesCacheKey!==null||this.subtitles.length>0)&&(this.subtitles=[],this.subtitlesCacheKey=null,await this.updateSubtitlesLangSelect()),this;if(this.subtitlesCacheKey===e){let t=this.cacheManager.getSubtitles(e)!==void 0;if(this.subtitles.length>0||t)return this}let t=this.cacheManager.getSubtitles(e);return t===void 0?(await this.loadSubtitles(),this):(this.subtitles=Array.isArray(t)?t:[],this.subtitlesCacheKey=e,await this.updateSubtitlesLangSelect(),this)}async function Sv(){let e=this.uiManager.votOverlayView;if(!e?.subtitlesSelect)return this;try{await xv.call(this)}catch{return this}let t=this.videoData?.detectedLanguage??this.translateFromLang,n=uv(this);if(!n)return this;let r=ag(Jh(this.subtitles),t,n);return r==null||eg(e.subtitlesSelect.selectedValues)===String(r)||await this.changeSubtitlesLang(String(r)),this}async function Cv(){let e=this.uiManager.votOverlayView;if(!e?.subtitlesSelect)return this;let t=eg(e.subtitlesSelect.selectedValues);return t&&t!==`disabled`?(await this.changeSubtitlesLang(Wh),this):(await this.enableSubtitlesForCurrentLangPair(),this)}async function wv(){if(!this.videoData?.videoId){console.error(`[VOT] ${V.getDefault(`VOTNoVideoIDFound`)}`),this.subtitles=[],this.subtitlesCacheKey=null;return}if(!await fv(this)){this.subtitles=[],this.subtitlesCacheKey=null,await this.updateSubtitlesLangSelect();return}let e=uv(this);if(!e){this.subtitles=[],this.subtitlesCacheKey=null,await this.updateSubtitlesLangSelect();return}let t=this.getSubtitlesCacheKey(this.videoData.videoId,this.videoData.detectedLanguage,e);try{let n=this.cacheManager.getSubtitles(t);if(!n){let r=this.subtitlesLoadPromises.get(t);if(r===void 0){let n=hv(this,e);r=cv.getSubtitles(this.votClient,n),this.subtitlesLoadPromises.set(t,r)}try{n=await r,n=Array.isArray(n)?n:[],this.cacheManager.setSubtitles(t,n)}finally{this.subtitlesLoadPromises.get(t)===r&&this.subtitlesLoadPromises.delete(t)}}this.subtitles=Array.isArray(n)?n:[],this.subtitlesCacheKey=t}catch(e){console.error(`[VOT] Failed to load subtitles:`,e),this.subtitles=[],this.subtitlesCacheKey=null}await this.updateSubtitlesLangSelect()}new Set(xn);var Tv=Promise.resolve(),Ev=class{video;container;site;translateFromLang=`auto`;translateToLang=Pi;data;videoData;firstPlay=!0;audioContext;votClient;audioPlayer;abortController;actionsAbortController;actionsGeneration=0;notifier=new Lm;cacheManager;votSessionStorage=new fa;subtitlesLoadPromises=new Map;downloadTranslationUrl=null;isRefreshingTranslation=!1;autoRetry;votOpts;volumeOnStart;volumeLinkState={initialized:!1,lastVideoPercent:0,lastTranslationPercent:0};internalVideoVolumeSetAt=0;internalVideoVolumeSetPercent=null;internalVideoVolumeSuppressionMs=250;internalVideoVolumeSetHistory=[];internalVideoVolumeSetHistoryLimit=48;smartVolumeDuckingInterval;smartVolumeDuckingTarget=.2;smartVolumeDuckingBaseline;smartVolumeLastApplied;smartVolumeLastTickAt=0;smartVolumeLastSoundAt=0;smartVolumeRmsMissingSinceAt=null;smartVolumeRmsEnvelope=0;smartVolumeSpeechGateOpen=!1;smartVolumeIsDucked=!1;longWaitingResCount=0;hadAsyncWait=!1;subtitles=[];subtitlesCacheKey=null;subtitlesWidget;activeTranslation=null;stopTranslatePromise=null;interactionChecker;uiManager;overlayVisibility;overlayVisibilityTargetsAbortController;translationOrchestrator;lifecycleController;translationHandler;videoManager;yandexSubtitles=null;resizeObserver;syncVolumeObserver;initialized=!1;mountCache;errorTranslationCache=new Map;getFullscreenOverlayRoot(){let e=document;return Po(e.fullscreenElement??e.webkitFullscreenElement,[this.container])}getOverlayMountPoints(e=this.container){let t=this.getFullscreenOverlayRoot(),{base:n,root:r,portalContainer:i,subtitlesMountContainer:a}=Vo({container:e,site:this.site,fullscreenRoot:t}),o=this.mountCache;return o?.container===e&&o.base===n&&o.subtitlesMountContainer===a&&o.fullscreenRoot===t&&(o.root.isConnected??document.documentElement.contains(o.root))?{root:o.root,portalContainer:o.portalContainer,subtitlesMountContainer:o.subtitlesMountContainer,fullscreenRoot:o.fullscreenRoot}:(this.mountCache={container:e,base:n,root:r,portalContainer:i,subtitlesMountContainer:a,fullscreenRoot:t},{root:r,portalContainer:i,subtitlesMountContainer:a,fullscreenRoot:t})}getOverlayMount(e=this.container){let{root:t,portalContainer:n,subtitlesMountContainer:r,fullscreenRoot:i}=this.getOverlayMountPoints(e);return{root:t,portalContainer:n,subtitlesMountContainer:r,tooltipLayoutRoot:i??this.tooltipLayoutRoot}}getTranslationCacheKey(e,t,n,r){let i=this.getRequestLangForTranslation(t,n),a=this.isLivelyVoiceAllowed(i,n)&&this.data?.useLivelyVoice,o=r==null?``:Ui(r);return`${e}_${i}_${n}_${a}_${o?Wi(o):`0`}`}getSubtitlesCacheKey(e,t,n){return`${e}_${t}_${n}_${!!this.data?.useLivelyVoice}`}getPreferredSubtitlesLanguage(e=this.videoData?.detectedLanguage??`auto`,t=this.videoData?.responseLanguage??this.translateToLang,n=this.data?.responseLanguageSubtitles){return ig(n,e,t)}isActionStale(e){return e?this.actionsGeneration!==e.gen||this.videoData?.videoId!==e.videoId:!1}updateVOTClientRequestSignal(){this.votClient&&(this.votClient.fetchOpts={...this.votClient.fetchOpts,signal:this.actionsAbortController.signal})}resetActionsAbortController(e){try{this.actionsAbortController?.abort(e)}catch{}this.actionsAbortController=new AbortController,this.actionsGeneration++,this.updateVOTClientRequestSignal()}constructor(e,t,n){L.log(`[VideoHandler] add video:`,e,`container:`,t,this),this.video=e,this.container=t,this.site=n,this.abortController=new AbortController,this.actionsAbortController=new AbortController,this.cacheManager=new pa,this.interactionChecker=Em(),this.interactionChecker.start(),this.uiManager=new vm({mount:this.getOverlayMount(t),data:this.data,videoHandler:this,intervalIdleChecker:this.interactionChecker}),this.overlayVisibility=new ym({checker:this.interactionChecker,getOverlayView:()=>this.uiManager.votOverlayView??null,getAutoHideDelay:()=>this.getAutoHideDelay(),isInteractiveNode:e=>this.isOverlayInteractiveNode(e)}),this.translationOrchestrator=new Ys({isFirstPlay:()=>this.firstPlay,setFirstPlay:e=>{this.firstPlay=e},isAutoTranslateEnabled:()=>!!this.data?.autoTranslate,getVideoId:()=>this.videoData?.videoId,scheduleAutoTranslate:()=>this.runAutoTranslate(),isMobileYouTubeMuted:()=>this.site.host===`youtube`&&this.site.additionalData===`mobile`&&this.video.muted,setMuteWatcher:e=>{let t=!1,n=()=>{t||(t=!0,this.video.removeEventListener(`volumechange`,r),e())},r=()=>{this.video.muted||n()};this.video.addEventListener(`volumechange`,r,{signal:this.abortController.signal}),queueMicrotask(()=>{this.video.muted||n()})}}),this.lifecycleController=new $s(ec(this,e=>this.getOverlayMount(e))),this.translationHandler=new Js(this),this.videoManager=new nl(this)}getSubtitlesWidget(){if(!this.subtitlesWidget){let{subtitlesMountContainer:e}=this.getOverlayMountPoints();this.subtitlesWidget=new jd(this.video,e,this.interactionChecker,this.tooltipLayoutRoot),this.applySavedSubtitlesWidgetSettings(this.subtitlesWidget)}return this.subtitlesWidget}applySavedSubtitlesWidgetSettings(e){this.data&&(e.setSmartLayout(typeof this.data.subtitlesSmartLayout==`boolean`?this.data.subtitlesSmartLayout:!0),typeof this.data.subtitlesMaxLength==`number`&&e.setMaxLength(this.data.subtitlesMaxLength),typeof this.data.highlightWords==`boolean`&&e.setHighlightWords(this.data.highlightWords),typeof this.data.subtitlesFontSize==`number`&&e.setFontSize(this.data.subtitlesFontSize),typeof this.data.subtitlesFontFamily==`string`&&e.setFontFamily(this.data.subtitlesFontFamily),typeof this.data.subtitlesOpacity==`number`&&e.setOpacity(this.data.subtitlesOpacity))}hasSubtitlesWidget(){return!!this.subtitlesWidget}resetSubtitlesWidget(){this.hasSubtitlesWidget()&&(this.subtitlesWidget?.release(),this.subtitlesWidget=void 0)}get uiRoot(){return this.getOverlayMountPoints().root}get portalContainer(){return this.getOverlayMountPoints().portalContainer}get tooltipLayoutRoot(){switch(this.site.host){case`kickstarter`:return document.getElementById(`react-project-header`)??void 0;case`custom`:return;default:return this.container}}getEventContainer(){return this.site.eventSelector?document.querySelector(this.site.eventSelector)??this.container:this.container}async runAutoTranslate(){await this.videoManager.videoValidator(),await this.uiManager.handleTranslationBtnClick()}getAudioContext(){if(this.audioContext)return this.audioContext;if(this.isAudioContextSupported)try{return this.audioContext=Qr(),this.audioContext}catch(e){console.warn(`[VOT] Failed to init AudioContext, falling back:`,e);return}}get isAudioContextSupported(){return globalThis.AudioContext!==void 0||globalThis.webkitAudioContext!==void 0}getPreferAudio(){return!this.getAudioContext()||!this.data||!this.data.newAudioPlayer||this.videoData?.isStream?!0:this.data.newAudioPlayer&&!this.data.onlyBypassMediaCSP?!1:!this.site.needBypassCSP}createPlayer(){let e=this.getPreferAudio();return L.log(`preferAudio:`,e),this.audioPlayer=new ri({video:this.video,debug:!1,fetchFn:z,fetchOpts:{timeout:0},preferAudio:e}),this}isLikelyInternalVideoVolumeChange(e){let t=Date.now(),n=this.internalVideoVolumeSetHistory;if(n.length>0){let r=0,i=!1;for(let a of n)t-a.at>a.suppressMs||(n[r++]=a,!i&&Math.abs(e-a.percent)<=1&&(i=!0));return n.length=r,i}return this.internalVideoVolumeSetPercent===null||t-this.internalVideoVolumeSetAt>this.internalVideoVolumeSuppressionMs?!1:Math.abs(e-this.internalVideoVolumeSetPercent)<=1}callModule(e,...t){return e.call(this,...t)}callModuleAsync(e,...t){return e.call(this,...t)}init(){return r_.call(this)}async initVOTClient(){let e=Lh(this.data??{}),t=this.data?.translateProxyEnabled===1?fi:e?Ih(this.data?.proxyWorkerHost):ui;this.votOpts={fetchFn:z,fetchOpts:{signal:this.actionsAbortController.signal,forceGmXhr:zh({...this.data,gmXhrSupported:La})},apiToken:this.data?.account?.token,hostVOT:mi,host:t},this.votClient=new(e?$t:Qt)(this.votOpts),this.votClient.sessions=await this.votSessionStorage.restore(t,this.votClient.sessions);let n=this.votClient.getSession.bind(this.votClient);return this.votClient.getSession=async e=>{let r=await n(e);return await this.votSessionStorage.persist(t,this.votClient.sessions),r},this}transformBtn(e,t){return this.uiManager.transformBtn(e,t),this}hasActiveSource(){return!!this.audioPlayer?.player?.src}initExtraEvents(){return this.callModule(Jg)}refreshOverlayMount(){this.mountCache=void 0;let e=this.getOverlayMount(this.container),t=!hp(this.uiManager.mount,e);this.uiManager.updateMount(e),t&&this.rebindOverlayVisibilityTargets()}rebindOverlayVisibilityTargets=Yg;setCanPlay(){return this.lifecycleController.setCanPlay()}isOverlayInteractiveNode(e){return this.callModule(Xg,e)}getAutoHideDelay(){return this.callModule(Zg)}changeSubtitlesLang=yv;updateSubtitlesLangSelect=bv;ensureSubtitlesForCurrentLangPair=xv;loadSubtitles=wv;enableSubtitlesForCurrentLangPair(){return this.callModuleAsync(Sv)}toggleSubtitlesForCurrentLangPair(){return this.callModuleAsync(Cv)}getRequestLangForTranslation(e,t){return this.data?.useLivelyVoice&&this.data?.account?.token&&t===`ru`?`en`:e}isLivelyVoiceAllowed(e=this.videoData?.detectedLanguage??`auto`,t=this.videoData?.responseLanguage??this.translateToLang){return!(this.getRequestLangForTranslation(e,t)!==`en`||t!==`ru`||!this.data?.account?.token)}getVideoVolume(){return this.videoManager.getVideoVolume()}setVideoVolume(e,t={}){let n=jc(e),r=typeof t.suppressSyncMs==`number`&&Number.isFinite(t.suppressSyncMs)?Math.max(0,t.suppressSyncMs):this.internalVideoVolumeSuppressionMs,i=Date.now(),a=Oc(n);return this.internalVideoVolumeSetAt=i,this.internalVideoVolumeSetPercent=a,this.internalVideoVolumeSetHistory.push({at:i,percent:a,suppressMs:r}),this.internalVideoVolumeSetHistory.length>this.internalVideoVolumeSetHistoryLimit&&this.internalVideoVolumeSetHistory.splice(0,this.internalVideoVolumeSetHistory.length-this.internalVideoVolumeSetHistoryLimit),this.videoManager.setVideoVolume(n),this}onVideoVolumeSliderSynced(e){let t=W(e);if(!this.volumeLinkState.initialized){Gm(this.volumeLinkState,t);return}this.data?.syncVolume&&this.hasActiveSource()&&!this.isLikelyInternalVideoVolumeChange(t)||Gm(this.volumeLinkState,t)}onTranslationVolumeSliderSynced(e){if(!this.volumeLinkState.initialized){Km(this.volumeLinkState,e);return}Km(this.volumeLinkState,e)}resetVolumeLinkState(e,t){Gm(this.volumeLinkState,e),Km(this.volumeLinkState,t),this.volumeLinkState.initialized=!0}clearVolumeLinkState(){this.volumeLinkState.initialized=!1,this.volumeLinkState.lastVideoPercent=0,this.volumeLinkState.lastTranslationPercent=0}isMuted(){return this.videoManager.isMuted()}syncVideoVolumeSlider(){this.videoManager.syncVideoVolumeSlider()}setSelectMenuValues(e,t){this.videoManager.setSelectMenuValues(e,t)}syncVolumeWrapper(e,t){let n=this.uiManager.votOverlayView;if(!n?.isInitialized())return;let r=n.videoVolumeSlider,i=n.translationVolumeSlider;if(!r||!i)return;let{nextVideo:a,nextTranslation:o}=Jm({state:this.volumeLinkState,fromType:e,newVolume:t,currentVideo:Number(r.value),currentTranslation:Number(i.value),translationMin:i.min,translationMax:i.max});if(typeof o==`number`){i.value=o,this.audioPlayer?.player&&(this.audioPlayer.player.volume=o/100);return}typeof a==`number`&&(r.value=a,this.setVideoVolume(a/100))}getVideoData(){return this.videoManager.getVideoData()}videoValidator(){return this.videoManager.videoValidator()}stopTranslate(){if(this.stopTranslatePromise!==null)return this.stopTranslatePromise;let e=(async()=>{if(this.audioPlayer?.player){try{this.audioPlayer.player.removeVideoEvents(),this.audioPlayer.player.src=``,await this.audioPlayer.player.clear()}catch(e){L.log(`[stopTranslate] audioPlayer cleanup error`,e)}L.log(`audioPlayer after stopTranslate`,this.audioPlayer)}this.activeTranslation=null;let e=this.uiManager.votOverlayView;e&&(e.videoVolumeSlider&&(e.videoVolumeSlider.hidden=!0),e.translationVolumeSlider&&(e.translationVolumeSlider.hidden=!0),e.downloadTranslationButton&&(e.downloadTranslationButton.hidden=!0)),this.downloadTranslationUrl=null,this.longWaitingResCount=0,this.hadAsyncWait=!1,this.transformBtn(`none`,V.get(`translateVideo`)),L.log(`Volume on start: ${this.volumeOnStart}`);let t=typeof this.smartVolumeDuckingBaseline==`number`?this.smartVolumeDuckingBaseline:this.volumeOnStart;Th(this,{restoreVolume:t}),this.volumeOnStart=void 0,this.autoRetry!==void 0&&(clearTimeout(this.autoRetry),this.autoRetry=void 0),this.resetActionsAbortController(`stopTranslate`)})().finally(()=>{this.stopTranslatePromise===e&&(this.stopTranslatePromise=null)});return this.stopTranslatePromise=e,e}waitForPendingStopTranslate(){return this.stopTranslatePromise??Tv}async updateTranslationErrorMsg(e,t){if(t?.aborted)return;let n=V.get(`translationTake`),r=V.lang;this.longWaitingResCount=e===V.get(`translationTakeAboutMinute`)?this.longWaitingResCount+1:0,L.log(`longWaitingResCount`,this.longWaitingResCount),this.longWaitingResCount>5&&(e=new U(`TranslationDelayed`)),L.log(`updateTranslationErrorMsg message`,e);let i=await this.resolveTranslationErrorDisplayMessage(e,n,r,t);t?.aborted||i===null||(this.transformBtn(`error`,i),!t?.aborted&&[`Подготавливаем перевод`,`Видео передано в обработку`,`Ожидаем перевод видео`,`Загружаем переведенное аудио`].includes(e)&&this.uiManager.votOverlayView?.votButton&&(this.uiManager.votOverlayView.votButton.loading=!0))}async resolveTranslationErrorDisplayMessage(e,t,n,r){return e?.name===`VOTLocalizedError`?e.localizedMessage:e instanceof Error?e.message:this.shouldTranslateErrorMessage(e,t,n)?await this.getTranslatedErrorMessage(e,n,r):this.stringifyTranslationError(e)}shouldTranslateErrorMessage(e,t,n){return!!this.data?.translateAPIErrors&&n!==`ru`&&!e?.includes(t)}stringifyTranslationError(e){return Array.isArray(e)?e.join(` +`):String(e??``)}async getTranslatedErrorMessage(e,t,n){let r=this.uiManager.votOverlayView;if(!r?.votButton)return null;let i=Array.isArray(e)?e.join(` `):String(e),a=`${t}:${i}`,o=this.errorTranslationCache.get(a);if(o)return o;r.votButton.loading=!0;let s=await yc(i,`ru`,t);if(n?.aborted)return null;let c=Array.isArray(s)?s.join(` +`):String(s);return this.errorTranslationCache.set(a,c),this.trimErrorTranslationCache(),c}trimErrorTranslationCache(){if(this.errorTranslationCache.size<=50)return;let e=this.errorTranslationCache.keys().next().value;e&&this.errorTranslationCache.delete(e)}afterUpdateTranslation(e){let t=this.uiManager.votOverlayView;if(!t?.votButton)return;let n=t.votButton.container.dataset.status===`success`;t.videoVolumeSlider&&(t.videoVolumeSlider.hidden=!this.data?.showVideoSlider||!n),t.translationVolumeSlider&&(t.translationVolumeSlider.hidden=!n),t.videoVolumeSlider&&t.translationVolumeSlider?(this.volumeLinkState.lastVideoPercent=Number(t.videoVolumeSlider.value),this.volumeLinkState.lastTranslationPercent=Number(t.translationVolumeSlider.value),this.volumeLinkState.initialized=!0):this.volumeLinkState.initialized=!1,this.videoData&&!this.videoData.isStream&&(t.downloadTranslationButton&&(t.downloadTranslationButton.hidden=!1),this.downloadTranslationUrl=e),L.log(`afterUpdateTranslation downloadTranslationUrl`,this.downloadTranslationUrl),this.data?.sendNotifyOnComplete&&this.hadAsyncWait&&n&&(this.notifier.translationCompleted(globalThis.location.hostname),this.hadAsyncWait=!1)}validateAudioUrl(e,t){return this.callModuleAsync(hg,e,t)}scheduleTranslationRefresh(){this.callModule(gg)}refreshTranslationAudio=yg;proxifyAudio(e){return this.callModule(bg,e)}unproxifyAudio(e){return this.callModule(xg,e)}handleProxySettingsChanged=Sg;isMultiMethodS3(e){return this.callModule(Cg,e)}updateTranslation=Eg;translateFunc(e,t,n,r,i){return Ag.call(this,e,t,n,r,i)}isYouTubeHosts(){return this.callModule(jg)}setupAudioSettings(){return this.callModule(Ah)}applyManualVideoVolumeOverride(e){return this.callModule(jh,e)}stopTranslation=async()=>{this.translationOrchestrator?.reset(),this.overlayVisibility?.cancel(),await this.stopTranslate(),this.syncVideoVolumeSlider()};handleSrcChanged(){return this.lifecycleController.handleSrcChanged()}async release(){L.log(`[VideoHandler] release`),this.initialized=!1;try{await this.stopTranslation()}catch(e){L.log(`[VideoHandler] stopTranslation failed during release`,e)}this.lifecycleController?.teardown(),this.abortController?.abort(),this.abortController=new AbortController,this.overlayVisibility?.release(),this.releaseExtraEvents(),this.hasSubtitlesWidget()&&(this.subtitlesWidget?.release(),this.subtitlesWidget=void 0),this.interactionChecker?.destroy(),this.uiManager.release()}collectReportInfo(){let e=tm(),t=this.videoData?.detectedLanguage??`unknown`,n=this.videoData?.responseLanguage??`unknown`,r=`
Autogenerated by VOT:
    -
  • OS: ${t.os}
  • -
  • Browser: ${t.browser}
  • -
  • Loader: ${t.loader}
  • -
  • Script version: ${t.scriptVersion}
  • -
  • URL: ${t.url}
  • -
  • Lang: ${e} -> ${i} (Lively voice: ${this.data?.useLivelyVoice??false} | Audio download: ${this.data?.useAudioDownload??false})
  • -
  • Player: ${this.data?.newAudioPlayer?"New":"Old"} (CSP only: ${this.data?.onlyBypassMediaCSP??false})
  • +
  • OS: ${e.os}
  • +
  • Browser: ${e.browser}
  • +
  • Loader: ${e.loader}
  • +
  • Script version: ${e.scriptVersion}
  • +
  • URL: ${e.url}
  • +
  • Lang: ${t} -> ${n} (Lively voice: ${this.data?.useLivelyVoice??!1} | Audio download: ${this.data?.useAudioDownload??!1})
  • +
  • Player: ${this.data?.newAudioPlayer?`New`:`Old`} (CSP only: ${this.data?.onlyBypassMediaCSP??!1})
  • Proxying mode: ${this.data?.translateProxyEnabled??0}
-
`;return {assignees:"ilyhalight",template:`1-bug-report-${v.lang==="ru"?"ru":"en"}.yml`,os:t.os,"script-version":t.scriptVersion,"additional-info":o}}releaseExtraEvents=Tv}const xy=Li(),Mr=new be(xy),ky=new WeakMap;let Un=null;const bt=qc();function Ty(){return {frame:gi()?"iframe":"top",host:globalThis.location.hostname||"unknown",path:globalThis.location.pathname||"/"}}function Jt(n,t){const e=Ty(),i={host:e.host,path:e.path};t&&Object.assign(i,t),console.log(`[VOT][bootstrap][${e.frame}] ${n}`,i);}function Ly(){return Un||(Un=Bc()),Un}function Ay(n,t){if(!n.selector)return t.parentElement;const e=ys(t,n.selector);return n.shadowRoot,e}async function Iy(){const n=df({isIframe:gi(),href:String(globalThis.location.href||""),origin:globalThis.location.origin});if(n==="skip"){Jt("Skipping bootstrap for non-runnable iframe");return}Jt("Loading extension"),n==="top-full"?await Mo("top-frame",Jt):Jt("Lazy iframe bootstrap enabled; waiting for video detection"),cf({videoObserver:Mr,videosWrappers:ky,ensureRuntimeActivated:t=>Mo(t,Jt),getServicesCached:Ly,findContainer:Ay,createVideoHandler:(t,e,i)=>new Sy(t,e,i)}),Mr.enable();}if(bt.status==="booting"||bt.status==="booted")Jt("bootstrap already initialized, skipping duplicate run",{status:bt.status});else {const n=async()=>{try{await Iy(),bt.status="booted";}catch(t){bt.status="failed",bt.error=t,console.error("[VOT]",t);}};bt.status="booting",bt.promise=n();} - - }) - }; -})); - -System.register("./__vite-browser-external-BIHI7g3E-DUmwvnCo.js", [], (function (exports, module) { - 'use strict'; - return { - execute: (function () { - - const e=exports("default", {}); - - }) - }; -})); - -System.import("./__entry.js", "./"); \ No newline at end of file +
`;return{assignees:`ilyhalight`,template:`1-bug-report-${V.lang===`ru`?`ru`:`en`}.yml`,os:e.os,"script-version":e.scriptVersion,"additional-info":r}}releaseExtraEvents=Qg},Dv=new Wm(Em()),Ov=new WeakMap,kv=null,Av=si();function jv(){return{frame:Co()?`iframe`:`top`,host:globalThis.location.hostname||`unknown`,path:globalThis.location.pathname||`/`}}function Mv(e,t){let n=jv(),r={host:n.host,path:n.path};t&&Object.assign(r,t),console.log(`[VOT][bootstrap][${n.frame}] ${e}`,r)}function Nv(){return kv||=qr(),kv}function Pv(e,t){if(L.log(`findContainer`,e,t),!e.selector)return L.log(`findContainer without selector, using parentElement`),t.parentElement;let n=zo(t,e.selector);return e.shadowRoot?L.log(`findContainer with site.shadowRoot`,n):L.log(`findContainer without shadowRoot`,n),n}async function Fv(){let e=Ao({isIframe:Co(),href:String(globalThis.location.href||``),origin:globalThis.location.origin,authOrigin:_i});if(e===`skip`){Mv(`Skipping bootstrap for non-runnable iframe`);return}li(),Mv(`Loading extension`,{mode:e}),e===`auth-eager`?await Eo(`auth-page`,Mv):Mv(`Lazy bootstrap enabled; waiting for video detection`),Oo({videoObserver:Dv,videosWrappers:Ov,ensureRuntimeActivated:e=>Eo(e,Mv),getServicesCached:Nv,findContainer:Pv,createVideoHandler:(e,t,n)=>new Ev(e,t,n)}),Dv.enable()}function Iv(){return Av.status===`booting`||Av.status===`booted`?(Mv(`bootstrap already initialized, skipping duplicate run`,{status:Av.status}),Av.promise??Promise.resolve()):(Av.status=`booting`,Av.promise=(async()=>{try{await Fv(),Av.status=`booted`}catch(e){Av.status=`failed`,Av.error=e,console.error(`[VOT]`,e)}})(),Av.promise)}return Iv(),e.VideoHandler=Ev,e.bootstrapContentScript=Iv,Object.defineProperty(e,`countryCode`,{enumerable:!0,get:function(){return $g}}),e.getEnvironmentInfo=tm,e})({}); \ No newline at end of file diff --git a/dist/vot.user.js b/dist/vot.user.js index b0e087cc..31d1b30b 100644 --- a/dist/vot.user.js +++ b/dist/vot.user.js @@ -1,4833 +1,4418 @@ // ==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.3 -// @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 *://*.kodik.info/* -// @match *://*.kodik.biz/* -// @match *://*.kodik.cc/* -// @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 *://*/*.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 cloudflare-dns.com -// @connect porntn.com -// @connect youtube.com -// @connect googlevideo.com -// @grant GM.deleteValue -// @grant GM.getValue -// @grant GM.getValues -// @grant GM.listValues -// @grant GM.setValue -// @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 window.focus +// @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.4 +// @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 *://*.kodik.info/* +// @match *://*.kodik.biz/* +// @match *://*.kodik.cc/* +// @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 *://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 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== -!function(){function e(e,t){return(t||"")+" (SystemJS Error#"+e+" https://github.com/systemjs/systemjs/blob/main/docs/errors.md#"+e+")"}function t(e,t){if(-1!==e.indexOf("\\")&&(e=e.replace(j,"/")),"/"===e[0]&&"/"===e[1])return t.slice(0,t.indexOf(":")+1)+e;if("."===e[0]&&("/"===e[1]||"."===e[1]&&("/"===e[2]||2===e.length&&(e+="/"))||1===e.length&&(e+="/"))||"/"===e[0]){var n,r=t.slice(0,t.indexOf(":")+1);if(n="/"===t[r.length+1]?"file:"!==r?(n=t.slice(r.length+2)).slice(n.indexOf("/")+1):t.slice(8):t.slice(r.length+("/"===t[r.length])),"/"===e[0])return t.slice(0,t.length-n.length-1)+e;for(var i=n.slice(0,n.lastIndexOf("/")+1)+e,o=[],s=-1,u=0;un.length&&"/"!==r[r.length-1]))return r+e.slice(n.length);u("W2",n,r,"should have a trailing '/'")}}function u(t,n,r,i){console.warn(e(t,"Package target "+i+", resolving target '"+r+"' for "+n))}function c(e,t,n){for(var r=e.scopes,i=n&&o(n,r);i;){var u=s(t,r[i]);if(u)return u;i=o(i.slice(0,i.lastIndexOf("/")),r)}return s(t,e.imports)||-1!==t.indexOf(":")&&t}function a(){this[M]={}}function f(e){return e.id}function l(e,t,n,r){if(e.onload(n,t.id,t.d&&t.d.map(f),!!r),n)throw n}function d(t,n,r,i){var o=t[M][n];if(o)return o;var s=[],u=Object.create(null);P&&Object.defineProperty(u,P,{value:"Module"});var c=Promise.resolve().then((function(){return t.instantiate(n,r,i)})).then((function(r){if(!r)throw Error(e(2,"Module "+n+" did not instantiate"));var i=r[1]((function(e,t){o.h=!0;var n=!1;if("string"==typeof e)e in u&&u[e]===t||(u[e]=t,n=!0);else{for(var r in e)t=e[r],r in u&&u[r]===t||(u[r]=t,n=!0);e&&e.__esModule&&(u.__esModule=e.__esModule)}if(n)for(var i=0;i-1){var n=document.createEvent("Event");n.initEvent("error",!1,!1),t.dispatchEvent(n)}return Promise.reject(e)}))}else if("systemjs-importmap"===t.type){t.sp=!0;var r=t.src?(System.fetch||fetch)(t.src,{integrity:t.integrity,priority:t.fetchPriority,passThrough:!0}).then((function(e){if(!e.ok)throw Error("Invalid status code: "+e.status);return e.text()})).catch((function(n){return n.message=e("W4","Error fetching systemjs-import map "+t.src)+"\n"+n.message,console.warn(n),"function"==typeof t.onerror&&t.onerror(),"{}"})):t.innerHTML;W=W.then((function(){return r})).then((function(n){!function(t,n,r){var o={};try{o=JSON.parse(n)}catch(s){console.warn(Error(e("W5","systemjs-importmap contains invalid JSON")+"\n\n"+n+"\n"))}i(o,r,t)}(N,n,t.src||g)}))}}))}var g,y="undefined"!=typeof Symbol,b="undefined"!=typeof self,S="undefined"!=typeof document,w=b?self:global;if(S){var O=document.querySelector("base[href]");O&&(g=O.href)}if(!g&&"undefined"!=typeof location){var E=(g=location.href.split("#")[0].split("?")[0]).lastIndexOf("/");-1!==E&&(g=g.slice(0,E+1))}var x,j=/\\/g,P=y&&Symbol.toStringTag,M=y?Symbol():"@",I=a.prototype;I.import=function(e,t,n){var r=this;return t&&"object"==typeof t&&(n=t,t=void 0),Promise.resolve(r.prepareImport()).then((function(){return r.resolve(e,t,n)})).then((function(e){var t=d(r,e,void 0,n);return t.C||p(r,t)}))},I.createContext=function(e){var t=this;return{url:e,resolve:function(n,r){return Promise.resolve(t.resolve(n,r||e))}}},I.onload=function(){},I.register=function(e,t,n){x=[e,t,n]},I.getRegister=function(){var e=x;return x=void 0,e};var L=Object.freeze(Object.create(null));w.System=new a;var C,R,W=Promise.resolve(),N={imports:{},scopes:{},depcache:{},integrity:{}},T=S;if(I.prepareImport=function(e){return(T||e)&&(m(),T=!1),W},I.getImportMap=function(){return JSON.parse(JSON.stringify(N))},S&&(m(),window.addEventListener("DOMContentLoaded",m)),I.addImportMap=function(e,t){i(e,t||g,N)},S){window.addEventListener("error",(function(e){J=e.filename,_=e.error}));var A=location.origin}I.createScript=function(e){var t=document.createElement("script");t.async=!0,e.indexOf(A+"/")&&(t.crossOrigin="anonymous");var n=N.integrity[e];return n&&(t.integrity=n),t.src=e,t};var J,_,k={},U=I.register;I.register=function(e,t){if(S&&"loading"===document.readyState&&"string"!=typeof e){var n=document.querySelectorAll("script[src]"),r=n[n.length-1];if(r){C=e;var i=this;R=setTimeout((function(){k[r.src]=[e,t],i.import(r.src)}))}}else C=void 0;return U.call(this,e,t)},I.instantiate=function(t,n){var r=k[t];if(r)return delete k[t],r;var i=this;return Promise.resolve(I.createScript(t)).then((function(r){return new Promise((function(o,s){r.addEventListener("error",(function(){s(Error(e(3,"Error loading "+t+(n?" from "+n:""))))})),r.addEventListener("load",(function(){if(document.head.removeChild(r),J===t)s(_);else{var e=i.getRegister(t);e&&e[0]===C&&clearTimeout(R),o(e)}})),document.head.appendChild(r)}))}))},I.shouldFetch=function(){return!1},"undefined"!=typeof fetch&&(I.fetch=fetch);var $=I.instantiate,B=/^(text|application)\/(x-)?javascript(;|$)/;I.instantiate=function(t,n,r){var i=this;return this.shouldFetch(t,n,r)?this.fetch(t,{credentials:"same-origin",integrity:N.integrity[t],meta:r}).then((function(r){if(!r.ok)throw Error(e(7,r.status+" "+r.statusText+", loading "+t+(n?" from "+n:"")));var o=r.headers.get("content-type");if(!o||!B.test(o))throw Error(e(4,'Unknown Content-Type "'+o+'", loading '+t+(n?" from "+n:"")));return r.text().then((function(e){return e.indexOf("//# sourceURL=")<0&&(e+="\n//# sourceURL="+t),(0,eval)(e),i.getRegister(t)}))})):$.apply(this,arguments)},I.resolve=function(n,r){return c(N,t(n,r=r||g)||n,r)||function(t,n){throw Error(e(8,"Unable to resolve bare specifier '"+t+(n?"' from "+n:"'")))}(n,r)};var F=I.instantiate;I.instantiate=function(e,t,n){var r=N.depcache[e];if(r)for(var i=0;i{t$1.has(e)||(t$1.add(e),(a=>GM_addStyle(a))(e));}; - - const votConfig = { - "host": "api.browser.yandex.ru", - "hostVOT": "vot.toil.cc/v1", - "hostWorker": "vot-worker.toil.cc", - "mediaProxy": "media-proxy.toil.cc", - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 YaBrowser/25.12.0.0 Safari/537.36", - "componentVersion": "25.12.4.1198", - "hmac": "bt8xH3VOlb4mqf0nqAibnDOoiPlXsisf", - "defaultDuration": 343, - "minChunkSize": 5295308, - "loggerLevel": 1, - "version": "2.4.14" - }; - function varint64read() { - let lowBits = 0; - let highBits = 0; - for (let shift = 0; shift < 28; shift += 7) { - let b2 = this.buf[this.pos++]; - lowBits |= (b2 & 127) << shift; - if ((b2 & 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 b2 = this.buf[this.pos++]; - highBits |= (b2 & 127) << shift; - if ((b2 & 128) == 0) { - this.assertBounds(); - return [lowBits, highBits]; - } - } - throw new Error("invalid varint"); - } - function varint64write(lo, hi, bytes) { - for (let i2 = 0; i2 < 28; i2 = i2 + 7) { - const shift = lo >>> i2; - 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 i2 = 3; i2 < 31; i2 = i2 + 7) { - const shift = hi >>> i2; - const hasNext = !(shift >>> 7 == 0); - const byte = (hasNext ? shift | 128 : shift) & 255; - bytes.push(byte); - if (!hasNext) { - return; - } - } - bytes.push(hi >>> 31 & 1); - } - const TWO_PWR_32_DBL = 4294967296; - 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); - } - 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; - } - 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 }; - } - function negate(lowBits, highBits) { - highBits = ~highBits; - if (lowBits) { - lowBits = ~lowBits + 1; - } else { - highBits += 1; - } - return newBits(lowBits, highBits); - } - const decimalFrom1e7WithLeadingZeros = (digit1e7) => { - const partial = String(digit1e7); - return "0000000".slice(partial.length) + partial; - }; - function varint32write(value, bytes) { - if (value >= 0) { - while (value > 127) { - bytes.push(value & 127 | 128); - value = value >>> 7; - } - bytes.push(value); - } else { - for (let i2 = 0; i2 < 9; i2++) { - bytes.push(value & 127 | 128); - value = value >> 7; - } - bytes.push(1); - } - } - function varint32read() { - let b2 = this.buf[this.pos++]; - let result = b2 & 127; - if ((b2 & 128) == 0) { - this.assertBounds(); - return result; - } - b2 = this.buf[this.pos++]; - result |= (b2 & 127) << 7; - if ((b2 & 128) == 0) { - this.assertBounds(); - return result; - } - b2 = this.buf[this.pos++]; - result |= (b2 & 127) << 14; - if ((b2 & 128) == 0) { - this.assertBounds(); - return result; - } - b2 = this.buf[this.pos++]; - result |= (b2 & 127) << 21; - if ((b2 & 128) == 0) { - this.assertBounds(); - return result; - } - b2 = this.buf[this.pos++]; - result |= (b2 & 15) << 28; - for (let readBytes = 5; (b2 & 128) !== 0 && readBytes < 10; readBytes++) - b2 = this.buf[this.pos++]; - if ((b2 & 128) != 0) - throw new Error("invalid varint"); - this.assertBounds(); - return result >>> 0; - } - var define_process_env_default = {}; - const protoInt64 = makeInt64Support(); - function makeInt64Support() { - const dv = new DataView(new ArrayBuffer(8)); - const ok = typeof BigInt === "function" && typeof dv.getBigInt64 === "function" && typeof dv.getBigUint64 === "function" && typeof dv.setBigInt64 === "function" && typeof dv.setBigUint64 === "function" && (typeof process != "object" || typeof define_process_env_default != "object" || define_process_env_default.BUF_BIGINT_DISABLE !== "1"); - if (ok) { - 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); - } - } - const 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 { - encodeURIComponent(text); - return true; - } catch (e2) { - return false; - } - } - }; - } - return globalThis[symbol]; - } - var WireType; - (function(WireType2) { - WireType2[WireType2["Varint"] = 0] = "Varint"; - WireType2[WireType2["Bit64"] = 1] = "Bit64"; - WireType2[WireType2["LengthDelimited"] = 2] = "LengthDelimited"; - WireType2[WireType2["StartGroup"] = 3] = "StartGroup"; - WireType2[WireType2["EndGroup"] = 4] = "EndGroup"; - WireType2[WireType2["Bit32"] = 5] = "Bit32"; - })(WireType || (WireType = {})); - const FLOAT32_MAX = 34028234663852886e22; - const FLOAT32_MIN = -34028234663852886e22; - const UINT32_MAX = 4294967295; - const INT32_MAX = 2147483647; - const INT32_MIN = -2147483648; - class BinaryWriter { - constructor(encodeUtf8 = getTextEncoding().encodeUtf8) { - this.encodeUtf8 = encodeUtf8; - this.stack = []; - this.chunks = []; - this.buf = []; - } -finish() { - if (this.buf.length) { - this.chunks.push(new Uint8Array(this.buf)); - this.buf = []; - } - let len = 0; - for (let i2 = 0; i2 < this.chunks.length; i2++) - len += this.chunks[i2].length; - let bytes = new Uint8Array(len); - let offset = 0; - for (let i2 = 0; i2 < this.chunks.length; i2++) { - bytes.set(this.chunks[i2], offset); - offset += this.chunks[i2].length; - } - this.chunks = []; - return bytes; - } -fork() { - this.stack.push({ chunks: this.chunks, buf: this.buf }); - this.chunks = []; - this.buf = []; - return this; - } -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); - } -tag(fieldNo, type) { - return this.uint32((fieldNo << 3 | type) >>> 0); - } -raw(chunk) { - if (this.buf.length) { - this.chunks.push(new Uint8Array(this.buf)); - this.buf = []; - } - this.chunks.push(chunk); - return this; - } -uint32(value) { - assertUInt32(value); - while (value > 127) { - this.buf.push(value & 127 | 128); - value = value >>> 7; - } - this.buf.push(value); - return this; - } -int32(value) { - assertInt32(value); - varint32write(value, this.buf); - return this; - } -bool(value) { - this.buf.push(value ? 1 : 0); - return this; - } -bytes(value) { - this.uint32(value.byteLength); - return this.raw(value); - } -string(value) { - let chunk = this.encodeUtf8(value); - this.uint32(chunk.byteLength); - return this.raw(chunk); - } -float(value) { - assertFloat32(value); - let chunk = new Uint8Array(4); - new DataView(chunk.buffer).setFloat32(0, value, true); - return this.raw(chunk); - } -double(value) { - let chunk = new Uint8Array(8); - new DataView(chunk.buffer).setFloat64(0, value, true); - return this.raw(chunk); - } -fixed32(value) { - assertUInt32(value); - let chunk = new Uint8Array(4); - new DataView(chunk.buffer).setUint32(0, value, true); - return this.raw(chunk); - } -sfixed32(value) { - assertInt32(value); - let chunk = new Uint8Array(4); - new DataView(chunk.buffer).setInt32(0, value, true); - return this.raw(chunk); - } -sint32(value) { - assertInt32(value); - value = (value << 1 ^ value >> 31) >>> 0; - varint32write(value, this.buf); - return this; - } -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); - } -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); - } -int64(value) { - let tc = protoInt64.enc(value); - varint64write(tc.lo, tc.hi, this.buf); - return this; - } -sint64(value) { - let tc = protoInt64.enc(value), sign = tc.hi >> 31, lo = tc.lo << 1 ^ sign, hi = (tc.hi << 1 | tc.lo >>> 31) ^ sign; - varint64write(lo, hi, this.buf); - return this; - } -uint64(value) { - let tc = protoInt64.uEnc(value); - varint64write(tc.lo, tc.hi, this.buf); - return this; - } - } - class BinaryReader { - constructor(buf, decodeUtf8 = getTextEncoding().decodeUtf8) { - this.decodeUtf8 = decodeUtf8; - this.varint64 = varint64read; - this.uint32 = varint32read; - this.buf = buf; - this.len = buf.length; - this.pos = 0; - this.view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); - } -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(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); - } -assertBounds() { - if (this.pos > this.len) - throw new RangeError("premature EOF"); - } -int32() { - return this.uint32() | 0; - } -sint32() { - let zze = this.uint32(); - return zze >>> 1 ^ -(zze & 1); - } -int64() { - return protoInt64.dec(...this.varint64()); - } -uint64() { - return protoInt64.uDec(...this.varint64()); - } -sint64() { - let [lo, hi] = this.varint64(); - let s2 = -(lo & 1); - lo = (lo >>> 1 | (hi & 1) << 31) ^ s2; - hi = hi >>> 1 ^ s2; - return protoInt64.dec(lo, hi); - } -bool() { - let [lo, hi] = this.varint64(); - return lo !== 0 || hi !== 0; - } -fixed32() { - return this.view.getUint32((this.pos += 4) - 4, true); - } -sfixed32() { - return this.view.getInt32((this.pos += 4) - 4, true); - } -fixed64() { - return protoInt64.uDec(this.sfixed32(), this.sfixed32()); - } -sfixed64() { - return protoInt64.dec(this.sfixed32(), this.sfixed32()); - } -float() { - return this.view.getFloat32((this.pos += 4) - 4, true); - } -double() { - return this.view.getFloat64((this.pos += 8) - 8, true); - } -bytes() { - let len = this.uint32(), start = this.pos; - this.pos += len; - this.assertBounds(); - return this.buf.subarray(start, start + len); - } -string() { - return this.decodeUtf8(this.bytes()); - } - } - 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 > INT32_MAX || arg < INT32_MIN) - throw new Error("invalid int32: " + arg); - } - 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 > UINT32_MAX || arg < 0) - throw new Error("invalid uint32: " + arg); - } - function assertFloat32(arg) { - if (typeof arg == "string") { - const o2 = arg; - arg = Number(arg); - if (isNaN(arg) && o2 !== "NaN") { - throw new Error("invalid float32: " + o2); - } - } else if (typeof arg != "number") { - throw new Error("invalid float32: " + typeof arg); - } - if (Number.isFinite(arg) && (arg > FLOAT32_MAX || arg < FLOAT32_MIN)) - throw new Error("invalid float32: " + arg); - } - var StreamInterval; - (function(StreamInterval2) { - StreamInterval2[StreamInterval2["NO_CONNECTION"] = 0] = "NO_CONNECTION"; - StreamInterval2[StreamInterval2["TRANSLATING"] = 10] = "TRANSLATING"; - StreamInterval2[StreamInterval2["STREAMING"] = 20] = "STREAMING"; - StreamInterval2[StreamInterval2["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; - case -1: - case "UNRECOGNIZED": - 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: "" }; - } - const 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: "" - }; - } - const 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 v2 of message.translationHelp) { - VideoTranslationHelpObject.encode(v2, 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((e2) => VideoTranslationHelpObject.fromJSON(e2)) : [], - 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((e2) => VideoTranslationHelpObject.toJSON(e2)); - } - 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((e2) => VideoTranslationHelpObject.fromPartial(e2)) || []; - 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 - }; - } - const 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 }; - } - const 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: "" }; - } - const 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 }; - } - const 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: "" }; - } - const 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 }; - } - const 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 }; - } - const 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 }; - } - const 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: [] }; - } - const VideoTranslationAudioResponse = { - encode(message, writer = new BinaryWriter()) { - if (message.status !== 0) { - writer.uint32(8).int32(message.status); - } - for (const v2 of message.remainingChunks) { - writer.uint32(18).string(v2); - } - 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((e2) => globalThis.String(e2)) : [] - }; - }, - 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((e2) => e2) || []; - return message; - } - }; - function createBaseSubtitlesObject() { - return { language: "", url: "", unknown0: 0, translatedLanguage: "", translatedUrl: "", unknown1: 0, unknown2: 0 }; - } - const 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: "" }; - } - const 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: [] }; - } - const SubtitlesResponse = { - encode(message, writer = new BinaryWriter()) { - if (message.waiting !== false) { - writer.uint32(8).bool(message.waiting); - } - for (const v2 of message.subtitles) { - SubtitlesObject.encode(v2, 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((e2) => SubtitlesObject.fromJSON(e2)) : [] - }; - }, - toJSON(message) { - const obj = {}; - if (message.waiting !== false) { - obj.waiting = message.waiting; - } - if (message.subtitles?.length) { - obj.subtitles = message.subtitles.map((e2) => SubtitlesObject.toJSON(e2)); - } - return obj; - }, - create(base) { - return SubtitlesResponse.fromPartial(base ?? {}); - }, - fromPartial(object) { - const message = createBaseSubtitlesResponse(); - message.waiting = object.waiting ?? false; - message.subtitles = object.subtitles?.map((e2) => SubtitlesObject.fromPartial(e2)) || []; - return message; - } - }; - function createBaseStreamTranslationObject() { - return { url: "", timestamp: "" }; - } - const 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 }; - } - const 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 }; - } - const 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 }; - } - const 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: "" }; - } - const 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 }; - } - const 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 i2 = 0; i2 < bin.length; ++i2) { - arr[i2] = bin.charCodeAt(i2); - } - 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; - } - const scriptRel = (function detectScriptRel() { - const relList = typeof document !== "undefined" && document.createElement("link").relList; - return relList && relList.supports && relList.supports("modulepreload") ? "modulepreload" : "preload"; - })(); - const assetsURL = function(dep) { - return "/" + dep; - }; - const seen = {}; - const __vitePreload = function preload(baseModule, deps, importerUrl) { - let promise = Promise.resolve(); - if (deps && deps.length > 0) { - let allSettled = function(promises$2) { - return Promise.all(promises$2.map((p2) => Promise.resolve(p2).then((value$1) => ({ - status: "fulfilled", - value: value$1 - }), (reason) => ({ - status: "rejected", - reason - })))); - }; - document.getElementsByTagName("link"); - const cspNonceMeta = document.querySelector("meta[property=csp-nonce]"); - const cspNonce = cspNonceMeta?.nonce || cspNonceMeta?.getAttribute("nonce"); - promise = allSettled(deps.map((dep) => { - dep = assetsURL(dep); - if (dep in seen) return; - seen[dep] = true; - const isCss = dep.endsWith(".css"); - const cssSelector = isCss ? '[rel="stylesheet"]' : ""; - if (document.querySelector(`link[href="${dep}"]${cssSelector}`)) return; - const link = document.createElement("link"); - link.rel = isCss ? "stylesheet" : scriptRel; - if (!isCss) link.as = "script"; - link.crossOrigin = ""; - link.href = dep; - if (cspNonce) link.setAttribute("nonce", cspNonce); - document.head.appendChild(link); - if (isCss) return new Promise((res, rej) => { - link.addEventListener("load", res); - link.addEventListener("error", () => rej( new Error(`Unable to preload CSS for ${dep}`))); - }); - })); - } - function handlePreloadError(err$2) { - const e$1 = new Event("vite:preloadError", { cancelable: true }); - e$1.payload = err$2; - window.dispatchEvent(e$1); - if (!e$1.defaultPrevented) throw err$2; - } - return promise.then((res) => { - for (const item of res || []) { - if (item.status !== "rejected") continue; - handlePreloadError(item.reason); - } - return baseModule().catch(handlePreloadError); - }); - }; - var LoggerLevel; - (function(LoggerLevel2) { - LoggerLevel2[LoggerLevel2["DEBUG"] = 0] = "DEBUG"; - LoggerLevel2[LoggerLevel2["INFO"] = 1] = "INFO"; - LoggerLevel2[LoggerLevel2["WARN"] = 2] = "WARN"; - LoggerLevel2[LoggerLevel2["ERROR"] = 3] = "ERROR"; - LoggerLevel2[LoggerLevel2["SILENCE"] = 4] = "SILENCE"; - })(LoggerLevel || (LoggerLevel = {})); - const prefix = `[vot.js v${votConfig.version}]`; - function canLog(level) { - return votConfig.loggerLevel <= level; - } - function log$1(...messages) { - if (!canLog(LoggerLevel.DEBUG)) { - return; - } - console.log(prefix, ...messages); - } - function info(...messages) { - if (!canLog(LoggerLevel.INFO)) { - return; - } - console.info(prefix, ...messages); - } - function warn$1(...messages) { - if (!canLog(LoggerLevel.WARN)) { - return; - } - console.warn(prefix, ...messages); - } - function error$1(...messages) { - if (!canLog(LoggerLevel.ERROR)) { - return; - } - console.error(prefix, ...messages); - } - const Logger = { - canLog, - log: log$1, - info, - warn: warn$1, - error: error$1 - }; - const { componentVersion } = votConfig; - async function getCrypto() { - if (typeof window !== "undefined" && window.crypto) { - return window.crypto; - } - return await __vitePreload(() => module.import('./__vite-browser-external-2Ng8QIWW-Xya9USxv.js'), void 0 ); - } - const utf8Encoder = new TextEncoder(); - async function signHMAC(hashName, hmac, data) { - const crypto2 = await getCrypto(); - const key = await crypto2.subtle.importKey("raw", utf8Encoder.encode(hmac), { name: "HMAC", hash: { name: hashName } }, false, ["sign", "verify"]); - return await crypto2.subtle.sign("HMAC", key, data); - } - async function getSignature(body) { - const signature = await signHMAC("SHA-256", votConfig.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 tokenBody = utf8Encoder.encode(token); - const tokenSign = await getSignature(tokenBody); - 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 i2 = 0; i2 < 32; i2++) { - const randomDigit = Math.floor(Math.random() * 16); - uuid += hexDigits[randomDigit]; - } - return uuid; - } - async function getHmacSha1(hmacKey, salt) { - try { - const hmacSalt = utf8Encoder.encode(salt); - const signature = await signHMAC("SHA-1", hmacKey, hmacSalt); - return btoa(String.fromCharCode(...new Uint8Array(signature))); - } catch (err) { - Logger.error(err); - return false; - } - } - const browserSecHeaders = { - "sec-ch-ua": `"Chromium";v="142", "YaBrowser";v="${componentVersion.slice(0, 5)}", "Not?A_Brand";v="24", "Yowser";v="2.5"`, - "sec-ch-ua-full-version-list": `"Chromium";v="142.0.7444.59", "YaBrowser";v="${componentVersion}", "Not?A_Brand";v="24.0.0.0", "Yowser";v="2.5"`, - "Sec-Fetch-Mode": "no-cors" - }; - const 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": votConfig.userAgent - } - }) { - const { timeout: timeout2 = 3e3, signal, ...fetchOptions } = options; - if (!signal && (!timeout2 || timeout2 <= 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 (timeout2 && timeout2 > 0) { - timeoutId = setTimeout(() => abort(new Error("Fetch timeout")), timeout2); - } - 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(lang2) { - if (lang2.length === 3) { - return iso6392to6391[lang2]; - } - return lang2.toLowerCase().split(/[_;-]/)[0].trim(); - } - function proxyMedia(url, format = "mp4") { - const generalUrl = `https://${votConfig.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}`; - } - function buildVkVideoUrl$1(videoId, sourceUrl) { - const cleanedVideoId = videoId.replace(/^\/+/, ""); - const out = new URL("https://vk.com/video"); - out.searchParams.set("z", cleanedVideoId); - for (const key of ["list", "access_key"]) { - const value = sourceUrl.searchParams.get(key); - if (value) { - out.searchParams.set(key, value); - } - } - return out.toString(); - } - 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)); - } - const 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)); - } - const YandexSessionProtobuf = { - encodeSessionRequest, - decodeSessionResponse - }; - var VideoTranslationStatus; - (function(VideoTranslationStatus2) { - VideoTranslationStatus2[VideoTranslationStatus2["FAILED"] = 0] = "FAILED"; - VideoTranslationStatus2[VideoTranslationStatus2["FINISHED"] = 1] = "FINISHED"; - VideoTranslationStatus2[VideoTranslationStatus2["WAITING"] = 2] = "WAITING"; - VideoTranslationStatus2[VideoTranslationStatus2["LONG_WAITING"] = 3] = "LONG_WAITING"; - VideoTranslationStatus2[VideoTranslationStatus2["PART_CONTENT"] = 5] = "PART_CONTENT"; - VideoTranslationStatus2[VideoTranslationStatus2["AUDIO_REQUESTED"] = 6] = "AUDIO_REQUESTED"; - VideoTranslationStatus2[VideoTranslationStatus2["SESSION_REQUIRED"] = 7] = "SESSION_REQUIRED"; - })(VideoTranslationStatus || (VideoTranslationStatus = {})); - var AudioDownloadType; - (function(AudioDownloadType2) { - AudioDownloadType2["WEB_API_VIDEO_SRC_FROM_IFRAME"] = "web_api_video_src_from_iframe"; - AudioDownloadType2["WEB_API_VIDEO_SRC"] = "web_api_video_src"; - AudioDownloadType2["WEB_API_GET_ALL_GENERATING_URLS_DATA_FROM_IFRAME"] = "web_api_get_all_generating_urls_data_from_iframe"; - AudioDownloadType2["WEB_API_GET_ALL_GENERATING_URLS_DATA_FROM_IFRAME_TMP_EXP"] = "web_api_get_all_generating_urls_data_from_iframe_tmp_exp"; - AudioDownloadType2["WEB_API_REPLACED_FETCH_INSIDE_IFRAME"] = "web_api_replaced_fetch_inside_iframe"; - AudioDownloadType2["ANDROID_API"] = "android_api"; - AudioDownloadType2["WEB_API_SLOW"] = "web_api_slow"; - AudioDownloadType2["WEB_API_STEAL_SIG_AND_N"] = "web_api_steal_sig_and_n"; - AudioDownloadType2["WEB_API_COMBINED"] = "web_api_get_all_generating_urls_data_from_iframe,web_api_steal_sig_and_n"; - })(AudioDownloadType || (AudioDownloadType = {})); - var VideoService; - (function(VideoService2) { - VideoService2["custom"] = "custom"; - VideoService2["directlink"] = "custom"; - VideoService2["youtube"] = "youtube"; - VideoService2["piped"] = "piped"; - VideoService2["invidious"] = "invidious"; - VideoService2["niconico"] = "niconico"; - VideoService2["vk"] = "vk"; - VideoService2["nine_gag"] = "nine_gag"; - VideoService2["gag"] = "nine_gag"; - VideoService2["twitch"] = "twitch"; - VideoService2["proxitok"] = "proxitok"; - VideoService2["tiktok"] = "tiktok"; - VideoService2["vimeo"] = "vimeo"; - VideoService2["xvideos"] = "xvideos"; - VideoService2["xhamster"] = "xhamster"; - VideoService2["spankbang"] = "spankbang"; - VideoService2["rule34video"] = "rule34video"; - VideoService2["picarto"] = "picarto"; - VideoService2["olympicsreplay"] = "olympics_replay"; - VideoService2["pornhub"] = "pornhub"; - VideoService2["twitter"] = "twitter"; - VideoService2["x"] = "twitter"; - VideoService2["rumble"] = "rumble"; - VideoService2["facebook"] = "facebook"; - VideoService2["rutube"] = "rutube"; - VideoService2["coub"] = "coub"; - VideoService2["bilibili"] = "bilibili"; - VideoService2["mail_ru"] = "mailru"; - VideoService2["mailru"] = "mailru"; - VideoService2["bitchute"] = "bitchute"; - VideoService2["eporner"] = "eporner"; - VideoService2["peertube"] = "peertube"; - VideoService2["dailymotion"] = "dailymotion"; - VideoService2["trovo"] = "trovo"; - VideoService2["yandexdisk"] = "yandexdisk"; - VideoService2["ok_ru"] = "okru"; - VideoService2["okru"] = "okru"; - VideoService2["googledrive"] = "googledrive"; - VideoService2["bannedvideo"] = "bannedvideo"; - VideoService2["weverse"] = "weverse"; - VideoService2["weibo"] = "weibo"; - VideoService2["newgrounds"] = "newgrounds"; - VideoService2["egghead"] = "egghead"; - VideoService2["youku"] = "youku"; - VideoService2["archive"] = "archive"; - VideoService2["kodik"] = "kodik"; - VideoService2["patreon"] = "patreon"; - VideoService2["reddit"] = "reddit"; - VideoService2["kick"] = "kick"; - VideoService2["apple_developer"] = "apple_developer"; - VideoService2["appledeveloper"] = "apple_developer"; - VideoService2["epicgames"] = "epicgames"; - VideoService2["odysee"] = "odysee"; - VideoService2["coursehunterLike"] = "coursehunterLike"; - VideoService2["sap"] = "sap"; - VideoService2["watchpornto"] = "watchpornto"; - VideoService2["linkedin"] = "linkedin"; - VideoService2["incestflix"] = "incestflix"; - VideoService2["porntn"] = "porntn"; - VideoService2["dzen"] = "dzen"; - VideoService2["bunnystream"] = "bunnystream"; - VideoService2["cloudflarestream"] = "cloudflarestream"; - VideoService2["loom"] = "loom"; - VideoService2["rtnews"] = "rtnews"; - VideoService2["bitview"] = "bitview"; - VideoService2["thisvid"] = "thisvid"; - VideoService2["ign"] = "ign"; - VideoService2["zdf"] = "zdf"; - VideoService2["bunkr"] = "bunkr"; - VideoService2["imdb"] = "imdb"; - VideoService2["telegram"] = "telegram"; - })(VideoService || (VideoService = {})); - function convertVOT(service, videoId, url) { - if (service === VideoService.patreon) { - return { - service: "mux", - videoId: new URL(url).pathname.slice(1) - }; - } - return { - service, - videoId - }; - } - class VOTJSError extends Error { - data; - constructor(message, data = void 0) { - super(message); - this.data = data; - this.name = "VOTJSError"; - } - } - class MinimalClient { - host; - schema; - fetch; - fetchOpts; - sessions = {}; - userAgent = votConfig.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 = votConfig.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); - } - const sessionResponse = YandexSessionProtobuf.decodeSessionResponse(res.data); - return { - ...sessionResponse, - uuid - }; - } - } - let VOTClient$1 = class VOTClient 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/${votConfig.version}`, - "Content-Type": "application/json", - Pragma: "no-cache", - "Cache-Control": "no-cache" - }; - constructor({ host, hostVOT = votConfig.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 = votConfig.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 = votConfig.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: url2, translatedLanguage, translatedUrl } = subtitle; - return { - language, - url: url2, - 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; - const 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; - }, []); - return { - waiting: false, - subtitles - }; - } - 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); - } - } - }; - let VOTWorkerClient$1 = class VOTWorkerClient extends VOTClient$1 { - constructor(opts = {}) { - opts.host = opts.host ?? votConfig.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 - }; - } - } - }; - class VOTClient2 extends VOTClient$1 { - constructor(opts) { - super(opts); - this.headers = { - ...browserSecHeaders, - ...this.headers - }; - } - } - class VOTWorkerClient2 extends VOTWorkerClient$1 { - constructor(opts) { - super(opts); - this.headers = { - ...browserSecHeaders, - ...this.headers - }; - } - } - class VideoDataError extends Error { - constructor(message) { - super(message); - this.name = "VideoDataError"; - } - } - const localLinkRe = /(file:\/\/(\/)?|(http(s)?:\/\/)(127\.0\.0\.1|localhost|192\.168\.(\d){1,3}\.(\d){1,3}))/; - const sitesInvidious = [ - "yewtu.be", - "inv.nadeko.net", - "invidious.nerdvpn.de", - "invidious.protokolla.fi", - "invidious.materialio.us", - "iv.melmac.space" - ]; - const sitesPiped = [ - "piped.video", - "piped.kavin.rocks", - "piped.private.coffee" - ]; - const 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" - ]; - const 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" - ]; - const sitesCoursehunterLike = ["coursehunter.net", "coursetrain.net"]; - var ExtVideoService; - (function(ExtVideoService2) { - ExtVideoService2["udemy"] = "udemy"; - ExtVideoService2["coursera"] = "coursera"; - ExtVideoService2["douyin"] = "douyin"; - ExtVideoService2["artstation"] = "artstation"; - ExtVideoService2["kickstarter"] = "kickstarter"; - ExtVideoService2["oraclelearn"] = "oraclelearn"; - ExtVideoService2["deeplearningai"] = "deeplearningai"; - ExtVideoService2["netacad"] = "netacad"; - })(ExtVideoService || (ExtVideoService = {})); - ({ - ...VideoService, - ...ExtVideoService - }); - const sharedSelectors = { - bilibiliPlayer: ".bpx-player-video-wrap, div.player-mobile-box.player-mobile-autoplay", - flowplayer: ".fp-player", - idPlayer: "#player", - jwPlayer: ".jwplayer, .jw-media", - player: ".player", - 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" - }; - const sites = [ - { - additionalData: "mobile", - host: VideoService.youtube, - url: "https://youtu.be/", - match: /^m.youtube.com$/, - selector: ".player-container", - needExtraData: true - }, - { - host: VideoService.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.youtube, - url: "https://youtu.be/", - match: /^(www.)?youtube(-nocookie|kids)?.com$/, - selector: ".html5-video-container:not(#inline-player *)", - needExtraData: true - }, - { - host: VideoService.invidious, - url: "https://youtu.be/", - match: sitesInvidious, - selector: sharedSelectors.idPlayer, - needBypassCSP: true - }, - { - host: VideoService.piped, - url: "https://youtu.be/", - match: sitesPiped, - selector: ".shaka-video-container", - needBypassCSP: true - }, - { - host: VideoService.zdf, - url: "https://www.zdf.de/play/", - match: [/^zdf.de$/, /^(www.)?zdf.de$/], - selector: "div.zdfplayer-app.zdfplayer-desktop, div.zdfplayer-app" - }, - { - host: VideoService.niconico, - url: "https://www.nicovideo.jp/watch/", - match: [/^(www\.|sp\.)?nicovideo\.jp$/, /^nico\.ms$/], - selector: `[class*="grid-area_[player]"] > div` - }, - { - additionalData: "mobile", - host: VideoService.vk, - url: "https://vk.com/video?z=", - match: [/^m.vk.(com|ru)$/, /^m.vkvideo.ru$/], - selector: sharedSelectors.vkVideoPlayer, - shadowRoot: true, - needExtraData: true - }, - { - additionalData: "clips", - host: VideoService.vk, - url: "https://vk.com/video?z=", - match: /^(www.|m.)?vk.(com|ru)$/, - selector: 'div[data-testid="clipcontainer-video"]', - needExtraData: true - }, - { - host: VideoService.vk, - url: "https://vk.com/video?z=", - match: [/^(www\.|m\.)?vk\.(com|ru)$/, /^(.*\.)?vkvideo\.ru$/], - selector: sharedSelectors.vkVideoPlayer, - needExtraData: true - }, - { - host: VideoService.nine_gag, - url: "https://9gag.com/gag/", - match: /^9gag.com$/, - selector: ".video-post", - needExtraData: true - }, - { - host: VideoService.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.proxitok, - url: "https://www.tiktok.com/", - match: sitesProxiTok, - selector: ".column.has-text-centered" - }, - { - host: VideoService.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.vimeo, - url: "https://vimeo.com/", - match: /^(www\.|m\.)?vimeo.com$/, - needExtraData: true, - selector: sharedSelectors.player - }, - { - host: VideoService.vimeo, - url: "https://player.vimeo.com/", - match: /^player.vimeo.com$/, - additionalData: "embed", - needExtraData: true, - needBypassCSP: true, - selector: sharedSelectors.player - }, - { - host: VideoService.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.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.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.rule34video, - url: "https://rule34video.com/video/", - match: (url) => /^(www\.)?rule34video\.com$/.test(url.host) && /\/videos?\/\d+/.test(url.pathname), - selector: sharedSelectors.flowplayer - }, - { - host: VideoService.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.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.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.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.twitter, - url: "https://twitter.com/i/status/", - match: /^(twitter|x).com$/, - selector: 'div[data-testid="videoComponent"] > div:nth-child(1) > div', - eventSelector: 'div[data-testid="videoPlayer"]', - needBypassCSP: true - }, - { - host: VideoService.rumble, - url: "https://rumble.com/", - match: /^rumble.com$/, - selector: "#videoPlayer > .videoPlayer-Rumble-cls > div" - }, - { - host: VideoService.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.facebook, - url: "https://facebook.com/", - match: (url) => url.host.includes("facebook.com") && url.pathname.includes("/reel/"), - selector: 'div[role="main"]', - needBypassCSP: true - }, - { - host: VideoService.rutube, - url: "https://rutube.ru/video/", - match: /^rutube.ru$/, - selector: ".video-player > div > div > div:nth-child(2)" - }, - { - additionalData: "embed", - host: VideoService.rutube, - url: "https://rutube.ru/video/", - match: /^rutube.ru$/, - selector: "#app > div > div" - }, - { - host: VideoService.bilibili, - url: "https://www.bilibili.com/", - match: /^(www|m|player).bilibili.com$/, - selector: sharedSelectors.bilibiliPlayer - }, - { - host: VideoService.bilibili, - url: "https://www.bilibili.tv/", - match: /^(?:www\.|m\.)?bilibili\.tv$/, - selector: sharedSelectors.bilibiliPlayer - }, - { - additionalData: "old", - host: VideoService.bilibili, - url: "https://www.bilibili.com/", - match: /^(www|m).bilibili.com$/, - selector: null - }, - { - host: VideoService.mailru, - url: "https://my.mail.ru/", - match: /^my.mail.ru$/, - selector: "#b-video-wrapper" - }, - { - host: VideoService.bitchute, - url: "https://www.bitchute.com/video/", - match: /^(www.)?bitchute.com$/, - selector: sharedSelectors.videoJsUniversal - }, - { - host: VideoService.eporner, - url: "https://www.eporner.com/", - match: /^(www.)?eporner.com$/, - selector: sharedSelectors.videoJsUniversal - }, - { - host: VideoService.peertube, - url: "stub", - match: sitesPeertube, - selector: sharedSelectors.videoJsUniversal - }, - { - host: VideoService.dailymotion, - url: "https://www.dailymotion.com/video/", - match: /^((www\.|player\.)?dailymotion\.com|geo(\d+)?\.dailymotion\.com|dai\.ly)$/, - selector: sharedSelectors.player - }, - { - host: VideoService.trovo, - url: "https://trovo.live/s/", - match: /^trovo.live$/, - selector: ".player-video" - }, - { - host: VideoService.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)", - eventSelector: ".video-player__player", - needBypassCSP: true, - needExtraData: true - }, - { - host: VideoService.okru, - url: "https://ok.ru/video/", - match: /^ok.ru$/, - selector: sharedSelectors.vkVideoPlayer, - shadowRoot: true - }, - { - host: VideoService.googledrive, - url: "https://drive.google.com/file/d/", - match: /^youtube.googleapis.com$/, - selector: ".html5-video-container" - }, - { - host: VideoService.bannedvideo, - url: "https://madmaxworld.tv/watch?id=", - match: /^(www.)?banned.video|madmaxworld.tv$/, - selector: sharedSelectors.videoJsUniversal, - needExtraData: true - }, - { - host: VideoService.weverse, - url: "https://weverse.io/", - match: /^weverse.io$/, - selector: ".webplayer-internal-source-wrapper", - needExtraData: true - }, - { - host: VideoService.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 - }, - { - host: VideoService.newgrounds, - url: "https://www.newgrounds.com/", - match: /^(www.)?newgrounds.com$/, - selector: ".ng-video-player" - }, - { - host: VideoService.egghead, - url: "https://egghead.io/", - match: /^egghead.io$/, - selector: ".cueplayer-react-video-holder" - }, - { - host: VideoService.youku, - url: "https://v.youku.com/", - match: /^v.youku.com$/, - selector: "#ykPlayer" - }, - { - host: VideoService.archive, - url: "https://archive.org/details/", - match: /^archive.org$/, - selector: sharedSelectors.jwPlayer - }, - { - host: VideoService.kodik, - url: "stub", - match: /^kodik.(info|biz|cc)$/, - selector: sharedSelectors.flowplayer, - needExtraData: true - }, - { - host: VideoService.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.reddit, - url: "stub", - match: /^old.reddit.com$/, - selector: ".reddit-video-player-root", - needExtraData: true, - needBypassCSP: true - }, - { - host: VideoService.reddit, - url: "stub", - match: /^(www.|new.)?reddit.com$/, - selector: "div[slot=post-media-container]", - shadowRoot: true, - needExtraData: true, - needBypassCSP: true - }, - { - host: VideoService.kick, - url: "https://kick.com/", - match: /^kick.com$/, - selector: "#injected-embedded-channel-player-video > div", - needExtraData: true - }, - { - host: VideoService.appledeveloper, - url: "https://developer.apple.com/", - match: /^developer.apple.com$/, - selector: ".developer-video-player", - needExtraData: true, - needBypassCSP: true - }, - { - host: VideoService.epicgames, - url: "https://dev.epicgames.com/community/learning/", - match: /^dev.epicgames.com$/, - selector: sharedSelectors.videoJsUniversal, - needExtraData: true - }, - { - host: VideoService.odysee, - url: "stub", - match: /^odysee.com$/, - selector: sharedSelectors.videoJsUniversal, - needExtraData: true - }, - { - host: VideoService.coursehunterLike, - url: "stub", - match: sitesCoursehunterLike, - selector: "#oframeplayer > pjsdiv:has(video)", - needExtraData: true - }, - { - host: VideoService.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: 'div[data-purpose="curriculum-item-viewer-content"] > section > div > div > div > div:nth-of-type(2)', - needExtraData: true - }, - { - host: ExtVideoService.coursera, - url: "https://www.coursera.org/", - match: /coursera.org$/, - selector: sharedSelectors.videoJsUniversal, - needExtraData: true - }, - { - host: VideoService.watchpornto, - url: "https://watchporn.to/", - match: /^watchporn.to$/, - selector: sharedSelectors.flowplayer - }, - { - host: VideoService.linkedin, - url: "https://www.linkedin.com/learning/", - match: /^(www.)?linkedin.com$/, - selector: sharedSelectors.videoJsUniversal, - needExtraData: true, - needBypassCSP: true - }, - { - host: VideoService.incestflix, - url: "https://www.incestflix.net/watch/", - match: /^(www.)?incestflix.(net|to|com)$/, - selector: "#incflix-stream", - needExtraData: true - }, - { - host: VideoService.porntn, - url: "https://porntn.com/videos/", - match: /^porntn.com$/, - selector: sharedSelectors.flowplayer, - needExtraData: true - }, - { - host: VideoService.dzen, - url: "https://dzen.ru/video/watch/", - match: /^dzen.ru$/, - selector: `[class*="player__playerWrap"] > div` - }, - { - host: VideoService.bunnystream, - url: "stub", - match: [ - /^video\.bunnycdn\.com$/, - /^iframe\.mediadelivery\.net$/, - /^(?:[^.]+\.)*b-cdn\.net$/ - ], - selector: null - }, - { - host: VideoService.cloudflarestream, - url: "stub", - match: /^(watch|embed|iframe|customer-[^.]+).cloudflarestream.com$/, - selector: null - }, - { - host: VideoService.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.rtnews, - url: "https://www.rt.com/", - match: /^(www.)?rt.com$/, - selector: sharedSelectors.jwPlayer, - needExtraData: true - }, - { - host: VideoService.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.thisvid, - url: "https://thisvid.com/", - match: /^(www.)?thisvid.com$/, - selector: sharedSelectors.flowplayer - }, - { - additionalData: "regional", - host: VideoService.ign, - url: "https://de.ign.com/", - match: /^(\w{2}.)?ign.com$/, - needExtraData: true, - selector: ".video-container" - }, - { - host: VideoService.ign, - url: "https://www.ign.com/", - match: /^(www.)?ign.com$/, - selector: sharedSelectors.player, - needExtraData: true - }, - { - host: VideoService.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.imdb, - url: "https://www.imdb.com/video/", - match: /^(www\.)?imdb\.com$/, - selector: sharedSelectors.jwPlayer - }, - { - host: VideoService.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: VideoService.custom, - url: "stub", - match: (url) => /([^.]+)\.(mp4|webm)/.test(url.pathname), - rawResult: true - } - ]; - class VideoHelperError extends Error { - constructor(message) { - super(message); - this.name = "VideoHelperError"; - } - } - class BaseHelper { - 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 void 0; - } - return { - url: this.service.url + videoId, - videoId, - host: this.service.host, - duration: void 0 - }; - } - } - class AppleDeveloperHelper extends BaseHelper { - API_ORIGIN = "https://developer.apple.com"; - async getVideoData(videoId) { - try { - const contentUrl2 = document.querySelector("meta[property='og:video']")?.content; - if (!contentUrl2) { - throw new VideoHelperError("Failed to find content url"); - } - return { - url: contentUrl2 - }; - } catch (err) { - Logger.error(`Failed to get apple developer video data by video ID: ${videoId}`, err.message); - return void 0; - } - } - async getVideoId(url) { - return /videos\/play\/([^/]+)\/([\d]+)/.exec(url.pathname)?.[0]; - } - } - class ArchiveHelper extends BaseHelper { - async getVideoId(url) { - return /(details|embed)\/([^/]+)/.exec(url.pathname)?.[2]; - } - } - class ArtstationHelper 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(); - const res = await this.fetch(`${this.API_ORIGIN}/courses/${courseId}/autoplay.json`, { - method: "POST", - headers: csrfToken ? { - "PUBLIC-CSRF-TOKEN": csrfToken - } : {} - }); - return await res.json(); - } catch (err) { - Logger.error(`Failed to get artstation course info by courseId: ${courseId}.`, err.message); - return false; - } - } - async getVideoUrl(chapterId) { - try { - const res = await this.fetch(`${this.API_ORIGIN}/quicksilver/video_url.json?chapter_id=${chapterId}`); - const data = await res.json(); - return data.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 void 0; - } - const chapter = courseInfo.chapters.find((chapter2) => chapter2.hash_id === chapterId); - if (!chapter) { - return void 0; - } - const videoUrl = await this.getVideoUrl(chapter.id); - if (!videoUrl) { - return void 0; - } - const { title, duration, subtitles: videoSubtitles } = chapter; - const subtitles = videoSubtitles.filter((subtitle) => subtitle.format === "vtt").map((subtitle) => ({ - language: normalizeLang$1(subtitle.locale), - source: "artstation", - format: "vtt", - url: subtitle.file_url - })); - return { - url: videoUrl, - title, - duration, - subtitles - }; - } - async getVideoId(url) { - return /courses\/(\w{3,5})\/([^/]+)\/chapters\/(\w{3,5})/.exec(url.pathname)?.[0]; - } - } - class BannedVideoHelper extends BaseHelper { - API_ORIGIN = "https://api.banned.video"; - async getVideoInfo(videoId) { - try { - const res = await this.fetch(`${this.API_ORIGIN}/graphql`, { - method: "POST", - body: JSON.stringify({ - operationName: "GetVideo", - query: `query GetVideo($id: String!) { +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.toil.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.1.981 Yowser/2.5 Safari/537.36", + "componentVersion": "26.3.1.981", + "hmac": "bt8xH3VOlb4mqf0nqAibnDOoiPlXsisf", + "defaultDuration": 310, + "minChunkSize": 5295308, + "loggerLevel": 1, + "version": "2.4.16" + }; + //#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}`; + } + function buildVkVideoUrl$1(videoId, sourceUrl) { + const cleanedVideoId = videoId.replace(/^\/+/, ""); + const out = new URL("https://vk.com/video"); + out.searchParams.set("z", cleanedVideoId); + for (const key of ["list", "access_key"]) { + const value = sourceUrl.searchParams.get(key); + if (value) out.searchParams.set(key, value); + } + return out.toString(); + } + //#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 || (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: /^(www.)?youtube(-nocookie|kids)?.com$/, + selector: ".html5-video-container:not(#inline-player *)", + 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/video?z=", + 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/video?z=", + match: /^(www.|m.)?vk.(com|ru)$/, + selector: "div[data-testid=\"clipcontainer-video\"]", + needExtraData: true + }, + { + host: VideoService$1.vk, + url: "https://vk.com/video?z=", + 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)", + eventSelector: ".video-player__player", + 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: ".html5-video-container" + }, + { + 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 + }, + { + host: VideoService$1.kodik, + url: "stub", + match: /^kodik.(info|biz|cc)$/, + 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: sharedSelectors.videoJsUniversal, + 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: 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) { + return /(details|embed)\/([^/]+)/.exec(url.pathname)?.[2]; + } + }; + //#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 @@ -4836,11913 +4421,11172 @@ string() { isStream: live } }`, - variables: { - id: videoId - } - }), - headers: { - "User-Agent": "bannedVideoFrontEnd", - "apollographql-client-name": "banned-web", - "apollographql-client-version": "1.3", - "content-type": "application/json" - } - }); - return await res.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 void 0; - } - 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; - } - } - class BilibiliHelper 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; - } - } - class BitchuteHelper extends BaseHelper { - async getVideoId(url) { - return /(video|embed)\/([^/]+)/.exec(url.pathname)?.[2]; - } - } - class BitviewHelper 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 void 0; - } - } - async getVideoId(url) { - return url.searchParams.get("v"); - } - } - class BunkrHelper extends BaseHelper { - async getVideoData(_videoId) { - const url = document.querySelector('#player > source[type="video/mp4"]')?.src; - if (!url) { - return void 0; - } - return { - url - }; - } - async getVideoId(url) { - return /\/f\/([^/]+)/.exec(url.pathname)?.[1]; - } - } - class BunnyStreamHelper extends BaseHelper { - async getVideoId(url) { - return url.pathname + url.search; - } - } - class CloudflareStreamHelper extends BaseHelper { - async getVideoId(url) { - return url.pathname + url.search; - } - } - class CoursehunterLikeHelper 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 { - const res = await this.fetch(`${this.API_ORIGIN}/api/v1/course/${courseId}/lessons`); - return await res.json(); - } catch (err) { - Logger.error(`Failed to get CoursehunterLike lessons data by courseId: ${courseId}, because ${err.message}`); - return void 0; - } - } - getLessondId(videoId) { - let lessondId = videoId.split("?lesson=")?.[1]; - if (lessondId) { - return +lessondId; - } - const activeLessondEl = document.querySelector(".lessons-item_active"); - lessondId = activeLessondEl?.dataset?.index; - if (lessondId) { - return +lessondId; - } - return 1; - } - async getVideoData(videoId) { - const courseId = await this.getCourseId(); - if (!courseId) { - return void 0; - } - const lessonsData = await this.getLessonsData(courseId); - if (!lessonsData) { - return void 0; - } - const lessonId = this.getLessondId(videoId); - const currentLesson = lessonsData?.[lessonId - 1]; - const { file: videoUrl, duration, title } = currentLesson; - if (!videoUrl) { - return void 0; - } - return { - url: proxyMedia(videoUrl), - duration, - title - }; - } - async getVideoId(url) { - const courseId = /course\/([^/]+)/.exec(url.pathname)?.[0]; - return courseId ? courseId + url.search : void 0; - } - } - const availableLangs = [ - "auto", - "ru", - "en", - "zh", - "ko", - "lt", - "lv", - "ar", - "fr", - "it", - "es", - "de", - "ja" - ]; - const availableTTS = ["ru", "en", "kk"]; - 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, -"_html5_api".length) : void 0; - if (vjs?.getPlayer) { - if (derivedPlayerId) { - const p2 = vjs.getPlayer(derivedPlayerId); - if (p2) - return p2; - } - if (techEl) { - const p2 = vjs.getPlayer(techEl); - if (p2) - return p2; - } - } - const players = (typeof vjs?.getPlayers === "function" ? vjs.getPlayers() : vjs?.players) ?? {}; - for (const p2 of Object.values(players)) { - const player2 = p2; - const el = typeof player2.el === "function" ? player2.el() : null; - const innerVideo = el?.querySelector?.("video.vjs-tech, video") ?? null; - if (innerVideo && techEl && innerVideo === techEl) { - return p2; - } - if (derivedPlayerId && typeof player2.id === "function" && player2.id() === derivedPlayerId) { - return p2; - } - } - return void 0; - } - getVideoDataByPlayer(videoId) { - try { - const player2 = VideoJSHelper.getPlayer(); - const techEl = document.querySelector("video.vjs-tech, video[id$='_html5_api'], video[src]"); - if (!player2 && !techEl) { - throw new Error(`Video player/video element not found, videoId ${videoId}`); - } - const duration = player2?.duration?.() ?? techEl?.duration; - let url; - if (player2) { - const sources = typeof player2.currentSources === "function" ? player2.currentSources() : player2.getCache?.()?.sources; - const videoUrl = Array.isArray(sources) ? sources.find((source) => source?.type === "video/mp4" || source?.type === "video/webm" || source?.src) : void 0; - url = videoUrl?.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 trackEls = techEl ? Array.from(techEl.querySelectorAll("track[src]")) : []; - const subtitles = trackEls.filter((t2) => t2.kind !== "metadata").flatMap((t2) => { - const src = t2.getAttribute("src"); - if (!src) { - return []; - } - const absUrl = new URL(src, window.location.href).toString(); - return [ - { - language: normalizeLang$1(t2.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 void 0; - } - } - } - class CourseraHelper extends VideoJSHelper { - API_ORIGIN = "https://www.coursera.org/api"; - SUBTITLE_SOURCE = "coursera"; - async getCourseData(courseId) { - try { - const response = await this.fetch(`${this.API_ORIGIN}/onDemandCourses.v1/${courseId}`); - const resJSON = await response.json(); - return resJSON?.elements?.[0]; - } catch (err) { - Logger.error(`Failed to get course data by courseId: ${courseId}`, err.message); - return void 0; - } - } - static getPlayer() { - return VideoJSHelper.getPlayer(); - } - async getVideoData(videoId) { - const data = this.getVideoDataByPlayer(videoId); - if (!data) { - return void 0; - } - 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 subtitleItem = data.subtitles.find((subtitle) => subtitle.language === courseLang) ?? data.subtitles?.[0]; - const subtitleUrl = subtitleItem?.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) { - const matched = /learn\/([^/]+)\/lecture\/([^/]+)/.exec(url.pathname) ?? /lecture\/([^/]+)\/([^/]+)/.exec(url.pathname); - return matched?.[0]; - } - } - class DailymotionHelper 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 timeout2 = setTimeout(() => { - window.removeEventListener("message", onMessage); - resolve(void 0); - }, 3e3); - const onMessage = (e2) => { - if (e2.origin !== origin) { - return; - } - if (!(typeof e2.data === "string" && e2.data.startsWith("getVideoId:"))) { - return; - } - clearTimeout(timeout2); - window.removeEventListener("message", onMessage); - resolve(e2.data.replace("getVideoId:", "")); - }; - window.addEventListener("message", onMessage); - window.top?.postMessage("getVideoId:", origin); - }); - } - async getVideoId(url) { - if (window.self !== window.top) { - return await this.resolveVideoIdViaPostMessage(); - } else { - return this.getVideoIdFromUrl(url); - } - } - } - class DeeplearningAIHelper extends BaseHelper { - async getVideoData(_videoId) { - if (!this.video) { - return void 0; - } - const sourceUrl = this.video.querySelector('source[type="application/x-mpegurl"]')?.src; - if (!sourceUrl) { - return void 0; - } - return { - url: sourceUrl - }; - } - async getVideoId(url) { - return /courses\/(([^/]+)\/lesson\/([^/]+)\/([^/]+))/.exec(url.pathname)?.[1]; - } - } - class DouyinHelper extends BaseHelper { - static getPlayer() { - if (typeof player === "undefined") { - return void 0; - } - return player; - } - async getVideoData(_videoId) { - const xgPlayer = DouyinHelper.getPlayer(); - if (!xgPlayer) { - return void 0; - } - const { config: { url: sources, duration, lang: lang2, isLive: isStream } } = xgPlayer; - if (!sources) { - return void 0; - } - const source = sources.find((s2) => s2.src.includes("www.douyin.com/aweme/v1/play/")); - if (!source) { - return void 0; - } - return { - url: proxyMedia(source.src), - duration, - isStream, - ...availableLangs.includes(lang2) ? { detectedLanguage: lang2 } : {} - }; - } - async getVideoId(url) { - const pathId = /video\/([\d]+)/.exec(url.pathname)?.[0]; - if (pathId) { - return pathId; - } - return DouyinHelper.getPlayer()?.config.vid; - } - } - class DzenHelper extends BaseHelper { - async getVideoId(url) { - return /video\/watch\/([^/]+)/.exec(url.pathname)?.[1]; - } - } - class EggheadHelper extends BaseHelper { - async getVideoId(url) { - return url.pathname.slice(1); - } - } - class EpicGamesHelper extends BaseHelper { - API_ORIGIN = "https://dev.epicgames.com/community/api/learning"; - async getPostInfo(videoId) { - try { - const res = await this.fetch(`${this.API_ORIGIN}/post.json?hash_id=${videoId}`); - return await res.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((s2) => videoUrlRe.exec(s2.innerHTML)); - if (!script) { - return void 0; - } - const content = script.innerHTML.trim(); - const playlistUrl = videoUrlRe.exec(content)?.[1]?.replace("qsep://", "https://"); - if (!playlistUrl) { - return void 0; - } - 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"); - const subtitlesObj = JSON.parse(subtitlesString); - const subtitles = subtitlesObj.filter((sub) => sub.type === "captions"); - return { - playlistUrl, - subtitles - }; - } catch { - return { - playlistUrl, - subtitles: [] - }; - } - } - async getVideoData(videoId) { - const courseId = videoId.split(":")?.[1]; - const postInfo = await this.getPostInfo(courseId); - if (!postInfo) { - return void 0; - } - const videoBlock = this.getVideoBlock(); - if (!videoBlock) { - return void 0; - } - const { playlistUrl, subtitles: videoSubtitles } = videoBlock; - const { title, description } = postInfo; - const subtitles = videoSubtitles.map((caption) => ({ - language: normalizeLang$1(caption.srclang), - source: "epicgames", - format: "vtt", - url: caption.src - })); - return { - url: playlistUrl, - title, - description, - subtitles - }; - } - async getVideoId(_url) { - return new Promise((resolve) => { - const origin = "https://dev.epicgames.com"; - const reqId = btoa(window.location.href); - window.addEventListener("message", (e2) => { - if (e2.origin !== origin) { - return void 0; - } - if (!(typeof e2.data === "string" && e2.data.startsWith("getVideoId:"))) { - return void 0; - } - const videoId = e2.data.replace("getVideoId:", ""); - return resolve(videoId); - }); - window.top?.postMessage(`getVideoId:${reqId}`, origin); - }); - } - } - class EpornerHelper extends BaseHelper { - async getVideoId(url) { - return /video-([^/]+)\/([^/]+)/.exec(url.pathname)?.[0]; - } - } - class FacebookHelper extends BaseHelper { - async getVideoId(url) { - return url.pathname.slice(1); - } - } - class GoogleDriveHelper extends BaseHelper { - getPlayerData() { - const playerEl = document.querySelector("#movie_player"); - return playerEl?.getVideoData?.() ?? void 0; - } - async getVideoId(_url) { - return this.getPlayerData()?.video_id; - } - } - class IgnHelper 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 data = JSON.parse(nextContent); - const { props: { pageProps: { page: { description, title, video: { videoMetadata: { duration }, assets } } } } } = data; - 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]; - } - } - class IMDbHelper extends BaseHelper { - async getVideoId(url) { - return /video\/([^/]+)/.exec(url.pathname)?.[1]; - } - } - class IncestflixHelper 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 void 0; - } - } - async getVideoId(url) { - return /\/watch\/([^/]+)/.exec(url.pathname)?.[1]; - } - } - class KickHelper extends BaseHelper { - API_ORIGIN = "https://kick.com/api"; - async getClipInfo(clipId) { - try { - const res = await this.fetch(`${this.API_ORIGIN}/v2/clips/${clipId}`); - const data = await res.json(); - const { clip_url: url, duration, title } = data.clip; - return { - url, - duration, - title - }; - } catch (err) { - Logger.error(`Failed to get kick clip info by clipId: ${clipId}.`, err.message); - return void 0; - } - } - async getVideoInfo(videoId) { - try { - const res = await this.fetch(`${this.API_ORIGIN}/v1/video/${videoId}`); - const data = await res.json(); - const { source: url, livestream } = data; - 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 void 0; - } - } - 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]; - } - } - class KickstarterHelper 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 lang2 = sub.getAttribute("srclang"); - const url2 = sub.getAttribute("src"); - if (!lang2 || !url2) { - return result; - } - result.push({ - language: normalizeLang$1(lang2), - url: url2, - format: "vtt", - source: "kickstarter" - }); - return result; - }, []) - }; - } catch (err) { - Logger.error(`Failed to get Kickstarter data by videoId: ${videoId}`, err.message); - return void 0; - } - } - async getVideoId(url) { - return url.pathname.slice(1); - } - } - class KodikHelper extends BaseHelper { - API_ORIGIN = window.location.origin; - getSecureData(videoPath) { - try { - const [videoType, videoId, hash] = videoPath.split("/").filter((a2) => a2); - const allScripts = Array.from(document.getElementsByTagName("script")); - const secureScript = allScripts.filter((s2) => s2.innerHTML.includes(`videoId = "${videoId}"`) || s2.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((s2) => s2.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: d2, d_sign, pd, pd_sign, ref, ref_sign } = secureData; - try { - const res = await this.fetch(`${this.API_ORIGIN}/ftor`, { - method: "POST", - headers: { - "User-Agent": votConfig.userAgent, - Origin: this.API_ORIGIN, - Referer: `${this.API_ORIGIN}/${videoType}/${id}/${hash}/360p` - }, - body: new URLSearchParams({ - d: d2, - d_sign, - pd, - pd_sign, - ref: decodeURIComponent(ref), - ref_sign, - bad_user: "false", - cdn_is_working: "true", - info: "{}", - type: videoType, - hash, - id - }) - }); - return await res.json(); - } catch (err) { - Logger.error(`Failed to get kodik video data (type: ${videoType}, id: ${id}, hash: ${hash})`, err.message); - return false; - } - } - decryptUrl(encryptedUrl) { - const decryptedUrl = atob(encryptedUrl.replace(/[a-zA-Z]/g, (e2) => { - const charCode = e2.charCodeAt(0) + 18; - const pos = e2 <= "Z" ? 90 : 122; - return String.fromCharCode(pos >= charCode ? charCode : charCode - 26); - })); - return `https:${decryptedUrl}`; - } - async getVideoData(videoId) { - const secureData = this.getSecureData(videoId); - if (!secureData) { - return void 0; - } - const videoData = await this.getFtor(secureData); - if (!videoData) { - return void 0; - } - const videoDataLinks = Object.entries(videoData.links[videoData.default.toString()]); - const videoLink = videoDataLinks.find(([, data]) => data.type === "application/x-mpegURL")?.[1]; - if (!videoLink) { - return void 0; - } - return { - url: videoLink.src.startsWith("//") ? `https:${videoLink.src}` : this.decryptUrl(videoLink.src) - }; - } - async getVideoId(url) { - return /\/(uv|video|seria|episode|season|serial)\/([^/]+)\/([^/]+)\/([\d]+)p/.exec(url.pathname)?.[0]; - } - } - class LinkedinHelper extends VideoJSHelper { - SUBTITLE_SOURCE = "linkedin"; - async getVideoData(videoId) { - const data = this.getVideoDataByPlayer(videoId); - if (!data) { - return void 0; - } - const { url, duration, subtitles } = data; - return { - url: proxyMedia(new URL(url)), - duration, - subtitles - }; - } - async getVideoId(url) { - return /\/learning\/(([^/]+)\/([^/]+))/.exec(url.pathname)?.[1]; - } - } - var TypeName; - (function(TypeName2) { - TypeName2["Channel"] = "Channel"; - TypeName2["Video"] = "Video"; - })(TypeName || (TypeName = {})); - class LoomHelper extends BaseHelper { - getClientVersion() { - if (typeof SENTRY_RELEASE === "undefined") { - return void 0; - } - 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": votConfig.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 result = await res.json(); - const data = result.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]; - } - } - class MailRuHelper extends BaseHelper { - API_ORIGIN = "https://my.mail.ru"; - async getVideoMeta(videoId) { - try { - const res = await this.fetch(`${this.API_ORIGIN}/+/video/meta/${videoId}?xemail=&ajax_call=1&func_name=&mna=&mnb=&ext=1&_=${Date.now()}`); - return await res.json(); - } catch (err) { - Logger.error("Failed to get mail.ru video data", err.message); - return void 0; - } - } - 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 void 0; - } - const videoData = await this.getVideoMeta(videoId); - if (!videoData) { - return void 0; - } - return videoData.meta.url.replace("//my.mail.ru/", ""); - } - } - class NetacadHelper extends VideoJSHelper { - SUBTITLE_SOURCE = "netacad"; - async getVideoData(videoId) { - const data = this.getVideoDataByPlayer(videoId); - if (!data) { - return void 0; - } - const { url, duration, subtitles } = data; - return { - url: proxyMedia(new URL(url)), - duration, - subtitles - }; - } - async getVideoId(url) { - return url.pathname + url.search; - } - } - class NewgroundsHelper extends BaseHelper { - async getVideoId(url) { - return /([^/]+)\/(view)\/([^/]+)/.exec(url.pathname)?.[0]; - } - } - class NicoNicoHelper extends BaseHelper { - async getVideoId(url) { - if (url.hostname === "nico.ms") { - return url.pathname.replace(/^\//, "").split("/")[0] || void 0; - } - return /\/watch\/([^/?#]+)/.exec(url.pathname)?.[1]; - } - } - class NineGAGHelper 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]; - } - } - class OdyseeHelper extends BaseHelper { - API_ORIGIN = "https://odysee.com"; - async getVideoData(videoId) { - try { - const res = await this.fetch(`${this.API_ORIGIN}/${videoId}`); - const content = await res.text(); - const url = /"contentUrl":(\s)?"([^"]+)"/.exec(content)?.[2]; - if (!url) { - throw new VideoHelperError("Odysee url doesn't parsed"); - } - return { url }; - } catch (err) { - Logger.error(`Failed to get odysee video data by video ID: ${videoId}`, err.message); - return void 0; - } - } - async getVideoId(url) { - return url.pathname.slice(1); - } - } - class OKRuHelper extends BaseHelper { - async getVideoId(url) { - return /\/video\/(\d+)/.exec(url.pathname)?.[1]; - } - } - class OlympicsReplayHelper extends BaseHelper { - async getVideoId(url) { - return /\/([a-z]{2}\/(?:[a-z0-9-]+\/)?(?:replay|videos?|original-series\/episode)\/[\w-]+)\/?$/i.exec(url.pathname)?.[1]; - } - } - class OracleLearnHelper extends VideoJSHelper { - SUBTITLE_SOURCE = "oraclelearn"; - async getVideoData(videoId) { - const data = this.getVideoDataByPlayer(videoId); - if (!data) { - return void 0; - } - 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]; - } - } - class PatreonHelper extends BaseHelper { - API_ORIGIN = "https://www.patreon.com/api"; - async getPosts(postId) { - try { - const res = await this.fetch(`${this.API_ORIGIN}/posts/${postId}?json-api-use-default-includes=false`); - return await res.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 void 0; - } - const postFileUrl = postData.data.attributes.post_file.url; - if (!postFileUrl) { - return void 0; - } - return { - url: postFileUrl - }; - } - async getVideoId(url) { - const fullPostId = /posts\/([^/]+)/.exec(url.pathname)?.[1]; - if (!fullPostId) { - return void 0; - } - return fullPostId.replace(/[^\d.]/g, ""); - } - } - class PeertubeHelper 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}`; - } - return void 0; - } - } - class PicartoHelper extends BaseHelper { - async getVideoId(url) { - return /\/((?:videopopout|[^/]+(?:\/profile)?\/videos)\/[^/?#&/]+)\/?$/.exec(url.pathname)?.[1] ?? /^\/([^/#?]+)\/?$/.exec(url.pathname)?.[1]; - } - } - class PornhubHelper extends BaseHelper { - async getVideoId(url) { - return url.searchParams.get("viewkey") ?? /embed\/([^/]+)/.exec(url.pathname)?.[1]; - } - } - class PornTNHelper extends BaseHelper { - async getVideoData(videoId) { - try { - if (typeof flashvars === "undefined") { - return void 0; - } - 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); - const proxiedUrl = proxyMedia(cdnUrl); - return { - url: proxiedUrl, - title - }; - } catch (err) { - Logger.error(`Failed to get PornTN data by videoId: ${videoId}`, err.message); - return void 0; - } - } - async getVideoId(url) { - return /\/videos\/(([^/]+)\/([^/]+))/.exec(url.pathname)?.[1]; - } - } - class RedditHelper extends BaseHelper { - API_ORIGIN = "https://www.reddit.com"; - async getContentUrl(_videoId) { - if (this.service?.additionalData !== "old") { - const player2 = document.querySelector("shreddit-player-2, shreddit-player"); - const src = player2?.getAttribute("src") ?? player2?.querySelector('source[type="application/vnd.apple.mpegURL"]')?.getAttribute("src"); - return src?.replaceAll("&", "&"); - } - const playerEl = document.querySelector("[data-hls-url]"); - return playerEl?.dataset.hlsUrl?.replaceAll("&", "&"); - } - async getVideoData(videoId) { - try { - const contentUrl2 = await this.getContentUrl(videoId); - if (!contentUrl2) { - throw new VideoHelperError("Failed to find content url"); - } - return { - url: decodeURIComponent(contentUrl2) - }; - } catch (err) { - Logger.error(`Failed to get reddit video data by video ID: ${videoId}`, err.message); - return void 0; - } - } - async getVideoId(url) { - return /\/r\/(([^/]+)\/([^/]+)\/([^/]+)\/([^/]+))/.exec(url.pathname)?.[1]; - } - } - class RtNewsHelper extends BaseHelper { - async getVideoData(videoId) { - const videoEl = document.querySelector(".jw-video, .media__video_noscript"); - if (!videoEl) { - return void 0; - } - let videoSrc = videoEl.getAttribute("src"); - if (!videoSrc) { - return void 0; - } - if (videoSrc.endsWith(".MP4")) { - videoSrc = proxyMedia(videoSrc); - } - return { - videoId, - url: videoSrc - }; - } - async getVideoId(url) { - return url.pathname.slice(1); - } - } - class Rule34VideoHelper extends BaseHelper { - async getVideoId(url) { - const parts = /\/videos?\/(\d+)(?:\/(.+))?\/?$/.exec(url.pathname); - if (!parts) { - return void 0; - } - const [, id, tail] = parts; - return tail ? `${id}/${tail.replace(/\/+$/, "")}/` : id; - } - } - class RumbleHelper extends BaseHelper { - async getVideoId(url) { - return url.pathname.slice(1); - } - } - class RutubeHelper extends BaseHelper { - async getVideoId(url) { - return /(?:video|embed)\/([^/]+)/.exec(url.pathname)?.[1]; - } - } - class SapHelper 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 { - const res = 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" - } - }); - return await res.json(); - } catch (err) { - Logger.error("Failed to request kaltura data", err.message); - return void 0; - } - } - 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 void 0; - } - } - async getVideoData(videoId) { - const kalturaData = await this.getKalturaData(videoId); - if (!kalturaData) { - return void 0; - } - 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 void 0; - } - const subtitles = playbackContext.playbackCaptions.map((caption) => { - return { - language: normalizeLang$1(caption.languageCode), - source: "sap", - format: "vtt", - url: caption.webVttUrl, - isAutoGenerated: caption.label.includes("auto-generated") - }; - }); - return { - url: videoUrl, - subtitles, - duration - }; - } - async getVideoId(url) { - return /((courses|learning-journeys)\/([^/]+)(\/[^/]+)?)/.exec(url.pathname)?.[1]; - } - } - class SpankBangHelper 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]; - } - } - class TelegramHelper extends BaseHelper { - static getMediaViewer() { - if (typeof appMediaViewer === "undefined") { - return void 0; - } - return appMediaViewer; - } - async getVideoId(_url) { - const mediaViewer = TelegramHelper.getMediaViewer(); - if (!mediaViewer) { - return void 0; - } - if (mediaViewer.live) { - return void 0; - } - const message = mediaViewer.target.message; - if (message.peer_id._ !== "peerChannel") { - return void 0; - } - const media = message.media; - if (media._ !== "messageMediaDocument") { - return void 0; - } - if (media.document.type !== "video") { - return void 0; - } - const postId = message.mid & 4294967295; - const username = await mediaViewer.managers.appPeersManager.getPeerUsername(message.peerId); - return `${username}/${postId}`; - } - } - class ThisVidHelper extends BaseHelper { - async getVideoId(url) { - return /(videos|embed)\/[^/]+/.exec(url.pathname)?.[0]; - } - } - class TikTokHelper extends BaseHelper { - async getVideoId(url) { - return /([^/]+)\/video\/([^/]+)/.exec(url.pathname)?.[0]; - } - } - class TrovoHelper extends BaseHelper { - async getVideoId(url) { - const vid = url.searchParams.get("vid"); - const path = /([^/]+)\/([\d]+)/.exec(url.pathname)?.[0]; - if (!vid || !path) { - return void 0; - } - return `${path}?vid=${vid}`; - } - } - class TwitchHelper 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 schemaJSON = JSON.parse(schema.innerText); - const channelLink2 = schemaJSON["@graph"].find((obj) => obj["@type"] === "VideoObject")?.creator.url; - if (!channelLink2) { - throw new VideoHelperError("Failed to find channel link"); - } - const channelName2 = channelLink2.replace("https://www.twitch.tv/", ""); - return `${channelName2}/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 void 0; - } - const channelName = channelLink.href.replace("https://www.twitch.tv/", ""); - return `${channelName}/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]; - } - const isClipsDomain = /^clips\.twitch\.tv$/.test(url.hostname); - if (isClipsDomain) { - 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; - } - } - class TwitterHelper extends BaseHelper { - async getVideoId(url) { - const videoId = /status\/([^/]+)/.exec(url.pathname)?.[1]; - if (videoId) { - return videoId; - } - const postEl = this.video?.closest('[data-testid="tweet"]'); - const newLink = postEl?.querySelector('a[role="link"][aria-label]')?.href; - return newLink ? /status\/([^/]+)/.exec(newLink)?.[1] : void 0; - } - } - 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; - const values = Array.isArray(source.Video) ? source.Video : Array.isArray(source.video) ? source.video : []; - return values.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; - } - return void 0; - } - function isM3U8(type, url) { - return type.includes("mpegurl") || /\.m3u8(?:$|[?#])/i.test(url); - } - function isDash(type, url) { - return type.includes("dash") || /\.mpd(?:$|[?#])/i.test(url); - } - class UdemyHelper extends BaseHelper { - API_ORIGIN = `${window.location.origin}/api-2.0`; - getModuleData() { - const appLoaderEl = document.querySelector(".ud-app-loader[data-module-id='course-taking']") ?? document.querySelector("[data-module-id='course-taking']"); - const moduleData = appLoaderEl?.dataset?.moduleArgs; - if (!moduleData) { - return void 0; - } - try { - return JSON.parse(moduleData); - } catch { - return void 0; - } - } - getLectureId(videoId) { - const lectureIdRe = /(?:\/learn\/(?:v4\/t\/)?lecture\/|#\/?lecture\/|\/lecture\/view\/\?(?:[^#]*?&)*lecture(?:_|)id=)(\d+)/i; - return lectureIdRe.exec(window.location.href)?.[1] ?? (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; - } - return void 0; - } - parseJson(value) { - try { - return JSON.parse(value); - } catch { - const normalized = value.replaceAll(""", '"').replaceAll(""", '"').replaceAll("'", "'").replaceAll("'", "'"); - try { - return JSON.parse(normalized); - } catch { - return void 0; - } - } - } - 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 res = 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()); - const data = await res.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 void 0; - } - } - async getCourseLang(courseId) { - try { - const res = await this.fetch(`${this.API_ORIGIN}/users/me/subscribed-courses/${courseId}?` + new URLSearchParams({ - "fields[course]": "locale" - }).toString()); - const data = await res.json(); - if (!this.isErrorData(data)) { - return data; - } - const res2 = await this.fetch(`${this.API_ORIGIN}/courses/${courseId}/?` + new URLSearchParams({ - "fields[course]": "locale" - }).toString()); - const data2 = await res2.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 void 0; - } - } - 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 = 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 void 0; - } - const mp4Candidates = candidates.filter((item) => item.type.includes("mp4") || /\.mp4(?:$|[?#])/i.test(item.url)); - if (mp4Candidates.length) { - mp4Candidates.sort((a2, b2) => b2.quality - a2.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 void 0; - } - 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 void 0; - } - const lectureData = await this.getLectureData(courseId, lectureId); - if (!lectureData) { - return void 0; - } - 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 void 0; - } - let courseLang = "en"; - const courseLangData = await this.getCourseLang(courseId); - const courseLocale = courseLangData?.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); - } - } - class VimeoHelper 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 void 0; - } - 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 res = await this.fetch("https://vimeo.com/_next/viewer"); - const data = await res.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 res = await this.fetch(`${this.API_ORIGIN}/videos/${videoId}?${params}`, { - headers: { - Authorization: this.API_KEY - } - }); - const data = await res.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((v2) => v2.mime_type === "audio/mp4" && v2.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 void 0; - } - 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 res = await this.fetch(`${this.API_ORIGIN}/videos/${videoId}/texttracks?${params}`, { - headers: { - Authorization: this.API_KEY - } - }); - const content = await res.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) { - const isPrivate = this.isPrivatePlayer(); - if (isPrivate) { - const videoInfo2 = await this.getPrivateVideoInfo(videoId); - if (!videoInfo2) { - return void 0; - } - const { url: url2, subs, video_url, title: title2, duration: duration2 } = videoInfo2; - const subtitles2 = 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 = subtitles2.length ? [ - { target: "video_file_url", targetUrl: video_url }, - { target: "subtitles_file_url", targetUrl: subtitles2[0].url } - ] : null; - return { - ...translationHelp ? { - url: url2, - translationHelp - } : { - url: video_url - }, - subtitles: subtitles2, - title: title2, - duration: duration2 - }; - } - if (!this.extraInfo) { - return this.returnPublicBaseData(videoId); - } - if (videoId.includes("/")) { - videoId = videoId.replace("/", ":"); - } - const viewerData = await this.getViewerData(); - if (!viewerData) { - return this.returnPublicBaseData(videoId); - } - const videoInfo = await this.getVideoInfo(videoId); - if (!videoInfo) { - return this.returnPublicBaseData(videoId); - } - const subsData = await this.getSubsInfo(videoId); - const subtitles = subsData.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; - const url = this.normalizePublicVideoUrl(link, videoId); - return { - url, - 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]; - } - } - class VKHelper extends BaseHelper { - static getPlayer() { - if (typeof Videoview === "undefined") { - return void 0; - } - try { - return Videoview?.getPlayerObject?.(); - } catch { - return void 0; - } - } - async getVideoData(videoId) { - const currentUrl = new URL(window.location.href); - const player2 = VKHelper.getPlayer(); - if (!player2) { - const base = this.returnBaseData(videoId); - return base ? { ...base, url: buildVkVideoUrl$1(videoId, currentUrl) } : base; - } - try { - const { description: descriptionHTML, duration, md_title: title } = player2.vars; - const parser = new DOMParser(); - const doc = parser.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(player2.vars, "subs")) { - subtitles = player2.vars.subs.map((sub) => ({ - language: normalizeLang$1(sub.lang), - source: "vk", - format: "vtt", - url: sub.url, - isAutoGenerated: !!sub.is_auto - })); - } - return { - url: buildVkVideoUrl$1(videoId, currentUrl), - title, - description, - duration, - subtitles - }; - } catch (err) { - Logger.error(`Failed to get VK video data, because: ${err.message}`); - const base = this.returnBaseData(videoId); - return base ? { ...base, url: buildVkVideoUrl$1(videoId, currentUrl) } : base; - } - } - async getVideoId(url) { - const pathID = /^\/((?:video|clip)-?\d+_\d+)(?:\/)?$/.exec(url.pathname); - if (pathID) { - return pathID[1]; - } - const idInsidePlaylist = /\/playlist\/[^/]+\/(video-?\d+_\d+)/.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) { - const ownerId = Math.abs(Number.parseInt(paramOID, 10)); - if (!Number.isNaN(ownerId)) { - return `video-${ownerId}_${paramID}`; - } - } - return void 0; - } - } - class WatchPornToHelper extends BaseHelper { - async getVideoId(url) { - return /(video|embed)\/(\d+)(\/[^/]+\/)?/.exec(url.pathname)?.[0]; - } - } - const weiboVideoIdRe = /^\d+:(?:[\da-f]{32}|\d{16,})$/i; - const weiboLayerIdRe = /^[A-Za-z0-9]+$/; - const weiboHostRe = /^(?:www\.)?weibo\.com$/; - const weiboLoginPathRe = /^\/newlogin\/?$/; - class WeiboHelper extends BaseHelper { - async getVideoId(url) { - if (url.hostname === "video.weibo.com") { - const fid = url.searchParams.get("fid"); - if (!fid || !weiboVideoIdRe.test(fid)) { - return void 0; - } - 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); - } - return void 0; - } - } - class WeverseHelper 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); - const res = await this.fetch(`${this.API_ORIGIN + pathname}&${urlParams}`, { - headers: this.HEADERS - }); - return await res.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); - const res = await this.fetch(`${this.API_ORIGIN + pathname}&${urlParams}`, { - method: "POST", - headers: this.HEADERS - }); - return await res.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(); - const res = await this.fetch(`https://global.apis.naver.com/rmcnmv/rmcnmv/vod/play/v2.0/${infraVideoId}?` + urlParams, { - headers: this.HEADERS - }); - return await res.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 void 0; - } - const { videoId: internalVideoId, serviceId, infraVideoId } = videoPreview.extension.video; - if (!(internalVideoId && serviceId && infraVideoId)) { - return void 0; - } - const inkeyData = await this.getVideoInKey(internalVideoId); - if (!inkeyData) { - return void 0; - } - const videoInfo = await this.getVideoInfo(infraVideoId, inkeyData.inKey, serviceId); - if (!videoInfo) { - return void 0; - } - const videoItem = this.extractVideoInfo(videoInfo.videos.list); - if (!videoItem) { - return void 0; - } - return { - url: videoItem.source, - duration: videoItem.duration - }; - } - async getVideoId(url) { - return /([^/]+)\/(live|media)\/([^/]+)/.exec(url.pathname)?.[3]; - } - } - class XHamsterHelper extends BaseHelper { - async getVideoId(url) { - return /\/(videos\/[^/]+-[\dA-Za-z]+)\/?$/.exec(url.pathname)?.[1]; - } - } - class XVideosHelper extends BaseHelper { - async getVideoId(url) { - return /[^/]+\/[^/]+$/.exec(url.pathname)?.[0]; - } - } - class YandexDiskHelper 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 url = new URL(window.location.href); - const dialogId = url.searchParams.get("idDialog"); - if (!dialogId) { - return void 0; - } - const preloadedScript = document.querySelector("#preloaded-data"); - if (!preloadedScript) { - return void 0; - } - try { - const preloadedData = JSON.parse(preloadedScript.innerText); - const { idClient, sk } = preloadedData.config; - const res = 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" - } - }); - const data = await res.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"); - } - const title = this.clearTitle(name); - const duration = Math.round(video_info.duration / 1e3); - return { - url: short_url, - title, - duration - }; - } catch (err) { - Logger.error(`Failed to get yandex disk video data by video ID: ${videoId}, because ${err.message}`); - return void 0; - } - } - 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 res = await this.fetch(`${this.API_ORIGIN}/public/api/fetch-list`, { - method: "POST", - body - }); - const data = await res.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 res = await this.fetch(`${this.API_ORIGIN}/public/api/download-url`, { - method: "POST", - body - }); - const data = await res.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 data = JSON.parse(prefetchEl.innerText); - const { resources, rootResourceId, environment: { sk } } = data; - const rootResource = resources[rootResourceId]; - const resourcePathsLastIdx = resourcePaths.length - 1; - const resourcePath = resourcePaths.filter((_2, idx) => idx !== resourcePathsLastIdx).join("/"); - let resourcesList = Object.values(resources); - if (resourcePath.includes("/")) { - resourcesList = await this.fetchList(`${rootResource.hash}:/${resourcePath}`, sk); - } - const resource = resourcesList.find((resource2) => resource2.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 void 0; - } - } - 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; - } - } - class YoukuHelper extends BaseHelper { - async getVideoId(url) { - return /v_show\/id_[\w=]+/.exec(url.pathname)?.[0]; - } - } - 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") { - const id = url.pathname.replace(/^\/+/, "").split("/")[0]; - return id || 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 player2 = YoutubeHelper.getPlayer(); - if (player2?.getVolume) { - return player2.getVolume() / 100; - } - return 1; - } - static setVolume(volume) { - const player2 = YoutubeHelper.getPlayer(); - if (player2?.setVolume) { - player2.setVolume(Math.round(volume * 100)); - return true; - } - return false; - } - static isMuted() { - const player2 = YoutubeHelper.getPlayer(); - if (player2?.isMuted) { - return player2.isMuted(); - } - return false; - } - static videoSeek(video, time) { - Logger.log("videoSeek", time); - const preTime = YoutubeHelper.getPlayer()?.getProgressState()?.seekableEnd ?? video.currentTime; - const finalTime = preTime - time; - video.currentTime = finalTime; - } - static getPoToken() { - const player2 = YoutubeHelper.getPlayer(); - if (!player2) { - return void 0; - } - const audioTrack = player2.getAudioTrack?.call(void 0); - if (!audioTrack?.captionTracks?.length) { - return void 0; - } - const audioTrackWithPoToken = audioTrack.captionTracks.find((captionTrack) => captionTrack.url.includes("&pot=")); - if (!audioTrackWithPoToken) { - return void 0; - } - 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 response = YoutubeHelper.getPlayerResponse(); - const playerCaptions = response?.captions?.playerCaptionsTracklistRenderer; - if (!playerCaptions) { - return []; - } - const captionTracks = playerCaptions.captionTracks ?? []; - const translationLanguages = playerCaptions.translationLanguages ?? []; - const userLangSupported = translationLanguages.find((language) => language.languageCode === userLang); - const asrSubtitleItem = captionTracks.find((captionTrack) => captionTrack?.kind === "asr"); - const asrLang = asrSubtitleItem?.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 player2 = YoutubeHelper.getPlayer(); - const trackInfo = player2?.getAudioTrack?.call(void 0)?.getLanguageInfo(); - if (trackInfo && trackInfo.id !== "und") { - return normalizeLang$1(trackInfo.id.split(".")[0]); - } - } - const response = YoutubeHelper.getPlayerResponse(); - const autoCaption = response?.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); - } - } - const zdfPlayPathRe = /^\/play\/([^/?#]+)\/([^/?#]+)\/([^/?#]+)\/?$/i; - class ZDFHelper extends BaseHelper { - async getVideoId(url) { - const match = zdfPlayPathRe.exec(url.pathname); - if (!match) { - return void 0; - } - const [, publicationForm, collectionCanonical, videoCanonical] = match; - return `${publicationForm}/${collectionCanonical}/${videoCanonical}`; - } - } - const availableHelpers = { - [VideoService.mailru]: MailRuHelper, - [VideoService.weverse]: WeverseHelper, - [VideoService.weibo]: WeiboHelper, - [VideoService.kodik]: KodikHelper, - [VideoService.patreon]: PatreonHelper, - [VideoService.reddit]: RedditHelper, - [VideoService.bannedvideo]: BannedVideoHelper, - [VideoService.kick]: KickHelper, - [VideoService.appledeveloper]: AppleDeveloperHelper, - [VideoService.epicgames]: EpicGamesHelper, - [VideoService.odysee]: OdyseeHelper, - [VideoService.coursehunterLike]: CoursehunterLikeHelper, - [VideoService.twitch]: TwitchHelper, - [VideoService.sap]: SapHelper, - [VideoService.linkedin]: LinkedinHelper, - [VideoService.vimeo]: VimeoHelper, - [VideoService.yandexdisk]: YandexDiskHelper, - [VideoService.vk]: VKHelper, - [VideoService.trovo]: TrovoHelper, - [VideoService.incestflix]: IncestflixHelper, - [VideoService.porntn]: PornTNHelper, - [VideoService.googledrive]: GoogleDriveHelper, - [VideoService.bilibili]: BilibiliHelper, - [VideoService.xvideos]: XVideosHelper, - [VideoService.xhamster]: XHamsterHelper, - [VideoService.spankbang]: SpankBangHelper, - [VideoService.rule34video]: Rule34VideoHelper, - [VideoService.picarto]: PicartoHelper, - [VideoService.olympicsreplay]: OlympicsReplayHelper, - [VideoService.watchpornto]: WatchPornToHelper, - [VideoService.archive]: ArchiveHelper, - [VideoService.dailymotion]: DailymotionHelper, - [VideoService.youku]: YoukuHelper, - [VideoService.egghead]: EggheadHelper, - [VideoService.newgrounds]: NewgroundsHelper, - [VideoService.okru]: OKRuHelper, - [VideoService.peertube]: PeertubeHelper, - [VideoService.eporner]: EpornerHelper, - [VideoService.bitchute]: BitchuteHelper, - [VideoService.rutube]: RutubeHelper, - [VideoService.facebook]: FacebookHelper, - [VideoService.rumble]: RumbleHelper, - [VideoService.twitter]: TwitterHelper, - [VideoService.pornhub]: PornhubHelper, - [VideoService.tiktok]: TikTokHelper, - [VideoService.proxitok]: TikTokHelper, - [VideoService.nine_gag]: NineGAGHelper, - [VideoService.youtube]: YoutubeHelper, - [VideoService.invidious]: YoutubeHelper, - [VideoService.piped]: YoutubeHelper, - [VideoService.zdf]: ZDFHelper, - [VideoService.dzen]: DzenHelper, - [VideoService.bunnystream]: BunnyStreamHelper, - [VideoService.cloudflarestream]: CloudflareStreamHelper, - [VideoService.loom]: LoomHelper, - [VideoService.rtnews]: RtNewsHelper, - [VideoService.bitview]: BitviewHelper, - [VideoService.thisvid]: ThisVidHelper, - [VideoService.ign]: IgnHelper, - [VideoService.bunkr]: BunkrHelper, - [VideoService.imdb]: IMDbHelper, - [VideoService.telegram]: TelegramHelper, - [VideoService.niconico]: NicoNicoHelper, - [ExtVideoService.udemy]: UdemyHelper, - [ExtVideoService.coursera]: CourseraHelper, - [ExtVideoService.douyin]: DouyinHelper, - [ExtVideoService.artstation]: ArtstationHelper, - [ExtVideoService.kickstarter]: KickstarterHelper, - [ExtVideoService.oraclelearn]: OracleLearnHelper, - [ExtVideoService.deeplearningai]: DeeplearningAIHelper, - [ExtVideoService.netacad]: NetacadHelper - }; - class VideoHelper { - helpersData; - constructor(helpersData = {}) { - this.helpersData = helpersData; - } - getHelper(service) { - return new availableHelpers[service](this.helpersData); - } - } - function hasHelper(host) { - return host in availableHelpers; - } - function buildVkVideoUrl(videoId, sourceUrl) { - const cleanedVideoId = videoId.replace(/^\/+/, ""); - const out = new URL("https://vk.com/video"); - out.searchParams.set("z", cleanedVideoId); - for (const key of ["list", "access_key"]) { - const value = sourceUrl.searchParams.get(key); - if (value) { - out.searchParams.set(key, value); - } - } - return out.toString(); - } - 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.filter((e2) => { - return !!e2.match && (Array.isArray(e2.match) ? e2.match.some(isMatches) : isMatches(e2.match)) && e2.host && e2.url; - }); - } - async function getVideoID(service, opts = {}) { - const url = new URL(window.location.href); - const serviceHost = service.host; - if (hasHelper(serviceHost)) { - const helper = new VideoHelper(opts).getHelper(serviceHost); - return await helper.getVideoId(url); - } - return serviceHost === VideoService.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.peertube, - VideoService.coursehunterLike, - VideoService.bunnystream, - VideoService.cloudflarestream - ].includes(service.host)) { - service.url = origin; - } - if (service.rawResult) { - return { - url: videoId, - videoId, - host: service.host, - duration: void 0 - }; - } - if (!service.needExtraData) { - if (service.host === VideoService.vk) { - return { - url: buildVkVideoUrl(videoId, currentUrl), - videoId, - host: service.host, - duration: void 0 - }; - } - 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 helper = new VideoHelper({ - ...opts, - service, - origin - }).getHelper(service.host); - const result = await helper.getVideoData(videoId); - if (!result) { - throw new VideoDataError(`Failed to get video raw url for ${service.host}`); - } - return { - ...result, - url: service.host === VideoService.vk ? buildVkVideoUrl(videoId, currentUrl) : result.url, - videoId, - host: service.host - }; - } - const config = { - version: "1.0.6", - debug: false, - fetchFn: fetch.bind(window) - }; - const debug$1 = { - log: (...text) => { - if (!config.debug) { - return; - } - return console.log(`%c✦ chaimu.js v${config.version} ✦`, "background: #000; color: #fff; padding: 0 8px", ...text); - } - }; - const videoLipSyncEvents = [ - "playing", - "ratechange", - "play", - "waiting", - "pause", - "seeked" - ]; - function initAudioContext() { - const audioContext = window.AudioContext || window.webkitAudioContext; - return audioContext ? new audioContext() : void 0; - } - class BasePlayer { - static name = "BasePlayer"; - chaimu; - fetch; - _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$1.log(`handle video ${event.type}`); - this.lipSync(event.type); - return this; - }; - removeVideoEvents() { - for (const e2 of videoLipSyncEvents) { - this.chaimu.video?.removeEventListener(e2, this.handleVideoEvent); - } - return this; - } - addVideoEvents() { - for (const e2 of videoLipSyncEvents) { - this.chaimu.video?.addEventListener(e2, 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) { - return; - } - get volume() { - return 0; - } - get playbackRate() { - return 0; - } - set playbackRate(_value) { - return; - } - get currentTime() { - return 0; - } - } - class AudioPlayer extends BasePlayer { - static name = "AudioPlayer"; - audio; - gainNode; - audioSource; - constructor(chaimu, src) { - super(chaimu, src); - this.updateAudio(); - } - initAudioBooster() { - if (!this.chaimu.audioContext) { - return this; - } - this.disconnectAudioNodes(); - this.gainNode = this.chaimu.audioContext.createGain(); - this.gainNode.connect(this.chaimu.audioContext.destination); - this.audioSource = this.chaimu.audioContext.createMediaElementSource(this.audio); - this.audioSource.connect(this.gainNode); - return this; - } - disconnectAudioNodes() { - if (this.audioSource) { - this.audioSource.disconnect(); - this.audioSource = void 0; - } - if (this.gainNode) { - this.gainNode.disconnect(); - this.gainNode = void 0; - } - } - updateAudio() { - this.audio = new Audio(this.src); - this.audio.crossOrigin = "anonymous"; - return this; - } - async init() { - this.updateAudio(); - this.initAudioBooster(); - return this; - } - audioErrorHandle = (e2) => { - console.error("[AudioPlayer]", e2); - }; - lipSync(mode = false) { - debug$1.log("[AudioPlayer] lipsync video", this.chaimu.video); - if (!this.chaimu.video) { - return this; - } - this.audio.currentTime = this.chaimu.video.currentTime; - this.audio.playbackRate = this.chaimu.video.playbackRate; - if (!mode) { - debug$1.log("[AudioPlayer] lipsync mode isn't set"); - return this; - } - debug$1.log(`[AudioPlayer] lipsync mode is ${mode}`); - switch (mode) { - case "play": - case "playing": - case "seeked": { - if (!this.chaimu.video.paused) { - this.syncPlay(); - } - return this; - } - case "pause": - case "waiting": { - void this.pause(); - return this; - } - default: { - return this; - } - } - } - async clear() { - this.audio.pause(); - this.audio.src = ""; - this.audio.removeAttribute("src"); - this.disconnectAudioNodes(); - return this; - } - syncPlay() { - debug$1.log("[AudioPlayer] sync play called"); - if (this.audio) { - this.audio.play().catch(this.audioErrorHandle); - } - return this; - } - async play() { - debug$1.log("[AudioPlayer] play called"); - if (this.audio) { - await this.audio.play().catch(this.audioErrorHandle); - } - return this; - } - async pause() { - debug$1.log("[AudioPlayer] pause called"); - if (this.audio) { - this.audio.pause(); - } - return this; - } - set src(url) { - this._src = url; - if (!url) { - void 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; - } - } - class ChaimuPlayer extends BasePlayer { - static name = "ChaimuPlayer"; - audioBuffer; - audioElement; - mediaElementSource; - gainNode; - blobUrl; - isClearing = false; - isInitializing = false; - clearingPromise; - async fetchAudio() { - if (!this._src) { - throw new Error("No audio source provided"); - } - if (!this.chaimu.audioContext) { - throw new Error("No audio context available"); - } - debug$1.log(`[ChaimuPlayer] Fetching audio from ${this._src}...`); - let tempBlobUrl; - try { - const res = await this.fetch(this._src, this.fetchOpts); - debug$1.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; - } - } - 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; - this.mediaElementSource = this.chaimu.audioContext.createMediaElementSource(audio); - this.mediaElementSource.connect(this.gainNode); - this.gainNode.connect(this.chaimu.audioContext.destination); - } - lipSync(mode = false) { - debug$1.log("[ChaimuPlayer] lipsync video", this.chaimu.video, this); - if (!this.chaimu.video) { - return this; - } - if (!mode) { - debug$1.log("[ChaimuPlayer] lipsync mode isn't set"); - return this; - } - debug$1.log(`[ChaimuPlayer] lipsync mode is ${mode}`); - switch (mode) { - case "play": - case "playing": - case "ratechange": - case "seeked": { - if (!this.chaimu.video.paused) { - void this.start(); - } - return this; - } - case "pause": - case "waiting": { - void 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$1.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$1.log("clear audio context"); - this.isClearing = true; - this.clearingPromise = (async () => { - try { - await this.pause(); - if (this.audioElement) { - this.audioElement.pause(); - this.audioElement = void 0; - } - if (this.blobUrl) { - URL.revokeObjectURL(this.blobUrl); - this.blobUrl = void 0; - } - this.disconnectAudioNodes(); - const oldVolume = this.gainNode ? this.gainNode.gain.value : 1; - await this.reopenCtx(); - if (this.chaimu.audioContext) { - this.initAudioBooster(); - this.volume = oldVolume; - } - 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$1.log("The other cleaner is still running, waiting..."); - await this.clearingPromise; - } - debug$1.log("starting audio via HTMLAudioElement"); - await this.play(); - if (this.chaimu.video) { - this.audioElement.currentTime = this.chaimu.video.currentTime; - this.audioElement.playbackRate = this.chaimu.video.playbackRate; - } - this.audioElement.play().catch((err) => debug$1.log("[ChaimuPlayer] Play audioElement failed:", err)); - return this; - } - async pause() { - if (!this.chaimu.audioContext) { - throw new Error("No audio context available"); - } - if (this.audioElement) { - this.audioElement.pause(); - } - if (this.chaimu.audioContext.state === "running") { - await this.chaimu.audioContext.suspend(); - } - return this; - } - async play() { - if (!this.chaimu.audioContext) { - throw new Error("No audio context available"); - } - await this.chaimu.audioContext.resume(); - 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; - } - } - class Chaimu { - _debug = false; - audioContext; - player; - video; - fetchFn; - fetchOpts; - constructor({ url, video, debug: debug2 = false, fetchFn = config.fetchFn, fetchOpts = {}, preferAudio = false }) { - this._debug = config.debug = debug2; - this.fetchFn = fetchFn; - this.fetchOpts = fetchOpts; - this.audioContext = initAudioContext(); - this.player = this.audioContext && !preferAudio ? new ChaimuPlayer(this, url) : new AudioPlayer(this, url); - this.video = video; - } - async init() { - await this.player.init(); - if (this.video && !this.video.paused) { - this.player.lipSync("play"); - } - this.player.addVideoEvents(); - } - set debug(value) { - this._debug = config.debug = value; - } - get debug() { - return this._debug; - } - } - const 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; - const candidate = value; - return isBootstrapStatus(candidate.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; - } - const workerHost = "api.browser.yandex.ru"; - const m3u8ProxyHost = "media-proxy.toil.cc/v1/proxy/m3u8"; - const proxyWorkerHost = "vot-worker.kload.workers.dev"; - const votBackendUrl = "https://vot.toil.cc/v1"; - const foswlyTranslateUrl = "https://translate-backend.transly.workers.dev/v2"; - const detectRustServerUrl = "https://rust-server-531j.onrender.com/detect"; - const authServerUrl = "https://rust-server-531j.onrender.com"; - const avatarServerUrl = "https://avatars.mds.yandex.net/get-yapic"; - const repoPath = "ilyhalight/voice-over-translation"; - const contentUrl = `https://raw.githubusercontent.com/${repoPath}`; - const repositoryUrl = `https://github.com/${repoPath}`; - const defaultAutoVolume = 15; - const maxAudioVolume = 900; - const minLongWaitingCount = 5; - const defaultTranslationService = "yandexbrowser"; - const defaultDetectService = "yandexbrowser"; - const nonProxyExtensions = ["Tampermonkey", "Violentmonkey"]; - const proxyOnlyCountries = ["UA", "LV", "LT"]; - const defaultAutoHideDelay = 1e3; - const actualCompatVersion = "2025-05-09"; - const storageKeys = [ - "autoTranslate", - "autoSubtitles", - "dontTranslateLanguages", - "enabledDontTranslateLanguages", - "enabledAutoVolume", - "enabledSmartDucking", - "autoVolume", - "buttonPos", - "showVideoSlider", - "syncVolume", - "downloadWithName", - "sendNotifyOnComplete", - "subtitlesMaxLength", - "subtitlesSmartLayout", - "highlightWords", - "subtitlesFontSize", - "subtitlesFontFamily", - "subtitlesOpacity", - "subtitlesDownloadFormat", - "responseLanguage", - "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" - ]; - const noop = () => { - }; - const log = noop; - const warn = noop; - const error = noop; - const debug = { log, warn, error }; - function getNavigatorLang() { - return navigator.language?.substring(0, 2).toLowerCase() || "en"; - } - const slavicLangs = new Set([ - "uk", - "be", - "bg", - "mk", - "sr", - "bs", - "hr", - "sl", - "pl", - "sk", - "cs" - ]); - function resolveCalculatedResLang(baseLang2) { - if (availableTTS.includes(baseLang2)) { - return baseLang2; - } - if (slavicLangs.has(baseLang2)) { - return "ru"; - } - return "en"; - } - const lang = getNavigatorLang(); - const calculatedResLang = resolveCalculatedResLang(lang); - const DEFAULT_OBJECT_URL_REVOKE_DELAY_MS = 3e4; - const INVALID_FILENAME_CHARS_RE = /[\\/:*?"'<>|]+/g; - const URL_PROTOCOL_RE = /^https?:\/\//i; - const MULTIPLE_DASHES_RE = /-{2,}/g; - const EDGE_FILE_CHARS_RE = /^[.\s-]+|[.\s-]+$/g; - function getDateFallbackFilename() { - return ( new Date()).toISOString().slice(0, 10); - } - function stripAsciiControlChars(value) { - let out = ""; - for (let index = 0; index < value.length; index++) { - const code = value.codePointAt(index) ?? 0; - if (code >= 32 && code !== 127) { - out += value[index]; - } - } - return out; - } - function stableStringify(value) { - const seen2 = new WeakSet(); - return JSON.stringify(value, (_key, val) => { - if (val && typeof val === "object") { - if (seen2.has(val)) { - return "[Circular]"; - } - seen2.add(val); - if (Array.isArray(val)) { - return val; - } - const sorted = {}; - const keys = Object.keys(val).sort( - (a2, b2) => a2.localeCompare(b2) - ); - for (const key of keys) { - sorted[key] = val[key]; - } - return sorted; - } - return val; - }); - } - function fnv1a32ToKeyPart(str) { - let hash = 2166136261; - let i2 = 0; - while (i2 < str.length) { - const codePoint = str.codePointAt(i2) ?? 0; - hash ^= codePoint; - hash = Math.imul(hash, 16777619); - i2 += codePoint > 65535 ? 2 : 1; - } - return (hash >>> 0).toString(36); - } - const 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); - } - } - async function downloadBlob(blob, filename, options = {}) { - if (options.fileHandle) { - const saved = await writeBlobToHandle(options.fileHandle, blob); - if (saved) { - return true; - } - } - if (options.preferShare) { - const shareResult = await shareBlob(blob, filename); - if (shareResult === "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(); - const sanitized = stripAsciiControlChars(trimmed).replace(URL_PROTOCOL_RE, "").replaceAll(INVALID_FILENAME_CHARS_RE, "-").replaceAll(MULTIPLE_DASHES_RE, "-").replaceAll(EDGE_FILE_CHARS_RE, ""); - return sanitized || getDateFallbackFilename(); - } - const getTimestamp = () => Math.floor(Date.now() / 1e3); - const getHeaders = (headers) => headers ? Object.fromEntries(new Headers(headers).entries()) : {}; - function clamp$2(value, min = 0, max = 100) { - const lower = Math.min(min, max); - const upper = Math.max(min, max); - return Math.min(Math.max(value, lower), upper); - } - 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; - const isFlattenable = val !== null && typeof val === "object" && !Array.isArray(val); - if (!isFlattenable) { - out[key] = val; - continue; - } - for (const [k2, v2] of Object.entries(val)) { - stack.push([`${key}.${k2}`, v2]); - } - } - return out; - } - const YANDEX_TTL_MS = 2 * 60 * 60 * 1e3; - const RESPONSE_CACHE_CREATED_AT_HEADER = "x-vot-cache-created-at"; - const RESPONSE_CACHE_KEY_HEADER = "x-vot-cache-key"; - const DEFAULT_RESPONSE_CACHE_NAME = "vot-http-cache-v1"; - const MAX_MEMORY_CACHE_ENTRIES = 500; - const VOT_SESSION_STORAGE_KEY = "VOTSession"; - const EXPIRY_FIELD_BY_FIELD = { - translation: "translationExpiresAt", - subtitles: "subtitlesExpiresAt" - }; - function getCurrentUnixTimestampSeconds() { - return Math.floor(Date.now() / 1e3); - } - function isVOTSession(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 now2 = getCurrentUnixTimestampSeconds(); - const entries = Object.entries(value).flatMap( - ([module, session]) => { - if (!isVOTSession(session)) { - return []; - } - if (session.timestamp + session.expires <= now2) { - return []; - } - return [[module, session]]; - } - ); - return Object.fromEntries(entries); - } - function isStoredVOTSession(value) { - if (!value || typeof value !== "object") { - return false; - } - const candidate = value; - return typeof candidate.expiresAt === "number" && Number.isFinite(candidate.expiresAt) && typeof candidate.secretKey === "string" && candidate.secretKey.length > 0 && typeof candidate.uuid === "string" && candidate.uuid.length > 0; - } - class VOTSessionStorageCache { - constructor(storage = votStorage) { - this.storage = storage; - } - getStorageKey(host) { - return VOT_SESSION_STORAGE_KEY; - } - async restore(host, currentSessions = {}) { - const storageKey = this.getStorageKey(host); - const rawStoredSession = await this.storage.getRaw(storageKey); - if (!isStoredVOTSession(rawStoredSession)) { - return currentSessions; - } - const nowMs = Date.now(); - if (rawStoredSession.expiresAt <= nowMs) { - await this.storage.deleteRaw(storageKey); - return currentSessions; - } - const remainingSeconds = Math.max( - 1, - Math.ceil((rawStoredSession.expiresAt - nowMs) / 1e3) - ); - return { - ...currentSessions, - "video-translation": { - secretKey: rawStoredSession.secretKey, - uuid: rawStoredSession.uuid, - expires: remainingSeconds, - timestamp: Math.floor(nowMs / 1e3) - } - }; - } - async persist(host, sessions) { - const storageKey = this.getStorageKey(host); - const translationSession = sanitizeVOTSessions(sessions)["video-translation"]; - if (!translationSession) { - await this.storage.deleteRaw(storageKey); - return; - } - await this.storage.setRaw(storageKey, { - secretKey: translationSession.secretKey, - uuid: translationSession.uuid, - expiresAt: (translationSession.timestamp + translationSession.expires) * 1e3 - }); - } - } - class InMemoryCacheManager { - cache = new Map(); -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 now2 = 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 <= now2) { - entry[field] = void 0; - entry[expiryField] = void 0; - this.evictIfEmpty(key, entry); - return void 0; - } - return entry[field]; - } - setValue(key, field, value) { - const now2 = Date.now(); - const entry = this.getOrCreateEntry(key); - const expiresAt = now2 + 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; - } - } - class ResponseCacheManager { - memoryCache = new Map(); - inFlightRequests = 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(); - let staleFallback; - if (useMemory) { - const memoryHit = this.readMemoryCache(key, nowMs); - if (memoryHit.fresh) { - return memoryHit.fresh; - } - staleFallback = memoryHit.stale ?? staleFallback; - } - if (useCacheApi) { - const cacheApiHit = await this.readCacheApi( - cacheName, - context.url, - cacheApiKey, - ttlMs, - nowMs, - allowStaleOnError - ); - if (cacheApiHit.fresh) { - if (useMemory) { - this.writeMemoryCache( - key, - cacheApiHit.fresh.clone(), - cacheApiHit.expiresAt ?? nowMs + ttlMs - ); - } - return cacheApiHit.fresh; - } - staleFallback = staleFallback ?? cacheApiHit.stale; - } - const runNetworkRequest = async () => { - 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, context.url, cacheApiKey, storable); - } - return response; - }; - if (!dedupe) { - try { - return await runNetworkRequest(); - } catch (err) { - if (allowStaleOnError && staleFallback) { - return staleFallback; - } - throw err; - } - } - const inFlight = this.inFlightRequests.get(key); - if (inFlight !== void 0) { - return (await inFlight).clone(); - } - const networkPromise = (async () => { - try { - return await runNetworkRequest(); - } catch (err) { - if (allowStaleOnError && staleFallback) { - return staleFallback.clone(); - } - throw err; - } - })(); - this.inFlightRequests.set(key, networkPromise); - try { - return (await networkPromise).clone(); - } finally { - this.inFlightRequests.delete(key); - } - } - computeExpiresAt(createdAtMs, ttlMs) { - if (!Number.isFinite(ttlMs) || ttlMs <= 0) { - return createdAtMs; - } - const maxAdd = Number.MAX_SAFE_INTEGER - createdAtMs; - if (ttlMs >= maxAdd) { - 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(); - return void 0; - } - buildDefaultCacheKey(context) { - const method = this.normalizeMethod(context.method); - if (method === "GET" || method === "HEAD") { - 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 - } - }); - const cache = await caches.open(cacheName); - await cache.put(request, response); - } catch { - } - } - } - const responseCacheManager = new ResponseCacheManager(); - async function executeWithResponseCache(context, options, fetcher) { - return responseCacheManager.execute(context, options, fetcher); - } - function stringifyUnknownObject(value) { - const seen2 = new WeakSet(); - try { - const serialized = JSON.stringify(value, (_key, currentValue) => { - if (typeof currentValue !== "object" || currentValue === null) { - return currentValue; - } - if (seen2.has(currentValue)) { - return "[Circular]"; - } - seen2.add(currentValue); - return currentValue; - }); - return serialized ?? null; - } catch { - return null; - } - } - function toErrorMessage(error2, fallback = "Unknown error") { - if (error2 instanceof Error) { - return error2.message || fallback; - } - if (typeof error2 === "string") { - return error2 || fallback; - } - if (error2 === null || error2 === void 0) { - return fallback; - } - if (typeof error2 === "object") { - const anyErr = error2; - if (typeof anyErr?.data?.message === "string" && anyErr.data.message) { - return anyErr.data.message; - } - if (typeof anyErr?.error?.message === "string" && anyErr.error.message) { - return anyErr.error.message; - } - if (typeof anyErr?.message === "string" && anyErr.message) { - return anyErr.message; - } - const serialized = stringifyUnknownObject(error2); - if (serialized && serialized !== "{}") { - return serialized; - } - const ctorName = error2.constructor?.name; - return ctorName ? `[${ctorName}]` : fallback; - } - if (typeof error2 === "number" || typeof error2 === "boolean" || typeof error2 === "bigint") { - return `${error2}`; - } - if (typeof error2 === "symbol") { - return error2.description ? `Symbol(${error2.description})` : "Symbol"; - } - if (typeof error2 === "function") { - return error2.name ? `[Function ${error2.name}]` : "[Function]"; - } - return fallback; - } - function getErrorMessage(error2) { - return toErrorMessage(error2, ""); - } - 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"; - } - function makeAbortError(message = "Aborted") { - try { - return new DOMException(message, "AbortError"); - } catch { - const err = new Error(message); - err.name = "AbortError"; - return err; - } - } - const NEVER_ABORTED_SIGNAL = new AbortController().signal; - function throwIfAborted(signal) { - const maybeThrow = signal.throwIfAborted; - if (typeof maybeThrow === "function") { - try { - maybeThrow.call(signal); - return; - } catch (e2) { - if (signal.aborted || isAbortError(e2)) { - throw makeAbortError(); - } - throw e2 instanceof Error ? e2 : new Error(String(e2)); - } - } - if (signal.aborted) { - throw makeAbortError(); - } - } - function createTimeoutSignal(timeoutMs, external) { - const hasEffectiveTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0; - if (!hasEffectiveTimeout) { - 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); - } - }; - } - const YANDEX_API_HOST = "api.browser.yandex.ru"; - const GOOGLEVIDEO_HOST_SUFFIX = "googlevideo.com"; - const HEADER_LINE_RE = /^([\w-]+):\s*(.+)$/; - const URL_SCHEME_RE = /^[a-zA-Z][a-zA-Z\d+.-]*:/; - const scriptHandler = typeof GM_info === "undefined" ? void 0 : GM_info?.scriptHandler; - const isProxyOnlyExtension = ( - -!(typeof IS_EXTENSION !== "undefined" && IS_EXTENSION) && !!scriptHandler && !nonProxyExtensions.includes(scriptHandler) - ); - const isSupportGM4 = typeof GM !== "undefined"; - const isSupportGMXhr = typeof GM_xmlhttpRequest !== "undefined"; - 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 void 0; - } - } - 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(error2) { - const maybeError = error2; - if (typeof maybeError?.error === "string") { - return maybeError.error; - } - if (typeof maybeError?.statusText === "string") { - return maybeError.statusText; - } - return getErrorMessage(error2) || "Unknown error"; - } - async function gmXhrFetch(urlStr, timeout2, fetchOptions) { - const headers = getHeaders(fetchOptions.headers); - return await new Promise((resolve, reject) => { - const gmXhr = typeof GM_xmlhttpRequest === "undefined" ? globalThis.GM_xmlhttpRequest : GM_xmlhttpRequest; - if (typeof gmXhr !== "function") { - reject(new TypeError("GM_xmlhttpRequest is not available")); - return; - } - let settled = false; - let onAbort; - const cleanupAbort = () => { - if (onAbort) { - fetchOptions.signal?.removeEventListener("abort", onAbort); - } - }; - const failOnce = (error2) => { - if (settled) return; - settled = true; - cleanupAbort(); - reject(error2); - }; - const request = gmXhr({ - method: fetchOptions.method || "GET", - url: urlStr, - responseType: "blob", - data: fetchOptions.body, - timeout: timeout2, - headers, - onload: (resp) => { - if (settled) return; - settled = true; - cleanupAbort(); - const responseHeaders = parseResponseHeaders(resp.responseHeaders); - const response = new Response(resp.response, { - status: resp.status, - statusText: typeof resp.statusText === "string" ? resp.statusText : "", - headers: responseHeaders - }); - Object.defineProperty(response, "url", { - value: resp.finalUrl ?? urlStr - }); - resolve(response); - }, - ontimeout: () => failOnce(new Error("Timeout")), - onerror: (error2) => failOnce(new Error(getGmXhrErrorMessage(error2))), - onabort: () => 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 GM_fetch(url, opts = {}) { - const { - timeout: timeout2 = 15e3, - forceGmXhr = false, - responseCache, - ...fetchOptions - } = opts; - const urlStr = toRequestUrl(url); - const host = getRequestHost(urlStr); - const method = resolveRequestMethod(url, fetchOptions.method); - const performRequest = async () => { - if (shouldUseGmXhr(host, urlStr, forceGmXhr)) { - return await gmXhrFetch(urlStr, timeout2, fetchOptions); - } - const { signal, cleanup } = createTimeoutSignal( - timeout2, - fetchOptions.signal - ); - try { - return await fetch(url, { - ...fetchOptions, - signal - }); - } catch (err) { - if (signal.aborted || isAbortError(err)) { - throw err; - } - debug.log( - "GM_fetch preventing CORS by GM_xmlhttpRequest", - getErrorMessage(err) || "Unknown error" - ); - return await gmXhrFetch(urlStr, timeout2, fetchOptions); - } finally { - cleanup(); - } - }; - if (!responseCache) { - return await performRequest(); - } - return await executeWithResponseCache( - { - url: urlStr, - method, - body: fetchOptions.body - }, - responseCache, - performRequest - ); - } - const compatMay2025Data = { - 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"] - ] - }; - const compatRules = Object.entries(compatMay2025Data).flatMap( - ([category, entries]) => entries.map(([oldKey, maybeNewKey]) => ({ - category, - oldKey, - newKey: maybeNewKey ?? oldKey, - shouldDeleteOldKey: Boolean(maybeNewKey) - })) - ); - const compatRuleByOldKey = new Map( - compatRules.map((rule) => [rule.oldKey, rule]) - ); - const 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(a2, b2) { - if (Array.isArray(a2) && Array.isArray(b2)) { - return a2.length === b2.length && a2.every((item, index) => Object.is(item, b2[index])); - } - return Object.is(a2, b2); - } - async function updateConfig(data) { - if (data.compatVersion === actualCompatVersion) { - 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 - }; - } - class VOTStorage { - support = null; - resolveSupport() { - if (this.support) { - return this.support; - } - const support = { - legacyGet: typeof GM_getValue === "function", - legacySet: typeof GM_setValue === "function", - legacyDelete: typeof GM_deleteValue === "function", - legacyList: typeof GM_listValues === "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" - }; - this.support = support; - debug.log( - `[VOT Storage] GM Promises: ${support.promiseGet} | GM legacy: ${support.legacyGet}` - ); - return support; - } -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]) => { - const storedValue = await GM.getValue(key, value); - return [key, storedValue]; - }) - ); - 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(); - if (support.promiseSet && GM.setValue) { - return await GM.setValue(name, value); - } - return this.syncSetByName(name, value, support); - } - 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(); - if (support.promiseDelete && GM.deleteValue) { - return await GM.deleteValue(name); - } - return this.syncDeleteByName(name, support); - } - async delete(name) { - return this.deleteRaw(name); - } - 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); - } - } - const VOT_STORAGE_GLOBAL_KEY = "__VOT_STORAGE_SINGLETON__"; - const 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; - })(); - 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 - }); - } - 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 - }); - } - async function initAuth() { - if (globalThis.location.pathname === "/auth/callback") { - return handleAuthCallbackPage(); - } - if (globalThis.location.pathname === "/my/profile") { - return handleProfilePage(); - } - } - const recommended = "recommended"; - const translateVideo = "Translate video"; - const disableTranslate = "Turn off"; - const translationSettings = "Translation settings"; - const subtitlesSettings = "Subtitles settings"; - const subtitlesSmartLayout = "Smart subtitle layout"; - const resetSettings = "Reset settings"; - const videoBeingTranslated = "The video is being translated"; - const videoLanguage = "Video language"; - const translationLanguage = "Translation language"; - const translationTake = "The translation will take"; - const translationTakeMoreThanHour = "The translation will take more than an hour"; - const translationTakeAboutMinute = "The translation will take about a minute"; - const translationTakeFewMinutes = "The translation will take a few minutes"; - const translationTakeApproximatelyMinutes = "The translation will take approximately {0} minutes"; - const translationTakeApproximatelyMinute = "The translation will take approximately {0} minutes"; - const requestTranslationFailed = "Failed to request video translation"; - const audioNotReceived = "Audio link not received"; - const VOTFailedDownloadAudio = "Failed to download audio"; - const audioFormatNotSupported = "The audio format is not supported"; - const VOTAutoTranslate = "Translate on open"; - const VOTAutoSubtitles = "Subtitles on open"; - const VOTDontTranslateYourLang = "Don't translate from my language"; - const VOTVolume = "Video volume:"; - const VOTVolumeTranslation = "Translation volume:"; - const VOTAutoSetVolume = "Reduce video volume to"; - const VOTShowVideoSlider = "Video volume slider"; - const VOTSyncVolume = "Link translation and video volume"; - const VOTDisableFromYourLang = "You have disabled the translation of the video in your language"; - const VOTVideoIsTooLong = "Video is too long"; - const VOTNoVideoIDFound = "No video ID found"; - const VOTSubtitles = "Subtitles"; - const VOTSubtitlesDisabled = "Disabled"; - const VOTSubtitlesMaxLength = "Subtitles max length"; - const VOTHighlightWords = "Highlight words"; - const VOTTranslatedFrom = "translated from"; - const VOTAutogenerated = "autogenerated"; - const VOTSettings = "VOT Settings"; - const VOTMenuLanguage = "Menu language"; - const VOTAuthors = "Authors"; - const VOTVersion = "Version"; - const VOTLoader = "Loader"; - const VOTBrowser = "Browser"; - const VOTShowPiPButton = "Show PiP button"; - const 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" }; - const streamNoConnectionToServer = "There is no connection to the server"; - const searchField = "Search..."; - const VOTTranslateAPIErrors = "Translate errors from the API"; - const VOTDetectService = "Language detection service"; - const VOTProxyWorkerHost = "Enter the proxy worker address"; - const VOTM3u8ProxyHost = "Enter the address of the m3u8 proxy worker"; - const proxySettings = "Proxy Settings"; - const translationTakeApproximatelyMinute2 = "The translation will take approximately {0} minutes"; - const VOTAudioBooster = "Extended translation volume increase"; - const VOTSubtitlesDesign = "Subtitles design"; - const VOTSubtitlesFont = "Subtitle font"; - const VOTSubtitlesFontSize = "Font size of subtitles"; - const VOTSubtitlesOpacity = "Transparency of the subtitle background"; - const VOTSubtitlesDownloadFormat = "The format for downloading subtitles"; - const VOTDownloadWithName = "Download files with the video name"; - const VOTUpdateLocaleFiles = "Update localization files"; - const VOTLocaleHash = "Locale hash"; - const VOTUpdatedAt = "Updated at"; - const VOTNeedWebAudioAPI = "To enable this, you must have a Web Audio API"; - const VOTMediaCSPEnabledOnSite = "Media CSP is enabled on this site"; - const VOTOnlyBypassMediaCSP = "Use it only for bypassing Media CSP"; - const VOTNewAudioPlayer = "Use the new audio player"; - const VOTUseNewModel = "Use an experimental variation of Yandex voices for some videos"; - const TranslationDelayed = "The translation is slightly delayed"; - const VOTTranslationCompletedNotify = "The translation on the {0} has been completed!"; - const VOTSendNotifyOnComplete = "Send a notification that the video has been translated"; - const VOTBugReport = "Report a bug"; - const VOTTranslateProxyDisabled = "Disabled"; - const VOTTranslateProxyEnabled = "Enabled"; - const VOTTranslateProxyEverything = "Proxy everything"; - const VOTTranslateProxyStatus = "Proxying mode"; - const VOTTranslatedBy = "Translated by {0}"; - const VOTStreamNotAvailable = "Translate stream isn't available"; - const VOTTranslationTextService = "Text translation service"; - const VOTNotAffectToVoice = "Doesn't affect the translation of text in voice over"; - const DontTranslateSelectedLanguages = "Don't translate from selected languages"; - const showVideoVolumeSlider = "Display the video volume slider"; - const hotkeysSettings = "Hotkeys settings"; - const None = "None"; - const VOTUseLivelyVoice = "Use lively voices. Speakers sound like native Russians."; - const miscSettings = "Misc settings"; - const services = { "yandexbrowser": "Yandex Browser", "msedge": "Microsoft Edge", "rust-server": "Rust Server" }; - const aboutExtension = "About extension"; - const appearance = "Appearance"; - const buttonPosition = "Button position in the player"; - const position = { "left": "Left", "right": "Right", "top": "Top", "default": "Default" }; - const secs = "secs"; - const autoHideButtonDelay = "Delay before hiding the translate button"; - const notFound = "not found"; - const minButtonPositionContainer = "The button position only changes in players larger than 600 pixels."; - const VOTTranslateProxyStatusDefault = "Completely disabling proxying in your country may break the extension"; - const PressTheKeyCombination = "Press the key combination..."; - const VOTUseAudioDownload = "Use audio download"; - const VOTUseAudioDownloadWarning = "Disabling audio downloads may affect the functionality of the extension"; - const VOTAccountRequired = "You need to log in to use this feature"; - const VOTMyAccount = "My account"; - const VOTLogin = "Login"; - const VOTLogout = "Logout"; - const VOTRefresh = "Refresh"; - const VOTYandexToken = "Enter the Yandex OAuth Token"; - const 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"; - const VOTLoginViaToken = "Login via token"; - const smartDucking = "Adaptive volume"; - const rawDefaultLocale = { - recommended, - translateVideo, - disableTranslate, - translationSettings, - subtitlesSettings, - subtitlesSmartLayout, - resetSettings, - videoBeingTranslated, - videoLanguage, - translationLanguage, - translationTake, - translationTakeMoreThanHour, - translationTakeAboutMinute, - translationTakeFewMinutes, - translationTakeApproximatelyMinutes, - translationTakeApproximatelyMinute, - requestTranslationFailed, - audioNotReceived, - VOTFailedDownloadAudio, - audioFormatNotSupported, - VOTAutoTranslate, - VOTAutoSubtitles, - VOTDontTranslateYourLang, - VOTVolume, - VOTVolumeTranslation, - VOTAutoSetVolume, - VOTShowVideoSlider, - VOTSyncVolume, - VOTDisableFromYourLang, - VOTVideoIsTooLong, - VOTNoVideoIDFound, - VOTSubtitles, - VOTSubtitlesDisabled, - VOTSubtitlesMaxLength, - VOTHighlightWords, - VOTTranslatedFrom, - VOTAutogenerated, - VOTSettings, - VOTMenuLanguage, - VOTAuthors, - VOTVersion, - VOTLoader, - VOTBrowser, - VOTShowPiPButton, - langs, - streamNoConnectionToServer, - searchField, - VOTTranslateAPIErrors, - VOTDetectService, - VOTProxyWorkerHost, - VOTM3u8ProxyHost, - proxySettings, - translationTakeApproximatelyMinute2, - VOTAudioBooster, - VOTSubtitlesDesign, - VOTSubtitlesFont, - VOTSubtitlesFontSize, - VOTSubtitlesOpacity, - VOTSubtitlesDownloadFormat, - VOTDownloadWithName, - VOTUpdateLocaleFiles, - VOTLocaleHash, - VOTUpdatedAt, - VOTNeedWebAudioAPI, - VOTMediaCSPEnabledOnSite, - VOTOnlyBypassMediaCSP, - VOTNewAudioPlayer, - VOTUseNewModel, - TranslationDelayed, - VOTTranslationCompletedNotify, - VOTSendNotifyOnComplete, - VOTBugReport, - VOTTranslateProxyDisabled, - VOTTranslateProxyEnabled, - VOTTranslateProxyEverything, - VOTTranslateProxyStatus, - VOTTranslatedBy, - VOTStreamNotAvailable, - VOTTranslationTextService, - VOTNotAffectToVoice, - DontTranslateSelectedLanguages, - showVideoVolumeSlider, - hotkeysSettings, - None, - VOTUseLivelyVoice, - miscSettings, - services, - aboutExtension, - appearance, - buttonPosition, - position, - secs, - autoHideButtonDelay, - notFound, - minButtonPositionContainer, - VOTTranslateProxyStatusDefault, - PressTheKeyCombination, - VOTUseAudioDownload, - VOTUseAudioDownloadWarning, - VOTAccountRequired, - VOTMyAccount, - VOTLogin, - VOTLogout, - VOTRefresh, - VOTYandexToken, - VOTYandexTokenInfo, - VOTLoginViaToken, - smartDucking - }; - var define_AVAILABLE_LOCALES_default = ["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"]; - const LOCALE_STORAGE_KEYS = [ - "localePhrases", - "localeLang", - "localeHash", - "localeVersion", - "localeUpdatedAt", - "localeLangOverride" - ]; - const DEFAULT_LOCALE = toFlatObj(rawDefaultLocale); - const repoBranch = "master"; - const availableLocales = (() => { - const locales = typeof define_AVAILABLE_LOCALES_default !== "undefined" && Array.isArray(define_AVAILABLE_LOCALES_default) ? define_AVAILABLE_LOCALES_default : ["en"]; - return locales.includes("auto") ? locales : ["auto", ...locales]; - })(); - function resolveRuntimeLocaleVersion(buildVersion, scriptVersion) { - return buildVersion || scriptVersion || "unknown"; - } - function getRuntimeLocaleVersion() { - const buildVersion = String("1.11.3"); - const scriptVersion = typeof GM_info !== "undefined" ? String(GM_info?.script?.version || "") : ""; - return resolveRuntimeLocaleVersion(buildVersion, scriptVersion); - } - class LocalizationProvider { -lang; -locale; - defaultLocale = DEFAULT_LOCALE; - localesUrl = `${contentUrl}/${repoBranch}/src/localization/locales`; - hashesUrl = `${contentUrl}/${repoBranch}/src/localization/hashes.json`; - warnedMissingKeys = 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) { - const query = force ? `?timestamp=${getTimestamp()}` : ""; - return `${baseUrl}${path}${query}`; - } - async changeLang(newLang) { - const oldLang = this.langOverride; - if (oldLang === 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) { - try { - 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; - } - const currentHash = await votStorage.get("localeHash", ""); - return currentHash === 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", - "" - ); - if (!force && storedLocaleVersion === runtimeLocaleVersion) { - return this; - } - const hash = await this.checkUpdates(force); - if (hash === null) { - return this; - } - if (!hash) { - return this; - } - const timestamp = getTimestamp(); - 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") { - const phrase = locale[key]; - return phrase ?? this.warnMissingKey(locale, key, source); - } - warnMissingKey(locale, key, source) { - const warningKey = `${source}:${key}`; - if (this.warnedMissingKeys.has(warningKey)) { - return void 0; - } - this.warnedMissingKeys.add(warningKey); - console.warn( - "[VOT] [localizationProvider] locale", - locale, - "doesn't contain key", - key - ); - return void 0; - } - getDefault(key) { - return this.getFromLocale(this.defaultLocale, key, "default") ?? key; - } - get(key) { - return this.getFromLocale(this.locale, key) ?? this.getDefault(key); - } - getLangLabel(lang2) { - const key = `langs.${lang2}`; - if (key in this.defaultLocale) { - const label = this.get(key); - if (label) { - return label; - } - } - return lang2.toUpperCase(); - } - } - const localizationProvider = new LocalizationProvider(); - let localizationProviderReadyPromise = null; - function ensureLocalizationProviderReady() { - localizationProviderReadyPromise ??= localizationProvider.init(); - return localizationProviderReadyPromise; - } - const isIframe = () => globalThis.self !== globalThis.top; - function initIframeInteractor() { - const configs = { - "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}` - } - }; - const currentConfig = Object.entries(configs).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 (error2) { - console.error("Iframe communication error:", error2); - } - }); - } - let runtimeActivated = false; - let runtimeActivationPromise = null; - let iframeInteractorBound = false; - async function ensureRuntimeActivated(reason, logBootstrap2) { - if (runtimeActivated) return; - if (runtimeActivationPromise !== null) { - await runtimeActivationPromise; - return; - } - runtimeActivationPromise = (async () => { - logBootstrap2("Activating runtime", { reason }); - if (globalThis.location.origin === authServerUrl) { - await initAuth(); - runtimeActivated = true; - return; - } - await ensureLocalizationProviderReady(); - if (!isIframe()) { - await localizationProvider.update(); - } - debug.log(`Selected menu language: ${localizationProvider.lang}`); - if (!iframeInteractorBound) { - iframeInteractorBound = true; - initIframeInteractor(); - } - runtimeActivated = true; - })(); - try { - await runtimeActivationPromise; - } finally { - runtimeActivationPromise = null; - } - } - const boundObservers = new WeakSet(); - function bindObserverListeners(options) { - const { - videoObserver: videoObserver2, - videosWrappers: videosWrappers2, - ensureRuntimeActivated: ensureRuntimeActivated2, - getServicesCached: getServicesCached2, - findContainer: findContainer2, - createVideoHandler - } = options; - if (boundObservers.has(videoObserver2)) return; - boundObservers.add(videoObserver2); - const initializingVideos = new WeakSet(); - const containerOwners = new WeakMap(); - const videoContainers = new WeakMap(); - const pendingVideoByContainer = 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 = videosWrappers2.get(video); - if (!videoHandler) { - return; - } - try { - await videoHandler.release(); - } catch (error2) { - console.error(`[VOT] Failed to release videoHandler (${reason})`, error2); - } finally { - videosWrappers2.delete(video); - } - }; - const getMatchedSiteAndContainer = (video) => { - for (const candidate of getServicesCached2()) { - const container = findContainer2(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 || videosWrappers2.has(pendingVideo) || initializingVideos.has(pendingVideo)) { - return; - } - await handleVideoAdded(pendingVideo); - }; - const handleVideoAdded = async (video) => { - if (videosWrappers2.has(video) || initializingVideos.has(video)) return; - initializingVideos.add(video); - try { - try { - await ensureRuntimeActivated2("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) - ); - videosWrappers2.set(video, videoHandler); - videoContainers.set(video, container); - containerOwners.set(container, video); - try { - await videoHandler.init(); - if (videosWrappers2.get(video) !== videoHandler) { - return; - } - try { - await videoHandler.setCanPlay(); - } catch (err) { - console.error("[VOT] Failed to get video data", err); - } - } catch (err) { - if (videosWrappers2.get(video) === videoHandler) { - await releaseVideoHandler(video, "init failed"); - const container2 = clearContainerOwner(video); - clearPendingVideo(container2); - await promotePendingVideo(container2); - } - console.error("[VOT] Failed to initialize videoHandler", err); - } - } finally { - initializingVideos.delete(video); - } - }; - videoObserver2.onVideoAdded.addListener(handleVideoAdded); - videoObserver2.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); - }); - } - 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) { - return "iframe-lazy"; - } - return "top-full"; - } - function getComposableParent(node) { - if (!node) return null; - if (typeof ShadowRoot !== "undefined" && node instanceof ShadowRoot) { - return node.host; - } - return node.parentNode ?? null; - } - function containsCrossShadow(container, target) { - let node = target; - while (node) { - if (node === container) return true; - node = getComposableParent(node); - } - return false; - } - function isDocumentViewportElement(element) { - return element === document.body || element === document.documentElement; - } - function resolveScopedFullscreenElement(fullscreenEl, anchors, options = {}) { - const { allowDocumentViewport = false } = options; - if (!(fullscreenEl instanceof HTMLElement)) { - return null; - } - if (isDocumentViewportElement(fullscreenEl) && !allowDocumentViewport) { - return null; - } - for (const anchor of anchors) { - if (anchor && (containsCrossShadow(fullscreenEl, anchor) || containsCrossShadow(anchor, fullscreenEl))) { - return fullscreenEl; - } - } - return null; - } - function closestCrossShadow(element, selector) { - if (!element || !selector) return null; - const origin = element instanceof Document ? null : element; - const walk = (current) => { - if (!current) return null; - if (current instanceof Document) { - if (origin) { - const matches = current.querySelectorAll(selector); - for (const match of matches) { - if (containsCrossShadow(match, origin)) { - return match; - } - } - return null; - } - return current.querySelector(selector); - } - const closest = current.closest(selector); - if (closest) return closest; - const root = current.getRootNode(); - if (root instanceof ShadowRoot) { - return walk(root.host); - } - if (root instanceof Document) { - return walk(root); - } - if (root !== current) { - const parent = getComposableParent(root); - if (parent && parent !== current && parent instanceof Element) { - return walk(parent); - } - } - return null; - }; - return walk(element); - } - 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; - } - 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 - }; - } - class EventImpl { - listeners = 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(); - } - } - function makeFileId(downloadType, itag, fileSize, minChunkSize) { - return JSON.stringify({ - downloadType, - itag, - minChunkSize, - fileSize - }); - } - 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"); - } - class YtWatchContextForbiddenError extends Error { - status; - constructor(status = 403) { - super(`Failed to load watch page: ${status}`); - this.name = "YtWatchContextForbiddenError"; - this.status = status; - } - } - const VIDEO_ID_PATTERN = /^[a-zA-Z0-9_-]{11}$/; - const YT_BASE = "https://www.youtube.com"; - const ANDROID_CLIENT_VERSION = "19.44.38"; - const ANDROID_VR_CLIENT_VERSION = "1.60.19"; - const IOS_CLIENT_VERSION = "19.45.4"; - const CLIENT_FALLBACK_ORDER = [ - "ANDROID_VR", - "ANDROID", - "IOS", - "WEB", - "MWEB" - ]; - const DEFAULT_HEADERS = { - accept: "*/*", - origin: YT_BASE, - referer: `${YT_BASE}/` - }; - const RANGE_FALLBACK_CHUNK_SIZE = 256 * 1024; - function withSignal(signal) { - return signal ? { signal } : {}; - } - function resolveInnertubeClient(requestedClient, watchContext, videoId) { - switch (requestedClient) { - case "ANDROID": - case "YTMUSIC_ANDROID": - case "YTSTUDIO_ANDROID": - return { - clientName: "ANDROID", - clientVersion: ANDROID_CLIENT_VERSION, - hl: "en", - gl: "US", - androidSdkVersion: 34, - osName: "Android", - osVersion: "14", - platform: "MOBILE" - }; - case "ANDROID_VR": - return { - clientName: "ANDROID_VR", - clientVersion: ANDROID_VR_CLIENT_VERSION, - hl: "en", - gl: "US", - androidSdkVersion: 31, - osName: "Android", - osVersion: "12", - platform: "MOBILE" - }; - case "IOS": - return { - clientName: "IOS", - clientVersion: IOS_CLIENT_VERSION, - hl: "en", - gl: "US", - platform: "MOBILE", - osName: "iPhone", - osVersion: "18.0.0.22A3354", - deviceMake: "Apple", - deviceModel: "iPhone16,2" - }; - case "MWEB": - return { - clientName: "MWEB", - clientVersion: watchContext.clientVersion, - hl: "en", - gl: "US", - originalUrl: `${YT_BASE}/watch?v=${videoId}` - }; - default: - return { - clientName: "WEB", - clientVersion: watchContext.clientVersion, - hl: "en", - gl: "US", - utcOffsetMinutes: 0, - originalUrl: `${YT_BASE}/watch?v=${videoId}` - }; - } - } - 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")) { - const id = url.pathname.split("/").find(Boolean); - if (id && VIDEO_ID_PATTERN.test(id)) { - return id; - } - } - const searchId = url.searchParams.get("v"); - if (searchId && VIDEO_ID_PATTERN.test(searchId)) { - return searchId; - } - const pathSegments = url.pathname.split("/").filter(Boolean); - const shortsIndex = pathSegments.indexOf("shorts"); - if (shortsIndex !== -1) { - const shortsId = pathSegments[shortsIndex + 1]; - if (shortsId && VIDEO_ID_PATTERN.test(shortsId)) { - return shortsId; - } - } - const embedIndex = pathSegments.indexOf("embed"); - if (embedIndex !== -1) { - const embedId = pathSegments[embedIndex + 1]; - if (embedId && VIDEO_ID_PATTERN.test(embedId)) { - return embedId; - } - } - throw new Error(`Cannot extract YouTube video id from: ${input}`); - } - 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; - } - } - return void 0; - } - 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 % alphabet.length] ?? "a"; - } - return output; - } - for (let i2 = 0; i2 < length; i2++) { - output += alphabet[Math.floor(Math.random() * alphabet.length)] ?? "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 parseContentLengthFromContentRange(contentRange) { - if (!contentRange) { - return null; - } - const matched = /\/(\d+)\s*$/i.exec(contentRange); - return parsePositiveInteger(matched?.[1]); - } - 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, ...CLIENT_FALLBACK_ORDER] : [...CLIENT_FALLBACK_ORDER]; - const seen2 = new Set(); - return ordered.filter((client) => { - if (seen2.has(client)) { - return false; - } - seen2.add(client); - return true; - }); - } - let AudioDownloader$1 = class AudioDownloader { - 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: { - ...DEFAULT_HEADERS, - 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; - } - async downloadStreamByRanges(streamUrl, contentLengthHint, signal) { - const fileSize = await this.resolveStreamContentLength( - streamUrl, - contentLengthHint, - signal, - true - ); - const merged = new Uint8Array(fileSize); - let offset = 0; - for (let start = 0; start < fileSize; start += RANGE_FALLBACK_CHUNK_SIZE) { - const end = Math.min(fileSize - 1, start + RANGE_FALLBACK_CHUNK_SIZE - 1); - const chunk = await this.fetchRangeChunk(streamUrl, start, end, signal); - if (offset + chunk.byteLength > merged.byteLength) { - throw new Error( - "Downloaded stream chunk exceeds probed stream content length" - ); - } - merged.set(chunk, offset); - offset += chunk.byteLength; - } - if (offset === merged.byteLength) { - return merged; - } - return merged.slice(0, offset); - } - async downloadAudioToChunkStream(request, options) { - if (options.chunkSize <= 0) { - throw new RangeError("Audio downloader. ytAudio. chunkSize must be > 0"); - } - return this.withResolvedPlayableAudioFormat( - request, - request.videoQuality ?? "best", - "Chunk mode requires an adaptive audio stream format", - "Unable to resolve streamable format for chunk mode", - async ({ resolved, signal }) => { - const fileSize = await this.resolveStreamContentLength( - resolved.streamUrl, - resolved.chosenFormat.contentLength, - signal, - true - ); - const mediaPartsLength = Math.max( - 1, - Math.ceil(fileSize / options.chunkSize) - ); - return { - videoId: resolved.videoId, - fileSize, - itag: resolved.chosenFormat.itag ?? 0, - mediaPartsLength, - getMediaBuffers: (async function* () { - for (let index = 0; index < mediaPartsLength; index++) { - const start = index * options.chunkSize; - const end = Math.min(fileSize - 1, start + options.chunkSize - 1); - const bytes = await this.fetchRangeChunk( - resolved.streamUrl, - start, - end, - signal - ); - 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.videoQuality ?? "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 (error2) { - const message = error2 instanceof Error ? error2.message : String(error2); - attemptErrors.push(`${client}: ${message}`); - } - } - throw new Error(`${failurePrefix}. Attempts: ${attemptErrors.join(" | ")}`); - } - async resolvePlayableFormatForClient({ - videoId, - watchContext, - client, - quality, - signal - }) { - const response = await this.fetchPlayerResponse( - videoId, - watchContext, - client, - signal - ); - const adaptiveFormats = response.streamingData?.adaptiveFormats ?? []; - const directAdaptiveFormats = 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 - ); - const streamUrl = this.resolveFormatUrl( - chosenFormat, - watchContext.clientVersion - ); - return { - videoId, - chosenFormat, - streamUrl - }; - } - async resolveStreamContentLength(streamUrl, contentLengthHint, signal, forceProbe = false) { - const hintedLength = parsePositiveInteger(contentLengthHint); - if (hintedLength !== null && !forceProbe) { - return hintedLength; - } - let probeResponse; - try { - probeResponse = await this.fetchFn(streamUrl, { - headers: { - ...DEFAULT_HEADERS, - range: "bytes=0-0" - }, - ...withSignal(signal) - }); - } catch (error2) { - if (hintedLength !== null) { - return hintedLength; - } - const message = error2 instanceof Error ? error2.message : String(error2); - throw new Error(`Failed to probe stream content length: ${message}`); - } - if (!probeResponse.ok) { - if (hintedLength !== null) { - return hintedLength; - } - throw new Error( - `Failed to probe stream content length: ${probeResponse.status}` - ); - } - const contentRangeLength = parseContentLengthFromContentRange( - probeResponse.headers.get("content-range") - ); - const storedLength = parsePositiveInteger( - probeResponse.headers.get("x-goog-stored-content-length") - ); - const contentLength = parsePositiveInteger( - probeResponse.headers.get("content-length") - ); - if (typeof probeResponse.body?.cancel === "function") { - try { - await probeResponse.body.cancel(); - } catch { - } - } - return contentRangeLength ?? storedLength ?? hintedLength ?? contentLength ?? (() => { - throw new Error("Failed to resolve stream content length"); - })(); - } - 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, clientVersion) { - if (!format.url) { - throw new Error("Selected format does not contain a direct stream URL"); - } - const streamUrl = new URL(format.url); - const client = streamUrl.searchParams.get("c"); - if (client === "WEB") { - streamUrl.searchParams.set("cver", clientVersion); - } - streamUrl.searchParams.set("cpn", makeCPN()); - return streamUrl.toString(); - } - async fetchWatchContext(videoId, signal) { - const watchUrl = `${YT_BASE}/watch?v=${encodeURIComponent(videoId)}&hl=en`; - const response = await this.fetchFn(watchUrl, { - headers: DEFAULT_HEADERS, - ...withSignal(signal) - }); - if (!response.ok) { - if (response.status === 403) { - throw new YtWatchContextForbiddenError(response.status); - } - 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, - watchContext, - videoId - ); - 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 = `${YT_BASE}/youtubei/v1/player?key=${encodeURIComponent(watchContext.apiKey)}`; - const response = await this.fetchFn(endpoint, { - method: "POST", - headers: { - ...DEFAULT_HEADERS, - "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; - } - }; - const DEFAULT_YT_AUDIO_QUALITY = "bestefficiency"; - const 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, init2 = {}) => await GM_fetch(input, { - ...init2, - signal: init2.signal ?? signal, - forceGmXhr: true, - timeout: timeoutMs - }); - } - function isWatchContextForbiddenError(error2) { - if (error2 instanceof YtWatchContextForbiddenError) { - return true; - } - return error2 instanceof Error && /failed to load watch page:\s*403/i.test(error2.message); - } - async function getAudioFromYtAudio({ videoId, signal }, deps = {}) { - const chunkSize = deps.chunkSize ?? votConfig.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, - videoQuality: 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 (error2) { - if (isWatchContextForbiddenError(error2)) { - throw error2; - } - console.warn( - "[VOT] ytAudio streaming mode failed, falling back to buffered mode", - error2 - ); - } - const result = await downloader.downloadAudioToUint8Array({ - videoId, - videoQuality: DEFAULT_YT_AUDIO_QUALITY, - signal - }); - const bytes = result.bytes; - if (!bytes || bytes.byteLength === 0) { - throw new Error("Audio downloader. ytAudio. Empty audio"); - } - const mediaPartsLength = Math.max(1, Math.ceil(bytes.byteLength / chunkSize)); - const fileId = makeFileId( - AudioDownloadType.WEB_API_STEAL_SIG_AND_N, - 0, - String(bytes.byteLength), - chunkSize - ); - return { - fileId, - 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); - } - } - }; - } - const YT_AUDIO_STRATEGY = "ytAudio"; - const strategies = { - [YT_AUDIO_STRATEGY]: getAudioFromYtAudio - }; - 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 iterator = getMediaBuffers(); - const { value } = await iterator.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}` - ); - } - } - class AudioDownloader2 { - onDownloadedAudio = new EventImpl(); - onDownloadedPartialAudio = new EventImpl(); - onDownloadAudioError = new EventImpl(); - strategy; - constructor(strategy = YT_AUDIO_STRATEGY) { - this.strategy = 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; - } - } - const MAX_SECS_FRACTION = 0.66; - function formatTranslationEta(secs2, getMessage) { - let minutes = Math.floor(secs2 / 60); - const seconds = Math.floor(secs2 % 60); - const fraction = seconds / 60; - if (fraction >= 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 - ); - } - class VOTLocalizedError extends Error { - name = "VOTLocalizedError"; -unlocalizedMessage; -localizedMessage; - constructor(message) { - super(localizationProvider.getDefault(message)); - this.unlocalizedMessage = message; - this.localizedMessage = localizationProvider.get(message); - } - } - 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 updateTranslationAndSchedule(options) { - if (options.isActionStale(options.actionContext)) { - return false; - } - await options.updateTranslation(options.url, options.actionContext); - if (options.isActionStale(options.actionContext)) { - return false; - } - options.scheduleTranslationRefresh(); - 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; - } - const updated = await updateTranslationAndSchedule({ - url: translateRes.url, - actionContext: options.actionContext, - isActionStale: options.isActionStale, - updateTranslation: options.updateTranslation, - scheduleTranslationRefresh: options.scheduleTranslationRefresh - }); - if (!updated || 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; - } - 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 err = asVotClientErrorShape(value); - const message = err?.data?.message; - return typeof message === "string" && message.length > 0 ? message : void 0; - } - function mapVotClientErrorForUi(error2) { - const err = asVotClientErrorShape(error2); - if (!err) { - return error2; - } - if (err.name !== "VOTJSError") { - return error2; - } - 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 error2; - } - class VOTTranslationHandler { - videoHandler; - audioDownloader; - downloading; - downloadWaiters = new Set(); - -requestedFailAudio = new Set(); - constructor(videoHandler) { - this.videoHandler = videoHandler; - this.audioDownloader = new AudioDownloader2(); - this.downloading = false; - this.audioDownloader.addEventListener("downloadedAudio", this.onDownloadedAudio).addEventListener("downloadedPartialAudio", this.onDownloadedPartialAudio).addEventListener("downloadAudioError", this.onDownloadAudioError); - } - onDownloadedAudio = async (translationId, data) => { - if (!this.downloading) { - return; - } - const { videoId, fileId, audioData } = data; - const videoUrl = this.getCanonicalUrl(videoId); - try { - await this.videoHandler.votClient.requestVtransAudio( - videoUrl, - translationId, - { - audioFile: audioData, - fileId - } - ); - } catch (error2) { - this.finishDownloadFailure( - new Error("Audio downloader failed while uploading full audio") - ); - return; - } - this.finishDownloadSuccess(); - }; - onDownloadedPartialAudio = async (translationId, data) => { - if (!this.downloading) { - 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 (error2) { - this.finishDownloadFailure( - new Error("Audio downloader failed while uploading chunk") - ); - return; - } - if (index === amount - 1) { - this.finishDownloadSuccess(); - } - }; - onDownloadAudioError = async (videoId) => { - if (!this.downloading) { - return; - } - const videoUrl = this.getCanonicalUrl(videoId); - const shouldUseFallback = this.videoHandler.site.host === "youtube" && Boolean(this.videoHandler.data?.useAudioDownload); - if (!shouldUseFallback) { - 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 (error2) { - this.finishDownloadFailure( - new VOTLocalizedError("VOTFailedDownloadAudio") - ); - } - }; - finishDownloadSuccess() { - this.downloading = false; - this.resolveDownloadWaiters(); - } - finishDownloadFailure(error2) { - this.downloading = false; - this.rejectDownloadWaiters(error2); - } - getCanonicalUrl(videoId) { - return `https://youtu.be/${videoId}`; - } - -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 { - const result = await fn(); - resolve(result); - } catch (error2) { - reject(error2); - } - }, delayMs); - if (timeoutId !== null) { - this.videoHandler.autoRetry = timeoutId; - } - }); - } - async translateVideoImpl(videoData, requestLang, responseLang, translationHelp = null, shouldSendFailedAudio = false, signal = NEVER_ABORTED_SIGNAL, disableLivelyVoice = false) { - clearTimeout(this.videoHandler.autoRetry); - this.finishDownloadSuccess(); - const requestLangForApi = this.videoHandler.getRequestLangForTranslation( - requestLang, - responseLang - ); - let livelyDisabled = disableLivelyVoice; - try { - throwIfAborted(signal); - const livelyVoiceAllowed = this.videoHandler.isLivelyVoiceAllowed( - requestLangForApi, - responseLang - ); - let useLivelyVoice = !livelyDisabled && livelyVoiceAllowed && Boolean(this.videoHandler.data?.useLivelyVoice); - let res; - for (let attempt = 0; attempt < 2; attempt++) { - try { - res = await this.videoHandler.votClient.translateVideo({ - videoData, - requestLang: requestLangForApi, - responseLang, - translationHelp, - extraOpts: { - useLivelyVoice, - videoTitle: this.videoHandler.videoData?.title - }, - shouldSendFailedAudio - }); - } catch (err) { - if (useLivelyVoice && this.isLivelyVoiceUnavailableError(err)) { - debug.log( - "[translateVideoImpl] Lively voices are unavailable. Falling back to standard translation.", - err - ); - livelyDisabled = true; - useLivelyVoice = false; - continue; - } - throw err; - } - if (useLivelyVoice && this.isLivelyVoiceUnavailableError(res)) { - debug.log( - "[translateVideoImpl] Server responded that lively voices are unavailable. Falling back to standard translation.", - res - ); - livelyDisabled = true; - useLivelyVoice = false; - res = void 0; - continue; - } - break; - } - if (!res) { - throw new Error("Failed to get translation response"); - } - debug.log("Translate video result", res); - throwIfAborted(signal); - if (res.translated && res.remainingTime < 1) { - debug.log("Video translation finished with this data: ", res); - return { ...res, usedLivelyVoice: useLivelyVoice }; - } - const message = res.message ?? localizationProvider.get("translationTakeFewMinutes"); - 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("Start audio download"); - this.downloading = true; - await this.audioDownloader.runAudioDownload( - videoData.videoId, - res.translationId, - signal - ); - debug.log("waiting downloading finish"); - await this.waitForAudioDownloadCompletion(signal, 15e3); - return await this.translateVideoImpl( - videoData, - requestLang, - responseLang, - translationHelp, - true, - signal, - livelyDisabled - ); - } - } catch (err) { - if (isAbortError(err)) { - return null; - } - const uiError = mapVotClientErrorForUi(err); - 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) - }); - console.error("[VOT]", err); - return null; - } - this.videoHandler.hadAsyncWait = true; - return this.scheduleRetry( - () => this.translateVideoImpl( - videoData, - requestLang, - responseLang, - translationHelp, - shouldSendFailedAudio, - signal, - livelyDisabled - ), - 2e4, - signal - ); - } - 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: (error2) => { - cleanup(); - reject(error2); - } - }; - this.downloadWaiters.add(entry); - signal.addEventListener("abort", onAbort, { once: true }); - if (signal.aborted) { - onAbort(); - } - }); - } - resolveDownloadWaiters() { - this.forEachDownloadWaiter((waiter) => waiter.resolve()); - } - rejectDownloadWaiters(error2) { - this.forEachDownloadWaiter((waiter) => waiter.reject(error2)); - } - forEachDownloadWaiter(handler) { - if (!this.downloadWaiters.size) { - return; - } - const waiters = Array.from(this.downloadWaiters); - this.downloadWaiters.clear(); - for (const waiter of waiters) { - handler(waiter); - } - } - } - class TranslationOrchestrator { - state = { status: "idle" }; - deps; - constructor(deps) { - this.deps = deps; - } - get currentState() { - return this.state; - } - setState(next) { - this.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?.()) { - this.setState({ status: "deferred", reason: "muted" }); - this.deps.setMuteWatcher?.(() => { - this.setState({ status: "idle" }); - void 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; - } - } - } - 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 }); - } - class VideoLifecycleController { - 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}`); - 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; - const stableUrlKey = `${globalThis.location.origin}${path}${globalThis.location.search}`; - return `${stableUrlKey}||${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" - ); - } - 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) { - return; - } - let nextVideoData; - try { - nextVideoData = await this.host.getVideoData(); - } catch (err) { - this.host.videoData = void 0; - hideLifecycleOverlay(this.host.uiManager.votOverlayView, { - hideMenu: true - }); - return; - } - if (this.getCurrentSourceKey() !== sourceKey) { - return; - } - this.host.videoData = nextVideoData; - this.activeSetCanPlaySourceKey = sourceKey; - const currentId = this.startSession(`setCanPlay (source: ${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) { - } - } - 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; - } - this.host.translationOrchestrator.reset(); - this.host.firstPlay = true; - const overlayView = this.host.uiManager.votOverlayView; - resetAndHideLifecycle(this.host, overlayView, { requireVideoData: true }); - const noSrc = !this.host.video.src && !this.host.video.currentSrc && !this.host.video.srcObject; - if (noSrc) { - 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) { - hideLifecycleOverlay(overlayView, { hideMenu: true }); - return; - } - const cacheKey = this.host.getSubtitlesCacheKey( - this.host.videoData.videoId, - this.host.videoData.detectedLanguage, - this.host.videoData.responseLanguage - ); - const cachedSubtitles = this.host.cacheManager.getSubtitles(cacheKey); - this.host.subtitles = cachedSubtitles ?? []; - this.host.subtitlesCacheKey = cachedSubtitles !== void 0 ? cacheKey : 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; - } - } - const MAX_TEXT_LENGTH = 450; - const 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" - ); - const NOISE_CHARACTER_FILTER = /[\p{N}\p{P}\p{S}]+/gu; - const WHITESPACE_FILTER = /\s+/g; - const LETTER_FILTER = new RegExp("\\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); - } - const SETTINGS_CACHE_TTL_MS = 5e3; - const IMMUTABLE_API_CACHE_TTL_MS = Number.MAX_SAFE_INTEGER; - let cachedTranslationService = null; - let cachedTranslationServiceAt = 0; - let cachedDetectService = null; - let cachedDetectServiceAt = 0; - async function getTranslationServiceCached() { - const now2 = Date.now(); - if (cachedTranslationService && now2 - cachedTranslationServiceAt < SETTINGS_CACHE_TTL_MS) { - return cachedTranslationService; - } - const service = await votStorage.get( - "translationService", - defaultTranslationService - ); - cachedTranslationService = String(service); - cachedTranslationServiceAt = now2; - return cachedTranslationService; - } - async function getDetectServiceCached() { - const now2 = Date.now(); - if (cachedDetectService && now2 - cachedDetectServiceAt < SETTINGS_CACHE_TTL_MS) { - return cachedDetectService; - } - const service = await votStorage.get("detectService", defaultDetectService); - cachedDetectService = String(service); - cachedDetectServiceAt = now2; - return cachedDetectService; - } - const foswlyServices = ["yandexbrowser", "msedge"]; - const FOSWLYTranslateAPI = new class { - isFOSWLYError(data) { - return Object.hasOwn(data, "error"); - } - async request(path, opts = {}) { - try { - const res = await GM_fetch(`${foswlyTranslateUrl}${path}`, { - timeout: 3e3, - responseCache: { - ttlMs: IMMUTABLE_API_CACHE_TTL_MS, - cacheName: "vot-foswly-api-v1", - allowStaleOnError: true - }, - ...opts - }); - const data = await res.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 void 0; - } - } - async translateMultiple(text, lang2, service) { - const result = await this.request( - "/translate", - { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - text, - lang: lang2, - service - }) - } - ); - return result ? result.translations : text; - } - async translate(text, lang2, service) { - const result = await this.request( - `/translate?${new URLSearchParams({ - text, - lang: lang2, - 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"; - } - }(); - const RustServerAPI = { - async detect(text) { - try { - const response = await GM_fetch(detectRustServerUrl, { - method: "POST", - body: text, - timeout: 3e3, - responseCache: { - ttlMs: IMMUTABLE_API_CACHE_TTL_MS, - cacheName: "vot-rust-detect-v1", - allowStaleOnError: true - } - }); - return await response.text(); - } catch (error2) { - console.error( - `[VOT] Error getting lang from text, because ${error2.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"; - } - } - const detectServices = [...foswlyServices, "rust-server"]; - const VIDEO_VOLUME_MIN_PERCENT = 0; - const VIDEO_VOLUME_MAX_PERCENT = 100; - const VIDEO_VOLUME_STEP_01 = 0.01; - const EPS = 1e-6; - 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 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) { - const v2 = clampNumber$1(volume01, 0, 1); - return clampPercentInt(v2 * 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) { - const clamped = clampNumber$1(volume01, 0, 1); - const quantized = quantizeToStep(clamped, step, direction); - return clampNumber$1(quantized, 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); - } - const EXTERNAL_VOLUME_HOSTS = new Set(["youtube", "googledrive"]); - const YOUTUBE_LIKE_HOSTS = EXTERNAL_VOLUME_HOSTS; - const MUTE_SYNC_DISABLED_HOSTS = new Set(["rutube", "ok"]); - const 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); - } - const 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" - }; - const YT_VOLUME_NOW_SELECTOR = ".ytp-volume-panel [aria-valuenow]"; - const MIN_DETECT_TEXT_LENGTH = 35; - const MAX_SHARED_LANGUAGE_STATES = 500; - const REQUEST_LANG_SET = new Set( - availableLangs - ); - const sharedLanguageStateByVideoId = 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) { - const textTitle = typeof title === "string" ? title : ""; - const textDescription = typeof description === "string" ? description : void 0; - return cleanText(textTitle, textDescription); - } - 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); - } - return void 0; - } - function resolveYoutubeDetectedLanguageFromSubtitles(subtitles) { - if (!Array.isArray(subtitles) || subtitles.length === 0) { - return void 0; - } - 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 void 0; - }; - return pickLanguage(true) ?? pickLanguage(false); - } - 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 now2 = rawNow == null ? Number.NaN : Number.parseFloat(rawNow); - const max = rawMax == null ? Number.NaN : Number.parseFloat(rawMax); - if (!Number.isFinite(now2)) return null; - if (Number.isFinite(max) && max > 0) { - return clampPercentInt(now2 / max * 100); - } - return clampPercentInt(now2); - } - class VOTVideoManager { - videoHandler; - constructor(videoHandler) { - this.videoHandler = videoHandler; - } - setDetectedLanguageCache(videoId, language) { - getSharedLanguageState(videoId).detectedLanguage = language; - } - rememberUserLanguageSelection(videoId, language) { - const normalizedLanguage = normalizeToRequestLang(language); - if (!isResolvedLanguage(normalizedLanguage)) { - const sharedLanguageState2 = sharedLanguageStateByVideoId.get(videoId); - if (sharedLanguageState2) { - delete sharedLanguageState2.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 () => { - 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 === "auto") { - return; - } - videoData.detectedLanguage = detectedLanguage; - if (this.videoHandler.translateFromLang === "auto") { - this.videoHandler.translateFromLang = detectedLanguage; - } - } - 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 || votConfig.defaultDuration, -videoId, - url, - host, - detectedLanguage, - responseLanguage: this.videoHandler.translateToLang, - subtitles, - title, - localizedTitle, - description, - downloadTitle: localizedTitle ?? title ?? videoId - }; - if (sharedLanguageState.lastLoggedDetectedLanguage !== detectedLanguage) { - console.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; - } -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); - } -setVideoVolume(volume) { - const snapped = snapVolume01(volume); - if (!isExternalVolumeHost(this.videoHandler.site.host)) { - this.videoHandler.video.volume = snapped; - return this; - } - try { - const result = YoutubeHelper.setVolume(snapped); - const ok = typeof result === "boolean" && result || typeof result === "number" && Number.isFinite(result); - if (ok) return this; - } catch { - } - this.videoHandler.video.volume = snapped; - return this; - } -isMuted() { - return isExternalVolumeHost(this.videoHandler.site.host) ? YoutubeHelper.isMuted() : this.videoHandler.video?.muted; - } -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) { - console.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; - } - } - const t = globalThis, i = (t2) => t2, s = t.trustedTypes, e = s ? s.createPolicy("lit-html", { createHTML: (t2) => t2 }) : void 0, h = "$lit$", o = `lit$${Math.random().toFixed(9).slice(2)}$`, n = "?" + o, r = `<${n}>`, l = document, c = () => l.createComment(""), a = (t2) => null === t2 || "object" != typeof t2 && "function" != typeof t2, u = Array.isArray, d = (t2) => u(t2) || "function" == typeof t2?.[Symbol.iterator], f = "[ \n\f\r]", v = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, _ = /-->/g, m = />/g, p = RegExp(`>|${f}(?:([^\\s"'>=/]+)(${f}*=${f}*(?:[^ -\f\r"'\`<>=]|("|')|))|$)`, "g"), g = /'/g, $ = /"/g, y = /^(?:script|style|textarea|title)$/i, x = (t2) => (i2, ...s2) => ({ _$litType$: t2, strings: i2, values: s2 }), b = x(1), w = x(2), E = Symbol.for("lit-noChange"), A = Symbol.for("lit-nothing"), C = new WeakMap(), P = l.createTreeWalker(l, 129); - function V(t2, i2) { - if (!u(t2) || !t2.hasOwnProperty("raw")) throw Error("invalid template strings array"); - return void 0 !== e ? e.createHTML(i2) : i2; - } - const N = (t2, i2) => { - const s2 = t2.length - 1, e2 = []; - let n2, l2 = 2 === i2 ? "" : 3 === i2 ? "" : "", c2 = v; - for (let i3 = 0; i3 < s2; i3++) { - const s3 = t2[i3]; - let a2, u2, d2 = -1, f2 = 0; - for (; f2 < s3.length && (c2.lastIndex = f2, u2 = c2.exec(s3), null !== u2); ) f2 = c2.lastIndex, c2 === v ? "!--" === u2[1] ? c2 = _ : void 0 !== u2[1] ? c2 = m : void 0 !== u2[2] ? (y.test(u2[2]) && (n2 = RegExp("" === u2[0] ? (c2 = n2 ?? v, d2 = -1) : void 0 === u2[1] ? d2 = -2 : (d2 = c2.lastIndex - u2[2].length, a2 = u2[1], c2 = void 0 === u2[3] ? p : '"' === u2[3] ? $ : g) : c2 === $ || c2 === g ? c2 = p : c2 === _ || c2 === m ? c2 = v : (c2 = p, n2 = void 0); - const x2 = c2 === p && t2[i3 + 1].startsWith("/>") ? " " : ""; - l2 += c2 === v ? s3 + r : d2 >= 0 ? (e2.push(a2), s3.slice(0, d2) + h + s3.slice(d2) + o + x2) : s3 + o + (-2 === d2 ? i3 : x2); - } - return [V(t2, l2 + (t2[s2] || "") + (2 === i2 ? "" : 3 === i2 ? "" : "")), e2]; - }; - class S { - constructor({ strings: t2, _$litType$: i2 }, e2) { - let r2; - this.parts = []; - let l2 = 0, a2 = 0; - const u2 = t2.length - 1, d2 = this.parts, [f2, v2] = N(t2, i2); - if (this.el = S.createElement(f2, e2), P.currentNode = this.el.content, 2 === i2 || 3 === i2) { - const t3 = this.el.content.firstChild; - t3.replaceWith(...t3.childNodes); - } - for (; null !== (r2 = P.nextNode()) && d2.length < u2; ) { - if (1 === r2.nodeType) { - if (r2.hasAttributes()) for (const t3 of r2.getAttributeNames()) if (t3.endsWith(h)) { - const i3 = v2[a2++], s2 = r2.getAttribute(t3).split(o), e3 = /([.?@])?(.*)/.exec(i3); - d2.push({ type: 1, index: l2, name: e3[2], strings: s2, ctor: "." === e3[1] ? I : "?" === e3[1] ? L : "@" === e3[1] ? z : H }), r2.removeAttribute(t3); - } else t3.startsWith(o) && (d2.push({ type: 6, index: l2 }), r2.removeAttribute(t3)); - if (y.test(r2.tagName)) { - const t3 = r2.textContent.split(o), i3 = t3.length - 1; - if (i3 > 0) { - r2.textContent = s ? s.emptyScript : ""; - for (let s2 = 0; s2 < i3; s2++) r2.append(t3[s2], c()), P.nextNode(), d2.push({ type: 2, index: ++l2 }); - r2.append(t3[i3], c()); - } - } - } else if (8 === r2.nodeType) if (r2.data === n) d2.push({ type: 2, index: l2 }); - else { - let t3 = -1; - for (; -1 !== (t3 = r2.data.indexOf(o, t3 + 1)); ) d2.push({ type: 7, index: l2 }), t3 += o.length - 1; - } - l2++; - } - } - static createElement(t2, i2) { - const s2 = l.createElement("template"); - return s2.innerHTML = t2, s2; - } - } - function M(t2, i2, s2 = t2, e2) { - if (i2 === E) return i2; - let h2 = void 0 !== e2 ? s2._$Co?.[e2] : s2._$Cl; - const o2 = a(i2) ? void 0 : i2._$litDirective$; - return h2?.constructor !== o2 && (h2?._$AO?.(false), void 0 === o2 ? h2 = void 0 : (h2 = new o2(t2), h2._$AT(t2, s2, e2)), void 0 !== e2 ? (s2._$Co ??= [])[e2] = h2 : s2._$Cl = h2), void 0 !== h2 && (i2 = M(t2, h2._$AS(t2, i2.values), h2, e2)), i2; - } - class R { - constructor(t2, i2) { - this._$AV = [], this._$AN = void 0, this._$AD = t2, this._$AM = i2; - } - get parentNode() { - return this._$AM.parentNode; - } - get _$AU() { - return this._$AM._$AU; - } - u(t2) { - const { el: { content: i2 }, parts: s2 } = this._$AD, e2 = (t2?.creationScope ?? l).importNode(i2, true); - P.currentNode = e2; - let h2 = P.nextNode(), o2 = 0, n2 = 0, r2 = s2[0]; - for (; void 0 !== r2; ) { - if (o2 === r2.index) { - let i3; - 2 === r2.type ? i3 = new k(h2, h2.nextSibling, this, t2) : 1 === r2.type ? i3 = new r2.ctor(h2, r2.name, r2.strings, this, t2) : 6 === r2.type && (i3 = new Z(h2, this, t2)), this._$AV.push(i3), r2 = s2[++n2]; - } - o2 !== r2?.index && (h2 = P.nextNode(), o2++); - } - return P.currentNode = l, e2; - } - p(t2) { - let i2 = 0; - for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t2, s2, i2), i2 += s2.strings.length - 2) : s2._$AI(t2[i2])), i2++; - } - } - class k { - get _$AU() { - return this._$AM?._$AU ?? this._$Cv; - } - constructor(t2, i2, s2, e2) { - this.type = 2, this._$AH = A, this._$AN = void 0, this._$AA = t2, this._$AB = i2, this._$AM = s2, this.options = e2, this._$Cv = e2?.isConnected ?? true; - } - get parentNode() { - let t2 = this._$AA.parentNode; - const i2 = this._$AM; - return void 0 !== i2 && 11 === t2?.nodeType && (t2 = i2.parentNode), t2; - } - get startNode() { - return this._$AA; - } - get endNode() { - return this._$AB; - } - _$AI(t2, i2 = this) { - t2 = M(this, t2, i2), a(t2) ? t2 === A || null == t2 || "" === t2 ? (this._$AH !== A && this._$AR(), this._$AH = A) : t2 !== this._$AH && t2 !== E && this._(t2) : void 0 !== t2._$litType$ ? this.$(t2) : void 0 !== t2.nodeType ? this.T(t2) : d(t2) ? this.k(t2) : this._(t2); - } - O(t2) { - return this._$AA.parentNode.insertBefore(t2, this._$AB); - } - T(t2) { - this._$AH !== t2 && (this._$AR(), this._$AH = this.O(t2)); - } - _(t2) { - this._$AH !== A && a(this._$AH) ? this._$AA.nextSibling.data = t2 : this.T(l.createTextNode(t2)), this._$AH = t2; - } - $(t2) { - const { values: i2, _$litType$: s2 } = t2, e2 = "number" == typeof s2 ? this._$AC(t2) : (void 0 === s2.el && (s2.el = S.createElement(V(s2.h, s2.h[0]), this.options)), s2); - if (this._$AH?._$AD === e2) this._$AH.p(i2); - else { - const t3 = new R(e2, this), s3 = t3.u(this.options); - t3.p(i2), this.T(s3), this._$AH = t3; - } - } - _$AC(t2) { - let i2 = C.get(t2.strings); - return void 0 === i2 && C.set(t2.strings, i2 = new S(t2)), i2; - } - k(t2) { - u(this._$AH) || (this._$AH = [], this._$AR()); - const i2 = this._$AH; - let s2, e2 = 0; - for (const h2 of t2) e2 === i2.length ? i2.push(s2 = new k(this.O(c()), this.O(c()), this, this.options)) : s2 = i2[e2], s2._$AI(h2), e2++; - e2 < i2.length && (this._$AR(s2 && s2._$AB.nextSibling, e2), i2.length = e2); - } - _$AR(t2 = this._$AA.nextSibling, s2) { - for (this._$AP?.(false, true, s2); t2 !== this._$AB; ) { - const s3 = i(t2).nextSibling; - i(t2).remove(), t2 = s3; - } - } - setConnected(t2) { - void 0 === this._$AM && (this._$Cv = t2, this._$AP?.(t2)); - } - } - class H { - get tagName() { - return this.element.tagName; - } - get _$AU() { - return this._$AM._$AU; - } - constructor(t2, i2, s2, e2, h2) { - this.type = 1, this._$AH = A, this._$AN = void 0, this.element = t2, this.name = i2, this._$AM = e2, this.options = h2, s2.length > 2 || "" !== s2[0] || "" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = A; - } - _$AI(t2, i2 = this, s2, e2) { - const h2 = this.strings; - let o2 = false; - if (void 0 === h2) t2 = M(this, t2, i2, 0), o2 = !a(t2) || t2 !== this._$AH && t2 !== E, o2 && (this._$AH = t2); - else { - const e3 = t2; - let n2, r2; - for (t2 = h2[0], n2 = 0; n2 < h2.length - 1; n2++) r2 = M(this, e3[s2 + n2], i2, n2), r2 === E && (r2 = this._$AH[n2]), o2 ||= !a(r2) || r2 !== this._$AH[n2], r2 === A ? t2 = A : t2 !== A && (t2 += (r2 ?? "") + h2[n2 + 1]), this._$AH[n2] = r2; - } - o2 && !e2 && this.j(t2); - } - j(t2) { - t2 === A ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t2 ?? ""); - } - } - class I extends H { - constructor() { - super(...arguments), this.type = 3; - } - j(t2) { - this.element[this.name] = t2 === A ? void 0 : t2; - } - } - class L extends H { - constructor() { - super(...arguments), this.type = 4; - } - j(t2) { - this.element.toggleAttribute(this.name, !!t2 && t2 !== A); - } - } - class z extends H { - constructor(t2, i2, s2, e2, h2) { - super(t2, i2, s2, e2, h2), this.type = 5; - } - _$AI(t2, i2 = this) { - if ((t2 = M(this, t2, i2, 0) ?? A) === E) return; - const s2 = this._$AH, e2 = t2 === A && s2 !== A || t2.capture !== s2.capture || t2.once !== s2.once || t2.passive !== s2.passive, h2 = t2 !== A && (s2 === A || e2); - e2 && this.element.removeEventListener(this.name, this, s2), h2 && this.element.addEventListener(this.name, this, t2), this._$AH = t2; - } - handleEvent(t2) { - "function" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t2) : this._$AH.handleEvent(t2); - } - } - class Z { - constructor(t2, i2, s2) { - this.element = t2, this.type = 6, this._$AN = void 0, this._$AM = i2, this.options = s2; - } - get _$AU() { - return this._$AM._$AU; - } - _$AI(t2) { - M(this, t2); - } - } - const B = t.litHtmlPolyfillSupport; - B?.(S, k), (t.litHtmlVersions ??= []).push("3.3.2"); - const D = (t2, i2, s2) => { - const e2 = i2; - let h2 = e2._$litPart$; - if (void 0 === h2) { - const t3 = null; - e2._$litPart$ = h2 = new k(i2.insertBefore(c(), t3), t3, void 0, {}); - } - return h2._$AI(t2), h2; - }; - const mainScss = '.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 (min-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(max-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:"";inherits:true;initial-value:.8}@property --vot-subtitles-scale-compensation{syntax:"";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);width:max-content;background:var(--vot-subtitles-background,#2e2f34cc);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;margin:0 auto;display:block}.vot-subtitles.vot-subtitles--clamped{overflow:hidden}@supports (line-clamp:2){.vot-subtitles.vot-subtitles--clamped{line-clamp:2}}@supports not (line-clamp:2){.vot-subtitles.vot-subtitles--clamped{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box}}.vot-subtitles{text-wrap:balance;white-space:normal;overflow-wrap:anywhere}.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:42ch;--vot-subtitles-smart-min-width-ratio:.55;--vot-subtitles-smart-max-width-ratio:.68;--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-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"].passed{color:var(--vot-subtitles-passed-color,#2196f3)}.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-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(max-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(min-aspect-ratio:1)and (max-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(max-width:900px)and (pointer:coarse){.vot-subtitles-widget{--vot-subtitles-fallback-bottom-inset:env(safe-area-inset-bottom,0px)}}:-webkit-any(:-webkit-full-screen .vot-subtitles-widget,:-webkit-full-screen .vot-subtitles-widget){--vot-subtitles-smart-max-width-ratio:.8}:is(:fullscreen .vot-subtitles-widget){--vot-subtitles-smart-max-width-ratio:.8}:-webkit-any(:-webkit-full-screen .vot-subtitles,:-webkit-full-screen .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))}:is(: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;-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}'; - importCSS(mainScss); - 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", - (e2) => { - if (e2.key === "Tab") enable(); - }, - true - ); - for (const evt of ["pointerdown", "mousedown", "touchstart"]) { - globalThis.addEventListener(evt, disable, { - capture: true, - passive: true - }); - } - } - initKeyboardNavigationMode(); - const UI = { -makeButtonLike(el, { ariaLabel } = {}) { - el.setAttribute("role", "button"); - if (!el.hasAttribute("tabindex")) { - el.tabIndex = 0; - } - const enabledTabIndex = el.tabIndex; - const syncDisabledState = () => { - const isDisabled = el.getAttribute("disabled") === "true"; - if (isDisabled) { - 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); - } - el.addEventListener("keydown", (e2) => { - const disabled = el.getAttribute("disabled") === "true" || el.getAttribute("aria-disabled") === "true"; - if (disabled) return; - if (e2.key === "Enter" || e2.key === " ") { - e2.preventDefault(); - 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 - }; - } - }; - const positions$1 = ["left", "top", "right", "bottom"]; - const triggers = ["hover", "click"]; - class Tooltip { -showed = false; - target; - anchor; - content; - position; - preferredPosition; - trigger; - parentElement; - layoutRoot; - offsetX; - offsetY; - _hidden; - autoLayout; - pageWidth; - pageHeight; - globalOffsetX; - globalOffsetY; - maxWidth; - backgroundColor; - borderRadius; - _bordered; - container; - onResizeObserver; - intersectionObserver; - scrollListening = false; - positionRafId = null; - destroyFallbackTimerId; - static DESTROY_FALLBACK_MS = 700; -tooltipId = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `vot-tooltip-${Math.random().toString(36).slice(2)}`; - prevAriaDescribedBy = null; - constructor({ - target, - anchor = void 0, - content = "", - position: position2 = "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(position2) ? position2 : "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(position2) { - return positions$1.includes(position2); - } - static validateTrigger(trigger) { - return triggers.includes(trigger); - } - setPosition(position2) { - this.preferredPosition = Tooltip.validatePos(position2) ? position2 : "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; - } -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(); - }; - onTargetKeyDown = (event) => { - if (event.key !== "Escape" || !this.showed) { - return; - } - this.destroy(); - }; - onScroll = () => { - this.schedulePositionUpdate(); - }; - onHoverPointerDown = (e2) => { - if (e2.pointerType === "mouse") { - return; - } - this.create(); - }; - onHoverPointerUp = (e2) => { - if (e2.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; - } - 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); - } - 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$2(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, position2 = 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$2(containerWidth, 0, this.pageWidth); - const height = clamp$2(containerHeight, 0, this.pageHeight); - const left = anchorLeft - this.globalOffsetX; - const right = anchorRight - this.globalOffsetX; - const top = anchorTop - this.globalOffsetY; - const bottom = anchorBottom - this.globalOffsetY; - let resolvedPosition = position2; - if (autoLayout) { - switch (position2) { - case "top": { - const pTop = clamp$2(top - height - this.offsetY, 0, this.pageHeight); - if (pTop + this.offsetY < height) { - resolvedPosition = "bottom"; - } - break; - } - case "right": { - const pLeft = clamp$2(right + this.offsetX, 0, this.pageWidth - width); - if (pLeft + width > this.pageWidth - this.offsetX) { - resolvedPosition = "left"; - } - break; - } - case "bottom": { - const pTop = clamp$2( - bottom + this.offsetY, - 0, - this.pageHeight - height - ); - if (pTop + height > this.pageHeight - this.offsetY) { - resolvedPosition = "top"; - } - break; - } - case "left": { - const pLeft = Math.max(0, left - width - this.offsetX); - if (pLeft + width > left - this.offsetX) { - resolvedPosition = "right"; - } - break; - } - } - } - let coords; - switch (resolvedPosition) { - case "top": - coords = { - top: clamp$2(top - height - this.offsetY, 0, this.pageHeight), - left: clamp$2( - left - width / 2 + anchorWidth / 2, - this.offsetX, - this.pageWidth - width - this.offsetX - ) - }; - break; - case "right": - coords = { - top: clamp$2( - top + (anchorHeight - height) / 2, - this.offsetY, - this.pageHeight - height - this.offsetY - ), - left: clamp$2(right + this.offsetX, 0, this.pageWidth - width) - }; - break; - case "bottom": - coords = { - top: clamp$2(bottom + this.offsetY, 0, this.pageHeight - height), - left: clamp$2( - left - width / 2 + anchorWidth / 2, - this.offsetX, - this.pageWidth - width - this.offsetX - ) - }; - break; - case "left": - coords = { - top: clamp$2( - top + (anchorHeight - height) / 2, - this.offsetY, - this.pageHeight - height - this.offsetY - ), - left: Math.max(0, left - width - this.offsetX) - }; - break; - default: - coords = { top: 0, left: 0 }; - } - this.position = resolvedPosition; - return { - top: coords.top + this.globalOffsetY, - left: coords.left + this.globalOffsetX - }; - } - 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(); - 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; - } - 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 - }); - } - } - const subtitleFormats = ["srt", "vtt", "ass", "json"]; - const subtitleFontFamilies = [ - "default-sans", - "arial", - "helvetica", - "roboto", - "verdana", - "open-sans", - "poppins", - "lato", - "montserrat", - "barlow" - ]; - const 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` - }; - function isBuiltInSubtitleFontFamily(fontFamily) { - return subtitleFontFamilies.includes(fontFamily); - } - const GOOGLE_SUBTITLE_FONT_PREFIX = "google:"; - const GOOGLE_FONTS_CSS_API_URL = "https://fonts.googleapis.com/css2"; - const GOOGLE_FONTS_METADATA_URL = "https://fonts.google.com/metadata/fonts"; - const subtitleGoogleFontFamilyNames = { - roboto: "Roboto", - "open-sans": "Open Sans", - poppins: "Poppins", - lato: "Lato", - montserrat: "Montserrat", - barlow: "Barlow" - }; - const loadedSubtitleGoogleFonts = new Set(); - const pendingSubtitleGoogleFonts = new Map(); - let 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(GOOGLE_SUBTITLE_FONT_PREFIX.length).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; - } - const familyQuery = googleFontFamily.trim().replaceAll(/\s+/g, "+"); - return `${GOOGLE_FONTS_CSS_API_URL}?family=${familyQuery}&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 (error2) { - } 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 rawText = await response.text(); - const sanitizedText = rawText.replace(/^\)\]\}'\n?/, ""); - const payload = JSON.parse(sanitizedText); - const fontFamilies = payload.familyMetadataList?.map((entry) => entry.family?.trim() ?? "").filter((familyName) => familyName.length > 0); - return Array.from(new Set(fontFamilies)).sort( - (left, right) => left.localeCompare(right) - ); - })().catch((error2) => { - googleFontsCatalogPromise = null; - return []; - }); - return await googleFontsCatalogPromise; - } - class FullscreenLayerController { - video; - container; - fullscreenLayer = null; - constructor({ video, container }) { - this.video = video; - this.container = container; - } - updateContainer(container) { - this.container = container; - } - getWidgetParentElement() { - return this.shouldUseFullscreenViewportLayer() ? this.ensureFullscreenLayer() : this.container; - } - getLayoutRootElement() { - return this.fullscreenLayer?.isConnected ? this.fullscreenLayer : this.container; - } - syncWidgetContainer(widgetContainer) { - const widgetParent = this.getWidgetParentElement(); - if (widgetParent === this.container && getComputedStyle(this.container).position === "static") { - this.container.style.position = "relative"; - } - if (widgetContainer && widgetContainer.parentElement !== widgetParent) { - widgetParent.appendChild(widgetContainer); - } - if (widgetParent === this.container && this.fullscreenLayer?.parentElement) { - this.fullscreenLayer.remove(); - this.fullscreenLayer = null; - } - } - release() { - if (!this.fullscreenLayer) return; - this.fullscreenLayer.remove(); - this.fullscreenLayer = null; - } - getActiveFullscreenElement() { - const doc = document; - const fullscreenEl = doc.fullscreenElement ?? doc.webkitFullscreenElement; - return resolveScopedFullscreenElement( - fullscreenEl, - [this.container, this.video], - { - allowDocumentViewport: true - } - ); - } - isCurrentVideoInFullscreenSession() { - const fullscreenEl = this.getActiveFullscreenElement(); - if (!fullscreenEl) return false; - if (fullscreenEl === this.container || fullscreenEl.contains(this.container) || this.container.contains(fullscreenEl)) { - return true; - } - return Boolean( - this.video && (fullscreenEl === this.video || fullscreenEl.contains(this.video) || this.video.contains(fullscreenEl)) - ); - } - shouldUseFullscreenViewportLayer() { - return this.isCurrentVideoInFullscreenSession(); - } - ensureFullscreenLayer() { - if (!this.fullscreenLayer) { - const layer = document.createElement("vot-block"); - layer.classList.add("vot-subtitles-layer"); - this.fullscreenLayer = layer; - } - if (this.fullscreenLayer.parentElement !== this.container) { - this.container.appendChild(this.fullscreenLayer); - } - return this.fullscreenLayer; - } - } - function isTimeInLine(time, line) { - return time >= line.startMs && time < line.startMs + line.durationMs; - } - function findActiveSubtitleLineIndex(time, subtitlesList) { - let low = 0; - let high = subtitlesList.length - 1; - while (low <= high) { - const mid = low + high >> 1; - const line = subtitlesList[mid]; - if (time < line.startMs) { - high = mid - 1; - } else if (time >= line.startMs + line.durationMs) { - low = mid + 1; - } else { - return mid; - } - } - return -1; - } - 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 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 }; - } - const stylesEqual$1 = (left, right) => Boolean(left?.italic) === Boolean(right?.italic) && Boolean(left?.bold) === Boolean(right?.bold) && Boolean(left?.underline) === Boolean(right?.underline); - function buildSubtitleRenderPlan(tokens, renderEndTokenIndex, breakAfterTokenIndexSet) { - const plan = []; - let pendingPrefix = ""; - let pendingPrefixStyle; - for (let i2 = 0; i2 <= renderEndTokenIndex; ) { - const token = tokens[i2]; - const tokenText = token?.text ?? ""; - if (!tokenText) { - i2 += 1; - continue; - } - if (tokenText === "\n") { - if (pendingPrefix) { - plan.push({ - kind: "text", - text: pendingPrefix, - style: pendingPrefixStyle - }); - pendingPrefix = ""; - pendingPrefixStyle = void 0; - } - plan.push({ kind: "break" }); - i2 += 1; - continue; - } - if (token.isWordLike) { - let text = pendingPrefix + tokenText; - const style = pendingPrefix ? pendingPrefixStyle : token.style; - pendingPrefix = ""; - pendingPrefixStyle = void 0; - let endIndex = i2; - const hasBreakAfterWord = Boolean(breakAfterTokenIndexSet?.has(i2)); - let breakTokenIndex = hasBreakAfterWord ? i2 : null; - while (breakTokenIndex === null && endIndex + 1 <= renderEndTokenIndex) { - const next = tokens[endIndex + 1]; - if (!next || next.isWordLike || next.text === "\n" || !stylesEqual$1(next.style, style)) - break; - text += next.text; - endIndex += 1; - if (breakAfterTokenIndexSet?.has(endIndex)) { - breakTokenIndex = endIndex; - break; - } - } - if (breakTokenIndex !== null) { - plan.push( - { - kind: "word", - text, - style - }, - { kind: "break" } - ); - i2 = breakTokenIndex + 1; - while (i2 <= renderEndTokenIndex && !tokens[i2]?.isWordLike && !tokens[i2]?.text.trim()) { - i2 += 1; - } - continue; - } - plan.push({ - kind: "word", - text, - style - }); - i2 = endIndex + 1; - continue; - } - const hasBreakAfter = Boolean(breakAfterTokenIndexSet?.has(i2)); - const isWhitespaceOnly = tokenText.trim().length === 0; - if (!isWhitespaceOnly) { - if (hasBreakAfter) { - plan.push( - { - kind: "text", - text: pendingPrefix + tokenText, - style: pendingPrefix ? pendingPrefixStyle : token.style - }, - { kind: "break" } - ); - pendingPrefix = ""; - pendingPrefixStyle = void 0; - } else { - if (pendingPrefix && !stylesEqual$1(pendingPrefixStyle, token.style)) { - plan.push({ - kind: "text", - text: pendingPrefix, - style: pendingPrefixStyle - }); - pendingPrefix = ""; - } - pendingPrefix += tokenText; - pendingPrefixStyle = token.style; - } - i2 += 1; - continue; - } - if (pendingPrefix) { - plan.push({ - kind: "text", - text: pendingPrefix, - style: pendingPrefixStyle - }); - pendingPrefix = ""; - pendingPrefixStyle = void 0; - } - if (hasBreakAfter) { - plan.push( - { - kind: "text", - text: tokenText, - style: token.style - }, - { kind: "break" } - ); - } else { - plan.push({ - kind: "text", - text: tokenText, - style: token.style - }); - } - i2 += 1; - } - if (pendingPrefix) { - plan.push({ - kind: "text", - text: pendingPrefix, - style: pendingPrefixStyle - }); - } - return plan; - } - const EST_CHAR_WIDTH_RATIO = 0.55; - function clamp$1(value, min, max) { - if (Number.isNaN(value)) return min; - return Math.min(max, Math.max(min, value)); - } - function targetCharsPerLine(aspect) { - if (aspect < 1) return 28; - if (aspect < 1.4) return 32; - return 42; - } - function computeSmartLayoutForBox(anchorBox, cssMetrics) { - const w2 = Math.max(1, anchorBox.w); - const h2 = Math.max(1, anchorBox.h); - const aspect = w2 / h2; - let computedCPL = targetCharsPerLine(aspect); - if (cssMetrics) { - const { fontSizePx, maxWidthPx } = cssMetrics; - if (Number.isFinite(fontSizePx) && Number.isFinite(maxWidthPx) && fontSizePx > 0 && maxWidthPx > 0) { - const estCharW = fontSizePx * EST_CHAR_WIDTH_RATIO; - if (estCharW > 0) { - computedCPL = maxWidthPx / estCharW; - } - } - } - const maxLength = clamp$1(Math.round(computedCPL * 2), 50, 180); - return { maxLength }; - } - const isWordToken = (token) => Boolean(token?.isWordLike && token.text?.trim()); - const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); - const rangeSum = (prefix2, start, end) => { - if (end < start) return 0; - return prefix2[end + 1] - prefix2[start]; - }; - const sentenceEndingWordRegexp = /[.!?\u2026]+(?:["'`)\]}\u00BB\u201D\u2019]+)?$/u; - function buildWordSlices(tokens) { - const slices = []; - let key = ""; - for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) { - const token = tokens[tokenIndex]; - if (!isWordToken(token)) continue; - let textToNextWord = token.text; - let breakAfterTokenIndex = tokenIndex; - let breakTextLength = token.text.length; - let cursor = tokenIndex + 1; - let seenTrailingPunctuation = false; - let punctuationAttachmentClosed = false; - while (cursor < tokens.length) { - const nextToken = tokens[cursor]; - if (!nextToken) { - cursor += 1; - continue; - } - if (isWordToken(nextToken)) break; - if (nextToken.text === "\n") { - breakAfterTokenIndex = cursor; - break; - } - const tokenText = nextToken.text ?? ""; - textToNextWord += tokenText; - if (!tokenText.trim()) { - if (seenTrailingPunctuation) { - punctuationAttachmentClosed = true; - } - cursor += 1; - continue; - } - if (!punctuationAttachmentClosed) { - breakAfterTokenIndex = cursor; - seenTrailingPunctuation = true; - breakTextLength = textToNextWord.length; - } - cursor += 1; - } - const trailingGapAfterBreakText = textToNextWord.slice(breakTextLength); - slices.push({ - tokenIndex, - breakAfterTokenIndex, - textToNextWord, - trailingGapAfterBreakText - }); - if (key) key += ""; - key += `${textToNextWord}${trailingGapAfterBreakText}${breakAfterTokenIndex}`; - } - return { - slices, - key - }; - } - function measureWordSlices(slices, measure) { - const wordsCount = slices.length; - const widths = new Array(wordsCount); - const chars = new Array(wordsCount); - const trailingGapWidths = new Array(wordsCount); - const trailingGapChars = new Array(wordsCount); - const prefixWidths = new Array(wordsCount + 1); - const prefixChars = new Array(wordsCount + 1); - prefixWidths[0] = 0; - prefixChars[0] = 0; - for (let i2 = 0; i2 < wordsCount; i2 += 1) { - const textToNextWord = slices[i2].textToNextWord; - const trailingGapAfterBreakText = slices[i2].trailingGapAfterBreakText; - const width = measure(textToNextWord); - const charCount = textToNextWord.length; - const trailingCharCount = trailingGapAfterBreakText.length; - const trailingWidth = trailingCharCount ? measure(trailingGapAfterBreakText) : 0; - widths[i2] = width; - chars[i2] = charCount; - trailingGapWidths[i2] = trailingWidth; - trailingGapChars[i2] = trailingCharCount; - prefixWidths[i2 + 1] = prefixWidths[i2] + width; - prefixChars[i2 + 1] = prefixChars[i2] + charCount; - } - return { - widths, - chars, - trailingGapWidths, - trailingGapChars, - prefixWidths, - prefixChars - }; - } - const getWordRangeWidthUnsafe = (metrics, startWord, endWord) => { - const total = rangeSum(metrics.prefixWidths, startWord, endWord); - return total - (metrics.trailingGapWidths[endWord] ?? 0); - }; - function getWordRangeWidth(metrics, startWord, endWord) { - if (endWord < startWord) return 0; - if (!metrics.widths.length) return 0; - const start = clamp(startWord, 0, metrics.widths.length - 1); - const end = clamp(endWord, 0, metrics.widths.length - 1); - if (end < start) return 0; - return getWordRangeWidthUnsafe(metrics, start, end); - } - const getWordRangeCharsUnsafe = (metrics, startWord, endWord) => { - const total = rangeSum(metrics.prefixChars, startWord, endWord); - return total - (metrics.trailingGapChars[endWord] ?? 0); - }; - function fitsInTwoLines(metrics, startWord, endWord, maxWidth) { - if (endWord < startWord) return true; - if (maxWidth <= 0) return false; - if (!metrics.widths.length) return true; - const start = clamp(startWord, 0, metrics.widths.length - 1); - const end = clamp(endWord, 0, metrics.widths.length - 1); - if (end < start) return true; - const prefixWidths = metrics.prefixWidths; - const trailingGapWidths = metrics.trailingGapWidths; - const startPrefix = prefixWidths[start]; - const endPrefix = prefixWidths[end + 1]; - const endTrailingGapWidth = trailingGapWidths[end] ?? 0; - if (endPrefix - startPrefix - endTrailingGapWidth <= maxWidth) { - return true; - } - for (let k2 = start; k2 < end; k2 += 1) { - const splitPrefix = prefixWidths[k2 + 1]; - const top = splitPrefix - startPrefix - (trailingGapWidths[k2] ?? 0); - const bottom = endPrefix - splitPrefix - endTrailingGapWidth; - if (top <= maxWidth && bottom <= maxWidth) { - return true; - } - } - return false; - } - const scoreTwoLineCandidate = (w1, w2, maxWidth, count1, count2, wordsInRange) => { - const mean = (w1 + w2) / 2; - const slack1 = maxWidth - w1; - const slack2 = maxWidth - w2; - const slackCost = slack1 * slack1 + slack2 * slack2; - const delta1 = w1 - mean; - const delta2 = w2 - mean; - const variance = delta1 * delta1 + delta2 * delta2; - const canAvoidSingleton = wordsInRange >= 4; - const singletonPenalty = canAvoidSingleton && (count1 <= 1 || count2 <= 1) ? 1e9 : 0; - const canAvoidTwoWordOrphans = wordsInRange >= 6; - const twoWordOrphanPenalty = canAvoidTwoWordOrphans && (count1 <= 2 || count2 <= 2) ? 2e7 : 0; - let cost = slackCost + variance + singletonPenalty + twoWordOrphanPenalty; - if (w1 > w2) { - cost += (w1 - w2) / Math.max(1, maxWidth) * 0.15; - } - return cost; - }; - function computeBestTwoLineBreak(metrics, startWord, endWord, maxWidth) { - if (maxWidth <= 0) return null; - if (!metrics.widths.length) return null; - const start = clamp(startWord, 0, metrics.widths.length - 1); - const end = clamp(endWord, 0, metrics.widths.length - 1); - if (end <= start) return null; - const prefixWidths = metrics.prefixWidths; - const trailingGapWidths = metrics.trailingGapWidths; - const startPrefix = prefixWidths[start]; - const endPrefix = prefixWidths[end + 1]; - const endTrailingGapWidth = trailingGapWidths[end] ?? 0; - let bestBreak = null; - let bestCost = Number.POSITIVE_INFINITY; - const wordsInRange = end - start + 1; - for (let k2 = start; k2 < end; k2 += 1) { - const splitPrefix = prefixWidths[k2 + 1]; - const w1 = splitPrefix - startPrefix - (trailingGapWidths[k2] ?? 0); - const w2 = endPrefix - splitPrefix - endTrailingGapWidth; - if (w1 > maxWidth || w2 > maxWidth) continue; - const count1 = k2 - start + 1; - const count2 = end - k2; - const cost = scoreTwoLineCandidate( - w1, - w2, - maxWidth, - count1, - count2, - wordsInRange - ); - if (cost < bestCost) { - bestCost = cost; - bestBreak = k2; - } - } - return bestBreak; - } - function computeBalancedBreaks(metrics, maxWidth) { - const n2 = metrics.widths.length; - if (n2 <= 1 || maxWidth <= 0) return []; - if (getWordRangeWidthUnsafe(metrics, 0, n2 - 1) <= maxWidth) { - return []; - } - const breakIndex = computeBestTwoLineBreak(metrics, 0, n2 - 1, maxWidth); - return breakIndex === null ? [] : [breakIndex]; - } - function findLongestPrefixFittingTwoLines(metrics, maxWidth) { - const n2 = metrics.widths.length; - if (n2 <= 0 || maxWidth <= 0) return null; - let low = 0; - let high = n2 - 1; - let best = null; - while (low <= high) { - const middle = low + high >> 1; - if (fitsInTwoLines(metrics, 0, middle, maxWidth)) { - best = middle; - low = middle + 1; - } else { - high = middle - 1; - } - } - return best; - } - function resolveStrictTwoLineLayout(metrics, maxWidth) { - if (maxWidth <= 0) { - return { - breakAfterWordIndices: [], - truncateAfterWordIndex: null - }; - } - const n2 = metrics.widths.length; - if (n2 <= 1) { - return { - breakAfterWordIndices: [], - truncateAfterWordIndex: null - }; - } - const fullLineFits = getWordRangeWidthUnsafe(metrics, 0, n2 - 1) <= maxWidth; - if (fullLineFits) { - return { - breakAfterWordIndices: [], - truncateAfterWordIndex: null - }; - } - const balancedBreaks = computeBalancedBreaks(metrics, maxWidth); - if (balancedBreaks.length) { - return { - breakAfterWordIndices: balancedBreaks, - truncateAfterWordIndex: null - }; - } - const prefixEndWordRaw = findLongestPrefixFittingTwoLines(metrics, maxWidth); - const prefixEndWord = prefixEndWordRaw ?? 0; - if (prefixEndWord >= n2 - 1) { - return { - breakAfterWordIndices: [], - truncateAfterWordIndex: null - }; - } - const prefixBreak = computeBestTwoLineBreak( - metrics, - 0, - prefixEndWord, - maxWidth - ); - return { - breakAfterWordIndices: prefixBreak === null ? [] : [prefixBreak], - truncateAfterWordIndex: prefixEndWord - }; - } - const getRangeWordCount = (range) => range.endWord - range.startWord + 1; - const buildInitialWordRanges = (wordsCount, isRangeAllowed) => { - const wordRanges = []; - let startWord = 0; - while (startWord < wordsCount) { - let endWord = startWord; - while (endWord + 1 < wordsCount && isRangeAllowed(startWord, endWord + 1)) { - endWord += 1; - } - wordRanges.push({ startWord, endWord }); - startWord = endWord + 1; - } - return wordRanges; - }; - const tryBorrowFromPrevious = (wordRanges, index, isRangeAllowed) => { - if (index <= 0) return false; - const current = wordRanges[index]; - const previous = wordRanges[index - 1]; - if (!current || !previous) return false; - if (getRangeWordCount(previous) < 3) return false; - const movedWord = previous.endWord; - const nextPrevious = { - startWord: previous.startWord, - endWord: movedWord - 1 - }; - const nextCurrent = { - startWord: movedWord, - endWord: current.endWord - }; - if (!isRangeAllowed(nextPrevious.startWord, nextPrevious.endWord) || !isRangeAllowed(nextCurrent.startWord, nextCurrent.endWord)) { - return false; - } - previous.endWord = nextPrevious.endWord; - current.startWord = nextCurrent.startWord; - return true; - }; - const tryBorrowFromNext = (wordRanges, index, isRangeAllowed) => { - if (index >= wordRanges.length - 1) return false; - const current = wordRanges[index]; - const next = wordRanges[index + 1]; - if (!current || !next) return false; - if (getRangeWordCount(next) < 3) return false; - const movedWord = next.startWord; - const nextCurrent = { - startWord: current.startWord, - endWord: movedWord - }; - const nextNext = { - startWord: movedWord + 1, - endWord: next.endWord - }; - if (!isRangeAllowed(nextCurrent.startWord, nextCurrent.endWord) || !isRangeAllowed(nextNext.startWord, nextNext.endWord)) { - return false; - } - current.endWord = nextCurrent.endWord; - next.startWord = nextNext.startWord; - return true; - }; - const rebalanceSingletonRanges = (wordRanges, isRangeAllowed) => { - for (let i2 = wordRanges.length - 1; i2 >= 0; i2 -= 1) { - if (getRangeWordCount(wordRanges[i2]) !== 1) continue; - if (!tryBorrowFromPrevious(wordRanges, i2, isRangeAllowed)) { - tryBorrowFromNext(wordRanges, i2, isRangeAllowed); - } - } - }; - const getWordPayload = (tokens, words, wordIndex) => { - const word = words[wordIndex]; - if (!word) return ""; - let text = ""; - for (let tokenIndex = word.tokenIndex; tokenIndex <= word.breakAfterTokenIndex; tokenIndex += 1) { - text += tokens[tokenIndex]?.text ?? ""; - } - return text.trimEnd(); - }; - const applySentenceBoundarySplits = (wordRanges, tokens, words, isRangeAllowed) => { - for (let i2 = 0; i2 < wordRanges.length - 1; i2 += 1) { - const previous = wordRanges[i2]; - const next = wordRanges[i2 + 1]; - if (!previous || !next) continue; - let sentenceBoundaryWord = -1; - const minCandidate = Math.max(previous.startWord, previous.endWord - 2); - for (let candidate = previous.endWord - 1; candidate >= minCandidate; candidate -= 1) { - if (sentenceEndingWordRegexp.test(getWordPayload(tokens, words, candidate))) { - sentenceBoundaryWord = candidate; - break; - } - } - if (sentenceBoundaryWord < previous.startWord) continue; - const nextPreviousStartWord = previous.startWord; - const nextPreviousEndWord = sentenceBoundaryWord; - const nextPreviousWordCount = nextPreviousEndWord - nextPreviousStartWord + 1; - if (nextPreviousWordCount < 3) continue; - const nextStartWord = sentenceBoundaryWord + 1; - const nextEndWord = next.endWord; - if (!isRangeAllowed(nextPreviousStartWord, nextPreviousEndWord) || !isRangeAllowed(nextStartWord, nextEndWord)) { - continue; - } - previous.endWord = nextPreviousEndWord; - next.startWord = nextStartWord; - } - }; - const resolveSegmentStartToken = (tokens, words, startWordIndex) => { - const startWord = words[startWordIndex]; - if (!startWord) return 0; - const previousBreakAfterTokenIndex = startWordIndex > 0 ? words[startWordIndex - 1]?.breakAfterTokenIndex ?? startWord.tokenIndex - 1 : -1; - let startToken = clamp( - previousBreakAfterTokenIndex + 1, - 0, - startWord.tokenIndex - ); - while (startToken < startWord.tokenIndex && !tokens[startToken]?.text.trim()) { - startToken += 1; - } - return startToken; - }; - const mapWordRangesToTimedSegments = (wordRanges, words, tokens) => { - const segments = []; - for (const range of wordRanges) { - const startWord = words[range.startWord]; - const endWord = words[range.endWord]; - if (!startWord || !endWord) continue; - const startToken = resolveSegmentStartToken(tokens, words, range.startWord); - const endToken = endWord.breakAfterTokenIndex + 1; - const startMs = tokens[startWord.tokenIndex]?.startMs ?? tokens[startToken]?.startMs ?? 0; - const endTokenStartMs = tokens[endWord.tokenIndex]?.startMs ?? startMs; - const endTokenDurationMs = tokens[endWord.tokenIndex]?.durationMs ?? 0; - const nextWord = words[range.endWord + 1]; - const nextWordStartMs = nextWord ? tokens[nextWord.tokenIndex]?.startMs : void 0; - const endMs = nextWordStartMs ?? endTokenStartMs + endTokenDurationMs; - segments.push({ - startToken, - endToken, - startMs, - endMs - }); - } - return segments; - }; - function computeTwoLineSegments(tokens, words, metrics, maxWidthPx, maxChars) { - const wordsCount = words.length; - if (wordsCount === 0) return []; - const charLimit = Number.isFinite(maxChars) && maxChars > 0 ? maxChars : null; - const isRangeAllowed = (startWord, endWord) => { - if (endWord < startWord) return false; - if (charLimit !== null && getWordRangeCharsUnsafe(metrics, startWord, endWord) > charLimit) { - return false; - } - return fitsInTwoLines(metrics, startWord, endWord, maxWidthPx); - }; - const wordRanges = buildInitialWordRanges(wordsCount, isRangeAllowed); - rebalanceSingletonRanges(wordRanges, isRangeAllowed); - applySentenceBoundarySplits(wordRanges, tokens, words, isRangeAllowed); - return mapWordRangesToTimedSegments(wordRanges, words, tokens); - } - const WRAP_WIDTH_GUARD_PX = 8; - const WRAP_WIDTH_GUARD_RATIO = 0.97; - const 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; - const guarded = Math.min(byPixelGuard, byRatioGuard); - return Math.max(MIN_EFFECTIVE_WRAP_WIDTH_PX, guarded); - } - class SubtitlesWidget { - video; - container; - fullscreenLayerController; - tooltipLayoutRoot; - subtitlesContainer = null; - subtitlesBlock = null; - renderedTokenEls = []; - passedFlagsBuffer = []; - subtitles = null; - subtitleLang; - lastRenderKey = null; - lastActiveLineIndex = null; - highlightWords = false; - fontSize = 20; - fontSizeOverridden = false; - fontFamily = "default-sans"; - manualMaxLength = 300; - smartLayoutEnabled = true; - smartFontSizePx = 0; - smartMaxWidthPx = 0; - smartMaxLength = 0; - smartAnchorWidthPx = 0; - smartAnchorHeightPx = 0; - lastSmartLayoutKey = null; - lastSmartLayoutCheckTs = 0; - opacity = "0.2"; - maxLength = 300; - repositionPending = false; - positionRefreshPending = false; - updatePending = false; - lastUpdateRequestTs = 0; - updateMinIntervalMs = 100; - updateMinIntervalHighlightMs = 33; - useVideoFrameCallbacks; - videoFrameRequestId = null; - dragDocListenersAttached = false; - lastPositionRefreshTs = 0; - positionRefreshIntervalMs = 250; - subtitleMaxWidthPx = 0; - breakAfterTokenIndices = []; - breakAfterTokenIndexSet = null; - smartTruncateAfterTokenIndex = 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 - }; - 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: 0.1, - minPx: 56, - maxPx: 220, - gapPx: 10 - }, - fullscreen: { - ratio: 0.07, - minPx: 44, - maxPx: 140, - gapPx: 9 - } - }; - safeAreaProbeEl = null; - guidesLayer = null; - verticalGuide = null; - horizontalGuide = null; - onPointerDownBound; - onPointerUpBound; - onPointerMoveBound; - onTimeUpdateBound; - onPlaybackStateChangeBound; - onVisualViewportChangeBound; - constructor(video, container, intervalIdleChecker, tooltipLayoutRoot = void 0) { - this.video = video; - this.container = container; - this.fullscreenLayerController = new FullscreenLayerController({ - video, - container - }); - this.intervalIdleChecker = intervalIdleChecker; - this.tooltipLayoutRoot = tooltipLayoutRoot; - 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.onTimeUpdateBound = () => this.requestUpdate(); - this.onPlaybackStateChangeBound = () => this.handlePlaybackStateChange(); - this.onVisualViewportChangeBound = () => this.scheduleReposition(); - this.checkerUnsubscribe = this.intervalIdleChecker.subscribe(() => { - this.onCheckerTick(); - }); - this.bindEvents(); - } - updateMount({ - container, - tooltipLayoutRoot - }) { - const containerChanged = this.container !== container; - const tooltipRootChanged = this.tooltipLayoutRoot !== tooltipLayoutRoot; - this.container = container; - this.fullscreenLayerController.updateContainer(container); - this.tooltipLayoutRoot = tooltipLayoutRoot; - this.syncWidgetMount(); - if (containerChanged || tooltipRootChanged) { - this.tokenTooltip?.updateMount({ - parentElement: this.getTokenTooltipParentElement(), - layoutRoot: this.tooltipLayoutRoot ?? document.documentElement - }); - } - 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.smartTruncateAfterTokenIndex = null; - 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; - const intersects = videoRect.right > containerRect.left && videoRect.left < containerRect.right && videoRect.bottom > containerRect.top && videoRect.top < containerRect.bottom; - if (!intersects) return fallback; - const w2 = videoRect.width / layout.scaleX; - const h2 = videoRect.height / layout.scaleY; - if (!(w2 > 0 && h2 > 0)) return fallback; - const rawLeft = (videoRect.left - containerRect.left) / layout.scaleX; - const rawTop = (videoRect.top - containerRect.top) / layout.scaleY; - const maxLeft = layout.w - w2; - const maxTop = layout.h - h2; - const left = maxLeft >= 0 ? clampToRange(rawLeft, 0, maxLeft) : (layout.w - w2) / 2; - const top = maxTop >= 0 ? clampToRange(rawTop, 0, maxTop) : (layout.h - h2) / 2; - return { left, top, w: w2, h: h2 }; - } - 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) { - this.maxLength = this.manualMaxLength; - return null; - } - const cssMetrics = this.readSmartCssMetrics(); - const nextFontSizePx = cssMetrics?.fontSizePx ?? this.smartFontSizePx; - const nextMaxWidthPx = cssMetrics?.maxWidthPx ?? this.smartMaxWidthPx; - const next = computeSmartLayoutForBox(anchorBox, cssMetrics); - const nextKey = `${Math.round(nextFontSizePx)}|${Math.round( - nextMaxWidthPx - )}|${next.maxLength}`; - const fontChanged = Math.abs(nextFontSizePx - this.smartFontSizePx) > 0.5; - const widthChanged = Math.abs(nextMaxWidthPx - this.smartMaxWidthPx) > 0.5; - const lengthChanged = next.maxLength !== this.smartMaxLength; - if (nextKey !== this.lastSmartLayoutKey) { - this.lastSmartLayoutKey = nextKey; - this.smartFontSizePx = nextFontSizePx; - this.smartMaxWidthPx = nextMaxWidthPx; - this.smartMaxLength = next.maxLength; - } - if (lengthChanged) { - this.maxLength = next.maxLength; - this.resetRenderMemo(); - this.resetSegmentationMemo(); - } - if ((fontChanged || widthChanged) && this.lastWrapTokens) { - this.lastWrapKey = null; - this.scheduleWrapRecompute(); - this.resetSegmentationMemo(); - } - 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) - ); - void 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; - const layer = this.ensureGuidesLayer(); - if (!layer.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 widgetParent = this.fullscreenLayerController.getWidgetParentElement(); - const guidesLayer = this.ensureGuidesLayer(); - if (guidesLayer.parentElement !== widgetParent) { - widgetParent.appendChild(guidesLayer); - } - } - syncWidgetMount() { - this.fullscreenLayerController.syncWidgetContainer(this.subtitlesContainer); - this.syncGuideLayerMount(); - } - getTokenTooltipParentElement() { - const widgetParent = this.fullscreenLayerController.getWidgetParentElement(); - return widgetParent === this.container ? document.documentElement : widgetParent; - } - 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: true - }); - this.syncVisualStyleVars(); - this.insetCacheReady = false; - this.updateContainerRect(); - return container; - } - bindEvents() { - const { signal } = this.abortController; - const opts = { signal }; - this.video?.addEventListener("play", this.onPlaybackStateChangeBound, opts); - this.video?.addEventListener( - "pause", - this.onPlaybackStateChangeBound, - opts - ); - this.video?.addEventListener( - "seeking", - this.onPlaybackStateChangeBound, - opts - ); - this.video?.addEventListener( - "seeked", - this.onPlaybackStateChangeBound, - opts - ); - this.video?.addEventListener( - "ended", - this.onPlaybackStateChangeBound, - opts - ); - this.resizeObserver = new ResizeObserver(() => this.onResize()); - this.resizeObserver.observe(this.container); - 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(now2 = performance.now()) { - if (this.abortController.signal.aborted) return; - if (!this.subtitles) return; - const minInterval = this.getUpdateMinIntervalMs(); - if (now2 - this.lastUpdateRequestTs < minInterval) return; - this.lastUpdateRequestTs = now2; - this.updatePending = true; - this.intervalIdleChecker.requestImmediateTick(); - } - handlePlaybackStateChange() { - if (!this.subtitles) { - this.stopVideoFrameLoop(); - return; - } - this.scheduleReposition(); - this.requestUpdate(); - 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 = (now2, _metadata) => { - this.videoFrameRequestId = null; - if (this.abortController.signal.aborted) return; - const video = this.video; - if (!video || video.paused || video.ended) return; - if (!this.subtitles) return; - this.requestUpdate(now2); - 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.dragDocListenersAttached) return; - this.dragDocListenersAttached = true; - document.addEventListener("pointermove", this.onPointerMoveBound, { - passive: false - }); - document.addEventListener("pointerup", this.onPointerUpBound); - document.addEventListener("pointercancel", this.onPointerUpBound); - } - detachDragDocumentListeners() { - if (!this.dragDocListenersAttached) return; - this.dragDocListenersAttached = false; - document.removeEventListener("pointermove", this.onPointerMoveBound); - document.removeEventListener("pointerup", this.onPointerUpBound); - document.removeEventListener("pointercancel", this.onPointerUpBound); - } - 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 w2 = layoutRoot.clientWidth || rect.width; - const h2 = layoutRoot.clientHeight || rect.height; - const scaleX = rect.width && w2 ? rect.width / w2 : 1; - const scaleY = rect.height && h2 ? rect.height / h2 : 1; - return { w: w2, h: h2, rect, scaleX, scaleY }; - } - 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; - const h2 = this.safeAreaProbeEl.offsetHeight || 0; - return h2; - } - refreshInsetCache() { - const layoutRoot = this.fullscreenLayerController.getLayoutRootElement(); - this.safeAreaBottomInsetCachedPx = this.getSafeAreaBottomInsetPx(); - this.containerPaddingBottomCachedPx = Number.parseFloat(getComputedStyle(layoutRoot).paddingBottom || "0") || 0; - this.insetCacheReady = true; - } - isMobileViewport() { - if (typeof globalThis.matchMedia !== "function") return false; - return globalThis.matchMedia("(max-width: 900px) and (pointer: coarse)").matches; - } - getBottomInsetPreset() { - const doc = document; - const fullscreenEl = doc.fullscreenElement ?? doc.webkitFullscreenElement; - if (!(fullscreenEl instanceof Element)) { - return this.bottomInsetByMode.normal; - } - const { container, video } = this; - const fullscreenContainsContainer = fullscreenEl === container || fullscreenEl.contains(container) || container.contains(fullscreenEl); - if (fullscreenContainsContainer) { - 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()) { - const raw = anchorBoxH * preset.ratio; - return clampToRange(raw, 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; - const layout = this.getLayoutSize(); - const { rect: containerRect, w: w2, h: h2, scaleX, scaleY } = layout; - if (!w2 || !h2) 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(); - const captureEl = this.subtitlesBlock ?? (target instanceof Element ? target : null); - try { - captureEl?.setPointerCapture(event.pointerId); - } catch { - } - } - 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) { - const thresholdExceeded = hasDragThresholdBeenExceeded( - this.dragging.startClientX, - this.dragging.startClientY, - event.clientX, - event.clientY, - this.dragStartThresholdPx - ); - if (!thresholdExceeded) { - return; - } - this.dragging.active = true; - this.dragging.moved = true; - this.suppressTokenClicksUntil = performance.now() + 450; - this.releaseTooltip(); - } else { - this.dragging.moved = true; - } - event.preventDefault(); - event.stopPropagation(); - const layout = this.getLayoutSize(); - const { rect: containerRect, w: w2, h: h2, scaleX, scaleY } = layout; - if (!w2 || !h2) 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.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() { - const subtitlesContainer = this.subtitlesContainer; - if (!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; - const visualScale = Math.min(layout.scaleX || 1, layout.scaleY || 1); - const compensate = visualScale > 0 && visualScale < 0.999 ? Math.min(1 / visualScale, 3) : 1; - if (Math.abs(compensate - 1) < 1e-3) { - subtitlesContainer.style.removeProperty( - "--vot-subtitles-scale-compensation" - ); - } else { - subtitlesContainer.style.setProperty( - "--vot-subtitles-scale-compensation", - compensate.toFixed(3) - ); - } - const anchorWidthPx = Math.max(1, Math.round(anchorBox.w)); - const anchorHeightPx = Math.max(1, Math.round(anchorBox.h)); - const anchorDimsChanged = anchorWidthPx !== this.smartAnchorWidthPx || anchorHeightPx !== this.smartAnchorHeightPx; - if (anchorDimsChanged) { - 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(); - } - } - if (this.smartLayoutEnabled) this.ensureSmartLayout(anchorBox); - const elW = subtitlesContainer.offsetWidth; - const elH = subtitlesContainer.offsetHeight; - const bottomInset = this.getBottomInsetPx(layout, anchorBox); - let anchorX = this.position.left / 100 * anchorBox.w; - let anchorY = this.position.top / 100 * anchorBox.h; - if (this.positionPreset !== "custom") { - const presetPosition = this.resolvePresetAnchorPosition({ - preset: this.positionPreset, - anchorBox, - elementWidth: elW, - elementHeight: elH, - 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; - } - } - let leftPx = anchorX - elW / 2; - let topPx = anchorY - elH; - const maxLeftPx = anchorBox.w - elW; - const maxTopPx = anchorBox.h - bottomInset - elH; - if (maxLeftPx >= 0) { - leftPx = clampToRange(leftPx, 0, maxLeftPx); - } else { - leftPx = maxLeftPx / 2; - } - if (maxTopPx >= 0) { - topPx = clampToRange(topPx, 0, maxTopPx); - } else { - topPx = 0; - } - anchorX = leftPx + elW / 2; - anchorY = topPx + elH; - const containerAnchorX = anchorBox.left + anchorX; - const containerAnchorY = anchorBox.top + anchorY; - const leftPct = containerAnchorX / layout.w * 100; - const topPct = containerAnchorY / layout.h * 100; - if (this.lastAppliedLeftPct === null || Math.abs(leftPct - this.lastAppliedLeftPct) >= 0.01) { - subtitlesContainer.style.left = `${leftPct}%`; - this.lastAppliedLeftPct = leftPct; - } - if (this.lastAppliedTopPct === null || Math.abs(topPct - this.lastAppliedTopPct) >= 0.01) { - subtitlesContainer.style.top = `${topPct}%`; - this.lastAppliedTopPct = topPct; - } - this.tokenTooltip?.updatePos(); - } - 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; - } - 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 s2 = 0; - let e2 = tokens.length; - while (s2 < e2 && !tokens[s2]?.text.trim()) s2 += 1; - while (e2 > s2 && !tokens[e2 - 1]?.text.trim()) e2 -= 1; - if (s2 === 0 && e2 === tokens.length) return tokens; - return s2 >= e2 ? [] : tokens.slice(s2, e2); - } - 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 nextStartMs = rangeEnd < tokens.length ? tokens[rangeEnd]?.startMs : void 0; - const endMs = nextStartMs ?? 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 words = slices.map((slice) => ({ - tokenIndex: slice.tokenIndex, - breakAfterTokenIndex: slice.breakAfterTokenIndex - })); - const value = { - words, - wordSlices: slices, - normalizedWordsKey: key - }; - this.tokenPrecomputeMemo = { - tokens, - value - }; - return value; - } - getTokenLayoutInputs(ctx) { - const block = this.subtitlesBlock; - if (block) { - const cs = getComputedStyle(block); - const fontKey2 = `${cs.fontStyle} ${cs.fontVariant} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`; - ctx.font = fontKey2; - const cssMaxWidth = Number.parseFloat(cs.maxWidth); - const paddingLeft = Number.parseFloat(cs.paddingLeft) || 0; - const paddingRight = Number.parseFloat(cs.paddingRight) || 0; - const baseMaxWidth2 = Number.isFinite(cssMaxWidth) ? cssMaxWidth : this.subtitleMaxWidthPx || globalThis.innerWidth * 0.8; - if (Number.isFinite(baseMaxWidth2) && baseMaxWidth2 > 0) { - this.subtitleMaxWidthPx = baseMaxWidth2; - } - return { - fontKey: fontKey2, - maxWidthPx: Math.max(0, baseMaxWidth2 - paddingLeft - paddingRight) - }; - } - const remPx = Number.parseFloat(getComputedStyle(document.documentElement).fontSize) || 16; - const maxRem = 52; - const cssFallbackVw = 0.8; - const baseMaxWidth = Math.min( - remPx * maxRem, - this.subtitleMaxWidthPx || globalThis.innerWidth * cssFallbackVw - ); - const fontSizePx = this.fontSizeOverridden ? this.fontSize : Math.min(24, Math.max(14, globalThis.innerWidth * 0.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.lastActiveLineIndex !== null) { - return `${this.lastActiveLineIndex}`; - } - return `${tokens[0]?.startMs ?? 0}:${tokens[0]?.durationMs ?? 0}:${tokens.length}`; - } - getLineMeasureMemo(tokens, activeLineKey) { - const { words, wordSlices, normalizedWordsKey } = this.buildTokenPrecomputeInput(tokens); - if (!words.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 metrics = measureWordSlices( - wordSlices, - (text) => ctx.measureText(text).width - ); - const memo = { - key, - words, - metrics, - 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 segmentRanges = computeTwoLineSegments( - tokens, - lineMeasure.words, - lineMeasure.metrics, - safeMaxWidthPx, - this.maxLength - ); - const memo = { - key: memoKey, - segmentRanges - }; - 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( - (s2) => time >= s2.startMs && time < s2.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 translated2 = await translate(text, fromLang, toLang); - return [ - this.strTranslatedTokens, - typeof translated2 === "string" ? translated2 : "" - ]; - } - 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]; - } - isTokenSpanElement(el) { - return el instanceof HTMLSpanElement && el.dataset.votToken === "1"; - } - findTokenSpanInPath(path, root) { - for (const node of path) { - if (this.isTokenSpanElement(node) && root.contains(node)) { - return node; - } - } - return null; - } - findTokenSpanByPoint(x2, y2, root) { - const hit = document.elementFromPoint(x2, y2); - if (this.isTokenSpanElement(hit) && root.contains(hit)) { - return hit; - } - if (!(hit instanceof Element)) return null; - const closest = hit.closest('span[data-vot-token="1"]'); - if (closest instanceof HTMLSpanElement && root.contains(closest)) { - return closest; - } - return null; - } - resolveTokenSpanFromClick(event) { - const root = this.subtitlesBlock ?? this.subtitlesContainer; - if (!root) return null; - if (this.isTokenSpanElement(event.target) && root.contains(event.target)) { - return event.target; - } - const path = typeof event.composedPath === "function" ? event.composedPath() : []; - const fromPath = this.findTokenSpanInPath(path, root); - if (fromPath) { - return fromPath; - } - const x2 = event.clientX; - const y2 = event.clientY; - if (Number.isFinite(x2) && Number.isFinite(y2)) { - return this.findTokenSpanByPoint(x2, y2, root); - } - return null; - } - releaseTooltip() { - this.tooltipTranslationRequestId += 1; - if (this.tokenTooltip?.target) { - this.tokenTooltip.target.classList.remove("selected"); - } - this.tokenTooltip?.release(); - this.tokenTooltip = 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.lastActiveLineIndex = null; - this.strTokens = ""; - this.resetTranslationContext(); - this.subtitlesBlock = null; - this.renderedTokenEls = []; - 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.tokenTooltip?.target === target && this.tokenTooltip?.container) { - if (this.tokenTooltip.showed) target.classList.add("selected"); - else target.classList.remove("selected"); - 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 tooltipMaxWidth = Math.max( - this.subtitleMaxWidthPx, - this.subtitlesContainer?.offsetWidth ?? 0, - this.subtitlesBlock?.offsetWidth ?? 0, - Math.min(globalThis.innerWidth * 0.6, 320) - ); - const tooltip = new Tooltip({ - target, - anchor: this.subtitlesBlock ?? target, - layoutRoot: this.tooltipLayoutRoot, - content: subtitlesInfo.container, - parentElement: this.getTokenTooltipParentElement(), - offset: { x: 4, y: 12 }, - maxWidth: tooltipMaxWidth, - borderRadius: 12, - bordered: false, - position: "top", - trigger: "click" - }); - this.tokenTooltip = tooltip; - tooltip.onClick(); - const strTokens = this.strTokens; - const translated = await this.translateStrTokens(text); - if (requestId !== this.tooltipTranslationRequestId) return; - if (strTokens !== this.strTokens || this.tokenTooltip !== tooltip || tooltip.target !== target || !tooltip.showed) - return; - subtitlesInfo.header.textContent = translated[1]; - subtitlesInfo.context.textContent = translated[0]; - tooltip.setContent(subtitlesInfo.container); - }; - 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 i2 = 0; i2 < thresholds.length; i2 += 1) { - flags[i2] = time > thresholds[i2]; - } - flags.length = thresholds.length; - return flags; - } - renderTokens(tokens) { - const breakAfter = this.breakAfterTokenIndexSet; - const out = []; - const plan = buildSubtitleRenderPlan(tokens, tokens.length - 1, breakAfter); - for (const part of plan) { - if (part.kind === "word") { - out.push( - b`${part.text}` - ); - continue; - } - if (part.kind === "break") { - out.push(b`
`); - continue; - } - if (part.style) { - out.push( - b`${part.text}` - ); - continue; - } - out.push(part.text); - } - return out; - } - updatePassedClasses(passedFlags) { - const tokenEls = this.renderedTokenEls; - const len = Math.min(tokenEls.length, passedFlags.length); - for (let i2 = 0; i2 < len; i2 += 1) { - tokenEls[i2].classList.toggle("passed", passedFlags[i2]); - } - for (let i2 = len; i2 < tokenEls.length; i2 += 1) { - tokenEls[i2].classList.remove("passed"); - } - } - clearPassedClasses() { - for (const tokenEl of this.renderedTokenEls) { - 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 now2 = performance.now(); - if (!force && now2 - this.lastPositionRefreshTs < this.positionRefreshIntervalMs) - return; - this.lastPositionRefreshTs = now2; - 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; - } - arraysEqual(a2, b2) { - if (a2 === b2) return true; - if (a2.length !== b2.length) return false; - for (let i2 = 0; i2 < a2.length; i2 += 1) { - if (a2[i2] !== b2[i2]) return false; - } - return true; - } - recomputeWrapNow() { - const tokens = this.lastWrapTokens; - const block = this.subtitlesBlock; - if (!tokens || !block) return; - const lineMeasure = this.getLineMeasureMemo( - tokens, - this.getActiveLineKey(tokens) - ); - if (!lineMeasure || lineMeasure.maxWidthPx < 50) return; - const { words, metrics, maxWidthPx } = lineMeasure; - const safeMaxWidthPx = applyWrapWidthGuard(maxWidthPx); - if (words.length <= 1) { - if (this.breakAfterTokenIndices.length || this.smartTruncateAfterTokenIndex !== null) { - this.resetWrapMemo(); - this.resetRenderMemo(); - this.update(); - } - return; - } - const wrapKey = lineMeasure.key; - if (wrapKey === this.lastWrapKey) return; - this.lastWrapKey = wrapKey; - let nextBreakAfterTokens = []; - let nextSmartTruncateAfterTokenIndex = null; - const lineFitsOneLine = getWordRangeWidth(metrics, 0, words.length - 1) <= safeMaxWidthPx; - if (!lineFitsOneLine) { - const breakWordIndices = computeBalancedBreaks( - metrics, - safeMaxWidthPx - ); - if (breakWordIndices.length) { - nextBreakAfterTokens = breakWordIndices.map( - (wordIdx) => words[wordIdx].breakAfterTokenIndex - ); - } else if (this.smartLayoutEnabled) { - const strict = resolveStrictTwoLineLayout(metrics, safeMaxWidthPx); - nextBreakAfterTokens = strict.breakAfterWordIndices.map( - (wordIdx) => words[wordIdx].breakAfterTokenIndex - ); - if (strict.truncateAfterWordIndex !== null) { - nextSmartTruncateAfterTokenIndex = words[strict.truncateAfterWordIndex]?.breakAfterTokenIndex ?? null; - } - } - } - const breaksChanged = !this.arraysEqual( - nextBreakAfterTokens, - this.breakAfterTokenIndices - ); - const truncateChanged = nextSmartTruncateAfterTokenIndex !== this.smartTruncateAfterTokenIndex; - if (breaksChanged || truncateChanged) { - this.setBreakAfterTokenIndices(nextBreakAfterTokens); - this.smartTruncateAfterTokenIndex = nextSmartTruncateAfterTokenIndex; - this.resetRenderMemo(); - this.update(); - } - } - setContent(subtitles, lang2 = void 0) { - this.releaseTooltip(); - this.subtitleLang = lang2; - if (!subtitles || !this.video) { - this.clearRenderedContent(); - this.subtitles = null; - this.clearPendingSchedulerState(); - this.video?.removeEventListener("timeupdate", this.onTimeUpdateBound); - this.stopVideoFrameLoop(); - this.detachDragDocumentListeners(); - return; - } - this.createSubtitlesContainer(); - this.subtitles = subtitles; - this.lastActiveLineIndex = null; - if (!this.useVideoFrameCallbacks) { - this.video.addEventListener("timeupdate", this.onTimeUpdateBound, { - signal: this.abortController.signal - }); - } - this.syncVideoFrameLoop(); - this.updateContainerRect(); - this.update(); - this.intervalIdleChecker.requestImmediateTick(); - } - setMaxLength(len) { - if (typeof len === "number" && len > 0) { - this.manualMaxLength = len; - if (!this.smartLayoutEnabled) { - 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(); - if (!this.smartLayoutEnabled) { - this.maxLength = this.manualMaxLength; - } - 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); - const clampedRate = Number.isFinite(numericRate) ? clampToRange(numericRate, 0, 100) : 0; - this.opacity = ((100 - clampedRate) / 100).toFixed(2); - this.applyOpacityStyle(); - } - stringifyTokens(tokens) { - let out = ""; - for (const token of tokens) { - out += token.text; - } - return out; - } - update() { - if (!this.video || !this.subtitles) return; - const time = this.video.currentTime * 1e3; - const subtitlesList = this.subtitles.subtitles; - let line; - let lineIndex = -1; - const lastIndex = this.lastActiveLineIndex; - if (typeof lastIndex === "number" && lastIndex >= 0 && lastIndex < subtitlesList.length) { - const candidate = subtitlesList[lastIndex]; - if (isTimeInLine(time, candidate)) { - line = candidate; - lineIndex = lastIndex; - } - } - if (!line) { - const index = findActiveSubtitleLineIndex(time, subtitlesList); - if (index !== -1) { - line = subtitlesList[index]; - lineIndex = index; - } - } - if (!line) { - this.lastActiveLineIndex = null; - if (this.subtitlesBlock || this.lastRenderKey !== null || this.strTokens) { - this.clearRenderedContent({ releaseTooltip: true }); - } else { - this.releaseTooltip(); - } - return; - } - this.lastActiveLineIndex = lineIndex; - if (this.smartLayoutEnabled) { - const now2 = performance.now(); - if (this.lastSmartLayoutKey === null || now2 - this.lastSmartLayoutCheckTs > 500) { - this.lastSmartLayoutCheckTs = now2; - const layout = this.getLayoutSize(); - if (layout.w && layout.h) { - const anchorBox = this.computeAnchorBoxLayout(layout); - if (anchorBox.w && anchorBox.h) { - this.ensureSmartLayout(anchorBox); - } - } - } - } else { - this.maxLength = this.manualMaxLength; - } - const tokens = this.processTokens(line.tokens, time); - this.lastWrapTokens = tokens; - const hasSmartTruncation = this.smartLayoutEnabled && typeof this.smartTruncateAfterTokenIndex === "number" && this.smartTruncateAfterTokenIndex >= 0 && this.smartTruncateAfterTokenIndex < tokens.length - 1; - const strTokens = this.stringifyTokens(tokens); - const tokensChanged = strTokens !== this.strTokens; - if (tokensChanged) { - this.releaseTooltip(); - this.strTokens = strTokens; - this.resetTranslationContext(); - this.resetWrapMemo(); - } - const passedStateKey = `${lineIndex}:${strTokens}`; - const passedFlags = this.highlightWords ? this.buildPassedState(tokens, time, passedStateKey) : null; - const wrapKey = `${this.breakAfterTokenIndices.join(",")}|${this.smartTruncateAfterTokenIndex ?? ""}`; - const renderKey = `${lineIndex}:${strTokens}:${wrapKey}`; - if (renderKey === this.lastRenderKey) { - if (this.highlightWords && !tokensChanged && passedFlags) { - this.updatePassedClasses(passedFlags); - } - this.maybeRefreshPosition(); - return; - } - this.lastRenderKey = renderKey; - this.subtitlesContainer = this.subtitlesContainer ?? this.createSubtitlesContainer(); - const subtitlesClass = hasSmartTruncation ? "vot-subtitles vot-subtitles--clamped" : "vot-subtitles"; - D( - b` 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: proxyMedia(videoUrl), + duration, + title + }; + } + async getVideoId(url) { + const courseId = /course\/([^/]+)/.exec(url.pathname)?.[0]; + return courseId ? courseId + url.search : void 0; + } + }; + //#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) { + if (window.self !== window.top) return await this.resolveVideoIdViaPostMessage(); + else return this.getVideoIdFromUrl(url); + } + }; + //#endregion + //#region node_modules/@vot.js/ext/dist/helpers/datacamp.js + var DataCampHelper = class extends VideoJSHelper { + SUBTITLE_SOURCE = "datacamp"; + getVideoDataFromInput() { + try { + const input = 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) { + const data = this.getVideoDataByPlayer(videoId); + if (!data) 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 ?? data.url; + 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; + 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\/(?\d+)\/(?[^/?#]+)\/?$/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 `https:${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("//") ? `https:${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) { + try { + const content = await (await this.fetch(`${this.API_ORIGIN}/${videoId}`)).text(); + const url = /"contentUrl":(\s)?"([^"]+)"/.exec(content)?.[2]; + if (!url) throw new VideoHelperError("Odysee url doesn't parsed"); + return { url }; + } catch (err) { + Logger.error(`Failed to get odysee video data by video ID: ${videoId}`, err.message); + return; + } + } + 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 getContentUrl(_videoId) { + if (this.service?.additionalData !== "old") { + const player = document.querySelector("shreddit-player-2, shreddit-player"); + return (player?.getAttribute("src") ?? player?.querySelector("source[type=\"application/vnd.apple.mpegURL\"]")?.getAttribute("src"))?.replaceAll("&", "&"); + } + return document.querySelector("[data-hls-url]")?.dataset.hlsUrl?.replaceAll("&", "&"); + } + async getVideoData(videoId) { + try { + const contentUrl = await this.getContentUrl(videoId); + if (!contentUrl) throw new VideoHelperError("Failed to find content url"); + return { url: decodeURIComponent(contentUrl) }; + } 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/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; + try { + return Videoview?.getPlayerObject?.(); + } catch { + return; + } + } + async getVideoData(videoId) { + const currentUrl = new URL(window.location.href); + const player = VKHelper.getPlayer(); + if (!player) { + const base = this.returnBaseData(videoId); + return base ? { + ...base, + url: buildVkVideoUrl$1(videoId, currentUrl) + } : base; + } + 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: buildVkVideoUrl$1(videoId, currentUrl), + title, + description, + duration, + subtitles + }; + } catch (err) { + Logger.error(`Failed to get VK video data, because: ${err.message}`); + const base = this.returnBaseData(videoId); + return base ? { + ...base, + url: buildVkVideoUrl$1(videoId, currentUrl) + } : base; + } + } + async getVideoId(url) { + const pathID = /^\/((?:video|clip)-?\d+_\d+)(?:\/)?$/.exec(url.pathname); + if (pathID) return pathID[1]; + const idInsidePlaylist = /\/playlist\/[^/]+\/(video-?\d+_\d+)/.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) { + const ownerId = Math.abs(Number.parseInt(paramOID, 10)); + if (!Number.isNaN(ownerId)) return `video-${ownerId}_${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 + }; + 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 buildVkVideoUrl(videoId, sourceUrl) { + const cleanedVideoId = videoId.replace(/^\/+/, ""); + const out = new URL("https://vk.com/video"); + out.searchParams.set("z", cleanedVideoId); + for (const key of ["list", "access_key"]) { + const value = sourceUrl.searchParams.get(key); + if (value) out.searchParams.set(key, value); + } + return out.toString(); + } + 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) { + if (service.host === VideoService$1.vk) return { + url: buildVkVideoUrl(videoId, currentUrl), + videoId, + host: service.host, + duration: void 0 + }; + 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: service.host === VideoService$1.vk ? buildVkVideoUrl(videoId, currentUrl) : 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" + ]; + function initAudioContext() { + 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; + _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); + } + 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; + } + } + 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.audio.currentTime = this.chaimu.video.currentTime; + this.audio.playbackRate = this.chaimu.video.playbackRate; + if (!mode) return this; + switch (mode) { + case "play": + case "playing": + case "seeked": + this.isBuffering = false; + if (!this.chaimu.video.paused) this.syncPlay(); + return this; + case "waiting": + case "stalled": + case "seeking": + this.isBuffering = true; + this.audio.pause(); + return this; + case "pause": + case "ended": + this.isBuffering = false; + this.pause(); + return this; + default: return this; + } + } + async clear() { + this.cancelSuspend(); + this.audio.pause(); + this.audio.src = ""; + this.audio.removeAttribute("src"); + 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.isBuffering = false; + 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.isBuffering = false; + if (!this.chaimu.video.paused) this.start(); + return this; + case "waiting": + case "stalled": + case "seeking": + this.isBuffering = true; + if (this.audioElement) this.audioElement.pause(); + return this; + case "pause": + case "ended": + 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.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; + } + 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; + } + 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.isBuffering = false; + 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 = 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-new.toil-dump.workers.dev"; + var proxyWorkerHost = "vot-worker.kload.workers.dev"; + var votBackendUrl = "https://vot.toil.cc/v1"; + /** + * @see https://github.com/FOSWLY/translate-backend + */ + var foswlyTranslateUrl = "https://translate-backend.transly.workers.dev/v2"; + var detectRustServerUrl = "https://rust-server-531j.onrender.com/detect"; + var authServerUrl = "https://rust-server-531j.onrender.com"; + 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/localization.ts + function getNavigatorLang() { + 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(); + 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) return await shareBlob(blob, filename) === "shared"; + 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 Math.min(Math.max(value, Math.min(min, max)), Math.max(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$1(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$1(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); + } + }; + } + //#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) && !!scriptHandler && !hasSupportedGmXhr(); + 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") return maybeError.error; + if (typeof maybeError?.statusText === "string") return maybeError.statusText; + return getErrorMessage(error) || "Unknown error"; + } + async function gmXhrFetch(urlStr, timeout, fetchOptions) { + const headers = getHeaders(fetchOptions.headers); + const callbackGmXhr = getCallbackGmXhr(); + const promiseGmXhr = getPromiseGmXhr(); + if (callbackGmXhr) return await 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 request = callbackGmXhr({ + method: fetchOptions.method || "GET", + url: urlStr, + responseType: "blob", + data: fetchOptions.body, + timeout, + headers, + onload: (resp) => { + if (settled) return; + settled = true; + cleanupAbort(); + const responseHeaders = parseResponseHeaders(resp.responseHeaders); + const response = new Response(resp.response, { + status: resp.status, + statusText: typeof resp.statusText === "string" ? resp.statusText : "", + headers: responseHeaders + }); + Object.defineProperty(response, "url", { value: resp.finalUrl ?? urlStr }); + resolve(response); + }, + ontimeout: () => failOnce(/* @__PURE__ */ new Error("Timeout")), + onerror: (error) => failOnce(new Error(getGmXhrErrorMessage(error))), + onabort: () => 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; + } + } + }); + if (!promiseGmXhr) throw new TypeError("GM_xmlhttpRequest is not available"); + const request = promiseGmXhr({ + method: fetchOptions.method || "GET", + url: urlStr, + responseType: "blob", + data: fetchOptions.body, + timeout, + headers + }); + 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 resp = await Promise.race([request, abortPromise]); + const responseHeaders = parseResponseHeaders(resp.responseHeaders); + const response = new Response(resp.response, { + status: resp.status, + statusText: typeof resp.statusText === "string" ? resp.statusText : "", + headers: responseHeaders + }); + Object.defineProperty(response, "url", { value: resp.finalUrl ?? urlStr }); + return response; + } finally { + if (abortHandler) fetchOptions.signal?.removeEventListener("abort", abortHandler); + } + } + 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 performRequest = async () => { + if (shouldUseGmXhr(host, urlStr, forceGmXhr)) { + debug.log("GM_fetch: routing request via GM_xmlhttpRequest", { + host: host ?? "unknown", + reason: forceGmXhr ? "forced" : "host-policy", + url: urlStr + }); + return await gmXhrFetch(urlStr, timeout, fetchOptions); + } + const { signal, cleanup } = createTimeoutSignal(timeout, fetchOptions.signal); + try { + return await fetch(url, { + ...fetchOptions, + signal + }); + } catch (err) { + if (signal.aborted || isAbortError$1(err)) throw err; + debug.log("GM_fetch preventing CORS by GM_xmlhttpRequest", 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.4"), 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 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; + } + /** + * 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 isDocumentViewportElement(element) { + return element === document.body || element === document.documentElement; + } + function resolveScopedFullscreenElement(fullscreenEl, anchors, options = {}) { + const { allowDocumentViewport = false } = options; + if (!(fullscreenEl instanceof HTMLElement)) return null; + if (isDocumentViewportElement(fullscreenEl) && !allowDocumentViewport) return null; + for (const anchor of anchors) if (anchor && (containsCrossShadow(fullscreenEl, anchor) || containsCrossShadow(anchor, fullscreenEl))) return fullscreenEl; + return null; + } + 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 YtWatchContextForbiddenError = class extends Error { + status; + constructor(status = 403) { + super(`Failed to load watch page: ${status}`); + this.name = "YtWatchContextForbiddenError"; + this.status = status; + } + }; + var VIDEO_ID_PATTERN = /^[a-zA-Z0-9_-]{11}$/; + var YT_BASE = "https://www.youtube.com"; + var ANDROID_CLIENT_VERSION = "19.44.38"; + var ANDROID_VR_CLIENT_VERSION = "1.60.19"; + var IOS_CLIENT_VERSION = "19.45.4"; + var CLIENT_FALLBACK_ORDER = [ + "ANDROID_VR", + "ANDROID", + "IOS", + "WEB", + "MWEB" + ]; + var DEFAULT_HEADERS = { + accept: "*/*", + origin: YT_BASE, + referer: `${YT_BASE}/` + }; + var RANGE_FALLBACK_CHUNK_SIZE = 256 * 1024; + function withSignal(signal) { + return signal ? { signal } : {}; + } + function resolveInnertubeClient(requestedClient, watchContext, videoId) { + switch (requestedClient) { + case "ANDROID": + case "YTMUSIC_ANDROID": + case "YTSTUDIO_ANDROID": return { + clientName: "ANDROID", + clientVersion: ANDROID_CLIENT_VERSION, + hl: "en", + gl: "US", + androidSdkVersion: 34, + osName: "Android", + osVersion: "14", + platform: "MOBILE" + }; + case "ANDROID_VR": return { + clientName: "ANDROID_VR", + clientVersion: ANDROID_VR_CLIENT_VERSION, + hl: "en", + gl: "US", + androidSdkVersion: 31, + osName: "Android", + osVersion: "12", + platform: "MOBILE" + }; + case "IOS": return { + clientName: "IOS", + clientVersion: IOS_CLIENT_VERSION, + hl: "en", + gl: "US", + platform: "MOBILE", + osName: "iPhone", + osVersion: "18.0.0.22A3354", + deviceMake: "Apple", + deviceModel: "iPhone16,2" + }; + case "MWEB": return { + clientName: "MWEB", + clientVersion: watchContext.clientVersion, + hl: "en", + gl: "US", + originalUrl: `${YT_BASE}/watch?v=${videoId}` + }; + default: return { + clientName: "WEB", + clientVersion: watchContext.clientVersion, + hl: "en", + gl: "US", + utcOffsetMinutes: 0, + originalUrl: `${YT_BASE}/watch?v=${videoId}` + }; + } + } + 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 parseContentLengthFromContentRange(contentRange) { + if (!contentRange) return null; + return parsePositiveInteger(/\/(\d+)\s*$/i.exec(contentRange)?.[1]); + } + 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, ...CLIENT_FALLBACK_ORDER] : [...CLIENT_FALLBACK_ORDER]; + 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: { + ...DEFAULT_HEADERS, + 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; + } + async downloadStreamByRanges(streamUrl, contentLengthHint, signal) { + const fileSize = await this.resolveStreamContentLength(streamUrl, contentLengthHint, signal, true); + const merged = new Uint8Array(fileSize); + let offset = 0; + for (let start = 0; start < fileSize; start += RANGE_FALLBACK_CHUNK_SIZE) { + const end = Math.min(fileSize - 1, start + RANGE_FALLBACK_CHUNK_SIZE - 1); + const chunk = await this.fetchRangeChunk(streamUrl, start, end, signal); + if (offset + chunk.byteLength > merged.byteLength) throw new Error("Downloaded stream chunk exceeds probed stream content length"); + merged.set(chunk, offset); + offset += chunk.byteLength; + } + if (offset === merged.byteLength) return merged; + return merged.slice(0, offset); + } + async downloadAudioToChunkStream(request, options) { + if (options.chunkSize <= 0) throw new RangeError("Audio downloader. ytAudio. chunkSize must be > 0"); + return this.withResolvedPlayableAudioFormat(request, request.videoQuality ?? "best", "Chunk mode requires an adaptive audio stream format", "Unable to resolve streamable format for chunk mode", async ({ resolved, signal }) => { + const fileSize = await this.resolveStreamContentLength(resolved.streamUrl, resolved.chosenFormat.contentLength, signal, true); + const mediaPartsLength = Math.max(1, Math.ceil(fileSize / options.chunkSize)); + return { + videoId: resolved.videoId, + fileSize, + itag: resolved.chosenFormat.itag ?? 0, + mediaPartsLength, + getMediaBuffers: async function* () { + for (let index = 0; index < mediaPartsLength; index++) { + const start = index * options.chunkSize; + const end = Math.min(fileSize - 1, start + options.chunkSize - 1); + yield await this.fetchRangeChunk(resolved.streamUrl, start, end, signal); + } + }.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.videoQuality ?? "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, watchContext.clientVersion) + }; + } + async resolveStreamContentLength(streamUrl, contentLengthHint, signal, forceProbe = false) { + const hintedLength = parsePositiveInteger(contentLengthHint); + if (hintedLength !== null && !forceProbe) return hintedLength; + let probeResponse; + try { + probeResponse = await this.fetchFn(streamUrl, { + headers: { + ...DEFAULT_HEADERS, + range: "bytes=0-0" + }, + ...withSignal(signal) + }); + } catch (error) { + if (hintedLength !== null) return hintedLength; + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to probe stream content length: ${message}`); + } + if (!probeResponse.ok) { + if (hintedLength !== null) return hintedLength; + throw new Error(`Failed to probe stream content length: ${probeResponse.status}`); + } + const contentRangeLength = parseContentLengthFromContentRange(probeResponse.headers.get("content-range")); + const storedLength = parsePositiveInteger(probeResponse.headers.get("x-goog-stored-content-length")); + const contentLength = parsePositiveInteger(probeResponse.headers.get("content-length")); + if (typeof probeResponse.body?.cancel === "function") try { + await probeResponse.body.cancel(); + } catch {} + return contentRangeLength ?? storedLength ?? hintedLength ?? contentLength ?? (() => { + throw new Error("Failed to resolve stream content length"); + })(); + } + 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, clientVersion) { + if (!format.url) throw new Error("Selected format does not contain a direct stream URL"); + const streamUrl = new URL(format.url); + if (streamUrl.searchParams.get("c") === "WEB") streamUrl.searchParams.set("cver", clientVersion); + streamUrl.searchParams.set("cpn", makeCPN()); + return streamUrl.toString(); + } + async fetchWatchContext(videoId, signal) { + const watchUrl = `${YT_BASE}/watch?v=${encodeURIComponent(videoId)}&hl=en`; + const response = await this.fetchFn(watchUrl, { + headers: DEFAULT_HEADERS, + ...withSignal(signal) + }); + if (!response.ok) { + if (response.status === 403) throw new YtWatchContextForbiddenError(response.status); + 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, watchContext, videoId); + 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 = `${YT_BASE}/youtubei/v1/player?key=${encodeURIComponent(watchContext.apiKey)}`; + const response = await this.fetchFn(endpoint, { + method: "POST", + headers: { + ...DEFAULT_HEADERS, + "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 + }); + } + function isWatchContextForbiddenError(error) { + if (error instanceof YtWatchContextForbiddenError) return true; + return error instanceof Error && /failed to load watch page:\s*403/i.test(error.message); + } + 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, + videoQuality: 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) { + if (isWatchContextForbiddenError(error)) throw error; + console.warn("[VOT] ytAudio streaming mode failed, falling back to buffered mode", error); + } + const bytes = (await downloader.downloadAudioToUint8Array({ + videoId, + videoQuality: 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; + } + var VOTTranslationHandler = class { + 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; + }); + } + getVideoTranslationRetryDelayMs(retryAttempt, videoDurationSeconds) { + if (retryAttempt > 0) return 25e3; + return videoDurationSeconds <= 600 ? 6e4 : 75e3; + } + 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(videoData, `Translate video (requestLang: ${requestLang}, requestLangForApi: ${requestLangForApi}, responseLang: ${responseLang})`); + let livelyDisabled = disableLivelyVoice; + 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; + if (!res) throw new Error("Failed to get translation response"); + debug.log("Translate video result", res); + throwIfAborted(signal); + if (res.translated && res.remainingTime < 1) { + debug.log("Video translation finished with this data: ", res); + return { + ...res, + usedLivelyVoice: useLivelyVoice + }; + } + const message = res.message ?? localizationProvider.get("translationTakeFewMinutes"); + 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("Start audio download"); + this.downloading = true; + await this.audioDownloader.runAudioDownload(videoData.videoId, res.translationId, signal); + debug.log("waiting downloading finish"); + await this.waitForAudioDownloadCompletion(signal, 15e3); + return await this.translateVideoImpl(videoData, requestLang, responseLang, translationHelp, true, signal, { + disableLivelyVoice: livelyDisabled, + retryAttempt + }); + } + } catch (err) { + if (isAbortError$1(err)) { + debug.log("aborted video translation"); + return null; + } + const uiError = mapVotClientErrorForUi(err); + 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) + }); + console.error("[VOT]", err); + return null; + } + this.videoHandler.hadAsyncWait = true; + return this.scheduleRetry(() => this.translateVideoImpl(videoData, requestLang, responseLang, translationHelp, shouldSendFailedAudio, signal, { + disableLivelyVoice: livelyDisabled, + retryAttempt: retryAttempt + 1 + }), this.getVideoTranslationRetryDelayMs(retryAttempt, videoData.duration), signal); + } + async requestTranslationWithLivelyFallback({ videoData, requestLangForApi, responseLang, translationHelp, shouldSendFailedAudio, livelyDisabled, livelyVoiceAllowed }) { + let useLivelyVoice = !livelyDisabled && livelyVoiceAllowed && Boolean(this.videoHandler.data?.useLivelyVoice); + while (true) { + try { + 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)) return { + response, + useLivelyVoice, + livelyDisabled + }; + debug.log("[translateVideoImpl] Server responded that lively voices are unavailable. Falling back to standard translation.", response); + } catch (err) { + if (!useLivelyVoice || !this.isLivelyVoiceUnavailableError(err)) throw err; + debug.log("[translateVideoImpl] Lively voices are unavailable. Falling back to standard translation.", err); + } + livelyDisabled = true; + useLivelyVoice = false; + } + } + 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) { + self().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 clampNumber$2(value, min, max) { + if (!Number.isFinite(value)) return min; + if (max < min) return min; + return Math.max(min, Math.min(max, value)); + } + function clampInt(value, min, max) { + return Math.trunc(clampNumber$2(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$2(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$2(quantizeToStep(clampNumber$2(volume01, 0, 1), step, direction), 0, 1); + } + function snapVolume01Towards(next, current, desired, step = VIDEO_VOLUME_STEP_01) { + const cur = clampNumber$2(current, 0, 1); + const des = clampNumber$2(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 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); + } + 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) { + console.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) { + const snapped = snapVolume01(volume); + const shouldUnmute = snapped > 0; + if (!isExternalVolumeHost(this.videoHandler.site.host)) { + if (shouldUnmute) this.videoHandler.video.muted = false; + this.videoHandler.video.volume = snapped; + return this; + } + try { + const player = YoutubeHelper.getPlayer(); + const result = YoutubeHelper.setVolume(snapped); + if (shouldUnmute) { + player?.unMute?.(); + this.videoHandler.video.muted = false; + } + if (typeof result === "boolean" && result || typeof result === "number" && Number.isFinite(result)) return this; + } catch {} + if (shouldUnmute) this.videoHandler.video.muted = false; + this.videoHandler.video.volume = snapped; + return this; + } + /** + * Checks if the video is muted + */ + isMuted() { + return isExternalVolumeHost(this.videoHandler.site.host) ? YoutubeHelper.isMuted() : 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) { + console.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 ? "" : 3 === i ? "" : "", 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[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 ? "" : 3 === i ? "" : "")), 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/styles/main.scss?inline + var main_default = ".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:\"\";inherits:true;initial-value:.8}@property --vot-subtitles-scale-compensation{syntax:\"\";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}"; + //#endregion + //#region src/ui.ts + function injectMainStyles(css) { + const gmAddStyle = globalThis.GM_addStyle; + if (typeof gmAddStyle === "function") return gmAddStyle(css); + const style = document.createElement("style"); + style.textContent = css; + (document.head || document.documentElement).appendChild(style); + return style; + } + injectMainStyles(main_default); + 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); + el.addEventListener("keydown", (e) => { + if (el.getAttribute("disabled") === "true" || el.getAttribute("aria-disabled") === "true") return; + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + 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; + maxWidth; + backgroundColor; + borderRadius; + _bordered; + container; + onResizeObserver; + intersectionObserver; + scrollListening = false; + positionRafId = null; + destroyFallbackTimerId; + static DESTROY_FALLBACK_MS = 700; + tooltipId = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `vot-tooltip-${Math.random().toString(36).slice(2)}`; + 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(); + }; + 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; + } + 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); + 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, + left: coords.left + this.globalOffsetX + }; + } + 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(); + 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; + } + 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/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 { + video; + container; + fullscreenLayer = null; + constructor({ video, container }) { + this.video = video; + this.container = container; + } + updateContainer(container) { + this.container = container; + } + getWidgetParentElement() { + return this.shouldUseFullscreenViewportLayer() ? this.ensureFullscreenLayer() : this.container; + } + getLayoutRootElement() { + return this.fullscreenLayer?.isConnected ? this.fullscreenLayer : this.container; + } + syncWidgetContainer(widgetContainer) { + const widgetParent = this.getWidgetParentElement(); + if (widgetParent === this.container && getComputedStyle(this.container).position === "static") this.container.style.position = "relative"; + if (widgetContainer && widgetContainer.parentElement !== widgetParent) widgetParent.appendChild(widgetContainer); + if (widgetParent === this.container && this.fullscreenLayer?.parentElement) { + this.fullscreenLayer.remove(); + this.fullscreenLayer = null; + } + } + release() { + if (!this.fullscreenLayer) return; + this.fullscreenLayer.remove(); + this.fullscreenLayer = null; + } + getActiveFullscreenElement() { + const doc = document; + return resolveScopedFullscreenElement(doc.fullscreenElement ?? doc.webkitFullscreenElement, [this.container, this.video], { allowDocumentViewport: true }); + } + isCurrentVideoInFullscreenSession() { + const fullscreenEl = this.getActiveFullscreenElement(); + if (!fullscreenEl) return false; + if (fullscreenEl === this.container || fullscreenEl.contains(this.container) || this.container.contains(fullscreenEl)) return true; + return Boolean(this.video && (fullscreenEl === this.video || fullscreenEl.contains(this.video) || this.video.contains(fullscreenEl))); + } + shouldUseFullscreenViewportLayer() { + return this.isCurrentVideoInFullscreenSession(); + } + ensureFullscreenLayer() { + if (!this.fullscreenLayer) { + const layer = document.createElement("vot-block"); + layer.classList.add("vot-subtitles-layer"); + this.fullscreenLayer = layer; + } + if (this.fullscreenLayer.parentElement !== this.container) this.container.appendChild(this.fullscreenLayer); + return this.fullscreenLayer; + } + }; + //#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$1 = (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$1(height * fontHeightRatio, 16, 42); + const fontSizePx = cssMetrics?.fontSizePx ?? derivedFontSizePx; + const averageGlyphWidth = estimateAverageGlyphWidth(fontSizePx); + const minWidthPx = width * Math.min(.92, widthRatio); + const maxWidthPx = width * clampNumber$1(widthRatio * widthScale, .66, .92); + return { + fontSizePx, + maxWidthPx: roundToInt(clampNumber$1(clampNumber$1(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 DISCOURAGED_LINE_START_RE = /^\s*[\p{Pe}\p{Pf},.;:!?%‰…]/u; + var DISCOURAGED_LINE_END_RE = /\s*[\p{Ps}\p{Pi}¿¡([{«“"'`-]\s*$/u; + var normalizeTokenText = (text) => text.replaceAll(/\s+/gu, " ").trim(); + var resolveBoundary = (text) => { + if (STRONG_BREAK_RE.test(text)) return "strong"; + if (SOFT_BREAK_RE.test(text)) return "soft"; + return "neutral"; + }; + var startsWithDiscouragedLineStart = (text) => DISCOURAGED_LINE_START_RE.test(text); + var endsWithDiscouragedLineEnd = (text) => DISCOURAGED_LINE_END_RE.test(text); + 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) => { + 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 = tokens.slice(startToken, endToken).map((token) => token.text).join(""); + return { + text, + tokenIndex: wordTokenIndex, + breakAfterTokenIndex: endToken - 1, + startToken, + endToken, + charLength: normalizeTokenText(text).length, + startMs: getRangeStartMs(tokens, startToken, endToken), + endMs: getRangeEndMs(tokens, startToken, endToken), + boundary: resolveBoundary(text), + forcesLineBreak: false + }; + }; + function buildWordSlices(tokens) { + const slices = []; + const keyParts = []; + 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); + slices.push(slice); + keyParts.push(normalizeTokenText(slice.text)); + index = slice.breakAfterTokenIndex + 1; + } + if (!slices.length && tokens.length) { + const text = tokens.map((token) => token.text).join(""); + slices.push({ + text, + tokenIndex: 0, + breakAfterTokenIndex: tokens.length - 1, + startToken: 0, + endToken: tokens.length, + charLength: normalizeTokenText(text).length, + startMs: getRangeStartMs(tokens, 0, tokens.length), + endMs: getRangeEndMs(tokens, 0, tokens.length), + boundary: resolveBoundary(text), + forcesLineBreak: false + }); + keyParts.push(normalizeTokenText(text)); + } + return { + slices, + key: keyParts.join("|") + }; + } + 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, maxWidthPx); + const charBudget = Math.max(1, maxLength); + 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 = (tokens, startToken, endToken, measureText) => { + if (endToken <= startToken) return 0; + return measureText(tokens.slice(startToken, endToken).map((token) => token.text).join("")); + }; + 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, 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(tokens, 0, firstEndToken, measureText); + const secondWidth = measureTokenRange(tokens, secondStartToken, tokens.length, measureText); + const firstText = tokens.slice(0, firstEndToken).map((currentToken) => currentToken.text).join(""); + const secondText = tokens.slice(secondStartToken).map((currentToken) => currentToken.text).join(""); + const score = Math.max(0, firstWidth - maxWidthPx) * 12 + Math.max(0, secondWidth - maxWidthPx) * 12 + Math.abs(secondWidth - firstWidth) * .4 + (startsWithDiscouragedLineStart(secondText) ? 260 : 0) + (endsWithDiscouragedLineEnd(firstText) ? 70 : 0); + if (score < bestScore) { + bestScore = score; + bestBreakAfterTokenIndex = candidateBreakAfterTokenIndex; + } + } + return bestBreakAfterTokenIndex; + }; + var scoreBreakCandidate = ({ firstWidth, secondWidth, firstText, secondText, 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 lineStartPenalty = startsWithDiscouragedLineStart(secondText) ? 260 : 0; + const lineEndPenalty = endsWithDiscouragedLineEnd(firstText) ? 70 : 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: [] }; + if (tokens.reduce((count, token) => count + Number(token.text === "\n"), 0) > 0) return { breakAfterTokenIndices: [] }; + const { slices } = buildWordSlices(tokens); + const measurableSlices = slices.filter((slice) => !slice.forcesLineBreak); + if (!measurableSlices.length) return { breakAfterTokenIndices: [] }; + if (measureTokenRange(tokens, 0, tokens.length, measureText) <= maxWidthPx) 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(tokens, 0, firstEndToken, measureText), + secondWidth: measureTokenRange(tokens, secondStartToken, tokens.length, measureText), + firstText: tokens.slice(0, firstEndToken).map((token) => token.text).join(""), + secondText: tokens.slice(secondStartToken).map((token) => token.text).join(""), + firstWordCount: index + 1, + secondWordCount: measurableSlices.length - (index + 1), + maxWidthPx, + boundary: slice.boundary + }); + if (score < bestScore) { + bestScore = score; + bestBreakAfterTokenIndex = candidateBreakAfterTokenIndex; + } + } + if (bestBreakAfterTokenIndex !== null) return { breakAfterTokenIndices: [bestBreakAfterTokenIndex] }; + const fallbackBreakAfterTokenIndex = findFallbackBreakAfterTokenIndex(tokens, measureText, maxWidthPx); + 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; + tooltipLayoutRoot; + 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; + dragDocListenersAttached = false; + 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, tooltipLayoutRoot = void 0) { + this.video = video; + this.container = container; + this.fullscreenLayerController = new FullscreenLayerController({ + video, + container + }); + this.intervalIdleChecker = intervalIdleChecker; + this.tooltipLayoutRoot = tooltipLayoutRoot; + 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, tooltipLayoutRoot }) { + const containerChanged = this.container !== container; + const tooltipRootChanged = this.tooltipLayoutRoot !== tooltipLayoutRoot; + this.container = container; + this.fullscreenLayerController.updateContainer(container); + this.tooltipLayoutRoot = tooltipLayoutRoot; + this.syncWidgetMount(); + if (containerChanged || tooltipRootChanged) this.tokenTooltip?.updateMount({ + parentElement: this.getTokenTooltipParentElement(), + layoutRoot: this.tooltipLayoutRoot ?? document.documentElement + }); + 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 widgetParent = this.fullscreenLayerController.getWidgetParentElement(); + const guidesLayer = this.ensureGuidesLayer(); + if (guidesLayer.parentElement !== widgetParent) widgetParent.appendChild(guidesLayer); + } + syncWidgetMount() { + this.fullscreenLayerController.syncWidgetContainer(this.subtitlesContainer); + this.syncGuideLayerMount(); + } + getTokenTooltipParentElement() { + const widgetParent = this.fullscreenLayerController.getWidgetParentElement(); + return widgetParent === this.container ? document.documentElement : widgetParent; + } + 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: true + }); + this.syncVisualStyleVars(); + this.insetCacheReady = false; + this.updateContainerRect(); + return container; + } + bindEvents() { + const { signal } = this.abortController; + const opts = { signal }; + this.video?.addEventListener("play", this.onPlaybackStateChangeBound, opts); + this.video?.addEventListener("pause", this.onPlaybackStateChangeBound, opts); + this.video?.addEventListener("seeking", this.onPlaybackStateChangeBound, opts); + this.video?.addEventListener("seeked", this.onPlaybackStateChangeBound, opts); + this.video?.addEventListener("ended", this.onPlaybackStateChangeBound, opts); + this.resizeObserver = new ResizeObserver(() => this.onResize()); + this.resizeObserver.observe(this.container); + 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) return; + if (!this.subtitles) return; + const playbackTimeMs = typeof metadata.mediaTime === "number" && Number.isFinite(metadata.mediaTime) ? metadata.mediaTime * 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.dragDocListenersAttached) return; + this.dragDocListenersAttached = true; + document.addEventListener("pointermove", this.onPointerMoveBound, { passive: false }); + document.addEventListener("pointerup", this.onPointerUpBound); + document.addEventListener("pointercancel", this.onPointerUpBound); + } + detachDragDocumentListeners() { + if (!this.dragDocListenersAttached) return; + this.dragDocListenersAttached = false; + document.removeEventListener("pointermove", this.onPointerMoveBound); + document.removeEventListener("pointerup", this.onPointerUpBound); + document.removeEventListener("pointercancel", this.onPointerUpBound); + } + 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() { + if (typeof globalThis.matchMedia !== "function") return false; + return globalThis.matchMedia("(max-width: 900px) and (pointer: coarse)").matches; + } + 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; + 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(); + const captureEl = this.subtitlesBlock ?? (target instanceof Element ? target : null); + try { + captureEl?.setPointerCapture(event.pointerId); + } catch {} + } + 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(); + } 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]; + } + isTokenSpanElement(el) { + return el instanceof HTMLSpanElement && el.dataset.votToken === "1"; + } + findTokenSpanInPath(path, root) { + for (const node of path) if (this.isTokenSpanElement(node) && root.contains(node)) return node; + return null; + } + findTokenSpanByPoint(x, y, root) { + const hit = document.elementFromPoint(x, y); + if (this.isTokenSpanElement(hit) && root.contains(hit)) return hit; + if (!(hit instanceof Element)) return null; + const closest = hit.closest("span[data-vot-token=\"1\"]"); + if (closest instanceof HTMLSpanElement && root.contains(closest)) return closest; + return null; + } + resolveTokenSpanFromClick(event) { + const root = this.subtitlesBlock ?? this.subtitlesContainer; + if (!root) return null; + if (this.isTokenSpanElement(event.target) && root.contains(event.target)) return event.target; + const path = typeof event.composedPath === "function" ? event.composedPath() : []; + const fromPath = this.findTokenSpanInPath(path, root); + if (fromPath) return fromPath; + const x = event.clientX; + const y = event.clientY; + if (Number.isFinite(x) && Number.isFinite(y)) return this.findTokenSpanByPoint(x, y, root); + return null; + } + releaseTooltip() { + this.tooltipTranslationRequestId += 1; + if (this.tokenTooltip?.target) this.tokenTooltip.target.classList.remove("selected"); + this.tokenTooltip?.release(); + this.tokenTooltip = 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)); + return new Tooltip({ + target, + anchor: this.subtitlesBlock ?? target, + layoutRoot: this.tooltipLayoutRoot, + content, + parentElement: this.getTokenTooltipParentElement(), + 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`${text}`; + } + renderPlanPart(part) { + if (part.kind === "break") return b`
`; + 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; + } + arraysEqual(a, b) { + if (a === b) return true; + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false; + return true; + } + 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 (!this.arraysEqual(next.breakAfterTokenIndices, this.breakAfterTokenIndices)) { + 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) { + let out = ""; + for (const token of tokens) out += token.text; + return out; + } + 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` ${this.renderTokens(tokens)} - `, - this.subtitlesContainer - ); - const firstChild = this.subtitlesContainer.firstElementChild; - this.subtitlesBlock = firstChild instanceof HTMLElement && firstChild.classList.contains("vot-subtitles") ? firstChild : null; - this.renderedTokenEls = this.subtitlesBlock ? Array.from( - this.subtitlesBlock.querySelectorAll( - 'span[data-vot-token="1"]' - ) - ) : []; - 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; - } - 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; - } - } - const HTML_TAG_RE = /^<\s*(\/?)\s*([a-z0-9]+)(?:[.\s][^>]*)?>/iu; - const ASS_OVERRIDE_RE = /^\{([^}]*)\}/u; - const LEADING_SPEAKER_MARKER_RE = /^(\s*)>>\s*/u; - const toTokenStyle = (style) => { - const tokenStyle = {}; - if (style.italic) tokenStyle.italic = true; - if (style.bold) tokenStyle.bold = true; - if (style.underline) tokenStyle.underline = true; - return Object.keys(tokenStyle).length > 0 ? tokenStyle : void 0; - }; - const stylesEqual = (left, right) => Boolean(left?.italic) === Boolean(right?.italic) && Boolean(left?.bold) === Boolean(right?.bold) && Boolean(left?.underline) === Boolean(right?.underline); - const pushSegment = (segments, text, style) => { - if (!text) return; - const tokenStyle = toTokenStyle(style); - const previous = segments.at(-1); - if (previous && stylesEqual(previous.style, tokenStyle)) { - previous.text += text; - return; - } - segments.push({ - text, - style: tokenStyle - }); - }; - const applyAssStyleDirective = (directive, style) => { - const match = /^\\([ibu])([01])$/u.exec(directive.trim()); - if (!match) return; - const enabled = match[2] === "1"; - if (match[1] === "i") { - style.italic = enabled; - return; - } - if (match[1] === "b") { - style.bold = enabled; - return; - } - if (match[1] === "u") { - style.underline = enabled; - } - }; - const 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(); - } - }; - const buildStyledDisplayModel = (rawText) => { - const segments = []; - const activeStyle = { - italic: false, - bold: false, - underline: false - }; - let cursor = 0; - while (cursor < rawText.length) { - const remainder = rawText.slice(cursor); - if (remainder.startsWith("\\N") || remainder.startsWith("\\n")) { - pushSegment(segments, "\n", activeStyle); - cursor += 2; - continue; - } - if (remainder.startsWith("\\h")) { - pushSegment(segments, " ", activeStyle); - cursor += 2; - continue; - } - if (remainder[0] === "\n") { - pushSegment(segments, "\n", activeStyle); - cursor += 1; - continue; - } - const assMatch = ASS_OVERRIDE_RE.exec(remainder); - if (assMatch) { - const directives = assMatch[1].match(/\\[ibu][01]/gu) ?? []; - for (const directive of directives) { - applyAssStyleDirective(directive, activeStyle); - } - cursor += assMatch[0].length; - continue; - } - const htmlMatch = HTML_TAG_RE.exec(remainder); - if (htmlMatch) { - const isClosing = htmlMatch[1] === "/"; - const tagName = htmlMatch[2].toLowerCase(); - if (tagName === "br") { - pushSegment(segments, "\n", activeStyle); - } else if (tagName === "i") { - activeStyle.italic = !isClosing; - } else if (tagName === "b") { - activeStyle.bold = !isClosing; - } else if (tagName === "u") { - activeStyle.underline = !isClosing; - } - cursor += htmlMatch[0].length; - continue; - } - pushSegment(segments, remainder[0], activeStyle); - cursor += 1; - } - normalizeLeadingSpeakerMarker(segments); - const styledSpans = []; - let text = ""; - for (const segment of segments) { - if (!segment.text) continue; - const start = text.length; - text += segment.text; - const end = text.length; - styledSpans.push({ - start, - end, - style: segment.style - }); - } - const normalizedText = text.replaceAll(" ", " "); - 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); - const finalSpans = 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) - })); - return { - text: finalText, - styledSpans: finalSpans - }; - }; - const getStyleForRange = (styledSpans, start, end) => { - if (!styledSpans?.length || end <= start) { - return void 0; - } - const overlap = styledSpans.find( - (span) => start < span.end && end > span.start && span.style - ); - return overlap?.style; - }; - const BOM = "\uFEFF"; - const ASS_OVERRIDE_TAG_RE = /\{[^}]*\}/gu; - const SRT_TIMING_RE = /^\s*(?\d{1,2}:\d{2}:\d{2}[,.]\d{1,3})\s*-->\s*(?\d{1,2}:\d{2}:\d{2}[,.]\d{1,3})\s*$/u; - const VTT_TIMING_RE = /^(?(?:\d{2}:)?\d{2}:\d{2}\.\d{3})\s+-->\s+(?(?:\d{2}:)?\d{2}:\d{2}\.\d{3})(?(?:[ \t]+.+)?)$/u; - const normalizeNewlines = (value) => value.replace(BOM, "").replaceAll(/\r\n?/gu, "\n"); - const padMilliseconds = (value) => value.length >= 3 ? value.slice(0, 3) : value.padEnd(3, "0"); - const parseClockTime = (value, millisecondsLength) => { - const parts = value.trim().split(":"); - if (parts.length < 2 || parts.length > 3) { - return null; - } - const normalizedParts = parts.length === 2 ? ["00", ...parts] : parts; - const [hoursRaw, minutesRaw, secondsRaw] = normalizedParts; - 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; - }; - const formatClockTime = (totalMs, { - delimiter, - allowOptionalHours, - fractionDigits - }) => { - const safeMs = Math.max(0, Math.round(totalMs)); - const hours = Math.floor(safeMs / 36e5); - const minutes = Math.floor(safeMs % 36e5 / 6e4); - const seconds = Math.floor(safeMs % 6e4 / 1e3); - const milliseconds = safeMs % 1e3; - const fraction = milliseconds.toString().padStart(3, "0").slice(0, fractionDigits); - const hourPart = allowOptionalHours && hours === 0 ? "" : `${hours.toString().padStart(2, "0")}:`; - return `${hourPart}${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}${delimiter}${fraction}`; - }; - const formatAssTime = (totalMs) => { - const safeMs = Math.max(0, Math.round(totalMs)); - const hours = Math.floor(safeMs / 36e5); - const minutes = Math.floor(safeMs % 36e5 / 6e4); - const seconds = Math.floor(safeMs % 6e4 / 1e3); - const centiseconds = Math.floor(safeMs % 1e3 / 10); - return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}.${centiseconds.toString().padStart(2, "0")}`; - }; - const parseAssTime = (value) => { - const match = /^(?\d+):(?\d{2}):(?\d{2})\.(?\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; - }; - const normalizeSubtitleTextForDisplay = (value) => buildStyledDisplayModel(value).text; - const toComparableSubtitleOrder = (subtitles) => subtitles.sort((left, right) => { - const startDiff = left.line.startMs - right.line.startMs; - if (startDiff !== 0) return startDiff; - const endDiff = left.line.startMs + Math.max(0, left.line.durationMs) - (right.line.startMs + Math.max(0, right.line.durationMs)); - if (endDiff !== 0) return endDiff; - return left.index - right.index; - }).map(({ line }) => line); - const 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); - }; - const 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 - }; - }; - const extractVttVoice = (payload) => { - const match = /^\s*]+)?(?:\s+([^>]*?))?>/iu.exec(payload) ?? /^\s*]*?)>/iu.exec(payload); - const voice = match?.[1]?.trim(); - return voice ? voice : void 0; - }; - const toSrtDisplayText = (payload) => normalizeSubtitleTextForDisplay(payload); - const toVttDisplayText = (payload) => normalizeSubtitleTextForDisplay(payload); - const toAssDisplayText = (rawText) => normalizeSubtitleTextForDisplay(rawText); - const parseSrt = (text) => { - const normalized = normalizeNewlines(text); - const lines = normalized.split("\n"); - const cues = []; - const isSrtCueStart = (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); - }; - let cursor = 0; - let cueIndex = 0; - while (cursor < lines.length) { - while (cursor < lines.length && lines[cursor].trim() === "") { - cursor += 1; - } - if (cursor >= lines.length) break; - let timingLineIndex = cursor; - if (/^\d+$/u.test(lines[cursor].trim()) && SRT_TIMING_RE.test(lines[cursor + 1]?.trim() ?? "")) { - timingLineIndex = cursor + 1; - } - const timingMatch = SRT_TIMING_RE.exec( - lines[timingLineIndex]?.trim() ?? "" - ); - if (!timingMatch?.groups) { - cursor += 1; - continue; - } - const startMs = parseClockTime(timingMatch.groups.start, 3); - const endMs = parseClockTime(timingMatch.groups.end, 3); - if (startMs == null || endMs == null || endMs < startMs) { - cursor = timingLineIndex + 1; - continue; - } - cursor = timingLineIndex + 1; - const payloadLines = []; - while (cursor < lines.length) { - if (lines[cursor].trim() === "") { - const nextCursor = cursor + 1; - if (isSrtCueStart(nextCursor)) { - break; - } - cursor += 1; - continue; - } - if (payloadLines.length > 0 && isSrtCueStart(cursor)) { - break; - } - payloadLines.push(lines[cursor]); - cursor += 1; - } - const rawText = trimEmptyBoundaryLines(payloadLines).join("\n"); - const displayModel = buildStyledDisplayModel(rawText); - cues.push({ - index: cueIndex, - line: { - text: toSrtDisplayText(rawText), - startMs, - durationMs: Math.max(0, endMs - startMs), - speakerId: "0", - tokens: [], - metadata: { - rawText, - styledSpans: displayModel.styledSpans - } - } - }); - cueIndex += 1; - } - return { - format: "srt", - subtitles: toComparableSubtitleOrder(cues) - }; - }; - const 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; - }; - const serializeSrt = (processed) => processed.subtitles.map((line, index) => { - const rawText = resolveSerializedText(processed, line, "srt"); - const endMs = line.startMs + Math.max(0, line.durationMs); - 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"); - const pushWebVttBlock = (blocks, cueIndex, lines) => { - blocks.push({ - cueIndex, - lines - }); - }; - const parseVtt = (text) => { - const normalized = normalizeNewlines(text); - const lines = normalized.split("\n"); - const headerLine = lines[0] ?? ""; - if (!headerLine.startsWith("WEBVTT")) { - return { - format: "vtt", - subtitles: [], - metadata: { - vtt: { - headerText: "", - blocks: [] - } - } - }; - } - const metadata = { - headerText: headerLine.slice("WEBVTT".length).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 (lines[cursor].startsWith("NOTE") || lines[cursor] === "STYLE" || lines[cursor] === "REGION") { - const blockLines = []; - while (cursor < lines.length && lines[cursor].trim() !== "") { - blockLines.push(lines[cursor]); - cursor += 1; - } - pushWebVttBlock(metadata.blocks, cues.length, blockLines); - continue; - } - let cueId; - if (!VTT_TIMING_RE.test(lines[cursor] ?? "") && VTT_TIMING_RE.test(lines[cursor + 1] ?? "")) { - cueId = lines[cursor]; - cursor += 1; - } - const timingMatch = VTT_TIMING_RE.exec(lines[cursor] ?? ""); - if (!timingMatch?.groups) { - cursor += 1; - continue; - } - const startMs = parseClockTime(timingMatch.groups.start, 3); - const endMs = parseClockTime(timingMatch.groups.end, 3); - if (startMs == null || endMs == null || endMs < startMs) { - cursor += 1; - continue; - } - cursor += 1; - const payloadLines = []; - while (cursor < lines.length && lines[cursor].trim() !== "") { - payloadLines.push(lines[cursor]); - cursor += 1; - } - const rawText = payloadLines.join("\n"); - const displayModel = buildStyledDisplayModel(rawText); - const voice = extractVttVoice(rawText); - cues.push({ - index: cues.length, - line: { - text: toVttDisplayText(rawText), - startMs, - durationMs: Math.max(0, endMs - startMs), - speakerId: voice ?? "0", - tokens: [], - metadata: { - rawText, - styledSpans: displayModel.styledSpans, - vtt: { - cueId, - settings: parseCueSettings(timingMatch.groups.settings ?? ""), - voice, - rawPayload: payloadLines - } - } - } - }); - } - return { - format: "vtt", - subtitles: toComparableSubtitleOrder(cues), - metadata: { - vtt: metadata - } - }; - }; - const serializeVttTiming = (line) => { - const endMs = line.startMs + Math.max(0, line.durationMs); - const settings = line.metadata?.vtt?.settings?.raw; - return `${formatClockTime(line.startMs, { - delimiter: ".", - allowOptionalHours: true, - fractionDigits: 3 - })} --> ${formatClockTime(endMs, { - delimiter: ".", - allowOptionalHours: true, - fractionDigits: 3 - })}${settings ? ` ${settings}` : ""}`; - }; - const serializeVtt = (processed) => { - const metadata = processed.metadata?.vtt; - const sections = [ - `WEBVTT${metadata?.headerText ? ` ${metadata.headerText}` : ""}` - ]; - const blocksByIndex = 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"); - }; - const splitAssFields = (value, fieldCount) => { - if (fieldCount <= 1) { - return [value]; - } - const fields = []; - let cursor = 0; - for (let i2 = 0; i2 < fieldCount - 1; i2 += 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; - }; - const createEmptyAssMetadata = () => ({ - scriptInfoLines: [], - styleFormat: "", - styleLines: [], - eventFormat: "", - preEventLines: [], - commentLines: [] - }); - const 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: [] - }); - const parseAss = (text) => { - const normalized = normalizeNewlines(text); - const lines = normalized.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") { - if (line.startsWith("Format:")) { - metadata.styleFormat = line; - return; - } - if (line.startsWith("Style:")) { - metadata.styleLines.push(line); - return; - } - metadata.preEventLines.push(line); - return; - } - if (currentSection === "Events") { - if (line.startsWith("Format:")) { - metadata.eventFormat = line; - eventFields = line.slice("Format:".length).split(",").map((field) => field.trim()); - return; - } - if (!line.includes(":")) { - metadata.preEventLines.push(line); - return; - } - const separatorIndex = line.indexOf(":"); - const kind = line.slice(0, separatorIndex).trim(); - const value = line.slice(separatorIndex + 1).trim(); - if (!eventFields.length && metadata.eventFormat) { - eventFields = metadata.eventFormat.slice("Format:".length).split(",").map((field) => field.trim()); - } - const values = splitAssFields(value, eventFields.length); - const event = Object.fromEntries( - eventFields.map((field, fieldIndex) => [ - field, - values[fieldIndex] ?? "" - ]) - ); - if (kind === "Comment") { - metadata.commentLines.push(line); - return; - } - if (kind !== "Dialogue") { - metadata.preEventLines.push(line); - return; - } - const startMs = parseAssTime(event.Start ?? ""); - const endMs = parseAssTime(event.End ?? ""); - if (startMs == null || endMs == null || endMs < startMs) { - return; - } - 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) ?? [] - }; - cues.push({ - index, - line: { - text: toAssDisplayText(rawText), - startMs, - durationMs: Math.max(0, endMs - startMs), - speakerId: assMetadata.name || "0", - tokens: [], - metadata: { - rawText, - styledSpans: displayModel.styledSpans, - ass: assMetadata - } - } - }); - } - }); - return { - format: "ass", - subtitles: toComparableSubtitleOrder(cues), - metadata: { - ass: metadata - } - }; - }; - const serializeAssDialogue = (line) => { - const ass = line.metadata?.ass; - const endMs = line.startMs + Math.max(0, line.durationMs); - 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(""); - }; - const 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 - }; - }; - const serializeAss = (processed, options) => { - const sourceMetadata = processed.metadata?.ass; - const metadataBase = sourceMetadata && sourceMetadata.scriptInfoLines.length > 0 && sourceMetadata.styleFormat && sourceMetadata.styleLines.length > 0 && sourceMetadata.eventFormat ? sourceMetadata : createDefaultAssMetadata(options?.assTitle); - const metadata = withAssTitle(metadataBase, options?.assTitle); - const sections = [ - "[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") - } - }) - ) - ]; - return sections.join("\n").trim(); - }; - const parseSubtitleText = (text, format) => { - if (format === "srt") return parseSrt(text); - if (format === "vtt") return parseVtt(text); - return parseAss(text); - }; - const sortProcessedSubtitles = (processed) => ({ - ...processed, - subtitles: toComparableSubtitleOrder( - processed.subtitles.map((line, index) => ({ - index, - line - })) - ) - }); - const 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 - }; - }; - const 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); - }; - 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" }); - } - 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 needed = loaded + value.byteLength; - if (needed > out.length) { - const grown = new Uint8Array(Math.max(needed, out.length * 2)); - grown.set(out.subarray(0, loaded)); - out = grown; - } - out.set(value, loaded); - loaded = needed; - } else { - chunks.push(value); - loaded += value.byteLength; - } - if (total > 0) { - onProgress(clamp$2(Math.round(loaded / total * 100))); - } - } - if (out) { - return out.buffer.slice(0, loaded); - } - const merged = new Uint8Array(loaded); - let offset = 0; - for (const c2 of chunks) { - merged.set(c2, offset); - offset += c2.byteLength; - } - return merged.buffer; - } - async function downloadTranslation(res, filename, onProgress = () => { - }, saveOptions = {}) { - const blob = await buildTranslationBlob(res, filename, onProgress); - return await downloadBlob(blob, `${filename}.mp3`, saveOptions); - } - async function buildTranslationBlob(res, filename, onProgress = () => { - }) { - const arrayBuffer = await readResponseArrayBuffer(res, onProgress); - onProgress(100); - return addTitleId3Tag(arrayBuffer, filename); - } - function isSameOverlayMount(previous, next) { - return previous.root === next.root && previous.portalContainer === next.portalContainer && previous.subtitlesMountContainer === next.subtitlesMountContainer && previous.tooltipLayoutRoot === next.tooltipLayoutRoot; - } - function applyOverlayMountUpdate(previous, next, onChanged) { - if (isSameOverlayMount(previous, next)) { - return previous; - } - onChanged(next); - return next; - } - function didTooltipMountContextChange(previous, next) { - return previous.root !== next.root || previous.tooltipLayoutRoot !== next.tooltipLayoutRoot; - } - const TRANSLATE_ICON_SVG = w` +
`, 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; + } + 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 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 = segment.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[0] === "\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 buildStyledDisplayModel = (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*(?\d{1,2}:\d{2}:\d{2}[,.]\d{1,3})\s*-->\s*(?\d{1,2}:\d{2}:\d{2}[,.]\d{1,3})\s*$/u; + var VTT_TIMING_RE = /^(?(?:\d{2}:)?\d{2}:\d{2}\.\d{3})\s+-->\s+(?(?:\d{2}:)?\d{2}:\d{2}\.\d{3})(?(?:[ \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 = /^(?\d+):(?\d{2}):(?\d{2})\.(?\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.sort((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*]+)?(?:\s+([^>]*?))?>/iu.exec(payload) ?? /^\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/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 && previous.tooltipLayoutRoot === next.tooltipLayoutRoot; + } + /** + * 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; + } + /** + * Tooltip geometry must be refreshed when either the button/root is reparented + * or the layout root itself changes. + */ + function didTooltipMountContextChange(previous, next) { + return previous.root !== next.root || previous.tooltipLayoutRoot !== next.tooltipLayoutRoot; + } + //#endregion + //#region src/ui/translationCommands.ts + function isAbortError(error) { + return error instanceof Error && error.name === "AbortError"; + } + 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/ui/icons.ts + var TRANSLATE_ICON_SVG = w` `; - const PIP_ICON_SVG = w` + var PIP_ICON_SVG = w` `; - const MENU_ICON = w` + var MENU_ICON = w` `; - const DOWNLOAD_ICON = w` + var DOWNLOAD_ICON = w` `; - const SUBTITLES_ICON = w` + var SUBTITLES_ICON = w` `; - const SETTINGS_ICON = w` + var SETTINGS_ICON = w` `; - const CHEVRON_ICON = w` + var CHEVRON_ICON = w` `; - const ARROW_RIGHT_ICON = w` + var ARROW_RIGHT_ICON = w` `; - const CLOSE_ICON = w` + var CLOSE_ICON = w` `; - const WARNING_ICON = w` + var WARNING_ICON = w` `; - const HELP_ICON = w` + var HELP_ICON = w` `; - const REFRESH_ICON = w` + var REFRESH_ICON = w` `; - const KEY_ICON = w` + var KEY_ICON = w` `; - function addComponentEventListener(events, type, listener) { - events[type].addListener(listener); - } - function removeComponentEventListener(events, type, listener) { - events[type].removeListener(listener); - } - function setHiddenState(element, isHidden) { - element.hidden = isHidden; - } - function getHiddenState(element) { - return element.hidden; - } - class DownloadButton { - button; - loaderMain; - loaderCircle; - onClick = new EventImpl(); - events = { - click: this.onClick - }; - _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) { - addComponentEventListener(this.events, "click", listener); - return this; - } - removeEventListener(_type, listener) { - removeComponentEventListener(this.events, "click", 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) { - setHiddenState(this.button, isHidden); - } - get hidden() { - return getHiddenState(this.button); - } - } - function clampProgress(value) { - if (!Number.isFinite(value)) return 0; - const asPercent = value < 1 ? value * 100 : value; - return Math.max(0, Math.min(100, Math.round(asPercent))); - } - class Label { - 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) { - setHiddenState(this.container, isHidden); - } - get hidden() { - return getHiddenState(this.container); - } - } - class Dialog { - container; - backdrop; - box; - contentWrapper; - headerContainer; - titleContainer; - title; - closeButton; - bodyContainer; - footerContainer; - onClose = new EventImpl(); - events = { - close: this.onClose - }; -previouslyFocused = null; - keydownListener; -adaptiveAlignObserver; - adaptiveAlignRaf = null; - handleViewportChange = () => { - this.scheduleAdaptiveVerticalAlign(); - }; - titleId = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `vot-dialog-title-${Math.random().toString(36).slice(2)}`; - _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; - container.setAttribute("aria-hidden", container.hidden ? "true" : "false"); - container.toggleAttribute("inert", 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", (e2) => { - e2.stopPropagation(); - }); - return { - container, - backdrop, - box, - contentWrapper, - headerContainer, - titleContainer, - title, - closeButton, - bodyContainer, - footerContainer - }; - } - addEventListener(_type, listener) { - addComponentEventListener(this.events, "close", listener); - return this; - } - removeEventListener(_type, listener) { - removeComponentEventListener(this.events, "close", listener); - return this; - } - open() { - this.previouslyFocused ??= document.activeElement; - 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 * 0.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 * 0.6); - const shouldTop = currentlyTop ? contentHeightPx > exitTopThresholdPx : contentHeightPx >= enterTopThresholdPx; - if (shouldTop) { - 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() { - const selectors = [ - "button:not([disabled])", - "[href]", - "input:not([disabled])", - "select:not([disabled])", - "textarea:not([disabled])", - "[tabindex]:not([tabindex='-1'])", - "[role='button']:not([aria-disabled='true'])" - ]; - return Array.from( - this.container.querySelectorAll(selectors.join(",")) - ).filter((el) => !el.hidden && el.getClientRects().length > 0); - } - focusFirst() { - const focusables = this.getFocusableElements(); - (focusables[0] ?? this.closeButton ?? this.box).focus?.(); - } - attachKeydownTrap() { - if (this.keydownListener) return; - this.keydownListener = (e2) => { - if (e2.key === "Escape") { - e2.preventDefault(); - this.close(); - return; - } - if (e2.key !== "Tab") { - return; - } - const focusables = this.getFocusableElements(); - if (!focusables.length) { - e2.preventDefault(); - this.box.focus(); - return; - } - const first = focusables[0]; - const last = focusables.at(-1) ?? first; - const active = document.activeElement; - if (e2.shiftKey) { - if (active === first || active === this.box) { - e2.preventDefault(); - last.focus(); - } - } else if (active === last) { - e2.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) { - setHiddenState(this.container, isHidden); - this.container.setAttribute("aria-hidden", isHidden ? "true" : "false"); - this.container.toggleAttribute("inert", isHidden); - } - get hidden() { - return getHiddenState(this.container); - } - get isDialogOpen() { - return !this.container.hidden; - } - } - class Textfield { - 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) { - addComponentEventListener(this.events, type, listener); - return this; - } - removeEventListener(type, listener) { - removeComponentEventListener(this.events, type, listener); - return this; - } - get value() { - return this._value; - } -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) { - setHiddenState(this.container, isHidden); - } - get hidden() { - return getHiddenState(this.container); - } - } - class Select { - 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(langs2, conditionString) { - return langs2.map((lang2) => { - const phrase = `langs.${lang2}`; - const label = localizationProvider.get(phrase); - return { - label: label === phrase ? lang2.toUpperCase() : label, - value: lang2, - selected: conditionString === lang2 - }; - }); - } - 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) { - const searchableText = contentItem.dataset[this.contentItemSearchDatasetKey] ?? ""; - contentItem.hidden = !searchableText.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) { - addComponentEventListener(this.events, type, listener); - return this; - } - removeEventListener(type, listener) { - removeComponentEventListener(this.events, type, 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; - } -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) { - setHiddenState(this.container, isHidden); - } - get hidden() { - return getHiddenState(this.container); - } - get disabled() { - return this.outer.getAttribute("disabled") === "true" || this.outer.getAttribute("aria-disabled") === "true"; - } - set disabled(isDisabled) { - if (isDisabled) { - this.outer.setAttribute("disabled", "true"); - return; - } - this.outer.removeAttribute("disabled"); - } - } - class LanguagePairSelect { - 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; - } - } - class Slider { - 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; - } -set value(val) { - this._value = clampNumber(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(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(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) { - setHiddenState(this.container, isHidden); - } - get hidden() { - return getHiddenState(this.container); - } - } - function clampNumber(value, min, max) { - if (!Number.isFinite(value)) return min; - if (max < min) return min; - return Math.max(min, Math.min(max, value)); - } - class SliderLabel { - container; - strong; - text; - _labelText; - _labelEOL; - _value; - _symbol; - constructor({ - labelText, - labelEOL = "", - value = 50, - symbol: symbol2 = "%" - }) { - this._labelText = labelText; - this._labelEOL = labelEOL; - this._value = value; - this._symbol = symbol2; - 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) { - setHiddenState(this.container, isHidden); - } - get hidden() { - return getHiddenState(this.container); - } - } - class VOTButton { - container; - translateButton; - separator; - pipButton; - separator2; - menuButton; - label; - -_opacity = 1; - _position; - _direction; - _status; -_labelText; - constructor({ - position: position2 = "default", - direction = "default", - status = "none", - labelHtml = "" - }) { - this._position = position2; - 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(position2) { - return ["default", "top"].includes(position2) ? "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) { - setHiddenState(this.container, isHidden); - } - get hidden() { - return getHiddenState(this.container); - } - get position() { - return this._position; - } - set position(position2) { - this._position = this.container.dataset.position = position2; - } - get direction() { - return this._direction; - } - set direction(direction) { - this._direction = this.container.dataset.direction = direction; - } - set opacity(opacity) { - const o2 = Number.isFinite(opacity) ? opacity : 1; - this._opacity = o2; - const isHidden = o2 <= 0.01; - this.container.classList.toggle("vot-segmented-button--hidden", isHidden); - } - get opacity() { - return this._opacity; - } - } - class VOTMenu { - container; - contentWrapper; - headerContainer; - bodyContainer; - footerContainer; - titleContainer; - title; - _position; - _titleHtml; -menuId = typeof crypto !== "undefined" && "randomUUID" in crypto ? `vot-menu-${crypto.randomUUID()}` : `vot-menu-${Math.random().toString(36).slice(2)}`; - titleId = typeof crypto !== "undefined" && "randomUUID" in crypto ? `vot-menu-title-${crypto.randomUUID()}` : `vot-menu-title-${Math.random().toString(36).slice(2)}`; - constructor({ position: position2 = "default", titleHtml = "" }) { - this._position = position2; - 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"); - container.setAttribute("aria-hidden", "true"); - container.toggleAttribute("inert", 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) { - setHiddenState(this.container, isHidden); - this.container.setAttribute("aria-hidden", isHidden ? "true" : "false"); - this.container.toggleAttribute("inert", isHidden); - } - get hidden() { - return getHiddenState(this.container); - } - get position() { - return this._position; - } - set position(position2) { - this._position = this.container.dataset.position = position2; - } - } - class OverlayView { - static BIG_CONTAINER_WIDTH_PX = 550; - mount; - globalPortal; - abortController = null; - defaultVolumePersistTimer; - defaultVolumePersistDelayMs = 250; - dragging = false; - dragCandidate = false; - dragDirty = false; - dragStartX = 0; - dragStartY = 0; - currentClientX = 0; - dragThresholdPx = 6; - containerRect = null; - dragIsBigContainer = null; - checkerUnsubscribe = null; - initialized = false; - data; - videoHandler; - intervalIdleChecker; - 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; - } - get root() { - return this.mount.root; - } - get portalContainer() { - return this.mount.portalContainer; - } - get tooltipLayoutRoot() { - return this.mount.tooltipLayoutRoot; - } -updateMount(nextMount) { - const prevRoot = this.mount.root; - const nextRoot = nextMount.root; - const prevTooltipRoot = this.mount.tooltipLayoutRoot; - const nextTooltipRoot = nextMount.tooltipLayoutRoot; - this.mount = nextMount; - if (!this.isInitialized()) { - return this; - } - if (prevRoot !== nextRoot) { - if (this.votButton) { - nextRoot.appendChild(this.votButton.container); - } - if (this.votMenu) { - nextRoot.appendChild(this.votMenu.container); - } - } - if (this.votButtonTooltip && didTooltipMountContextChange( - { - root: prevRoot, - portalContainer: this.mount.portalContainer, - subtitlesMountContainer: this.mount.subtitlesMountContainer, - tooltipLayoutRoot: prevTooltipRoot - }, - nextMount - )) { - this.votButtonTooltip.updateMount({ - layoutRoot: nextTooltipRoot ?? document.documentElement - }); - } - return this; - } - isInitialized() { - return this.initialized; - } - calcButtonLayout(position2) { - if (this.isBigContainer && isSidePosition(position2)) { - return { - direction: "column", - position: position2 - }; - } - 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); - } - flushDefaultVolumePersist() { - if (this.defaultVolumePersistTimer !== void 0) { - globalThis.clearTimeout(this.defaultVolumePersistTimer); - this.defaultVolumePersistTimer = void 0; - } - if (typeof this.data.defaultVolume !== "number") { - return; - } - void votStorage.set("defaultVolume", this.data.defaultVolume); - } - initUI(buttonPosition2 = "default") { - if (this.isInitialized()) { - throw new Error("[VOT] OverlayView is already initialized"); - } - this.initialized = true; - const { position: position2, direction } = this.calcButtonLayout(buttonPosition2); - this.votButton = new VOTButton({ - position: position2, - 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.globalPortal, - layoutRoot: this.tooltipLayoutRoot - }); - this.votMenu = new VOTMenu({ - titleHtml: localizationProvider.get("VOTSettings"), - position: position2 - }); - this.root.appendChild(this.votMenu.container); - 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 ? maxAudioVolume : 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", - (e2) => { - e2.preventDefault(); - e2.stopPropagation(); - e2.stopImmediatePropagation(); - }, - { signal } - ); - const activateOnKey = (handler) => (e2) => { - if (e2.key === "Enter" || e2.key === " ") { - e2.preventDefault(); - handler(); - } - }; - const isPrimaryActionPointer = (event) => event.isPrimary && event.button === 0; - 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.votButton.translateButton.addEventListener( - "pointerdown", - (event) => { - if (!isPrimaryActionPointer(event)) return; - closeMenu(); - this.events["click:translate"].dispatch(); - }, - { signal } - ); - this.votButton.translateButton.addEventListener( - "keydown", - activateOnKey(() => { - closeMenu(); - this.events["click:translate"].dispatch(); - }), - { signal } - ); - this.votButton.pipButton.addEventListener( - "pointerdown", - (event) => { - if (!isPrimaryActionPointer(event)) return; - closeMenu(); - this.events["click:pip"].dispatch(); - }, - { signal } - ); - this.votButton.pipButton.addEventListener( - "keydown", - activateOnKey(() => { - closeMenu(); - this.events["click:pip"].dispatch(); - }), - { signal } - ); - this.votButton.menuButton.addEventListener( - "pointerdown", - (e2) => { - if (!isPrimaryActionPointer(e2)) return; - e2.preventDefault(); - toggleMenu(); - }, - { signal } - ); - this.votButton.menuButton.addEventListener( - "keydown", - activateOnKey(toggleMenu), - { signal } - ); - 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( - "touchstart", - this.onTouchDragStart, - { signal, passive: false } - ); - this.votMenu.container.addEventListener( - "click", - (e2) => { - e2.preventDefault(); - e2.stopPropagation(); - e2.stopImmediatePropagation(); - }, - { signal } - ); - for (const event of ["pointerdown", "mousedown"]) { - this.votMenu.container.addEventListener( - event, - (e2) => { - e2.stopImmediatePropagation(); - }, - { signal } - ); - } - document.addEventListener( - "pointerdown", - (e2) => { - if (this.votMenu.hidden) return; - const target = e2.target; - const path = typeof e2.composedPath === "function" ? e2.composedPath() : []; - const isInsideMenu = target && this.votMenu.container.contains(target) || path.includes(this.votMenu.container); - const isInsideToggle = target && this.votButton.menuButton.contains(target) || path.includes(this.votButton.menuButton); - const isInsideButton = target && this.votButton.container.contains(target) || path.includes(this.votButton.container); - const isInsideDialog = target instanceof HTMLElement && !!target.closest(".vot-dialog-container"); - if (isInsideMenu || isInsideToggle || isInsideButton || isInsideDialog) { - return; - } - closeMenu(false); - }, - { signal, capture: true, passive: true } - ); - this.votMenu.container.addEventListener( - "keydown", - (e2) => { - if (e2.key !== "Escape") return; - const keyboardNav = document.documentElement.classList.contains("vot-keyboard-nav"); - e2.preventDefault(); - e2.stopPropagation(); - closeMenu(keyboardNav); - const hovered = this.votButton.container.matches(":hover") || this.votMenu.container.matches(":hover"); - if (!hovered) { - 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 cacheKey = this.videoHandler.getSubtitlesCacheKey( - this.videoHandler.videoData.videoId, - this.videoHandler.videoData.detectedLanguage, - this.videoHandler.videoData.responseLanguage - ); - 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(position2, direction) { - if (!this.isInitialized()) { - return this; - } - this.votMenu.position = position2; - this.votButton.position = position2; - 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 position2 = VOTButton.calcPosition(percentX, isBigContainer); - if (position2 === this.votButton.position) { - return this; - } - const direction = VOTButton.calcDirection(position2); - this.data.buttonPos = position2; - this.updateButtonLayout(position2, 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.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) { - const dx = Math.abs(this.currentClientX - this.dragStartX); - const dy = Math.abs(clientY - this.dragStartY); - if (dx + dy >= this.dragThresholdPx) { - this.dragging = true; - } - } - if (!this.dragging) { - return; - } - this.queueDragTick(activitySource); - } - onDragStart = (event) => { - if (!event.isPrimary || event.button !== 0) return; - if (event.pointerType === "touch") return; - event.preventDefault(); - this.startDragSession(event.clientX, event.clientY, "overlay-pointer-down"); - document.addEventListener("pointermove", this.onGlobalPointerMove, { - passive: true - }); - document.addEventListener("pointerup", this.onDragEnd); - document.addEventListener("pointercancel", this.onDragEnd); - }; - -onTouchDragStart = (event) => { - if (!event.touches || event.touches.length === 0) return; - const touch = event.touches[0]; - this.startDragSession(touch.clientX, touch.clientY, "overlay-touch-start"); - document.addEventListener("touchmove", this.onGlobalTouchMove, { - passive: false - }); - document.addEventListener("touchend", this.onDragEnd); - document.addEventListener("touchcancel", this.onDragEnd); - }; - onGlobalTouchMove = (event) => { - if (!event.touches || event.touches.length === 0) return; - const t2 = event.touches[0]; - this.updateDragFromMove(t2.clientX, t2.clientY, "overlay-touch-move"); - if (this.dragging) { - event.preventDefault(); - } - }; - onGlobalPointerMove = (event) => { - this.updateDragFromMove( - event.clientX, - event.clientY, - "overlay-pointer-move" - ); - }; - 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 x2 = this.currentClientX - this.containerRect.left; - const clampedX = Math.max(0, Math.min(x2, width)); - const percentX = clampedX / width * 100; - this.moveButton(percentX); - }; - onCheckerTick = () => { - this.applyDragFromState(); - }; - onDragEnd = () => { - document.removeEventListener("pointermove", this.onGlobalPointerMove); - document.removeEventListener("pointerup", this.onDragEnd); - document.removeEventListener("pointercancel", this.onDragEnd); - document.removeEventListener("touchmove", this.onGlobalTouchMove); - document.removeEventListener("touchend", this.onDragEnd); - document.removeEventListener("touchcancel", this.onDragEnd); - this.applyDragFromState(); - const isBigContainer = this.dragIsBigContainer ?? this.isBigContainer; - if (this.dragging && isBigContainer && this.data.buttonPos) { - void votStorage.set("buttonPos", this.data.buttonPos); - } - this.dragging = false; - this.dragCandidate = false; - this.dragDirty = false; - this.containerRect = null; - this.dragIsBigContainer = null; - }; - updateButtonOpacity(opacity) { - if (!this.isInitialized() || !this.votMenu.hidden) { - return this; - } - if (Math.abs(this.votButton.opacity - opacity) > 0.01) { - this.votButton.opacity = opacity; - } - return this; - } - doReleaseUI() { - this.votButton?.remove(); - this.votMenu?.remove(); - this.votButtonTooltip?.release(); - } - 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() { - const widthFromVideo = this.videoHandler?.video?.getBoundingClientRect?.().width; - if (typeof widthFromVideo === "number" && Number.isFinite(widthFromVideo)) { - return widthFromVideo > OverlayView.BIG_CONTAINER_WIDTH_PX; - } - const widthFromContainer = this.videoHandler?.container?.getBoundingClientRect?.().width; - if (typeof widthFromContainer === "number" && Number.isFinite(widthFromContainer)) { - return widthFromContainer > OverlayView.BIG_CONTAINER_WIDTH_PX; - } - return this.root.clientWidth > OverlayView.BIG_CONTAINER_WIDTH_PX; - } - get pipButtonVisible() { - return isPiPAvailable() && !!this.data.showPiPButton; - } - } - function isSidePosition(position2) { - return position2 === "left" || position2 === "right"; - } - const positions = ["default", "top", "left", "right"]; - const 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" - }; - const 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" - }; - const PLATFORMS_MAP = { - bot: "bot", - desktop: "desktop", - mobile: "mobile", - tablet: "tablet", - tv: "tv" - }; - const 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" - }; - const ENGINE_MAP = { - Blink: "Blink", - EdgeHTML: "EdgeHTML", - Gecko: "Gecko", - Presto: "Presto", - Trident: "Trident", - WebKit: "WebKit" - }; - class Utils { -static getFirstMatch(regexp, ua) { - const match = ua.match(regexp); - return match && match.length > 0 && match[1] || ""; - } -static getSecondMatch(regexp, ua) { - const match = ua.match(regexp); - return match && match.length > 1 && match[2] || ""; - } -static matchAndReturnConst(regexp, ua, _const) { - if (regexp.test(ua)) { - return _const; - } - return void 0; - } - static getWindowsVersionName(version) { - switch (version) { - 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 void 0; - } - } -static getMacOSVersionName(version) { - const v2 = version.split(".").splice(0, 2).map((s2) => parseInt(s2, 10) || 0); - v2.push(0); - const major = v2[0]; - const minor = v2[1]; - if (major === 10) { - switch (minor) { - 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 void 0; - } - } - switch (major) { - case 11: - return "Big Sur"; - case 12: - return "Monterey"; - case 13: - return "Ventura"; - case 14: - return "Sonoma"; - case 15: - return "Sequoia"; - default: - return void 0; - } - } -static getAndroidVersionName(version) { - const v2 = version.split(".").splice(0, 2).map((s2) => parseInt(s2, 10) || 0); - v2.push(0); - if (v2[0] === 1 && v2[1] < 5) return void 0; - if (v2[0] === 1 && v2[1] < 6) return "Cupcake"; - if (v2[0] === 1 && v2[1] >= 6) return "Donut"; - if (v2[0] === 2 && v2[1] < 2) return "Eclair"; - if (v2[0] === 2 && v2[1] === 2) return "Froyo"; - if (v2[0] === 2 && v2[1] > 2) return "Gingerbread"; - if (v2[0] === 3) return "Honeycomb"; - if (v2[0] === 4 && v2[1] < 1) return "Ice Cream Sandwich"; - if (v2[0] === 4 && v2[1] < 4) return "Jelly Bean"; - if (v2[0] === 4 && v2[1] >= 4) return "KitKat"; - if (v2[0] === 5) return "Lollipop"; - if (v2[0] === 6) return "Marshmallow"; - if (v2[0] === 7) return "Nougat"; - if (v2[0] === 8) return "Oreo"; - if (v2[0] === 9) return "Pie"; - return void 0; - } -static getVersionPrecision(version) { - return version.split(".").length; - } - -static compareVersions(versionA, versionB, isLoose = false) { - const versionAPrecision = Utils.getVersionPrecision(versionA); - const versionBPrecision = Utils.getVersionPrecision(versionB); - let precision = Math.max(versionAPrecision, versionBPrecision); - let lastPrecision = 0; - const chunks = Utils.map([versionA, versionB], (version) => { - const delta = precision - Utils.getVersionPrecision(version); - const _version = version + new Array(delta + 1).join(".0"); - return Utils.map(_version.split("."), (chunk) => new Array(20 - chunk.length).join("0") + chunk).reverse(); - }); - if (isLoose) { - lastPrecision = precision - Math.min(versionAPrecision, versionBPrecision); - } - precision -= 1; - while (precision >= lastPrecision) { - if (chunks[0][precision] > chunks[1][precision]) { - return 1; - } - if (chunks[0][precision] === chunks[1][precision]) { - if (precision === lastPrecision) { - return 0; - } - precision -= 1; - } else if (chunks[0][precision] < chunks[1][precision]) { - return -1; - } - } - return void 0; - } -static map(arr, iterator) { - const result = []; - let i2; - if (Array.prototype.map) { - return Array.prototype.map.call(arr, iterator); - } - for (i2 = 0; i2 < arr.length; i2 += 1) { - result.push(iterator(arr[i2])); - } - return result; - } -static find(arr, predicate) { - let i2; - let l2; - if (Array.prototype.find) { - return Array.prototype.find.call(arr, predicate); - } - for (i2 = 0, l2 = arr.length; i2 < l2; i2 += 1) { - const value = arr[i2]; - if (predicate(value, i2)) { - return value; - } - } - return void 0; - } -static assign(obj, ...assigners) { - const result = obj; - let i2; - let l2; - if (Object.assign) { - return Object.assign(obj, ...assigners); - } - for (i2 = 0, l2 = assigners.length; i2 < l2; i2 += 1) { - const assigner = assigners[i2]; - if (typeof assigner === "object" && assigner !== null) { - const keys = Object.keys(assigner); - keys.forEach((key) => { - result[key] = assigner[key]; - }); - } - } - return obj; - } -static getBrowserAlias(browserName) { - return BROWSER_ALIASES_MAP[browserName]; - } -static getBrowserTypeByAlias(browserAlias) { - return BROWSER_MAP[browserAlias] || ""; - } - } - const commonVersionIdentifier = /version\/(\d+(\.?_?\d+)+)/i; - const browsersList = [ -{ - test: [/gptbot/i], - describe(ua) { - const browser = { - name: "GPTBot" - }; - const version = Utils.getFirstMatch(/gptbot\/(\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/chatgpt-user/i], - describe(ua) { - const browser = { - name: "ChatGPT-User" - }; - const version = Utils.getFirstMatch(/chatgpt-user\/(\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/oai-searchbot/i], - describe(ua) { - const browser = { - name: "OAI-SearchBot" - }; - const version = Utils.getFirstMatch(/oai-searchbot\/(\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/claudebot/i, /claude-web/i, /claude-user/i, /claude-searchbot/i], - describe(ua) { - const browser = { - name: "ClaudeBot" - }; - const version = Utils.getFirstMatch(/(?:claudebot|claude-web|claude-user|claude-searchbot)\/(\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/omgilibot/i, /webzio-extended/i], - describe(ua) { - const browser = { - name: "Omgilibot" - }; - const version = Utils.getFirstMatch(/(?:omgilibot|webzio-extended)\/(\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/diffbot/i], - describe(ua) { - const browser = { - name: "Diffbot" - }; - const version = Utils.getFirstMatch(/diffbot\/(\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/perplexitybot/i], - describe(ua) { - const browser = { - name: "PerplexityBot" - }; - const version = Utils.getFirstMatch(/perplexitybot\/(\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/perplexity-user/i], - describe(ua) { - const browser = { - name: "Perplexity-User" - }; - const version = Utils.getFirstMatch(/perplexity-user\/(\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/youbot/i], - describe(ua) { - const browser = { - name: "YouBot" - }; - const version = Utils.getFirstMatch(/youbot\/(\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/meta-webindexer/i], - describe(ua) { - const browser = { - name: "Meta-WebIndexer" - }; - const version = Utils.getFirstMatch(/meta-webindexer\/(\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/meta-externalads/i], - describe(ua) { - const browser = { - name: "Meta-ExternalAds" - }; - const version = Utils.getFirstMatch(/meta-externalads\/(\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/meta-externalagent/i], - describe(ua) { - const browser = { - name: "Meta-ExternalAgent" - }; - const version = Utils.getFirstMatch(/meta-externalagent\/(\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/meta-externalfetcher/i], - describe(ua) { - const browser = { - name: "Meta-ExternalFetcher" - }; - const version = Utils.getFirstMatch(/meta-externalfetcher\/(\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/googlebot/i], - describe(ua) { - const browser = { - name: "Googlebot" - }; - const version = Utils.getFirstMatch(/googlebot\/(\d+(\.\d+))/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/linespider/i], - describe(ua) { - const browser = { - name: "Linespider" - }; - const version = Utils.getFirstMatch(/(?:linespider)(?:-[-\w]+)?[\s/](\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/amazonbot/i], - describe(ua) { - const browser = { - name: "AmazonBot" - }; - const version = Utils.getFirstMatch(/amazonbot\/(\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/bingbot/i], - describe(ua) { - const browser = { - name: "BingCrawler" - }; - const version = Utils.getFirstMatch(/bingbot\/(\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/baiduspider/i], - describe(ua) { - const browser = { - name: "BaiduSpider" - }; - const version = Utils.getFirstMatch(/baiduspider\/(\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/duckduckbot/i], - describe(ua) { - const browser = { - name: "DuckDuckBot" - }; - const version = Utils.getFirstMatch(/duckduckbot\/(\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/ia_archiver/i], - describe(ua) { - const browser = { - name: "InternetArchiveCrawler" - }; - const version = Utils.getFirstMatch(/ia_archiver\/(\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/facebookexternalhit/i, /facebookcatalog/i], - describe() { - return { - name: "FacebookExternalHit" - }; - } - }, -{ - test: [/slackbot/i, /slack-imgProxy/i], - describe(ua) { - const browser = { - name: "SlackBot" - }; - const version = Utils.getFirstMatch(/(?:slackbot|slack-imgproxy)(?:-[-\w]+)?[\s/](\d+(\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/yahoo!?[\s/]*slurp/i], - describe() { - return { - name: "YahooSlurp" - }; - } - }, -{ - test: [/yandexbot/i, /yandexmobilebot/i], - describe() { - return { - name: "YandexBot" - }; - } - }, -{ - test: [/pingdom/i], - describe() { - return { - name: "PingdomBot" - }; - } - }, -{ - test: [/opera/i], - describe(ua) { - const browser = { - name: "Opera" - }; - const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:opera)[\s/](\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/opr\/|opios/i], - describe(ua) { - const browser = { - name: "Opera" - }; - const version = Utils.getFirstMatch(/(?:opr|opios)[\s/](\S+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/SamsungBrowser/i], - describe(ua) { - const browser = { - name: "Samsung Internet for Android" - }; - const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:SamsungBrowser)[\s/](\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/Whale/i], - describe(ua) { - const browser = { - name: "NAVER Whale Browser" - }; - const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:whale)[\s/](\d+(?:\.\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/PaleMoon/i], - describe(ua) { - const browser = { - name: "Pale Moon" - }; - const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:PaleMoon)[\s/](\d+(?:\.\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/MZBrowser/i], - describe(ua) { - const browser = { - name: "MZ Browser" - }; - const version = Utils.getFirstMatch(/(?:MZBrowser)[\s/](\d+(?:\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/focus/i], - describe(ua) { - const browser = { - name: "Focus" - }; - const version = Utils.getFirstMatch(/(?:focus)[\s/](\d+(?:\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/swing/i], - describe(ua) { - const browser = { - name: "Swing" - }; - const version = Utils.getFirstMatch(/(?:swing)[\s/](\d+(?:\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/coast/i], - describe(ua) { - const browser = { - name: "Opera Coast" - }; - const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:coast)[\s/](\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/opt\/\d+(?:.?_?\d+)+/i], - describe(ua) { - const browser = { - name: "Opera Touch" - }; - const version = Utils.getFirstMatch(/(?:opt)[\s/](\d+(\.?_?\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/yabrowser/i], - describe(ua) { - const browser = { - name: "Yandex Browser" - }; - const version = Utils.getFirstMatch(/(?:yabrowser)[\s/](\d+(\.?_?\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/ucbrowser/i], - describe(ua) { - const browser = { - name: "UC Browser" - }; - const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:ucbrowser)[\s/](\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/Maxthon|mxios/i], - describe(ua) { - const browser = { - name: "Maxthon" - }; - const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:Maxthon|mxios)[\s/](\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/epiphany/i], - describe(ua) { - const browser = { - name: "Epiphany" - }; - const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:epiphany)[\s/](\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/puffin/i], - describe(ua) { - const browser = { - name: "Puffin" - }; - const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:puffin)[\s/](\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/sleipnir/i], - describe(ua) { - const browser = { - name: "Sleipnir" - }; - const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:sleipnir)[\s/](\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/k-meleon/i], - describe(ua) { - const browser = { - name: "K-Meleon" - }; - const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:k-meleon)[\s/](\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/micromessenger/i], - describe(ua) { - const browser = { - name: "WeChat" - }; - const version = Utils.getFirstMatch(/(?:micromessenger)[\s/](\d+(\.?_?\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/qqbrowser/i], - describe(ua) { - const browser = { - name: /qqbrowserlite/i.test(ua) ? "QQ Browser Lite" : "QQ Browser" - }; - const version = Utils.getFirstMatch(/(?:qqbrowserlite|qqbrowser)[/](\d+(\.?_?\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/msie|trident/i], - describe(ua) { - const browser = { - name: "Internet Explorer" - }; - const version = Utils.getFirstMatch(/(?:msie |rv:)(\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/\sedg\//i], - describe(ua) { - const browser = { - name: "Microsoft Edge" - }; - const version = Utils.getFirstMatch(/\sedg\/(\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/edg([ea]|ios)/i], - describe(ua) { - const browser = { - name: "Microsoft Edge" - }; - const version = Utils.getSecondMatch(/edg([ea]|ios)\/(\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/vivaldi/i], - describe(ua) { - const browser = { - name: "Vivaldi" - }; - const version = Utils.getFirstMatch(/vivaldi\/(\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/seamonkey/i], - describe(ua) { - const browser = { - name: "SeaMonkey" - }; - const version = Utils.getFirstMatch(/seamonkey\/(\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/sailfish/i], - describe(ua) { - const browser = { - name: "Sailfish" - }; - const version = Utils.getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/silk/i], - describe(ua) { - const browser = { - name: "Amazon Silk" - }; - const version = Utils.getFirstMatch(/silk\/(\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/phantom/i], - describe(ua) { - const browser = { - name: "PhantomJS" - }; - const version = Utils.getFirstMatch(/phantomjs\/(\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/slimerjs/i], - describe(ua) { - const browser = { - name: "SlimerJS" - }; - const version = Utils.getFirstMatch(/slimerjs\/(\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/blackberry|\bbb\d+/i, /rim\stablet/i], - describe(ua) { - const browser = { - name: "BlackBerry" - }; - const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/blackberry[\d]+\/(\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/(web|hpw)[o0]s/i], - describe(ua) { - const browser = { - name: "WebOS Browser" - }; - const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/w(?:eb)?[o0]sbrowser\/(\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/bada/i], - describe(ua) { - const browser = { - name: "Bada" - }; - const version = Utils.getFirstMatch(/dolfin\/(\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/tizen/i], - describe(ua) { - const browser = { - name: "Tizen" - }; - const version = Utils.getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.?_?\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/qupzilla/i], - describe(ua) { - const browser = { - name: "QupZilla" - }; - const version = Utils.getFirstMatch(/(?:qupzilla)[\s/](\d+(\.?_?\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/librewolf/i], - describe(ua) { - const browser = { - name: "LibreWolf" - }; - const version = Utils.getFirstMatch(/(?:librewolf)[\s/](\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/firefox|iceweasel|fxios/i], - describe(ua) { - const browser = { - name: "Firefox" - }; - const version = Utils.getFirstMatch(/(?:firefox|iceweasel|fxios)[\s/](\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/electron/i], - describe(ua) { - const browser = { - name: "Electron" - }; - const version = Utils.getFirstMatch(/(?:electron)\/(\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/sogoumobilebrowser/i, /metasr/i, /se 2\.[x]/i], - describe(ua) { - const browser = { - name: "Sogou Browser" - }; - const sogouMobileVersion = Utils.getFirstMatch(/(?:sogoumobilebrowser)[\s/](\d+(\.?_?\d+)+)/i, ua); - const chromiumVersion = Utils.getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.?_?\d+)+)/i, ua); - const seVersion = Utils.getFirstMatch(/se ([\d.]+)x/i, ua); - const version = sogouMobileVersion || chromiumVersion || seVersion; - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/MiuiBrowser/i], - describe(ua) { - const browser = { - name: "Miui" - }; - const version = Utils.getFirstMatch(/(?:MiuiBrowser)[\s/](\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test(parser) { - if (parser.hasBrand("DuckDuckGo")) { - return true; - } - return parser.test(/\sDdg\/[\d.]+$/i); - }, - describe(ua, parser) { - const browser = { - name: "DuckDuckGo" - }; - if (parser) { - const hintsVersion = parser.getBrandVersion("DuckDuckGo"); - if (hintsVersion) { - browser.version = hintsVersion; - return browser; - } - } - const uaVersion = Utils.getFirstMatch(/\sDdg\/([\d.]+)$/i, ua); - if (uaVersion) { - browser.version = uaVersion; - } - return browser; - } - }, -{ - test(parser) { - return parser.hasBrand("Brave"); - }, - describe(ua, parser) { - const browser = { - name: "Brave" - }; - if (parser) { - const hintsVersion = parser.getBrandVersion("Brave"); - if (hintsVersion) { - browser.version = hintsVersion; - return browser; - } - } - return browser; - } - }, - { - test: [/chromium/i], - describe(ua) { - const browser = { - name: "Chromium" - }; - const version = Utils.getFirstMatch(/(?:chromium)[\s/](\d+(\.?_?\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/chrome|crios|crmo/i], - describe(ua) { - const browser = { - name: "Chrome" - }; - const version = Utils.getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, - { - test: [/GSA/i], - describe(ua) { - const browser = { - name: "Google Search" - }; - const version = Utils.getFirstMatch(/(?:GSA)\/(\d+(\.?_?\d+)+)/i, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test(parser) { - const notLikeAndroid = !parser.test(/like android/i); - const butAndroid = parser.test(/android/i); - return notLikeAndroid && butAndroid; - }, - describe(ua) { - const browser = { - name: "Android Browser" - }; - const version = Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/playstation 4/i], - describe(ua) { - const browser = { - name: "PlayStation 4" - }; - const version = Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/safari|applewebkit/i], - describe(ua) { - const browser = { - name: "Safari" - }; - const version = Utils.getFirstMatch(commonVersionIdentifier, ua); - if (version) { - browser.version = version; - } - return browser; - } - }, -{ - test: [/.*/i], - describe(ua) { - const regexpWithoutDeviceSpec = /^(.*)\/(.*) /; - const regexpWithDeviceSpec = /^(.*)\/(.*)[ \t]\((.*)/; - const hasDeviceSpec = ua.search("\\(") !== -1; - const regexp = hasDeviceSpec ? regexpWithDeviceSpec : regexpWithoutDeviceSpec; - return { - name: Utils.getFirstMatch(regexp, ua), - version: Utils.getSecondMatch(regexp, ua) - }; - } - } - ]; - const osParsersList = [ -{ - test: [/Roku\/DVP/], - describe(ua) { - const version = Utils.getFirstMatch(/Roku\/DVP-(\d+\.\d+)/i, ua); - return { - name: OS_MAP.Roku, - version - }; - } - }, -{ - test: [/windows phone/i], - describe(ua) { - const version = Utils.getFirstMatch(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i, ua); - return { - name: OS_MAP.WindowsPhone, - version - }; - } - }, -{ - test: [/windows /i], - describe(ua) { - const version = Utils.getFirstMatch(/Windows ((NT|XP)( \d\d?.\d)?)/i, ua); - const versionName = Utils.getWindowsVersionName(version); - return { - name: OS_MAP.Windows, - version, - versionName - }; - } - }, -{ - test: [/Macintosh(.*?) FxiOS(.*?)\//], - describe(ua) { - const result = { - name: OS_MAP.iOS - }; - const version = Utils.getSecondMatch(/(Version\/)(\d[\d.]+)/, ua); - if (version) { - result.version = version; - } - return result; - } - }, -{ - test: [/macintosh/i], - describe(ua) { - const version = Utils.getFirstMatch(/mac os x (\d+(\.?_?\d+)+)/i, ua).replace(/[_\s]/g, "."); - const versionName = Utils.getMacOSVersionName(version); - const os = { - name: OS_MAP.MacOS, - version - }; - if (versionName) { - os.versionName = versionName; - } - return os; - } - }, -{ - test: [/(ipod|iphone|ipad)/i], - describe(ua) { - const version = Utils.getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i, ua).replace(/[_\s]/g, "."); - return { - name: OS_MAP.iOS, - version - }; - } - }, -{ - test: [/OpenHarmony/i], - describe(ua) { - const version = Utils.getFirstMatch(/OpenHarmony\s+(\d+(\.\d+)*)/i, ua); - return { - name: OS_MAP.HarmonyOS, - version - }; - } - }, -{ - test(parser) { - const notLikeAndroid = !parser.test(/like android/i); - const butAndroid = parser.test(/android/i); - return notLikeAndroid && butAndroid; - }, - describe(ua) { - const version = Utils.getFirstMatch(/android[\s/-](\d+(\.\d+)*)/i, ua); - const versionName = Utils.getAndroidVersionName(version); - const os = { - name: OS_MAP.Android, - version - }; - if (versionName) { - os.versionName = versionName; - } - return os; - } - }, -{ - test: [/(web|hpw)[o0]s/i], - describe(ua) { - const version = Utils.getFirstMatch(/(?:web|hpw)[o0]s\/(\d+(\.\d+)*)/i, ua); - const os = { - name: OS_MAP.WebOS - }; - if (version && version.length) { - os.version = version; - } - return os; - } - }, -{ - test: [/blackberry|\bbb\d+/i, /rim\stablet/i], - describe(ua) { - const version = Utils.getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i, ua) || Utils.getFirstMatch(/blackberry\d+\/(\d+([_\s]\d+)*)/i, ua) || Utils.getFirstMatch(/\bbb(\d+)/i, ua); - return { - name: OS_MAP.BlackBerry, - version - }; - } - }, -{ - test: [/bada/i], - describe(ua) { - const version = Utils.getFirstMatch(/bada\/(\d+(\.\d+)*)/i, ua); - return { - name: OS_MAP.Bada, - version - }; - } - }, -{ - test: [/tizen/i], - describe(ua) { - const version = Utils.getFirstMatch(/tizen[/\s](\d+(\.\d+)*)/i, ua); - return { - name: OS_MAP.Tizen, - version - }; - } - }, -{ - test: [/linux/i], - describe() { - return { - name: OS_MAP.Linux - }; - } - }, -{ - test: [/CrOS/], - describe() { - return { - name: OS_MAP.ChromeOS - }; - } - }, -{ - test: [/PlayStation 4/], - describe(ua) { - const version = Utils.getFirstMatch(/PlayStation 4[/\s](\d+(\.\d+)*)/i, ua); - return { - name: OS_MAP.PlayStation4, - version - }; - } - } - ]; - const platformParsersList = [ -{ - test: [/googlebot/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Google" - }; - } - }, -{ - test: [/linespider/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Line" - }; - } - }, -{ - test: [/amazonbot/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Amazon" - }; - } - }, -{ - test: [/gptbot/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "OpenAI" - }; - } - }, -{ - test: [/chatgpt-user/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "OpenAI" - }; - } - }, -{ - test: [/oai-searchbot/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "OpenAI" - }; - } - }, -{ - test: [/baiduspider/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Baidu" - }; - } - }, -{ - test: [/bingbot/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Bing" - }; - } - }, -{ - test: [/duckduckbot/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "DuckDuckGo" - }; - } - }, -{ - test: [/claudebot/i, /claude-web/i, /claude-user/i, /claude-searchbot/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Anthropic" - }; - } - }, -{ - test: [/omgilibot/i, /webzio-extended/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Webz.io" - }; - } - }, -{ - test: [/diffbot/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Diffbot" - }; - } - }, -{ - test: [/perplexitybot/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Perplexity AI" - }; - } - }, -{ - test: [/perplexity-user/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Perplexity AI" - }; - } - }, -{ - test: [/youbot/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "You.com" - }; - } - }, -{ - test: [/ia_archiver/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Internet Archive" - }; - } - }, -{ - test: [/meta-webindexer/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Meta" - }; - } - }, -{ - test: [/meta-externalads/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Meta" - }; - } - }, -{ - test: [/meta-externalagent/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Meta" - }; - } - }, -{ - test: [/meta-externalfetcher/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Meta" - }; - } - }, -{ - test: [/facebookexternalhit/i, /facebookcatalog/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Meta" - }; - } - }, -{ - test: [/slackbot/i, /slack-imgProxy/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Slack" - }; - } - }, -{ - test: [/yahoo/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Yahoo" - }; - } - }, -{ - test: [/yandexbot/i, /yandexmobilebot/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Yandex" - }; - } - }, -{ - test: [/pingdom/i], - describe() { - return { - type: PLATFORMS_MAP.bot, - vendor: "Pingdom" - }; - } - }, -{ - test: [/huawei/i], - describe(ua) { - const model = Utils.getFirstMatch(/(can-l01)/i, ua) && "Nova"; - const platform = { - type: PLATFORMS_MAP.mobile, - vendor: "Huawei" - }; - if (model) { - platform.model = model; - } - return platform; - } - }, -{ - test: [/nexus\s*(?:7|8|9|10).*/i], - describe() { - return { - type: PLATFORMS_MAP.tablet, - vendor: "Nexus" - }; - } - }, -{ - test: [/ipad/i], - describe() { - return { - type: PLATFORMS_MAP.tablet, - vendor: "Apple", - model: "iPad" - }; - } - }, -{ - test: [/Macintosh(.*?) FxiOS(.*?)\//], - describe() { - return { - type: PLATFORMS_MAP.tablet, - vendor: "Apple", - model: "iPad" - }; - } - }, -{ - test: [/kftt build/i], - describe() { - return { - type: PLATFORMS_MAP.tablet, - vendor: "Amazon", - model: "Kindle Fire HD 7" - }; - } - }, -{ - test: [/silk/i], - describe() { - return { - type: PLATFORMS_MAP.tablet, - vendor: "Amazon" - }; - } - }, -{ - test: [/tablet(?! pc)/i], - describe() { - return { - type: PLATFORMS_MAP.tablet - }; - } - }, -{ - test(parser) { - const iDevice = parser.test(/ipod|iphone/i); - const likeIDevice = parser.test(/like (ipod|iphone)/i); - return iDevice && !likeIDevice; - }, - describe(ua) { - const model = Utils.getFirstMatch(/(ipod|iphone)/i, ua); - return { - type: PLATFORMS_MAP.mobile, - vendor: "Apple", - model - }; - } - }, -{ - test: [/nexus\s*[0-6].*/i, /galaxy nexus/i], - describe() { - return { - type: PLATFORMS_MAP.mobile, - vendor: "Nexus" - }; - } - }, -{ - test: [/Nokia/i], - describe(ua) { - const model = Utils.getFirstMatch(/Nokia\s+([0-9]+(\.[0-9]+)?)/i, ua); - const platform = { - type: PLATFORMS_MAP.mobile, - vendor: "Nokia" - }; - if (model) { - platform.model = model; - } - return platform; - } - }, -{ - test: [/[^-]mobi/i], - describe() { - return { - type: PLATFORMS_MAP.mobile - }; - } - }, -{ - test(parser) { - return parser.getBrowserName(true) === "blackberry"; - }, - describe() { - return { - type: PLATFORMS_MAP.mobile, - vendor: "BlackBerry" - }; - } - }, -{ - test(parser) { - return parser.getBrowserName(true) === "bada"; - }, - describe() { - return { - type: PLATFORMS_MAP.mobile - }; - } - }, -{ - test(parser) { - return parser.getBrowserName() === "windows phone"; - }, - describe() { - return { - type: PLATFORMS_MAP.mobile, - vendor: "Microsoft" - }; - } - }, -{ - test(parser) { - const osMajorVersion = Number(String(parser.getOSVersion()).split(".")[0]); - return parser.getOSName(true) === "android" && osMajorVersion >= 3; - }, - describe() { - return { - type: PLATFORMS_MAP.tablet - }; - } - }, -{ - test(parser) { - return parser.getOSName(true) === "android"; - }, - describe() { - return { - type: PLATFORMS_MAP.mobile - }; - } - }, -{ - test: [/smart-?tv|smarttv/i], - describe() { - return { - type: PLATFORMS_MAP.tv - }; - } - }, -{ - test: [/netcast/i], - describe() { - return { - type: PLATFORMS_MAP.tv - }; - } - }, -{ - test(parser) { - return parser.getOSName(true) === "macos"; - }, - describe() { - return { - type: PLATFORMS_MAP.desktop, - vendor: "Apple" - }; - } - }, -{ - test(parser) { - return parser.getOSName(true) === "windows"; - }, - describe() { - return { - type: PLATFORMS_MAP.desktop - }; - } - }, -{ - test(parser) { - return parser.getOSName(true) === "linux"; - }, - describe() { - return { - type: PLATFORMS_MAP.desktop - }; - } - }, -{ - test(parser) { - return parser.getOSName(true) === "playstation 4"; - }, - describe() { - return { - type: PLATFORMS_MAP.tv - }; - } - }, -{ - test(parser) { - return parser.getOSName(true) === "roku"; - }, - describe() { - return { - type: PLATFORMS_MAP.tv - }; - } - } - ]; - const enginesParsersList = [ -{ - test(parser) { - return parser.getBrowserName(true) === "microsoft edge"; - }, - describe(ua) { - const isBlinkBased = /\sedg\//i.test(ua); - if (isBlinkBased) { - return { - name: ENGINE_MAP.Blink - }; - } - const version = Utils.getFirstMatch(/edge\/(\d+(\.?_?\d+)+)/i, ua); - return { - name: ENGINE_MAP.EdgeHTML, - version - }; - } - }, -{ - test: [/trident/i], - describe(ua) { - const engine = { - name: ENGINE_MAP.Trident - }; - const version = Utils.getFirstMatch(/trident\/(\d+(\.?_?\d+)+)/i, ua); - if (version) { - engine.version = version; - } - return engine; - } - }, -{ - test(parser) { - return parser.test(/presto/i); - }, - describe(ua) { - const engine = { - name: ENGINE_MAP.Presto - }; - const version = Utils.getFirstMatch(/presto\/(\d+(\.?_?\d+)+)/i, ua); - if (version) { - engine.version = version; - } - return engine; - } - }, -{ - test(parser) { - const isGecko = parser.test(/gecko/i); - const likeGecko = parser.test(/like gecko/i); - return isGecko && !likeGecko; - }, - describe(ua) { - const engine = { - name: ENGINE_MAP.Gecko - }; - const version = Utils.getFirstMatch(/gecko\/(\d+(\.?_?\d+)+)/i, ua); - if (version) { - engine.version = version; - } - return engine; - } - }, -{ - test: [/(apple)?webkit\/537\.36/i], - describe() { - return { - name: ENGINE_MAP.Blink - }; - } - }, -{ - test: [/(apple)?webkit/i], - describe(ua) { - const engine = { - name: ENGINE_MAP.WebKit - }; - const version = Utils.getFirstMatch(/webkit\/(\d+(\.?_?\d+)+)/i, ua); - if (version) { - engine.version = version; - } - return engine; - } - } - ]; - class Parser { -constructor(UA, skipParsingOrHints = false, clientHints = null) { - if (UA === void 0 || UA === null || UA === "") { - throw new Error("UserAgent parameter can't be empty"); - } - this._ua = UA; - let skipParsing = false; - if (typeof skipParsingOrHints === "boolean") { - skipParsing = skipParsingOrHints; - this._hints = clientHints; - } else if (skipParsingOrHints != null && typeof skipParsingOrHints === "object") { - this._hints = skipParsingOrHints; - } else { - this._hints = null; - } - this.parsedResult = {}; - if (skipParsing !== true) { - this.parse(); - } - } -getHints() { - return this._hints; - } -hasBrand(brandName) { - if (!this._hints || !Array.isArray(this._hints.brands)) { - return false; - } - const brandLower = brandName.toLowerCase(); - return this._hints.brands.some( - (b2) => b2.brand && b2.brand.toLowerCase() === brandLower - ); - } -getBrandVersion(brandName) { - if (!this._hints || !Array.isArray(this._hints.brands)) { - return void 0; - } - const brandLower = brandName.toLowerCase(); - const brand = this._hints.brands.find( - (b2) => b2.brand && b2.brand.toLowerCase() === brandLower - ); - return brand ? brand.version : void 0; - } -getUA() { - return this._ua; - } -test(regex) { - return regex.test(this._ua); - } -parseBrowser() { - this.parsedResult.browser = {}; - const browserDescriptor = Utils.find(browsersList, (_browser) => { - if (typeof _browser.test === "function") { - return _browser.test(this); - } - if (Array.isArray(_browser.test)) { - return _browser.test.some((condition) => this.test(condition)); - } - throw new Error("Browser's test function is not valid"); - }); - if (browserDescriptor) { - this.parsedResult.browser = browserDescriptor.describe(this.getUA(), this); - } - return this.parsedResult.browser; - } -getBrowser() { - if (this.parsedResult.browser) { - return this.parsedResult.browser; - } - return this.parseBrowser(); - } -getBrowserName(toLowerCase) { - if (toLowerCase) { - return String(this.getBrowser().name).toLowerCase() || ""; - } - return this.getBrowser().name || ""; - } -getBrowserVersion() { - return this.getBrowser().version; - } -getOS() { - if (this.parsedResult.os) { - return this.parsedResult.os; - } - return this.parseOS(); - } -parseOS() { - this.parsedResult.os = {}; - const os = Utils.find(osParsersList, (_os) => { - if (typeof _os.test === "function") { - return _os.test(this); - } - if (Array.isArray(_os.test)) { - return _os.test.some((condition) => this.test(condition)); - } - throw new Error("Browser's test function is not valid"); - }); - if (os) { - this.parsedResult.os = os.describe(this.getUA()); - } - return this.parsedResult.os; - } -getOSName(toLowerCase) { - const { name } = this.getOS(); - if (toLowerCase) { - return String(name).toLowerCase() || ""; - } - return name || ""; - } -getOSVersion() { - return this.getOS().version; - } -getPlatform() { - if (this.parsedResult.platform) { - return this.parsedResult.platform; - } - return this.parsePlatform(); - } -getPlatformType(toLowerCase = false) { - const { type } = this.getPlatform(); - if (toLowerCase) { - return String(type).toLowerCase() || ""; - } - return type || ""; - } -parsePlatform() { - this.parsedResult.platform = {}; - const platform = Utils.find(platformParsersList, (_platform) => { - if (typeof _platform.test === "function") { - return _platform.test(this); - } - if (Array.isArray(_platform.test)) { - return _platform.test.some((condition) => this.test(condition)); - } - throw new Error("Browser's test function is not valid"); - }); - if (platform) { - this.parsedResult.platform = platform.describe(this.getUA()); - } - return this.parsedResult.platform; - } -getEngine() { - if (this.parsedResult.engine) { - return this.parsedResult.engine; - } - return this.parseEngine(); - } -getEngineName(toLowerCase) { - if (toLowerCase) { - return String(this.getEngine().name).toLowerCase() || ""; - } - return this.getEngine().name || ""; - } -parseEngine() { - this.parsedResult.engine = {}; - const engine = Utils.find(enginesParsersList, (_engine) => { - if (typeof _engine.test === "function") { - return _engine.test(this); - } - if (Array.isArray(_engine.test)) { - return _engine.test.some((condition) => this.test(condition)); - } - throw new Error("Browser's test function is not valid"); - }); - if (engine) { - this.parsedResult.engine = engine.describe(this.getUA()); - } - return this.parsedResult.engine; - } -parse() { - this.parseBrowser(); - this.parseOS(); - this.parsePlatform(); - this.parseEngine(); - return this; - } -getResult() { - return Utils.assign({}, this.parsedResult); - } -satisfies(checkTree) { - const platformsAndOSes = {}; - let platformsAndOSCounter = 0; - const browsers = {}; - let browsersCounter = 0; - const allDefinitions = Object.keys(checkTree); - allDefinitions.forEach((key) => { - const currentDefinition = checkTree[key]; - if (typeof currentDefinition === "string") { - browsers[key] = currentDefinition; - browsersCounter += 1; - } else if (typeof currentDefinition === "object") { - platformsAndOSes[key] = currentDefinition; - platformsAndOSCounter += 1; - } - }); - if (platformsAndOSCounter > 0) { - const platformsAndOSNames = Object.keys(platformsAndOSes); - const OSMatchingDefinition = Utils.find(platformsAndOSNames, (name) => this.isOS(name)); - if (OSMatchingDefinition) { - const osResult = this.satisfies(platformsAndOSes[OSMatchingDefinition]); - if (osResult !== void 0) { - return osResult; - } - } - const platformMatchingDefinition = Utils.find( - platformsAndOSNames, - (name) => this.isPlatform(name) - ); - if (platformMatchingDefinition) { - const platformResult = this.satisfies(platformsAndOSes[platformMatchingDefinition]); - if (platformResult !== void 0) { - return platformResult; - } - } - } - if (browsersCounter > 0) { - const browserNames = Object.keys(browsers); - const matchingDefinition = Utils.find(browserNames, (name) => this.isBrowser(name, true)); - if (matchingDefinition !== void 0) { - return this.compareVersion(browsers[matchingDefinition]); - } - } - return void 0; - } -isBrowser(browserName, includingAlias = false) { - const defaultBrowserName = this.getBrowserName().toLowerCase(); - let browserNameLower = browserName.toLowerCase(); - const alias = Utils.getBrowserTypeByAlias(browserNameLower); - if (includingAlias && alias) { - browserNameLower = alias.toLowerCase(); - } - return browserNameLower === defaultBrowserName; - } - compareVersion(version) { - let expectedResults = [0]; - let comparableVersion = version; - let isLoose = false; - const currentBrowserVersion = this.getBrowserVersion(); - if (typeof currentBrowserVersion !== "string") { - return void 0; - } - if (version[0] === ">" || version[0] === "<") { - comparableVersion = version.substr(1); - if (version[1] === "=") { - isLoose = true; - comparableVersion = version.substr(2); - } else { - expectedResults = []; - } - if (version[0] === ">") { - expectedResults.push(1); - } else { - expectedResults.push(-1); - } - } else if (version[0] === "=") { - comparableVersion = version.substr(1); - } else if (version[0] === "~") { - isLoose = true; - comparableVersion = version.substr(1); - } - return expectedResults.indexOf( - Utils.compareVersions(currentBrowserVersion, comparableVersion, isLoose) - ) > -1; - } -isOS(osName) { - return this.getOSName(true) === String(osName).toLowerCase(); - } -isPlatform(platformType) { - return this.getPlatformType(true) === String(platformType).toLowerCase(); - } -isEngine(engineName) { - return this.getEngineName(true) === String(engineName).toLowerCase(); - } -is(anything, includingAlias = false) { - return this.isBrowser(anything, includingAlias) || this.isOS(anything) || this.isPlatform(anything); - } -some(anythings = []) { - return anythings.some((anything) => this.is(anything)); - } - } - class Bowser { -static getParser(UA, skipParsingOrHints = false, clientHints = null) { - if (typeof UA !== "string") { - throw new Error("UserAgent should be a string"); - } - return new Parser(UA, skipParsingOrHints, clientHints); - } -static parse(UA, clientHints = null) { - return new Parser(UA, clientHints).getResult(); - } - static get BROWSER_MAP() { - return BROWSER_MAP; - } - static get ENGINE_MAP() { - return ENGINE_MAP; - } - static get OS_MAP() { - return OS_MAP; - } - static get PLATFORMS_MAP() { - return PLATFORMS_MAP; - } - } - const browserInfo = Bowser.getParser( - globalThis.navigator.userAgent - ).getResult(); - const UNKNOWN_VALUE = "unknown"; - const joinParts = (...parts) => { - const value = parts.filter(Boolean).join(" ").trim(); - return value || UNKNOWN_VALUE; - }; - function getEnvironmentInfo() { - const os = joinParts(browserInfo.os?.name, browserInfo.os?.version); - const browser = joinParts( - browserInfo.browser?.name, - browserInfo.browser?.version - ); - const loader = (() => { - const handler = GM_info?.scriptHandler; - const version = GM_info?.version; - if (handler && version) return `${handler} v${version}`; - return handler || version || UNKNOWN_VALUE; - })(); - const scriptVersion = GM_info?.script?.version ?? UNKNOWN_VALUE; - const scriptName = GM_info?.script?.name ?? UNKNOWN_VALUE; - const url = globalThis?.location?.href ?? UNKNOWN_VALUE; - return { - os, - browser, - loader, - scriptVersion, - scriptName, - url - }; - } - class AccountButton { - 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) { - addComponentEventListener(this.events, type, listener); - return this; - } - removeEventListener(type, listener) { - removeComponentEventListener(this.events, type, 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) { - setHiddenState(this.container, isHidden); - } - get hidden() { - return getHiddenState(this.container); - } - } - class Checkbox { - container; - input; - label; - onChange = new EventImpl(); - events = { - change: this.onChange - }; - _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) { - addComponentEventListener(this.events, "change", listener); - return this; - } - removeEventListener(_type, listener) { - removeComponentEventListener(this.events, "change", listener); - return this; - } - set hidden(isHidden) { - setHiddenState(this.container, isHidden); - } - get hidden() { - return getHiddenState(this.container); - } - get disabled() { - return this.input.disabled; - } - set disabled(isDisabled) { - this.input.disabled = isDisabled; - } - get checked() { - return this._checked; - } -set checked(isChecked) { - if (this._checked === isChecked) { - return; - } - this._checked = this.input.checked = isChecked; - this.onChange.dispatch(this._checked); - } - } - class Details { - container; - header; - arrowIcon; - onClick = new EventImpl(); - events = { - click: this.onClick - }; - _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) { - addComponentEventListener(this.events, "click", listener); - return this; - } - removeEventListener(_type, listener) { - removeComponentEventListener(this.events, "click", listener); - return this; - } - set hidden(isHidden) { - setHiddenState(this.container, isHidden); - } - get hidden() { - return getHiddenState(this.container); - } - } - class HotkeyButton { - container; - button; - onChange = new EventImpl(); - events = { - change: this.onChange - }; - _labelHtml; - _key; -pressedKeys; -comboKeys; - recording = false; - constructor({ labelHtml, key = null }) { - this._labelHtml = labelHtml; - this._key = key; - this.pressedKeys = new Set(); - this.comboKeys = 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) { - addComponentEventListener(this.events, "change", listener); - return this; - } - removeEventListener(_type, listener) { - removeComponentEventListener(this.events, "change", listener); - return this; - } - set hidden(isHidden) { - setHiddenState(this.container, isHidden); - } - get hidden() { - return getHiddenState(this.container); - } - get key() { - return this._key; - } - get keyText() { - if (!this._key) { - return localizationProvider.get("None"); - } - return formatKeysComboDisplay(this._key); - } -set key(newKey) { - if (this._key === newKey) { - return; - } - this._key = newKey; - this.button.textContent = this.keyText; - this.onChange.dispatch(this._key); - } - } - function formatKeysCombo(keys) { - const keysArray = Array.isArray(keys) ? keys : Array.from(keys); - return keysArray.map((code) => code.replace("Key", "").replace("Digit", "")).join("+"); - } - 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 = (k2) => { - switch (k2) { - 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 k2.replace("Key", "").replace("Digit", ""); - } - }; - const priority = (k2) => { - const m2 = map(k2); - if (m2 === "Ctrl") return 0; - if (m2 === "Alt") return 1; - if (m2 === "Shift") return 2; - if (m2 === "Meta") return 3; - return 10; - }; - return parts.slice().sort((a2, b2) => priority(a2) - priority(b2)).map(map).join("+"); - } - const 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: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; - } - const GOOGLE_FONTS_SEARCH_LIMIT = 30; - const 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"; - } - class SettingsView { - static PERSIST_DELAY_MS = 250; - globalPortal; - initialized = false; - data; - videoHandler; - suppressSubtitlesSmartLayoutCheckboxChange = false; - events = createSettingsEvents(); - persistTimerIds = {}; - dialog; - accountButton; - accountButtonRefreshTooltip; - accountButtonTokenTooltip; - autoTranslateCheckbox; - autoSubtitlesCheckbox; - dontTranslateLanguagesCheckbox; - dontTranslateLanguagesSelect; - autoSetVolumeSliderLabel; - autoSetVolumeCheckbox; - smartDuckingCheckbox; - autoSetVolumeSlider; - showVideoVolumeSliderCheckbox; - audioBoosterCheckbox; - audioBoosterTooltip; - syncVolumeCheckbox; - downloadWithNameCheckbox; - sendNotifyOnCompleteCheckbox; - useLivelyVoiceCheckbox; - useLivelyVoiceTooltip; - useAudioDownloadCheckbox; - useAudioDownloadCheckboxLabel; - useAudioDownloadCheckboxTooltip; - 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 = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`; - const headerId = `vot-settings-section-header-${sectionId}`; - const contentId = `vot-settings-section-content-${sectionId}`; - 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", () => { - const isOpen = header.container.dataset.open === "true"; - setOpen(!isOpen); - }); - section.append(header.container, content); - return { - title, - container: section, - header: header.container, - content, - setOpen, - getOpen - }; - } - setSubtitlesSmartLayout(checked) { - this.data.subtitlesSmartLayout = checked; - void votStorage.set("subtitlesSmartLayout", 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; - void 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") { - void 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()); - if (afterPersist) { - await afterPersist(value); - } - dispatch?.(value); - }); - } - 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 = this.createAccordionSection( - localizationProvider.get("VOTMyAccount"), - { open: true } - ); - const translationSection = this.createAccordionSection( - localizationProvider.get("translationSettings"), - { open: true } - ); - const subtitlesSection = this.createAccordionSection( - localizationProvider.get("subtitlesSettings") - ); - const hotkeysSection = this.createAccordionSection( - localizationProvider.get("hotkeysSettings") - ); - const proxySection = this.createAccordionSection( - localizationProvider.get("proxySettings") - ); - const miscSection = this.createAccordionSection( - localizationProvider.get("miscSettings") - ); - const appearanceSection = this.createAccordionSection( - localizationProvider.get("appearance") - ); - const aboutSection = this.createAccordionSection( - localizationProvider.get("aboutExtension") - ); - const sections = [ - accountSection, - translationSection, - subtitlesSection, - hotkeysSection, - proxySection, - miscSection, - appearanceSection, - aboutSection - ]; - this.dialog.bodyContainer.append( - ...sections.map((section) => section.container) - ); - 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 - }); - 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((lang2) => localizationProvider.get(`langs.${lang2}`)).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 ?? defaultAutoVolume; - 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 - })) - }); - this.subtitlesHighlightWordsCheckbox = new Checkbox({ - labelHtml: localizationProvider.get("VOTHighlightWords"), - checked: this.data.highlightWords - }); - const subtitlesSmartLayout2 = this.data.subtitlesSmartLayout ?? true; - this.subtitlesSmartLayoutCheckbox = new Checkbox({ - labelHtml: localizationProvider.get("subtitlesSmartLayout"), - checked: subtitlesSmartLayout2 - }); - 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"; - const 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]; - }; - 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: buildSubtitleFontItems(subtitlesFontFamily), - searchItemsProvider: async (query) => { - const activeFontFamily = Array.from(this.subtitlesFontFamilySelect?.selectedValues ?? [])[0] ?? subtitlesFontFamily; - const normalizedQuery = query.trim().toLowerCase(); - if (!normalizedQuery) { - return buildSubtitleFontItems(activeFontFamily); - } - const googleFontsCatalog = await loadGoogleFontsCatalog(); - const matchingGoogleFonts = googleFontsCatalog.filter( - (familyName) => familyName.toLowerCase().includes(normalizedQuery) - ).slice(0, GOOGLE_FONTS_SEARCH_LIMIT); - return buildSubtitleFontItems(activeFontFamily, matchingGoogleFonts); - } - }); - this.subtitlesFontFamilySelect.addEventListener("selectItem", (item) => { - if (!this.subtitlesFontFamilySelect) { - return; - } - this.subtitlesFontFamilySelect.updateItems(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.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 - }); - } - const onlyBypassMediaCSPLabel = this.videoHandler?.site.needBypassCSP ? `${localizationProvider.get("VOTOnlyBypassMediaCSP")} (${localizationProvider.get("VOTMediaCSPEnabledOnSite")})` : localizationProvider.get("VOTOnlyBypassMediaCSP"); - this.onlyBypassMediaCSPCheckbox = new Checkbox({ - labelHtml: onlyBypassMediaCSPLabel, - 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 ?? defaultTranslationService; - 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 ?? defaultDetectService; - 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 ?? defaultAutoHideDelay) / 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: 0.1, - max: 3, - step: 0.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((position2) => ({ - label: localizationProvider.get(`position.${position2}`), - value: position2, - selected: position2 === 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 = new Date( - (this.data.localeUpdatedAt ?? 0) * 1e3 - ).toLocaleString(); - const localeHashValue = this.data.localeHash ?? localizationProvider.get("notFound"); - const localeInfoValue = b`${localeHashValue}
(${localizationProvider.get( - "VOTUpdatedAt" - )} + //#endregion + //#region src/ui/components/componentShared.ts + function addComponentEventListener(events, type, listener) { + events[type].addListener(listener); + } + function removeComponentEventListener(events, type, listener) { + events[type].removeListener(listener); + } + function setHiddenState(element, isHidden) { + element.hidden = isHidden; + } + function getHiddenState(element) { + return element.hidden; + } + //#endregion + //#region src/ui/components/downloadButton.ts + var DownloadButton = class { + button; + loaderMain; + loaderCircle; + onClick = new EventImpl(); + events = { click: this.onClick }; + _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) { + addComponentEventListener(this.events, "click", listener); + return this; + } + removeEventListener(_type, listener) { + removeComponentEventListener(this.events, "click", 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) { + setHiddenState(this.button, isHidden); + } + get hidden() { + return getHiddenState(this.button); + } + }; + function clampProgress(value) { + if (!Number.isFinite(value)) return 0; + const asPercent = value < 1 ? value * 100 : value; + return Math.max(0, Math.min(100, Math.round(asPercent))); + } + //#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) { + setHiddenState(this.container, isHidden); + } + get hidden() { + return getHiddenState(this.container); + } + }; + //#endregion + //#region src/ui/components/dialog.ts + var Dialog = class { + container; + backdrop; + box; + contentWrapper; + headerContainer; + titleContainer; + title; + closeButton; + bodyContainer; + footerContainer; + onClose = new EventImpl(); + events = { close: this.onClose }; + previouslyFocused = null; + keydownListener; + adaptiveAlignObserver; + adaptiveAlignRaf = null; + handleViewportChange = () => { + this.scheduleAdaptiveVerticalAlign(); + }; + titleId = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `vot-dialog-title-${Math.random().toString(36).slice(2)}`; + _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; + container.setAttribute("aria-hidden", container.hidden ? "true" : "false"); + container.toggleAttribute("inert", 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) { + addComponentEventListener(this.events, "close", listener); + return this; + } + removeEventListener(_type, listener) { + removeComponentEventListener(this.events, "close", listener); + return this; + } + open() { + this.previouslyFocused ??= document.activeElement; + 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 = document.activeElement; + 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) { + setHiddenState(this.container, isHidden); + this.container.setAttribute("aria-hidden", isHidden ? "true" : "false"); + this.container.toggleAttribute("inert", isHidden); + } + get hidden() { + return getHiddenState(this.container); + } + 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) { + addComponentEventListener(this.events, type, listener); + return this; + } + removeEventListener(type, listener) { + removeComponentEventListener(this.events, type, 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) { + setHiddenState(this.container, isHidden); + } + get hidden() { + return getHiddenState(this.container); + } + }; + //#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) { + addComponentEventListener(this.events, type, listener); + return this; + } + removeEventListener(type, listener) { + removeComponentEventListener(this.events, type, 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) { + setHiddenState(this.container, isHidden); + } + get hidden() { + return getHiddenState(this.container); + } + get disabled() { + return this.outer.getAttribute("disabled") === "true" || this.outer.getAttribute("aria-disabled") === "true"; + } + set disabled(isDisabled) { + if (isDisabled) { + this.outer.setAttribute("disabled", "true"); + return; + } + 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(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(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(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) { + setHiddenState(this.container, isHidden); + } + get hidden() { + return getHiddenState(this.container); + } + }; + function clampNumber(value, min, max) { + if (!Number.isFinite(value)) return min; + if (max < min) return min; + return Math.max(min, Math.min(max, value)); + } + //#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) { + setHiddenState(this.container, isHidden); + } + get hidden() { + return getHiddenState(this.container); + } + }; + //#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) { + setHiddenState(this.container, isHidden); + } + get hidden() { + return getHiddenState(this.container); + } + 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 = typeof crypto !== "undefined" && "randomUUID" in crypto ? `vot-menu-${crypto.randomUUID()}` : `vot-menu-${Math.random().toString(36).slice(2)}`; + titleId = typeof crypto !== "undefined" && "randomUUID" in crypto ? `vot-menu-title-${crypto.randomUUID()}` : `vot-menu-title-${Math.random().toString(36).slice(2)}`; + 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"); + container.setAttribute("aria-hidden", "true"); + container.toggleAttribute("inert", 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) { + setHiddenState(this.container, isHidden); + this.container.setAttribute("aria-hidden", isHidden ? "true" : "false"); + this.container.toggleAttribute("inert", isHidden); + } + get hidden() { + return getHiddenState(this.container); + } + 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; + 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; + 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; + } + get root() { + return this.mount.root; + } + get portalContainer() { + return this.mount.portalContainer; + } + get tooltipLayoutRoot() { + return this.mount.tooltipLayoutRoot; + } + /** + * Update mount points (root/tooltipLayoutRoot) 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; + const prevTooltipRoot = this.mount.tooltipLayoutRoot; + const nextTooltipRoot = nextMount.tooltipLayoutRoot; + this.mount = nextMount; + if (!this.isInitialized()) return this; + if (prevRoot !== nextRoot) { + if (this.votButton) nextRoot.appendChild(this.votButton.container); + if (this.votMenu) nextRoot.appendChild(this.votMenu.container); + } + if (this.votButtonTooltip && didTooltipMountContextChange({ + root: prevRoot, + portalContainer: this.mount.portalContainer, + subtitlesMountContainer: this.mount.subtitlesMountContainer, + tooltipLayoutRoot: prevTooltipRoot + }, nextMount)) this.votButtonTooltip.updateMount({ layoutRoot: nextTooltipRoot ?? document.documentElement }); + 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); + } + 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; + 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.globalPortal, + layoutRoot: this.tooltipLayoutRoot + }); + this.votMenu = new VOTMenu({ + titleHtml: localizationProvider.get("VOTSettings"), + position + }); + this.root.appendChild(this.votMenu.container); + 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 activateOnKey = (handler) => (e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handler(); + } + }; + const isPrimaryActionPointer = (event) => event.isPrimary && event.button === 0; + 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.votButton.translateButton.addEventListener("pointerdown", (event) => { + if (!isPrimaryActionPointer(event)) return; + closeMenu(); + this.events["click:translate"].dispatch(); + }, { signal }); + this.votButton.translateButton.addEventListener("keydown", activateOnKey(() => { + closeMenu(); + this.events["click:translate"].dispatch(); + }), { signal }); + this.votButton.pipButton.addEventListener("pointerdown", (event) => { + if (!isPrimaryActionPointer(event)) return; + closeMenu(); + this.events["click:pip"].dispatch(); + }, { signal }); + this.votButton.pipButton.addEventListener("keydown", activateOnKey(() => { + closeMenu(); + this.events["click:pip"].dispatch(); + }), { signal }); + this.votButton.menuButton.addEventListener("pointerdown", (e) => { + if (!isPrimaryActionPointer(e)) return; + e.preventDefault(); + toggleMenu(); + }, { signal }); + this.votButton.menuButton.addEventListener("keydown", activateOnKey(toggleMenu), { signal }); + 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 target = e.target; + const path = typeof e.composedPath === "function" ? e.composedPath() : []; + const isInsideMenu = target && this.votMenu.container.contains(target) || path.includes(this.votMenu.container); + const isInsideToggle = target && this.votButton.menuButton.contains(target) || path.includes(this.votButton.menuButton); + const isInsideButton = target && this.votButton.container.contains(target) || path.includes(this.votButton.container); + const isInsideDialog = target instanceof HTMLElement && !!target.closest(".vot-dialog-container"); + if (isInsideMenu || isInsideToggle || isInsideButton || 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.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(); + } + 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() { + const widthFromVideo = this.videoHandler?.video?.getBoundingClientRect?.().width; + if (typeof widthFromVideo === "number" && Number.isFinite(widthFromVideo)) return widthFromVideo > OverlayView.BIG_CONTAINER_WIDTH_PX; + const widthFromContainer = this.videoHandler?.container?.getBoundingClientRect?.().width; + if (typeof widthFromContainer === "number" && Number.isFinite(widthFromContainer)) return widthFromContainer > OverlayView.BIG_CONTAINER_WIDTH_PX; + return this.root.clientWidth > OverlayView.BIG_CONTAINER_WIDTH_PX; + } + 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" + ]; + 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/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) { + addComponentEventListener(this.events, type, listener); + return this; + } + removeEventListener(type, listener) { + removeComponentEventListener(this.events, type, 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) { + setHiddenState(this.container, isHidden); + } + get hidden() { + return getHiddenState(this.container); + } + }; + //#endregion + //#region src/ui/components/checkbox.ts + var Checkbox = class { + container; + input; + label; + onChange = new EventImpl(); + events = { change: this.onChange }; + _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) { + addComponentEventListener(this.events, "change", listener); + return this; + } + removeEventListener(_type, listener) { + removeComponentEventListener(this.events, "change", listener); + return this; + } + set hidden(isHidden) { + setHiddenState(this.container, isHidden); + } + get hidden() { + return getHiddenState(this.container); + } + 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(); + events = { click: this.onClick }; + _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) { + addComponentEventListener(this.events, "click", listener); + return this; + } + removeEventListener(_type, listener) { + removeComponentEventListener(this.events, "click", listener); + return this; + } + set hidden(isHidden) { + setHiddenState(this.container, isHidden); + } + get hidden() { + return getHiddenState(this.container); + } + }; + //#endregion + //#region src/ui/components/hotkeyButton.ts + var HotkeyButton = class { + container; + button; + onChange = new EventImpl(); + events = { change: this.onChange }; + _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) { + addComponentEventListener(this.events, "change", listener); + return this; + } + removeEventListener(_type, listener) { + removeComponentEventListener(this.events, "change", listener); + return this; + } + set hidden(isHidden) { + setHiddenState(this.container, isHidden); + } + get hidden() { + return getHiddenState(this.container); + } + 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)).sort((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 = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`; + const headerId = `vot-settings-section-header-${sectionId}`; + const contentId = `vot-settings-section-content-${sectionId}`; + 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); + }); + } + 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")}
(${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"); - } - 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(authServerUrl, "_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 () => { - if (votStorage.isSupportOnlyLS) return; - this.data.account = await votStorage.get("account", {}); - this.updateAccountInfo(); - }); - 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 - ); - } - ); - this.dontTranslateLanguagesSelect.addEventListener( - "selectItem", - async (values) => { - this.data.dontTranslateLanguages = values; - await votStorage.set( - "dontTranslateLanguages", - this.data.dontTranslateLanguages - ); - } - ); - 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.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); - }); - this.subtitlesMaxLengthSlider.addEventListener("input", (value) => { - this.subtitlesMaxLengthSliderLabel.value = value; - if ((this.data.subtitlesSmartLayout ?? true) === true) { - this.setSubtitlesSmartLayout(false); - } - this.data.subtitlesMaxLength = value; - this.scheduleStoragePersist( - "subtitlesMaxLength", - this.data.subtitlesMaxLength - ); - this.events["input:subtitlesMaxLength"].dispatch(value); - }); - this.subtitlesFontSizeSlider.addEventListener("input", (value) => { - this.subtitlesFontSizeSliderLabel.value = value; - if ((this.data.subtitlesSmartLayout ?? true) === true) { - this.setSubtitlesSmartLayout(false); - } - this.data.subtitlesFontSize = value; - this.scheduleStoragePersist( - "subtitlesFontSize", - this.data.subtitlesFontSize - ); - this.events["input:subtitlesFontSize"].dispatch(value); - }); - this.subtitlesBackgroundOpacitySlider.addEventListener("input", (value) => { - this.subtitlesBackgroundOpacitySliderLabel.value = value; - this.data.subtitlesOpacity = value; - this.scheduleStoragePersist( - "subtitlesOpacity", - this.data.subtitlesOpacity - ); - 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 || proxyWorkerHost; - 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.autoHideButtonDelaySlider.addEventListener("input", (value) => { - this.autoHideButtonDelaySliderLabel.value = value; - const newDelay = Math.round(value * 1e3); - this.data.autoHideButtonDelay = newDelay; - this.scheduleStoragePersist( - "autoHideButtonDelay", - this.data.autoHideButtonDelay - ); - 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) => { - const result = await localizationProvider.changeLang(item); - if (!result) 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.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(); - } - } - class UIManager { - mount; - initialized = false; - videoHandler; - intervalIdleChecker; - data; - votGlobalPortal; -votOverlayView; -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; - } - get tooltipLayoutRoot() { - return this.mount.tooltipLayoutRoot; - } - isInitialized() { - return this.initialized; - } - initUI() { - if (this.isInitialized()) { - throw new Error("[VOT] UIManager is already initialized"); - } - this.initialized = true; - this.votGlobalPortal = UI.createPortal(); - this.getGlobalPortalHost(this.mount).appendChild(this.votGlobalPortal); - 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(); - return this; - } - updateMount(mount) { - const globalPortalHost = this.getGlobalPortalHost(mount); - if (this.votGlobalPortal?.parentElement !== globalPortalHost) { - globalPortalHost.appendChild(this.votGlobalPortal); - } - this.mount = applyOverlayMountUpdate(this.mount, mount, (nextMount) => { - this.votOverlayView?.updateMount(nextMount); - }); - this.videoHandler?.subtitlesWidget?.updateMount({ - container: mount.subtitlesMountContainer, - tooltipLayoutRoot: mount.tooltipLayoutRoot - }); - return this; - } - getGlobalPortalHost(mount) { - const doc = document; - const fullscreenEl = doc.fullscreenElement ?? doc.webkitFullscreenElement; - const isCurrentVideoFullscreen = Boolean( - resolveScopedFullscreenElement(fullscreenEl, [mount.root], { - allowDocumentViewport: true - }) - ); - return isCurrentVideoFullscreen ? mount.root : 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 { - const isPiPActive = this.videoHandler.video === document.pictureInPictureElement; - await (isPiPActive ? document.exitPictureInPicture() : this.videoHandler.video.requestPictureInPicture()); - } catch (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; - } - this.videoHandler.setVideoVolume(volume / 100); - 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; - this.videoHandler.audioPlayer.player.volume = nextVolume / 100; - if (!this.data.syncVolume) { - this.videoHandler.onTranslationVolumeSliderSynced(nextVolume); - return; - } - this.videoHandler.syncVolumeWrapper("translation", nextVolume); - }).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.enableSubtitlesForCurrentLangPair(); - }).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 ? maxAudioVolume : 100; - overlayView.translationVolumeSlider.max = maxVolume; - const nextVolume = clamp$2(currentVolume, 0, maxVolume); - overlayView.translationVolumeSlider.value = nextVolume; - this.videoHandler?.onTranslationVolumeSliderSynced(nextVolume); - }); - }).addEventListener("change:syncVolume", (checked) => { - if (!this.videoHandler) { - return; - } - this.videoHandler.setupAudioSettings(); - if (!checked) { - return; - } - this.withInitializedOverlayView((overlayView) => { - const videoSlider = overlayView.videoVolumeSlider; - const translationSlider = overlayView.translationVolumeSlider; - if (!videoSlider || !translationSlider) { - return; - } - this.videoHandler.resetVolumeLinkState( - Number(videoSlider.value), - Number(translationSlider.value) - ); - }); - }).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: position2, direction } = overlayView.calcButtonLayout(preferredPosition); - overlayView.updateButtonLayout(position2, 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; - if (!overlayView?.isInitialized() || !videoHandler?.downloadTranslationUrl || !videoHandler.videoData) { - return; - } - const downloadButton = overlayView.downloadTranslationButton; - const downloadUrl = videoHandler.downloadTranslationUrl; - const filename = this.data.downloadWithName ? clearFileName(videoHandler.videoData.downloadTitle) : `translation_${videoHandler.videoData.videoId}`; - const isMobile = this.isLikelyMobileDownloadContext(); - const saveOptions = { preferShare: isMobile }; - 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 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 - } - ); - const blob = new Blob( - [ - subsFormat === "json" ? JSON.stringify(subsContent) : subsContent - ], - { - type: "text/plain" - } - ); - const filename = this.data.downloadWithName ? clearFileName(videoHandler.videoData.downloadTitle) : `subtitles_${videoHandler.videoData.videoId}`; - const targetFilename = `${filename}.${subsFormat}`; - const isMobile = this.isLikelyMobileDownloadContext(); - const saveOptions = { preferShare: isMobile }; - await downloadBlob(blob, targetFilename, saveOptions); - } - 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: position2, direction } = this.votOverlayView.calcButtonLayout(prevButtonPos); - this.votOverlayView.updateButtonLayout(position2, direction); - this.votOverlayView.votMenu.hidden = prevMenuHidden; - this.votOverlayView.votButton.container.hidden = prevButtonHidden; - this.votOverlayView.votButton.opacity = prevButtonOpacity; - } catch (err) { - } - try { - this.videoHandler.rebindOverlayVisibilityTargets(); - } catch (err) { - } - if (settingsWasOpen) { - try { - this.votSettingsView?.open(); - } catch (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"); - } - const videoHandler = this.videoHandler; - if (!videoHandler) { - return this; - } - if (videoHandler.hasActiveSource()) { - await videoHandler.stopTranslation(); - return this; - } - if (this.votOverlayView.votButton.status === "error" && !this.votOverlayView.votButton.loading) { - this.transformBtn("none", localizationProvider.get("translateVideo")); - } - if (this.votOverlayView.votButton.status !== "none" || this.votOverlayView.votButton.loading) { - videoHandler.actionsAbortController.abort(); - await videoHandler.stopTranslation(); - return this; - } - try { - debug.log("[handleTranslationBtnClick] trying execute translation"); - const videoData = await this.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 (this.isAbortError(err)) { - this.transformBtn("none", localizationProvider.get("translateVideo")); - return this; - } - console.error("[VOT]", err); - if (!(err instanceof Error)) { - this.transformBtn("error", String(err)); - return this; - } - const message = err.name === "VOTLocalizedError" ? err.localizedMessage : err.message; - this.transformBtn("error", message); - } - return this; - } - async getVideoDataForTranslation(videoHandler) { - if (!videoHandler.videoData?.videoId) { - throw new VOTLocalizedError("VOTNoVideoIDFound"); - } - if (this.shouldRefreshVideoDataBeforeTranslation(videoHandler)) { - videoHandler.videoData = await videoHandler.getVideoData(); - } - if (!videoHandler.videoData?.videoId) { - throw new VOTLocalizedError("VOTNoVideoIDFound"); - } - return videoHandler.videoData; - } - shouldRefreshVideoDataBeforeTranslation(videoHandler) { - return videoHandler.site.host === "vk" && videoHandler.site.additionalData === "clips" || videoHandler.site.host === "douyin"; - } - isAbortError(error2) { - return error2 instanceof Error && error2.name === "AbortError"; - } - 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(); - this.votGlobalPortal.remove(); - 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) { - void task.catch((err) => { - }); - } - triggerUrlDownload(url, filename) { - try { - const a2 = document.createElement("a"); - a2.href = url; - a2.download = filename; - a2.target = "_blank"; - a2.rel = "noopener noreferrer"; - a2.style.display = "none"; - document.body.appendChild(a2); - a2.click(); - a2.remove(); - return true; - } catch { - return false; - } - } - isLikelyMobileDownloadContext() { - if (this.videoHandler?.site.additionalData === "mobile") { - return true; - } - return typeof matchMedia === "function" && matchMedia("(pointer: coarse)").matches; - } - restartAudioPlayer() { - void this.restartAudioPlayerSafely(); - } - async restartAudioPlayerSafely() { - const videoHandler = this.videoHandler; - if (!videoHandler) { - return; - } - try { - await videoHandler.stopTranslate(); - videoHandler.createPlayer(); - } catch (err) { - } - } - } - class OverlayVisibilityController { - deps; - hideDeadlineMs = 0; - hideArmed = false; - unsubscribeChecker; - constructor(deps) { - this.deps = deps; - this.unsubscribeChecker = this.deps.checker.subscribe(() => { - this.onCheckerTick(); - }); - } -show() { - const view = this.getView(); - if (!view) { - return null; - } - view.updateButtonOpacity(1); - return view; - } -cancel() { - this.hideDeadlineMs = 0; - this.hideArmed = false; - } - release() { - this.cancel(); - this.unsubscribeChecker(); - } -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(); - } -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); - } -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(); - } -scheduleHide(event) { - if (!this.getView()) { - return; - } - const currentTarget = event?.currentTarget; - let relatedTarget = event?.relatedTarget ?? null; - if (!relatedTarget && typeof event?.composedPath === "function") { - const path = event.composedPath(); - relatedTarget = path[1] ?? null; - } - const relatedNode = relatedTarget instanceof Node ? relatedTarget : null; - const currentNode = currentTarget instanceof Node ? currentTarget : null; - if (relatedNode && (currentNode?.contains(relatedNode) || this.deps.isInteractiveNode(relatedNode))) { - return; - } - this.queueAutoHide(); - } - onCheckerTick() { - if (!this.hideArmed || this.hideDeadlineMs <= 0) return; - const now2 = this.nowMs(); - if (now2 + 2 < this.hideDeadlineMs) { - return; - } - this.hideArmed = false; - let active = null; - const canCheckFocus = typeof document !== "undefined" && typeof document.hasFocus === "function"; - if (canCheckFocus && document.hasFocus()) { - active = document.activeElement; - } - if (active && this.deps.isInteractiveNode(active)) { - return; - } - const view = this.getView(); - view?.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(); - } - } - const 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); - }, - isDocumentHidden: () => typeof document !== "undefined" && typeof document.hidden === "boolean" ? document.hidden : false, - 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); - } - }; - } - }; - } - class IntervalIdleChecker { - profile; - runtime; - subscribers = new Set(); - intervalId = null; - unsubscribeVisibilityChange = null; - running = false; - destroyed = false; - immediateQueued = false; - currentMode = "active"; - lastActivityAt; - onVisibilityChangeHandler = () => { - if (this.destroyed || !this.running) return; - if (this.runtime.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 (this.runtime.isDocumentHidden()) { - return "hidden"; - } - const inactiveFor = nowMs - this.lastActivityAt; - return inactiveFor >= 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 }); - } - const now = () => Date.now(); - function getScriptTitle() { - return GM_info?.script?.name || "VOT"; - } - function safeL10n(key, fallback) { - try { - const value = localizationProvider?.get?.(key); - return value || 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); - const isKnownPhrase = localized !== key || defaultText !== key; - return isKnownPhrase ? localized || defaultText || key : null; - } catch { - return null; - } - } - function resolveLocalizedErrorMessage(message) { - if (message && typeof message === "object") { - const localizedError = message; - if (localizedError.name === "VOTLocalizedError") { - if (typeof localizedError.localizedMessage === "string" && localizedError.localizedMessage.trim()) { - return localizedError.localizedMessage; - } - if (typeof localizedError.unlocalizedMessage === "string") { - const byPhraseKey = localizePhraseText( - localizedError.unlocalizedMessage - ); - if (byPhraseKey) return byPhraseKey; - } - } - } - if (typeof message === "string") { - const byPhraseKey = localizePhraseText(message); - if (byPhraseKey) return byPhraseKey; - } - const extracted = getErrorMessage(message); - if (extracted) { - const byPhraseKey = localizePhraseText(extracted); - return byPhraseKey || extracted; - } - 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) { - } - return false; - } - class Notifier { - lastSentAt = 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() - }; - const ok = trySendViaUserscriptApi(normalized); - if (ok) { - markSent(this.lastSentAt, key); - } else { - debug.log("[notify] unavailable", normalized); - } - } catch (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 } - ); - } - } - const AD_ATTRS = ["class", "id", "title"]; - const AD_KEYWORDS = [ - "advertise", - "advertisement", - "promo", - "sponsor", - "banner", - "commercial", - "preroll", - "midroll", - "postroll", - "ad-container", - "sponsored" - ]; - const AD_KEYWORD_PATTERN = new RegExp( - AD_KEYWORDS.map( - (keyword) => keyword.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`) - ).join("|") - ); - const 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 g2 = globalThis; - const existing = g2[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: new Set() - }; - const patchedAttachShadow = function(init2) { - const root = original.call(this, init2); - for (const sub of state.subscribers) { - try { - sub(root); - } catch (error2) { - } - } - return root; - }; - try { - Object.defineProperty(Element.prototype, "attachShadow", { - ...descriptor, - value: patchedAttachShadow - }); - } catch { - return null; - } - g2[ATTACH_SHADOW_HOOK_KEY] = state; - return state; - } - function removeAttachShadowSubscriber(subscriber) { - const g2 = globalThis; - const state = g2[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 g2[ATTACH_SHADOW_HOOK_KEY]; - } - class VideoObserver { - seenVideos = new WeakSet(); - activeVideos = new WeakSet(); - observedRoots = new WeakSet(); - videoListenerControllers = new Map(); - pendingAdded = new Set(); - pendingRemoved = 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) { - for (let p2 = video.parentElement; p2; p2 = p2.parentElement) { - if (this.isAdRelated(p2)) return true; - } - return false; - } - getCapturedAudioTrackCount(video) { - const candidate = video; - const captureStream = candidate.captureStream ?? candidate.mozCaptureStream; - if (typeof captureStream !== "function") return null; - try { - const stream = captureStream.call(video); - return stream.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 v2 = video; - if (typeof v2.mozHasAudio === "boolean") { - return !v2.mozHasAudio; - } - if ("audioTracks" in v2 && typeof v2.audioTracks?.length === "number") { - if (v2.audioTracks.length > 0) return false; - const capturedTrackCount2 = this.getCapturedAudioTrackCount(video); - if (capturedTrackCount2 !== null) { - return capturedTrackCount2 === 0; - } - return true; - } - const capturedTrackCount = this.getCapturedAudioTrackCount(video); - if (capturedTrackCount !== null) { - return capturedTrackCount === 0; - } - return false; - } - hasAudio(video) { - const v2 = video; - if (video.srcObject instanceof MediaStream) { - return video.srcObject.getAudioTracks().length > 0; - } - if (typeof v2.mozHasAudio === "boolean") return v2.mozHasAudio; - if (typeof v2.webkitAudioDecodedByteCount === "number" && v2.webkitAudioDecodedByteCount > 0) { - return true; - } - if ("audioTracks" in v2 && typeof v2.audioTracks?.length === "number") { - if (v2.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)) { - return false; - } - return true; - } - observeRoot(root) { - if (this.observedRoots.has(root)) return; - this.observedRoots.add(root); - this.observer.observe(root, { childList: true, subtree: true }); - } - 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; - } - const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, { - acceptNode: (node) => { - const el = node; - const isVideo = el.tagName === "VIDEO"; - const hasShadowRoot = Boolean(el.shadowRoot); - return isVideo || hasShadowRoot ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; - } - }); - while (walker.nextNode()) { - const el = walker.currentNode; - if (el instanceof HTMLVideoElement) { - this.trackVideo(el); - continue; - } - const sr = el.shadowRoot; - if (sr) { - this.observeRoot(sr); - this.scan(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 = new Set(); - const addAll = (videos) => { - for (const v2 of videos) set.add(v2); - }; - if (node instanceof HTMLVideoElement) set.add(node); - if (node instanceof Document || node instanceof DocumentFragment || node instanceof Element) { - addAll(node.querySelectorAll("video")); - } - if (node instanceof Element) { - const shadowRoot = node.shadowRoot; - if (shadowRoot) addAll(shadowRoot.querySelectorAll("video")); - } - 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 r2 = document.documentElement; - if (!r2) return; - document.removeEventListener("readystatechange", onReady); - this.onDocumentReady = null; - if (!this.enabled) return; - this.observeRoot(r2); - this.scan(r2); - }; - this.onDocumentReady = onReady; - document.addEventListener("readystatechange", onReady); - if (typeof queueMicrotask === "function") queueMicrotask(onReady); - else void 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 = new WeakSet(); - this.activeVideos = new WeakSet(); - this.observedRoots = new WeakSet(); - } - } - function syncVideoLinkSnapshot(state, volumePercent) { - const normalized = clampPercentInt(volumePercent); - state.lastVideoPercent = normalized; - } - function syncTranslationLinkSnapshot(state, volumePercent) { - const numeric = Number(volumePercent); - if (!Number.isFinite(numeric)) { - return; - } - const normalized = Math.max(0, Math.round(numeric)); - state.lastTranslationPercent = normalized; - } - function applyVolumeLinkDelta({ - state, - fromType, - newVolume, - currentVideo, - currentTranslation, - translationMin, - translationMax - }) { - if (!state.initialized) { - state.lastVideoPercent = Number(currentVideo); - state.lastTranslationPercent = Number(currentTranslation); - state.initialized = true; - } - if (fromType === "video") { - const normalizedVideo = clampPercentInt(newVolume); - const delta2 = normalizedVideo - clampPercentInt(state.lastVideoPercent); - state.lastVideoPercent = normalizedVideo; - const nextTranslation = clampInt( - state.lastTranslationPercent + delta2, - translationMin, - translationMax - ); - state.lastTranslationPercent = nextTranslation; - return { nextTranslation }; - } - const normalizedTranslation = clampInt( - Number.isFinite(newVolume) ? newVolume : currentTranslation, - translationMin, - translationMax - ); - const delta = normalizedTranslation - state.lastTranslationPercent; - state.lastTranslationPercent = normalizedTranslation; - const nextVideo = clampPercentInt(state.lastVideoPercent + delta); - state.lastVideoPercent = nextVideo; - return { nextVideo }; - } - const defaultPlatformConfig = { - allowTouchMoveHandler: true, - disableContainerDrag: false - }; - const platformOverrides = { - xvideos: { - allowTouchMoveHandler: false - }, - youtube: { - disableContainerDrag: true - } - }; - function getPlatformEventConfig(host) { - if (!host) { - return defaultPlatformConfig; - } - const overrides = platformOverrides[host] ?? {}; - return { - ...defaultPlatformConfig, - ...overrides - }; - } - function mergeListenerSignals(primary, secondary) { - if (!secondary || secondary === primary) { - return primary; - } - if (primary.aborted) { - return primary; - } - if (secondary.aborted) { - return secondary; - } - const canCombine = typeof AbortSignal !== "undefined" && "any" in AbortSignal; - if (canCombine) { - return AbortSignal.any([primary, secondary]); - } - const controller = new AbortController(); - const cleanup = () => { - primary.removeEventListener("abort", onPrimaryAbort); - secondary.removeEventListener("abort", onSecondaryAbort); - }; - const onPrimaryAbort = () => { - cleanup(); - controller.abort(primary.reason); - }; - const onSecondaryAbort = () => { - cleanup(); - controller.abort(secondary.reason); - }; - primary.addEventListener("abort", onPrimaryAbort, { once: true }); - secondary.addEventListener("abort", onSecondaryAbort, { once: true }); - return controller.signal; - } - function createScopedListeners(signal) { - const add = (element, event, handler, options) => { - const mergedSignal = mergeListenerSignals(signal, options?.signal); - if (!options) { - element.addEventListener(event, handler, { signal: mergedSignal }); - return; - } - const { signal: _ignoredSignal, ...restOptions } = options; - element.addEventListener(event, handler, { - ...restOptions, - signal: mergedSignal - }); - }; - const addMany = (element, events, handler, options) => { - for (const event of events) { - add(element, event, handler, options); - } - }; - return { add, addMany }; - } - function bindOverlayHoverFocusEvents(addMany, target, overlayVisibility) { - addMany( - target, - ["pointerenter", "focusin"], - (event) => overlayVisibility.handleOverlayInteraction(event) - ); - addMany( - target, - ["pointermove"], - (event) => overlayVisibility.handleOverlayInteraction(event), - { passive: true } - ); - addMany( - target, - ["pointerleave", "focusout"], - (event) => overlayVisibility.scheduleHide(event) - ); - } - 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) { - const height = heightPx ?? self.video.getBoundingClientRect().height; - menu.style.setProperty("--vot-container-height", `${height}px`); - } - const { position: position2, direction } = overlayView.calcButtonLayout( - self.data?.buttonPos ?? "default" - ); - overlayView.updateButtonLayout(position2, direction); - } - function normalizeHotkeyPart(value) { - return value.replace("Key", "").replace("Digit", ""); - } - function buildPressedHotkeyPartsSet(userPressedKeys) { - const pressedParts = 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; - let lastObservedAriaValue = null; - for (const mutation of mutations) { - if (mutation.type !== "attributes" || mutation.attributeName !== "aria-valuenow") { - continue; - } - hasVolumeMutation = true; - const ariaValueNow = mutation.target instanceof Element ? mutation.target.getAttribute("aria-valuenow") : null; - const parsedAriaValue = ariaValueNow != null ? Number.parseFloat(ariaValueNow) : Number.NaN; - if (Number.isFinite(parsedAriaValue)) { - lastObservedAriaValue = parsedAriaValue; - } - } - if (!hasVolumeMutation) return; - let videoPercent; - if (lastObservedAriaValue != null) { - videoPercent = toPercentInt(lastObservedAriaValue); - } else { - const fallbackVolume = self.isMuted() ? 0 : self.getVideoVolume(); - videoPercent = toPercentInt(fallbackVolume * 100); - } - self.syncVideoVolumeSlider(); - syncAudioTranslationVolumeFromVideo(self, videoPercent); - }); - 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 player22 = YoutubeHelper.getPlayer(); - const availableTracks = player22?.getAvailableAudioTracks?.() ?? null; - if (!Array.isArray(availableTracks) || availableTracks.length <= 1) - return; - const currentTrackInfo = player22?.getAudioTrack?.()?.getLanguageInfo?.(); - const currentTrackId = currentTrackInfo?.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 (error2) { - debug.log( - "[VOT] Failed to trigger auto-translation on audio track change:", - error2 - ); - } - } - } catch (error2) { - } - }; - const player2 = YoutubeHelper.getPlayer(); - const listeners = ["onApiChange", "onStateChange"]; - if (player2?.addEventListener) { - for (const eventName of listeners) { - try { - player2.addEventListener(eventName, syncAudioTrackLanguage); - } catch (error2) { - } - } - } - void syncAudioTrackLanguage(); - self.abortController.signal.addEventListener( - "abort", - () => { - if (!player2?.removeEventListener) return; - for (const eventName of listeners) { - try { - player2.removeEventListener(eventName, syncAudioTrackLanguage); - } catch (error2) { - } - } - }, - { 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 isButton = target && button ? button.contains(target) : false; - const isMenu = target && menu ? menu.contains(target) : false; - const isVideo = target ? self.container.contains(target) : false; - const isSettings = target && settings ? settings.contains(target) : false; - const isTempDialog = target instanceof Element && target.closest(".vot-dialog-temp") instanceof Element; - if (isButton || isMenu || isSettings || isTempDialog) return; - if (!isVideo) overlayView.updateButtonOpacity(0); - if (menu && !menu.hidden) { - menu.hidden = true; - self.overlayVisibility?.queueAutoHide(); - } - }); - const userPressedKeys = new Set(); - const hotkeyCache = new Map(); - const clearUserPressedKeys = () => userPressedKeys.clear(); - const runHotkeyAction = (action, actionName) => { - void action().catch((error2) => { - }); - }; - add(document, "keydown", (event) => { - const keyboardEvent = event; - if (keyboardEvent.repeat) return; - userPressedKeys.add(keyboardEvent.code); - const activeElement = document.activeElement; - const activeTag = activeElement?.tagName?.toLowerCase?.() ?? ""; - const isInputElement = ["input", "textarea"].includes(activeTag) || Boolean(activeElement?.isContentEditable); - if (isInputElement) return; - const pressedParts = buildPressedHotkeyPartsSet(userPressedKeys); - if (isHotkeyMatch( - pressedParts, - getParsedHotkey(self.data?.translationHotkey, hotkeyCache) - )) { - clearUserPressedKeys(); - runHotkeyAction( - () => self.uiManager.handleTranslationBtnClick() - ); - return; - } - if (isHotkeyMatch( - pressedParts, - getParsedHotkey(self.data?.subtitlesHotkey, hotkeyCache) - )) { - clearUserPressedKeys(); - runHotkeyAction( - () => self.toggleSubtitlesForCurrentLangPair() - ); - } - }); - 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) { - addMany( - eventContainer, - ["pointerenter", "pointerdown"], - (event) => self.overlayVisibility.handleHostInteraction(event) - ); - add( - eventContainer, - "pointermove", - (event) => self.overlayVisibility.handleHostInteraction(event), - { passive: true } - ); - add( - eventContainer, - "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 bindVideoLifecycleEvents(ctx) { - const { self, overlayView, add } = ctx; - const safeSetCanPlay = async () => { - try { - await self.setCanPlay(); - } catch (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 (error2) { - } - if (self.videoData && videoId && videoId === self.videoData.videoId) { - return; - } - resetAndHideLifecycle(self, overlayView, { - clearVideoData: true, - hideMenu: true - }); - }; - add(self.video, "emptied", () => { - void handleVideoEmptied().catch((error2) => { - }); - }); - if (!isMuteSyncDisabledHost(self.site.host)) { - add(self.video, "volumechange", () => { - self.syncVideoVolumeSlider(); - const activeOverlayView = self.uiManager.votOverlayView; - if (!activeOverlayView?.isInitialized()) return; - const videoPercent = toPercentInt( - activeOverlayView.videoVolumeSlider.value - ); - syncAudioTranslationVolumeFromVideo(self, videoPercent, { - skipYouTubeLikeHosts: true - }); - }); - } - if (self.site.host === "youtube" && !self.site.additionalData) { - add(document, "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 - }; - 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 && buttonContainer.contains(node) || menuContainer instanceof Node && menuContainer.contains(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(); - } - } - let countryCode; - function setCountryCode(next) { - countryCode = next; - } - let countryCodeRequestInFlight = null; - async function ensureCountryCode() { - if (countryCode) { - return; - } - countryCodeRequestInFlight ??= (async () => { - try { - const response = await GM_fetch( - "https://cloudflare-dns.com/cdn-cgi/trace", - { - timeout: 7e3 - } - ); - const trace = await response.text(); - const loc = trace.split("\n").find((line) => line.startsWith("loc=")); - setCountryCode(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: defaultAutoVolume, - 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, - 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 !== actualCompatVersion) { - 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 - ); - 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; - } - function timeout(ms, message = "Operation timed out") { - return new Promise( - (_2, reject) => setTimeout(() => reject(new Error(message)), ms) - ); - } - const DEFAULT_CACHE_LOCALE = "und"; - const wordSegmenterCache = new Map(); - const sentenceSegmenterCache = new Map(); - const canonicalizeLocale = (locale) => { - if (!locale) return void 0; - try { - return Intl.getCanonicalLocales(locale)[0]; - } catch { - return void 0; - } - }; - const resolveSegmenterLocale = (locale) => { - const canonicalLocale = canonicalizeLocale(locale); - if (!canonicalLocale) return void 0; - return Intl.Segmenter.supportedLocalesOf([canonicalLocale])[0]; - }; - const 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; - }; - const 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) - })); - }; - const segmentSentences = (text, locale) => { - if (!text) return []; - return Array.from(getSegmenter(locale, "sentence").segment(text), (part) => ({ - text: part.segment, - index: part.index - })); - }; - const WHITESPACE_RE = /\s/u; - const NO_SPACE_BEFORE_RE = /^[,.:;!?%)\]}>В»]/u; - const NO_SPACE_AFTER_RE = /[([{'"«„-]$/u; - const CJK_CHAR_RE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u; - const hasVisibleText = (value) => Boolean(value.trim()); - const shouldInsertSpaceBetween = (leftText, rightText) => { - const leftLastChar = leftText.at(-1) ?? ""; - const rightFirstChar = rightText[0] ?? ""; - if (!leftLastChar || !rightFirstChar) return false; - if (WHITESPACE_RE.test(leftLastChar) || WHITESPACE_RE.test(rightFirstChar)) { - return false; - } - if (NO_SPACE_AFTER_RE.test(leftText) || NO_SPACE_BEFORE_RE.test(rightText)) { - return false; - } - if (CJK_CHAR_RE.test(leftLastChar) && CJK_CHAR_RE.test(rightFirstChar)) { - return false; - } - return true; - }; - const buildNormalizedLineSpans = (lines) => { - let streamText = ""; - let previousText = ""; - const spans = []; - for (const line of lines) { - const normalizedText = line.text.trim(); - if (!normalizedText) continue; - if (streamText && shouldInsertSpaceBetween(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 }; - }; - const 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 - }; - }; - const 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) - }; - }; - const buildSentenceLine = (streamText, spans, segment) => { - const rawStart = segment.index; - const rawEnd = rawStart + segment.text.length; - const trimmedRange = trimSentenceRange(streamText, rawStart, rawEnd); - 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 - }; - }; - const mergeAutoGeneratedSubtitleLines = (lines, locale) => { - const { streamText, spans } = buildNormalizedLineSpans(lines); - if (!streamText || !spans.length) { - return []; - } - const segments = segmentSentences(streamText, locale); - const sentenceLines = segments.map((segment) => buildSentenceLine(streamText, spans, segment)).filter((line) => line !== null); - return sentenceLines.length ? sentenceLines : spans.map(({ line }) => line); - }; - const isSubtitleFormat = (value) => value === "json" || value === "srt" || value === "vtt" || value === "ass"; - const isRecord$1 = (value) => Boolean(value && typeof value === "object"); - const toFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0; - const toNonNegativeNumber = (value) => Math.max(0, toFiniteNumber(value)); - const isSubtitleDescriptor = (arg) => { - if (!isRecord$1(arg)) return false; - return typeof arg.source === "string" && isSubtitleFormat(arg.format) && typeof arg.language === "string" && typeof arg.url === "string" && (arg.translatedFromLanguage === void 0 || typeof arg.translatedFromLanguage === "string") && (arg.isAutoGenerated === void 0 || typeof arg.isAutoGenerated === "boolean"); - }; - const 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; - }; - const appendYoutubePoTokenParams = (inputUrl) => { - const poToken = YoutubeHelper.getPoToken(); - if (!poToken) return inputUrl; - let normalizedPoToken = poToken; - for (let i2 = 0; i2 < 10; i2 += 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}`; - } - }; - const compareNumbers = (left, right) => left - right; - const compareStrings = (left, right) => { - if (left < right) return -1; - if (left > right) return 1; - return 0; - }; - const compareRankArrays = (left, right) => { - const length = Math.min(left.length, right.length); - for (let i2 = 0; i2 < length; i2 += 1) { - const diff = left[i2] - right[i2]; - if (diff !== 0) return diff; - } - if (left.length !== right.length) { - return left.length - right.length; - } - return 0; - }; - const getYandexTranslationKindRank = (isYandex, requestLanguageRank, isTranslated) => { - if (!isYandex) return 0; - if (requestLanguageRank === 0) { - return isTranslated ? 1 : 0; - } - return isTranslated ? 0 : 1; - }; - const getTranslatedFromRequestRank = (isYandex, isTranslated, translatedFromLanguage, requestLang) => { - if (!isYandex || !isTranslated) return 0; - return translatedFromLanguage === requestLang ? 0 : 1; - }; - const 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; - const translationKindRank = getYandexTranslationKindRank( - isYandex, - requestLanguageRank, - isTranslated - ); - const translatedFromRequestRank = getTranslatedFromRequestRank( - isYandex, - isTranslated, - descriptor.translatedFromLanguage, - requestLang - ); - const originalRequestLanguageRank = isYandex && !isTranslated ? requestLanguageRank : 0; - const nonYandexAutogeneratedRank = isYandex ? 0 : Number(Boolean(descriptor.isAutoGenerated)); - return [ - sourceRank, - uiLanguageRank, - translationKindRank, - translatedFromRequestRank, - originalRequestLanguageRank, - nonYandexAutogeneratedRank - ]; - }; - const 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)) - }); - const 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); - }; - const resolveTokenWordLike = (value, text) => { - if (typeof value === "boolean") return value; - return Boolean(text.trim()); - }; - const 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) - }; - }; - const 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 - }; - }; - const 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 }; - }; - const VK_INLINE_TIMESTAMP_RE = /<(?:\d{1,2}:)?\d{2}:\d{2}(?:[.,]\d{1,3})?>/gu; - const VK_DUPLICATE_MAX_GAP_MS = 120; - const VK_PUNCTUATION_SPACING_RE = /\s+([,.:;!?])/gu; - const VK_AROUND_NEWLINE_SPACING_RE = /[ \t]*\n[ \t]*/gu; - const VK_MULTIPLE_SPACES_RE = /[ \t]{2,}/gu; - const VK_EXCESS_NEWLINES_RE = /\n{3,}/gu; - const VK_COMPARABLE_SPACING_RE = /\s+/gu; - let htmlStripTemplate = null; - const stripHtmlToText = (value) => { - if (!value.includes("<")) return value; - if (typeof document !== "undefined") { - if (!htmlStripTemplate) { - htmlStripTemplate = document.createElement("template"); - } - const template = htmlStripTemplate; - template.innerHTML = value; - return template.content.textContent ?? ""; - } - return value.replaceAll(/<\/?[^>]+>/gu, ""); - }; - const normalizeSubtitleText = (value) => { - let normalized = value; - if (normalized.includes("<")) { - normalized = stripHtmlToText( - normalized.replaceAll(VK_INLINE_TIMESTAMP_RE, "") - ); - } - return normalized.replaceAll(VK_PUNCTUATION_SPACING_RE, "$1").replaceAll(VK_AROUND_NEWLINE_SPACING_RE, "\n").replaceAll(VK_MULTIPLE_SPACES_RE, " ").replaceAll(VK_EXCESS_NEWLINES_RE, "\n\n").trim(); - }; - const normalizeComparableText = (value) => value.toLowerCase().replaceAll(VK_COMPARABLE_SPACING_RE, " ").trim(); - const 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); - }; - const buildYoutubeSourceTokens = (event, segs, durationMs) => { - const sourceTokens = []; - let text = ""; - let remainingDuration = durationMs; - 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); - } - sourceTokens.push({ - text: rawText, - startMs: event.tStartMs + offset, - durationMs: tokenDuration, - isWordLike: Boolean(rawText.trim()) - }); - text += rawText; - } - return { text, sourceTokens }; - }; - const hasPositiveDuration = (token) => token.durationMs > 0; - const normalizeLineText = (line) => { - if (line.text) return normalizeSubtitleTextForDisplay(line.text); - if (!line.tokens.length) return ""; - return normalizeSubtitleTextForDisplay( - line.tokens.map((token) => token.text).join("") - ); - }; - const 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 i2 = 0; i2 < weights.length; i2 += 1) { - prefixWeights[i2 + 1] = prefixWeights[i2] + weights[i2]; - } - const timings = []; - for (let i2 = 0; i2 < texts.length; i2 += 1) { - const from = Math.round(safeDuration * prefixWeights[i2] / totalWeight); - const to = i2 === texts.length - 1 ? safeDuration : Math.round(safeDuration * prefixWeights[i2 + 1] / totalWeight); - timings.push({ - startMs: startMs + from, - durationMs: Math.max(0, to - from) - }); - } - return timings; - }; - const 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 - } - ]; - }; - const 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; - }; - const buildLineTokens = (line, descriptor, lineText) => { - if (!lineText) return []; - 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; - const slices = splitSegmentText(segment.text); - if (slices.length === 1 && slices[0].text === segment.text) { - nextTokens.push({ - text: segment.text, - startMs: segmentStartMs, - durationMs: segmentDurationMs, - isWordLike: segment.isWordLike, - style: getStyleForRange( - styledSpans, - segment.index, - segment.index + segment.text.length - ) - }); - continue; - } - const sliceTimings = allocateTimingsByLength( - slices.map((slice) => slice.text === "\n" ? "" : slice.text), - segmentStartMs, - segmentDurationMs - ); - for (let sliceIndex = 0; sliceIndex < slices.length; sliceIndex += 1) { - const slice = slices[sliceIndex]; - const isLineBreak = slice.text === "\n"; - nextTokens.push({ - 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 - ) - }); - } - } - const sourceTimedWords = collectSourceTimedWords(line.tokens, locale); - if (!sourceTimedWords.length) return nextTokens; - const wordIndices = nextTokens.reduce((indices, token, index) => { - if (token.isWordLike && token.text.trim()) indices.push(index); - return indices; - }, []); - if (!wordIndices.length) return nextTokens; - const totalTargetWords = wordIndices.length; - for (let i2 = 0; i2 < totalTargetWords; i2 += 1) { - const targetIndex = wordIndices[i2]; - const sourceIndex = Math.floor( - i2 * sourceTimedWords.length / totalTargetWords - ); - const sourceTiming = sourceTimedWords[Math.min(sourceIndex, sourceTimedWords.length - 1)]; - nextTokens[targetIndex] = { - ...nextTokens[targetIndex], - startMs: sourceTiming.startMs, - durationMs: sourceTiming.durationMs - }; - } - return nextTokens; - }; - const fetchRawSubtitles = async (url, format) => { - const response = await GM_fetch(url, { timeout: 7e3 }); - if (format === "vtt" || format === "srt" || format === "ass") { - const text = await response.text(); - return parseSubtitleText(text, format); - } - return response.json(); - }; - const 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)); - } - const normalized = ensureProcessedSubtitles(rawSubtitles); - if (descriptor.source === "vk") { - return SubtitlesProcessor.cleanJsonSubtitles(normalized); - } - return sortProcessedSubtitles(normalized); - }; - const processFetchedSubtitles = (subtitles, descriptor) => { - const maybeMerged = SubtitlesProcessor.autoMerge(subtitles, descriptor); - return { - ...maybeMerged, - subtitles: SubtitlesProcessor.processTokens(maybeMerged, descriptor) - }; - }; - const buildYandexSubtitles = (response) => { - const subtitles = []; - const seenOriginal = 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; - }; - const 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 i2 = 0; i2 < events.length; i2 += 1) { - const event = events[i2]; - const segs = event.segs; - if (!segs?.length) continue; - const nextEvent = events[i2 + 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 - ) - }; - }, - cleanJsonSubtitles(subtitles) { - const lineEndMs = (line) => line.startMs + Math.max(0, line.durationMs); - const tokensTextLength = (tokens) => { - let totalLength = 0; - for (const token of tokens) { - totalLength += token.text.length; - } - return totalLength; - }; - const cleanedEntries = []; - for (const line of subtitles.subtitles) { - const normalizedLineText = normalizeSubtitleText(line.text); - if (!normalizedLineText) continue; - const normalizedTokens = []; - for (const token of line.tokens) { - const normalizedTokenText = normalizeSubtitleText(token.text); - if (!normalizedTokenText) continue; - normalizedTokens.push({ - ...token, - text: normalizedTokenText - }); - } - cleanedEntries.push({ - line: { - ...line, - text: normalizedLineText, - tokens: normalizedTokens - }, - comparableText: normalizeComparableText(normalizedLineText), - tokensTextLength: tokensTextLength(normalizedTokens) - }); - } - const mergedEntries = []; - for (const entry of cleanedEntries) { - const { - line, - comparableText: currentComparable, - tokensTextLength: currentTokensLength - } = entry; - if (!currentComparable) continue; - const previous = mergedEntries.at(-1); - if (!previous) { - mergedEntries.push(entry); - continue; - } - const previousEnd = lineEndMs(previous.line); - const currentEnd = lineEndMs(line); - const isDuplicateText = previous.comparableText === currentComparable; - const isNearPrevious = line.startMs <= previousEnd + VK_DUPLICATE_MAX_GAP_MS; - if (!isDuplicateText || !isNearPrevious) { - mergedEntries.push(entry); - continue; - } - const mergedStart = Math.min(previous.line.startMs, line.startMs); - const mergedEnd = Math.max(previousEnd, currentEnd); - const mergedTokens = currentTokensLength >= previous.tokensTextLength ? line.tokens : previous.line.tokens; - previous.line = { - ...previous.line, - startMs: mergedStart, - durationMs: Math.max(0, mergedEnd - mergedStart), - tokens: mergedTokens - }; - previous.tokensTextLength = Math.max( - previous.tokensTextLength, - currentTokensLength - ); - } - return { - ...subtitles, - subtitles: mergedEntries.map(({ line }) => line) - }; - }, - async fetchSubtitles(descriptorOrVideoData, requestLang, spokenLang) { - const descriptor = isSubtitleDescriptor(descriptorOrVideoData) ? descriptorOrVideoData : 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 rawSubtitles = await fetchRawSubtitles(url, format); - const normalized = normalizeFetchedSubtitles(rawSubtitles, descriptor); - const subtitlesWithTokens = processFetchedSubtitles( - normalized, - descriptor - ); - debug.log("[VOT] Processed subtitles:", subtitlesWithTokens); - return subtitlesWithTokens; - } catch (error2) { - console.error("[VOT] Failed to process subtitles:", error2); - 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 response = await Promise.race([ - client.getSubtitles(requestPayload), - timeout(5e3, "Timeout") - ]); - const res = response; - debug.log("[VOT] Subtitles response:", res); - if (res.waiting) { - console.error("[VOT] Failed to get Yandex subtitles"); - } - const yandexSubs = buildYandexSubtitles(res); - const all = [...yandexSubs, ...extraSubtitles]; - return sortSubtitles(all, requestLang); - } catch (error2) { - let message = "Error in getSubtitles function"; - if (error2 instanceof Error && error2.message === "Timeout") { - message = "Failed to get Yandex subtitles: timeout"; - } - console.error(`[VOT] ${message}`, error2); - throw error2; - } - } - }; - const AUDIO_SOURCE_PREFIX = "https://vtrans.s3-private.mds.yandex.net/tts/prod/"; - const AUDIO_PROXY_PATH_PREFIX = "/video-translation/audio-proxy/"; - const SUBTITLE_SOURCE_PREFIX = "https://brosubs.s3-private.mds.yandex.net/vtrans/"; - const SUBTITLE_PROXY_PATH_PREFIX = "/video-subtitles/subtitles-proxy/"; - function getProxyHost(host) { - return host ?? proxyWorkerHost; - } - function isProxyRoutingEnabled(config2) { - return config2.translateProxyEnabled === 2; - } - function proxifyYandexAudioUrl(audioUrl, config2) { - if (!isProxyRoutingEnabled(config2) || !audioUrl.startsWith(AUDIO_SOURCE_PREFIX)) { - return audioUrl; - } - return audioUrl.replace( - AUDIO_SOURCE_PREFIX, - `https://${getProxyHost(config2.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(AUDIO_PROXY_PATH_PREFIX.length).replace(/^\/+/, "")}`; - url.protocol = "https:"; - return url.toString(); - } catch { - return str; - } - } - function isYandexAudioUrlOrProxy(url, config2) { - return url.startsWith(AUDIO_SOURCE_PREFIX) || url.startsWith( - `https://${getProxyHost(config2.proxyWorkerHost)}${AUDIO_PROXY_PATH_PREFIX}` - ); - } - function proxifyYandexSubtitlesUrl(subtitlesUrl, config2) { - if (!isProxyRoutingEnabled(config2) || !subtitlesUrl.startsWith(SUBTITLE_SOURCE_PREFIX)) { - return subtitlesUrl; - } - const subtitlesPath = subtitlesUrl.slice(SUBTITLE_SOURCE_PREFIX.length); - return `https://${getProxyHost(config2.proxyWorkerHost)}${SUBTITLE_PROXY_PATH_PREFIX}${subtitlesPath}`; - } - const DISABLED_SUBTITLES_VALUE = "disabled"; - const VALID_SUBTITLE_FORMATS = new Set([ - "srt", - "vtt", - "ass", - "json" - ]); - const SUBTITLES_INDEX_OPTION_PATTERN = /^\d+$/u; - const subtitlesSelectionRequestVersion = new WeakMap(); - function getCurrentSubtitlesCacheKey(handler) { - const videoData = handler.videoData; - if (!videoData?.videoId) { - return null; - } - return handler.getSubtitlesCacheKey( - videoData.videoId, - videoData.detectedLanguage, - videoData.responseLanguage - ); - } - function isRecord(value) { - return value !== null && typeof value === "object"; - } - function asSubtitleDescriptor(value) { - if (!isRecord(value)) { - return null; - } - const descriptor = value; - const format = descriptor.format; - if (typeof descriptor.source !== "string" || typeof descriptor.language !== "string" || typeof descriptor.url !== "string" || typeof format !== "string" || !VALID_SUBTITLE_FORMATS.has(format)) { - return null; - } - return { - source: descriptor.source, - format, - language: descriptor.language, - url: descriptor.url, - translatedFromLanguage: typeof descriptor.translatedFromLanguage === "string" ? descriptor.translatedFromLanguage : void 0, - isAutoGenerated: typeof descriptor.isAutoGenerated === "boolean" ? descriptor.isAutoGenerated : void 0 - }; - } - function getIndexedSubtitleDescriptors(subtitles) { - const descriptors = []; - for (let index = 0; index < subtitles.length; index += 1) { - const descriptor = asSubtitleDescriptor(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 asSubtitleDescriptor(subtitles[index]); - } - 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 createDisabledSubtitlesOption() { - return { - label: localizationProvider.get("VOTSubtitlesDisabled"), - value: DISABLED_SUBTITLES_VALUE, - selected: true, - disabled: false - }; - } - 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 iterator = selectedValues[Symbol.iterator](); - const first = iterator.next(); - return first.done ? void 0 : first.value; - } - function buildSubtitleLabel(subtitle) { - const languageLabel = localizationProvider.getLangLabel(subtitle.language); - const translatedFromLabel = subtitle.translatedFromLanguage ? ` ${localizationProvider.get("VOTTranslatedFrom")} ${localizationProvider.getLangLabel( - subtitle.translatedFromLanguage - )}` : ""; - const sourceSuffix = subtitle.source === "yandex" ? "" : `, ${globalThis.location.hostname}`; - const autogeneratedSuffix = subtitle.isAutoGenerated ? ` (${localizationProvider.get("VOTAutogenerated")})` : ""; - return `${languageLabel}${translatedFromLabel}${sourceSuffix}${autogeneratedSuffix}`; - } - function normalizeLang(lang2) { - return (lang2 ?? "").toLowerCase(); - } - function baseLang(lang2) { - const normalized = normalizeLang(lang2); - return normalized.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 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 = (s2) => s2.source === "yandex"; - const isAutoGenerated = (s2) => Boolean(s2.isAutoGenerated); - const matchesPair = (s2, wantFrom, wantTo) => { - if (!langMatches(s2.language, wantTo)) return false; - if (fromIsAuto) return true; - return langMatches(s2.translatedFromLanguage, wantFrom); - }; - const isSameLangOriginal = (s2, lang2) => { - if (!langMatches(s2.language, lang2)) return false; - if (!s2.translatedFromLanguage) return true; - return langMatches(s2.translatedFromLanguage, lang2); - }; - const find = (predicate) => subtitles.find(({ descriptor }) => predicate(descriptor))?.index ?? null; - const findOtherTarget = () => { - const otherTargetManual = find( - (s2) => !isYandex(s2) && langMatches(s2.language, to) && !isAutoGenerated(s2) - ); - if (otherTargetManual != null) return otherTargetManual; - return find( - (s2) => !isYandex(s2) && langMatches(s2.language, to) && isAutoGenerated(s2) - ); - }; - const yandexPair = find((s2) => isYandex(s2) && matchesPair(s2, from, to)); - if (yandexPair != null) return yandexPair; - if (!fromIsAuto && fromBase && toBase && fromBase === toBase) { - const nativeManual = find( - (s2) => isSameLangOriginal(s2, to) && !isAutoGenerated(s2) - ); - if (nativeManual != null) return nativeManual; - const nativeAuto = find( - (s2) => isSameLangOriginal(s2, to) && isAutoGenerated(s2) - ); - if (nativeAuto != null) return nativeAuto; - const otherTarget2 = findOtherTarget(); - if (otherTarget2 != null) return otherTarget2; - const yandexTargetSameLang = find( - (s2) => isYandex(s2) && langMatches(s2.language, to) - ); - if (yandexTargetSameLang != null) return yandexTargetSameLang; - } - const yandexTarget = find((s2) => isYandex(s2) && langMatches(s2.language, to)); - if (yandexTarget != null) return yandexTarget; - const otherPair = find((s2) => !isYandex(s2) && matchesPair(s2, from, to)); - if (otherPair != null) return otherPair; - const otherTarget = findOtherTarget(); - if (otherTarget != null) return otherTarget; - return null; - } - async function changeSubtitlesLang(subs) { - const requestVersion = nextSubtitlesSelectionRequestVersion(this); - const overlayView = this.uiManager.votOverlayView; - if (!overlayView?.subtitlesSelect || !overlayView.downloadSubtitlesButton) { - return this; - } - overlayView.subtitlesSelect.setSelectedValue(subs); - if (subs === DISABLED_SUBTITLES_VALUE) { - if (this.hasSubtitlesWidget()) { - this.subtitlesWidget?.setContent(null); - } - overlayView.downloadSubtitlesButton.hidden = true; - this.yandexSubtitles = null; - return this; - } - const subtitlesIndex = parseSubtitlesOptionIndex(subs); - if (subtitlesIndex == null) { - if (this.hasSubtitlesWidget()) { - this.subtitlesWidget?.setContent(null); - } - overlayView.downloadSubtitlesButton.hidden = true; - this.yandexSubtitles = null; - return this; - } - const descriptor = getSubtitleDescriptorAtIndex( - this.subtitles, - subtitlesIndex - ); - if (!descriptor) { - if (this.hasSubtitlesWidget()) { - this.subtitlesWidget?.setContent(null); - } - overlayView.downloadSubtitlesButton.hidden = true; - this.yandexSubtitles = null; - return this; - } - let subtitlesObj = { ...descriptor }; - const proxiedSubtitlesUrl = proxifyYandexSubtitlesUrl(subtitlesObj.url, { - translateProxyEnabled: this.data?.translateProxyEnabled, - proxyWorkerHost: this.data?.proxyWorkerHost - }); - if (proxiedSubtitlesUrl !== subtitlesObj.url) { - subtitlesObj = { - ...subtitlesObj, - url: proxiedSubtitlesUrl - }; - console.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 subtitleDescriptors = getIndexedSubtitleDescriptors(this.subtitles); - const updatedOptions = buildSubtitlesSelectOptions(subtitleDescriptors); - 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; - } - 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 = this.videoData?.responseLanguage ?? this.translateToLang; - const bestIdx = pickBestSubtitlesIndex( - getIndexedSubtitleDescriptors(this.subtitles), - fromLang, - toLang - ); - if (bestIdx == null) { - return this; - } - const currentValue = getSelectedSubtitlesValue( - overlayView.subtitlesSelect.selectedValues - ); - if (currentValue === String(bestIdx)) { - return this; - } - await this.changeSubtitlesLang(String(bestIdx)); - return this; - } - async function toggleSubtitlesForCurrentLangPair() { - const overlayView = this.uiManager.votOverlayView; - if (!overlayView?.subtitlesSelect) return this; - const currentValue = getSelectedSubtitlesValue( - overlayView.subtitlesSelect.selectedValues - ); - if (currentValue && currentValue !== DISABLED_SUBTITLES_VALUE) { - 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 cacheKey = this.getSubtitlesCacheKey( - this.videoData.videoId, - this.videoData.detectedLanguage, - this.videoData.responseLanguage - ); - try { - let cachedSubs = this.cacheManager.getSubtitles(cacheKey); - if (!cachedSubs) { - let inflight = this.subtitlesLoadPromises.get(cacheKey); - if (inflight === void 0) { - inflight = SubtitlesProcessor.getSubtitles( - this.votClient, - this.videoData - ); - 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 (error2) { - console.error("[VOT] Failed to load subtitles:", error2); - this.subtitles = []; - this.subtitlesCacheKey = null; - } - await this.updateSubtitlesLangSelect(); - } - const VOLUME_MIN_01 = 0; - const VOLUME_MAX_01 = 1; - const SMART_DUCKING_DEFAULT_CONFIG = Object.freeze({ - tickMs: 50, - thresholdOnRms: 0.012, - thresholdOffRms: 9e-3, - rmsAttackTauMs: 60, - rmsReleaseTauMs: 240, - holdMs: 520, - attackTauMs: 110, - releaseTauMs: 600, - maxDownPerSec: 3.5, - maxUpPerSec: 0.9, - rmsMissingGraceMs: 200, - maxDtMs: 250, - externalBaselineDelta01: 0.02, - unduckTolerance01: 0.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 resetSmartDuckingRuntime() { - return initSmartDuckingRuntime(); - } - function computeSmartDuckingStep(input, runtime, config2 = SMART_DUCKING_DEFAULT_CONFIG) { - const nextRuntime = normalizeRuntime(runtime); - const volumeOnStart = normalizeVolume01(input.volumeOnStart); - if (!input.translationActive || false) { - return { - kind: "stop", - runtime: nextRuntime, - restoreVolume: nextRuntime.baseline ?? volumeOnStart - }; - } - const now2 = Number.isFinite(input.nowMs) ? input.nowMs : Date.now(); - const prevTickAt = nextRuntime.lastTickAt || now2; - const dtMs = clamp$2(now2 - prevTickAt, 0, config2.maxDtMs); - const dtSec = dtMs / 1e3; - nextRuntime.lastTickAt = now2; - const hasRms = isFiniteNumber(input.rms); - const rmsValue = hasRms ? clamp$2(input.rms, VOLUME_MIN_01, VOLUME_MAX_01) : 0; - const prevEnv = nextRuntime.rmsEnvelope; - const envTauMs = rmsValue > prevEnv ? config2.rmsAttackTauMs : config2.rmsReleaseTauMs; - const envAlpha = envTauMs > 0 ? -Math.expm1(-dtMs / envTauMs) : 1; - nextRuntime.rmsEnvelope = clamp$2( - prevEnv + (rmsValue - prevEnv) * envAlpha, - VOLUME_MIN_01, - VOLUME_MAX_01 - ); - let gateOpen = nextRuntime.speechGateOpen; - if (input.audioIsPlaying && !hasRms) { - nextRuntime.rmsMissingSinceAt ??= now2; - if (gateOpen) { - nextRuntime.lastSoundAt = now2; - } - if (nextRuntime.rmsMissingSinceAt !== null && now2 - nextRuntime.rmsMissingSinceAt >= config2.rmsMissingGraceMs) { - gateOpen = true; - nextRuntime.lastSoundAt = now2; - } - } else { - nextRuntime.rmsMissingSinceAt = null; - if (!gateOpen) { - if (input.audioIsPlaying && nextRuntime.rmsEnvelope >= config2.thresholdOnRms) { - gateOpen = true; - nextRuntime.lastSoundAt = now2; - } - } else if (input.audioIsPlaying && nextRuntime.rmsEnvelope >= config2.thresholdOffRms) { - nextRuntime.lastSoundAt = now2; - } else if (now2 - nextRuntime.lastSoundAt > config2.holdMs) { - gateOpen = false; - } - } - nextRuntime.speechGateOpen = gateOpen; - const currentVideoVolume = normalizeVolume01(input.currentVideoVolume); - if (!isFiniteNumber(currentVideoVolume)) { - return { kind: "noop", runtime: nextRuntime }; - } - if (nextRuntime.isDucked && isFiniteNumber(nextRuntime.lastApplied) && Math.abs(currentVideoVolume - nextRuntime.lastApplied) > config2.externalBaselineDelta01) { - nextRuntime.baseline = currentVideoVolume; - } - if (!nextRuntime.isDucked) { - nextRuntime.baseline = currentVideoVolume; - } - const baseline = nextRuntime.baseline ?? volumeOnStart ?? currentVideoVolume; - nextRuntime.baseline = baseline; - if (!input.hostVideoActive) { - nextRuntime.lastApplied = currentVideoVolume; - return { kind: "noop", runtime: nextRuntime }; - } - const duckingTarget01 = normalizeVolume01(input.duckingTarget01) ?? baseline; - const duckedTarget = Math.min(baseline, duckingTarget01); - let desired = baseline; - if (gateOpen) { - if (!nextRuntime.isDucked) { - nextRuntime.isDucked = true; - } - desired = duckedTarget; - } else if (nextRuntime.isDucked && Math.abs(baseline - currentVideoVolume) < config2.unduckTolerance01) { - nextRuntime.isDucked = false; - } - const smoothingTauMs = desired < currentVideoVolume ? config2.attackTauMs : config2.releaseTauMs; - const smoothingAlpha = smoothingTauMs > 0 ? -Math.expm1(-dtMs / smoothingTauMs) : 1; - let nextVolume = currentVideoVolume + (desired - currentVideoVolume) * smoothingAlpha; - const maxDelta = (desired < currentVideoVolume ? config2.maxDownPerSec : config2.maxUpPerSec) * dtSec; - if (maxDelta > 0) { - nextVolume = clamp$2( - nextVolume, - currentVideoVolume - maxDelta, - currentVideoVolume + maxDelta - ); - } - nextVolume = clamp$2(nextVolume, VOLUME_MIN_01, VOLUME_MAX_01); - const quantized = snapVolume01Towards( - nextVolume, - currentVideoVolume, - desired, - config2.volumeStep01 - ); - const applyDeltaThreshold01 = config2.applyDeltaThreshold01; - if (Math.abs(quantized - currentVideoVolume) < applyDeltaThreshold01) { - nextRuntime.lastApplied = quantized; - return { kind: "noop", runtime: nextRuntime }; - } - if (!isFiniteNumber(nextRuntime.lastApplied) || Math.abs(quantized - nextRuntime.lastApplied) >= applyDeltaThreshold01) { - nextRuntime.lastApplied = quantized; - return { - kind: "apply", - runtime: nextRuntime, - volume01: quantized - }; - } - return { kind: "noop", runtime: nextRuntime }; - } - 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$2(value, VOLUME_MIN_01, VOLUME_MAX_01); - } - function isFiniteNumber(value) { - return typeof value === "number" && Number.isFinite(value); - } - const SMART_DUCKING_TICK_MS = SMART_DUCKING_DEFAULT_CONFIG.tickMs; - const AUDIO_PROBE_TIMEOUT_MS = 5e3; - const AUDIO_PROBE_RETRY_DELAY_MS = 150; - const AUDIO_PROBE_MAX_ATTEMPTS = 2; - const smartDuckingAnalyserState = 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(player2) { - return player2?.audio ?? player2?.audioElement; - } - function getNowMs() { - return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now(); - } - function getAutoVolumeMode(handler) { - if (handler.data?.syncVolume || !handler.data?.enabledAutoVolume) { - return "off"; - } - return handler.data?.enabledSmartDucking ?? true ? "smart" : "classic"; - } - 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) { - 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); - } - return result; - } - async function rollbackStaleAppliedSourceIfStillCurrent(handler, appliedSourceUrl) { - if (!appliedSourceUrl || !handler.audioPlayer) return; - const player2 = handler.audioPlayer.player; - const currentSource = String(player2.currentSrc || player2.src || ""); - const normalizedCurrentUrl = handler.proxifyAudio( - handler.unproxifyAudio(currentSource) - ); - const normalizedAppliedUrl = handler.proxifyAudio( - handler.unproxifyAudio(appliedSourceUrl) - ); - if (normalizedCurrentUrl !== normalizedAppliedUrl) return; - try { - await player2.clear(); - player2.src = ""; - debug.log("[updateTranslation] cleared stale partially-applied source"); - } catch (err) { - } - } - 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(player2, media, audioContext, state) { - if (isAudioNode(player2?.gainNode)) return player2.gainNode; - if (isAudioNode(player2?.audioSource)) return player2.audioSource; - if (isAudioNode(player2?.mediaElementSource)) return player2.mediaElementSource; - if (state.mediaSourceCreationFailed && state.mediaElement === media && state.audioContext === audioContext) { - return void 0; - } - 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; - return void 0; - } - } - function ensureSmartDuckingAnalyser(handler, player2, 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 analyser2 = audioContext.createAnalyser(); - analyser2.fftSize = 512; - state.analyser = analyser2; - } - const inputNode = resolveSmartDuckingInputNode( - player2, - 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 { - inputNode.connect(analyser); - state.connectedInputNode = inputNode; - } catch (err) { - return void 0; - } - } - 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 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 { - } - } - releaseSmartDuckingAnalyser(handler); - writeSmartDuckingRuntime(handler, resetSmartDuckingRuntime()); - } - 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) { - 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 now2 = getNowMs(); - runtime.isDucked = true; - runtime.speechGateOpen = true; - runtime.lastApplied = currentVideoVolume; - runtime.lastSoundAt = now2; - } - writeSmartDuckingRuntime(handler, runtime); - handler.smartVolumeDuckingInterval = globalThis.setTimeout(() => { - }, 0); - clearTimeout(handler.smartVolumeDuckingInterval); - scheduleNextSmartDuckingTick(handler); - } - function getTranslatedAudioRms(handler, media) { - const player2 = handler.audioPlayer?.player; - const analyserBundle = ensureSmartDuckingAnalyser(handler, player2, 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 sum2 = 0; - for (const value of floatData) { - sum2 += value * value; - } - return clamp$2(Math.sqrt(sum2 / 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$2(Math.sqrt(sum / data.length), 0, 1); - } catch { - return void 0; - } - } - function smartDuckingTick(handler) { - if (getAutoVolumeMode(handler) !== "smart") { - setupAudioSettings.call(handler); - return; - } - const player2 = handler.audioPlayer?.player; - const media = getPlayerMediaElement(player2); - const audioIsPlaying = !!media && !media.paused && !media.muted && -(media.volume ?? 1) > 1e-3; - const now2 = getNowMs(); - const currentVideoVolume = handler.getVideoVolume(); - const hostVideo = handler.video; - const hostVideoActive = !(hostVideo && (hostVideo.paused || hostVideo.ended)); - const dynamicDuckingTarget = clamp$2(handler.data?.autoVolume ?? defaultAutoVolume, 0, 100) / 100; - handler.smartVolumeDuckingTarget = dynamicDuckingTarget; - const rms = audioIsPlaying && media ? getTranslatedAudioRms(handler, media) : 0; - const decision = computeSmartDuckingStep( - { - nowMs: now2, - translationActive: handler.hasActiveSource(), - 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); - writeSmartDuckingRuntime(handler, decision.runtime); - return; - case "noop": - writeSmartDuckingRuntime(handler, decision.runtime); - return; - default: - throw new TypeError("Unhandled smart ducking decision"); - } - } - function waitForProbeRetry(delayMs, signal) { - if (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 = handler.isMultiMethodS3(audioUrl) ? { - method: "HEAD", - signal, - timeout: AUDIO_PROBE_TIMEOUT_MS - } : { -headers: { - range: "bytes=0-0" - }, - signal, - timeout: AUDIO_PROBE_TIMEOUT_MS - }; - for (let attempt = 1; attempt <= AUDIO_PROBE_MAX_ATTEMPTS; attempt++) { - if (handler.isActionStale(actionContext)) return false; - try { - const response = await GM_fetch(audioUrl, fetchOpts); - if (handler.isActionStale(actionContext)) return false; - debug.log("[validateAudioUrl] probe response", { - audioUrl, - attempt, - ok: response.ok, - status: response.status - }); - if (response.ok) return true; - } catch (err) { - if (handler.isActionStale(actionContext) || signal.aborted) { - return false; - } - } - if (attempt < AUDIO_PROBE_MAX_ATTEMPTS) { - if (handler.isActionStale(actionContext) || signal.aborted) { - return false; - } - await waitForProbeRetry(AUDIO_PROBE_RETRY_DELAY_MS, signal); - if (handler.isActionStale(actionContext) || signal.aborted) { - return false; - } - } - } - return false; - } - async function validateAudioUrl(audioUrl, actionContext) { - if (this.isActionStale(actionContext)) return audioUrl; - const isPrimaryUrlValid = await probeAudioUrl(this, audioUrl, actionContext); - if (isPrimaryUrlValid) { - return audioUrl; - } - const directUrl = this.unproxifyAudio(audioUrl); - if (directUrl !== audioUrl) { - const isDirectUrlValid = await probeAudioUrl( - this, - directUrl, - actionContext - ); - if (isDirectUrlValid) { - return directUrl; - } - } - return audioUrl; - } - function scheduleTranslationRefresh() { - if (!this.videoData || this.videoData.isStream) { - return; - } - if (!this.hasActiveSource()) return; - clearTimeout(this.translationRefreshTimeout); - const refreshDelayMs = Math.max(3e4, YANDEX_TTL_MS - 5 * 60 * 1e3); - this.translationRefreshTimeout = setTimeout(() => { - this.refreshTranslationAudio().catch((error2) => { - }); - }, refreshDelayMs); - } - 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), - scheduleTranslationRefresh: () => self.scheduleTranslationRefresh() - }); - 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.downloadTranslationUrl, - 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 { - const translateRes = 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 - }); - if (!translateRes) return; - } finally { - this.isRefreshingTranslation = false; - } - } - function proxifyAudio(audioUrl) { - const proxiedAudioUrl = proxifyYandexAudioUrl(audioUrl, { - translateProxyEnabled: this.data?.translateProxyEnabled, - proxyWorkerHost: this.data?.proxyWorkerHost - }); - return proxiedAudioUrl; - } - function unproxifyAudio(audioUrl) { - return unproxifyYandexAudioUrl(audioUrl); - } - async function handleProxySettingsChanged(reason = "proxySettingsChanged") { - 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 (error2) { - return { - status: "error", - didSetSource, - appliedSourceUrl, - error: error2 - }; - } - } - async function updateTranslation(audioUrl, actionContext) { - await this.waitForPendingStopTranslate(); - if (this.isActionStale(actionContext)) return; - if (!this.audioPlayer) { - this.createPlayer(); - } - if (this.audioPlayer.audioContext?.state === "closed") { - this.createPlayer(); - } - const normalizedTargetUrl = normalizeManagedAudioUrl(this, audioUrl); - const currentSource = this.audioPlayer.player.currentSrc || this.audioPlayer.player.src || ""; - const normalizedCurrentUrl = normalizeManagedAudioUrl(this, currentSource); - let nextAudioUrl = normalizedTargetUrl; - if (normalizedTargetUrl !== normalizedCurrentUrl) { - nextAudioUrl = await this.validateAudioUrl( - normalizedTargetUrl, - actionContext - ); - } - if (this.isActionStale(actionContext)) return; - let applyResult = await applyTranslationSource( - this, - nextAudioUrl, - actionContext - ); - let appliedSourceUrl = applyResult.appliedSourceUrl; - if (applyResult.status === "error" && applyResult.didSetSource && !this.isActionStale(actionContext)) { - const directUrl = this.unproxifyAudio(nextAudioUrl); - if (directUrl !== nextAudioUrl) { - try { - debug.log( - "[updateTranslation] proxied audio init failed, retrying direct URL" - ); - const validatedDirectUrl = await this.validateAudioUrl( - directUrl, - actionContext - ); - if (this.isActionStale(actionContext)) { - await rollbackStaleAppliedSourceIfStillCurrent( - this, - appliedSourceUrl - ); - return; - } - nextAudioUrl = validatedDirectUrl; - applyResult = await applyTranslationSource( - this, - validatedDirectUrl, - actionContext - ); - appliedSourceUrl = applyResult.appliedSourceUrl; - } catch (fallbackErr) { - applyResult = { - status: "error", - didSetSource: true, - appliedSourceUrl, - error: fallbackErr - }; - } - } - } - 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.setupAudioSettings(); - this.transformBtn("success", localizationProvider.get("disableTranslate")); - this.afterUpdateTranslation(nextAudioUrl); - } - async function translateFunc(VIDEO_ID, _isStream, requestLang, responseLang, translationHelp) { - await this.waitForPendingStopTranslate(); - await this.videoValidator(); - if (this.actionsAbortController?.signal?.aborted) { - this.resetActionsAbortController("translateFunc"); - } - const overlayView = this.uiManager.votOverlayView; - if (!overlayView?.votButton) { - return; - } - overlayView.votButton.loading = true; - this.hadAsyncWait = false; - this.volumeOnStart = this.getVideoVolume(); - if (!VIDEO_ID) { - 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) { - await this.activeTranslation.promise; - return; - } - const actionContext = { - gen: this.actionsGeneration, - videoId: VIDEO_ID - }; - const translationPromise = (async () => { - if (this.isActionStale(actionContext)) { - return; - } - const reqLang = requestLang; - const resLang = responseLang; - const applyTranslationUrl = async (url) => await updateTranslationAndSchedule({ - url, - actionContext, - isActionStale: (ctx) => this.isActionStale(ctx), - updateTranslation: (nextUrl, ctx) => this.updateTranslation(nextUrl, ctx), - scheduleTranslationRefresh: () => this.scheduleTranslationRefresh() - }); - const cachedEntry = this.cacheManager.getTranslation(cacheKey); - if (cachedEntry?.url) { - const updated = await applyTranslationUrl(cachedEntry.url); - if (!updated) return; - 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 subsCacheKey = this.videoData ? this.getSubtitlesCacheKey( - VIDEO_ID, - this.videoData.detectedLanguage, - this.videoData.responseLanguage - ) : null; - const cachedSubs = subsCacheKey ? this.cacheManager.getSubtitles(subsCacheKey) : null; - if (!cachedSubs?.some( - (item) => item.source === "yandex" && item.translatedFromLanguage === videoData.detectedLanguage && item.language === videoData.responseLanguage - )) { - if (subsCacheKey) this.cacheManager.deleteSubtitles(subsCacheKey); - this.subtitles = []; - this.subtitlesCacheKey = null; - } - } - }); - if (!translateRes) { - return; - } - })(); - 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()) { - this.transformBtn("none", localizationProvider.get("translateVideo")); - } - } - } - function isYouTubeHosts() { - return isTranslationDownloadHost(this.site.host); - } - function setupAudioSettings() { - if (typeof this.data?.defaultVolume === "number") { - this.audioPlayer.player.volume = this.data.defaultVolume / 100; - } - const autoVolumeMode = getAutoVolumeMode(this); - if (autoVolumeMode === "off") { - stopSmartVolumeDucking(this, { - restoreVolume: this.smartVolumeDuckingBaseline ?? this.volumeOnStart - }); - return; - } - const targetVolume = clamp$2(this.data.autoVolume ?? defaultAutoVolume, 0, 100) / 100; - this.smartVolumeDuckingTarget = targetVolume; - if (!this.hasActiveSource()) { - return; - } - 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(); - this.setVideoVolume(Math.min(baseline, targetVolume)); - writeSmartDuckingRuntime( - this, - initSmartDuckingRuntime(this.smartVolumeDuckingBaseline) - ); - this.smartVolumeIsDucked = true; - } - const RESPONSE_LANG_SET = new Set(availableTTS); - const isResponseLang = (value) => RESPONSE_LANG_SET.has(value); - const RESOLVED_VOID_PROMISE = Promise.resolve(); - class VideoHandler { - video; - container; - site; -translateFromLang = "auto"; - translateToLang = calculatedResLang; - data; - videoData; - firstPlay = true; - audioContext; - votClient; - audioPlayer; - abortController; - actionsAbortController; -actionsGeneration = 0; - notifier = new Notifier(); - cacheManager; - votSessionStorage = new VOTSessionStorageCache(); -subtitlesLoadPromises = new Map(); - downloadTranslationUrl = null; - translationRefreshTimeout; - isRefreshingTranslation = false; - autoRetry; -votOpts; - volumeOnStart; -volumeLinkState = { - initialized: false, - lastVideoPercent: 0, - lastTranslationPercent: 0 - }; -internalVideoVolumeSetAt = 0; - internalVideoVolumeSetPercent = null; - internalVideoVolumeSuppressionMs = 250; - internalVideoVolumeSetHistory = []; - internalVideoVolumeSetHistoryLimit = 48; - -smartVolumeDuckingInterval; - smartVolumeDuckingTarget = 0.2; - smartVolumeDuckingBaseline; - smartVolumeLastApplied; -smartVolumeLastTickAt = 0; - smartVolumeLastSoundAt = 0; - smartVolumeRmsMissingSinceAt = null; -smartVolumeRmsEnvelope = 0; -smartVolumeSpeechGateOpen = false; - smartVolumeIsDucked = false; - longWaitingResCount = 0; - hadAsyncWait = false; - -subtitles = []; - subtitlesCacheKey = null; - subtitlesWidget; - activeTranslation = null; -stopTranslatePromise = null; -interactionChecker; - uiManager; - overlayVisibility; - overlayVisibilityTargetsAbortController; - translationOrchestrator; - lifecycleController; - translationHandler; - videoManager; -yandexSubtitles = null; -resizeObserver; - syncVolumeObserver; -initialized = false; -mountCache; -errorTranslationCache = new Map(); -getFullscreenOverlayRoot() { - const doc = document; - const fullscreenEl = doc.fullscreenElement ?? doc.webkitFullscreenElement; - return resolveScopedFullscreenElement(fullscreenEl, [this.container]); - } - 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, fullscreenRoot } = this.getOverlayMountPoints(container); - return { - root, - portalContainer, - subtitlesMountContainer, - tooltipLayoutRoot: fullscreenRoot ?? this.tooltipLayoutRoot - }; - } -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); - const helpHash = helpStr ? fnv1a32ToKeyPart(helpStr) : "0"; - return `${videoId}_${requestLangForApi}_${to}_${useLivelyVoice}_${helpHash}`; - } -getSubtitlesCacheKey(videoId, detectedLanguage, responseLanguage) { - return `${videoId}_${detectedLanguage}_${responseLanguage}_${Boolean(this.data?.useLivelyVoice)}`; - } - 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(); - } -constructor(video, container, site) { - 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(); - const self = () => this; - const mount = this.getOverlayMount(container); - this.uiManager = new UIManager({ - mount, - 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(); - } - }); - } - }); - const lifecycleHost = { - 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(self().getOverlayMount(value)); - }, - get firstPlay() { - return self().firstPlay; - }, - set firstPlay(value) { - self().firstPlay = value; - }, - stopTranslation: () => this.stopTranslation(), - get uiManager() { - return self().uiManager; - }, - getVideoData: () => this.getVideoData(), - cacheManager: { - getSubtitles: (key) => self().cacheManager.getSubtitles(key) - }, - getSubtitlesCacheKey: (videoId, detectedLanguage, responseLanguage) => this.getSubtitlesCacheKey(videoId, detectedLanguage, responseLanguage), - updateSubtitlesLangSelect: () => this.updateSubtitlesLangSelect(), - enableSubtitlesForCurrentLangPair: () => this.enableSubtitlesForCurrentLangPair(), - setSelectMenuValues: (from, to) => this.setSelectMenuValues(from, to), - get translateToLang() { - return self().translateToLang; - }, - set translateToLang(value) { - if (isResponseLang(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) { - self().videoData = value; - }, - get actionsAbortController() { - return self().actionsAbortController; - }, - set actionsAbortController(value) { - self().actionsAbortController = value; - }, - resetActionsAbortController: (reason) => this.resetActionsAbortController(reason), - initVOTClient: () => this.initVOTClient(), - translationOrchestrator: this.translationOrchestrator, - resetSubtitlesWidget: () => this.resetSubtitlesWidget(), - queueOverlayAutoHide: () => this.overlayVisibility?.queueAutoHide() - }; - this.lifecycleController = new VideoLifecycleController(lifecycleHost); - this.translationHandler = new VOTTranslationHandler(this); - this.videoManager = new VOTVideoManager(this); - } -getSubtitlesWidget() { - if (!this.subtitlesWidget) { - const { subtitlesMountContainer } = this.getOverlayMountPoints(); - this.subtitlesWidget = new SubtitlesWidget( - this.video, - subtitlesMountContainer, - this.interactionChecker, - this.tooltipLayoutRoot - ); - if (this.data) { - this.subtitlesWidget.setSmartLayout( - typeof this.data.subtitlesSmartLayout === "boolean" ? this.data.subtitlesSmartLayout : true - ); - if (typeof this.data.subtitlesMaxLength === "number") { - this.subtitlesWidget.setMaxLength(this.data.subtitlesMaxLength); - } - if (typeof this.data.highlightWords === "boolean") { - this.subtitlesWidget.setHighlightWords(this.data.highlightWords); - } - if (typeof this.data.subtitlesFontSize === "number") { - this.subtitlesWidget.setFontSize(this.data.subtitlesFontSize); - } - if (typeof this.data.subtitlesFontFamily === "string") { - this.subtitlesWidget.setFontFamily( - this.data.subtitlesFontFamily - ); - } - if (typeof this.data.subtitlesOpacity === "number") { - this.subtitlesWidget.setOpacity(this.data.subtitlesOpacity); - } - } - } - return this.subtitlesWidget; - } -hasSubtitlesWidget() { - return Boolean(this.subtitlesWidget); - } - resetSubtitlesWidget() { - if (this.hasSubtitlesWidget()) { - this.subtitlesWidget?.release(); - this.subtitlesWidget = void 0; - } - } -get uiRoot() { - return this.getOverlayMountPoints().root; - } -get portalContainer() { - return this.getOverlayMountPoints().portalContainer; - } -get tooltipLayoutRoot() { - switch (this.site.host) { - case "kickstarter": { - return document.getElementById("react-project-header") ?? void 0; - } - case "custom": { - return void 0; - } - default: { - return this.container; - } - } - } -getEventContainer() { - if (!this.site.eventSelector) return this.container; - return document.querySelector(this.site.eventSelector) ?? this.container; - } -async runAutoTranslate() { - await this.videoManager.videoValidator(); - await this.uiManager.handleTranslationBtnClick(); - } -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 void 0; - } - } - get isAudioContextSupported() { - return globalThis.AudioContext !== void 0 || globalThis.webkitAudioContext !== void 0; - } -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; - } -createPlayer() { - const preferAudio = this.getPreferAudio(); - this.audioPlayer = new Chaimu({ - video: this.video, -debug: Boolean(false), - fetchFn: GM_fetch, - fetchOpts: { - timeout: 0 - }, - preferAudio - }); - return this; - } -isLikelyInternalVideoVolumeChange(observedPercent) { - const now2 = Date.now(); - const history = this.internalVideoVolumeSetHistory; - if (history.length > 0) { - let writeIndex = 0; - let matchFound = false; - for (const entry of history) { - if (now2 - 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; - const ageMs = now2 - this.internalVideoVolumeSetAt; - if (ageMs > this.internalVideoVolumeSuppressionMs) return false; - return Math.abs(observedPercent - this.internalVideoVolumeSetPercent) <= 1; - } - callModule(impl, ...args) { - return impl.call(this, ...args); - } - callModuleAsync(impl, ...args) { - return impl.call(this, ...args); - } -init() { - return init.call(this); - } -async initVOTClient() { - const transportHost = this.data?.translateProxyEnabled ? this.data?.proxyWorkerHost ?? proxyWorkerHost : workerHost; - this.votOpts = { - fetchFn: GM_fetch, - fetchOpts: { - signal: this.actionsAbortController.signal - }, - apiToken: this.data?.account?.token, - hostVOT: votBackendUrl, - host: transportHost - }; - this.votClient = new (this.data?.translateProxyEnabled ? VOTWorkerClient2 : VOTClient2)(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; - } -transformBtn(status, text) { - this.uiManager.transformBtn(status, text); - return this; - } -hasActiveSource() { - return !!this.audioPlayer?.player?.src; - } -initExtraEvents() { - return this.callModule(initExtraEvents); - } -refreshOverlayMount() { - this.mountCache = void 0; - const nextMount = this.getOverlayMount(this.container); - const mountChanged = !isSameOverlayMount(this.uiManager.mount, nextMount); - this.uiManager.updateMount(nextMount); - if (!mountChanged) { - return; - } - this.rebindOverlayVisibilityTargets(); - } -rebindOverlayVisibilityTargets = rebindOverlayVisibilityTargets; -setCanPlay() { - return this.lifecycleController.setCanPlay(); - } - isOverlayInteractiveNode(node) { - return this.callModule(isOverlayInteractiveNode, node); - } -getAutoHideDelay() { - return this.callModule(getAutoHideDelay); - } -changeSubtitlesLang = changeSubtitlesLang; -updateSubtitlesLangSelect = updateSubtitlesLangSelect; -ensureSubtitlesForCurrentLangPair = ensureSubtitlesForCurrentLangPair; -loadSubtitles = loadSubtitles; -enableSubtitlesForCurrentLangPair() { - return this.callModuleAsync(enableSubtitlesForCurrentLangPair); - } -toggleSubtitlesForCurrentLangPair() { - return this.callModuleAsync(toggleSubtitlesForCurrentLangPair); - } - 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) { - const requestLangForApi = this.getRequestLangForTranslation( - requestLang, - responseLang - ); - if (requestLangForApi !== "en" || responseLang !== "ru") { - return false; - } - if (!this.data?.account?.token) { - return false; - } - return true; - } -getVideoVolume() { - return this.videoManager.getVideoVolume(); - } -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 now2 = Date.now(); - const percent = volume01ToPercent(snapped); - this.internalVideoVolumeSetAt = now2; - this.internalVideoVolumeSetPercent = percent; - this.internalVideoVolumeSetHistory.push({ - at: now2, - percent, - suppressMs: suppressSyncMs - }); - if (this.internalVideoVolumeSetHistory.length > this.internalVideoVolumeSetHistoryLimit) { - this.internalVideoVolumeSetHistory.splice( - 0, - this.internalVideoVolumeSetHistory.length - this.internalVideoVolumeSetHistoryLimit - ); - } - this.videoManager.setVideoVolume(snapped); - return this; - } -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); - } -onTranslationVolumeSliderSynced(volumePercent) { - if (!this.volumeLinkState.initialized) { - syncTranslationLinkSnapshot(this.volumeLinkState, volumePercent); - return; - } - syncTranslationLinkSnapshot(this.volumeLinkState, volumePercent); - } -resetVolumeLinkState(videoPercent, translationPercent) { - syncVideoLinkSnapshot(this.volumeLinkState, videoPercent); - syncTranslationLinkSnapshot(this.volumeLinkState, translationPercent); - this.volumeLinkState.initialized = true; - } -isMuted() { - return this.videoManager.isMuted(); - } -syncVideoVolumeSlider() { - this.videoManager.syncVideoVolumeSlider(); - } -setSelectMenuValues(from, to) { - this.videoManager.setSelectMenuValues( - from, - to - ); - } -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 { nextVideo, nextTranslation } = applyVolumeLinkDelta({ - state: this.volumeLinkState, - fromType, - newVolume, - currentVideo: Number(videoSlider.value), - currentTranslation: Number(translationSlider.value), - translationMin: translationSlider.min, - translationMax: translationSlider.max - }); - if (typeof nextTranslation === "number") { - translationSlider.value = nextTranslation; - if (this.audioPlayer?.player) { - this.audioPlayer.player.volume = nextTranslation / 100; - } - return; - } - if (typeof nextVideo === "number") { - videoSlider.value = nextVideo; - this.setVideoVolume(nextVideo / 100); - } - } -getVideoData() { - return this.videoManager.getVideoData(); - } -videoValidator() { - return this.videoManager.videoValidator(); - } -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("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.downloadTranslationUrl = 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; - if (this.autoRetry !== void 0) { - clearTimeout(this.autoRetry); - this.autoRetry = void 0; - } - if (this.translationRefreshTimeout !== void 0) { - clearTimeout(this.translationRefreshTimeout); - this.translationRefreshTimeout = 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; - } -async updateTranslationErrorMsg(errorMessage, signal) { - if (signal?.aborted) { - return; - } - const translationTake2 = localizationProvider.get("translationTake"); - const lang2 = localizationProvider.lang; - this.longWaitingResCount = errorMessage === localizationProvider.get("translationTakeAboutMinute") ? this.longWaitingResCount + 1 : 0; - debug.log("longWaitingResCount", this.longWaitingResCount); - if (this.longWaitingResCount > minLongWaitingCount) { - errorMessage = new VOTLocalizedError("TranslationDelayed"); - } - if (errorMessage?.name === "VOTLocalizedError") { - this.transformBtn("error", errorMessage.localizedMessage); - } else if (errorMessage instanceof Error) { - this.transformBtn("error", errorMessage?.message); - } else if (this.data?.translateAPIErrors && lang2 !== "ru" && !errorMessage?.includes(translationTake2)) { - const overlayView = this.uiManager.votOverlayView; - if (!overlayView?.votButton) { - return; - } - const messageStr = Array.isArray(errorMessage) ? errorMessage.join(" ") : String(errorMessage); - const cacheKey = `${lang2}:${messageStr}`; - const cached = this.errorTranslationCache.get(cacheKey); - if (cached) { - this.transformBtn("error", cached); - } else { - overlayView.votButton.loading = true; - const translatedMessage = await translate(messageStr, "ru", lang2); - const translatedText = Array.isArray(translatedMessage) ? translatedMessage.join("\n") : String(translatedMessage); - if (signal?.aborted) { - return; - } - this.errorTranslationCache.set(cacheKey, translatedText); - if (this.errorTranslationCache.size > 50) { - const oldestKey = this.errorTranslationCache.keys().next().value; - if (oldestKey) this.errorTranslationCache.delete(oldestKey); - } - this.transformBtn("error", translatedText); - } - if (signal?.aborted) { - return; - } - } else { - const msg = Array.isArray(errorMessage) ? errorMessage.join("\n") : String(errorMessage ?? ""); - this.transformBtn("error", msg); - } - if (signal?.aborted) { - return; - } - if ([ - "Подготавливаем перевод", - "Видео передано в обработку", - "Ожидаем перевод видео", - "Загружаем переведенное аудио" - ].includes(errorMessage)) { - if (this.uiManager.votOverlayView?.votButton) { - this.uiManager.votOverlayView.votButton.loading = true; - } - } - } -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.downloadTranslationUrl = audioUrl; - } - debug.log( - "afterUpdateTranslation downloadTranslationUrl", - this.downloadTranslationUrl - ); - if (this.data?.sendNotifyOnComplete && this.hadAsyncWait && isSuccess) { - this.notifier.translationCompleted(globalThis.location.hostname); - this.hadAsyncWait = false; - } - } -validateAudioUrl(audioUrl, actionContext) { - return this.callModuleAsync(validateAudioUrl, audioUrl, actionContext); - } - scheduleTranslationRefresh() { - this.callModule(scheduleTranslationRefresh); - } - refreshTranslationAudio = refreshTranslationAudio; -proxifyAudio(audioUrl) { - return this.callModule(proxifyAudio, audioUrl); - } -unproxifyAudio(audioUrl) { - return this.callModule(unproxifyAudio, audioUrl); - } -handleProxySettingsChanged = handleProxySettingsChanged; - isMultiMethodS3(url) { - return this.callModule(isMultiMethodS3, url); - } -updateTranslation = updateTranslation; -translateFunc(VIDEO_ID, isStream, requestLang, responseLang, translationHelp) { - return translateFunc.call( - this, - VIDEO_ID, - isStream, - requestLang, - responseLang, - translationHelp - ); - } -isYouTubeHosts() { - return this.callModule(isYouTubeHosts); - } -setupAudioSettings() { - return this.callModule(setupAudioSettings); - } -stopTranslation = async () => { - this.translationOrchestrator?.reset(); - this.overlayVisibility?.cancel(); - await this.stopTranslate(); - this.syncVideoVolumeSlider(); - }; -handleSrcChanged() { - return this.lifecycleController.handleSrcChanged(); - } -async release() { - this.initialized = false; - try { - await this.stopTranslation(); - } catch (err) { - } - this.lifecycleController?.teardown(); - this.abortController?.abort(); - this.abortController = new AbortController(); - this.overlayVisibility?.release(); - this.releaseExtraEvents(); - if (this.hasSubtitlesWidget()) { - this.subtitlesWidget?.release(); - this.subtitlesWidget = void 0; - } - this.interactionChecker?.destroy(); - this.uiManager.release(); - } -collectReportInfo() { - const info2 = getEnvironmentInfo(); - const detectedLanguage = this.videoData?.detectedLanguage ?? "unknown"; - const responseLanguage = this.videoData?.responseLanguage ?? "unknown"; - const additionalInfo = `
+ 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(authServerUrl, "_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); + }); + this.subtitlesMaxLengthSlider.addEventListener("input", (value) => { + this.subtitlesMaxLengthSliderLabel.value = value; + if ((this.data.subtitlesSmartLayout ?? true) === true) this.setSubtitlesSmartLayout(false); + this.data.subtitlesMaxLength = value; + this.scheduleStoragePersist("subtitlesMaxLength", this.data.subtitlesMaxLength); + debug.log("subtitlesMaxLength value changed. New value:", value); + this.events["input:subtitlesMaxLength"].dispatch(value); + }); + this.subtitlesFontSizeSlider.addEventListener("input", (value) => { + this.subtitlesFontSizeSliderLabel.value = value; + if ((this.data.subtitlesSmartLayout ?? true) === true) this.setSubtitlesSmartLayout(false); + this.data.subtitlesFontSize = value; + this.scheduleStoragePersist("subtitlesFontSize", this.data.subtitlesFontSize); + debug.log("subtitlesFontSize value changed. New value:", value); + this.events["input:subtitlesFontSize"].dispatch(value); + }); + this.subtitlesBackgroundOpacitySlider.addEventListener("input", (value) => { + this.subtitlesBackgroundOpacitySliderLabel.value = value; + this.data.subtitlesOpacity = value; + this.scheduleStoragePersist("subtitlesOpacity", this.data.subtitlesOpacity); + debug.log("subtitlesOpacity value changed. New value:", 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.kload.workers.dev"; + 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.autoHideButtonDelaySlider.addEventListener("input", (value) => { + this.autoHideButtonDelaySliderLabel.value = value; + const newDelay = Math.round(value * 1e3); + debug.log("autoHideButtonDelay value changed. New value:", newDelay); + this.data.autoHideButtonDelay = newDelay; + this.scheduleStoragePersist("autoHideButtonDelay", this.data.autoHideButtonDelay); + 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; + /** + * 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; + } + get tooltipLayoutRoot() { + return this.mount.tooltipLayoutRoot; + } + isInitialized() { + return this.initialized; + } + initUI() { + if (this.isInitialized()) throw new Error("[VOT] UIManager is already initialized"); + this.initialized = true; + this.votGlobalPortal = UI.createPortal(); + this.getGlobalPortalHost(this.mount).appendChild(this.votGlobalPortal); + 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(); + return this; + } + updateMount(mount) { + const globalPortalHost = this.getGlobalPortalHost(mount); + if (this.votGlobalPortal?.parentElement !== globalPortalHost) globalPortalHost.appendChild(this.votGlobalPortal); + this.mount = applyOverlayMountUpdate(this.mount, mount, (nextMount) => { + this.votOverlayView?.updateMount(nextMount); + }); + this.videoHandler?.subtitlesWidget?.updateMount({ + container: mount.subtitlesMountContainer, + tooltipLayoutRoot: mount.tooltipLayoutRoot + }); + return this; + } + getGlobalPortalHost(mount) { + const doc = document; + const fullscreenEl = doc.fullscreenElement ?? doc.webkitFullscreenElement; + return Boolean(resolveScopedFullscreenElement(fullscreenEl, [mount.root], { allowDocumentViewport: true })) ? mount.root : 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 { + await (this.videoHandler.video === document.pictureInPictureElement ? document.exitPictureInPicture() : 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; + this.videoHandler.audioPlayer.player.volume = nextVolume / 100; + if (!this.data.syncVolume) { + this.videoHandler.onTranslationVolumeSliderSynced(nextVolume); + return; + } + this.videoHandler.syncVolumeWrapper("translation", nextVolume); + }).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.enableSubtitlesForCurrentLangPair(); + }).addEventListener("select:responseLanguageSubtitles", async () => { + if (!this.videoHandler?.data.autoSubtitles || !this.videoHandler.videoData) return; + await this.videoHandler.enableSubtitlesForCurrentLangPair(); + }).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); + }); + }).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); + 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; + if (!overlayView?.isInitialized() || !videoHandler?.downloadTranslationUrl || !videoHandler.videoData) return; + const downloadButton = overlayView.downloadTranslationButton; + const downloadUrl = videoHandler.downloadTranslationUrl; + const filename = this.data.downloadWithName ? clearFileName(videoHandler.videoData.downloadTitle) : `translation_${videoHandler.videoData.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 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(); + this.votGlobalPortal.remove(); + 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; + } + restartAudioPlayer() { + this.restartAudioPlayerSafely(); + } + async restartAudioPlayerSafely() { + 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?.contains(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 = document.activeElement; + 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$1(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/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) { + for (let p = video.parentElement; p; p = p.parentElement) if (this.isAdRelated(p)) return true; + return false; + } + 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 + }); + } + 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; + const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, { acceptNode: (node) => { + const el = node; + const isVideo = el.tagName === "VIDEO"; + const hasShadowRoot = Boolean(el.shadowRoot); + return isVideo || hasShadowRoot ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; + } }); + while (walker.nextNode()) { + const el = walker.currentNode; + if (el instanceof HTMLVideoElement) { + this.trackVideo(el); + continue; + } + const sr = el.shadowRoot; + if (sr) { + this.observeRoot(sr); + this.scan(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(); + const addAll = (videos) => { + for (const v of videos) set.add(v); + }; + if (node instanceof HTMLVideoElement) set.add(node); + if (node instanceof Document || node instanceof DocumentFragment || node instanceof Element) addAll(node.querySelectorAll("video")); + if (node instanceof Element) { + const shadowRoot = node.shadowRoot; + if (shadowRoot) addAll(shadowRoot.querySelectorAll("video")); + } + 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 resetSmartDuckingRuntime() { + return initSmartDuckingRuntime(); + } + 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?.gainNode)) return player.gainNode; + 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 { + inputNode.connect(analyser); + state.connectedInputNode = inputNode; + } 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 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 {} + releaseSmartDuckingAnalyser(handler); + writeSmartDuckingRuntime(handler, resetSmartDuckingRuntime()); + } + 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; + handler.smartVolumeDuckingTarget = dynamicDuckingTarget; + 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); + 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") this.audioPlayer.player.volume = 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; + this.smartVolumeDuckingTarget = targetVolume; + if (!this.hasActiveSource()) return; + 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(); + this.setVideoVolume(Math.min(baseline, targetVolume)); + writeSmartDuckingRuntime(this, initSmartDuckingRuntime(this.smartVolumeDuckingBaseline)); + this.smartVolumeIsDucked = true; + } + function applyManualVideoVolumeOverride(volume01) { + if (!this.data?.enabledAutoVolume || !this.hasActiveSource()) return; + const nextVolume = snapVolume01(volume01); + this.smartVolumeDuckingTarget = nextVolume; + 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.kload.workers.dev"; + } + 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.downloadTranslationUrl, + 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); + } + 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; + if (primary.aborted) return primary; + if (secondary.aborted) return secondary; + if (typeof AbortSignal !== "undefined" && "any" in AbortSignal) return AbortSignal.any([primary, secondary]); + const controller = new AbortController(); + const cleanup = () => { + primary.removeEventListener("abort", onPrimaryAbort); + secondary.removeEventListener("abort", onSecondaryAbort); + }; + const onPrimaryAbort = () => { + cleanup(); + controller.abort(primary.reason); + }; + const onSecondaryAbort = () => { + cleanup(); + controller.abort(secondary.reason); + }; + primary.addEventListener("abort", onPrimaryAbort, { once: true }); + secondary.addEventListener("abort", onSecondaryAbort, { once: true }); + return controller.signal; + } + function createScopedListeners(signal) { + const add = (element, event, handler, options) => { + const mergedSignal = mergeListenerSignals(signal, options?.signal); + if (!options) { + element.addEventListener(event, handler, { signal: mergedSignal }); + return; + } + const { signal: _ignoredSignal, ...restOptions } = options; + element.addEventListener(event, handler, { + ...restOptions, + signal: mergedSignal + }); + }; + const addMany = (element, events, handler, options) => { + for (const event of events) add(element, event, handler, options); + }; + return { + add, + addMany + }; + } + function bindOverlayHoverFocusEvents(addMany, target, overlayVisibility) { + addMany(target, ["pointerenter", "focusin"], (event) => overlayVisibility.handleOverlayInteraction(event)); + addMany(target, ["pointermove"], (event) => overlayVisibility.handleOverlayInteraction(event), { passive: true }); + addMany(target, ["pointerleave", "focusout"], (event) => overlayVisibility.scheduleHide(event)); + } + 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) { + const height = heightPx ?? self.video.getBoundingClientRect().height; + 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; + let lastObservedAriaValue = null; + for (const mutation of mutations) { + if (mutation.type !== "attributes" || mutation.attributeName !== "aria-valuenow") continue; + hasVolumeMutation = true; + const ariaValueNow = mutation.target instanceof Element ? mutation.target.getAttribute("aria-valuenow") : null; + const parsedAriaValue = ariaValueNow != null ? Number.parseFloat(ariaValueNow) : NaN; + if (Number.isFinite(parsedAriaValue)) lastObservedAriaValue = parsedAriaValue; + } + if (!hasVolumeMutation) return; + let videoPercent; + if (lastObservedAriaValue != null) videoPercent = toPercentInt(lastObservedAriaValue); + else videoPercent = toPercentInt((self.isMuted() ? 0 : self.getVideoVolume()) * 100); + self.syncVideoVolumeSlider(); + syncAudioTranslationVolumeFromVideo(self, videoPercent); + }); + 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 isButton = target && button ? button.contains(target) : false; + const isMenu = target && menu ? menu.contains(target) : false; + const isVideo = target ? self.container.contains(target) : false; + const isSettings = target && settings ? settings.contains(target) : false; + 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 = document.activeElement; + 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) { + addMany(eventContainer, ["pointerenter", "pointerdown"], (event) => self.overlayVisibility.handleHostInteraction(event)); + add(eventContainer, "pointermove", (event) => self.overlayVisibility.handleHostInteraction(event), { passive: true }); + add(eventContainer, "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 && buttonContainer.contains(node) || menuContainer instanceof Node && menuContainer.contains(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 canonicalizeLocale = (locale) => { + if (!locale) return void 0; + try { + return Intl.getCanonicalLocales(locale)[0]; + } catch { + return; + } + }; + var resolveSegmenterLocale = (locale) => { + const canonicalLocale = canonicalizeLocale(locale); + if (!canonicalLocale) return void 0; + return Intl.Segmenter.supportedLocalesOf([canonicalLocale])[0]; + }; + 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); + } + function getCurrentSubtitlesCacheKey(handler) { + const videoData = handler.videoData; + if (!videoData?.videoId) return null; + const detectedLanguage = videoData.detectedLanguage?.toLowerCase(); + if (!detectedLanguage || detectedLanguage === "auto") return null; + const subtitleLanguage = getPreferredSubtitlesLanguage(handler); + if (!subtitleLanguage) return null; + return handler.getSubtitlesCacheKey(videoData.videoId, detectedLanguage, subtitleLanguage); + } + async function ensureResolvedVideoLanguageForSubtitles(handler) { + if (!handler.videoData?.videoId) return false; + await handler.videoManager.ensureDetectedLanguageForTranslation(handler.videoData); + const detectedLanguage = handler.videoData.detectedLanguage?.toLowerCase(); + return Boolean(detectedLanguage && detectedLanguage !== "auto"); + } + 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 + }; + console.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() { + if (!await ensureResolvedVideoLanguageForSubtitles(this)) { + if (this.subtitlesCacheKey !== null || this.subtitles.length > 0) { + this.subtitles = []; + this.subtitlesCacheKey = null; + await this.updateSubtitlesLangSelect(); + } + return this; + } + 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; + } + /** + * 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; + } + if (!await ensureResolvedVideoLanguageForSubtitles(this)) { + this.subtitles = []; + this.subtitlesCacheKey = null; + await this.updateSubtitlesLangSelect(); + 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 + new Set(availableTTS); + 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(); + downloadTranslationUrl = null; + isRefreshingTranslation = false; + autoRetry; + votOpts; + volumeOnStart; + /** + * 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; + smartVolumeDuckingTarget = .2; + 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(); + /** + * Returns fullscreen root for overlay if the active fullscreen session belongs + * to the current video/container. Otherwise returns null. + */ + getFullscreenOverlayRoot() { + const doc = document; + return resolveScopedFullscreenElement(doc.fullscreenElement ?? doc.webkitFullscreenElement, [this.container]); + } + 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, fullscreenRoot } = this.getOverlayMountPoints(container); + return { + root, + portalContainer, + subtitlesMountContainer, + tooltipLayoutRoot: fullscreenRoot ?? this.tooltipLayoutRoot + }; + } + /** + * 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); + } + 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); + } + /** + * Lazily creates the subtitles widget. + * @returns {SubtitlesWidget} + */ + getSubtitlesWidget() { + if (!this.subtitlesWidget) { + const { subtitlesMountContainer } = this.getOverlayMountPoints(); + this.subtitlesWidget = new SubtitlesWidget(this.video, subtitlesMountContainer, this.interactionChecker, this.tooltipLayoutRoot); + 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() { + return this.getOverlayMountPoints().root; + } + /** + * Determines the DOM container used for overlay portals. + * @returns {HTMLElement} + */ + get portalContainer() { + return this.getOverlayMountPoints().portalContainer; + } + /** + * Determines the root element used for tooltip layout calculations. + * @returns {HTMLElement | undefined} + */ + get tooltipLayoutRoot() { + switch (this.site.host) { + case "kickstarter": return document.getElementById("react-project-header") ?? void 0; + case "custom": return; + default: return this.container; + } + } + /** + * 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(); + debug.log("preferAudio:", preferAudio); + this.audioPlayer = new Chaimu({ + video: this.video, + debug: Boolean(false), + fetchFn: GM_fetch, + fetchOpts: { timeout: 0 }, + preferAudio + }); + 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; + } + callModule(impl, ...args) { + return impl.call(this, ...args); + } + callModuleAsync(impl, ...args) { + return impl.call(this, ...args); + } + /** + * Initializes the VideoHandler: loads settings, UI, video data, events, etc. + * @returns {Promise} + */ + 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 this.callModule(initExtraEvents); + } + /** + * 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; + 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() { + return this.lifecycleController.setCanPlay(); + } + isOverlayInteractiveNode(node) { + return this.callModule(isOverlayInteractiveNode, node); + } + /** + * Schedules hiding the overlay button with guard checks for internal navigation. + */ + getAutoHideDelay() { + return this.callModule(getAutoHideDelay); + } + /** + * 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 this.callModuleAsync(enableSubtitlesForCurrentLangPair); + } + /** + * 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 this.callModuleAsync(toggleSubtitlesForCurrentLangPair); + } + 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() { + return 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); + 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) { + if (!this.volumeLinkState.initialized) { + syncTranslationLinkSnapshot(this.volumeLinkState, volumePercent); + return; + } + 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() { + return 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 { nextVideo, nextTranslation } = applyVolumeLinkDelta({ + state: this.volumeLinkState, + fromType, + newVolume, + currentVideo: Number(videoSlider.value), + currentTranslation: Number(translationSlider.value), + translationMin: translationSlider.min, + translationMax: translationSlider.max + }); + if (typeof nextTranslation === "number") { + translationSlider.value = nextTranslation; + if (this.audioPlayer?.player) this.audioPlayer.player.volume = nextTranslation / 100; + return; + } + if (typeof nextVideo === "number") { + videoSlider.value = nextVideo; + this.setVideoVolume(nextVideo / 100); + } + } + /** + * Retrieves video data. + * @returns {Promise} The video data object. + */ + getVideoData() { + return this.videoManager.getVideoData(); + } + /** + * Validates the video. + * @returns {Promise} True if valid. + */ + videoValidator() { + return 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.downloadTranslationUrl = 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; + 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.downloadTranslationUrl = audioUrl; + } + debug.log("afterUpdateTranslation downloadTranslationUrl", this.downloadTranslationUrl); + 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} The valid audio URL. + */ + validateAudioUrl(audioUrl, actionContext) { + return this.callModuleAsync(validateAudioUrl, audioUrl, actionContext); + } + scheduleTranslationRefresh() { + this.callModule(scheduleTranslationRefresh); + } + refreshTranslationAudio = refreshTranslationAudio; + /** + * Proxifies the audio URL if needed. + * @param {string} audioUrl The original audio URL. + * @returns {string} The proxified audio URL. + */ + proxifyAudio(audioUrl) { + return this.callModule(proxifyAudio, 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 this.callModule(unproxifyAudio, 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 this.callModule(isMultiMethodS3, url); + } + /** + * Updates the translation audio source. + * @param {string} audioUrl The audio URL. + */ + updateTranslation = updateTranslation; + /** + * 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 this.callModule(isYouTubeHosts); + } + /** + * Configures audio settings such as volume. + */ + setupAudioSettings() { + return this.callModule(setupAudioSettings); + } + applyManualVideoVolumeOverride(volume) { + return this.callModule(applyManualVideoVolumeOverride, 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() { + return 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.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 = `
Autogenerated by VOT:
    -
  • OS: ${info2.os}
  • -
  • Browser: ${info2.browser}
  • -
  • Loader: ${info2.loader}
  • -
  • Script version: ${info2.scriptVersion}
  • -
  • URL: ${info2.url}
  • +
  • OS: ${info.os}
  • +
  • Browser: ${info.browser}
  • +
  • Loader: ${info.loader}
  • +
  • Script version: ${info.scriptVersion}
  • +
  • URL: ${info.url}
  • Lang: ${detectedLanguage} -> ${responseLanguage} (Lively voice: ${this.data?.useLivelyVoice ?? false} | Audio download: ${this.data?.useAudioDownload ?? false})
  • Player: ${this.data?.newAudioPlayer ? "New" : "Old"} (CSP only: ${this.data?.onlyBypassMediaCSP ?? false})
  • Proxying mode: ${this.data?.translateProxyEnabled ?? 0}
`; - const template = `1-bug-report-${localizationProvider.lang === "ru" ? "ru" : "en"}.yml`; - return { - assignees: "ilyhalight", - template, - os: info2.os, - "script-version": info2.scriptVersion, - "additional-info": additionalInfo - }; - } -releaseExtraEvents = releaseExtraEvents; - } - const videoObserverChecker = createIntervalIdleChecker(); - const videoObserver = new VideoObserver(videoObserverChecker); - const videosWrappers = new WeakMap(); - let servicesCache = null; - const 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); - } - console.log(`[VOT][bootstrap][${ctx.frame}] ${message}`, payload); - } - function getServicesCached() { - if (!servicesCache) { - servicesCache = getService(); - } - return servicesCache; - } - function findContainer(site, video) { - if (!site.selector) { - return video.parentElement; - } - const matched = findConnectedContainerBySelector(video, site.selector); - if (site.shadowRoot) ; - return matched; - } - async function main() { - const bootstrapMode = resolveBootstrapMode({ - isIframe: isIframe(), - href: String(globalThis.location.href || ""), - origin: globalThis.location.origin - }); - if (bootstrapMode === "skip") { - logBootstrap("Skipping bootstrap for non-runnable iframe"); - return; - } - logBootstrap("Loading extension"); - if (bootstrapMode === "top-full") { - await ensureRuntimeActivated("top-frame", logBootstrap); - } else { - logBootstrap("Lazy iframe 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(); - } - if (bootState.status === "booting" || bootState.status === "booted") { - logBootstrap("bootstrap already initialized, skipping duplicate run", { - status: bootState.status - }); - } else { - const runBootstrap = async () => { - try { - await main(); - bootState.status = "booted"; - } catch (e2) { - bootState.status = "failed"; - bootState.error = e2; - console.error("[VOT]", e2); - } - }; - bootState.status = "booting"; - bootState.promise = runBootstrap(); - } - - }) - }; -})); - -System.register("./__vite-browser-external-2Ng8QIWW-Xya9USxv.js", [], (function (exports, module) { - 'use strict'; - return { - execute: (function () { - - const __viteBrowserExternal = exports("default", {}); - - }) - }; -})); - -System.import("./__entry.js", "./"); \ No newline at end of file + 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); + console.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, 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; +})({}); diff --git a/package.json b/package.json index 161d1643..bd7834d6 100644 --- a/package.json +++ b/package.json @@ -16,17 +16,16 @@ ], "devDependencies": { "@toil/translate": "^1.0.8", - "@types/bun": "^1.3.9", + "@types/bun": "^1.3.11", "@vot.js/core": "^2.4.12", + "sass": "^1.98.0", "crx3": "^2.0.0", - "lefthook": "^2.1.1", - "lightningcss": "^1.31.1", + "lefthook": "^2.1.4", + "lightningcss": "^1.32.0", "npm-run-all2": "^8.0.4", - "sass": "^1.97.3", "typescript": "^5.9.3", - "vite": "^7.3.1", - "vite-plugin-monkey": "^7.1.9", - "zip-a-folder": "^4.0.4" + "vite": "^8.0.1", + "zip-a-folder": "^6.1.0" }, "scripts": { "test": "bun test", @@ -35,12 +34,12 @@ "test:ui": "vite build --config vite.test-ui.config.ts", "build": "vite build --config vite.config.ts", "build:min": "vite build --mode minify --config vite.config.ts", - "build:all": "npx run-p build build:min", + "build:all": "run-p build build:min", "build:ext": "vite build --config vite.extension.config.ts", "build:chrome": "vite build --mode chrome --config vite.extension.config.ts", "build:firefox": "vite build --mode firefox --config vite.extension.config.ts", "build:dev": "vite build --mode development --config vite.config.ts", - "dev": "vite dev", + "dev": "vite build --watch --mode development --config vite.config.ts", "format": "bunx @biomejs/biome check --write --unsafe ./src", "gen:wiki": "bun run ./scripts/wiki-gen/index.js", "localize": "bunx \"@toil/localize-tui\"" diff --git a/scripts/wiki-gen/data.js b/scripts/wiki-gen/data.js index 1eb181fb..f64cb81b 100644 --- a/scripts/wiki-gen/data.js +++ b/scripts/wiki-gen/data.js @@ -321,4 +321,4 @@ const extraData = { const sitesBlackList = ["porntn"]; -export { siteData, extraData, sitesBlackList }; +export { extraData, siteData, sitesBlackList }; diff --git a/scripts/wiki-gen/index.js b/scripts/wiki-gen/index.js index a1a0be44..7ad385e9 100644 --- a/scripts/wiki-gen/index.js +++ b/scripts/wiki-gen/index.js @@ -43,9 +43,9 @@ function normalizeDomain(domain) { return domain .trim() .toLowerCase() - .replaceAll(String.raw`\.`,".") - .replaceAll(String.raw`\-`,"-") - .replaceAll(String.raw`\/`,"/") + .replaceAll(String.raw`\.`, ".") + .replaceAll(String.raw`\-`, "-") + .replaceAll(String.raw`\/`, "/") .replace(/^https?:\/\//, "") .replace(/\/.*$/, "") .replaceAll(/[()]/g, "") @@ -152,27 +152,13 @@ function parseCharClass(regexSource, startIndex) { } if (char === "\\") { - const escaped = regexSource[index + 1]; - if (!escaped) { - canExpand = false; - index += 1; - continue; - } - - if ("dDsSwW".includes(escaped)) { - canExpand = false; - } else { - chars.push(escaped); - } - index += 2; + const escapedResult = parseCharClassEscape(regexSource, index, chars); + canExpand = canExpand && escapedResult.canExpand; + index = escapedResult.index; continue; } - if ( - regexSource[index + 1] === "-" && - regexSource[index + 2] && - regexSource[index + 2] !== "]" - ) { + if (isCharClassRange(regexSource, index)) { canExpand = false; index += 3; continue; @@ -189,6 +175,61 @@ function parseCharClass(regexSource, startIndex) { return { variants: unique(chars), index }; } +function parseCharClassEscape(regexSource, index, chars) { + const escaped = regexSource[index + 1]; + if (!escaped) { + return { + canExpand: false, + index: index + 1, + }; + } + + if ("dDsSwW".includes(escaped)) { + return { + canExpand: false, + index: index + 2, + }; + } + + chars.push(escaped); + return { + canExpand: true, + index: index + 2, + }; +} + +function isCharClassRange(regexSource, index) { + return ( + regexSource[index + 1] === "-" && + regexSource[index + 2] && + regexSource[index + 2] !== "]" + ); +} + +function consumeLazySuffix(regexSource, index) { + if (regexSource[index] === "?") { + return index + 1; + } + + return index; +} + +function buildSimpleQuantifierResult(startIndex, min, max, regexSource) { + return { + min, + max, + index: consumeLazySuffix(regexSource, startIndex + 1), + }; +} + +function buildRangeQuantifierResult(closeIndex, min, max, regexSource) { + return { + min, + max, + index: consumeLazySuffix(regexSource, closeIndex + 1), + }; +} + function parseQuantifier(regexSource, startIndex) { if (startIndex >= regexSource.length) { return null; @@ -196,27 +237,25 @@ function parseQuantifier(regexSource, startIndex) { const quantifier = regexSource[startIndex]; if (quantifier === "?") { - let index = startIndex + 1; - if (regexSource[index] === "?") { - index += 1; - } - return { min: 0, max: 1, index }; + return buildSimpleQuantifierResult(startIndex, 0, 1, regexSource); } if (quantifier === "+") { - let index = startIndex + 1; - if (regexSource[index] === "?") { - index += 1; - } - return { min: 1, max: Number.POSITIVE_INFINITY, index }; + return buildSimpleQuantifierResult( + startIndex, + 1, + Number.POSITIVE_INFINITY, + regexSource, + ); } if (quantifier === "*") { - let index = startIndex + 1; - if (regexSource[index] === "?") { - index += 1; - } - return { min: 0, max: Number.POSITIVE_INFINITY, index }; + return buildSimpleQuantifierResult( + startIndex, + 0, + Number.POSITIVE_INFINITY, + regexSource, + ); } if (quantifier !== "{") { @@ -234,11 +273,7 @@ function parseQuantifier(regexSource, startIndex) { if (exactMatch) { const value = Number(exactMatch[1]); - let index = closeIndex + 1; - if (regexSource[index] === "?") { - index += 1; - } - return { min: value, max: value, index }; + return buildRangeQuantifierResult(closeIndex, value, value, regexSource); } if (rangeMatch) { @@ -247,11 +282,7 @@ function parseQuantifier(regexSource, startIndex) { rangeMatch[2] === undefined ? Number.POSITIVE_INFINITY : Number(rangeMatch[2]); - let index = closeIndex + 1; - if (regexSource[index] === "?") { - index += 1; - } - return { min, max, index }; + return buildRangeQuantifierResult(closeIndex, min, max, regexSource); } return null; @@ -323,20 +354,44 @@ function parseSequence(regexSource, startIndex) { return { variants, index }; } +function parseEscapedAtom(regexSource, startIndex) { + const escaped = regexSource[startIndex + 1]; + if (!escaped) { + return { variants: [""], index: startIndex + 1 }; + } + + if ("dDsSwW".includes(escaped)) { + return { variants: ["*"], index: startIndex + 2 }; + } + + return { variants: [escaped], index: startIndex + 2 }; +} + +function parseGroupAtom(regexSource, startIndex) { + let index = startIndex + 1; + if (regexSource[index] === "?" && regexSource[index + 1] === ":") { + index += 2; + } else if (regexSource[index] === "?") { + const closeIndex = findGroupEnd(regexSource, startIndex); + return { + variants: ["*"], + index: closeIndex === -1 ? regexSource.length : closeIndex + 1, + }; + } + + const parsedGroup = parseExpression(regexSource, index); + if (regexSource[parsedGroup.index] !== ")") { + return { variants: ["*"], index: parsedGroup.index }; + } + + return { variants: parsedGroup.variants, index: parsedGroup.index + 1 }; +} + function parseAtom(regexSource, startIndex) { const char = regexSource[startIndex]; if (char === "\\") { - const escaped = regexSource[startIndex + 1]; - if (!escaped) { - return { variants: [""], index: startIndex + 1 }; - } - - if ("dDsSwW".includes(escaped)) { - return { variants: ["*"], index: startIndex + 2 }; - } - - return { variants: [escaped], index: startIndex + 2 }; + return parseEscapedAtom(regexSource, startIndex); } if (char === "[") { @@ -344,25 +399,13 @@ function parseAtom(regexSource, startIndex) { } if (char === "(") { - let index = startIndex + 1; - if (regexSource[index] === "?" && regexSource[index + 1] === ":") { - index += 2; - } else if (regexSource[index] === "?") { - const closeIndex = findGroupEnd(regexSource, startIndex); - return { - variants: ["*"], - index: closeIndex === -1 ? regexSource.length : closeIndex + 1, - }; - } - - const parsedGroup = parseExpression(regexSource, index); - if (regexSource[parsedGroup.index] !== ")") { - return { variants: ["*"], index: parsedGroup.index }; - } - - return { variants: parsedGroup.variants, index: parsedGroup.index + 1 }; + return parseGroupAtom(regexSource, startIndex); } + return parseLiteralAtom(char, startIndex); +} + +function parseLiteralAtom(char, startIndex) { if (char === ".") { return { variants: ["."], index: startIndex + 1 }; } @@ -420,7 +463,9 @@ function extractDomainsFromRegex(regex) { const parsed = parseExpression(source, 0); const variants = parsed.variants.length ? parsed.variants : [source]; - return unique(variants.map((domain) => normalizeDomain(domain)).filter(Boolean)); + return unique( + variants.map((domain) => normalizeDomain(domain)).filter(Boolean), + ); } function parseRegexLiteral(literal) { @@ -510,7 +555,8 @@ function mergeByHost(supportedSites) { domains: new Set(), }; - existing.needBypassCSP = existing.needBypassCSP || Boolean(site.needBypassCSP); + existing.needBypassCSP = + existing.needBypassCSP || Boolean(site.needBypassCSP); for (const domain of extractDomainsFromMatch(site.match)) { existing.domains.add(domain); } @@ -564,7 +610,9 @@ ${locales.availabledDomains[lang]}: } function genMarkdown(supportedSites, lang = "ru") { - return mergeByHost(supportedSites).map((site) => renderSiteMarkdown(site, lang)); + return mergeByHost(supportedSites).map((site) => + renderSiteMarkdown(site, lang), + ); } function getSupportedSites() { @@ -576,7 +624,9 @@ function getSupportedSites() { host, match: host === "custom" ? "any" : site.match, status: hasExtraData ? extraData[host].status : "✅", - statusPhrase: hasExtraData ? extraData[host].statusPhrase : locales.working, + statusPhrase: hasExtraData + ? extraData[host].statusPhrase + : locales.working, needBypassCSP: site.needBypassCSP, }; }); diff --git a/src/audioDownloader/ytAudio/index.ts b/src/audioDownloader/ytAudio/index.ts index e5fef4f3..f21b6cd1 100644 --- a/src/audioDownloader/ytAudio/index.ts +++ b/src/audioDownloader/ytAudio/index.ts @@ -1 +1,14 @@ -export * from "./src/AudioDownloader"; +export type { + AudioBufferResult, + AudioChunkStreamOptions, + AudioChunkStreamResult, + AudioDownloaderOptions, + AudioStreamRequest, + AudioStreamResult, +} from "./src/AudioDownloader"; +export { + AudioDownloader, + buildClientAttemptOrder, + extractVideoId, + YtWatchContextForbiddenError, +} from "./src/AudioDownloader"; diff --git a/src/audioDownloader/ytAudio/src/AudioDownloader.ts b/src/audioDownloader/ytAudio/src/AudioDownloader.ts index 00fb45bd..e9144516 100644 --- a/src/audioDownloader/ytAudio/src/AudioDownloader.ts +++ b/src/audioDownloader/ytAudio/src/AudioDownloader.ts @@ -206,35 +206,41 @@ export function extractVideoId(input: string): string { const hostname = url.hostname.toLowerCase(); if (hostname === "youtu.be" || hostname.endsWith(".youtu.be")) { - const id = url.pathname.split("/").find(Boolean); + return getValidatedVideoId(url.pathname.split("/").find(Boolean), input); + } + + const searchId = url.searchParams.get("v"); + if (searchId && VIDEO_ID_PATTERN.test(searchId)) return searchId; + + const pathSegments = url.pathname.split("/").filter(Boolean); + const pathId = getVideoIdFromPathSegments(pathSegments); + if (pathId) return pathId; + + throw new Error(`Cannot extract YouTube video id from: ${input}`); +} + +function getValidatedVideoId(id: string | undefined, input: string): string { + if (id && VIDEO_ID_PATTERN.test(id)) { + return id; + } + + throw new Error(`Cannot extract YouTube video id from: ${input}`); +} + +function getVideoIdFromPathSegments(pathSegments: string[]): string | null { + const pathMarkers = ["shorts", "embed"] as const; + + for (const marker of pathMarkers) { + const markerIndex = pathSegments.indexOf(marker); + if (markerIndex === -1) continue; + + const id = pathSegments[markerIndex + 1]; if (id && VIDEO_ID_PATTERN.test(id)) { return id; } } - const searchId = url.searchParams.get("v"); - if (searchId && VIDEO_ID_PATTERN.test(searchId)) { - return searchId; - } - - const pathSegments = url.pathname.split("/").filter(Boolean); - const shortsIndex = pathSegments.indexOf("shorts"); - if (shortsIndex !== -1) { - const shortsId = pathSegments[shortsIndex + 1]; - if (shortsId && VIDEO_ID_PATTERN.test(shortsId)) { - return shortsId; - } - } - - const embedIndex = pathSegments.indexOf("embed"); - if (embedIndex !== -1) { - const embedId = pathSegments[embedIndex + 1]; - if (embedId && VIDEO_ID_PATTERN.test(embedId)) { - return embedId; - } - } - - throw new Error(`Cannot extract YouTube video id from: ${input}`); + return null; } function decodeEscapedJsonString(input: string): string { diff --git a/src/audioDownloader/ytAudio/src/internal/format-selection.ts b/src/audioDownloader/ytAudio/src/internal/format-selection.ts index 56bebba8..53337d9c 100644 --- a/src/audioDownloader/ytAudio/src/internal/format-selection.ts +++ b/src/audioDownloader/ytAudio/src/internal/format-selection.ts @@ -54,7 +54,7 @@ export function extractAudioCodecFromMimeType( ); } -export function pickByBitrate( +function pickByBitrate( formats: readonly T[], direction: "max" | "min", ): T | null { diff --git a/src/audioDownloader/ytAudio/src/internal/mp4-aac.ts b/src/audioDownloader/ytAudio/src/internal/mp4-aac.ts deleted file mode 100644 index b9b2978d..00000000 --- a/src/audioDownloader/ytAudio/src/internal/mp4-aac.ts +++ /dev/null @@ -1,489 +0,0 @@ -interface AudioChunkSink { - write(chunk: Uint8Array): Promise; -} - -interface AacExtractionOptions { - codecHint?: string; - sampleRateHint?: number; - channelsHint?: number; -} - -interface AacExtractionResult { - codec: string; - sampleRate: number; - channels: number; - bytesWritten: number; -} - -interface Box { - type: string; - start: number; - size: number; - end: number; - payloadStart: number; - payloadEnd: number; -} - -interface StscEntry { - firstChunk: number; - samplesPerChunk: number; -} - -interface AudioTrackTables { - sampleSizes: number[]; - chunkOffsets: number[]; - sampleToChunk: StscEntry[]; -} - -interface AdtsConfig { - profile: number; - sampleRateIndex: number; - channelConfig: number; -} - -const AAC_SAMPLE_RATES = [ - 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, - 8000, 7350, -] as const; - -function readUint64(view: DataView, offset: number): number { - const high = view.getUint32(offset, false); - const low = view.getUint32(offset + 4, false); - const value = high * 2 ** 32 + low; - if (!Number.isSafeInteger(value)) { - throw new TypeError( - "Encountered 64-bit MP4 offset larger than Number.MAX_SAFE_INTEGER", - ); - } - return value; -} - -function readBox( - bytes: Uint8Array, - view: DataView, - start: number, - end: number, -): Box | null { - if (start + 8 > end) { - return null; - } - - let size = view.getUint32(start, false); - const type = String.fromCodePoint( - bytes[start + 4] ?? 0, - bytes[start + 5] ?? 0, - bytes[start + 6] ?? 0, - bytes[start + 7] ?? 0, - ); - let headerSize = 8; - - if (size === 1) { - if (start + 16 > end) { - return null; - } - size = readUint64(view, start + 8); - headerSize = 16; - } else if (size === 0) { - size = end - start; - } - - if (size < headerSize || start + size > end) { - return null; - } - - return { - type, - start, - size, - end: start + size, - payloadStart: start + headerSize, - payloadEnd: start + size, - }; -} - -function listChildBoxes( - bytes: Uint8Array, - view: DataView, - start: number, - end: number, -): Box[] { - const result: Box[] = []; - let offset = start; - - while (offset + 8 <= end) { - const box = readBox(bytes, view, offset, end); - if (!box) { - break; - } - result.push(box); - offset = box.end; - } - - return result; -} - -function findChildBox( - bytes: Uint8Array, - view: DataView, - start: number, - end: number, - type: string, -): Box | null { - const type0 = type.codePointAt(0) ?? 0; - const type1 = type.codePointAt(1) ?? 0; - const type2 = type.codePointAt(2) ?? 0; - const type3 = type.codePointAt(3) ?? 0; - const isComparableType = type.length === 4; - - let offset = start; - while (offset + 8 <= end) { - let boxSize = view.getUint32(offset, false); - let headerSize = 8; - - if (boxSize === 1) { - if (offset + 16 > end) { - return null; - } - boxSize = readUint64(view, offset + 8); - headerSize = 16; - } else if (boxSize === 0) { - boxSize = end - offset; - } - - const boxEnd = offset + boxSize; - if (boxSize < headerSize || boxEnd > end) { - return null; - } - - if ( - isComparableType && - bytes[offset + 4] === type0 && - bytes[offset + 5] === type1 && - bytes[offset + 6] === type2 && - bytes[offset + 7] === type3 - ) { - return { - type, - start: offset, - size: boxSize, - end: boxEnd, - payloadStart: offset + headerSize, - payloadEnd: boxEnd, - }; - } - - offset = boxEnd; - } - return null; -} - -function findBoxPath( - bytes: Uint8Array, - view: DataView, - start: number, - end: number, - path: string[], -): Box | null { - let currentStart = start; - let currentEnd = end; - let current: Box | null = null; - - for (const step of path) { - current = findChildBox(bytes, view, currentStart, currentEnd, step); - if (!current) { - return null; - } - currentStart = current.payloadStart; - currentEnd = current.payloadEnd; - } - - return current; -} - -function parseStsz(view: DataView, box: Box): number[] { - let offset = box.payloadStart; - offset += 4; // version + flags - - const sampleSize = view.getUint32(offset, false); - offset += 4; - - const sampleCount = view.getUint32(offset, false); - offset += 4; - - if (sampleSize !== 0) { - return new Array(sampleCount).fill(sampleSize); - } - - const sampleSizes = new Array(sampleCount); - for (let i = 0; i < sampleCount; i++) { - sampleSizes[i] = view.getUint32(offset, false); - offset += 4; - } - return sampleSizes; -} - -function parseStsc(view: DataView, box: Box): StscEntry[] { - let offset = box.payloadStart; - offset += 4; // version + flags - - const entryCount = view.getUint32(offset, false); - offset += 4; - - const entries = new Array(entryCount); - for (let i = 0; i < entryCount; i++) { - entries[i] = { - firstChunk: view.getUint32(offset, false), - samplesPerChunk: view.getUint32(offset + 4, false), - }; - offset += 12; - } - return entries; -} - -function parseChunkOffsets(view: DataView, box: Box): number[] { - let offset = box.payloadStart; - offset += 4; // version + flags - - const entryCount = view.getUint32(offset, false); - offset += 4; - - const offsets: number[] = new Array(entryCount); - - if (box.type === "co64") { - for (let i = 0; i < entryCount; i++) { - offsets[i] = readUint64(view, offset); - offset += 8; - } - return offsets; - } - - for (let i = 0; i < entryCount; i++) { - offsets[i] = view.getUint32(offset, false); - offset += 4; - } - return offsets; -} - -function getAudioTrackTables( - bytes: Uint8Array, - view: DataView, -): AudioTrackTables { - const moov = findChildBox(bytes, view, 0, bytes.byteLength, "moov"); - if (!moov) { - throw new Error("MP4 does not contain moov box"); - } - - const traks = listChildBoxes( - bytes, - view, - moov.payloadStart, - moov.payloadEnd, - ).filter((box) => box.type === "trak"); - let audioTrak: Box | null = null; - - for (const trak of traks) { - const hdlr = findBoxPath(bytes, view, trak.payloadStart, trak.payloadEnd, [ - "mdia", - "hdlr", - ]); - if (!hdlr) { - continue; - } - if (hdlr.payloadStart + 12 > hdlr.payloadEnd) { - continue; - } - const handlerType = String.fromCodePoint( - bytes[hdlr.payloadStart + 8] ?? 0, - bytes[hdlr.payloadStart + 9] ?? 0, - bytes[hdlr.payloadStart + 10] ?? 0, - bytes[hdlr.payloadStart + 11] ?? 0, - ); - if (handlerType === "soun") { - audioTrak = trak; - break; - } - } - - if (!audioTrak) { - throw new Error("Failed to locate audio track in MP4"); - } - - const stbl = findBoxPath( - bytes, - view, - audioTrak.payloadStart, - audioTrak.payloadEnd, - ["mdia", "minf", "stbl"], - ); - if (!stbl) { - throw new Error("Audio track is missing stbl box"); - } - - const stblChildren = listChildBoxes( - bytes, - view, - stbl.payloadStart, - stbl.payloadEnd, - ); - - let stsz: Box | null = null; - let stsc: Box | null = null; - let stco: Box | null = null; - let co64: Box | null = null; - - for (const child of stblChildren) { - if (!stsz && child.type === "stsz") { - stsz = child; - } else if (!stsc && child.type === "stsc") { - stsc = child; - } else if (!stco && child.type === "stco") { - stco = child; - } else if (!co64 && child.type === "co64") { - co64 = child; - } - } - - const chunkOffsetBox = stco ?? co64; - - if (!stsz || !stsc || !chunkOffsetBox) { - throw new Error("Audio track is missing one of stsz/stsc/stco/co64 tables"); - } - - return { - sampleSizes: parseStsz(view, stsz), - sampleToChunk: parseStsc(view, stsc), - chunkOffsets: parseChunkOffsets(view, chunkOffsetBox), - }; -} - -function parseAacObjectType(codec: string): number { - const match = /mp4a\.40\.(\d+)/i.exec(codec); - const value = match?.[1] ? Number.parseInt(match[1], 10) : 2; - return Number.isFinite(value) && value > 0 ? value : 2; -} - -function sampleRateToAdtsIndex(sampleRate: number): number { - const idx = (AAC_SAMPLE_RATES as readonly number[]).indexOf(sampleRate); - if (idx >= 0) { - return idx; - } - return 4; -} - -function buildAdtsConfig( - aacObjectType: number, - sampleRate: number, - channels: number, -): AdtsConfig { - return { - profile: Math.max(0, Math.min(3, aacObjectType - 1)), - sampleRateIndex: sampleRateToAdtsIndex(sampleRate), - channelConfig: Math.max(1, Math.min(7, channels)), - }; -} - -function buildAdtsHeader(frameLength: number, config: AdtsConfig): Uint8Array { - const header = new Uint8Array(7); - const adtsFrameLength = frameLength + 7; - - header[0] = 0xff; - header[1] = 0xf1; - header[2] = - ((config.profile & 0x03) << 6) | - ((config.sampleRateIndex & 0x0f) << 2) | - ((config.channelConfig >> 2) & 0x01); - header[3] = - ((config.channelConfig & 0x03) << 6) | ((adtsFrameLength >> 11) & 0x03); - header[4] = (adtsFrameLength >> 3) & 0xff; - header[5] = ((adtsFrameLength & 0x07) << 5) | 0x1f; - header[6] = 0xfc; - - return header; -} - -export async function extractAacFromMp4( - mp4Bytes: Uint8Array, - sink: AudioChunkSink, - options: AacExtractionOptions = {}, -): Promise { - const view = new DataView( - mp4Bytes.buffer, - mp4Bytes.byteOffset, - mp4Bytes.byteLength, - ); - const { sampleSizes, sampleToChunk, chunkOffsets } = getAudioTrackTables( - mp4Bytes, - view, - ); - - const codec = options.codecHint ?? "mp4a.40.2"; - const sampleRate = options.sampleRateHint ?? 44100; - const channels = options.channelsHint ?? 2; - const aacObjectType = parseAacObjectType(codec); - const adtsConfig = buildAdtsConfig(aacObjectType, sampleRate, channels); - - let sampleIndex = 0; - let stscIndex = 0; - let bytesWritten = 0; - - for (let chunkIndex = 1; chunkIndex <= chunkOffsets.length; chunkIndex++) { - while ( - stscIndex + 1 < sampleToChunk.length && - chunkIndex >= - (sampleToChunk[stscIndex + 1]?.firstChunk ?? Number.POSITIVE_INFINITY) - ) { - stscIndex++; - } - - const chunkRule = sampleToChunk[stscIndex]; - if (!chunkRule) { - throw new Error("stsc table lookup failed for current chunk index"); - } - - const chunkOffsetValue = chunkOffsets[chunkIndex - 1]; - if (typeof chunkOffsetValue !== "number") { - throw new TypeError( - "Chunk offset table lookup failed for current chunk index", - ); - } - let chunkOffset = chunkOffsetValue; - - for ( - let i = 0; - i < chunkRule.samplesPerChunk && sampleIndex < sampleSizes.length; - i++ - ) { - const sampleSize = sampleSizes[sampleIndex++]; - if (typeof sampleSize !== "number") { - throw new TypeError( - "Sample size table lookup failed for current sample index", - ); - } - const sampleEnd: number = chunkOffset + sampleSize; - - if (sampleEnd > mp4Bytes.byteLength) { - throw new Error("MP4 sample offset points outside file boundaries"); - } - - const adtsHeader = buildAdtsHeader(sampleSize, adtsConfig); - const sampleBytes = mp4Bytes.subarray(chunkOffset, sampleEnd); - - await sink.write(adtsHeader); - await sink.write(sampleBytes); - - bytesWritten += adtsHeader.byteLength + sampleBytes.byteLength; - chunkOffset = sampleEnd; - } - } - - if (sampleIndex !== sampleSizes.length) { - throw new Error("MP4 audio sample table traversal ended prematurely"); - } - - return { - codec, - sampleRate, - channels, - bytesWritten, - }; -} diff --git a/src/bootstrap/iframeInteractor.ts b/src/bootstrap/iframeInteractor.ts index d6334a1b..2253a1c1 100644 --- a/src/bootstrap/iframeInteractor.ts +++ b/src/bootstrap/iframeInteractor.ts @@ -5,7 +5,14 @@ type IframeConfig = { responseFormatter: (videoId: string, data: unknown) => unknown; }; +let iframeInteractorInitialized = false; + export function initIframeInteractor(): void { + if (iframeInteractorInitialized) { + return; + } + iframeInteractorInitialized = true; + const configs: Record = { "https://dev.epicgames.com": { targetOrigin: "https://dev.epicgames.com", diff --git a/src/bootstrap/runtimeActivation.ts b/src/bootstrap/runtimeActivation.ts index b57f126b..f0f9b99b 100644 --- a/src/bootstrap/runtimeActivation.ts +++ b/src/bootstrap/runtimeActivation.ts @@ -6,7 +6,6 @@ import { } from "../localization/localizationProvider"; import debug from "../utils/debug"; import { isIframe } from "../utils/iframeConnector"; -import { initIframeInteractor } from "./iframeInteractor"; type LogBootstrap = ( message: string, @@ -15,7 +14,6 @@ type LogBootstrap = ( let runtimeActivated = false; let runtimeActivationPromise: Promise | null = null; -let iframeInteractorBound = false; export async function ensureRuntimeActivated( reason: string, @@ -42,11 +40,6 @@ export async function ensureRuntimeActivated( } debug.log(`Selected menu language: ${localizationProvider.lang}`); - if (!iframeInteractorBound) { - iframeInteractorBound = true; - initIframeInteractor(); - } - runtimeActivated = true; })(); diff --git a/src/config/config.ts b/src/config/config.ts index c232be8b..476331d2 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -12,6 +12,7 @@ export const m3u8ProxyHost = "media-proxy.toil.cc/v1/proxy/m3u8"; /** * @see https://github.com/FOSWLY/vot-worker */ +export const proxyWorkerHostMode1 = "vot-new.toil-dump.workers.dev"; export const proxyWorkerHost = "vot-worker.kload.workers.dev"; // vot-worker.toil.cc export const votBackendUrl = "https://vot.toil.cc/v1"; @@ -52,7 +53,6 @@ export const defaultTranslationService: "yandexbrowser" | "msedge" = export const defaultDetectService: "yandexbrowser" | "msedge" | "rust-server" = "yandexbrowser"; -export const nonProxyExtensions: string[] = ["Tampermonkey", "Violentmonkey"]; export const proxyOnlyCountries: string[] = ["UA", "LV", "LT"]; /** diff --git a/src/core/auth.ts b/src/core/auth.ts index 29585714..cba377a2 100644 --- a/src/core/auth.ts +++ b/src/core/auth.ts @@ -1,5 +1,6 @@ import type { Account } from "../types/storage"; import { votStorage } from "../utils/storage"; +import { notifyAuthOpener } from "./authRefreshMessage"; type AuthProfilePayload = { avatar_id: string; @@ -51,6 +52,7 @@ async function handleAuthCallbackPage() { username: undefined, avatarId: undefined, }); + notifyAuthOpener(); } async function handleProfilePage() { @@ -70,6 +72,7 @@ async function handleProfilePage() { username, avatarId, }); + notifyAuthOpener(); } export async function initAuth() { diff --git a/src/core/authRefreshMessage.ts b/src/core/authRefreshMessage.ts new file mode 100644 index 00000000..6599f86d --- /dev/null +++ b/src/core/authRefreshMessage.ts @@ -0,0 +1,38 @@ +export const AUTH_REFRESH_MESSAGE_SOURCE = "vot-auth"; +export const AUTH_REFRESH_MESSAGE_TYPE = "account-updated"; + +export type AuthRefreshMessage = Readonly<{ + source: typeof AUTH_REFRESH_MESSAGE_SOURCE; + type: typeof AUTH_REFRESH_MESSAGE_TYPE; +}>; + +export function createAuthRefreshMessage(): AuthRefreshMessage { + return { + source: AUTH_REFRESH_MESSAGE_SOURCE, + type: AUTH_REFRESH_MESSAGE_TYPE, + }; +} + +export function isAuthRefreshMessage( + value: unknown, +): value is AuthRefreshMessage { + if (!value || typeof value !== "object") { + return false; + } + + const candidate = value as Partial; + return ( + candidate.source === AUTH_REFRESH_MESSAGE_SOURCE && + candidate.type === AUTH_REFRESH_MESSAGE_TYPE + ); +} + +export function notifyAuthOpener( + target: Pick | null | undefined = globalThis.opener, +): void { + if (!target || typeof target.postMessage !== "function") { + return; + } + + target.postMessage(createAuthRefreshMessage(), "*"); +} diff --git a/src/core/bootstrapPolicy.ts b/src/core/bootstrapPolicy.ts index 63feccff..8ad51089 100644 --- a/src/core/bootstrapPolicy.ts +++ b/src/core/bootstrapPolicy.ts @@ -1,9 +1,10 @@ -export type BootstrapMode = "skip" | "top-full" | "iframe-lazy"; +export type BootstrapMode = "skip" | "auth-eager" | "lazy"; export type BootstrapPolicyInput = { isIframe: boolean; href: string; origin: string; + authOrigin: string; }; export function shouldSkipIframeBootstrap( @@ -23,8 +24,8 @@ export function resolveBootstrapMode( if (shouldSkipIframeBootstrap(input)) { return "skip"; } - if (input.isIframe) { - return "iframe-lazy"; + if (!input.isIframe && input.origin === input.authOrigin) { + return "auth-eager"; } - return "top-full"; + return "lazy"; } diff --git a/src/core/cacheManager.ts b/src/core/cacheManager.ts index 9f2d9a2d..7bbdb61d 100644 --- a/src/core/cacheManager.ts +++ b/src/core/cacheManager.ts @@ -9,7 +9,6 @@ import { votStorage } from "../utils/storage"; import { fnv1a32ToKeyPart } from "../utils/utils"; export const YANDEX_TTL_MS = 2 * 60 * 60 * 1000; // 2 hours -export const VOT_SESSION_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour const RESPONSE_CACHE_CREATED_AT_HEADER = "x-vot-cache-created-at"; const RESPONSE_CACHE_KEY_HEADER = "x-vot-cache-key"; const DEFAULT_RESPONSE_CACHE_NAME = "vot-http-cache-v1"; @@ -46,13 +45,7 @@ type CacheReadResult = { expiresAt?: number; }; -type VOTSession = ClientSession; -type VOTSessions = Partial>; -type StoredVOTSession = { - secretKey: string; - uuid: string; - expiresAt: number; -}; +type VOTSessions = Partial>; type VOTSessionStorage = Pick< typeof votStorage, "getRaw" | "setRaw" | "deleteRaw" @@ -62,7 +55,7 @@ function getCurrentUnixTimestampSeconds(): number { return Math.floor(Date.now() / 1000); } -function isVOTSession(value: unknown): value is VOTSession { +function isClientSession(value: unknown): value is ClientSession { if (!value || typeof value !== "object") { return false; } @@ -94,7 +87,7 @@ function sanitizeVOTSessions(value: unknown): VOTSessions { const now = getCurrentUnixTimestampSeconds(); const entries = Object.entries(value as Record).flatMap( ([module, session]) => { - if (!isVOTSession(session)) { + if (!isClientSession(session)) { return []; } @@ -109,85 +102,49 @@ function sanitizeVOTSessions(value: unknown): VOTSessions { return Object.fromEntries(entries) as VOTSessions; } -function isStoredVOTSession(value: unknown): value is StoredVOTSession { - if (!value || typeof value !== "object") { - return false; - } - - const candidate = value as { - expiresAt?: unknown; - secretKey?: unknown; - uuid?: unknown; - }; - - return ( - typeof candidate.expiresAt === "number" && - Number.isFinite(candidate.expiresAt) && - typeof candidate.secretKey === "string" && - candidate.secretKey.length > 0 && - typeof candidate.uuid === "string" && - candidate.uuid.length > 0 - ); +function hasSessions(sessions: VOTSessions): boolean { + return Object.keys(sessions).length > 0; } export class VOTSessionStorageCache { constructor(private readonly storage: VOTSessionStorage = votStorage) {} - private getStorageKey(host: string): string { - void host; + private getStorageKey(): string { return VOT_SESSION_STORAGE_KEY; } async restore( - host: string, + _host: string, currentSessions: VOTSessions = {}, ): Promise { - const storageKey = this.getStorageKey(host); + const storageKey = this.getStorageKey(); const rawStoredSession = await this.storage.getRaw(storageKey); - if (!isStoredVOTSession(rawStoredSession)) { + const restoredSessions = sanitizeVOTSessions(rawStoredSession); + if (!hasSessions(restoredSessions)) { + if (rawStoredSession !== undefined) { + await this.storage.deleteRaw(storageKey); + } return currentSessions; } - const nowMs = Date.now(); - if (rawStoredSession.expiresAt <= nowMs) { - await this.storage.deleteRaw(storageKey); - return currentSessions; - } - - const remainingSeconds = Math.max( - 1, - Math.ceil((rawStoredSession.expiresAt - nowMs) / 1000), - ); return { ...currentSessions, - "video-translation": { - secretKey: rawStoredSession.secretKey, - uuid: rawStoredSession.uuid, - expires: remainingSeconds, - timestamp: Math.floor(nowMs / 1000), - }, + ...restoredSessions, }; } async persist( - host: string, + _host: string, sessions: VOTSessions | undefined, ): Promise { - void host; - const storageKey = this.getStorageKey(host); - const translationSession = - sanitizeVOTSessions(sessions)["video-translation"]; - if (!translationSession) { + const storageKey = this.getStorageKey(); + const sanitizedSessions = sanitizeVOTSessions(sessions); + if (!hasSessions(sanitizedSessions)) { await this.storage.deleteRaw(storageKey); return; } - await this.storage.setRaw(storageKey, { - secretKey: translationSession.secretKey, - uuid: translationSession.uuid, - expiresAt: - (translationSession.timestamp + translationSession.expires) * 1000, - } satisfies StoredVOTSession); + await this.storage.setRaw(storageKey, sanitizedSessions); } } @@ -322,67 +279,35 @@ class ResponseCacheManager { const allowStaleOnError = options.allowStaleOnError !== false; const nowMs = Date.now(); - let staleFallback: Response | undefined; - - if (useMemory) { - const memoryHit = this.readMemoryCache(key, nowMs); - if (memoryHit.fresh) { - return memoryHit.fresh; - } - staleFallback = memoryHit.stale ?? staleFallback; + const staleFallback = await this.readCachedResponse({ + key, + nowMs, + useMemory, + useCacheApi, + cacheName, + url: context.url, + cacheApiKey, + ttlMs, + allowStaleOnError, + }); + if (staleFallback.fresh) { + return staleFallback.fresh; } - if (useCacheApi) { - const cacheApiHit = await this.readCacheApi( - cacheName, - context.url, - cacheApiKey, - ttlMs, - nowMs, - allowStaleOnError, - ); - if (cacheApiHit.fresh) { - if (useMemory) { - this.writeMemoryCache( - key, - cacheApiHit.fresh.clone(), - cacheApiHit.expiresAt ?? nowMs + ttlMs, - ); - } - return cacheApiHit.fresh; - } - staleFallback = staleFallback ?? cacheApiHit.stale; - } - - const runNetworkRequest = async (): Promise => { - 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, context.url, cacheApiKey, storable); - } - - return response; - }; - if (!dedupe) { - try { - return await runNetworkRequest(); - } catch (err) { - if (allowStaleOnError && staleFallback) { - return staleFallback; - } - throw err; - } + return await this.runNetworkRequestWithFallback( + { + key, + cacheName, + url: context.url, + cacheApiKey, + ttlMs, + useMemory, + useCacheApi, + }, + fetcher, + allowStaleOnError ? staleFallback.stale : undefined, + ); } const inFlight = this.inFlightRequests.get(key); @@ -390,16 +315,19 @@ class ResponseCacheManager { return (await inFlight).clone(); } - const networkPromise = (async (): Promise => { - try { - return await runNetworkRequest(); - } catch (err) { - if (allowStaleOnError && staleFallback) { - return staleFallback.clone(); - } - throw err; - } - })(); + const networkPromise = this.runNetworkRequestWithFallback( + { + key, + cacheName, + url: context.url, + cacheApiKey, + ttlMs, + useMemory, + useCacheApi, + }, + fetcher, + allowStaleOnError ? staleFallback.stale?.clone() : undefined, + ); this.inFlightRequests.set(key, networkPromise); try { @@ -409,6 +337,126 @@ class ResponseCacheManager { } } + private async readCachedResponse({ + key, + nowMs, + useMemory, + useCacheApi, + cacheName, + url, + cacheApiKey, + ttlMs, + allowStaleOnError, + }: { + key: string; + nowMs: number; + useMemory: boolean; + useCacheApi: boolean; + cacheName: string; + url: string; + cacheApiKey: string; + ttlMs: number; + allowStaleOnError: boolean; + }): Promise<{ fresh?: Response; stale?: Response }> { + let staleFallback: Response | undefined; + + 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 }; + } + + private async runNetworkRequestWithFallback( + cacheConfig: { + key: string; + cacheName: string; + url: string; + cacheApiKey: string; + ttlMs: number; + useMemory: boolean; + useCacheApi: boolean; + }, + fetcher: () => Promise, + staleFallback?: Response, + ): Promise { + try { + return await this.runNetworkRequest(cacheConfig, fetcher); + } catch (err) { + if (staleFallback) { + return staleFallback; + } + throw err; + } + } + + private async runNetworkRequest( + { + key, + cacheName, + url, + cacheApiKey, + ttlMs, + useMemory, + useCacheApi, + }: { + key: string; + cacheName: string; + url: string; + cacheApiKey: string; + ttlMs: number; + useMemory: boolean; + useCacheApi: boolean; + }, + fetcher: () => Promise, + ): Promise { + 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; + } + private computeExpiresAt(createdAtMs: number, ttlMs: number): number { if (!Number.isFinite(ttlMs) || ttlMs <= 0) { return createdAtMs; @@ -437,7 +485,7 @@ class ResponseCacheManager { context: RequestCacheContext, ): string | undefined { const method = this.normalizeMethod(context.method); - if (method === "GET" || method === "HEAD") { + if (method === "GET") { return `${method}:${context.url}`; } diff --git a/src/core/eventImpl.ts b/src/core/eventImpl.ts index 389d32f1..61dc6c17 100644 --- a/src/core/eventImpl.ts +++ b/src/core/eventImpl.ts @@ -41,7 +41,7 @@ export class EventImpl { for (const handler of this.listeners) { try { const result = handler(...args); - if (result && typeof (result as Promise).then === "function") { + if (result && typeof result.then === "function") { pending.push(Promise.resolve(result)); } } catch (exception) { diff --git a/src/core/translationHandler.ts b/src/core/translationHandler.ts index cba30a52..51d6641b 100644 --- a/src/core/translationHandler.ts +++ b/src/core/translationHandler.ts @@ -102,6 +102,11 @@ type DownloadWaiter = { reject: (error: Error) => void; }; +type TranslateVideoImplOptions = { + disableLivelyVoice?: boolean; + retryAttempt?: number; +}; + export class VOTTranslationHandler { readonly videoHandler: VideoHandler; readonly audioDownloader: AudioDownloader; @@ -308,6 +313,17 @@ export class VOTTranslationHandler { }); } + private getVideoTranslationRetryDelayMs( + retryAttempt: number, + videoDurationSeconds: number, + ): number { + if (retryAttempt > 0) { + return 25_000; + } + + return videoDurationSeconds <= 10 * 60 ? 60_000 : 75_000; + } + async translateVideoImpl( videoData: VideoData, requestLang: RequestLang, @@ -315,10 +331,11 @@ export class VOTTranslationHandler { translationHelp: TranslationHelp[] | null = null, shouldSendFailedAudio = false, signal = NEVER_ABORTED_SIGNAL, - disableLivelyVoice = false, + options: TranslateVideoImplOptions = {}, ): Promise< (TranslatedVideoTranslationResponse & { usedLivelyVoice: boolean }) | null > { + const { disableLivelyVoice = false, retryAttempt = 0 } = options; clearTimeout(this.videoHandler.autoRetry); this.finishDownloadSuccess(); const requestLangForApi = this.videoHandler.getRequestLangForTranslation( @@ -339,53 +356,19 @@ export class VOTTranslationHandler { requestLangForApi, responseLang, ); - let useLivelyVoice = - !livelyDisabled && - livelyVoiceAllowed && - Boolean(this.videoHandler.data?.useLivelyVoice); - - let res: VideoTranslationResponse | undefined; - - // If server says lively voices are unavailable, immediately retry once - // without lively voices and keep that choice for subsequent retries. - for (let attempt = 0; attempt < 2; attempt++) { - try { - res = await this.videoHandler.votClient.translateVideo({ - videoData, - requestLang: requestLangForApi, - responseLang, - translationHelp, - extraOpts: { - useLivelyVoice, - videoTitle: this.videoHandler.videoData?.title, - }, - shouldSendFailedAudio, - }); - } catch (err) { - if (useLivelyVoice && this.isLivelyVoiceUnavailableError(err)) { - debug.log( - "[translateVideoImpl] Lively voices are unavailable. Falling back to standard translation.", - err, - ); - livelyDisabled = true; - useLivelyVoice = false; - continue; - } - throw err; - } - - if (useLivelyVoice && this.isLivelyVoiceUnavailableError(res)) { - debug.log( - "[translateVideoImpl] Server responded that lively voices are unavailable. Falling back to standard translation.", - res, - ); - livelyDisabled = true; - useLivelyVoice = false; - res = undefined; - continue; - } - break; - } + const translationAttempt = + await this.requestTranslationWithLivelyFallback({ + videoData, + requestLangForApi, + responseLang, + translationHelp, + shouldSendFailedAudio, + livelyDisabled, + livelyVoiceAllowed, + }); + livelyDisabled = translationAttempt.livelyDisabled; + const useLivelyVoice = translationAttempt.useLivelyVoice; + const res = translationAttempt.response; if (!res) { throw new Error("Failed to get translation response"); @@ -439,7 +422,10 @@ export class VOTTranslationHandler { translationHelp, true, signal, - livelyDisabled, + { + disableLivelyVoice: livelyDisabled, + retryAttempt, + }, ); } } catch (err) { @@ -487,13 +473,80 @@ export class VOTTranslationHandler { translationHelp, shouldSendFailedAudio, signal, - livelyDisabled, + { + disableLivelyVoice: livelyDisabled, + retryAttempt: retryAttempt + 1, + }, ), - 20000, + this.getVideoTranslationRetryDelayMs(retryAttempt, videoData.duration), signal, ); } + private async requestTranslationWithLivelyFallback({ + videoData, + requestLangForApi, + responseLang, + translationHelp, + shouldSendFailedAudio, + livelyDisabled, + livelyVoiceAllowed, + }: { + videoData: VideoData; + requestLangForApi: RequestLang; + responseLang: ResponseLang; + translationHelp: TranslationHelp[] | null; + shouldSendFailedAudio: boolean; + livelyDisabled: boolean; + livelyVoiceAllowed: boolean; + }): Promise<{ + response?: VideoTranslationResponse; + useLivelyVoice: boolean; + livelyDisabled: boolean; + }> { + let useLivelyVoice = + !livelyDisabled && + livelyVoiceAllowed && + Boolean(this.videoHandler.data?.useLivelyVoice); + + while (true) { + try { + 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)) { + return { response, useLivelyVoice, livelyDisabled }; + } + + debug.log( + "[translateVideoImpl] Server responded that lively voices are unavailable. Falling back to standard translation.", + response, + ); + } catch (err) { + if (!useLivelyVoice || !this.isLivelyVoiceUnavailableError(err)) { + throw err; + } + + debug.log( + "[translateVideoImpl] Lively voices are unavailable. Falling back to standard translation.", + err, + ); + } + + livelyDisabled = true; + useLivelyVoice = false; + } + } + private waitForAudioDownloadCompletion( signal: AbortSignal, timeoutMs: number, diff --git a/src/core/videoLifecycleController.ts b/src/core/videoLifecycleController.ts index a02f8479..89730be2 100644 --- a/src/core/videoLifecycleController.ts +++ b/src/core/videoLifecycleController.ts @@ -18,7 +18,7 @@ interface LifecycleUIManager { votOverlayView: LifecycleOverlayView; } -interface VideoLifecycleHost { +export interface VideoLifecycleHost { video: HTMLVideoElement; site: ServiceConf; container: HTMLElement; @@ -42,8 +42,12 @@ interface VideoLifecycleHost { getSubtitlesCacheKey( videoId: string, detectedLanguage: RequestLang, - responseLanguage: ResponseLang, + subtitleLanguage: string, ): string; + getPreferredSubtitlesLanguage( + detectedLanguage?: string, + responseLanguage?: string, + ): string | undefined; translationOrchestrator: { reset(): void; runAutoTranslationIfEligible(): Promise; @@ -333,16 +337,25 @@ export class VideoLifecycleController { return; } - const cacheKey = this.host.getSubtitlesCacheKey( - this.host.videoData.videoId, + 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 !== undefined ? cacheKey : null; + const cachedSubtitles = this.host.cacheManager.getSubtitles(cacheKey); + this.host.subtitles = cachedSubtitles ?? []; + this.host.subtitlesCacheKey = + cachedSubtitles !== undefined ? cacheKey : null; + } else { + this.host.subtitles = []; + this.host.subtitlesCacheKey = null; + } await this.host.updateSubtitlesLangSelect(); if (this.shouldAbortHandleSrcChanged(sessionId, "after subtitles update")) { diff --git a/src/core/videoLifecycleHost.ts b/src/core/videoLifecycleHost.ts new file mode 100644 index 00000000..386bc334 --- /dev/null +++ b/src/core/videoLifecycleHost.ts @@ -0,0 +1,99 @@ +import type { ResponseLang } from "@vot.js/shared/types/data"; + +import type { VideoHandler } from "../index"; +import type { OverlayMount } from "../types/uiManager"; +import type { VideoLifecycleHost } from "./videoLifecycleController"; + +export function createVideoLifecycleHost( + handler: VideoHandler, + resolveOverlayMount: (container: HTMLElement) => OverlayMount, +): VideoLifecycleHost { + const self = () => handler; + + return { + get video() { + return self().video; + }, + get site() { + return self().site; + }, + get container() { + return self().container; + }, + set container(value: HTMLElement) { + if (self().container === value) { + return; + } + self().container = value; + self().uiManager.updateMount(resolveOverlayMount(value)); + }, + get firstPlay() { + return self().firstPlay; + }, + set firstPlay(value: boolean) { + self().firstPlay = value; + }, + stopTranslation: () => handler.stopTranslation(), + get uiManager() { + return self().uiManager as VideoLifecycleHost["uiManager"]; + }, + getVideoData: () => handler.getVideoData(), + cacheManager: { + getSubtitles: (key: string) => self().cacheManager.getSubtitles(key), + }, + getSubtitlesCacheKey: ( + videoId: string, + detectedLanguage: string, + subtitleLanguage: string, + ) => + handler.getSubtitlesCacheKey(videoId, detectedLanguage, subtitleLanguage), + getPreferredSubtitlesLanguage: ( + detectedLanguage?: string, + responseLanguage?: string, + ) => + handler.getPreferredSubtitlesLanguage(detectedLanguage, responseLanguage), + updateSubtitlesLangSelect: () => handler.updateSubtitlesLangSelect(), + enableSubtitlesForCurrentLangPair: () => + handler.enableSubtitlesForCurrentLangPair(), + setSelectMenuValues: (from: string, to: string) => + handler.setSelectMenuValues(from, to), + get translateToLang() { + return self().translateToLang; + }, + set translateToLang(value: string) { + self().translateToLang = value as ResponseLang; + }, + get data() { + return self().data ?? {}; + }, + get subtitles() { + return self().subtitles; + }, + set subtitles(value: any[]) { + self().subtitles = value; + }, + get subtitlesCacheKey() { + return self().subtitlesCacheKey; + }, + set subtitlesCacheKey(value: string | null) { + self().subtitlesCacheKey = value; + }, + get videoData() { + return self().videoData; + }, + set videoData(value: any) { + self().videoData = value; + }, + get actionsAbortController() { + return self().actionsAbortController; + }, + set actionsAbortController(value: AbortController) { + self().actionsAbortController = value; + }, + resetActionsAbortController: (reason?: unknown) => + handler.resetActionsAbortController(reason), + translationOrchestrator: handler.translationOrchestrator, + resetSubtitlesWidget: () => handler.resetSubtitlesWidget(), + queueOverlayAutoHide: () => handler.overlayVisibility?.queueAutoHide(), + }; +} diff --git a/src/core/videoManager.ts b/src/core/videoManager.ts index bc520340..2e8ac781 100644 --- a/src/core/videoManager.ts +++ b/src/core/videoManager.ts @@ -343,14 +343,14 @@ export class VOTVideoManager { this.setDetectedLanguageCache(videoData.videoId, cacheLanguage); } - if (detectedLanguage === "auto") { + if (!detectedLanguage || detectedLanguage === "auto") { return; } - videoData.detectedLanguage = detectedLanguage; - if (this.videoHandler.translateFromLang === "auto") { - this.videoHandler.translateFromLang = detectedLanguage; - } + this.videoHandler.setSelectMenuValues( + detectedLanguage, + this.videoHandler.translateToLang, + ); } async getVideoData() { @@ -408,7 +408,7 @@ export class VOTVideoManager { title, localizedTitle, description, - downloadTitle: localizedTitle ?? title ?? videoId, + downloadTitle: localizedTitle ?? title ?? document.title ?? videoId, } satisfies RuntimeVideoData; if (sharedLanguageState.lastLoggedDetectedLanguage !== detectedLanguage) { @@ -475,8 +475,12 @@ export class VOTVideoManager { */ setVideoVolume(volume: number) { const snapped = snapVolume01(volume); + const shouldUnmute = snapped > 0; if (!isExternalVolumeHost(this.videoHandler.site.host)) { + if (shouldUnmute) { + this.videoHandler.video.muted = false; + } this.videoHandler.video.volume = snapped; return this; } @@ -485,7 +489,14 @@ export class VOTVideoManager { // Do NOT use a truthy check here, or setting volume to 0 (0%) will be treated // as a failure. try { + const player = YoutubeHelper.getPlayer() as + | { unMute?: () => void } + | undefined; const result = YoutubeHelper.setVolume(snapped) as unknown; + if (shouldUnmute) { + player?.unMute?.(); + this.videoHandler.video.muted = false; + } const ok = (typeof result === "boolean" && result) || (typeof result === "number" && Number.isFinite(result)); @@ -494,6 +505,9 @@ export class VOTVideoManager { // ignore - fall back to setting the HTMLMediaElement volume below. } + if (shouldUnmute) { + this.videoHandler.video.muted = false; + } this.videoHandler.video.volume = snapped; return this; } diff --git a/src/extension/background.ts b/src/extension/background.ts index 632a35a6..c94825d1 100644 --- a/src/extension/background.ts +++ b/src/extension/background.ts @@ -10,8 +10,12 @@ */ import debug from "../utils/debug"; -import { arrayBufferToBase64, base64ToBytes, bytesToBase64 } from "./base64"; +import { registerBackgroundNotifications } from "./backgroundNotifications"; +import { registerBackgroundStorageBridge } from "./backgroundStorage"; import { + arrayBufferToBase64, + base64ToBytes, + bytesToBase64, coerceBodyToBytes, decodeSerializedBody, summarizeBodyForDebug, @@ -20,10 +24,7 @@ import { PORT_NAME } from "./constants"; import { ext, lastErrorMessage, - notificationsClear, - notificationsCreate, - tabsUpdate, - windowsUpdate, + runtimeMessagesUseStructuredClone, } from "./webext"; import { filterYandexHeadersForDnr, @@ -56,6 +57,9 @@ type XhrAbortMessage = { type: "abort" }; type XhrPortMessage = XhrStartMessage | XhrAbortMessage; const MAX_INLINE_BINARY_RESPONSE_BYTES = 512 * 1024; +const PROTOBUF_CONTENT_TYPE_RE = + /application\/(?:x-)?protobuf|application\/octet-stream/; +const STRICT_BASE64_PAYLOAD_RE = /^(?=.*[+/=_-])[A-Za-z0-9+/=_-]+$/; // ----------------------------- // declarativeNetRequest helper @@ -139,15 +143,23 @@ function updateSessionRules(args: { } function isForbiddenToSetViaFetch(headerName: string): boolean { - const n = normalizeHeaderName(headerName).toLowerCase(); - if (!n) return false; - // Fetch "forbidden header names" (subset) that matter for VOT. - if (n.startsWith("sec-")) return true; - if (n.startsWith("proxy-")) return true; - if (n === "user-agent") return true; - if (n === "origin") return true; - if (n === "referer") return true; - return false; + const normalized = normalizeHeaderName(headerName).toLowerCase(); + if (!normalized) return false; + + switch (normalized[0]) { + case "s": + return normalized.startsWith("sec-"); + case "p": + return normalized.startsWith("proxy-"); + case "u": + return normalized === "user-agent"; + case "o": + return normalized === "origin"; + case "r": + return normalized === "referer"; + default: + return false; + } } function signatureFromDnrRequestHeaders( @@ -400,13 +412,13 @@ type XhrResponse = { status: number; statusText: string; responseHeaders: string; - // Extra metadata to help the content-script bridge reconstruct binary bodies - // without having to ship non-JSON types through extension messaging. + // Extra metadata to help the content-script bridge reconstruct binary bodies. + // Chromium still needs a JSON-safe fallback, while Firefox-like runtimes can + // carry native ArrayBuffer payloads over the port directly. responseType?: string; contentType?: string; - // Optional fallback for binary payloads in terminal messages. - responseB64?: string; response?: unknown; + responseB64?: string; responseText?: string; error?: string; }; @@ -424,6 +436,38 @@ function createTerminalXhrError(url: string, error: string): XhrResponse { }; } +function cloneArrayBufferView(view: Uint8Array): ArrayBuffer { + const out = new Uint8Array(view.byteLength); + out.set(view); + return out.buffer; +} + +function encodeProgressChunkForPort(chunk: Uint8Array): { + chunk?: ArrayBuffer; + chunkB64?: string; +} { + if (runtimeMessagesUseStructuredClone) { + return { chunk: cloneArrayBufferView(chunk) }; + } + + return { chunkB64: bytesToBase64(chunk) }; +} + +function encodeBinaryResponseForPort(ab: ArrayBuffer): { + response?: ArrayBuffer; + responseB64?: string; +} { + if (runtimeMessagesUseStructuredClone) { + return { response: ab }; + } + + if (ab.byteLength <= 0) { + return {}; + } + + return { responseB64: arrayBufferToBase64(ab) }; +} + function getHeader( headers: Record, name: string, @@ -436,21 +480,13 @@ function getHeader( } function isProtobufContentType(contentType: string | undefined): boolean { - const ct = String(contentType || "").toLowerCase(); - if (!ct) return false; - return ( - ct.includes("application/x-protobuf") || - ct.includes("application/protobuf") || - ct.includes("application/octet-stream") - ); + return PROTOBUF_CONTENT_TYPE_RE.test(String(contentType ?? "").toLowerCase()); } function tryDecodeStrictBase64Payload(s: string): Uint8Array | null { - const str = String(s || ""); - if (!str || /\s/.test(str)) return null; - if (!/^[A-Za-z0-9+/=_-]+$/.test(str)) return null; + const str = String(s ?? ""); // Require explicit base64/base64url markers to avoid decoding plain tokens. - if (!/[+/=_-]/.test(str)) return null; + if (!STRICT_BASE64_PAYLOAD_RE.test(str)) return null; const normalized = str.replaceAll("-", "+").replaceAll("_", "/"); const remainder = normalized.length % 4; @@ -525,38 +561,26 @@ ext?.runtime?.onConnect?.addListener?.((port: unknown) => { debug.warn("[VOT EXT][background][xhr] port disconnected", { xhrSessionId, }); + requestAbort(); + }); + + const requestAbort = () => { try { controller?.abort(); } catch { // ignore } - }); + }; - typedPort.onMessage.addListener(async (msg: XhrPortMessage) => { - if (!msg || typeof msg !== "object") return; + const handleAbortMessage = () => { + abortedByUser = true; + debug.warn("[VOT EXT][background][xhr] abort requested", { + xhrSessionId, + }); + requestAbort(); + }; - if (msg.type === "abort") { - abortedByUser = true; - debug.warn("[VOT EXT][background][xhr] abort requested", { - xhrSessionId, - }); - try { - controller?.abort(); - } catch { - // ignore - } - return; - } - - if (msg.type !== "start") return; - // Preserve "abort-before-start" semantics only when no request has been - // started on this port yet. Otherwise, allow the next request to proceed. - const shouldAbortImmediately = abortedByUser && controller === null; - abortedByUser = false; - - const { details } = msg; - const url = details.url; - const method = (details.method || "GET").toUpperCase(); + const splitRequestHeaders = (details: XhrStartMessage["details"]) => { const allHeaders = toHeaderRecord(details.headers); const headers: Record = {}; const forbiddenHeaders: Record = {}; @@ -564,6 +588,332 @@ ext?.runtime?.onConnect?.addListener?.((port: unknown) => { if (isForbiddenToSetViaFetch(k)) forbiddenHeaders[k] = v; else headers[k] = v; } + return { allHeaders, headers, forbiddenHeaders }; + }; + + const postAbortBeforeStart = (url: string) => { + const errorObj: XhrResponse = { + finalUrl: url, + readyState: 4, + status: 0, + statusText: "", + responseHeaders: "", + response: null, + responseText: "", + error: "Aborted", + }; + try { + safePostMessage({ type: "abort", state: "terminal", error: errorObj }); + } catch { + // ignore + } + }; + + const resolveFetchCredentials = ( + details: XhrStartMessage["details"], + ): RequestCredentials => + details.anonymous || details.withCredentials === false ? "omit" : "include"; + + const resolveFetchCache = ( + details: XhrStartMessage["details"], + ): RequestCache | undefined => { + if (details.nocache) return "no-store"; + if (details.revalidate) return "no-cache"; + return undefined; + }; + + const normalizeRequestBody = ( + details: XhrStartMessage["details"], + method: string, + allHeaders: Record, + url: string, + ): BodyInit | undefined => { + const isBodyAllowed = method !== "GET"; + if (!isBodyAllowed) { + return undefined; + } + + let body = decodeSerializedBody(details.data); + let recoveredBodyFromDetails: Uint8Array | null | undefined; + const getRecoveredBodyFromDetails = (): Uint8Array | null => { + if (recoveredBodyFromDetails !== undefined) { + return recoveredBodyFromDetails; + } + recoveredBodyFromDetails = coerceBodyToBytes(details.data); + return recoveredBodyFromDetails; + }; + const contentType = getHeader(allHeaders, "content-type"); + const isProtobufRequest = isProtobufContentType(contentType); + + debug.log("[VOT EXT][background][xhr] body decoded", { + xhrSessionId, + url, + method, + contentType: contentType ?? null, + isProtobufRequest, + sourceBody: summarizeBodyForDebug(details.data), + decodedBody: summarizeBodyForDebug(body), + }); + + if (isProtobufRequest && (body === undefined || body === null)) { + const recovered = getRecoveredBodyFromDetails(); + if (recovered) { + body = recovered as unknown as BodyInit; + debug.warn( + "[VOT EXT][background][xhr] protobuf body recovered from raw payload", + { + xhrSessionId, + url, + method, + recoveredBody: summarizeBodyForDebug(body), + }, + ); + } + } + + // Preserve Protobuf request bodies that were built as binary strings, + // which fetch() would otherwise UTF-8 re-encode and corrupt. + if (typeof body === "string" && isProtobufRequest) { + if (looksLikeObjectToStringPayload(body)) { + const recovered = getRecoveredBodyFromDetails(); + if (recovered) { + body = recovered as unknown as BodyInit; + debug.warn( + "[VOT EXT][background][xhr] recovered protobuf body from object-like string fallback", + { + xhrSessionId, + url, + method, + sourceBody: summarizeBodyForDebug(details.data), + recoveredBody: summarizeBodyForDebug(recovered), + }, + ); + } + } + + if (typeof body === "string") { + const maybeBase64Bytes = tryDecodeStrictBase64Payload(body); + body = (maybeBase64Bytes ?? + latin1StringToBytes(body)) as unknown as BodyInit; + debug.log( + "[VOT EXT][background][xhr] protobuf string converted to bytes", + { + xhrSessionId, + url, + method, + strategy: maybeBase64Bytes ? "base64" : "latin1", + convertedBody: summarizeBodyForDebug(body), + }, + ); + } + } + + return body; + }; + + const createRequestInit = (params: { + method: string; + headers: Record; + redirect: RequestRedirect; + credentials: RequestCredentials; + body: BodyInit | undefined; + cache: RequestCache | undefined; + }): RequestInit => { + const requestInit: RequestInit = { + method: params.method, + headers: params.headers, + redirect: params.redirect, + credentials: params.credentials, + signal: controller?.signal, + }; + if (params.body !== undefined) { + requestInit.body = params.body; + } + if (params.cache !== undefined) { + requestInit.cache = params.cache; + } + return requestInit; + }; + + const handleFetchSuccess = async (params: { + url: string; + method: string; + responseType: string; + res: Response; + }) => { + const { url, method, responseType, res } = params; + debug.log("[VOT EXT][background][xhr] fetch response received", { + xhrSessionId, + url: res.url || url, + method, + status: res.status, + statusText: res.statusText, + responseType, + contentType: res.headers.get("content-type") || null, + contentLength: res.headers.get("content-length") || null, + }); + + const responseHeaders = formatHeaders(res.headers); + const finalUrl = res.url || url; + const responseContentType = res.headers.get("content-type") || ""; + const makeBase = ( + readyState: number, + ): Omit< + XhrResponse, + "response" | "responseText" | "error" | "responseB64" + > => ({ + finalUrl, + readyState, + status: res.status, + statusText: res.statusText, + responseHeaders, + }); + + const wantBinary = + responseType === "arraybuffer" || + responseType === "blob" || + responseType === "stream"; + + let response: unknown; + let responseText: string | undefined; + let responseB64: string | undefined; + + if (wantBinary) { + const contentLength = Number(res.headers.get("content-length") || 0); + const shouldStreamBinary = + !!res.body && + (!Number.isFinite(contentLength) || + contentLength > MAX_INLINE_BINARY_RESPONSE_BYTES); + + if (shouldStreamBinary) { + let loaded = 0; + const total = Number.isFinite(contentLength) ? contentLength : 0; + const reader = res.body.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + if (!value) continue; + + loaded += value.byteLength; + safePostMessage({ + type: "progress", + state: "in_flight", + progress: { + ...makeBase(3), + loaded, + total, + lengthComputable: total > 0, + ...encodeProgressChunkForPort(value), + }, + }); + } + } else { + const ab = await res.arrayBuffer(); + const binaryResponse = encodeBinaryResponseForPort(ab); + response = binaryResponse.response; + responseB64 = binaryResponse.responseB64; + } + } else if (responseType === "json") { + responseText = await res.text(); + try { + response = JSON.parse(responseText); + } catch { + response = null; + } + } else { + responseText = await res.text(); + response = responseText; + } + + cleanup(); + debug.log("[VOT EXT][background][xhr] terminal", { + xhrSessionId, + state: "terminal", + kind: "load", + url: finalUrl, + status: res.status, + responseType, + responseBody: summarizeBodyForDebug(response), + responseTextLength: responseText?.length ?? 0, + responseB64Length: responseB64?.length ?? 0, + }); + safePostMessage({ + type: "load", + state: "terminal", + response: { + ...makeBase(4), + responseType, + ...(responseContentType ? { contentType: responseContentType } : {}), + ...(responseB64 ? { responseB64 } : {}), + response, + ...(typeof responseText === "string" ? { responseText } : {}), + } satisfies XhrResponse, + }); + }; + + const handleFetchFailure = ( + url: string, + method: string, + responseType: string, + err: unknown, + ) => { + cleanup(); + const isAbort = + abortedByUser || + timedOut || + (err instanceof DOMException && err.name === "AbortError"); + + if (isAbort) { + const kind: "abort" | "timeout" = timedOut ? "timeout" : "abort"; + const errorObj = createTerminalXhrError( + url, + kind === "timeout" ? "Timeout" : "Aborted", + ); + + try { + safePostMessage({ type: kind, state: "terminal", error: errorObj }); + } catch { + // ignore + } + debug.warn("[VOT EXT][background][xhr] terminal", { + xhrSessionId, + state: "terminal", + kind, + url, + method, + responseType, + }); + return; + } + + const errorObj = createTerminalXhrError(url, asErrorMessage(err)); + safePostMessage({ + type: "error", + state: "terminal", + error: errorObj, + }); + debug.error("[VOT EXT][background][xhr] terminal", { + xhrSessionId, + state: "terminal", + kind: "error", + url, + method, + responseType, + error: errorObj.error, + }); + }; + + const handleStartMessage = async ( + msg: Extract, + ) => { + const shouldAbortImmediately = abortedByUser && controller === null; + abortedByUser = false; + + const { details } = msg; + const url = details.url; + const method = (details.method || "GET").toUpperCase(); + const { allHeaders, headers, forbiddenHeaders } = + splitRequestHeaders(details); const timeout = Number(details.timeout || 0); const responseType = String(details.responseType || "text").toLowerCase(); debug.log("[VOT EXT][background][xhr] start", { @@ -579,24 +929,8 @@ ext?.runtime?.onConnect?.addListener?.((port: unknown) => { body: summarizeBodyForDebug(details.data), }); - // If the caller aborted before we even received the start payload, - // respond immediately without starting a network request. if (shouldAbortImmediately) { - const errorObj: XhrResponse = { - finalUrl: url, - readyState: 4, - status: 0, - statusText: "", - responseHeaders: "", - response: null, - responseText: "", - error: "Aborted", - }; - try { - safePostMessage({ type: "abort", state: "terminal", error: errorObj }); - } catch { - // ignore - } + postAbortBeforeStart(url); return; } @@ -606,36 +940,18 @@ ext?.runtime?.onConnect?.addListener?.((port: unknown) => { if (timeout > 0) { timeoutId = setTimeout(() => { timedOut = true; - try { - controller?.abort(); - } catch { - // ignore - } + requestAbort(); }, timeout) as unknown as number; } - let credentials: RequestCredentials = "include"; - if (details.anonymous || details.withCredentials === false) { - credentials = "omit"; - } + const credentials = resolveFetchCredentials(details); + const cache = resolveFetchCache(details); + const redirect: RequestRedirect = + details.redirect === "error" || details.redirect === "manual" + ? details.redirect + : "follow"; try { - let cache: RequestCache | undefined; - if (details.nocache) { - cache = "no-store"; - } else if (details.revalidate) { - cache = "no-cache"; - } - - const redirect: RequestRedirect = - details.redirect === "error" || details.redirect === "manual" - ? details.redirect - : "follow"; - - const isBodyAllowed = method !== "GET" && method !== "HEAD"; - // Make sure "forbidden" headers (Sec-*, User-Agent, ... ) are applied. - // Without this, no-proxy mode cannot faithfully emulate the userscript - // manager's GM_xmlhttpRequest and Yandex endpoints reject the request. try { await ensureDnrStripRuleForGooglevideo(url); await ensureDnrOriginStripRuleForYoutubei(url); @@ -647,88 +963,7 @@ ext?.runtime?.onConnect?.addListener?.((port: unknown) => { ); } - let body: BodyInit | undefined = isBodyAllowed - ? decodeSerializedBody(details.data) - : undefined; - let recoveredBodyFromDetails: Uint8Array | null | undefined; - const getRecoveredBodyFromDetails = (): Uint8Array | null => { - if (recoveredBodyFromDetails !== undefined) { - return recoveredBodyFromDetails; - } - recoveredBodyFromDetails = coerceBodyToBytes(details.data); - return recoveredBodyFromDetails; - }; - const contentType = getHeader(allHeaders, "content-type"); - const isProtobufRequest = isProtobufContentType(contentType); - - debug.log("[VOT EXT][background][xhr] body decoded", { - xhrSessionId, - url, - method, - contentType: contentType ?? null, - isProtobufRequest, - sourceBody: summarizeBodyForDebug(details.data), - decodedBody: summarizeBodyForDebug(body), - }); - - if ( - isBodyAllowed && - isProtobufRequest && - (body === undefined || body === null) - ) { - const recovered = getRecoveredBodyFromDetails(); - if (recovered) { - body = recovered as unknown as BodyInit; - debug.warn( - "[VOT EXT][background][xhr] protobuf body recovered from raw payload", - { - xhrSessionId, - url, - method, - recoveredBody: summarizeBodyForDebug(body), - }, - ); - } - } - - // Preserve Protobuf request bodies that were built as "binary strings" - // (Latin-1/byte-per-code-unit), which would otherwise be UTF-8 re-encoded - // by fetch() and corrupt the payload. - if (isBodyAllowed && typeof body === "string" && isProtobufRequest) { - if (looksLikeObjectToStringPayload(body)) { - const recovered = getRecoveredBodyFromDetails(); - if (recovered) { - body = recovered as unknown as BodyInit; - debug.warn( - "[VOT EXT][background][xhr] recovered protobuf body from object-like string fallback", - { - xhrSessionId, - url, - method, - sourceBody: summarizeBodyForDebug(details.data), - recoveredBody: summarizeBodyForDebug(recovered), - }, - ); - } - } - - if (typeof body === "string") { - const maybeBase64Bytes = tryDecodeStrictBase64Payload(body); - body = (maybeBase64Bytes ?? - latin1StringToBytes(body)) as unknown as BodyInit; - debug.log( - "[VOT EXT][background][xhr] protobuf string converted to bytes", - { - xhrSessionId, - url, - method, - strategy: maybeBase64Bytes ? "base64" : "latin1", - convertedBody: summarizeBodyForDebug(body), - }, - ); - } - } - + const body = normalizeRequestBody(details, method, allHeaders, url); debug.log("[VOT EXT][background][xhr] fetch dispatch", { xhrSessionId, url, @@ -739,347 +974,31 @@ ext?.runtime?.onConnect?.addListener?.((port: unknown) => { body: summarizeBodyForDebug(body), }); - const requestInit: RequestInit = { + const requestInit = createRequestInit({ method, headers, redirect, credentials, - signal: controller.signal, - }; - if (body !== undefined) { - requestInit.body = body; - } - if (cache !== undefined) { - requestInit.cache = cache; - } - + body, + cache, + }); const res = await fetch(url, requestInit); - - debug.log("[VOT EXT][background][xhr] fetch response received", { - xhrSessionId, - url: res.url || url, - method, - status: res.status, - statusText: res.statusText, - responseType, - contentType: res.headers.get("content-type") || null, - contentLength: res.headers.get("content-length") || null, - }); - - const responseHeaders = formatHeaders(res.headers); - const finalUrl = res.url || url; - const responseContentType = res.headers.get("content-type") || ""; - - const makeBase = ( - readyState: number, - ): Omit< - XhrResponse, - "response" | "responseText" | "error" | "responseB64" - > => ({ - finalUrl, - readyState, - status: res.status, - statusText: res.statusText, - responseHeaders, - }); - - const wantBinary = - responseType === "arraybuffer" || - responseType === "blob" || - responseType === "stream"; - - let response: unknown; - let responseText: string | undefined; - let responseB64: string | undefined; - - if (wantBinary) { - const contentLength = Number(res.headers.get("content-length") || 0); - const shouldStreamBinary = - !!res.body && - (!Number.isFinite(contentLength) || - contentLength > MAX_INLINE_BINARY_RESPONSE_BYTES); - - if (shouldStreamBinary) { - // Large binaries are streamed as progress chunks to avoid one huge - // base64 payload in extension messaging. - let loaded = 0; - const total = Number.isFinite(contentLength) ? contentLength : 0; - const reader = res.body.getReader(); - while (true) { - const { done, value } = await reader.read(); - if (done) break; - if (!value) continue; - - loaded += value.byteLength; - safePostMessage({ - type: "progress", - state: "in_flight", - progress: { - ...makeBase(3), - loaded, - total, - lengthComputable: total > 0, - chunkB64: bytesToBase64(value), - }, - }); - } - response = undefined; - } else { - // Small binaries can be delivered in one terminal payload. - const ab = await res.arrayBuffer(); - if (ab.byteLength > 0) { - responseB64 = arrayBufferToBase64(ab); - } - response = undefined; - } - } else if (responseType === "json") { - responseText = await res.text(); - try { - response = JSON.parse(responseText); - } catch { - response = null; - } - } else { - responseText = await res.text(); - response = responseText; - } - - cleanup(); - debug.log("[VOT EXT][background][xhr] terminal", { - xhrSessionId, - state: "terminal", - kind: "load", - url: finalUrl, - status: res.status, - responseType, - responseBody: summarizeBodyForDebug(response), - responseTextLength: responseText?.length ?? 0, - responseB64Length: responseB64?.length ?? 0, - }); - safePostMessage({ - type: "load", - state: "terminal", - response: { - ...makeBase(4), - responseType, - ...(responseContentType ? { contentType: responseContentType } : {}), - ...(responseB64 ? { responseB64 } : {}), - response, - ...(typeof responseText === "string" ? { responseText } : {}), - } satisfies XhrResponse, - }); + await handleFetchSuccess({ url, method, responseType, res }); } catch (err) { - cleanup(); - - const isAbort = - abortedByUser || - timedOut || - (err instanceof DOMException && err.name === "AbortError"); - - if (isAbort) { - let kind: "abort" | "timeout"; - if (timedOut) { - kind = "timeout"; - } else { - kind = "abort"; - } - const errorObj = createTerminalXhrError( - url, - kind === "timeout" ? "Timeout" : "Aborted", - ); - - try { - safePostMessage({ type: kind, state: "terminal", error: errorObj }); - } catch { - // ignore - } - debug.warn("[VOT EXT][background][xhr] terminal", { - xhrSessionId, - state: "terminal", - kind, - url, - method, - responseType, - }); - return; - } - - const errorObj = createTerminalXhrError(url, asErrorMessage(err)); - - safePostMessage({ - type: "error", - state: "terminal", - error: errorObj, - }); - debug.error("[VOT EXT][background][xhr] terminal", { - xhrSessionId, - state: "terminal", - kind: "error", - url, - method, - responseType, - error: errorObj.error, - }); + handleFetchFailure(url, method, responseType, err); } + }; + + typedPort.onMessage.addListener(async (msg: XhrPortMessage) => { + if (!msg || typeof msg !== "object") return; + if (msg.type === "abort") { + handleAbortMessage(); + return; + } + if (msg.type !== "start") return; + await handleStartMessage(msg); }); }); -// ----------------------------- -// GM_notification bridge -// ----------------------------- - -type GmNotificationSender = { - tab?: { - id?: number; - windowId?: number; - }; -}; - -type GmNotificationDetails = { - title: string; - text: string; - silent: boolean; - timeout: number; -}; - -type GmNotificationMessage = { - type: "gm_notification"; - details?: unknown; -}; - -export function isGmNotificationMessage( - msg: unknown, -): msg is GmNotificationMessage { - if (!msg || typeof msg !== "object") return false; - return (msg as { type?: unknown }).type === "gm_notification"; -} - -export function normalizeGmNotificationDetails( - details: unknown, -): GmNotificationDetails { - const raw = - details && typeof details === "object" - ? (details as Record) - : {}; - - const timeoutRaw = Number(raw.timeout ?? 0); - return { - title: raw.title != null ? String(raw.title) : "", - text: raw.text != null ? String(raw.text) : "", - silent: Boolean(raw.silent), - timeout: Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? timeoutRaw : 0, - }; -} - -export function createBridgeNotificationId( - sender: GmNotificationSender, -): string { - const tabId = sender.tab?.id; - const windowId = sender.tab?.windowId; - - const safeTab = typeof tabId === "number" ? tabId : -1; - const safeWin = typeof windowId === "number" ? windowId : -1; - - const nonce = - typeof crypto !== "undefined" && "randomUUID" in crypto - ? crypto.randomUUID() - : `${Date.now()}:${Math.random().toString(36).slice(2)}`; - - return `vot:${safeTab}:${safeWin}:${nonce}`; -} - -export function createBridgeNotificationOptions( - details: GmNotificationDetails, -): Record { - const isFirefox = - typeof (ext?.runtime as { getBrowserInfo?: unknown })?.getBrowserInfo === - "function"; - const iconUrl = ext?.runtime?.getURL - ? ext.runtime.getURL("icons/icon-128.png") - : "icons/icon-128.png"; - - // Firefox's notifications API does not support some Chrome-only fields - // (e.g. `silent`). Passing unsupported fields can cause the call to throw - // and the notification to never show. - const options: Record = { - type: "basic", - iconUrl, - title: details.title || "VOT", - message: details.text, - }; - if (!isFirefox) { - options.silent = details.silent; - } - return options; -} - -function sendNotificationResponse( - sendResponse: ((value: unknown) => void) | undefined, - payload: unknown, -): void { - if (typeof sendResponse !== "function") return; - try { - sendResponse(payload); - } catch { - // ignore - } -} - -ext?.runtime?.onMessage?.addListener?.( - ( - msg: unknown, - sender: GmNotificationSender, - sendResponse: ((value: unknown) => void) | undefined, - ) => { - if (!isGmNotificationMessage(msg)) return; - - const details = normalizeGmNotificationDetails(msg.details); - const notificationId = createBridgeNotificationId(sender); - const options = createBridgeNotificationOptions(details); - - void (async () => { - try { - await notificationsCreate(notificationId, options); - - if (details.timeout > 0) { - setTimeout(() => { - void notificationsClear(notificationId); - }, details.timeout); - } - - sendNotificationResponse(sendResponse, { ok: true }); - } catch (error) { - // Avoid crashing the background service worker on notification API errors. - debug.error( - "[VOT EXT][background] Failed to create notification", - error, - ); - sendNotificationResponse(sendResponse, { - ok: false, - error: asErrorMessage(error), - }); - } - })(); - - // Keep the channel alive while notification creation finishes in MV3 SW. - return true; - }, -); - -ext?.notifications?.onClicked?.addListener?.((notificationId: string) => { - if (!notificationId.startsWith("vot:")) return; - const parts = notificationId.split(":"); - if (parts.length < 3) return; - - const tabId = Number(parts[1]); - const windowId = Number(parts[2]); - - // Focus the originating tab/globalThis when possible. - if (Number.isFinite(windowId) && windowId >= 0) { - void windowsUpdate(windowId, { focused: true }); - } - - if (Number.isFinite(tabId) && tabId >= 0) { - void tabsUpdate(tabId, { active: true }); - } -}); +registerBackgroundStorageBridge(); +registerBackgroundNotifications(); diff --git a/src/extension/backgroundNotifications.ts b/src/extension/backgroundNotifications.ts new file mode 100644 index 00000000..a455e9e3 --- /dev/null +++ b/src/extension/backgroundNotifications.ts @@ -0,0 +1,176 @@ +import debug from "../utils/debug"; +import { + ext, + notificationsClear, + notificationsCreate, + tabsUpdate, + windowsUpdate, +} from "./webext"; + +type GmNotificationSender = { + tab?: { + id?: number; + windowId?: number; + }; +}; + +type GmNotificationDetails = { + title: string; + text: string; + silent: boolean; + timeout: number; +}; + +type GmNotificationMessage = { + type: "gm_notification"; + details?: unknown; +}; + +function asErrorMessage(err: unknown): string { + if (err instanceof Error) return err.message; + if (err && typeof err === "object") { + try { + return JSON.stringify(err); + } catch { + return Object.prototype.toString.call(err); + } + } + try { + return String(err); + } catch { + return "Unknown error"; + } +} + +function sendBridgeResponse( + sendResponse: ((value: unknown) => void) | undefined, + payload: unknown, +): void { + if (typeof sendResponse !== "function") return; + try { + sendResponse(payload); + } catch { + // ignore + } +} + +export function isGmNotificationMessage( + msg: unknown, +): msg is GmNotificationMessage { + if (!msg || typeof msg !== "object") return false; + return (msg as { type?: unknown }).type === "gm_notification"; +} + +export function normalizeGmNotificationDetails( + details: unknown, +): GmNotificationDetails { + const raw = + details && typeof details === "object" + ? (details as Record) + : {}; + + const timeoutRaw = Number(raw.timeout ?? 0); + return { + title: raw.title != null ? String(raw.title) : "", + text: raw.text != null ? String(raw.text) : "", + silent: Boolean(raw.silent), + timeout: Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? timeoutRaw : 0, + }; +} + +export function createBridgeNotificationId( + sender: GmNotificationSender, +): string { + const tabId = sender.tab?.id; + const windowId = sender.tab?.windowId; + + const safeTab = typeof tabId === "number" ? tabId : -1; + const safeWin = typeof windowId === "number" ? windowId : -1; + + const nonce = + typeof crypto !== "undefined" && "randomUUID" in crypto + ? crypto.randomUUID() + : `${Date.now()}:${Math.random().toString(36).slice(2)}`; + + return `vot:${safeTab}:${safeWin}:${nonce}`; +} + +export function createBridgeNotificationOptions( + details: GmNotificationDetails, +): Record { + const isFirefox = + typeof (ext?.runtime as { getBrowserInfo?: unknown })?.getBrowserInfo === + "function"; + const iconUrl = ext?.runtime?.getURL + ? ext.runtime.getURL("icons/icon-128.png") + : "icons/icon-128.png"; + + const options: Record = { + type: "basic", + iconUrl, + title: details.title || "VOT", + message: details.text, + }; + if (!isFirefox) { + options.silent = details.silent; + } + return options; +} + +export function registerBackgroundNotifications(): void { + ext?.runtime?.onMessage?.addListener?.( + ( + msg: unknown, + sender: GmNotificationSender, + sendResponse: ((value: unknown) => void) | undefined, + ) => { + if (!isGmNotificationMessage(msg)) return; + + const details = normalizeGmNotificationDetails(msg.details); + const notificationId = createBridgeNotificationId(sender); + const options = createBridgeNotificationOptions(details); + + void (async () => { + try { + await notificationsCreate(notificationId, options); + + if (details.timeout > 0) { + setTimeout(() => { + void notificationsClear(notificationId); + }, details.timeout); + } + + sendBridgeResponse(sendResponse, { ok: true }); + } catch (error) { + debug.error( + "[VOT EXT][background] Failed to create notification", + error, + ); + sendBridgeResponse(sendResponse, { + ok: false, + error: asErrorMessage(error), + }); + } + })(); + + return true; + }, + ); + + ext?.notifications?.onClicked?.addListener?.((notificationId: string) => { + if (!notificationId.startsWith("vot:")) return; + const parts = notificationId.split(":"); + if (parts.length < 3) return; + + const tabId = Number(parts[1]); + const windowId = Number(parts[2]); + + if (Number.isFinite(windowId) && windowId >= 0) { + void windowsUpdate(windowId, { focused: true }); + } + + if (Number.isFinite(tabId) && tabId >= 0) { + void tabsUpdate(tabId, { active: true }); + } + }); +} diff --git a/src/extension/backgroundStorage.ts b/src/extension/backgroundStorage.ts new file mode 100644 index 00000000..21aa3f50 --- /dev/null +++ b/src/extension/backgroundStorage.ts @@ -0,0 +1,121 @@ +import { ext, storageGet, storageRemove, storageSet } from "./webext"; + +type GmStorageMessage = { + type: "gm_storage"; + action?: string; + payload?: Record; +}; + +function isGmStorageMessage(msg: unknown): msg is GmStorageMessage { + if (!msg || typeof msg !== "object") return false; + return (msg as { type?: unknown }).type === "gm_storage"; +} + +function normalizeStorageRequestKey(value: unknown): string { + switch (typeof value) { + case "string": + return value; + case "number": + case "boolean": + case "bigint": + return String(value); + default: + return ""; + } +} + +function asErrorMessage(err: unknown): string { + if (err instanceof Error) return err.message; + if (err && typeof err === "object") { + try { + return JSON.stringify(err); + } catch { + return Object.prototype.toString.call(err); + } + } + try { + return String(err); + } catch { + return "Unknown error"; + } +} + +function sendBridgeResponse( + sendResponse: ((value: unknown) => void) | undefined, + payload: unknown, +): void { + if (typeof sendResponse !== "function") return; + try { + sendResponse(payload); + } catch { + // ignore + } +} + +async function handleStorageRequest( + action: string, + payload: Record | undefined, +): Promise { + switch (action) { + case "gm_getValue": { + const key = normalizeStorageRequestKey(payload?.key); + const def = payload?.def; + const items = await storageGet>(key); + return Object.hasOwn(items, key) ? items[key] : def; + } + + case "gm_setValue": { + const key = normalizeStorageRequestKey(payload?.key); + await storageSet({ [key]: payload?.value }); + return true; + } + + case "gm_deleteValue": { + const key = normalizeStorageRequestKey(payload?.key); + await storageRemove(key); + return true; + } + + case "gm_listValues": { + const items = await storageGet>(null); + return Object.keys(items ?? {}); + } + + case "gm_getValues": { + const defaults = (payload?.defaults ?? {}) as Record; + return await storageGet(defaults); + } + + default: + throw new Error(`Unknown storage action: ${action}`); + } +} + +export function registerBackgroundStorageBridge(): void { + ext?.runtime?.onMessage?.addListener?.( + ( + msg: unknown, + _sender: unknown, + sendResponse: ((value: unknown) => void) | undefined, + ) => { + if (!isGmStorageMessage(msg)) return; + + void (async () => { + try { + const result = await handleStorageRequest( + String(msg.action ?? ""), + msg.payload, + ); + sendBridgeResponse(sendResponse, { ok: true, result }); + } catch (error) { + sendBridgeResponse(sendResponse, { + ok: false, + error: asErrorMessage(error), + }); + } + })(); + + return true; + }, + ); +} diff --git a/src/extension/base64.ts b/src/extension/base64.ts deleted file mode 100644 index 552e4261..00000000 --- a/src/extension/base64.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Small Base64 helpers used by the extension bridge. - * - * We keep these in a dedicated module to avoid duplicating the same logic in - * both the content script and the service worker. - */ - -type FromBase64Options = { - alphabet?: "base64" | "base64url"; - lastChunkHandling?: "loose" | "strict" | "stop-before-partial"; -}; - -type Uint8ArrayPrototypeWithBase64 = { - toBase64?: (this: Uint8Array) => string; -}; - -type Uint8ArrayConstructorWithBase64 = Uint8ArrayConstructor & { - fromBase64?: (input: string, options?: FromBase64Options) => Uint8Array; -}; - -const BASE64URL_DECODE_OPTIONS: FromBase64Options = { - alphabet: "base64url", -}; -const nativeToBase64 = - typeof (Uint8Array.prototype as Uint8ArrayPrototypeWithBase64).toBase64 === - "function" - ? (Uint8Array.prototype as Uint8ArrayPrototypeWithBase64).toBase64 - : null; -const nativeFromBase64 = (Uint8Array as Uint8ArrayConstructorWithBase64) - .fromBase64; - -function normalizeBase64Input(input: string): string { - // Allow surrounding / embedded whitespace and missing padding. - const withoutWhitespace = String(input).replaceAll(/\s+/g, ""); - const remainder = withoutWhitespace.length % 4; - if (remainder === 1) { - throw new TypeError("Invalid base64 input."); - } - if (remainder === 0) return withoutWhitespace; - return withoutWhitespace + "=".repeat(4 - remainder); -} - -function bytesToBinaryString(bytes: Uint8Array): string { - let binary = ""; - for (const byte of bytes) { - binary += String.fromCharCode(byte); - } - return binary; -} - -function decodeWithLegacyBase64Normalized( - normalizedBase64: string, -): Uint8Array { - const atobFn = globalThis.atob; - if (typeof atobFn !== "function") { - throw new TypeError("Base64 decoder is not available in this environment."); - } - - const binary = atobFn(normalizedBase64); - const out = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i += 1) { - out[i] = binary.charCodeAt(i); - } - return out; -} - -function encodeWithLegacyBase64(bytes: Uint8Array): string { - const btoaFn = globalThis.btoa; - if (typeof btoaFn !== "function") { - throw new TypeError("Base64 encoder is not available in this environment."); - } - return btoaFn(bytesToBinaryString(bytes)); -} - -function decodeBase64ToBytes(input: string): Uint8Array { - const normalized = normalizeBase64Input(input); - const normalizedStandard = normalized - .replaceAll("-", "+") - .replaceAll("_", "/"); - - if (typeof nativeFromBase64 !== "function") { - return decodeWithLegacyBase64Normalized(normalizedStandard); - } - - const hasUrlAlphabet = normalized.includes("-") || normalized.includes("_"); - const hasStandardAlphabet = - normalized.includes("+") || normalized.includes("/"); - - if (hasUrlAlphabet && !hasStandardAlphabet) { - return nativeFromBase64(normalized, BASE64URL_DECODE_OPTIONS); - } - return nativeFromBase64( - hasUrlAlphabet && hasStandardAlphabet ? normalizedStandard : normalized, - ); -} - -function bytesViewToArrayBuffer(bytes: Uint8Array): ArrayBuffer { - const { buffer, byteOffset, byteLength } = bytes; - - if (buffer instanceof ArrayBuffer) { - if (byteOffset === 0 && byteLength === buffer.byteLength) return buffer; - return buffer.slice(byteOffset, byteOffset + byteLength); - } - - const out = new ArrayBuffer(byteLength); - new Uint8Array(out).set(bytes); - return out; -} - -export function bytesToBase64(bytes: Uint8Array): string { - if (typeof nativeToBase64 === "function") { - return nativeToBase64.call(bytes); - } - return encodeWithLegacyBase64(bytes); -} - -export function arrayBufferToBase64(ab: ArrayBuffer): string { - return bytesToBase64(new Uint8Array(ab)); -} - -export function base64ToBytes(b64: string): Uint8Array { - return decodeBase64ToBytes(b64); -} - -export function base64ToArrayBuffer(b64: string): ArrayBuffer { - return bytesViewToArrayBuffer(base64ToBytes(b64)); -} diff --git a/src/extension/bodySerialization.ts b/src/extension/bodySerialization.ts index ff4a22f3..819167d6 100644 --- a/src/extension/bodySerialization.ts +++ b/src/extension/bodySerialization.ts @@ -1,4 +1,102 @@ -import { base64ToBytes, bytesToBase64 } from "./base64"; +type FromBase64Options = { + alphabet?: "base64" | "base64url"; + lastChunkHandling?: "loose" | "strict" | "stop-before-partial"; +}; + +type Uint8ArrayPrototypeWithBase64 = { + toBase64?: (this: Uint8Array) => string; +}; + +type Uint8ArrayConstructorWithBase64 = Uint8ArrayConstructor & { + fromBase64?: (input: string, options?: FromBase64Options) => Uint8Array; +}; + +const nativeToBase64 = + typeof (Uint8Array.prototype as Uint8ArrayPrototypeWithBase64).toBase64 === + "function" + ? (Uint8Array.prototype as Uint8ArrayPrototypeWithBase64).toBase64 + : null; +const nativeFromBase64 = (Uint8Array as Uint8ArrayConstructorWithBase64) + .fromBase64; +const BASE64_URL_ALPHABET_RE = /[-_]/; + +function normalizeBase64Input(input: string): string { + const withoutWhitespace = String(input).replaceAll(/\s+/g, ""); + const remainder = withoutWhitespace.length % 4; + if (remainder === 1) { + throw new TypeError("Invalid base64 input."); + } + if (remainder === 0) return withoutWhitespace; + return withoutWhitespace + "=".repeat(4 - remainder); +} + +function bytesToBinaryString(bytes: Uint8Array): string { + let binary = ""; + for (const byte of bytes) { + binary += String.fromCharCode(byte); + } + return binary; +} + +function bytesViewToArrayBuffer(bytes: Uint8Array): ArrayBuffer { + const { buffer, byteOffset, byteLength } = bytes; + + if (buffer instanceof ArrayBuffer) { + if (byteOffset === 0 && byteLength === buffer.byteLength) return buffer; + return buffer.slice(byteOffset, byteOffset + byteLength); + } + + const out = new ArrayBuffer(byteLength); + new Uint8Array(out).set(bytes); + return out; +} + +export function bytesToBase64(bytes: Uint8Array): string { + if (typeof nativeToBase64 === "function") { + return nativeToBase64.call(bytes); + } + + const btoaFn = globalThis.btoa; + if (typeof btoaFn !== "function") { + throw new TypeError("Base64 encoder is not available in this environment."); + } + return btoaFn(bytesToBinaryString(bytes)); +} + +export function arrayBufferToBase64(ab: ArrayBuffer): string { + return bytesToBase64(new Uint8Array(ab)); +} + +export function base64ToBytes(input: string): Uint8Array { + const normalized = normalizeBase64Input(input); + const normalizedStandard = normalized + .replaceAll("-", "+") + .replaceAll("_", "/"); + + if (typeof nativeFromBase64 === "function") { + const hasUrlAlphabet = BASE64_URL_ALPHABET_RE.test(normalized); + return nativeFromBase64( + hasUrlAlphabet ? normalizedStandard : normalized, + hasUrlAlphabet ? { alphabet: "base64" } : undefined, + ); + } + + const atobFn = globalThis.atob; + if (typeof atobFn !== "function") { + throw new TypeError("Base64 decoder is not available in this environment."); + } + + const binary = atobFn(normalizedStandard); + const out = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i += 1) { + out[i] = binary.charCodeAt(i); + } + return out; +} + +export function base64ToArrayBuffer(b64: string): ArrayBuffer { + return bytesViewToArrayBuffer(base64ToBytes(b64)); +} /** * Shared helpers for serializing request bodies across the extension layers. @@ -134,11 +232,7 @@ function toUint8FromNumericArray( } function viewAsBytes(view: ArrayBufferView): Uint8Array { - return new Uint8Array( - view.buffer as ArrayBufferLike, - view.byteOffset, - view.byteLength, - ); + return new Uint8Array(view.buffer, view.byteOffset, view.byteLength); } function copyArrayBufferView(view: ArrayBufferView): Uint8Array { @@ -249,121 +343,142 @@ function tryCoerceByteLikeObjectToUint8Array( return toUint8FromNumericArray(v as unknown[]); } - // Node.js Buffer JSON form: { type: "Buffer", data: number[] } - if (obj.type === "Buffer" && Array.isArray(obj.data)) { - const fromBufferJson = toUint8FromNumericArray(obj.data); - if (fromBufferJson) return fromBufferJson; - } + const wrappedBytes = tryReadWrappedBytes(obj); + if (wrappedBytes) return wrappedBytes; - // Some runtimes wrap bytes as { data: number[] } / { bytes: number[] }. - if (Array.isArray(obj.data)) { - const fromData = toUint8FromNumericArray(obj.data); - if (fromData) return fromData; - } - - if (Array.isArray(obj.bytes)) { - const fromBytes = toUint8FromNumericArray(obj.bytes); - if (fromBytes) return fromBytes; - } - - // Base64 wrappers seen in bridge payloads. - if (typeof obj.b64 === "string") { - try { - return base64ToBytes(obj.b64); - } catch { - // ignore and continue - } - } - - if (typeof obj.base64 === "string") { - try { - return base64ToBytes(obj.base64); - } catch { - // ignore and continue - } - } + const base64WrappedBytes = tryDecodeWrappedBase64(obj); + if (base64WrappedBytes) return base64WrappedBytes; // TypedArray-like wrapper: { buffer, byteOffset, byteLength }. - const byteLength = toSafeLength(obj.byteLength); - const byteOffset = toSafeLength(obj.byteOffset ?? 0); - if (byteLength !== null && byteOffset !== null) { - const rawBuffer = obj.buffer; - - if (isArrayBufferLike(rawBuffer)) { - try { - return new Uint8Array( - rawBuffer as ArrayBuffer, - byteOffset, - byteLength, - ).slice(); - } catch { - // ignore and continue - } - } - - // In some cross-world cases `buffer` itself is coerced to a plain object. - if (rawBuffer && rawBuffer !== v) { - const recoveredBuffer = tryCoerceByteLikeObjectToUint8Array( - rawBuffer, - depth + 1, - ); - if (recoveredBuffer) { - if (byteOffset > recoveredBuffer.byteLength) return null; - const end = Math.min( - recoveredBuffer.byteLength, - byteOffset + byteLength, - ); - return recoveredBuffer.slice(byteOffset, end); - } - } - } + const typedArrayBytes = tryReadTypedArrayLikeObject(obj, v, depth); + if (typedArrayBytes) return typedArrayBytes; // Array-like wrappers with numeric indexes + length. - const length = toSafeLength(obj.length); - if (length !== null) { - const indexed = obj as Record; - const out = new Uint8Array(length); - for (let i = 0; i < length; i += 1) { - const byte = toByte(indexed[i]); - if (byte === null) { - // Not a true numeric array-like container. - return null; - } - out[i] = byte; - } - return out; - } + const arrayLikeBytes = tryReadArrayLikeObject(obj); + if (arrayLikeBytes) return arrayLikeBytes; // A plain object with numeric keys: {"0": 10, "1": 20, ...} if (isPlainObject(v)) { - const keys = Object.keys(obj); - if (!keys.length) return null; - - // Only consider objects whose keys are all non-negative integers. - const indexes = new Array(keys.length); - let max = -1; - for (let i = 0; i < keys.length; i += 1) { - const idx = parseNonNegativeIntegerKey(keys[i]); - if (idx === null || idx > MAX_RECOVERABLE_BYTES) return null; - indexes[i] = idx; - if (idx > max) max = idx; - } - - // Sparse numeric object where only a few indexes are present. - // Keep old behavior for compatibility with prior bridge payloads. - const out = new Uint8Array(max + 1); - for (let i = 0; i < keys.length; i += 1) { - const key = keys[i]; - const byte = toByte(obj[key]); - if (byte === null) return null; - out[indexes[i]] = byte; - } - return out; + return tryReadSparseNumericObject(obj); } return null; } +function tryReadWrappedBytes(obj: Record): Uint8Array | null { + const numericArrays = [ + obj.type === "Buffer" && Array.isArray(obj.data) ? obj.data : null, + Array.isArray(obj.data) ? obj.data : null, + Array.isArray(obj.bytes) ? obj.bytes : null, + ]; + + for (const source of numericArrays) { + if (!source) continue; + const bytes = toUint8FromNumericArray(source); + if (bytes) return bytes; + } + + return null; +} + +function tryDecodeWrappedBase64(obj: Record): Uint8Array | null { + const candidates = [obj.b64, obj.base64]; + + for (const candidate of candidates) { + if (typeof candidate !== "string") continue; + try { + return base64ToBytes(candidate); + } catch { + // ignore and continue + } + } + + return null; +} + +function tryReadTypedArrayLikeObject( + obj: Record, + originalValue: unknown, + depth: number, +): Uint8Array | null { + const byteLength = toSafeLength(obj.byteLength); + const byteOffset = toSafeLength(obj.byteOffset ?? 0); + if (byteLength === null || byteOffset === null) { + return null; + } + + const rawBuffer = obj.buffer; + if (isArrayBufferLike(rawBuffer)) { + try { + return new Uint8Array(rawBuffer, byteOffset, byteLength).slice(); + } catch { + // ignore and continue + } + } + + if (!rawBuffer || rawBuffer === originalValue) { + return null; + } + + const recoveredBuffer = tryCoerceByteLikeObjectToUint8Array( + rawBuffer, + depth + 1, + ); + if (!recoveredBuffer) { + return null; + } + if (byteOffset > recoveredBuffer.byteLength) { + return null; + } + + const end = Math.min(recoveredBuffer.byteLength, byteOffset + byteLength); + return recoveredBuffer.slice(byteOffset, end); +} + +function tryReadArrayLikeObject(obj: Record): Uint8Array | null { + const length = toSafeLength(obj.length); + if (length === null) { + return null; + } + + const indexed = obj as Record; + const out = new Uint8Array(length); + for (let i = 0; i < length; i += 1) { + const byte = toByte(indexed[i]); + if (byte === null) { + return null; + } + out[i] = byte; + } + + return out; +} + +function tryReadSparseNumericObject( + obj: Record, +): Uint8Array | null { + const keys = Object.keys(obj); + if (!keys.length) return null; + + const indexes = new Array(keys.length); + let max = -1; + for (let i = 0; i < keys.length; i += 1) { + const idx = parseNonNegativeIntegerKey(keys[i]); + if (idx === null || idx > MAX_RECOVERABLE_BYTES) return null; + indexes[i] = idx; + if (idx > max) max = idx; + } + + const out = new Uint8Array(max + 1); + for (let i = 0; i < keys.length; i += 1) { + const byte = toByte(obj[keys[i]]); + if (byte === null) return null; + out[indexes[i]] = byte; + } + + return out; +} + export function coerceBodyToBytes(body: any): Uint8Array | null { return tryCoerceByteLikeObjectToUint8Array(body); } @@ -373,65 +488,14 @@ export function summarizeBodyForDebug(body: any): BodyDebugSummary { const tag = safeObjectTag(body); const ctor = safeConstructorName(body); - if (body === null || body === undefined) { - return { kind: "empty", jsType, tag, ctor }; - } + const primitiveSummary = summarizePrimitiveBody(body, jsType, tag, ctor); + if (primitiveSummary) return primitiveSummary; - if (typeof body === "string") { - return { kind: "string", jsType, tag, ctor, textLength: body.length }; - } + const serializedSummary = summarizeSerializedBody(body, jsType, tag, ctor); + if (serializedSummary) return serializedSummary; - if (isSerializedBodyEnvelope(body)) { - const kind = - typeof body.kind === "string" && body.kind ? body.kind : "bytes"; - return { - kind: `serialized:${kind}`, - jsType, - tag, - ctor, - base64Length: body.b64.length, - mime: safeStringProp(body, "mime"), - }; - } - - if (isArrayBufferLike(body)) { - return { - kind: "ArrayBuffer", - jsType, - tag, - ctor, - byteLength: (body as ArrayBuffer).byteLength, - }; - } - - try { - if (ArrayBuffer.isView(body)) { - const view = body as ArrayBufferView; - return { - kind: ctor ? `TypedArray:${ctor}` : "TypedArray", - jsType, - tag, - ctor, - byteLength: view.byteLength, - }; - } - } catch { - // ignore and continue - } - - if (isBlobLike(body)) { - return { - kind: "BlobLike", - jsType, - tag, - ctor, - byteLength: - typeof (body as any).size === "number" - ? Number((body as any).size) - : -1, - mime: safeStringProp(body, "type"), - }; - } + const binarySummary = summarizeBinaryBody(body, jsType, tag, ctor); + if (binarySummary) return binarySummary; const coerced = coerceBodyToBytes(body); if (coerced) { @@ -459,6 +523,89 @@ export function summarizeBodyForDebug(body: any): BodyDebugSummary { return { kind: "primitive", jsType, tag, ctor }; } +function summarizePrimitiveBody( + body: any, + jsType: string, + tag: string, + ctor: string, +): BodyDebugSummary | null { + if (body === null || body === undefined) { + return { kind: "empty", jsType, tag, ctor }; + } + + if (typeof body === "string") { + return { kind: "string", jsType, tag, ctor, textLength: body.length }; + } + + return null; +} + +function summarizeSerializedBody( + body: any, + jsType: string, + tag: string, + ctor: string, +): BodyDebugSummary | null { + if (!isSerializedBodyEnvelope(body)) { + return null; + } + + const kind = typeof body.kind === "string" && body.kind ? body.kind : "bytes"; + return { + kind: `serialized:${kind}`, + jsType, + tag, + ctor, + base64Length: body.b64.length, + mime: safeStringProp(body, "mime"), + }; +} + +function summarizeBinaryBody( + body: any, + jsType: string, + tag: string, + ctor: string, +): BodyDebugSummary | null { + if (isArrayBufferLike(body)) { + return { + kind: "ArrayBuffer", + jsType, + tag, + ctor, + byteLength: body.byteLength, + }; + } + + try { + if (ArrayBuffer.isView(body)) { + return { + kind: ctor ? `TypedArray:${ctor}` : "TypedArray", + jsType, + tag, + ctor, + byteLength: body.byteLength, + }; + } + } catch { + // ignore and continue + } + + if (!isBlobLike(body)) { + return null; + } + + return { + kind: "BlobLike", + jsType, + tag, + ctor, + byteLength: + typeof (body as any).size === "number" ? Number((body as any).size) : -1, + mime: safeStringProp(body, "type"), + }; +} + /** * Serialize a GM_xmlhttpRequest body so it can be transported over * `postMessage` / extension messaging. @@ -540,18 +687,24 @@ export function decodeSerializedBody(body: any): BodyInit | undefined { if (body === null || body === undefined) return undefined; if (typeof body === "string") return body; - if (isSerializedBodyEnvelope(body)) { - const bytes = base64ToBytes(body.b64); - const kind = - typeof body.kind === "string" && body.kind ? body.kind : "bytes"; - if (kind === "blob") { - const mime = body.mime; - const ab = bytes.buffer as ArrayBuffer; - return typeof mime === "string" && mime - ? new Blob([ab], { type: mime }) - : new Blob([ab]); + if (isArrayBufferLike(body)) { + return body as unknown as BodyInit; + } + + try { + if (ArrayBuffer.isView(body)) { + return viewAsBytes(body) as unknown as BodyInit; } - return bytes as unknown as BodyInit; + } catch { + // ignore and continue + } + + if (isBlobLike(body)) { + return body as unknown as BodyInit; + } + + if (isSerializedBodyEnvelope(body)) { + return decodeSerializedEnvelope(body); } // Recovery for unexpected cross-world payload shapes. @@ -567,3 +720,17 @@ export function decodeSerializedBody(body: any): BodyInit | undefined { return String(body); } + +function decodeSerializedEnvelope(body: SerializedBodyEnvelope): BodyInit { + const bytes = base64ToBytes(body.b64); + const kind = typeof body.kind === "string" && body.kind ? body.kind : "bytes"; + if (kind !== "blob") { + return bytes as unknown as BodyInit; + } + + const mime = body.mime; + const ab = bytes.buffer as ArrayBuffer; + return typeof mime === "string" && mime + ? new Blob([ab], { type: mime }) + : new Blob([ab]); +} diff --git a/src/extension/bridge.ts b/src/extension/bridge.ts index 89444b77..a01add21 100644 --- a/src/extension/bridge.ts +++ b/src/extension/bridge.ts @@ -1,228 +1,39 @@ import debug from "../utils/debug"; -import { toErrorMessage } from "../utils/errors"; -import { base64ToArrayBuffer } from "./base64"; -import { - isBodySerializedForPort, - serializeBodyForPort, - summarizeBodyForDebug, -} from "./bodySerialization"; +import { handleBridgeRequest } from "./bridgeRequestRuntime"; import { toPageMessage } from "./bridgeTransport"; +import { abortBridgeXhr, startBridgeXhr } from "./bridgeXhr"; import { type AnyObject, type BridgeWireMessage, isOurMessage, - PORT_NAME, TYPE_NOTIFY, TYPE_REQ, TYPE_RES, TYPE_XHR_ABORT, - TYPE_XHR_ACK, - TYPE_XHR_EVENT, TYPE_XHR_START, } from "./constants"; -import { ext, storageGet, storageRemove, storageSet } from "./webext"; -import { isYandexApiHostname, shouldStripYandexHeader } from "./yandexHeaders"; +import { ext } from "./webext"; const BRIDGE_BOOT_KEY = "__VOT_EXT_BRIDGE_BOOTED__"; -/** - * VOT Extension bridge (ISOLATED world content script). - * - * MAIN-world content scripts have direct access to the page JS context but do - * NOT have access to WebExtension APIs (runtime/storage/etc.). - * - * This file runs in the default (ISOLATED) content-script world and exposes a - * minimal "userscript" API surface to the MAIN-world bundle via - * globalThis.postMessage: - * - GM.getValue / setValue / deleteValue / listValues / getValues - * - GM_xmlhttpRequest (proxied through the background service worker) - * - GM_notification (proxied through the background service worker) - */ - -type UaBrandVersion = { brand: string; version: string }; -const UA_CH_CACHE_TTL_MS = 10 * 60 * 1000; -const UA_CH_HIGH_ENTROPY_HINTS = ["fullVersionList"]; -const TERMINAL_XHR_EVENT_TYPES = new Set(["load", "error", "timeout", "abort"]); -const EMPTY_HEADERS = Object.freeze({}) as Readonly>; -let cachedUaChHeaders: Readonly> = EMPTY_HEADERS; -let cachedUaChHeadersExpiresAt = 0; -let cachedUaChHeadersPromise: Promise>> | null = - null; -const ESCAPED_DOUBLE_QUOTE = String.raw`\"`; - -// -- UA Client Hints helpers ------------------------------------------------- -// -// Some endpoints (notably api.browser.yandex.ru) validate that requests look -// like they were initiated by a real Chromium tab. When we proxy requests via -// the extension service worker, Chromium may omit high-entropy UA-CH headers. -// -// We collect UA-CH from the tab (content-script context) and forward them as -// request headers. The background service worker then injects them using -// declarativeNetRequest.modifyHeaders because `sec-ch-ua*` headers are -// forbidden to set from fetch/XHR. -// -// This mirrors the overall architecture used by userscript managers such as -// ScriptCat (content-side API -> privileged background operations). - -function escapeHeaderValue(value: string): string { - // Avoid breaking quoted header values. - return value.replaceAll('"', ESCAPED_DOUBLE_QUOTE); -} - -function formatUaBrands(brands: UaBrandVersion[]): string { - return brands - .filter( - (b) => b && typeof b.brand === "string" && typeof b.version === "string", - ) - .map( - (b) => - `"${escapeHeaderValue(b.brand)}";v="${escapeHeaderValue(b.version)}"`, - ) - .join(", "); -} - -/** - * Return the minimal UA-CH header set required by the valid request capture. - * - * Important: do NOT include other high-entropy UA-CH headers (arch/bitness/ - * platform-version/full-version/model). Those extra headers were present in - * the invalid request capture and must not be emitted. - */ -async function getUaChHeaders(): Promise> { - const uaData = ( - navigator as Navigator & { - userAgentData?: { - brands?: UaBrandVersion[]; - uaList?: UaBrandVersion[]; - mobile?: boolean; - platform?: string; - getHighEntropyValues?: ( - values: string[], - ) => Promise<{ fullVersionList?: UaBrandVersion[] }>; - }; - } - )?.userAgentData; - if (!uaData) return {}; - - const headers: Record = {}; - - const brands: UaBrandVersion[] = - (Array.isArray(uaData.brands) && uaData.brands) || - // Some Chromium variants used `uaList` historically. - (Array.isArray(uaData.uaList) && uaData.uaList) || - []; - - if (brands.length) headers["sec-ch-ua"] = formatUaBrands(brands); - - if (typeof uaData.mobile === "boolean") - headers["sec-ch-ua-mobile"] = uaData.mobile ? "?1" : "?0"; - - if (typeof uaData.platform === "string" && uaData.platform) - headers["sec-ch-ua-platform"] = `"${escapeHeaderValue(uaData.platform)}"`; - - // Only request the high entropy value required by the valid capture: - // `sec-ch-ua-full-version-list`. - try { - const high = await uaData.getHighEntropyValues?.(UA_CH_HIGH_ENTROPY_HINTS); - if (Array.isArray(high?.fullVersionList) && high.fullVersionList.length) { - headers["sec-ch-ua-full-version-list"] = formatUaBrands( - high.fullVersionList, - ); - } - } catch { - // High entropy values are optional; ignore failures. +function injectPageModule(fileName: string): void { + const parent = document.head ?? document.documentElement; + if (!parent) { + console.error("[VOT Extension] bridge: missing document root"); + return; } - return headers; -} - -function freezeHeaders( - headers: Record, -): Readonly> { - return Object.freeze({ ...headers }); -} - -async function getCachedUaChHeaders(): Promise< - Readonly> -> { - const now = Date.now(); - if (now < cachedUaChHeadersExpiresAt) { - return cachedUaChHeaders; - } - if (cachedUaChHeadersPromise !== null) { - return await cachedUaChHeadersPromise; - } - - cachedUaChHeadersPromise = (async () => { - const headers = freezeHeaders(await getUaChHeaders()); - cachedUaChHeaders = headers; - cachedUaChHeadersExpiresAt = Date.now() + UA_CH_CACHE_TTL_MS; - return headers; - })(); - - try { - return await cachedUaChHeadersPromise; - } finally { - cachedUaChHeadersPromise = null; - } -} - -function ensureHeadersObject(details: AnyObject): Record { - const raw = details?.headers; - if (!raw || typeof raw !== "object") { - const headers: Record = {}; - details.headers = headers; - return headers; - } - - // Normalize in place and drop unsupported values. - const headers = raw as Record; - for (const [name, value] of Object.entries(headers)) { - if (typeof value === "string") continue; - if (typeof value === "number" || typeof value === "boolean") { - headers[name] = String(value); - continue; - } - delete headers[name]; - } - - details.headers = headers; - return headers as Record; -} - -function stripYandexHeaders(headers: Record): void { - for (const headerName of Object.keys(headers)) { - if (shouldStripYandexHeader(headerName)) { - delete headers[headerName]; - } - } -} - -function mergeHeadersIfMissing( - headers: Record, - additions: Readonly>, -): void { - const existingNames = new Set(); - for (const name of Object.keys(headers)) { - existingNames.add(name.toLowerCase()); - } - - for (const [name, value] of Object.entries(additions)) { - if (!value) continue; - const normalizedName = name.toLowerCase(); - if (existingNames.has(normalizedName)) continue; - headers[name] = value; - existingNames.add(normalizedName); - } -} - -function getHostname(url: string): string { - if (!url) return ""; - try { - return new URL(url).hostname; - } catch { - return ""; - } + const script = document.createElement("script"); + script.type = "module"; + script.src = ext.runtime?.getURL(fileName); + script.addEventListener( + "error", + () => { + console.error(`[VOT Extension] bridge: failed to inject ${fileName}`); + }, + { once: true }, + ); + parent.appendChild(script); } function postToPage(payload: AnyObject) { @@ -234,396 +45,6 @@ function postToPage(payload: AnyObject) { globalThis.postMessage(message, "*"); } -type XhrPortState = { - port: { - onMessage: { addListener: (fn: (msg: AnyObject) => void) => void }; - onDisconnect: { addListener: (fn: () => void) => void }; - postMessage: (msg: AnyObject) => void; - disconnect: () => void; - }; - responseType: string; - chunks: ArrayBuffer[]; - totalBytes: number; - settled: boolean; -}; - -const xhrPorts = new Map(); - -function disconnectPortSafely(port: XhrPortState["port"]): void { - try { - port.disconnect(); - } catch { - // ignore - } -} - -function settleXhrPort(requestId: string, state: XhrPortState): void { - state.settled = true; - disconnectPortSafely(state.port); - xhrPorts.delete(requestId); -} - -function isRequestStateActive( - requestId: string, - expectedState: XhrPortState, -): boolean { - const currentState = xhrPorts.get(requestId); - return currentState === expectedState && !currentState.settled; -} - -function toLifecycleState(kind: string): "in_flight" | "terminal" { - return kind === "progress" ? "in_flight" : "terminal"; -} - -function postXhrEvent(requestId: string, payload: AnyObject): void { - const kind = String(payload?.type ?? ""); - postToPage({ - type: TYPE_XHR_EVENT, - requestId, - payload: { - ...payload, - state: toLifecycleState(kind), - }, - }); -} - -function makeBridgeXhrError(details: AnyObject, error: string): AnyObject { - return { - finalUrl: String(details?.url || ""), - readyState: 4, - status: 0, - statusText: "", - responseHeaders: "", - response: null, - responseText: "", - error, - }; -} - -function concatArrayBuffers( - chunks: ArrayBuffer[], - totalBytes: number, -): ArrayBuffer { - const out = new Uint8Array(totalBytes); - let offset = 0; - for (const ab of chunks) { - const u8 = new Uint8Array(ab); - out.set(u8, offset); - offset += u8.byteLength; - } - return out.buffer; -} - -function resolveBinaryResponseBuffer( - directResponse: unknown, - chunks: ArrayBuffer[], - totalBytes: number, - fallbackB64: unknown, -): ArrayBuffer { - if (directResponse instanceof ArrayBuffer) { - return directResponse; - } - if (totalBytes > 0) { - return concatArrayBuffers(chunks, totalBytes); - } - if (typeof fallbackB64 === "string" && fallbackB64.length > 0) { - return base64ToArrayBuffer(fallbackB64); - } - return new ArrayBuffer(0); -} - -async function startXhr(requestId: string, details: AnyObject) { - const normalizedRequestId = String(requestId || ""); - const safeDetails: AnyObject = details ?? {}; - - try { - if (!normalizedRequestId) { - throw new Error("Missing requestId for bridge XHR"); - } - requestId = normalizedRequestId; - - if (xhrPorts.has(requestId)) { - debug.warn("[VOT EXT][bridge] replacing active XHR request", { - requestId, - }); - abortXhr(requestId); - } - - debug.log("[VOT EXT][bridge] startXhr", { - requestId, - url: safeDetails?.url, - method: safeDetails?.method, - responseType: safeDetails?.responseType, - timeoutMs: Number(safeDetails?.timeout ?? 0), - headerCount: - safeDetails?.headers && typeof safeDetails.headers === "object" - ? Object.keys(safeDetails.headers).length - : 0, - body: summarizeBodyForDebug(safeDetails?.data), - }); - - const connected = ext?.runtime?.connect?.({ name: PORT_NAME }); - if (!connected || typeof connected !== "object") { - throw new Error("Bridge port is not available"); - } - const port = connected as XhrPortState["port"]; - const responseType = String( - safeDetails?.responseType || "text", - ).toLowerCase(); - const state: XhrPortState = { - port, - responseType, - chunks: [], - totalBytes: 0, - settled: false, - }; - xhrPorts.set(requestId, state); - postToPage({ - type: TYPE_XHR_ACK, - requestId, - payload: { - state: "acknowledged", - timeoutMs: Number(safeDetails?.timeout ?? 0), - responseType, - ts: Date.now(), - }, - }); - - port.onMessage.addListener((msg: AnyObject) => { - const st = xhrPorts.get(requestId); - if (!st || st.settled) return; - if (!msg || typeof msg !== "object") return; - - debug.log("[VOT EXT][bridge] port message", { - requestId, - kind: msg.type ?? "unknown", - state: msg.state ?? null, - status: - msg.response?.status ?? - msg.error?.status ?? - msg.progress?.status ?? - null, - loaded: msg.progress?.loaded ?? null, - total: msg.progress?.total ?? null, - }); - - // Decode binary chunks coming from the service worker (sent as base64 - // because extension messaging only supports JSON-serializable payloads). - if (msg.type === "progress" && msg.progress) { - const b64 = msg.progress.chunkB64; - if (typeof b64 === "string" && b64.length) { - const ab = base64ToArrayBuffer(b64); - // Important: `msg.progress.chunk` is posted to MAIN world with - // transferables. That detaches the transferred ArrayBuffer in this - // realm, so we must keep our own copy for final load aggregation. - const aggregateCopy = ab.slice(0); - st.chunks.push(aggregateCopy); - st.totalBytes += aggregateCopy.byteLength; - msg.progress.chunk = ab; - delete msg.progress.chunkB64; - } - } - - if (msg.type === "load" && msg.response) { - const rt = String( - msg.response.responseType || st.responseType || "text", - ).toLowerCase(); - - if (rt === "arraybuffer" || rt === "blob") { - const directResponse = msg.response.response; - const fallbackB64 = msg.response.responseB64; - const ab = resolveBinaryResponseBuffer( - directResponse, - st.chunks, - st.totalBytes, - fallbackB64, - ); - delete msg.response.responseB64; - st.chunks.length = 0; - st.totalBytes = 0; - - if (rt === "blob") { - const ct = - msg.response.contentType || msg.response.mime || undefined; - msg.response.response = ct - ? new Blob([ab], { type: String(ct) }) - : new Blob([ab]); - } else { - msg.response.response = ab; - } - } - } - - postXhrEvent(requestId, msg); - - // Close port for terminal events. - if (TERMINAL_XHR_EVENT_TYPES.has(String(msg.type ?? ""))) { - debug.log("[VOT EXT][bridge] terminal event", { - requestId, - kind: msg.type, - status: msg.response?.status ?? msg.error?.status ?? null, - }); - settleXhrPort(requestId, st); - } - }); - - port.onDisconnect.addListener(() => { - const st = xhrPorts.get(requestId); - if (!st || st.settled) return; - debug.warn("[VOT EXT][bridge] port disconnected before terminal event", { - requestId, - url: safeDetails?.url ?? null, - }); - settleXhrPort(requestId, st); - postXhrEvent(requestId, { - type: "error", - error: makeBridgeXhrError( - safeDetails, - "Bridge port disconnected before response", - ), - }); - }); - - const urlStr = String(safeDetails?.url ?? ""); - const hostname = getHostname(urlStr); - if (isYandexApiHostname(hostname)) { - const headers = ensureHeadersObject(safeDetails); - stripYandexHeaders(headers); - - const uaCh = await getCachedUaChHeaders(); - if (!isRequestStateActive(requestId, state)) return; - mergeHeadersIfMissing(headers, uaCh); - - debug.log("[VOT EXT][bridge] yandex header normalization", { - requestId, - url: urlStr, - headerCount: Object.keys(headers).length, - headerNames: Object.keys(headers), - }); - } - - if (!isRequestStateActive(requestId, state)) return; - - // Chrome extension messaging uses JSON serialization, so prelude serializes - // the request body before crossing worlds. Keep a defensive fallback for - // unexpected callers that bypass prelude. - const data = isBodySerializedForPort(safeDetails?.data) - ? safeDetails.data - : await serializeBodyForPort(safeDetails?.data); - const serializedBodySummary = summarizeBodyForDebug(data); - debug.log("[VOT EXT][bridge] serialized body", { - requestId, - url: safeDetails?.url ?? null, - from: summarizeBodyForDebug(safeDetails?.data), - to: serializedBodySummary, - }); - - // The request could be aborted while we're async-serializing (Blob -> bytes). - // If so, avoid starting a network request. - if (!isRequestStateActive(requestId, state)) return; - - const serializedDetails: AnyObject = { - ...safeDetails, - data, - responseType: safeDetails?.responseType, - }; - - debug.log("[VOT EXT][bridge] post start to background", { - requestId, - url: serializedDetails.url, - method: serializedDetails.method, - responseType: serializedDetails.responseType, - body: serializedBodySummary, - }); - - state.port.postMessage({ type: "start", details: serializedDetails }); - } catch (error: unknown) { - const requestKey = normalizedRequestId || requestId; - const st = xhrPorts.get(requestKey); - if (st && !st.settled) { - settleXhrPort(requestKey, st); - } - - const errorMessage = toErrorMessage(error); - debug.log("[VOT EXT][bridge] startXhr error", { - requestId: requestKey, - error: errorMessage, - lastError: ext?.runtime?.lastError ?? null, - }); - - if (requestKey) { - postXhrEvent(requestKey, { - type: "error", - error: makeBridgeXhrError(safeDetails, errorMessage), - }); - } - } -} - -function abortXhr(requestId: string) { - const st = xhrPorts.get(requestId); - if (!st || st.settled) return; - st.settled = true; - - debug.warn("[VOT EXT][bridge] abortXhr", { requestId }); - - try { - st.port.postMessage({ type: "abort" }); - } catch { - // ignore - } - - disconnectPortSafely(st.port); - xhrPorts.delete(requestId); -} - -async function handleRequest( - action: string, - payload: AnyObject, -): Promise { - switch (action) { - case "handshake": { - // Provide manifest metadata to the MAIN-world prelude. - const manifest = ext?.runtime?.getManifest?.() ?? {}; - const id = ext?.runtime?.id ?? null; - return { manifest, id }; - } - - case "gm_getValue": { - const key = String(payload?.key ?? ""); - const def = payload?.def; - const items = await storageGet({ [key]: def }); - return items[key]; - } - - case "gm_setValue": { - const key = String(payload?.key ?? ""); - await storageSet({ [key]: payload?.value }); - return true; - } - - case "gm_deleteValue": { - const key = String(payload?.key ?? ""); - await storageRemove(key); - return true; - } - - case "gm_listValues": { - const items = await storageGet(null); - return Object.keys(items ?? {}); - } - - case "gm_getValues": { - const defaults = payload?.defaults ?? {}; - const items = await storageGet(defaults); - return items; - } - - default: - throw new Error(`Unknown bridge action: ${action}`); - } -} - function sendResponse( id: string, ok: boolean, @@ -633,66 +54,68 @@ function sendResponse( postToPage({ type: TYPE_RES, id, ok, result, error }); } -// Guard: if the bridge cannot access extension APIs, there is nothing useful -// we can do. -const bridgeGlobal = globalThis as Record; -if (bridgeGlobal[BRIDGE_BOOT_KEY]) { - debug.log("[VOT EXT][bridge] already initialized"); -} else { +export function bootstrapExtensionBridge(): void { + const bridgeGlobal = globalThis as Record; + if (bridgeGlobal[BRIDGE_BOOT_KEY]) { + debug.log("[VOT EXT][bridge] already initialized"); + return; + } + bridgeGlobal[BRIDGE_BOOT_KEY] = true; if (!ext?.runtime || !ext?.storage?.local) { console.warn("[VOT Extension] bridge: missing WebExtension APIs"); - } else { - globalThis.addEventListener("message", async (event) => { - if (event.source !== globalThis.window) return; - const data = event.data as BridgeWireMessage; - if (!isOurMessage(data)) return; - - try { - if (data.type === TYPE_REQ) { - const id = String(data.id ?? ""); - const action = String(data.action ?? ""); - const payload = (data.payload ?? {}) as AnyObject; - const result = await handleRequest(action, payload); - sendResponse(id, true, result); - return; - } - - if (data.type === TYPE_NOTIFY) { - // Relay to background so we can use the privileged notifications API. - ext?.runtime?.sendMessage?.({ - type: "gm_notification", - details: data.details, - }); - return; - } - - if (data.type === TYPE_XHR_START) { - startXhr( - String(data.requestId ?? ""), - (data.details ?? {}) as AnyObject, - ); - return; - } - - if (data.type === TYPE_XHR_ABORT) { - abortXhr(String(data.requestId ?? "")); - return; - } - } catch (err: unknown) { - // Best-effort error reporting back to the page (only for REQ messages). - if (data?.type === TYPE_REQ) { - sendResponse( - String(data.id ?? ""), - false, - undefined, - err instanceof Error ? err.message : String(err), - ); - } else { - console.error("[VOT Extension] bridge error", err); - } - } - }); + return; } + + injectPageModule("prelude.module.js"); + injectPageModule("content.module.js"); + + globalThis.addEventListener("message", async (event) => { + if (event.source !== globalThis.window) return; + const data = event.data as BridgeWireMessage; + if (!isOurMessage(data)) return; + + try { + if (data.type === TYPE_REQ) { + const id = String(data.id ?? ""); + const action = String(data.action ?? ""); + const payload = data.payload ?? {}; + const result = await handleBridgeRequest(action, payload); + sendResponse(id, true, result); + return; + } + + if (data.type === TYPE_NOTIFY) { + ext?.runtime?.sendMessage?.({ + type: "gm_notification", + details: data.details, + }); + return; + } + + if (data.type === TYPE_XHR_START) { + await startBridgeXhr(String(data.requestId ?? ""), data.details ?? {}); + return; + } + + if (data.type === TYPE_XHR_ABORT) { + abortBridgeXhr(String(data.requestId ?? "")); + return; + } + } catch (err: unknown) { + if (data?.type === TYPE_REQ) { + sendResponse( + String(data.id ?? ""), + false, + undefined, + err instanceof Error ? err.message : String(err), + ); + } else { + console.error("[VOT Extension] bridge error", err); + } + } + }); } + +bootstrapExtensionBridge(); diff --git a/src/extension/bridgeRequestRuntime.ts b/src/extension/bridgeRequestRuntime.ts new file mode 100644 index 00000000..d3f93a00 --- /dev/null +++ b/src/extension/bridgeRequestRuntime.ts @@ -0,0 +1,81 @@ +import type { AnyObject } from "./constants"; +import { ext } from "./webext"; + +export const GM_STORAGE_MESSAGE_TYPE = "gm_storage"; + +async function sendRuntimeMessage(message: AnyObject): Promise { + const sendMessage = ext?.runtime?.sendMessage; + if (typeof sendMessage !== "function") { + throw new TypeError("runtime.sendMessage is not available"); + } + + return await new Promise((resolve, reject) => { + try { + const maybePromise = sendMessage(message, (response: unknown) => { + const runtimeError = + ( + globalThis as { + chrome?: { runtime?: { lastError?: { message?: string } } }; + } + ).chrome?.runtime?.lastError?.message ?? null; + if (runtimeError) { + reject(new Error(runtimeError)); + return; + } + + resolve(response as T); + }); + + if ( + maybePromise && + typeof (maybePromise as Promise).then === "function" + ) { + void (maybePromise as Promise).then(resolve, reject); + } + } catch (error) { + reject(error); + } + }); +} + +async function requestStorage( + action: string, + payload: AnyObject, +): Promise { + const response = await sendRuntimeMessage<{ + ok?: boolean; + result?: unknown; + error?: string; + }>({ + type: GM_STORAGE_MESSAGE_TYPE, + action, + payload, + }); + + if (!response?.ok) { + throw new Error(response?.error || `Storage request failed: ${action}`); + } + + return response.result; +} + +export async function handleBridgeRequest( + action: string, + payload: AnyObject, +): Promise { + switch (action) { + case "handshake": { + const manifest = ext?.runtime?.getManifest?.() ?? {}; + const id = ext?.runtime?.id ?? null; + return { manifest, id }; + } + case "gm_getValue": + case "gm_setValue": + case "gm_deleteValue": + case "gm_listValues": + case "gm_getValues": + return await requestStorage(action, payload); + default: + throw new Error(`Unknown bridge action: ${action}`); + } +} diff --git a/src/extension/bridgeXhr.ts b/src/extension/bridgeXhr.ts new file mode 100644 index 00000000..20c5f57c --- /dev/null +++ b/src/extension/bridgeXhr.ts @@ -0,0 +1,642 @@ +import debug from "../utils/debug"; +import { toErrorMessage } from "../utils/errors"; +import { + base64ToArrayBuffer, + isBodySerializedForPort, + serializeBodyForPort, + summarizeBodyForDebug, +} from "./bodySerialization"; +import { toPageMessage } from "./bridgeTransport"; +import type { AnyObject } from "./constants"; +import { PORT_NAME, TYPE_XHR_ACK, TYPE_XHR_EVENT } from "./constants"; +import { ext, runtimeMessagesUseStructuredClone } from "./webext"; +import { isYandexApiHostname, shouldStripYandexHeader } from "./yandexHeaders"; + +type UaBrandVersion = { brand: string; version: string }; +type XhrPortState = { + port: { + onMessage: { addListener: (fn: (msg: AnyObject) => void) => void }; + onDisconnect: { addListener: (fn: () => void) => void }; + postMessage: (msg: AnyObject) => void; + disconnect: () => void; + }; + responseType: string; + chunks: ArrayBuffer[]; + totalBytes: number; + settled: boolean; +}; + +const UA_CH_CACHE_TTL_MS = 10 * 60 * 1000; +const UA_CH_HIGH_ENTROPY_HINTS = ["fullVersionList"]; +const TERMINAL_XHR_EVENT_TYPES = new Set(["load", "error", "timeout", "abort"]); +const EMPTY_HEADERS = Object.freeze({}) as Readonly>; +const ESCAPED_DOUBLE_QUOTE = String.raw`\"`; + +let cachedUaChHeaders: Readonly> = EMPTY_HEADERS; +let cachedUaChHeadersExpiresAt = 0; +let cachedUaChHeadersPromise: Promise>> | null = + null; + +const xhrPorts = new Map(); + +function escapeHeaderValue(value: string): string { + return value.replaceAll('"', ESCAPED_DOUBLE_QUOTE); +} + +function formatUaBrands(brands: UaBrandVersion[]): string { + return brands + .filter( + (b) => b && typeof b.brand === "string" && typeof b.version === "string", + ) + .map( + (b) => + `"${escapeHeaderValue(b.brand)}";v="${escapeHeaderValue(b.version)}"`, + ) + .join(", "); +} + +async function getUaChHeaders(): Promise> { + const uaData = ( + navigator as Navigator & { + userAgentData?: { + brands?: UaBrandVersion[]; + uaList?: UaBrandVersion[]; + mobile?: boolean; + platform?: string; + getHighEntropyValues?: ( + values: string[], + ) => Promise<{ fullVersionList?: UaBrandVersion[] }>; + }; + } + )?.userAgentData; + if (!uaData) return {}; + + const headers: Record = {}; + const brands: UaBrandVersion[] = + (Array.isArray(uaData.brands) && uaData.brands) || + (Array.isArray(uaData.uaList) && uaData.uaList) || + []; + + if (brands.length) headers["sec-ch-ua"] = formatUaBrands(brands); + if (typeof uaData.mobile === "boolean") { + headers["sec-ch-ua-mobile"] = uaData.mobile ? "?1" : "?0"; + } + if (typeof uaData.platform === "string" && uaData.platform) { + headers["sec-ch-ua-platform"] = `"${escapeHeaderValue(uaData.platform)}"`; + } + + try { + const high = await uaData.getHighEntropyValues?.(UA_CH_HIGH_ENTROPY_HINTS); + if (Array.isArray(high?.fullVersionList) && high.fullVersionList.length) { + headers["sec-ch-ua-full-version-list"] = formatUaBrands( + high.fullVersionList, + ); + } + } catch { + // ignore optional high entropy lookup failures + } + + return headers; +} + +function freezeHeaders( + headers: Record, +): Readonly> { + return Object.freeze({ ...headers }); +} + +async function getCachedUaChHeaders(): Promise< + Readonly> +> { + const now = Date.now(); + if (now < cachedUaChHeadersExpiresAt) { + return cachedUaChHeaders; + } + if (cachedUaChHeadersPromise !== null) { + return await cachedUaChHeadersPromise; + } + + cachedUaChHeadersPromise = (async () => { + const headers = freezeHeaders(await getUaChHeaders()); + cachedUaChHeaders = headers; + cachedUaChHeadersExpiresAt = Date.now() + UA_CH_CACHE_TTL_MS; + return headers; + })(); + + try { + return await cachedUaChHeadersPromise; + } finally { + cachedUaChHeadersPromise = null; + } +} + +function ensureHeadersObject(details: AnyObject): Record { + const raw = details?.headers; + if (!raw || typeof raw !== "object") { + const headers: Record = {}; + details.headers = headers; + return headers; + } + + const headers = raw as Record; + for (const [name, value] of Object.entries(headers)) { + if (typeof value === "string") continue; + if (typeof value === "number" || typeof value === "boolean") { + headers[name] = String(value); + continue; + } + delete headers[name]; + } + + details.headers = headers; + return headers as Record; +} + +function stripYandexHeaders(headers: Record): void { + for (const headerName of Object.keys(headers)) { + if (shouldStripYandexHeader(headerName)) { + delete headers[headerName]; + } + } +} + +function mergeHeadersIfMissing( + headers: Record, + additions: Readonly>, +): void { + const existingNames = new Set(); + for (const name of Object.keys(headers)) { + existingNames.add(name.toLowerCase()); + } + + for (const [name, value] of Object.entries(additions)) { + if (!value) continue; + const normalizedName = name.toLowerCase(); + if (existingNames.has(normalizedName)) continue; + headers[name] = value; + existingNames.add(normalizedName); + } +} + +function getHostname(url: string): string { + if (!url) return ""; + try { + return new URL(url).hostname; + } catch { + return ""; + } +} + +function postToPage(payload: AnyObject) { + const { message, transfer } = toPageMessage(payload); + if (transfer.length) { + globalThis.postMessage(message, "*", transfer); + return; + } + globalThis.postMessage(message, "*"); +} + +function disconnectPortSafely(port: XhrPortState["port"]): void { + try { + port.disconnect(); + } catch { + // ignore + } +} + +function settleXhrPort(requestId: string, state: XhrPortState): void { + state.settled = true; + disconnectPortSafely(state.port); + xhrPorts.delete(requestId); +} + +function isRequestStateActive( + requestId: string, + expectedState: XhrPortState, +): boolean { + const currentState = xhrPorts.get(requestId); + return currentState === expectedState && !currentState.settled; +} + +function toLifecycleState(kind: string): "in_flight" | "terminal" { + return kind === "progress" ? "in_flight" : "terminal"; +} + +function postXhrEvent(requestId: string, payload: AnyObject): void { + const kind = String(payload?.type ?? ""); + postToPage({ + type: TYPE_XHR_EVENT, + requestId, + payload: { + ...payload, + state: toLifecycleState(kind), + }, + }); +} + +function makeBridgeXhrError(details: AnyObject, error: string): AnyObject { + return { + finalUrl: String(details?.url || ""), + readyState: 4, + status: 0, + statusText: "", + responseHeaders: "", + response: null, + responseText: "", + error, + }; +} + +function concatArrayBuffers( + chunks: ArrayBuffer[], + totalBytes: number, +): ArrayBuffer { + const out = new Uint8Array(totalBytes); + let offset = 0; + for (const ab of chunks) { + const u8 = new Uint8Array(ab); + out.set(u8, offset); + offset += u8.byteLength; + } + return out.buffer; +} + +function resolveBinaryResponseBuffer( + directResponse: unknown, + chunks: ArrayBuffer[], + totalBytes: number, + fallbackB64: unknown, +): ArrayBuffer { + if (directResponse instanceof ArrayBuffer) { + return directResponse; + } + try { + if (ArrayBuffer.isView(directResponse)) { + const view = directResponse; + const out = new Uint8Array(view.byteLength); + out.set(new Uint8Array(view.buffer, view.byteOffset, view.byteLength)); + return out.buffer; + } + } catch { + // ignore and continue + } + if (totalBytes > 0) { + return concatArrayBuffers(chunks, totalBytes); + } + if (typeof fallbackB64 === "string" && fallbackB64.length > 0) { + return base64ToArrayBuffer(fallbackB64); + } + return new ArrayBuffer(0); +} + +function logBridgeXhrStart(requestId: string, safeDetails: AnyObject): void { + debug.log("[VOT EXT][bridge] startXhr", { + requestId, + url: safeDetails?.url, + method: safeDetails?.method, + responseType: safeDetails?.responseType, + timeoutMs: Number(safeDetails?.timeout ?? 0), + headerCount: + safeDetails?.headers && typeof safeDetails.headers === "object" + ? Object.keys(safeDetails.headers).length + : 0, + body: summarizeBodyForDebug(safeDetails?.data), + }); +} + +function createXhrPortState( + port: XhrPortState["port"], + responseType: string, +): XhrPortState { + return { + port, + responseType, + chunks: [], + totalBytes: 0, + settled: false, + }; +} + +function postBridgeXhrAck( + requestId: string, + safeDetails: AnyObject, + responseType: string, +): void { + postToPage({ + type: TYPE_XHR_ACK, + requestId, + payload: { + state: "acknowledged", + timeoutMs: Number(safeDetails?.timeout ?? 0), + responseType, + ts: Date.now(), + }, + }); +} + +function logBridgePortMessage(requestId: string, msg: AnyObject): void { + debug.log("[VOT EXT][bridge] port message", { + requestId, + kind: msg.type ?? "unknown", + state: msg.state ?? null, + status: + msg.response?.status ?? msg.error?.status ?? msg.progress?.status ?? null, + loaded: msg.progress?.loaded ?? null, + total: msg.progress?.total ?? null, + }); +} + +function applyBridgeProgressChunk(st: XhrPortState, msg: AnyObject): void { + if (msg.type !== "progress" || !msg.progress) { + return; + } + + if (msg.progress.chunk instanceof ArrayBuffer) { + const aggregateCopy = msg.progress.chunk.slice(0); + st.chunks.push(aggregateCopy); + st.totalBytes += aggregateCopy.byteLength; + return; + } + + const b64 = msg.progress.chunkB64; + if (typeof b64 !== "string" || !b64.length) { + return; + } + + const ab = base64ToArrayBuffer(b64); + const aggregateCopy = ab.slice(0); + st.chunks.push(aggregateCopy); + st.totalBytes += aggregateCopy.byteLength; + msg.progress.chunk = ab; + delete msg.progress.chunkB64; +} + +function applyBridgeBinaryLoadResponse(st: XhrPortState, msg: AnyObject): void { + if (msg.type !== "load" || !msg.response) { + return; + } + + const rt = String( + msg.response.responseType || st.responseType || "text", + ).toLowerCase(); + if (rt !== "arraybuffer" && rt !== "blob") { + return; + } + + const ab = resolveBinaryResponseBuffer( + msg.response.response, + st.chunks, + st.totalBytes, + msg.response.responseB64, + ); + delete msg.response.responseB64; + st.chunks.length = 0; + st.totalBytes = 0; + + if (rt === "blob") { + const ct = msg.response.contentType || msg.response.mime || undefined; + msg.response.response = ct + ? new Blob([ab], { type: String(ct) }) + : new Blob([ab]); + return; + } + + msg.response.response = ab; +} + +function settleBridgePortOnTerminalEvent( + requestId: string, + st: XhrPortState, + msg: AnyObject, +): void { + if (!TERMINAL_XHR_EVENT_TYPES.has(String(msg.type ?? ""))) { + return; + } + + debug.log("[VOT EXT][bridge] terminal event", { + requestId, + kind: msg.type, + status: msg.response?.status ?? msg.error?.status ?? null, + }); + settleXhrPort(requestId, st); +} + +function handleBridgePortMessage(requestId: string, msg: AnyObject): void { + const st = xhrPorts.get(requestId); + if (!st || st.settled) return; + if (!msg || typeof msg !== "object") return; + + logBridgePortMessage(requestId, msg); + applyBridgeProgressChunk(st, msg); + applyBridgeBinaryLoadResponse(st, msg); + postXhrEvent(requestId, msg); + settleBridgePortOnTerminalEvent(requestId, st, msg); +} + +function handleBridgePortDisconnect( + requestId: string, + safeDetails: AnyObject, +): void { + const st = xhrPorts.get(requestId); + if (!st || st.settled) return; + + debug.warn("[VOT EXT][bridge] port disconnected before terminal event", { + requestId, + url: safeDetails?.url ?? null, + }); + settleXhrPort(requestId, st); + postXhrEvent(requestId, { + type: "error", + error: makeBridgeXhrError( + safeDetails, + "Bridge port disconnected before response", + ), + }); +} + +function attachBridgePortListeners( + requestId: string, + port: XhrPortState["port"], + safeDetails: AnyObject, +): void { + port.onMessage.addListener((msg: AnyObject) => { + handleBridgePortMessage(requestId, msg); + }); + port.onDisconnect.addListener(() => { + handleBridgePortDisconnect(requestId, safeDetails); + }); +} + +async function normalizeYandexBridgeHeaders( + requestId: string, + safeDetails: AnyObject, + state: XhrPortState, +): Promise { + const urlStr = String(safeDetails?.url ?? ""); + const hostname = getHostname(urlStr); + if (!isYandexApiHostname(hostname)) { + return; + } + + const headers = ensureHeadersObject(safeDetails); + stripYandexHeaders(headers); + + const uaCh = await getCachedUaChHeaders(); + if (!isRequestStateActive(requestId, state)) return; + mergeHeadersIfMissing(headers, uaCh); + + debug.log("[VOT EXT][bridge] yandex header normalization", { + requestId, + url: urlStr, + headerCount: Object.keys(headers).length, + headerNames: Object.keys(headers), + }); +} + +async function serializeBridgeRequestData( + safeDetails: AnyObject, +): Promise { + if (isBodySerializedForPort(safeDetails?.data)) { + return safeDetails.data; + } + + if (runtimeMessagesUseStructuredClone) { + return safeDetails?.data; + } + + return await serializeBodyForPort(safeDetails?.data); +} + +function buildSerializedBridgeDetails( + safeDetails: AnyObject, + data: AnyObject["data"], +): AnyObject { + return { + ...safeDetails, + data, + responseType: safeDetails?.responseType, + }; +} + +function logSerializedBridgeBody( + requestId: string, + safeDetails: AnyObject, + data: AnyObject["data"], +): void { + const serializedBodySummary = summarizeBodyForDebug(data); + debug.log("[VOT EXT][bridge] serialized body", { + requestId, + url: safeDetails?.url ?? null, + from: summarizeBodyForDebug(safeDetails?.data), + to: serializedBodySummary, + }); +} + +function postBridgeStart( + requestId: string, + state: XhrPortState, + serializedDetails: AnyObject, +): void { + debug.log("[VOT EXT][bridge] post start to background", { + requestId, + url: serializedDetails.url, + method: serializedDetails.method, + responseType: serializedDetails.responseType, + body: summarizeBodyForDebug(serializedDetails.data), + }); + state.port.postMessage({ type: "start", details: serializedDetails }); +} + +function handleStartXhrError( + requestId: string, + normalizedRequestId: string, + safeDetails: AnyObject, + error: unknown, +): void { + const requestKey = normalizedRequestId || requestId; + const st = xhrPorts.get(requestKey); + if (st && !st.settled) { + settleXhrPort(requestKey, st); + } + + const errorMessage = toErrorMessage(error); + debug.log("[VOT EXT][bridge] startXhr error", { + requestId: requestKey, + error: errorMessage, + lastError: ext?.runtime?.lastError ?? null, + }); + + if (requestKey) { + postXhrEvent(requestKey, { + type: "error", + error: makeBridgeXhrError(safeDetails, errorMessage), + }); + } +} + +export async function startBridgeXhr( + requestId: string, + details: AnyObject, +): Promise { + const normalizedRequestId = String(requestId || ""); + const safeDetails: AnyObject = details ?? {}; + + try { + if (!normalizedRequestId) { + throw new Error("Missing requestId for bridge XHR"); + } + requestId = normalizedRequestId; + + if (xhrPorts.has(requestId)) { + debug.warn("[VOT EXT][bridge] replacing active XHR request", { + requestId, + }); + abortBridgeXhr(requestId); + } + + logBridgeXhrStart(requestId, safeDetails); + + const connected = ext?.runtime?.connect?.({ name: PORT_NAME }); + if (!connected || typeof connected !== "object") { + throw new Error("Bridge port is not available"); + } + const port = connected as XhrPortState["port"]; + const responseType = String( + safeDetails?.responseType || "text", + ).toLowerCase(); + const state = createXhrPortState(port, responseType); + xhrPorts.set(requestId, state); + postBridgeXhrAck(requestId, safeDetails, responseType); + attachBridgePortListeners(requestId, port, safeDetails); + await normalizeYandexBridgeHeaders(requestId, safeDetails, state); + + if (!isRequestStateActive(requestId, state)) return; + + const data = await serializeBridgeRequestData(safeDetails); + logSerializedBridgeBody(requestId, safeDetails, data); + + if (!isRequestStateActive(requestId, state)) return; + + const serializedDetails = buildSerializedBridgeDetails(safeDetails, data); + postBridgeStart(requestId, state, serializedDetails); + } catch (error: unknown) { + handleStartXhrError(requestId, normalizedRequestId, safeDetails, error); + } +} + +export function abortBridgeXhr(requestId: string): void { + const st = xhrPorts.get(requestId); + if (!st || st.settled) return; + st.settled = true; + + debug.warn("[VOT EXT][bridge] abortXhr", { requestId }); + + try { + st.port.postMessage({ type: "abort" }); + } catch { + // ignore + } + + disconnectPortSafely(st.port); + xhrPorts.delete(requestId); +} diff --git a/src/extension/prelude.ts b/src/extension/prelude.ts index 40a0f0ea..89634461 100644 --- a/src/extension/prelude.ts +++ b/src/extension/prelude.ts @@ -1,9 +1,6 @@ import debug from "../utils/debug"; import { toErrorMessage } from "../utils/errors"; -import { - serializeBodyForPort, - summarizeBodyForDebug, -} from "./bodySerialization"; +import { summarizeBodyForDebug } from "./bodySerialization"; import { toBridgeMessage } from "./bridgeTransport"; import { type AnyObject, @@ -224,34 +221,17 @@ function toSerializableXhrDetails(details: AnyObject): AnyObject { }; } -async function toBridgeXhrDetails(details: AnyObject): Promise { - const sourceBodySummary = summarizeBodyForDebug(details.data); - const serializedData = await serializeBodyForPort(details.data); - const serializedBodySummary = summarizeBodyForDebug(serializedData); - debug.log("[VOT EXT][prelude] GM_xmlhttpRequest body serialized", { +function toBridgeXhrDetails(details: AnyObject): AnyObject { + debug.log("[VOT EXT][prelude] GM_xmlhttpRequest body passthrough", { url: details.url, method: details.method, - from: sourceBodySummary, - to: serializedBodySummary, + body: summarizeBodyForDebug(details.data), }); - - if ( - typeof serializedData === "string" && - /^\[object [^\]]+\]$/.test(serializedData.trim()) - ) { - debug.warn("[VOT EXT][prelude] suspicious serialized body string", { - url: details.url, - method: details.method, - serializedBody: serializedData, - sourceBody: sourceBodySummary, - }); - } - return { method: details.method, url: details.url, headers: details.headers, - data: serializedData, + data: details.data, timeout: details.timeout, responseType: details.responseType, anonymous: details.anonymous, @@ -285,7 +265,7 @@ function makeXhrTerminalErrorPayload( }; } -function installPageGmPolyfills() { +export function installPageGmPolyfills() { // Legacy GM_notification (callback-based). Used by src/utils/notify.ts. (globalThis as any).GM_notification = (details: unknown) => { try { @@ -330,15 +310,14 @@ function installPageGmPolyfills() { details: toSerializableXhrDetails(callbacks), }); - const startRequest = async () => { + const startRequest = () => { try { if (!active || !xhrCallbacks.has(requestId)) return; - // Serialize in MAIN world before crossing into the isolated bridge. - // This prevents Blob/TypedArray bodies from degrading during - // cross-world structured clone and turning into "[object Object]". - const requestDetails = await toBridgeXhrDetails(callbacks); - if (!active || !xhrCallbacks.has(requestId)) return; + // `window.postMessage()` already uses structured clone, so keep native + // bodies intact here and serialize only once at the runtime messaging + // boundary inside the isolated-world bridge. + const requestDetails = toBridgeXhrDetails(callbacks); debug.log("[VOT EXT][prelude] GM_xmlhttpRequest post TYPE_XHR_START", { requestId, @@ -477,7 +456,7 @@ function installPageGmPolyfills() { }; } -function wireMessageHandlers() { +export function wireMessageHandlers() { globalThis.addEventListener("message", (event) => { if (event.source !== globalThis.window) return; const data = event.data; @@ -610,29 +589,32 @@ function handleXhrEvent(data: AnyObject): void { }); } -const preludeGlobal = globalThis as Record; -if (preludeGlobal[PRELUDE_BOOT_KEY]) { - debug.log("[VOT EXT][prelude] already initialized"); -} else { - preludeGlobal[PRELUDE_BOOT_KEY] = true; +export async function initializePrelude(): Promise { + installPageGmPolyfills(); + wireMessageHandlers(); - const initializePrelude = async () => { - installPageGmPolyfills(); - wireMessageHandlers(); - - // Best-effort handshake to populate GM_info with real manifest metadata. - try { - const { manifest } = await request<{ manifest: AnyObject }>("handshake"); - const gmInfo = (globalThis as any).GM_info as AnyObject; - if (manifest?.name) gmInfo.script.name = manifest.name; - if (manifest?.version) { - gmInfo.script.version = manifest.version; - gmInfo.version = manifest.version; - } - } catch { - // ignore + // Best-effort handshake to populate GM_info with real manifest metadata. + try { + const { manifest } = await request<{ manifest: AnyObject }>("handshake"); + const gmInfo = (globalThis as any).GM_info as AnyObject; + if (manifest?.name) gmInfo.script.name = manifest.name; + if (manifest?.version) { + gmInfo.script.version = manifest.version; + gmInfo.version = manifest.version; } - }; + } catch { + // ignore + } +} + +export function bootstrapExtensionPrelude(): void { + const preludeGlobal = globalThis as Record; + if (preludeGlobal[PRELUDE_BOOT_KEY]) { + debug.log("[VOT EXT][prelude] already initialized"); + return; + } + + preludeGlobal[PRELUDE_BOOT_KEY] = true; void (async () => { try { @@ -642,3 +624,5 @@ if (preludeGlobal[PRELUDE_BOOT_KEY]) { } })(); } + +bootstrapExtensionPrelude(); diff --git a/src/extension/webext.ts b/src/extension/webext.ts index fb75d742..0322cad6 100644 --- a/src/extension/webext.ts +++ b/src/extension/webext.ts @@ -57,6 +57,13 @@ export const ext: WebExtNamespace | null = browserNamespace ?? chromeNamespace ?? null; const isBrowserNamespace = !!browserNamespace && ext === browserNamespace; +const isFirefoxLike = + typeof (browserNamespace?.runtime as { getBrowserInfo?: unknown } | undefined) + ?.getBrowserInfo === "function"; + +export const runtimeMessagesUseStructuredClone = isFirefoxLike; +export const runtimeMessagesUseJsonSerialization = + !runtimeMessagesUseStructuredClone; export function lastErrorMessage(): string | null { // `runtime.lastError` is a Chromium callback-era mechanism. @@ -103,7 +110,28 @@ async function callAsync( export async function storageGet>( keys: unknown, ): Promise { - const area = ext?.storage?.local; + const chromeArea = isFirefoxLike + ? undefined + : chromeNamespace?.storage?.local; + if (chromeArea && typeof chromeArea.get === "function") { + return await new Promise((resolve, reject) => { + try { + chromeArea.get(keys, (items: unknown) => { + const err = lastErrorMessage(); + if (err) { + reject(new Error(err)); + return; + } + + resolve(items as T); + }); + } catch (error) { + reject(error); + } + }); + } + + const area = browserNamespace?.storage?.local ?? ext?.storage?.local; return await callAsync( area?.get?.bind(area) as ((...args: unknown[]) => unknown) | undefined, [keys], @@ -113,7 +141,29 @@ export async function storageGet>( export async function storageSet( items: Record, ): Promise { - const area = ext?.storage?.local; + const chromeArea = isFirefoxLike + ? undefined + : chromeNamespace?.storage?.local; + if (chromeArea && typeof chromeArea.set === "function") { + await new Promise((resolve, reject) => { + try { + chromeArea.set(items, () => { + const err = lastErrorMessage(); + if (err) { + reject(new Error(err)); + return; + } + + resolve(); + }); + } catch (error) { + reject(error); + } + }); + return; + } + + const area = browserNamespace?.storage?.local ?? ext?.storage?.local; await callAsync( area?.set?.bind(area) as ((...args: unknown[]) => unknown) | undefined, [items], @@ -124,7 +174,29 @@ export async function storageSet( } export async function storageRemove(keys: string | string[]): Promise { - const area = ext?.storage?.local; + const chromeArea = isFirefoxLike + ? undefined + : chromeNamespace?.storage?.local; + if (chromeArea && typeof chromeArea.remove === "function") { + await new Promise((resolve, reject) => { + try { + chromeArea.remove(keys, () => { + const err = lastErrorMessage(); + if (err) { + reject(new Error(err)); + return; + } + + resolve(); + }); + } catch (error) { + reject(error); + } + }); + return; + } + + const area = browserNamespace?.storage?.local ?? ext?.storage?.local; await callAsync( area?.remove?.bind(area) as ((...args: unknown[]) => unknown) | undefined, [keys], diff --git a/src/global.d.ts b/src/global.d.ts index 02ebdbe4..46958471 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -5,7 +5,7 @@ const DEBUG_MODE: boolean; * Use `typeof IS_EXTENSION !== "undefined"` checks before reading. */ const IS_EXTENSION: boolean; -const AVAILABLE_LOCALES: import("./localization/localizationProvider").LangOverride[]; +const AVAILABLE_LOCALES: import("./types/localization").LangOverride[]; const REPO_BRANCH: "master" | "dev"; /** @@ -30,6 +30,8 @@ declare const GM: { setValue?(key: string, value: T): Promise; deleteValue?(key: string): Promise; listValues?(): Promise; + xmlHttpRequest?(details: any): Promise & { abort?: () => void }; + xmlhttpRequest?(details: any): Promise & { abort?: () => void }; // Some managers expose more APIs, but we only rely on the above. }; declare function GM_getValue(key: string, defaultValue?: T): T; @@ -48,5 +50,9 @@ interface Window { webkitAudioContext?: typeof AudioContext; } -// Allow side-effect style imports for styles in TS files (e.g. `import "./styles/main.scss"`). +// Allow style imports in TS files, including Vite's inline CSS string mode. declare module "*.scss"; +declare module "*.scss?inline" { + const content: string; + export default content; +} diff --git a/src/headers.json b/src/headers.json index e833a39b..fc9a61c3 100644 --- a/src/headers.json +++ b/src/headers.json @@ -1,7 +1,7 @@ { "name": "[VOT] - Voice Over Translation", "description": "A small extension that adds a Yandex Browser video translation to other browsers", - "version": "1.11.3", + "version": "1.11.4", "author": "Toil, SashaXser, MrSoczekXD, mynovelhost, sodapng", "namespace": "vot", "icon": "https://translate.yandex.ru/icons/favicon.ico", @@ -132,6 +132,10 @@ "*://iframe.mediadelivery.net/*", "*://video.bunnycdn.com/*", "*://*.weibo.com/*", + "*://*.jove.com/*", + "*://*.preservetube.com/*", + "*://*.mediafile.cc/*", + "*://projector.datacamp.com/*", "*://*/*.mp4*", "*://*/*.webm*" ], diff --git a/src/index.ts b/src/index.ts index fe4501f4..caf64e2f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,11 +7,13 @@ import type { ClientSession, SessionModule } from "@vot.js/shared/types/secure"; import Chaimu from "chaimu/client"; import { initAudioContext } from "chaimu/player"; import { getOrCreateBootState } from "./bootstrap/bootState"; +import { initIframeInteractor } from "./bootstrap/iframeInteractor"; import { ensureRuntimeActivated } from "./bootstrap/runtimeActivation"; import { bindObserverListeners } from "./bootstrap/videoObserverBinding"; import { + authServerUrl, minLongWaitingCount, - proxyWorkerHost, + proxyWorkerHostMode1, votBackendUrl, workerHost, } from "./config/config"; @@ -22,12 +24,12 @@ import { resolveOverlayMountTargets } from "./core/overlayMountTargets"; import { VOTTranslationHandler } from "./core/translationHandler"; import { TranslationOrchestrator } from "./core/translationOrchestrator"; import { VideoLifecycleController } from "./core/videoLifecycleController"; +import { createVideoLifecycleHost } from "./core/videoLifecycleHost"; import { VOTVideoManager } from "./core/videoManager"; import { localizationProvider } from "./localization/localizationProvider"; import type { ProcessedSubtitles } from "./subtitles/processor"; -import type { SubtitleFontFamily } from "./subtitles/types"; import { SubtitlesWidget } from "./subtitles/widget"; -import type { StorageData } from "./types/storage"; +import type { ResponseLanguageSubtitles, StorageData } from "./types/storage"; import type { OverlayMount } from "./types/uiManager"; import { UIManager } from "./ui/manager"; import { isSameOverlayMount } from "./ui/mount"; @@ -35,7 +37,7 @@ import { OverlayVisibilityController } from "./ui/overlayVisibilityController"; import debug from "./utils/debug"; import { resolveScopedFullscreenElement } from "./utils/dom"; import { getEnvironmentInfo as getEnvironmentInfoImpl } from "./utils/environment"; -import { GM_fetch } from "./utils/gm"; +import { GM_fetch, isSupportGMXhr } from "./utils/gm"; import { isIframe } from "./utils/iframeConnector"; import { createIntervalIdleChecker, @@ -71,15 +73,22 @@ import { // VideoHandler is large; keep the public API but move big feature areas into // dedicated modules for cohesion. import { init as initVideoHandler } from "./videoHandler/modules/init"; +import { + isProxyClientEnabled, + resolveProxyWorkerHost, + shouldForceProxyClientGmXhr, +} from "./videoHandler/modules/proxyShared"; import { changeSubtitlesLang as changeSubtitlesLangImpl, enableSubtitlesForCurrentLangPair as enableSubtitlesForCurrentLangPairImpl, ensureSubtitlesForCurrentLangPair as ensureSubtitlesForCurrentLangPairImpl, loadSubtitles as loadSubtitlesImpl, + resolveSubtitlesLanguage, toggleSubtitlesForCurrentLangPair as toggleSubtitlesForCurrentLangPairImpl, updateSubtitlesLangSelect as updateSubtitlesLangSelectImpl, } from "./videoHandler/modules/subtitles"; import { + applyManualVideoVolumeOverride as applyManualVideoVolumeOverrideImpl, handleProxySettingsChanged as handleProxySettingsChangedImpl, isMultiMethodS3 as isMultiMethodS3Impl, isYouTubeHosts as isYouTubeHostsImpl, @@ -102,9 +111,7 @@ export { getEnvironmentInfo } from "./utils/environment"; export type { VideoData } from "./videoHandler/shared"; export { countryCode } from "./videoHandler/shared"; -const RESPONSE_LANG_SET = new Set(availableTTS as readonly string[]); -const isResponseLang = (value: string): value is ResponseLang => - RESPONSE_LANG_SET.has(value); +const _RESPONSE_LANG_SET = new Set(availableTTS as readonly string[]); const RESOLVED_VOID_PROMISE: Promise = Promise.resolve(); type InternalVideoVolumeSetHistoryEntry = { @@ -155,7 +162,6 @@ export class VideoHandler { subtitlesLoadPromises = new Map>(); downloadTranslationUrl: string | null = null; - translationRefreshTimeout?: ReturnType; isRefreshingTranslation = false; autoRetry?: ReturnType; @@ -363,14 +369,28 @@ export class VideoHandler { * Bugfix: subtitles cache key must match the key used by loadSubtitles(). * @param {string} videoId * @param {string} detectedLanguage - * @param {string} responseLanguage + * @param {string} subtitleLanguage */ getSubtitlesCacheKey( videoId: string, detectedLanguage: string, - responseLanguage: string, + subtitleLanguage: string, ): string { - return `${videoId}_${detectedLanguage}_${responseLanguage}_${Boolean(this.data?.useLivelyVoice)}`; + return `${videoId}_${detectedLanguage}_${subtitleLanguage}_${Boolean(this.data?.useLivelyVoice)}`; + } + + getPreferredSubtitlesLanguage( + detectedLanguage: string = this.videoData?.detectedLanguage ?? "auto", + responseLanguage: string = this.videoData?.responseLanguage ?? + this.translateToLang, + preference: ResponseLanguageSubtitles | undefined = this.data + ?.responseLanguageSubtitles, + ): string | undefined { + return resolveSubtitlesLanguage( + preference, + detectedLanguage, + responseLanguage, + ); } isActionStale(actionContext?: { gen: number; videoId: string }): boolean { @@ -384,7 +404,7 @@ export class VideoHandler { private updateVOTClientRequestSignal(): void { if (!this.votClient) return; this.votClient.fetchOpts = { - ...(this.votClient.fetchOpts ?? {}), + ...this.votClient.fetchOpts, signal: this.actionsAbortController.signal, }; } @@ -427,7 +447,6 @@ export class VideoHandler { this.cacheManager = new CacheManager(); this.interactionChecker = createIntervalIdleChecker(); this.interactionChecker.start(); - const self = () => this; // Create helper instances const mount = this.getOverlayMount(container); this.uiManager = new UIManager({ @@ -485,89 +504,9 @@ export class VideoHandler { }); }, }); - const lifecycleHost = { - get video() { - return self().video; - }, - get site() { - return self().site; - }, - get container() { - return self().container; - }, - set container(value: HTMLElement) { - if (self().container === value) { - return; - } - self().container = value; - self().uiManager.updateMount(self().getOverlayMount(value)); - }, - get firstPlay() { - return self().firstPlay; - }, - set firstPlay(value: boolean) { - self().firstPlay = value; - }, - stopTranslation: () => this.stopTranslation(), - get uiManager() { - return self().uiManager as any; - }, - getVideoData: () => this.getVideoData(), - cacheManager: { - getSubtitles: (key: string) => self().cacheManager.getSubtitles(key), - }, - getSubtitlesCacheKey: ( - videoId: string, - detectedLanguage: string, - responseLanguage: string, - ) => - this.getSubtitlesCacheKey(videoId, detectedLanguage, responseLanguage), - updateSubtitlesLangSelect: () => this.updateSubtitlesLangSelect(), - enableSubtitlesForCurrentLangPair: () => - this.enableSubtitlesForCurrentLangPair(), - setSelectMenuValues: (from: string, to: string) => - this.setSelectMenuValues(from, to), - get translateToLang() { - return self().translateToLang; - }, - set translateToLang(value: string) { - if (isResponseLang(value)) self().translateToLang = value; - }, - get data() { - return self().data ?? {}; - }, - get subtitles() { - return self().subtitles; - }, - set subtitles(value: any[]) { - self().subtitles = value; - }, - get subtitlesCacheKey() { - return self().subtitlesCacheKey; - }, - set subtitlesCacheKey(value: string | null) { - self().subtitlesCacheKey = value; - }, - get videoData() { - return self().videoData; - }, - set videoData(value: any) { - self().videoData = value; - }, - get actionsAbortController() { - return self().actionsAbortController; - }, - set actionsAbortController(value: AbortController) { - self().actionsAbortController = value; - }, - resetActionsAbortController: (reason?: unknown) => - this.resetActionsAbortController(reason), - initVOTClient: () => this.initVOTClient(), - translationOrchestrator: this.translationOrchestrator, - resetSubtitlesWidget: () => this.resetSubtitlesWidget(), - queueOverlayAutoHide: () => this.overlayVisibility?.queueAutoHide(), - }; - this.lifecycleController = new VideoLifecycleController(lifecycleHost); + this.lifecycleController = new VideoLifecycleController( + createVideoLifecycleHost(this, (value) => this.getOverlayMount(value)), + ); this.translationHandler = new VOTTranslationHandler(this); this.videoManager = new VOTVideoManager(this); } @@ -585,37 +524,41 @@ export class VideoHandler { this.interactionChecker, this.tooltipLayoutRoot, ); - - if (this.data) { - // Smart layout is enabled by default for new users. - // When enabled, the widget will compute font-size / line length based on player size. - this.subtitlesWidget.setSmartLayout( - typeof this.data.subtitlesSmartLayout === "boolean" - ? this.data.subtitlesSmartLayout - : true, - ); - if (typeof this.data.subtitlesMaxLength === "number") { - this.subtitlesWidget.setMaxLength(this.data.subtitlesMaxLength); - } - if (typeof this.data.highlightWords === "boolean") { - this.subtitlesWidget.setHighlightWords(this.data.highlightWords); - } - if (typeof this.data.subtitlesFontSize === "number") { - this.subtitlesWidget.setFontSize(this.data.subtitlesFontSize); - } - if (typeof this.data.subtitlesFontFamily === "string") { - this.subtitlesWidget.setFontFamily( - this.data.subtitlesFontFamily as SubtitleFontFamily, - ); - } - if (typeof this.data.subtitlesOpacity === "number") { - this.subtitlesWidget.setOpacity(this.data.subtitlesOpacity); - } - } + this.applySavedSubtitlesWidgetSettings(this.subtitlesWidget); } return this.subtitlesWidget; } + private applySavedSubtitlesWidgetSettings(widget: SubtitlesWidget): void { + if (!this.data) { + return; + } + + // Smart layout is enabled by default for new users. + // When enabled, the widget will compute font-size / line length based on player size. + 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} @@ -805,21 +748,31 @@ export class VideoHandler { * @returns {VideoHandler} This instance. */ async initVOTClient() { - const transportHost = this.data?.translateProxyEnabled - ? (this.data?.proxyWorkerHost ?? proxyWorkerHost) - : workerHost; + 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, + // Proxy mode routes requests through the worker/backend where page-world + // fetch() adds an avoidable CORS preflight. GM transport skips that hop. + forceGmXhr: shouldForceProxyClientGmXhr({ + ...this.data, + gmXhrSupported: isSupportGMXhr, + }), }, apiToken: this.data?.account?.token, hostVOT: votBackendUrl, host: transportHost, }; - this.votClient = new ( - this.data?.translateProxyEnabled ? VOTWorkerClient : VOTClient - )(this.votOpts); + this.votClient = new (proxyClientEnabled ? VOTWorkerClient : VOTClient)( + this.votOpts, + ); this.votClient.sessions = await this.votSessionStorage.restore( transportHost, this.votClient.sessions, @@ -1093,6 +1046,12 @@ export class VideoHandler { this.volumeLinkState.initialized = true; } + clearVolumeLinkState(): void { + this.volumeLinkState.initialized = false; + this.volumeLinkState.lastVideoPercent = 0; + this.volumeLinkState.lastTranslationPercent = 0; + } + /** * Checks if the video is muted. * @returns {boolean} True if muted. @@ -1123,10 +1082,9 @@ export class VideoHandler { /** * Keeps translation and video sliders linked (syncVolume option). * - * The implementation is delta-based: when the user changes one slider, the - * other slider moves by the same delta. This preserves the relative - * difference between volumes and works with "audio booster" (translation can - * exceed 100%). + * 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: "translation" | "video", @@ -1234,10 +1192,6 @@ export class VideoHandler { clearTimeout(this.autoRetry); this.autoRetry = undefined; } - if (this.translationRefreshTimeout !== undefined) { - clearTimeout(this.translationRefreshTimeout); - this.translationRefreshTimeout = undefined; - } // Cancel in-flight translation work. this.resetActionsAbortController("stopTranslate"); }; @@ -1278,52 +1232,17 @@ export class VideoHandler { errorMessage = new VOTLocalizedError("TranslationDelayed"); } debug.log("updateTranslationErrorMsg message", errorMessage); - if (errorMessage?.name === "VOTLocalizedError") { - this.transformBtn("error", errorMessage.localizedMessage); - } else if (errorMessage instanceof Error) { - this.transformBtn("error", errorMessage?.message); - } else if ( - this.data?.translateAPIErrors && - lang !== "ru" && - !errorMessage?.includes(translationTake) - ) { - const overlayView = this.uiManager.votOverlayView; - if (!overlayView?.votButton) { - return; - } - const messageStr = Array.isArray(errorMessage) - ? errorMessage.join(" ") - : String(errorMessage); - const cacheKey = `${lang}:${messageStr}`; - const cached = this.errorTranslationCache.get(cacheKey); - if (cached) { - this.transformBtn("error", cached); - } else { - overlayView.votButton.loading = true; - const translatedMessage = await translate(messageStr, "ru", lang); - const translatedText = Array.isArray(translatedMessage) - ? translatedMessage.join("\n") - : String(translatedMessage); - if (signal?.aborted) { - return; - } - this.errorTranslationCache.set(cacheKey, translatedText); - // Prevent unbounded growth. - if (this.errorTranslationCache.size > 50) { - const oldestKey = this.errorTranslationCache.keys().next().value; - if (oldestKey) this.errorTranslationCache.delete(oldestKey); - } - this.transformBtn("error", translatedText); - } - if (signal?.aborted) { - return; - } - } else { - const msg = Array.isArray(errorMessage) - ? errorMessage.join("\n") - : String(errorMessage ?? ""); - this.transformBtn("error", msg); + const resolvedMessage = await this.resolveTranslationErrorDisplayMessage( + errorMessage, + translationTake, + lang, + signal, + ); + if (signal?.aborted || resolvedMessage === null) { + return; } + this.transformBtn("error", resolvedMessage); + if (signal?.aborted) { return; } @@ -1341,6 +1260,90 @@ export class VideoHandler { } } + private async resolveTranslationErrorDisplayMessage( + errorMessage: any, + translationTake: string, + lang: string, + signal?: AbortSignal, + ): Promise { + 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); + } + + private shouldTranslateErrorMessage( + errorMessage: any, + translationTake: string, + lang: string, + ): boolean { + return ( + Boolean(this.data?.translateAPIErrors) && + lang !== "ru" && + !errorMessage?.includes(translationTake) + ); + } + + private stringifyTranslationError(errorMessage: any): string { + return Array.isArray(errorMessage) + ? errorMessage.join("\n") + : String(errorMessage ?? ""); + } + + private async getTranslatedErrorMessage( + errorMessage: any, + lang: string, + signal?: AbortSignal, + ): Promise { + 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; + } + + private trimErrorTranslationCache(): void { + 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. @@ -1486,6 +1489,10 @@ export class VideoHandler { return this.callModule(setupAudioSettingsImpl); } + applyManualVideoVolumeOverride(volume: number) { + return this.callModule(applyManualVideoVolumeOverrideImpl, volume); + } + /** * Stops translation and synchronizes volume. */ @@ -1636,6 +1643,7 @@ async function main(): Promise { isIframe: isIframe(), href: String(globalThis.location.href || ""), origin: globalThis.location.origin, + authOrigin: authServerUrl, }); if (bootstrapMode === "skip") { @@ -1643,11 +1651,15 @@ async function main(): Promise { return; } - logBootstrap("Loading extension"); - if (bootstrapMode === "top-full") { - await ensureRuntimeActivated("top-frame", logBootstrap); + // Some hosts exchange iframe video identifiers via postMessage before a + // playable