diff --git a/materialious/android/app/build.gradle b/materialious/android/app/build.gradle
index b4e17bb7..9054238c 100644
--- a/materialious/android/app/build.gradle
+++ b/materialious/android/app/build.gradle
@@ -8,7 +8,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 104
- versionName "1.7.22"
+ versionName "1.8.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
diff --git a/materialious/electron/materialious.metainfo.xml b/materialious/electron/materialious.metainfo.xml
index f86c319b..6a4627e3 100644
--- a/materialious/electron/materialious.metainfo.xml
+++ b/materialious/electron/materialious.metainfo.xml
@@ -65,9 +65,12 @@
-
- https://github.com/Materialious/Materialious/releases/tag/1.7.22
-
+
+ https://github.com/Materialious/Materialious/releases/tag/1.8.0
+
+
+ https://github.com/Materialious/Materialious/releases/tag/1.7.22
+
https://github.com/Materialious/Materialious/releases/tag/1.7.21
diff --git a/materialious/electron/package-lock.json b/materialious/electron/package-lock.json
index 29b0bd5a..fa9baf87 100644
--- a/materialious/electron/package-lock.json
+++ b/materialious/electron/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "Materialious",
- "version": "1.7.21",
+ "version": "1.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Materialious",
- "version": "1.7.21",
+ "version": "1.8.0",
"license": "MIT",
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
diff --git a/materialious/electron/package.json b/materialious/electron/package.json
index 1b0685a8..1fb139ba 100644
--- a/materialious/electron/package.json
+++ b/materialious/electron/package.json
@@ -1,6 +1,6 @@
{
"name": "Materialious",
- "version": "1.7.22",
+ "version": "1.8.0",
"description": "Modern material design for Invidious.",
"author": {
"name": "Ward Pearce",
diff --git a/materialious/package-lock.json b/materialious/package-lock.json
index 48ecbc9c..3b9febc2 100644
--- a/materialious/package-lock.json
+++ b/materialious/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "materialious",
- "version": "1.7.21",
+ "version": "1.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "materialious",
- "version": "1.7.21",
+ "version": "1.8.0",
"hasInstallScript": true,
"dependencies": {
"@capacitor-community/electron": "^5.0.1",
@@ -23,7 +23,7 @@
"bgutils-js": "^3.2.0",
"capacitor-nodejs": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz",
"fuse.js": "^7.0.0",
- "googlevideo": "^2.0.0",
+ "googlevideo": "^3.0.0",
"he": "^1.2.0",
"human-number": "^2.0.4",
"iso-3166": "^4.3.0",
@@ -54,7 +54,6 @@
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.18.0",
"@vite-pwa/sveltekit": "^0.6.6",
- "buffer": "^6.0.3",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.45.1",
@@ -65,7 +64,7 @@
"svelte-check": "^4.0.0",
"tslib": "^2.7.0",
"typescript": "^5.6.3",
- "vite": "^6.2.6"
+ "vite": "^6.3.4"
}
},
"node_modules/@ampproject/remapping": {
@@ -2278,9 +2277,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz",
- "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz",
+ "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==",
"cpu": [
"arm64"
],
@@ -2311,9 +2310,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz",
- "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz",
+ "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==",
"cpu": [
"arm64"
],
@@ -2408,9 +2407,9 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
- "version": "4.5.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz",
- "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==",
+ "version": "4.6.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz",
+ "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2941,6 +2940,19 @@
"node": ">= 10"
}
},
+ "node_modules/@noble/hashes": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2979,10 +2991,20 @@
"node": ">= 8"
}
},
+ "node_modules/@paralleldrive/cuid2": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
+ "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "^1.1.5"
+ }
+ },
"node_modules/@polka/url": {
- "version": "1.0.0-next.28",
- "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz",
- "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==",
+ "version": "1.0.0-next.29",
+ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
+ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
"dev": true,
"license": "MIT"
},
@@ -3086,9 +3108,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.38.0.tgz",
- "integrity": "sha512-ldomqc4/jDZu/xpYU+aRxo3V4mGCV9HeTgUBANI3oIQMOL+SsxB+S2lxMpkFp5UamSS3XuTMQVbsS24R4J4Qjg==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz",
+ "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==",
"cpu": [
"arm"
],
@@ -3100,9 +3122,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.38.0.tgz",
- "integrity": "sha512-VUsgcy4GhhT7rokwzYQP+aV9XnSLkkhlEJ0St8pbasuWO/vwphhZQxYEKUP3ayeCYLhk6gEtacRpYP/cj3GjyQ==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz",
+ "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==",
"cpu": [
"arm64"
],
@@ -3114,9 +3136,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.38.0.tgz",
- "integrity": "sha512-buA17AYXlW9Rn091sWMq1xGUvWQFOH4N1rqUxGJtEQzhChxWjldGCCup7r/wUnaI6Au8sKXpoh0xg58a7cgcpg==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz",
+ "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==",
"cpu": [
"arm64"
],
@@ -3128,9 +3150,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.38.0.tgz",
- "integrity": "sha512-Mgcmc78AjunP1SKXl624vVBOF2bzwNWFPMP4fpOu05vS0amnLcX8gHIge7q/lDAHy3T2HeR0TqrriZDQS2Woeg==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz",
+ "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==",
"cpu": [
"x64"
],
@@ -3142,9 +3164,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.38.0.tgz",
- "integrity": "sha512-zzJACgjLbQTsscxWqvrEQAEh28hqhebpRz5q/uUd1T7VTwUNZ4VIXQt5hE7ncs0GrF+s7d3S4on4TiXUY8KoQA==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz",
+ "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==",
"cpu": [
"arm64"
],
@@ -3156,9 +3178,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.38.0.tgz",
- "integrity": "sha512-hCY/KAeYMCyDpEE4pTETam0XZS4/5GXzlLgpi5f0IaPExw9kuB+PDTOTLuPtM10TlRG0U9OSmXJ+Wq9J39LvAg==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz",
+ "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==",
"cpu": [
"x64"
],
@@ -3170,9 +3192,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.38.0.tgz",
- "integrity": "sha512-mimPH43mHl4JdOTD7bUMFhBdrg6f9HzMTOEnzRmXbOZqjijCw8LA5z8uL6LCjxSa67H2xiLFvvO67PT05PRKGg==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz",
+ "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==",
"cpu": [
"arm"
],
@@ -3184,9 +3206,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.38.0.tgz",
- "integrity": "sha512-tPiJtiOoNuIH8XGG8sWoMMkAMm98PUwlriOFCCbZGc9WCax+GLeVRhmaxjJtz6WxrPKACgrwoZ5ia/uapq3ZVg==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz",
+ "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==",
"cpu": [
"arm"
],
@@ -3198,9 +3220,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.38.0.tgz",
- "integrity": "sha512-wZco59rIVuB0tjQS0CSHTTUcEde+pXQWugZVxWaQFdQQ1VYub/sTrNdY76D1MKdN2NB48JDuGABP6o6fqos8mA==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz",
+ "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==",
"cpu": [
"arm64"
],
@@ -3212,9 +3234,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.38.0.tgz",
- "integrity": "sha512-fQgqwKmW0REM4LomQ+87PP8w8xvU9LZfeLBKybeli+0yHT7VKILINzFEuggvnV9M3x1Ed4gUBmGUzCo/ikmFbQ==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz",
+ "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==",
"cpu": [
"arm64"
],
@@ -3226,9 +3248,9 @@
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.38.0.tgz",
- "integrity": "sha512-hz5oqQLXTB3SbXpfkKHKXLdIp02/w3M+ajp8p4yWOWwQRtHWiEOCKtc9U+YXahrwdk+3qHdFMDWR5k+4dIlddg==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz",
+ "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==",
"cpu": [
"loong64"
],
@@ -3240,9 +3262,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.38.0.tgz",
- "integrity": "sha512-NXqygK/dTSibQ+0pzxsL3r4Xl8oPqVoWbZV9niqOnIHV/J92fe65pOir0xjkUZDRSPyFRvu+4YOpJF9BZHQImw==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz",
+ "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==",
"cpu": [
"ppc64"
],
@@ -3254,9 +3276,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.38.0.tgz",
- "integrity": "sha512-GEAIabR1uFyvf/jW/5jfu8gjM06/4kZ1W+j1nWTSSB3w6moZEBm7iBtzwQ3a1Pxos2F7Gz+58aVEnZHU295QTg==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz",
+ "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==",
"cpu": [
"riscv64"
],
@@ -3268,9 +3290,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.38.0.tgz",
- "integrity": "sha512-9EYTX+Gus2EGPbfs+fh7l95wVADtSQyYw4DfSBcYdUEAmP2lqSZY0Y17yX/3m5VKGGJ4UmIH5LHLkMJft3bYoA==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz",
+ "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==",
"cpu": [
"riscv64"
],
@@ -3282,9 +3304,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.38.0.tgz",
- "integrity": "sha512-Mpp6+Z5VhB9VDk7RwZXoG2qMdERm3Jw07RNlXHE0bOnEeX+l7Fy4bg+NxfyN15ruuY3/7Vrbpm75J9QHFqj5+Q==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz",
+ "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==",
"cpu": [
"s390x"
],
@@ -3296,9 +3318,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.38.0.tgz",
- "integrity": "sha512-vPvNgFlZRAgO7rwncMeE0+8c4Hmc+qixnp00/Uv3ht2x7KYrJ6ERVd3/R0nUtlE6/hu7/HiiNHJ/rP6knRFt1w==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz",
+ "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==",
"cpu": [
"x64"
],
@@ -3310,9 +3332,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.38.0.tgz",
- "integrity": "sha512-q5Zv+goWvQUGCaL7fU8NuTw8aydIL/C9abAVGCzRReuj5h30TPx4LumBtAidrVOtXnlB+RZkBtExMsfqkMfb8g==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz",
+ "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==",
"cpu": [
"x64"
],
@@ -3324,9 +3346,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.38.0.tgz",
- "integrity": "sha512-u/Jbm1BU89Vftqyqbmxdq14nBaQjQX1HhmsdBWqSdGClNaKwhjsg5TpW+5Ibs1mb8Es9wJiMdl86BcmtUVXNZg==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz",
+ "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==",
"cpu": [
"arm64"
],
@@ -3338,9 +3360,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.38.0.tgz",
- "integrity": "sha512-mqu4PzTrlpNHHbu5qleGvXJoGgHpChBlrBx/mEhTPpnAL1ZAYFlvHD7rLK839LLKQzqEQMFJfGrrOHItN4ZQqA==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz",
+ "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==",
"cpu": [
"ia32"
],
@@ -3352,9 +3374,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.38.0.tgz",
- "integrity": "sha512-jjqy3uWlecfB98Psxb5cD6Fny9Fupv9LrDSPTQZUROqjvZmcCqNu4UMl7qqhlUUGpwiAkotj6GYu4SZdcr/nLw==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz",
+ "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==",
"cpu": [
"x64"
],
@@ -3717,12 +3739,12 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "22.13.14",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz",
- "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==",
+ "version": "22.15.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz",
+ "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==",
"license": "MIT",
"dependencies": {
- "undici-types": "~6.20.0"
+ "undici-types": "~6.21.0"
}
},
"node_modules/@types/normalize-package-data": {
@@ -4265,9 +4287,9 @@
}
},
"node_modules/axios": {
- "version": "1.8.4",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
- "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
+ "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@@ -4362,9 +4384,9 @@
"optional": true
},
"node_modules/bare-fs": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.2.tgz",
- "integrity": "sha512-S5mmkMesiduMqnz51Bfh0Et9EX0aTCJxhsI4bvzFFLs8Z1AV8RDHadfY5CyLwdoLHgXbNBEN1gQcbEtGwuvixw==",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.3.tgz",
+ "integrity": "sha512-OeEZYIg+2qepaWLyphaOXHAHKo3xkM8y3BeGAvHdMN8GNWvEAU1Yw6rYpGzu/wDDbKxgEjVeVDpgGhDzaeMpjg==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
@@ -4451,9 +4473,9 @@
"license": "MIT"
},
"node_modules/beercss": {
- "version": "3.9.7",
- "resolved": "https://registry.npmjs.org/beercss/-/beercss-3.9.7.tgz",
- "integrity": "sha512-b/mqRLlzE4aO943minjHA934p0o635EGx0Mgjw2VEgfD+c27GyRsadyzxcEaL/JA6FaSsKZ9+mrsdwL4yxYQLQ==",
+ "version": "3.10.8",
+ "resolved": "https://registry.npmjs.org/beercss/-/beercss-3.10.8.tgz",
+ "integrity": "sha512-LWwkkhF5F3VAD2z3pkDuWD540l/OUKdway9n+ZW+6naX+bKYPl9PKh/nD47/djYqYeH4jGu9QZcpk7p3Xv7YfQ==",
"license": "MIT",
"dependencies": {
"material-dynamic-colors": "^1.1.2"
@@ -4597,31 +4619,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
- "node_modules/buffer": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
- "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.2.1"
- }
- },
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
@@ -4725,9 +4722,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001707",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz",
- "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==",
+ "version": "1.0.30001716",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001716.tgz",
+ "integrity": "sha512-49/c1+x3Kwz7ZIWt+4DvK3aMJy9oYXXG6/97JKsnjdCk/6n9vVyWL8NAwVt95Lwt9eigI10Hl782kDfZUUlRXw==",
"dev": true,
"funding": [
{
@@ -5267,9 +5264,9 @@
}
},
"node_modules/core-js-compat": {
- "version": "3.41.0",
- "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
- "integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==",
+ "version": "3.42.0",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.42.0.tgz",
+ "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -5708,9 +5705,9 @@
}
},
"node_modules/detect-libc": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
- "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+ "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -5884,9 +5881,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.128",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz",
- "integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==",
+ "version": "1.5.145",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.145.tgz",
+ "integrity": "sha512-pZ5EcTWRq/055MvSBgoFEyKf2i4apwfoqJbK/ak2jnFq8oHjZ+vzc3AhRcz37Xn+ZJfL58R666FLJx0YOK9yTw==",
"dev": true,
"license": "ISC",
"peer": true
@@ -6426,9 +6423,9 @@
}
},
"node_modules/esrap": {
- "version": "1.4.5",
- "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.5.tgz",
- "integrity": "sha512-CjNMjkBWWZeHn+VX+gS8YvFwJ5+NDhg8aWZBSFJPR8qQduDNjbJodA2WcwCm7uQa5Rjqj+nZvVmceg1RbHFB9g==",
+ "version": "1.4.6",
+ "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.6.tgz",
+ "integrity": "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
@@ -6828,16 +6825,19 @@
}
},
"node_modules/formidable": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz",
- "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==",
+ "version": "3.5.4",
+ "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
+ "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
"dev": true,
"license": "MIT",
"dependencies": {
+ "@paralleldrive/cuid2": "^2.2.2",
"dezalgo": "^1.0.4",
- "hexoid": "^2.0.0",
"once": "^1.4.0"
},
+ "engines": {
+ "node": ">=14.0.0"
+ },
"funding": {
"url": "https://ko-fi.com/tunnckoCore/commissions"
}
@@ -7344,9 +7344,9 @@
"license": "MIT"
},
"node_modules/googlevideo": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/googlevideo/-/googlevideo-2.0.0.tgz",
- "integrity": "sha512-OVlNWZ07TPIelaEII6mH9od+Cxljl7P4AzhEYVNN5d4FhFT9L5otpcLtgvraTE9u69KfVVw+L4pVeczArcD33w==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/googlevideo/-/googlevideo-3.0.0.tgz",
+ "integrity": "sha512-8LGFjbZFG4RCsyhMbjX2+IlU5Suj94FeQr1JandPSsKfAMrqL08pOoQsBYl/5P+bqIFm0wQEVpKIhm5wpiekwg==",
"funding": [
"https://github.com/sponsors/LuanRT"
],
@@ -7526,16 +7526,6 @@
"he": "bin/he"
}
},
- "node_modules/hexoid": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz",
- "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/hosted-git-info": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
@@ -8425,14 +8415,14 @@
"license": "MIT"
},
"node_modules/json-stable-stringify": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.2.1.tgz",
- "integrity": "sha512-Lp6HbbBgosLmJbjx0pBLbgvx68FaFU1sdkmBuckmhhJ88kL13OA51CDtR2yJB50eCNMH9wRqtQNNiAqQH4YXnA==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
+ "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
- "call-bound": "^1.0.3",
+ "call-bound": "^1.0.4",
"isarray": "^2.0.5",
"jsonify": "^0.0.1",
"object-keys": "^1.1.1"
@@ -11123,9 +11113,9 @@
}
},
"node_modules/rollup": {
- "version": "4.38.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.38.0.tgz",
- "integrity": "sha512-5SsIRtJy9bf1ErAOiFMFzl64Ex9X5V7bnJ+WlFMb+zmP459OSWCEG7b0ERZ+PEU7xPt4OG3RHbrp1LJlXxYTrw==",
+ "version": "4.40.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz",
+ "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11139,26 +11129,26 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.38.0",
- "@rollup/rollup-android-arm64": "4.38.0",
- "@rollup/rollup-darwin-arm64": "4.38.0",
- "@rollup/rollup-darwin-x64": "4.38.0",
- "@rollup/rollup-freebsd-arm64": "4.38.0",
- "@rollup/rollup-freebsd-x64": "4.38.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.38.0",
- "@rollup/rollup-linux-arm-musleabihf": "4.38.0",
- "@rollup/rollup-linux-arm64-gnu": "4.38.0",
- "@rollup/rollup-linux-arm64-musl": "4.38.0",
- "@rollup/rollup-linux-loongarch64-gnu": "4.38.0",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.38.0",
- "@rollup/rollup-linux-riscv64-gnu": "4.38.0",
- "@rollup/rollup-linux-riscv64-musl": "4.38.0",
- "@rollup/rollup-linux-s390x-gnu": "4.38.0",
- "@rollup/rollup-linux-x64-gnu": "4.38.0",
- "@rollup/rollup-linux-x64-musl": "4.38.0",
- "@rollup/rollup-win32-arm64-msvc": "4.38.0",
- "@rollup/rollup-win32-ia32-msvc": "4.38.0",
- "@rollup/rollup-win32-x64-msvc": "4.38.0",
+ "@rollup/rollup-android-arm-eabi": "4.40.1",
+ "@rollup/rollup-android-arm64": "4.40.1",
+ "@rollup/rollup-darwin-arm64": "4.40.1",
+ "@rollup/rollup-darwin-x64": "4.40.1",
+ "@rollup/rollup-freebsd-arm64": "4.40.1",
+ "@rollup/rollup-freebsd-x64": "4.40.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.40.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.40.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.40.1",
+ "@rollup/rollup-linux-arm64-musl": "4.40.1",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.40.1",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.40.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.40.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.40.1",
+ "@rollup/rollup-linux-x64-gnu": "4.40.1",
+ "@rollup/rollup-linux-x64-musl": "4.40.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.40.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.40.1",
+ "@rollup/rollup-win32-x64-msvc": "4.40.1",
"fsevents": "~2.3.2"
}
},
@@ -12032,9 +12022,9 @@
}
},
"node_modules/svelte": {
- "version": "5.25.5",
- "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.25.5.tgz",
- "integrity": "sha512-ULi9rkVWQJyJYZSpy6SIgSTchWadyWG1QYAUx3JAXL2gXrnhdXtoB20KmXGSNdtNyquq3eYd/gkwAkLcL5PGWw==",
+ "version": "5.28.2",
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.28.2.tgz",
+ "integrity": "sha512-FbWBxgWOpQfhKvoGJv/TFwzqb4EhJbwCD17dB0tEpQiw1XyUEKZJtgm4nA4xq3LLsMo7hu5UY/BOFmroAxKTMg==",
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.3.0",
@@ -12046,7 +12036,7 @@
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"esm-env": "^1.2.1",
- "esrap": "^1.4.3",
+ "esrap": "^1.4.6",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
@@ -12057,9 +12047,9 @@
}
},
"node_modules/svelte-check": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.5.tgz",
- "integrity": "sha512-Gb0T2IqBNe1tLB9EB1Qh+LOe+JB8wt2/rNBDGvkxQVvk8vNeAoG+vZgFB/3P5+zC7RWlyBlzm9dVjZFph/maIg==",
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.6.tgz",
+ "integrity": "sha512-P7w/6tdSfk3zEVvfsgrp3h3DFC75jCdZjTQvgGJtjPORs1n7/v2VMPIoty3PWv7jnfEm3x0G/p9wH4pecTb0Wg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -12081,9 +12071,9 @@
}
},
"node_modules/svelte-check/node_modules/fdir": {
- "version": "6.4.3",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
- "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
+ "version": "6.4.4",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
+ "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -12367,13 +12357,13 @@
}
},
"node_modules/tinyglobby": {
- "version": "0.2.12",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
- "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==",
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
+ "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "fdir": "^6.4.3",
+ "fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
@@ -12384,9 +12374,9 @@
}
},
"node_modules/tinyglobby/node_modules/fdir": {
- "version": "6.4.3",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
- "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
+ "version": "6.4.4",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
+ "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -12671,9 +12661,9 @@
}
},
"node_modules/typescript": {
- "version": "5.8.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
- "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
+ "version": "5.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -12731,9 +12721,9 @@
}
},
"node_modules/undici-types": {
- "version": "6.20.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
- "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
"node_modules/unicode-canonical-property-names-ecmascript": {
@@ -12904,15 +12894,18 @@
}
},
"node_modules/vite": {
- "version": "6.2.6",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz",
- "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==",
+ "version": "6.3.4",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz",
+ "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
"postcss": "^8.5.3",
- "rollup": "^4.30.1"
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
},
"bin": {
"vite": "bin/vite.js"
@@ -13035,9 +13028,9 @@
"peer": true
},
"node_modules/vite/node_modules/@esbuild/aix-ppc64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
- "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz",
+ "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==",
"cpu": [
"ppc64"
],
@@ -13052,9 +13045,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/android-arm": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz",
- "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz",
+ "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==",
"cpu": [
"arm"
],
@@ -13069,9 +13062,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/android-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz",
- "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz",
+ "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==",
"cpu": [
"arm64"
],
@@ -13086,9 +13079,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/android-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz",
- "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz",
+ "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==",
"cpu": [
"x64"
],
@@ -13103,9 +13096,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz",
- "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz",
+ "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==",
"cpu": [
"arm64"
],
@@ -13120,9 +13113,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/darwin-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz",
- "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz",
+ "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==",
"cpu": [
"x64"
],
@@ -13137,9 +13130,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz",
- "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz",
+ "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==",
"cpu": [
"arm64"
],
@@ -13154,9 +13147,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/freebsd-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz",
- "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz",
+ "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==",
"cpu": [
"x64"
],
@@ -13171,9 +13164,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/linux-arm": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz",
- "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz",
+ "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==",
"cpu": [
"arm"
],
@@ -13188,9 +13181,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/linux-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz",
- "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz",
+ "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==",
"cpu": [
"arm64"
],
@@ -13205,9 +13198,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/linux-ia32": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz",
- "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz",
+ "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==",
"cpu": [
"ia32"
],
@@ -13222,9 +13215,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/linux-loong64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz",
- "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz",
+ "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==",
"cpu": [
"loong64"
],
@@ -13239,9 +13232,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/linux-mips64el": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz",
- "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz",
+ "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==",
"cpu": [
"mips64el"
],
@@ -13256,9 +13249,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/linux-ppc64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz",
- "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz",
+ "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==",
"cpu": [
"ppc64"
],
@@ -13273,9 +13266,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/linux-riscv64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz",
- "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz",
+ "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==",
"cpu": [
"riscv64"
],
@@ -13290,9 +13283,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/linux-s390x": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz",
- "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz",
+ "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==",
"cpu": [
"s390x"
],
@@ -13307,9 +13300,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/linux-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz",
- "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz",
+ "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==",
"cpu": [
"x64"
],
@@ -13324,9 +13317,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/netbsd-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz",
- "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz",
+ "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==",
"cpu": [
"x64"
],
@@ -13341,9 +13334,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/openbsd-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz",
- "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz",
+ "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==",
"cpu": [
"x64"
],
@@ -13358,9 +13351,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/sunos-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz",
- "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz",
+ "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==",
"cpu": [
"x64"
],
@@ -13375,9 +13368,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/win32-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz",
- "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz",
+ "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==",
"cpu": [
"arm64"
],
@@ -13392,9 +13385,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/win32-ia32": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz",
- "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz",
+ "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==",
"cpu": [
"ia32"
],
@@ -13409,9 +13402,9 @@
}
},
"node_modules/vite/node_modules/@esbuild/win32-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz",
- "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz",
+ "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==",
"cpu": [
"x64"
],
@@ -13426,9 +13419,9 @@
}
},
"node_modules/vite/node_modules/esbuild": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
- "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
+ "version": "0.25.3",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz",
+ "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -13439,31 +13432,59 @@
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.2",
- "@esbuild/android-arm": "0.25.2",
- "@esbuild/android-arm64": "0.25.2",
- "@esbuild/android-x64": "0.25.2",
- "@esbuild/darwin-arm64": "0.25.2",
- "@esbuild/darwin-x64": "0.25.2",
- "@esbuild/freebsd-arm64": "0.25.2",
- "@esbuild/freebsd-x64": "0.25.2",
- "@esbuild/linux-arm": "0.25.2",
- "@esbuild/linux-arm64": "0.25.2",
- "@esbuild/linux-ia32": "0.25.2",
- "@esbuild/linux-loong64": "0.25.2",
- "@esbuild/linux-mips64el": "0.25.2",
- "@esbuild/linux-ppc64": "0.25.2",
- "@esbuild/linux-riscv64": "0.25.2",
- "@esbuild/linux-s390x": "0.25.2",
- "@esbuild/linux-x64": "0.25.2",
- "@esbuild/netbsd-arm64": "0.25.2",
- "@esbuild/netbsd-x64": "0.25.2",
- "@esbuild/openbsd-arm64": "0.25.2",
- "@esbuild/openbsd-x64": "0.25.2",
- "@esbuild/sunos-x64": "0.25.2",
- "@esbuild/win32-arm64": "0.25.2",
- "@esbuild/win32-ia32": "0.25.2",
- "@esbuild/win32-x64": "0.25.2"
+ "@esbuild/aix-ppc64": "0.25.3",
+ "@esbuild/android-arm": "0.25.3",
+ "@esbuild/android-arm64": "0.25.3",
+ "@esbuild/android-x64": "0.25.3",
+ "@esbuild/darwin-arm64": "0.25.3",
+ "@esbuild/darwin-x64": "0.25.3",
+ "@esbuild/freebsd-arm64": "0.25.3",
+ "@esbuild/freebsd-x64": "0.25.3",
+ "@esbuild/linux-arm": "0.25.3",
+ "@esbuild/linux-arm64": "0.25.3",
+ "@esbuild/linux-ia32": "0.25.3",
+ "@esbuild/linux-loong64": "0.25.3",
+ "@esbuild/linux-mips64el": "0.25.3",
+ "@esbuild/linux-ppc64": "0.25.3",
+ "@esbuild/linux-riscv64": "0.25.3",
+ "@esbuild/linux-s390x": "0.25.3",
+ "@esbuild/linux-x64": "0.25.3",
+ "@esbuild/netbsd-arm64": "0.25.3",
+ "@esbuild/netbsd-x64": "0.25.3",
+ "@esbuild/openbsd-arm64": "0.25.3",
+ "@esbuild/openbsd-x64": "0.25.3",
+ "@esbuild/sunos-x64": "0.25.3",
+ "@esbuild/win32-arm64": "0.25.3",
+ "@esbuild/win32-ia32": "0.25.3",
+ "@esbuild/win32-x64": "0.25.3"
+ }
+ },
+ "node_modules/vite/node_modules/fdir": {
+ "version": "6.4.4",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
+ "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/picomatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/vitefu": {
@@ -13502,9 +13523,9 @@
"license": "BSD-2-Clause"
},
"node_modules/webrtc-adapter": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.1.tgz",
- "integrity": "sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==",
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.3.tgz",
+ "integrity": "sha512-5fALBcroIl31OeXAdd1YUntxiZl1eHlZZWzNg3U4Fn+J9/cGL3eT80YlrsWGvj2ojuz1rZr2OXkgCzIxAZ7vRQ==",
"license": "BSD-3-Clause",
"dependencies": {
"sdp": "^3.2.0"
diff --git a/materialious/package.json b/materialious/package.json
index 2b938511..bcbd065a 100644
--- a/materialious/package.json
+++ b/materialious/package.json
@@ -1,6 +1,6 @@
{
"name": "materialious",
- "version": "1.7.22",
+ "version": "1.8.0",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -26,7 +26,6 @@
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.18.0",
"@vite-pwa/sveltekit": "^0.6.6",
- "buffer": "^6.0.3",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.45.1",
@@ -37,7 +36,7 @@
"svelte-check": "^4.0.0",
"tslib": "^2.7.0",
"typescript": "^5.6.3",
- "vite": "^6.2.6"
+ "vite": "^6.3.4"
},
"type": "module",
"dependencies": {
@@ -55,7 +54,7 @@
"bgutils-js": "^3.2.0",
"capacitor-nodejs": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz",
"fuse.js": "^7.0.0",
- "googlevideo": "^2.0.0",
+ "googlevideo": "^3.0.0",
"he": "^1.2.0",
"human-number": "^2.0.4",
"iso-3166": "^4.3.0",
@@ -72,4 +71,4 @@
"terser": "^5.34.1",
"youtubei.js": "^13.4.0"
}
-}
\ No newline at end of file
+}
diff --git a/materialious/src/lib/android/http/androidRequests.ts b/materialious/src/lib/android/http/androidRequests.ts
index ff355a23..a3fe52ed 100644
--- a/materialious/src/lib/android/http/androidRequests.ts
+++ b/materialious/src/lib/android/http/androidRequests.ts
@@ -1,52 +1,56 @@
-import { goto } from "$app/navigation";
-import { Capacitor } from "@capacitor/core";
+import { goto } from '$app/navigation';
+import { Capacitor } from '@capacitor/core';
+
+const originalFetch = window.fetch;
+const corsProxyUrl: string = 'http://localhost:3000/';
+
+function needsProxying(target: string): boolean {
+ if (!target.startsWith('http')) return false;
+ return true;
+}
+
+export const androidFetch = async (
+ requestInput: string | URL | Request,
+ requestOptions?: RequestInit
+): Promise => {
+ const uri = requestInput instanceof Request ? requestInput.url : requestInput.toString();
+
+ if (needsProxying(uri)) {
+ if (requestInput instanceof Request) {
+ requestInput = new Request(corsProxyUrl + uri, {
+ method: requestInput.method,
+ headers: requestInput.headers,
+ body: requestInput.body,
+ mode: requestInput.mode,
+ credentials: requestInput.credentials,
+ cache: requestInput.cache,
+ redirect: requestInput.redirect,
+ referrer: requestInput.referrer,
+ integrity: requestInput.integrity,
+ keepalive: requestInput.keepalive,
+ ...(requestInput.body ? { duplex: 'half' } : {})
+ });
+ } else {
+ requestInput = corsProxyUrl + uri;
+ }
+ }
+
+ // Use the original fetch with the proxied URL and options
+ return originalFetch(requestInput, requestOptions);
+};
if (Capacitor.getPlatform() === 'android') {
- const originalFetch = window.fetch;
+ window.fetch = androidFetch;
- function needsProxying(target: string): boolean {
- if (!target.startsWith('http')) return false;
- return true;
- }
+ const originalXhrOpen = XMLHttpRequest.prototype.open;
- const corsProxyUrl: string = 'http://localhost:3000/';
+ XMLHttpRequest.prototype.open = function (...args: any[]): void {
+ if (needsProxying(args[1])) {
+ args[1] = corsProxyUrl + args[1];
+ }
+ /* @ts-ignore */
+ return originalXhrOpen.apply(this, args);
+ };
- window.fetch = async (requestInput: string | URL | Request, requestOptions?: RequestInit): Promise => {
- const uri = requestInput instanceof Request ? requestInput.url : requestInput.toString();
-
- if (needsProxying(uri)) {
- if (requestInput instanceof Request) {
- requestInput = new Request(corsProxyUrl + uri, {
- method: requestInput.method,
- headers: requestInput.headers,
- body: requestInput.body,
- mode: requestInput.mode,
- credentials: requestInput.credentials,
- cache: requestInput.cache,
- redirect: requestInput.redirect,
- referrer: requestInput.referrer,
- integrity: requestInput.integrity,
- keepalive: requestInput.keepalive,
- ...(requestInput.body ? { duplex: "half" } : {})
- });
- } else {
- requestInput = corsProxyUrl + uri;
- }
- }
-
- // Use the original fetch with the proxied URL and options
- return originalFetch(requestInput, requestOptions);
- };
-
- const originalXhrOpen = XMLHttpRequest.prototype.open;
-
- XMLHttpRequest.prototype.open = function (...args: any[]): void {
- if (needsProxying(args[1])) {
- args[1] = corsProxyUrl + args[1];
- }
- /* @ts-ignore */
- return originalXhrOpen.apply(this, args);
- };
-
- setTimeout(() => goto('/', { replaceState: true }), 100);
+ setTimeout(() => goto('/', { replaceState: true }), 100);
}
diff --git a/materialious/src/lib/api/model.ts b/materialious/src/lib/api/model.ts
index 90f41105..f28267e9 100644
--- a/materialious/src/lib/api/model.ts
+++ b/materialious/src/lib/api/model.ts
@@ -1,5 +1,4 @@
-import type { YT } from 'youtubei.js';
-
+import type { ApiResponse, Innertube, YT } from 'youtubei.js';
export interface Image {
url: string;
@@ -101,7 +100,12 @@ export interface VideoPlay extends Video {
authorThumbnails: Image[];
captions: Captions[];
storyboards?: StoryBoard[];
- ytJsVideoInfo?: YT.VideoInfo,
+ ytjs?: {
+ innertube: Innertube;
+ video: YT.VideoInfo;
+ clientPlaybackNonce: string;
+ rawApiResponse: ApiResponse;
+ };
fallbackPatch?: 'youtubejs' | 'piped';
}
diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte
index 3db43052..fca7f24f 100644
--- a/materialious/src/lib/components/Player.svelte
+++ b/materialious/src/lib/components/Player.svelte
@@ -9,7 +9,8 @@
import { StatusBar, Style } from '@capacitor/status-bar';
import { NavigationBar } from '@hugotomazi/capacitor-navigation-bar';
import { type Page } from '@sveltejs/kit';
- import { GoogleVideo, Protos } from 'googlevideo';
+ import { base64ToU8, Protos } from 'googlevideo';
+ import ui from 'beercss';
import ISO6391 from 'iso-639-1';
import Mousetrap from 'mousetrap';
import 'shaka-player/dist/controls.css';
@@ -37,6 +38,14 @@
synciousStore
} from '../store';
import { getDynamicTheme, setStatusBarColor } from '../theme';
+ import { HttpFetchPlugin, type SabrStreamingContext } from '$lib/sabr/shakaHttpPlugin';
+ import { Constants, type Misc } from 'youtubei.js';
+ import {
+ fromFormat,
+ fromFormatInitializationMetadata,
+ fromMediaHeader,
+ getUniqueFormatId
+ } from '$lib/sabr/formatKeyUtils';
interface Props {
data: { video: VideoPlay; content: PhasedDescription; playlistId: string | null };
@@ -61,6 +70,50 @@
let player: shaka.Player;
let shakaUi: shaka.ui.Overlay;
+ // Shaka player required variables for SABR based off LuanRT's example
+ const sessionId = Array.from(Array(16), () => Math.floor(Math.random() * 36).toString(36)).join(
+ ''
+ );
+ let isLive = false;
+ let isPostLiveDVR = false;
+ let videoPlaybackUstreamerConfig: string | undefined;
+ let serverAbrStreamingUrl: URL | undefined = undefined;
+ let drmParams: string | undefined;
+ let lastRequestMs = 0;
+ let lastSeekMs = 0;
+ let lastManualFormatSelectionMs = 0;
+ let lastActionMs = 0;
+ let playerElementResizeObserver: ResizeObserver | undefined;
+ let clientViewportHeight = playerElement?.clientHeight || 0;
+ let clientViewportWidth = playerElement?.clientWidth || 0;
+ let lastPlaybackCookie: Protos.PlaybackCookie | undefined;
+ const initializedFormats = new Map<
+ string,
+ {
+ lastSegmentMetadata?: {
+ formatId: Protos.FormatId;
+ startTimeMs: number;
+ startSequenceNumber: number;
+ endSequenceNumber: number;
+ durationMs: number;
+ };
+ formatInitializationMetadata: Protos.FormatInitializationMetadata;
+ }
+ >();
+ let formatList: Misc.Format[] = [];
+
+ shaka.net.NetworkingEngine.registerScheme(
+ 'http',
+ HttpFetchPlugin.parse,
+ shaka.net.NetworkingEngine.PluginPriority.PREFERRED
+ );
+ shaka.net.NetworkingEngine.registerScheme(
+ 'https',
+ HttpFetchPlugin.parse,
+ shaka.net.NetworkingEngine.PluginPriority.PREFERRED
+ );
+ // Shaka player SABR var end
+
const STORAGE_KEY_QUALITY = 'shaka-preferred-quality';
const STORAGE_KEY_VOLUME = 'shaka-preferred-volume';
@@ -122,9 +175,20 @@
return;
}
+ HttpFetchPlugin.cacheManager.clearCache();
+
player = new shaka.Player();
playerElement = document.getElementById('player') as HTMLMediaElement;
+ playerElementResizeObserver = new ResizeObserver(() => {
+ if (playerElement) {
+ clientViewportHeight = playerElement.clientHeight;
+ clientViewportWidth = playerElement.clientWidth;
+ }
+ });
+
+ playerElementResizeObserver.observe(playerElement);
+
// Change instantly to stop video from being loud for a second
const savedVolume = localStorage.getItem(STORAGE_KEY_VOLUME);
if (savedVolume) {
@@ -166,35 +230,70 @@
});
player.configure({
- streaming: {
- bufferingGoal: data.video.ytJsVideoInfo
- ? (data.video.ytJsVideoInfo.page[0].player_config?.media_common_config
- .dynamic_readahead_config.max_read_ahead_media_time_ms || 0) / 1000
- : 180,
- rebufferingGoal: data.video.ytJsVideoInfo
- ? (data.video.ytJsVideoInfo.page[0].player_config?.media_common_config
- .dynamic_readahead_config.read_ahead_growth_rate_ms || 0) / 1000
- : 0.02,
- bufferBehind: 300,
- autoLowLatencyMode: true
- },
-
abr: {
- enabled: true,
- restrictToElementSize: true,
- restrictions: {
- maxBandwidth: data.video.ytJsVideoInfo
- ? Number(
- data.video.ytJsVideoInfo.page[0].player_config?.stream_selection_config.max_bitrate
- )
- : null
- }
+ enabled: true
},
- preferredDecodingAttributes: !data.video.hlsUrl ? ['smooth', 'powerEfficient'] : [],
- autoShowText: shaka.config.AutoShowText.NEVER
+ streaming: {
+ bufferingGoal: 120,
+ rebufferingGoal: 0.01,
+ bufferBehind: 300,
+ retryParameters: {
+ maxAttempts: 30,
+ baseDelay: 1500,
+ backoffFactor: 2.5,
+ fuzzFactor: 0.7,
+ timeout: 120000
+ },
+ stallThreshold: 2,
+ stallSkip: 0.5
+ }
});
- if (data.video.fallbackPatch === 'youtubejs') {
+ if (data.video.fallbackPatch === 'youtubejs' && data.video.ytjs) {
+ isLive = !!data.video.ytjs.video.basic_info.is_live;
+ isPostLiveDVR = !!data.video.ytjs.video.basic_info.is_post_live_dvr;
+
+ if (
+ data.video.ytjs.rawApiResponse.data.streamingData &&
+ (data.video.ytjs.rawApiResponse.data.streamingData as any).drmParams
+ ) {
+ drmParams = (data.video.ytjs.rawApiResponse.data.streamingData as any).drmParams;
+ player.configure({
+ drm: {
+ servers: {
+ 'com.widevine.alpha':
+ 'https://www.youtube.com/youtubei/v1/player/get_drm_license?alt=json'
+ }
+ }
+ });
+ }
+
+ videoPlaybackUstreamerConfig =
+ data.video.ytjs.video.page[0].player_config?.media_common_config
+ .media_ustreamer_request_config?.video_playback_ustreamer_config;
+
+ if (data.video.ytjs.video.streaming_data) {
+ formatList = data.video.ytjs.video.streaming_data.adaptive_formats;
+ }
+
+ if (data.video.ytjs.video.streaming_data?.server_abr_streaming_url)
+ serverAbrStreamingUrl = new URL(
+ data.video.ytjs.innertube.session.player!.decipher(
+ data.video.ytjs.video.streaming_data.server_abr_streaming_url
+ )
+ );
+
+ playerElement.addEventListener('seeked', () => (lastSeekMs = Date.now()));
+
+ player.addEventListener('variantchanged', (event) => {
+ // Technically, all variant changes here are manual, given we don't handle ABR updates from the server.
+ if (event.type !== 'variant') {
+ lastManualFormatSelectionMs = Date.now();
+ }
+
+ lastActionMs = Date.now();
+ });
+
const networkingEngine = player.getNetworkingEngine();
if (!networkingEngine) return;
@@ -202,125 +301,322 @@
// Based off the following
// https://github.com/FreeTubeApp/FreeTube/blob/d270c9e251a433f1e4246a3f6a37acef707d22aa/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js#L1206
// https://github.com/LuanRT/BgUtils/blob/6b121166be1ccb0b952dee1bdac488808365ae6b/examples/browser/web/src/main.ts#L293
+ // https://github.com/LuanRT/yt-sabr-shaka-demo/blob/main/src/components/VideoPlayer.vue
- networkingEngine.registerRequestFilter(async (type, request) => {
- if (type === shaka.net.NetworkingEngine.RequestType.SEGMENT) {
- const url = new URL(request.uris[0]);
+ networkingEngine.registerRequestFilter(async (type, request, context) => {
+ if (!player) return;
- if (url.hostname.endsWith('.googlevideo.com') && url.pathname === '/videoplayback') {
- if (request.headers.Range) {
- url.searchParams.set('range', request.headers.Range.split('=')[1]);
- url.searchParams.set('ump', '1');
- url.searchParams.set('srfvp', '1');
+ const originalUrl = new URL(request.uris[0]);
+ let url = originalUrl.hostname === 'sabr' ? serverAbrStreamingUrl : originalUrl;
+ const headers = request.headers;
- const cachedPoToken = get(poTokenCacheStore);
- if (cachedPoToken) url.searchParams.set('pot', cachedPoToken);
+ if (!url) return;
- delete request.headers.Range;
- }
+ if (
+ type === shaka.net.NetworkingEngine.RequestType.SEGMENT &&
+ url.pathname.includes('videoplayback')
+ ) {
+ const isUmp = url.searchParams.get('ump') === '1';
+ const isSabr = url.searchParams.get('sabr') === '1';
- request.method = 'POST';
- request.body = new Uint8Array([120, 0]);
- }
-
- request.uris[0] = url.toString();
- }
- });
-
- networkingEngine.registerResponseFilter(async (type, response) => {
- let mediaData = new Uint8Array(0);
-
- const handleRedirect = async (redirectData: Protos.SabrRedirect) => {
- const redirectRequest = shaka.net.NetworkingEngine.makeRequest(
- [redirectData.url!],
- player!.getConfiguration().streaming.retryParameters
- );
- const requestOperation = player!.getNetworkingEngine()!.request(type, redirectRequest);
- const redirectResponse = await requestOperation.promise;
-
- response.data = redirectResponse.data;
- response.headers = redirectResponse.headers;
- response.uri = redirectResponse.uri;
- };
-
- const handleMediaData = async (data: Uint8Array) => {
- const combinedLength = mediaData.length + data.length;
- const tempMediaData = new Uint8Array(combinedLength);
-
- tempMediaData.set(mediaData);
- tempMediaData.set(data, mediaData.length);
-
- mediaData = tempMediaData;
- };
-
- if (type == shaka.net.NetworkingEngine.RequestType.SEGMENT) {
- const url = new URL(response.uri);
-
- // Fix positioning for auto-generated subtitles
- if (
- url.hostname.endsWith('.youtube.com') &&
- url.pathname === '/api/timedtext' &&
- url.searchParams.get('caps') === 'asr' &&
- url.searchParams.get('kind') === 'asr' &&
- url.searchParams.get('fmt') === 'vtt'
- ) {
- const stringBody = new TextDecoder().decode(response.data);
- // position:0% for LTR text and position:100% for RTL text
- const cleaned = stringBody.replaceAll(/ align:start position:(?:10)?0%$/gm, '');
-
- response.data = new TextEncoder().encode(cleaned).buffer as ArrayBuffer;
- } else {
- const googUmp = new GoogleVideo.UMP(
- new GoogleVideo.ChunkedDataBuffer([new Uint8Array(response.data as ArrayBuffer)])
+ if (isSabr) {
+ const currentFormat = formatList.find(
+ (format) =>
+ fromFormat(format) === (new URL(request.uris[0]).searchParams.get('___key') || '')
);
- let redirect: Protos.SabrRedirect | undefined;
+ if (!videoPlaybackUstreamerConfig) throw new Error('Ustreamer config not found.');
- googUmp.parse((part) => {
- try {
- const data = part.data.chunks[0];
- switch (part.type) {
- case 20: {
- const mediaHeader = Protos.MediaHeader.decode(data);
- console.info('[MediaHeader]:', mediaHeader);
- break;
- }
- case 21: {
- handleMediaData(part.data.split(1).remainingBuffer.chunks[0]);
- break;
- }
- case 43: {
- redirect = Protos.SabrRedirect.decode(data);
- console.info('[SABRRedirect]:', redirect);
- break;
- }
- case 58: {
- const streamProtectionStatus = Protos.StreamProtectionStatus.decode(data);
- switch (streamProtectionStatus.status) {
- case 1:
- console.info('[StreamProtectionStatus]: Ok');
- break;
- case 2:
- console.error('[StreamProtectionStatus]: Attestation pending');
- break;
- case 3:
- console.error('[StreamProtectionStatus]: Attestation required');
- break;
- default:
- break;
- }
- break;
- }
+ if (!currentFormat) throw new Error('No format found for SABR request.');
+
+ if (!playerElement) throw new Error('No video element found.');
+
+ const activeVariant = player
+ .getVariantTracks()
+ .find(
+ (track) =>
+ getUniqueFormatId(currentFormat) ===
+ (currentFormat.has_video ? track.originalVideoId : track.originalAudioId)
+ );
+
+ let videoFormat: Misc.Format | undefined;
+ let audioFormat: Misc.Format | undefined;
+ let videoFormatId: Protos.FormatId | undefined;
+ let audioFormatId: Protos.FormatId | undefined;
+
+ if (activeVariant) {
+ for (const fmt of formatList) {
+ const uniqueFormatId = getUniqueFormatId(fmt);
+ if (uniqueFormatId === activeVariant.originalVideoId) {
+ videoFormat = fmt;
+ } else if (uniqueFormatId === activeVariant.originalAudioId) {
+ audioFormat = fmt;
}
- } catch (error) {
- console.error('An error occurred while processing the part:', error);
}
- });
+ }
- if (redirect) return handleRedirect(redirect);
+ if (videoFormat) {
+ videoFormatId = {
+ itag: videoFormat.itag,
+ lastModified: parseInt(videoFormat.last_modified_ms),
+ xtags: videoFormat.xtags
+ };
+ }
- if (mediaData.length) response.data = mediaData;
+ if (audioFormat) {
+ audioFormatId = {
+ itag: audioFormat.itag,
+ lastModified: parseInt(audioFormat.last_modified_ms),
+ xtags: audioFormat.xtags
+ };
+ }
+
+ const isInit = context ? !context.segment : true;
+
+ const videoPlaybackAbrRequest: Protos.VideoPlaybackAbrRequest = {
+ clientAbrState: {
+ playbackRate: player.getPlaybackRate(),
+ playerTimeMs: Math.round(
+ (context?.segment?.getStartTime() ?? playerElement.currentTime) * 1000
+ ),
+ elapsedWallTimeMs: Date.now() - lastRequestMs,
+ timeSinceLastSeek: lastSeekMs === 0 ? 0 : Date.now() - lastSeekMs,
+ timeSinceLastActionMs: lastActionMs === 0 ? 0 : Date.now() - lastActionMs,
+ timeSinceLastManualFormatSelectionMs:
+ lastManualFormatSelectionMs === 0 ? 0 : Date.now() - lastManualFormatSelectionMs,
+ clientViewportIsFlexible: false,
+ bandwidthEstimate: Math.round(player.getStats().estimatedBandwidth),
+ drcEnabled: currentFormat.is_drc === true,
+ enabledTrackTypesBitfield: currentFormat.has_audio ? 1 : 2,
+ clientViewportHeight,
+ clientViewportWidth
+ },
+ bufferedRanges: [],
+ selectedFormatIds: [],
+ selectedAudioFormatIds: [audioFormatId || {}],
+ selectedVideoFormatIds: [videoFormatId || {}],
+ videoPlaybackUstreamerConfig: base64ToU8(videoPlaybackUstreamerConfig),
+ streamerContext: {
+ poToken: base64ToU8(get(poTokenCacheStore) ?? ''),
+ playbackCookie: lastPlaybackCookie
+ ? Protos.PlaybackCookie.encode(lastPlaybackCookie).finish()
+ : undefined,
+ clientInfo: {
+ clientName: parseInt(Constants.CLIENT_NAME_IDS.WEB),
+ clientVersion: data.video.ytjs?.innertube.session.context.client.clientVersion,
+ osName: 'Windows',
+ osVersion: '10.0'
+ },
+ field5: [],
+ field6: []
+ },
+ field1000: []
+ };
+
+ // Normalize the resolution.
+ if (currentFormat.width && currentFormat.height) {
+ let resolution = currentFormat.height;
+
+ const aspectRatio = currentFormat.height / currentFormat.width;
+
+ if (aspectRatio > 16 / 9) {
+ resolution = Math.round((currentFormat.width * 9) / 16);
+ }
+
+ if (resolution && videoPlaybackAbrRequest.clientAbrState) {
+ videoPlaybackAbrRequest.clientAbrState.stickyResolution = resolution;
+ videoPlaybackAbrRequest.clientAbrState.lastManualSelectedResolution = resolution;
+ }
+ }
+
+ if (!isInit) {
+ // Add the currently/previously active formats to the list of buffered ranges and selected formats
+ // so that the server doesn't send its init data again.
+ const initializedFormatsArray = Array.from(initializedFormats.values());
+
+ for (const initializedFormat of initializedFormatsArray) {
+ if (initializedFormat.lastSegmentMetadata) {
+ videoPlaybackAbrRequest.bufferedRanges.push({
+ formatId: initializedFormat.lastSegmentMetadata.formatId,
+ startSegmentIndex: initializedFormat.lastSegmentMetadata.startSequenceNumber,
+ durationMs: initializedFormat.lastSegmentMetadata.durationMs,
+ startTimeMs: 0,
+ endSegmentIndex: initializedFormat.lastSegmentMetadata.endSequenceNumber
+ });
+ }
+ }
+
+ if (audioFormatId) videoPlaybackAbrRequest.selectedFormatIds.push(audioFormatId);
+
+ if (videoFormatId) videoPlaybackAbrRequest.selectedFormatIds.push(videoFormatId);
+ }
+
+ console.log(videoPlaybackAbrRequest);
+
+ request.body = Protos.VideoPlaybackAbrRequest.encode(videoPlaybackAbrRequest).finish();
+
+ const byteRange = headers.Range
+ ? {
+ start: Number(headers.Range.split('=')[1].split('-')[0]),
+ end: Number(headers.Range.split('=')[1].split('-')[1])
+ }
+ : null;
+
+ const sabrStreamingContext = {
+ byteRange,
+ format: currentFormat,
+ isInit,
+ isUMP: true,
+ isSABR: true,
+ playerTimeMs: videoPlaybackAbrRequest.clientAbrState?.playerTimeMs
+ };
+
+ // @NOTE: Not a real header. See the http plugin code for more info.
+ request.headers['X-Streaming-Context'] = btoa(JSON.stringify(sabrStreamingContext));
+ delete headers.Range;
+ } else if (isUmp) {
+ if (!isLive && !isPostLiveDVR) {
+ url.searchParams.set('ump', '1');
+ url.searchParams.set('srfvp', '1');
+ if (headers.Range) {
+ url.searchParams.set('range', headers.Range?.split('=')[1]);
+ delete headers.Range;
+ }
+ } else {
+ url.pathname += '/ump/1';
+ url.pathname += '/srfvp/1';
+ }
+
+ request.headers['X-Streaming-Context'] = btoa(
+ JSON.stringify({
+ isUMP: true,
+ isSABR: false
+ })
+ );
}
+
+ request.method = 'POST';
+
+ if (!isSabr) {
+ if (request.method === 'POST') {
+ request.body = new Uint8Array([120, 0]);
+ }
+
+ const poToken = get(poTokenCacheStore);
+
+ if (poToken) {
+ // Set Proof of Origin Token
+ if (isLive || isPostLiveDVR) {
+ url.pathname += '/pot/' + poToken;
+ } else {
+ url.searchParams.set('pot', poToken);
+ }
+ }
+ }
+ } else if (type == shaka.net.NetworkingEngine.RequestType.LICENSE) {
+ const wrapped = {} as Record;
+ wrapped.context = data.video.ytjs?.innertube.session.context;
+ wrapped.cpn = data.video.ytjs?.clientPlaybackNonce;
+ wrapped.drmParams = decodeURIComponent(drmParams || '');
+ wrapped.drmSystem = 'DRM_SYSTEM_WIDEVINE';
+ wrapped.drmVideoFeature = 'DRM_VIDEO_FEATURE_SDR';
+ wrapped.licenseRequest = shaka.util.Uint8ArrayUtils.toBase64(
+ request.body as ArrayBuffer | ArrayBufferView
+ );
+ wrapped.sessionId = sessionId;
+ wrapped.videoId = data.video.videoId;
+ request.body = shaka.util.StringUtils.toUTF8(JSON.stringify(wrapped));
+ }
+
+ request.uris[0] = url.toString();
+ });
+
+ networkingEngine.registerResponseFilter(async (type, response, context) => {
+ if (type === shaka.net.NetworkingEngine.RequestType.SEGMENT) {
+ const sabrStreamingContext = response.headers['X-Streaming-Context'];
+
+ if (sabrStreamingContext) {
+ const { streamInfo, isSABR, format, byteRange } = JSON.parse(
+ atob(sabrStreamingContext)
+ ) as SabrStreamingContext;
+
+ if (streamInfo) {
+ const sabrRedirect = streamInfo.redirect;
+ const playbackCookie = streamInfo.playbackCookie;
+ const streamProtectionStatus = streamInfo.streamProtectionStatus;
+ const formatInitMetadata = streamInfo.formatInitMetadata || [];
+ const mainSegmentMediaHeader = streamInfo.mediaHeader;
+
+ // If we have a redirect, follow it.
+ if (sabrRedirect?.url && !response.data.byteLength) {
+ let redirectUrl = new URL(sabrRedirect.url);
+
+ // For SABR, create a fake URL so we can identify it in the request filter.
+ if (isSABR) {
+ serverAbrStreamingUrl = redirectUrl;
+ redirectUrl = new URL(`https://sabr?___key=${fromFormat(format) || ''}`);
+ }
+
+ const retryParameters = player!.getConfiguration().streaming.retryParameters;
+
+ const redirectRequest = shaka.net.NetworkingEngine.makeRequest(
+ [redirectUrl.toString()],
+ retryParameters
+ );
+
+ // Keep range so we can slice the response (only if it's the init segment).
+ if (isSABR && byteRange) {
+ redirectRequest.headers['Range'] = `bytes=${byteRange.start}-${byteRange.end}`;
+ }
+
+ const requestOperation = player!
+ .getNetworkingEngine()
+ ?.request(type, redirectRequest, context);
+ const redirectResponse = await requestOperation!.promise;
+
+ // Modify the original response to contain the results of the redirect
+ // response.
+ Object.assign(response, redirectResponse);
+ return;
+ }
+
+ if (playbackCookie) lastPlaybackCookie = streamInfo.playbackCookie;
+
+ if (streamProtectionStatus && streamProtectionStatus.status === 3) {
+ console.warn('[UMP] Attestation required.');
+ }
+
+ for (const metadata of formatInitMetadata) {
+ const key = fromFormatInitializationMetadata(metadata);
+ if (!initializedFormats.has(key)) {
+ initializedFormats.set(key, {
+ formatInitializationMetadata: metadata
+ });
+ console.log(`[SABR] Initialized format ${key}`);
+ }
+ }
+
+ if (mainSegmentMediaHeader) {
+ const formatKey = fromMediaHeader(mainSegmentMediaHeader);
+ const initializedFormat = initializedFormats.get(formatKey);
+
+ if (initializedFormat) {
+ initializedFormat.lastSegmentMetadata = {
+ formatId: mainSegmentMediaHeader.formatId!,
+ startTimeMs: mainSegmentMediaHeader.startMs || 0,
+ startSequenceNumber: mainSegmentMediaHeader.sequenceNumber || 1,
+ endSequenceNumber: mainSegmentMediaHeader.sequenceNumber || 1,
+ durationMs: mainSegmentMediaHeader.durationMs || 0
+ };
+ }
+ }
+ }
+ }
+ } else if (type == shaka.net.NetworkingEngine.RequestType.LICENSE) {
+ const wrappedString = shaka.util.StringUtils.fromUTF8(response.data);
+ const wrapped = JSON.parse(wrappedString);
+ const rawLicenseBase64 = wrapped.license;
+ response.data = shaka.util.Uint8ArrayUtils.fromBase64(rawLicenseBase64);
}
});
}
@@ -460,7 +756,11 @@
});
}
} else {
- await player.load(data.video.hlsUrl + '?local=true');
+ if (data.video.fallbackPatch === 'youtubejs') {
+ await player.load(data.video.dashUrl);
+ } else {
+ await player.load(data.video.hlsUrl + '?local=true');
+ }
}
if (data.video.fallbackPatch === 'youtubejs') {
@@ -608,6 +908,12 @@
}
onDestroy(async () => {
+ HttpFetchPlugin.cacheManager.clearCache();
+
+ if (playerElementResizeObserver) {
+ playerElementResizeObserver.disconnect();
+ }
+
if (Capacitor.getPlatform() === 'android') {
if (originalOrigination) {
await StatusBar.setOverlaysWebView({ overlay: false });
@@ -623,7 +929,6 @@
try {
savePlayerPos();
} catch (error) {}
- await playerElement.pause();
await player.destroy();
await shakaUi.destroy();
playerPosSet = false;
diff --git a/materialious/src/lib/css/shaka-player-theme.css b/materialious/src/lib/css/shaka-player-theme.css
index 4188a110..e9195df6 100644
--- a/materialious/src/lib/css/shaka-player-theme.css
+++ b/materialious/src/lib/css/shaka-player-theme.css
@@ -115,14 +115,16 @@
.youtube-theme .shaka-overflow-menu,
.youtube-theme .shaka-settings-menu {
border-radius: 0.25rem;
- background-color: var(--surface-container-low);
+ background: var(--surface-container-low);
right: 0.625rem; /* 10px */
bottom: 3.125rem; /* 50px */
min-width: 12.5rem; /* 200px */
- transition:
- transform var(--speed3),
- border-radius var(--speed3),
- padding var(--speed3);
+ animation: none !important;
+ transition: none !important;
+ opacity: 1 !important;
+ visibility: visible !important;
+ z-index: 9999 !important;
+ pointer-events: auto !important;
}
.youtube-theme .shaka-settings-menu {
diff --git a/materialious/src/lib/patches/youtubejs.ts b/materialious/src/lib/patches/youtubejs.ts
index 1aa50611..baef9927 100644
--- a/materialious/src/lib/patches/youtubejs.ts
+++ b/materialious/src/lib/patches/youtubejs.ts
@@ -1,195 +1,231 @@
import { androidPoTokenMinter } from '$lib/android/youtube/minter';
-import type { AdaptiveFormats, Captions, Image, StoryBoard, Thumbnail, VideoBase, VideoPlay } from '$lib/api/model';
+import type {
+ AdaptiveFormats,
+ Captions,
+ Image,
+ StoryBoard,
+ Thumbnail,
+ VideoBase,
+ VideoPlay
+} from '$lib/api/model';
import { numberWithCommas } from '$lib/numbers';
+import { fromFormat } from '$lib/sabr/formatKeyUtils';
import { interfaceRegionStore, poTokenCacheStore } from '$lib/store';
import { Capacitor } from '@capacitor/core';
import { USER_AGENT } from 'bgutils-js';
-import { Buffer } from 'buffer';
import { get } from 'svelte/store';
-import { Innertube, UniversalCache, YT, YTNodes } from 'youtubei.js';
+import { Innertube, UniversalCache, Utils, YT, YTNodes } from 'youtubei.js';
export async function patchYoutubeJs(videoId: string): Promise {
- if (!Capacitor.isNativePlatform()) {
- throw new Error('Yt.js: Platform not supported');
- }
+ if (!Capacitor.isNativePlatform()) {
+ throw new Error('Platform not supported');
+ }
- const youtube = await Innertube.create({
- fetch: fetch,
- cache: new UniversalCache(false),
- location: get(interfaceRegionStore),
- user_agent: USER_AGENT,
- enable_session_cache: false
- });
+ const youtube = await Innertube.create({
+ fetch: fetch,
+ cache: new UniversalCache(false),
+ location: get(interfaceRegionStore),
+ user_agent: USER_AGENT,
+ enable_session_cache: false
+ });
- let sessionPoToken: string;
- let contentPoToken: string;
+ let sessionPoToken: string;
+ let contentPoToken: string;
- const requestKey = 'O43z0dpjhgX20SCx4KAo';
- const challengeResponse = await youtube.getAttestationChallenge('ENGAGEMENT_TYPE_UNBOUND');
+ const requestKey = 'O43z0dpjhgX20SCx4KAo';
+ const challengeResponse = await youtube.getAttestationChallenge('ENGAGEMENT_TYPE_UNBOUND');
- if (Capacitor.getPlatform() === 'android') {
- [sessionPoToken, contentPoToken] = await androidPoTokenMinter(
- challengeResponse,
- requestKey,
- youtube.session.context.client.visitorData ?? '',
- videoId
- );
- } else {
- [sessionPoToken, contentPoToken] = await window.electronAPI.generatePoToken(
- challengeResponse,
- requestKey,
- youtube.session.context.client.visitorData ?? '',
- videoId
- );
- }
+ if (Capacitor.getPlatform() === 'android') {
+ [sessionPoToken, contentPoToken] = await androidPoTokenMinter(
+ challengeResponse,
+ requestKey,
+ youtube.session.context.client.visitorData ?? '',
+ videoId
+ );
+ } else {
+ [sessionPoToken, contentPoToken] = await window.electronAPI.generatePoToken(
+ challengeResponse,
+ requestKey,
+ youtube.session.context.client.visitorData ?? '',
+ videoId
+ );
+ }
- poTokenCacheStore.set(sessionPoToken);
+ poTokenCacheStore.set(sessionPoToken);
- const extraArgs: Record = {
- playbackContext: {
- contentPlaybackContext: {
- vis: 0,
- splay: false,
- lactMilliseconds: '-1',
- signatureTimestamp: youtube.session.player?.sts
- }
- },
- serviceIntegrityDimensions: {
- poToken: contentPoToken
- },
- contentCheckOk: true,
- racyCheckOk: true
- };
+ const extraArgs: Record = {
+ playbackContext: {
+ contentPlaybackContext: {
+ vis: 0,
+ splay: false,
+ lactMilliseconds: '-1',
+ signatureTimestamp: youtube.session.player?.sts
+ }
+ },
+ contentCheckOk: true,
+ racyCheckOk: true
+ };
- const watchEndpoint = new YTNodes.NavigationEndpoint({ watchEndpoint: { videoId } });
- const rawPlayerResponse = await watchEndpoint.call(youtube.actions, extraArgs);
- const rawNextResponse = await watchEndpoint.call(youtube.actions, {
- override_endpoint: '/next',
- racyCheckOk: true,
- contentCheckOk: true
- });
+ const clientPlaybackNonce = Utils.generateRandomString(12);
- const video = new YT.VideoInfo([rawPlayerResponse, rawNextResponse], youtube!.actions, '');
+ const watchEndpoint = new YTNodes.NavigationEndpoint({ watchEndpoint: { videoId } });
+ const rawPlayerResponse = await watchEndpoint.call(youtube.actions, {
+ ...extraArgs,
+ parse: false
+ });
+ const rawNextResponse = await watchEndpoint.call(youtube.actions, {
+ override_endpoint: '/next',
+ racyCheckOk: true,
+ contentCheckOk: true
+ });
+ const video = new YT.VideoInfo(
+ [rawPlayerResponse, rawNextResponse],
+ youtube.actions,
+ clientPlaybackNonce
+ );
- console.log(video);
+ if (!video.primary_info || !video.secondary_info) {
+ throw new Error('Unable to pull video info from youtube.js');
+ }
- if (!video.primary_info || !video.secondary_info) {
- throw new Error('Yt.js: Unable to pull video info from youtube.js');
- }
+ let dashUri: string | undefined;
- let dashUri: string = '';
+ if (video.streaming_data) {
+ video.streaming_data.adaptive_formats.forEach((format) => {
+ const formatKey = fromFormat(format) || '';
+ format.url = `https://sabr?___key=${formatKey}`;
+ format.signature_cipher = undefined;
+ format.decipher = () => format.url || '';
+ });
- if (!video.basic_info.is_live) {
- const manifest = await video.toDash();
- dashUri = `data:application/dash+xml;charset=utf-8;base64,${Buffer.from(manifest).toString('base64')}`;
- }
+ if (video.basic_info.is_live) {
+ dashUri = video.streaming_data.dash_manifest_url
+ ? `${video.streaming_data.dash_manifest_url}/mpd_version/7`
+ : video.streaming_data.hls_manifest_url;
+ } else if (video.streaming_data.dash_manifest_url && video.basic_info.is_post_live_dvr) {
+ dashUri = video.streaming_data.hls_manifest_url
+ ? video.streaming_data.hls_manifest_url // HLS is preferred for DVR streams.
+ : `${video.streaming_data.dash_manifest_url}/mpd_version/7`;
+ } else {
+ dashUri = `data:application/dash+xml;base64,${btoa(await video.toDash(undefined, undefined, { captions_format: 'vtt' }))}`;
+ }
+ }
- const descString = video.secondary_info.description?.toString() || '';
+ if (!dashUri) throw Error('Unable to find suitable dash manifest');
- let authorThumbnails: Image[];
- if (video.basic_info.channel_id) {
- const channel = await youtube.getChannel(video.basic_info.channel_id);
- authorThumbnails = channel.metadata.avatar as Image[];
- } else {
- authorThumbnails = [];
- }
+ const descString = video.secondary_info.description?.toString() || '';
- let recommendedVideos: VideoBase[] = [];
- video.watch_next_feed?.forEach((recommended: Record) => {
- recommendedVideos.push({
- videoThumbnails: recommended?.thumbnails as Thumbnail[] || [],
- videoId: recommended?.id || '',
- title: recommended?.title?.toString() || '',
- viewCountText: recommended.view_count ? numberWithCommas(Number(recommended?.view_count.toString().replace(/\D/g, ''))) || '' : '',
- lengthSeconds: recommended?.duration?.seconds || 0,
- author: recommended?.author?.name || '',
- authorId: recommended?.author?.id || ''
- });
- });
+ let authorThumbnails: Image[];
+ if (video.basic_info.channel_id) {
+ const channel = await youtube.getChannel(video.basic_info.channel_id);
+ authorThumbnails = channel.metadata.avatar as Image[];
+ } else {
+ authorThumbnails = [];
+ }
- let captions: Captions[] = [];
- video.captions?.caption_tracks?.forEach(caption => {
- captions.push({
- label: caption.name?.toString() || '',
- language_code: caption.language_code,
- // Add correct format to url.
- url: caption.base_url + '&fmt=vtt'
- });
- });
+ const recommendedVideos: VideoBase[] = [];
+ video.watch_next_feed?.forEach((recommended: Record) => {
+ recommendedVideos.push({
+ videoThumbnails: (recommended?.thumbnails as Thumbnail[]) || [],
+ videoId: recommended?.id || '',
+ title: recommended?.title?.toString() || '',
+ viewCountText: recommended.view_count
+ ? numberWithCommas(Number(recommended?.view_count.toString().replace(/\D/g, ''))) || ''
+ : '',
+ lengthSeconds: recommended?.duration?.seconds || 0,
+ author: recommended?.author?.name || '',
+ authorId: recommended?.author?.id || ''
+ });
+ });
- let adaptiveFormats: AdaptiveFormats[] = [];
- video.streaming_data?.adaptive_formats.forEach(format => {
- adaptiveFormats.push({
- index: format.index_range?.start?.toString() || '',
- bitrate: format.bitrate?.toString() || '',
- init: format.init_range?.start?.toString() || '',
- url: format.url || '',
- itag: format.itag?.toString() || '',
- type: format.mime_type,
- clen: '',
- lmt: '',
- projectionType: 0,
- resolution: format.width ? `${format.width}x${format.height}` : undefined
- });
- });
+ const captions: Captions[] = [];
+ video.captions?.caption_tracks?.forEach((caption) => {
+ captions.push({
+ label: caption.name?.toString() || '',
+ language_code: caption.language_code,
+ // Add correct format to url.
+ url: caption.base_url + '&fmt=vtt'
+ });
+ });
- let storyboard: StoryBoard[] = [];
- if (video.storyboards && 'boards' in video.storyboards) {
- video.storyboards.boards.forEach(board => {
- storyboard.push({
- templateUrl: board.template_url,
- url: board.template_url,
- count: board.storyboard_count,
- height: board.thumbnail_height,
- width: board.thumbnail_width,
- interval: board.interval,
- storyboardCount: board.storyboard_count,
- storyboardHeight: board.thumbnail_height,
- storyboardWidth: board.thumbnail_width
- });
- });
- }
+ const adaptiveFormats: AdaptiveFormats[] = [];
+ video.streaming_data?.adaptive_formats.forEach((format) => {
+ adaptiveFormats.push({
+ index: format.index_range?.start?.toString() || '',
+ bitrate: format.bitrate?.toString() || '',
+ init: format.init_range?.start?.toString() || '',
+ url: format.url || '',
+ itag: format.itag?.toString() || '',
+ type: format.mime_type,
+ clen: '',
+ lmt: '',
+ projectionType: 0,
+ resolution: format.width ? `${format.width}x${format.height}` : undefined
+ });
+ });
- return {
- type: 'video',
- title: video.primary_info.title?.toString() || '',
- viewCount: Number(video.basic_info.view_count || 0),
- viewCountText: video.basic_info.view_count?.toString() || '0',
- likeCount: video.basic_info.like_count || 0,
- dislikeCount: 0,
- allowRatings: false,
- rating: 0,
- isListed: 0,
- isFamilyFriendly: video.basic_info.is_family_safe || true,
- genre: video.basic_info.category || '',
- genreUrl: '',
- dashUrl: dashUri,
- adaptiveFormats: adaptiveFormats,
- formatStreams: [],
- recommendedVideos: recommendedVideos,
- authorThumbnails: authorThumbnails,
- captions: captions,
- authorId: video.basic_info.channel_id || '',
- authorUrl: `/channel/${video.basic_info.channel_id}`,
- authorVerified: false,
- description: descString,
- descriptionHtml: video.secondary_info.description?.toHTML() || descString,
- published: 0,
- publishedText: video.primary_info.published?.toString() || '',
- premiereTimestamp: 0,
- hlsUrl: video.streaming_data?.hls_manifest_url || undefined,
- liveNow: video.basic_info.is_live || false,
- premium: false,
- storyboards: storyboard,
- isUpcoming: false,
- videoId: videoId,
- videoThumbnails: video.basic_info.thumbnail as Thumbnail[],
- author: video.basic_info.author || 'Unknown',
- lengthSeconds: video.basic_info.duration || 0,
- subCountText: '',
- keywords: video.basic_info.keywords || [],
- allowedRegions: [],
- ytJsVideoInfo: video,
- fallbackPatch: 'youtubejs',
- };
+ const storyboard: StoryBoard[] = [];
+ if (video.storyboards && 'boards' in video.storyboards) {
+ video.storyboards.boards.forEach((board) => {
+ storyboard.push({
+ templateUrl: board.template_url,
+ url: board.template_url,
+ count: board.storyboard_count,
+ height: board.thumbnail_height,
+ width: board.thumbnail_width,
+ interval: board.interval,
+ storyboardCount: board.storyboard_count,
+ storyboardHeight: board.thumbnail_height,
+ storyboardWidth: board.thumbnail_width
+ });
+ });
+ }
+
+ return {
+ type: 'video',
+ title: video.primary_info.title?.toString() || '',
+ viewCount: Number(video.basic_info.view_count || 0),
+ viewCountText: video.basic_info.view_count?.toString() || '0',
+ likeCount: video.basic_info.like_count || 0,
+ dislikeCount: 0,
+ allowRatings: false,
+ rating: 0,
+ isListed: 0,
+ isFamilyFriendly: video.basic_info.is_family_safe || true,
+ genre: video.basic_info.category || '',
+ genreUrl: '',
+ dashUrl: dashUri,
+ adaptiveFormats: adaptiveFormats,
+ formatStreams: [],
+ recommendedVideos: recommendedVideos,
+ authorThumbnails: authorThumbnails,
+ captions: captions,
+ authorId: video.basic_info.channel_id || '',
+ authorUrl: `/channel/${video.basic_info.channel_id}`,
+ authorVerified: false,
+ description: descString,
+ descriptionHtml: video.secondary_info.description?.toHTML() || descString,
+ published: 0,
+ publishedText: video.primary_info.published?.toString() || '',
+ premiereTimestamp: 0,
+ hlsUrl: video.streaming_data?.hls_manifest_url || undefined,
+ liveNow: video.basic_info.is_live || false,
+ premium: false,
+ storyboards: storyboard,
+ isUpcoming: false,
+ videoId: videoId,
+ videoThumbnails: video.basic_info.thumbnail as Thumbnail[],
+ author: video.basic_info.author || 'Unknown',
+ lengthSeconds: video.basic_info.duration || 0,
+ subCountText: '',
+ keywords: video.basic_info.keywords || [],
+ allowedRegions: [],
+ ytjs: {
+ innertube: youtube,
+ video: video,
+ clientPlaybackNonce: clientPlaybackNonce,
+ rawApiResponse: rawPlayerResponse
+ },
+ fallbackPatch: 'youtubejs'
+ };
}
diff --git a/materialious/src/lib/sabr/cacheHelper.ts b/materialious/src/lib/sabr/cacheHelper.ts
new file mode 100644
index 00000000..9296f26f
--- /dev/null
+++ b/materialious/src/lib/sabr/cacheHelper.ts
@@ -0,0 +1,74 @@
+import type shaka from 'shaka-player/dist/shaka-player.ui';
+import type { Misc } from 'youtubei.js';
+
+import type { SabrStreamingContext } from './shakaHttpPlugin';
+import { HttpFetchPlugin } from './shakaHttpPlugin';
+import { createSegmentCacheKey, createSegmentCacheKeyFromContext } from './formatKeyUtils';
+import type { Segment } from './sabrUmpParser';
+
+/**
+ * Caches a segment with its associated format
+ * @param segment - The segment to cache
+ * @param format - The format object
+ */
+export function cacheSegment(segment: Segment, format: Misc.Format) {
+ const isInit = segment.mediaHeader.isInitSeg;
+ const segmentKey = createSegmentCacheKey(segment.mediaHeader, isInit, format);
+
+ if (isInit) {
+ HttpFetchPlugin.cacheManager.setInitSegment(
+ segmentKey,
+ segment.data as Uint8Array
+ );
+ } else {
+ HttpFetchPlugin.cacheManager.setSegment(segmentKey, segment.data as Uint8Array);
+ }
+}
+
+/**
+ * Retrieves a cached segment based on streaming context
+ * @param decodedStreamingContext - The SABR streaming context
+ * @param request - The original request.
+ * @param requestType - The Shaka request type
+ * @param uri - The request URI
+ * @returns A Shaka response object or undefined if not found
+ */
+export function retrieveCachedSegment(
+ decodedStreamingContext: SabrStreamingContext,
+ request: shaka.extern.Request,
+ requestType: shaka.net.NetworkingEngine.RequestType,
+ uri: string
+): shaka.extern.Response | null {
+ if (!decodedStreamingContext.byteRange || !decodedStreamingContext.format) return null;
+
+ const segmentKey = createSegmentCacheKeyFromContext({
+ ...decodedStreamingContext,
+ isInit: decodedStreamingContext.isInit
+ });
+
+ let arrayBuffer: ArrayBuffer | undefined;
+
+ arrayBuffer = (
+ decodedStreamingContext.isInit
+ ? HttpFetchPlugin.cacheManager.getInitSegment(segmentKey)
+ : HttpFetchPlugin.cacheManager.getSegment(segmentKey)
+ )?.buffer;
+
+ if (arrayBuffer) {
+ if (decodedStreamingContext.isInit) {
+ arrayBuffer = arrayBuffer.slice(
+ decodedStreamingContext.byteRange.start,
+ decodedStreamingContext.byteRange.end + 1
+ );
+ }
+
+ const headers = HttpFetchPlugin.headersToGenericObject_(new Headers());
+ headers['X-Streaming-Context'] = btoa(JSON.stringify(decodedStreamingContext));
+ headers['content-type'] = decodedStreamingContext.format.mime_type.split(';')[0];
+ headers['content-length'] = arrayBuffer.byteLength.toString();
+
+ return HttpFetchPlugin.makeResponse(headers, arrayBuffer, 200, uri, uri, request, requestType);
+ }
+
+ return null;
+}
diff --git a/materialious/src/lib/sabr/cacheManager.ts b/materialious/src/lib/sabr/cacheManager.ts
new file mode 100644
index 00000000..d7a697dc
--- /dev/null
+++ b/materialious/src/lib/sabr/cacheManager.ts
@@ -0,0 +1,147 @@
+export interface CacheEntry {
+ data: Uint8Array;
+ timestamp: number;
+ size: number;
+}
+
+/**
+ * A "proper" cache for storing segments.
+ */
+export class CacheManager {
+ private initSegmentCache = new Map();
+ private segmentCache = new Map();
+ private currentSize = 0;
+ private readonly maxCacheSize: number;
+ private readonly maxAge: number;
+
+ constructor(maxSizeMB = 50, maxAgeSeconds = 300) {
+ this.maxCacheSize = maxSizeMB * 1024 * 1024;
+ this.maxAge = maxAgeSeconds * 1000;
+ this.startGarbageCollection();
+ }
+
+ public setInitSegment(key: string, data: Uint8Array): void {
+ const entry: CacheEntry = {
+ data,
+ timestamp: Date.now(),
+ size: data.byteLength
+ };
+
+ if (!this.initSegmentCache.has(key)) {
+ this.currentSize += entry.size;
+ this.enforceStorageLimit();
+ }
+
+ this.initSegmentCache.set(key, entry);
+ }
+
+ public setSegment(key: string, data: Uint8Array): void {
+ const entry: CacheEntry = {
+ data,
+ timestamp: Date.now(),
+ size: data.byteLength
+ };
+
+ this.currentSize += entry.size;
+ this.enforceStorageLimit();
+ this.segmentCache.set(key, entry);
+ }
+
+ public getInitSegment(key: string): Uint8Array | undefined {
+ const entry = this.initSegmentCache.get(key);
+
+ if (entry && !this.isExpired(entry)) {
+ entry.timestamp = Date.now(); // Update last access time
+ return entry.data;
+ }
+
+ // Expired. Get rid of it.
+ if (entry) {
+ this.initSegmentCache.delete(key);
+ this.currentSize -= entry.size;
+ }
+
+ return undefined;
+ }
+
+ public getSegment(key: string): Uint8Array | undefined {
+ const entry = this.segmentCache.get(key);
+ if (entry && !this.isExpired(entry)) {
+ const data = entry.data;
+ this.segmentCache.delete(key);
+ this.currentSize -= entry.size;
+ return data;
+ }
+
+ // Expired. Get rid of it.
+ if (entry) {
+ this.segmentCache.delete(key);
+ this.currentSize -= entry.size;
+ }
+
+ return undefined;
+ }
+
+ private isExpired(entry: CacheEntry): boolean {
+ return Date.now() - entry.timestamp > this.maxAge;
+ }
+
+ private enforceStorageLimit(): void {
+ if (this.currentSize <= this.maxCacheSize) return;
+
+ this.clearExpiredEntries();
+
+ // If still over limit, remove oldest entries
+ if (this.currentSize > this.maxCacheSize) {
+ this.removeOldestEntries();
+ }
+ }
+
+ private clearExpiredEntries(): void {
+ const now = Date.now();
+
+ for (const [key, entry] of this.segmentCache.entries()) {
+ if (now - entry.timestamp > this.maxAge) {
+ this.segmentCache.delete(key);
+ this.currentSize -= entry.size;
+ }
+ }
+
+ for (const [key, entry] of this.initSegmentCache.entries()) {
+ if (now - entry.timestamp > this.maxAge) {
+ this.initSegmentCache.delete(key);
+ this.currentSize -= entry.size;
+ }
+ }
+ }
+
+ private removeOldestEntries(): void {
+ const segments = Array.from(this.segmentCache.entries());
+ const initSegments = Array.from(this.initSegmentCache.entries());
+
+ const allEntries = [...segments, ...initSegments].sort(
+ (a, b) => a[1].timestamp - b[1].timestamp
+ );
+
+ // Remove oldest entries until under limit
+ while (this.currentSize > this.maxCacheSize && allEntries.length > 0) {
+ const [key, entry] = allEntries.shift()!;
+ this.segmentCache.delete(key);
+ this.initSegmentCache.delete(key);
+ this.currentSize -= entry.size;
+ }
+ }
+
+ private startGarbageCollection(): void {
+ // Should there be a way to stop this? :p
+ setInterval(() => {
+ this.clearExpiredEntries();
+ }, 60000);
+ }
+
+ public clearCache(): void {
+ this.initSegmentCache.clear();
+ this.segmentCache.clear();
+ this.currentSize = 0;
+ }
+}
diff --git a/materialious/src/lib/sabr/formatKeyUtils.ts b/materialious/src/lib/sabr/formatKeyUtils.ts
new file mode 100644
index 00000000..8bac92c4
--- /dev/null
+++ b/materialious/src/lib/sabr/formatKeyUtils.ts
@@ -0,0 +1,102 @@
+import type { Misc } from 'youtubei.js/web';
+import type { Protos } from 'googlevideo';
+import type { SabrStreamingContext } from './shakaHttpPlugin';
+
+/**
+ * Creates a format key based on itag and xtags
+ * @param itag - The itag value
+ * @param xtags - The xtags value (optional)
+ * @returns A string format key
+ */
+export function createKey(itag: number | undefined, xtags: string | undefined): string {
+ return `${itag || ''}:${xtags || ''}`;
+}
+
+/**
+ * Creates a format key from a Format object
+ * @param format - The format object
+ * @returns A string format key or undefined if format is undefined
+ */
+export function fromFormat(format: Misc.Format | undefined): string | undefined {
+ if (!format) return undefined;
+ return createKey(format.itag, format.xtags);
+}
+
+/**
+ * Creates a format key from a MediaHeader object
+ * @param mediaHeader - The MediaHeader object
+ * @returns A string format key
+ */
+export function fromMediaHeader(mediaHeader: Protos.MediaHeader): string {
+ return createKey(mediaHeader.itag, mediaHeader.xtags);
+}
+
+/**
+ * Creates a format key from FormatInitializationMetadata
+ * @param formatInitMetadata - The FormatInitializationMetadata object
+ * @returns A string format key or undefined if formatId is undefined
+ */
+export function fromFormatInitializationMetadata(
+ formatInitMetadata: Protos.FormatInitializationMetadata
+): string {
+ if (!formatInitMetadata.formatId) return '';
+ return createKey(formatInitMetadata.formatId.itag, formatInitMetadata.formatId.xtags);
+}
+
+/**
+ * Creates a segment cache key
+ * @param mediaHeader - The MediaHeader object
+ * @param isInit - Whether it's an initialization segment
+ * @param format - Format object (needed for init segments)
+ * @returns A string key for caching segments
+ */
+export function createSegmentCacheKey(
+ mediaHeader: Protos.MediaHeader,
+ isInit?: boolean,
+ format?: Misc.Format
+): string {
+ if (isInit && format) {
+ return `${mediaHeader.itag}:${mediaHeader.xtags || ''}:${format.content_length || ''}:${format.mime_type || ''}`;
+ }
+ /* @ts-ignore */
+ return `${mediaHeader.startRange || '0'}-${mediaHeader.itag}-${mediaHeader.xtags || ''}`;
+}
+
+/**
+ * Creates a segment cache key from a SabrStreamingContext object
+ * @param context - The SabrStreamingContext object
+ * @returns A string key for caching segments
+ */
+export function createSegmentCacheKeyFromContext(context: SabrStreamingContext): string {
+ if (!context.byteRange || !context.format)
+ throw new Error('Invalid context: byteRange or format is missing');
+
+ const pseudoMediaHeader = {
+ itag: context.format.itag,
+ xtags: context.format.xtags || '',
+ startDataRange: context.byteRange.start,
+ isInitSeg: context.isInit
+ };
+
+ return createSegmentCacheKey(
+ pseudoMediaHeader as any,
+ !!context.isInit,
+ context.isInit ? context.format : undefined
+ );
+}
+
+export function getUniqueFormatId(format: Misc.Format) {
+ if (format.has_video) return format.itag.toString();
+
+ const uid_parts = [format.itag.toString()];
+
+ if (format.audio_track) {
+ uid_parts.push(format.audio_track.id);
+ }
+
+ if (format.is_drc) {
+ uid_parts.push('drc');
+ }
+
+ return uid_parts.join('-');
+}
diff --git a/materialious/src/lib/sabr/sabrUmpParser.ts b/materialious/src/lib/sabr/sabrUmpParser.ts
new file mode 100644
index 00000000..b4318990
--- /dev/null
+++ b/materialious/src/lib/sabr/sabrUmpParser.ts
@@ -0,0 +1,281 @@
+import { GoogleVideo, Protos, concatenateChunks, PART } from 'googlevideo';
+import type { Part } from 'googlevideo';
+import shaka from 'shaka-player/dist/shaka-player.ui';
+
+import { cacheSegment } from './cacheHelper';
+import { fromFormat, fromMediaHeader } from './formatKeyUtils';
+import { HttpFetchPlugin } from './shakaHttpPlugin';
+import type { SabrStreamingContext } from './shakaHttpPlugin';
+
+export interface Segment {
+ headerId?: number;
+ mediaHeader: Protos.MediaHeader;
+ complete?: boolean;
+ data: Uint8Array;
+}
+
+export class SabrUmpParser {
+ private partialPart?: Part;
+ private formatInitMetadata: Protos.FormatInitializationMetadata[] = [];
+ private playbackCookie?: Protos.PlaybackCookie;
+ private targetHeaderId?: number;
+ private targetSegment?: Segment;
+
+ constructor(
+ private response: Response,
+ private decodedStreamingContext: SabrStreamingContext,
+ private uri: string,
+ private request: shaka.extern.Request,
+ private requestType: shaka.net.NetworkingEngine.RequestType,
+ private abortController: AbortController
+ ) {}
+
+ async parse(): Promise {
+ const reader = this.response.clone().body!.getReader();
+
+ while (!this.abortController.signal.aborted) {
+ const { value, done } = await reader.read();
+
+ if (done) {
+ // If we got here, we read the whole stream but there was no data; it means we must follow this redirect.
+ if (
+ this.decodedStreamingContext.isSABR &&
+ this.decodedStreamingContext.streamInfo?.redirect
+ ) {
+ return this.createShakaResponse();
+ }
+
+ // We should never reach here, but if we do, it means we got nothing at all.
+ throw this.createRecoverableError(
+ 'Empty response with no redirect information',
+ this.decodedStreamingContext
+ );
+ }
+
+ let chunk;
+
+ if (this.partialPart) {
+ chunk = this.partialPart.data;
+ chunk.append(value);
+ } else {
+ chunk = new GoogleVideo.ChunkedDataBuffer([value]);
+ }
+
+ const result = await this.process(chunk);
+ if (result) {
+ if (this.shouldThrowServerError(result)) {
+ throw this.createRecoverableError('Server streaming error', this.decodedStreamingContext);
+ }
+ return result;
+ }
+ }
+
+ // Never happens. Here just in case.
+ throw this.createRecoverableError(
+ "Couldn't read any data from the stream",
+ this.decodedStreamingContext
+ );
+ }
+
+ private shouldThrowServerError(result: shaka.extern.Response): boolean {
+ return (
+ !result.data.byteLength &&
+ (!!this.decodedStreamingContext.error ||
+ this.decodedStreamingContext.streamInfo?.streamProtectionStatus?.status === 3)
+ );
+ }
+
+ private createRecoverableError(message: string, info?: Record) {
+ return new shaka.util.Error(
+ shaka.util.Error.Severity.RECOVERABLE,
+ shaka.util.Error.Category.NETWORK,
+ shaka.util.Error.Code.HTTP_ERROR,
+ message,
+ { info }
+ );
+ }
+
+ private process(
+ value: GoogleVideo.ChunkedDataBuffer
+ ): Promise {
+ return new Promise((resolve) => {
+ const ump = new GoogleVideo.UMP(value);
+
+ this.partialPart = ump.parse((part: Part) => {
+ const result = this.handlePart(part);
+ if (result) {
+ resolve(result);
+ }
+ });
+
+ resolve(undefined);
+ });
+ }
+
+ private handlePart(part: Part): shaka.extern.Response | undefined {
+ switch (part.type) {
+ case PART.FORMAT_INITIALIZATION_METADATA:
+ this.handleFormatInitMetadata(part);
+ break;
+ case PART.NEXT_REQUEST_POLICY:
+ this.handleNextRequestPolicy(part);
+ break;
+ case PART.MEDIA_HEADER:
+ this.handleMediaHeader(part);
+ break;
+ case PART.MEDIA:
+ this.handleMedia(part);
+ break;
+ case PART.MEDIA_END:
+ return this.handleMediaEnd(part);
+ case PART.SABR_ERROR:
+ return this.handleSabrError(part);
+ case PART.STREAM_PROTECTION_STATUS:
+ return this.handleStreamProtectionStatus(part);
+ case PART.SABR_REDIRECT:
+ return this.handleSabrRedirect(part);
+ default:
+ }
+ }
+
+ private handleFormatInitMetadata(part: Part) {
+ const formatInitMetadata = Protos.FormatInitializationMetadata.decode(part.data.chunks[0]);
+ this.formatInitMetadata.push(formatInitMetadata);
+ }
+
+ private handleNextRequestPolicy(part: Part) {
+ const nextRequestPolicy = Protos.NextRequestPolicy.decode(part.data.chunks[0]);
+ if (this.decodedStreamingContext.format?.has_video) {
+ this.playbackCookie = nextRequestPolicy.playbackCookie;
+ }
+ }
+
+ private handleMediaHeader(part: Part) {
+ const mediaHeader = Protos.MediaHeader.decode(part.data.chunks[0]);
+ const formatKey = fromFormat(this.decodedStreamingContext.format);
+ const segmentFormatKey = fromMediaHeader(mediaHeader);
+
+ if (!this.decodedStreamingContext.isSABR || segmentFormatKey === formatKey) {
+ if (!this.targetHeaderId) {
+ this.targetHeaderId = mediaHeader.headerId;
+ this.targetSegment = {
+ headerId: mediaHeader.headerId,
+ mediaHeader: mediaHeader,
+ data: new Uint8Array()
+ };
+ }
+ }
+ }
+
+ private handleMedia(part: Part) {
+ const headerId = part.data.getUint8(0);
+ const buffer = part.data.split(1).remainingBuffer;
+
+ if (this.targetSegment && headerId === this.targetHeaderId) {
+ this.targetSegment.data = concatenateChunks([this.targetSegment.data, ...buffer.chunks]);
+ }
+ }
+
+ private handleMediaEnd(part: Part) {
+ const headerId = part.data.getUint8(0);
+
+ if (this.targetSegment && headerId === this.targetHeaderId) {
+ if (this.decodedStreamingContext) {
+ this.decodedStreamingContext.streamInfo = {
+ ...this.decodedStreamingContext.streamInfo,
+ playbackCookie: this.playbackCookie,
+ formatInitMetadata: this.formatInitMetadata,
+ mediaHeader: this.targetSegment.mediaHeader.isInitSeg
+ ? undefined
+ : this.targetSegment.mediaHeader
+ };
+ }
+
+ let arrayBuffer: Uint8Array;
+
+ // Why cache the init segment? Well, SABR responses are still a bit larger than usual - caching the init segment
+ // helps reduce the delay when switching between different qualities or initializing a new stream.
+ if (
+ this.decodedStreamingContext.isInit &&
+ this.decodedStreamingContext.format &&
+ this.decodedStreamingContext.byteRange
+ ) {
+ cacheSegment(this.targetSegment, this.decodedStreamingContext.format);
+ arrayBuffer = this.targetSegment.data.slice(
+ this.decodedStreamingContext.byteRange.start,
+ this.decodedStreamingContext.byteRange.end + 1
+ );
+ } else {
+ arrayBuffer = this.targetSegment.data;
+ }
+
+ // We got what we wanted; close the stream and abort the request.
+ this.abortController.abort();
+ return this.createShakaResponse(arrayBuffer);
+ }
+ }
+
+ private handleSabrError(part: Part) {
+ const sabrError = Protos.SabrError.decode(part.data.chunks[0]);
+
+ if (this.decodedStreamingContext) {
+ this.decodedStreamingContext.error = { sabrError };
+ }
+
+ this.abortController.abort();
+ return this.createShakaResponse();
+ }
+
+ private handleStreamProtectionStatus(part: Part) {
+ const streamProtectionStatus = Protos.StreamProtectionStatus.decode(part.data.chunks[0]);
+
+ if (this.decodedStreamingContext) {
+ this.decodedStreamingContext.streamInfo = {
+ ...this.decodedStreamingContext.streamInfo,
+ streamProtectionStatus
+ };
+ }
+
+ if (streamProtectionStatus.status === 3) {
+ this.abortController.abort();
+ return this.createShakaResponse();
+ }
+ }
+
+ private handleSabrRedirect(part: Part) {
+ const redirect = Protos.SabrRedirect.decode(part.data.chunks[0]);
+
+ if (this.decodedStreamingContext) {
+ this.decodedStreamingContext.streamInfo = {
+ ...this.decodedStreamingContext.streamInfo,
+ redirect
+ };
+ }
+
+ // With pure UMP, redirects should be followed immediately.
+ if (this.decodedStreamingContext.isUMP && !this.decodedStreamingContext.isSABR) {
+ this.abortController.abort();
+ return this.createShakaResponse();
+ }
+ }
+
+ private createShakaResponse(body?: Uint8Array): shaka.extern.Response {
+ return HttpFetchPlugin.makeResponse(
+ this.createContextHeaders(),
+ body || new ArrayBuffer(0),
+ this.response.status,
+ this.uri,
+ this.response.url,
+ this.request,
+ this.requestType
+ );
+ }
+
+ private createContextHeaders(): Record {
+ const headers = HttpFetchPlugin.headersToGenericObject_(this.response.headers);
+ if (this.decodedStreamingContext) {
+ headers['X-Streaming-Context'] = btoa(JSON.stringify(this.decodedStreamingContext));
+ }
+ return headers;
+ }
+}
diff --git a/materialious/src/lib/sabr/shakaHttpPlugin.ts b/materialious/src/lib/sabr/shakaHttpPlugin.ts
new file mode 100644
index 00000000..fbeb11dc
--- /dev/null
+++ b/materialious/src/lib/sabr/shakaHttpPlugin.ts
@@ -0,0 +1,266 @@
+import shaka from 'shaka-player/dist/shaka-player.ui';
+import type { Misc } from 'youtubei.js/web';
+import type { Protos } from 'googlevideo';
+
+import { retrieveCachedSegment } from './cacheHelper';
+import { SabrUmpParser } from './sabrUmpParser';
+import { CacheManager } from './cacheManager';
+import { Capacitor } from '@capacitor/core';
+import { androidFetch } from '$lib/android/http/androidRequests';
+
+export interface SabrStreamingContext {
+ byteRange?: { start: number; end: number };
+ format?: Misc.Format;
+ isInit?: boolean;
+ isUMP?: boolean;
+ isSABR?: boolean;
+ playerTimeMs?: number;
+ streamInfo?: {
+ playbackCookie?: Protos.PlaybackCookie;
+ formatInitMetadata?: Protos.FormatInitializationMetadata[];
+ streamProtectionStatus?: Protos.StreamProtectionStatus;
+ mediaHeader?: Protos.MediaHeader;
+ redirect?: Protos.SabrRedirect;
+ };
+ error?: {
+ sabrError?: Protos.SabrError;
+ };
+}
+
+export class HttpFetchPlugin {
+ private static fetch_ = Capacitor.getPlatform() === 'android' ? androidFetch : window.fetch;
+ private static AbortController_ = window.AbortController;
+ private static Headers_ = window.Headers;
+ public static cacheManager = new CacheManager();
+
+ private static asMap(object: Record): Map {
+ const map = new Map();
+ for (const key of Object.keys(object)) {
+ map.set(key as K, object[key]);
+ }
+ return map;
+ }
+
+ static parse(
+ uri: string,
+ request: shaka.extern.Request,
+ requestType: shaka.net.NetworkingEngine.RequestType,
+ _progressUpdated: shaka.extern.ProgressUpdated,
+ headersReceived: shaka.extern.HeadersReceived
+ ): shaka.extern.IAbortableOperation {
+ const headers = new HttpFetchPlugin.Headers_();
+
+ HttpFetchPlugin.asMap(request.headers).forEach((value, key) => {
+ headers.append(key as string, value as string);
+ });
+
+ let sabrStreamingContext: string | null = null;
+
+ // Save the streaming context for later use, then remove it from the headers
+ // as it's only used by the player and not the server.
+ if (headers.has('X-Streaming-Context')) {
+ sabrStreamingContext = headers.get('X-Streaming-Context');
+ headers.delete('X-Streaming-Context');
+ }
+
+ // Parse uri and remove "___key" query param (same situation as above).
+ const url = new URL(uri);
+ url.searchParams.delete('___key');
+ uri = url.toString();
+
+ const controller = new HttpFetchPlugin.AbortController_();
+ const init: RequestInit = {
+ body: request.body || undefined,
+ headers,
+ method: request.method,
+ signal: controller.signal,
+ credentials: request.allowCrossSiteCredentials ? 'include' : undefined
+ };
+
+ const abortStatus = {
+ canceled: false,
+ timedOut: false
+ };
+
+ const pendingRequest = HttpFetchPlugin.request_(
+ uri,
+ request,
+ requestType,
+ init,
+ controller,
+ abortStatus,
+ headersReceived,
+ sabrStreamingContext
+ );
+
+ const op = new shaka.util.AbortableOperation(pendingRequest, () => {
+ abortStatus.canceled = true;
+ controller.abort();
+ return Promise.resolve();
+ });
+
+ const timeoutMs = request.retryParameters.timeout;
+ if (timeoutMs) {
+ const timer = new shaka.util.Timer(() => {
+ abortStatus.timedOut = true;
+ controller.abort();
+ });
+
+ timer.tickAfter(timeoutMs / 1000);
+ op.finally(() => timer.stop());
+ }
+
+ return op;
+ }
+
+ private static async request_(
+ uri: string,
+ request: shaka.extern.Request,
+ requestType: shaka.net.NetworkingEngine.RequestType,
+ init: RequestInit,
+ abortController: AbortController,
+ abortStatus: { canceled: boolean; timedOut: boolean },
+ headersReceived: shaka.extern.HeadersReceived,
+ streamingContext: string | null
+ ): Promise {
+ const fetch = HttpFetchPlugin.fetch_;
+ const decodedStreamingContext = streamingContext
+ ? (JSON.parse(atob(streamingContext)) as SabrStreamingContext)
+ : undefined;
+
+ let response: Response;
+ let arrayBuffer: ArrayBuffer | undefined;
+
+ try {
+ // Try to use cached segment first
+ if (decodedStreamingContext && decodedStreamingContext.format) {
+ const cachedResponse = retrieveCachedSegment(
+ decodedStreamingContext,
+ request,
+ requestType,
+ uri
+ );
+ if (cachedResponse) {
+ return cachedResponse;
+ }
+ }
+
+ response = await fetch(uri, init);
+ headersReceived(HttpFetchPlugin.headersToGenericObject_(response.headers));
+
+ // Handle UMP responses
+ if (
+ init.method !== 'HEAD' &&
+ decodedStreamingContext &&
+ response.headers.get('content-type') === 'application/vnd.yt-ump'
+ ) {
+ const parser = new SabrUmpParser(
+ response,
+ decodedStreamingContext,
+ uri,
+ request,
+ requestType,
+ abortController
+ );
+ return parser.parse();
+ }
+
+ arrayBuffer = await response.arrayBuffer();
+ } catch (error) {
+ if (abortStatus.canceled) {
+ throw new shaka.util.Error(
+ shaka.util.Error.Severity.RECOVERABLE,
+ shaka.util.Error.Category.NETWORK,
+ shaka.util.Error.Code.OPERATION_ABORTED,
+ uri,
+ requestType
+ );
+ } else if (abortStatus.timedOut) {
+ throw new shaka.util.Error(
+ shaka.util.Error.Severity.RECOVERABLE,
+ shaka.util.Error.Category.NETWORK,
+ shaka.util.Error.Code.TIMEOUT,
+ uri,
+ requestType
+ );
+ }
+ throw new shaka.util.Error(
+ shaka.util.Error.Severity.RECOVERABLE,
+ shaka.util.Error.Category.NETWORK,
+ shaka.util.Error.Code.HTTP_ERROR,
+ uri,
+ error,
+ requestType
+ );
+ }
+
+ const headers = HttpFetchPlugin.headersToGenericObject_(response.headers);
+ if (streamingContext) {
+ headers['X-Streaming-Context'] = streamingContext;
+ }
+
+ return HttpFetchPlugin.makeResponse(
+ headers,
+ arrayBuffer!,
+ response.status,
+ uri,
+ response.url,
+ request,
+ requestType
+ );
+ }
+
+ public static makeResponse(
+ headers: Record,
+ data: BufferSource,
+ status: number,
+ uri: string,
+ responseURL: string,
+ request: shaka.extern.Request,
+ requestType: shaka.net.NetworkingEngine.RequestType
+ ): shaka.extern.Response & { originalRequest: shaka.extern.Request } {
+ if (status >= 200 && status <= 299 && status !== 202) {
+ return {
+ uri: responseURL || uri,
+ originalUri: uri,
+ data,
+ status,
+ headers,
+ originalRequest: request,
+ fromCache: !!headers['x-shaka-from-cache']
+ };
+ }
+
+ let responseText: string | null = null;
+ try {
+ responseText = shaka.util.StringUtils.fromBytesAutoDetect(data);
+ } catch {
+ /* no-op */
+ }
+
+ const severity =
+ status === 401 || status === 403
+ ? shaka.util.Error.Severity.CRITICAL
+ : shaka.util.Error.Severity.RECOVERABLE;
+
+ throw new shaka.util.Error(
+ severity,
+ shaka.util.Error.Category.NETWORK,
+ shaka.util.Error.Code.BAD_HTTP_STATUS,
+ uri,
+ status,
+ responseText,
+ headers,
+ requestType,
+ responseURL || uri
+ );
+ }
+
+ public static headersToGenericObject_(headers: Headers): Record {
+ const headersObj: Record = {};
+ headers.forEach((value, key) => {
+ headersObj[key.trim()] = value;
+ });
+ return headersObj;
+ }
+}
diff --git a/materialious/src/lib/timestamps.ts b/materialious/src/lib/timestamps.ts
index a4145b09..f11fbd31 100644
--- a/materialious/src/lib/timestamps.ts
+++ b/materialious/src/lib/timestamps.ts
@@ -1,81 +1,93 @@
-import { decodeHtmlCharCodes } from "./misc";
-import { convertToSeconds } from "./numbers";
+import { decodeHtmlCharCodes } from './misc';
+import { convertToSeconds } from './numbers';
export interface PhasedDescription {
- description: string;
- timestamps: { title: string; time: number; timePretty: string; }[];
+ description: string;
+ timestamps: { title: string; time: number; timePretty: string }[];
}
export function extractActualLink(url: string): string {
- const urlParams = new URLSearchParams(url.split('?')[1]);
- const actualLink = urlParams.get('q');
- if (actualLink) {
- return decodeURIComponent(actualLink);
- }
- return url;
+ const urlParams = new URLSearchParams(url.split('?')[1]);
+ const actualLink = urlParams.get('q');
+ if (actualLink) {
+ return decodeURIComponent(actualLink);
+ }
+ return url;
}
export function processYoutubeLink(line: string): string {
- // Regex to match the tag and extract the href (YouTube redirect link)
- const urlRegex = / tag and extract the href (YouTube redirect link)
+ const urlRegex = /(\d+:\d+(?::\d+)?)<\/a>\s*(.+)/;
- const timestampRegexYtJs = new RegExp(
- `href="https://www\\.youtube\\.com/watch\\?v=${videoId}(?:&t=(\\d+)s)?"[^>]*>\\s*]*>\\s*([^<]+)\\s*\\s*\\s*]*>\\s*([^<]+)\\s*`,
- 'i'
- );
+ // Regular expressions for different timestamp formats
+ const urlRegex = /(\d+:\d+(?::\d+)?)<\/a>\s*(.+)/;
+ const timestampRegexYtJs = new RegExp(
+ `href="https://www\\.youtube\\.com/watch\\?v=${videoId}(?:&t=(\\d+)s)?"[^>]*>\\s*]*>\\s*([^<]+)\\s*\\s*\\s*]*>\\s*([^<]+)\\s*`,
+ 'i'
+ );
- let filteredLines: string[] = [];
- lines.forEach((line) => {
- const urlMatch = urlRegex.exec(line);
- // Use appropriate regex based on the `usingYoutubeJs` flag
- const timestampMatch = (fallbackPatch === 'youtubejs' ? timestampRegexYtJs : timestampRegexInvidious).exec(fallbackPatch === 'youtubejs' ? line + '' : line);
+ const filteredLines: string[] = [];
+ lines.forEach((line) => {
+ const urlMatch = urlRegex.exec(line);
+ // Use appropriate regex based on the `usingYoutubeJs` flag
+ const timestampMatch = (
+ fallbackPatch === 'youtubejs' ? timestampRegexYtJs : timestampRegexInvidious
+ ).exec(fallbackPatch === 'youtubejs' ? line + '' : line);
- if (urlMatch !== null && timestampMatch === null) {
- // If line contains a URL but not a timestamp, modify the URL
- const modifiedLine = processYoutubeLink(line).replace(
- /]+>/g, '').replace(/\n/g, '').trim()),
- timePretty: timestamp
- });
- } else {
- filteredLines.push(line);
- }
- });
+ timestamps.push({
+ time: convertToSeconds(time),
+ // Remove any HTML in the timestamp title.
+ title: decodeHtmlCharCodes(
+ title
+ .replace(/<[^>]+>/g, '')
+ .replace(/\n/g, '')
+ .trim()
+ ),
+ timePretty: timestamp
+ });
+ } else {
+ filteredLines.push(line);
+ }
+ });
- const filteredContent = filteredLines.join('\n');
+ const filteredContent = filteredLines.join('\n');
- return { description: filteredContent, timestamps: timestamps };
-}
\ No newline at end of file
+ return { description: filteredContent, timestamps: timestamps };
+}
diff --git a/materialious/src/routes/(app)/+layout.svelte b/materialious/src/routes/(app)/+layout.svelte
index cd9e57f9..15e200cf 100644
--- a/materialious/src/routes/(app)/+layout.svelte
+++ b/materialious/src/routes/(app)/+layout.svelte
@@ -192,11 +192,9 @@
const link = (event.target as HTMLElement).closest('a');
- if (link && link.href) {
- if (link.href && link.href.startsWith('http') && link.target === '_blank') {
- event.preventDefault();
- Browser.open({ url: link.href });
- }
+ if (link && link.href && link.href.startsWith('http') && link.target === '_blank') {
+ event.preventDefault();
+ Browser.open({ url: link.href });
}
}
diff --git a/materialious/static/nodejs-android/index.js b/materialious/static/nodejs-android/index.js
index f3c41640..42ef0331 100644
--- a/materialious/static/nodejs-android/index.js
+++ b/materialious/static/nodejs-android/index.js
@@ -5,129 +5,154 @@ const HOST = 'localhost';
const PORT = 3000;
const MAX_REDIRECTS = 10;
-const CORS_HEADERS = 'Origin, X-Requested-With, Content-Type, Accept, Authorization, x-goog-visitor-id, x-goog-api-key, x-origin, x-youtube-client-version, x-youtube-client-name, x-goog-api-format-version, x-user-agent, Accept-Language, Range, Referer'
-const CORS_ORIGIN = 'https://www.youtube.com'
-const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36(KHTML, like Gecko)'
+const CORS_HEADERS = [
+ 'Origin',
+ 'X-Requested-With',
+ 'Content-Type',
+ 'Accept',
+ 'Authorization',
+ 'x-goog-visitor-id',
+ 'x-goog-api-key',
+ 'x-origin',
+ 'x-youtube-client-version',
+ 'x-youtube-client-name',
+ 'x-goog-api-format-version',
+ 'x-goog-authuser',
+ 'x-user-agent',
+ 'Accept-Language',
+ 'X-Goog-FieldMask',
+ 'Range',
+ 'Referer',
+ 'Cookie'
+].join(', ');
+const CORS_ORIGIN = 'https://www.youtube.com';
+const USER_AGENT =
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36(KHTML, like Gecko)';
function setCorsHeaders(res) {
- res.setHeader('Access-Control-Allow-Origin', CORS_ORIGIN);
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
- res.setHeader('Access-Control-Allow-Headers', CORS_HEADERS);
- res.setHeader('Access-Control-Max-Age', '86400')
- res.setHeader('Access-Control-Allow-Credentials', 'true');
+ res.setHeader('Access-Control-Allow-Origin', CORS_ORIGIN);
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
+ res.setHeader('Access-Control-Allow-Headers', CORS_HEADERS);
+ res.setHeader('Access-Control-Max-Age', '86400');
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
}
-function fetchWithRedirects(targetUrl, options, redirectCount = 0) {
- return new Promise((resolve, reject) => {
- const httpClient = targetUrl.protocol.startsWith('https') ? https : http;
+function fetchWithRedirects(targetUrl, options, bodyChunks, redirectCount = 0) {
+ return new Promise((resolve, reject) => {
+ const httpClient = targetUrl.protocol.startsWith('https') ? https : http;
- const req = httpClient.request(targetUrl, options, (res) => {
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
- if (redirectCount >= MAX_REDIRECTS) {
- req.end();
- return reject(new Error('Too many redirects'));
- }
+ const req = httpClient.request(targetUrl, options, (res) => {
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
+ if (redirectCount >= MAX_REDIRECTS) {
+ req.end();
+ return reject(new Error('Too many redirects'));
+ }
- try {
- // Attempt to create the redirect URL
- const redirectUrl = new URL(res.headers.location, targetUrl);
- return resolve(fetchWithRedirects(redirectUrl, options, redirectCount + 1));
- } catch (error) {
- req.end();
- return reject(new Error(`Invalid URL in redirect: ${error.message}`));
- }
- }
- resolve(res); // Resolve with the final response if not a redirect
- });
+ try {
+ // Attempt to create the redirect URL
+ const redirectUrl = new URL(res.headers.location, targetUrl);
+ return resolve(fetchWithRedirects(redirectUrl, options, bodyChunks, redirectCount + 1));
+ } catch (error) {
+ req.end();
+ return reject(new Error(`Invalid URL in redirect: ${error.message}`));
+ }
+ }
+ resolve(res); // Resolve with the final response if not a redirect
+ });
- if (options.body && (req.method === 'POST' || req.method === 'PUT')) {
- req.write(options.body);
- }
+ // For POST and PUT methods, pass the body to the outgoing request
+ if (bodyChunks && (req.method === 'POST' || req.method === 'PUT')) {
+ const buffer = Buffer.concat(bodyChunks);
+ options.headers['Content-Length'] = buffer.length;
- req.setTimeout(10000, () => reject(new Error('Request timeout')), req.end());
- req.on('error', (error) => reject(error), req.end());
- req.end();
- });
+ req.write(buffer);
+ }
+
+ req.setTimeout(10000, () => reject(new Error('Request timeout')), req.end());
+ req.on('error', (error) => reject(error), req.end());
+ req.end();
+ });
}
const server = http.createServer(async (req, res) => {
- setCorsHeaders(res);
+ setCorsHeaders(res);
- if (req.method === 'OPTIONS') {
- res.writeHead(200, { 'Content-Type': 'text/plain' });
- return res.end();
- }
+ if (req.method === 'OPTIONS') {
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
+ return res.end();
+ }
- if (!req.url || req.url === '/') {
- res.writeHead(400, { 'Content-Type': 'text/plain' });
- return res.end('No URL provided to fetch.');
- }
+ if (!req.url || req.url === '/') {
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
+ return res.end('No URL provided to fetch.');
+ }
- let targetUrl = req.url.slice(1); // Remove leading '/'
- let parsedTarget;
- try {
- // Ensure protocol (http) is added if missing
- if (!targetUrl.startsWith('http')) {
- targetUrl = 'http://' + targetUrl;
- }
- parsedTarget = new URL(targetUrl);
- } catch (error) {
- res.writeHead(400, { 'Content-Type': 'text/plain' });
- return res.end(`Invalid URL: ${error.message}`);
- }
+ let targetUrl = req.url.slice(1); // Remove leading '/'
+ let parsedTarget;
+ try {
+ // Ensure protocol (http) is added if missing
+ if (!targetUrl.startsWith('http')) {
+ targetUrl = 'http://' + targetUrl;
+ }
+ parsedTarget = new URL(targetUrl);
+ } catch (error) {
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
+ return res.end(`Invalid URL: ${error.message}`);
+ }
- let body = '';
- req.on('data', chunk => {
- body += chunk;
- });
+ let chunks = [];
+ req.on('data', (chunk) => {
+ chunks.push(chunk);
+ });
- req.on('end', async () => {
- const options = {
- method: req.method,
- headers: Object.fromEntries(
- Object.entries(req.headers).filter(([key]) => ![
- 'host', 'origin', 'referer', 'x-forwarded-for', 'x-requested-with'
- ].includes(key.toLowerCase()))
- )
- };
+ req.on('end', async () => {
+ const options = {
+ method: req.method,
+ headers: Object.fromEntries(
+ Object.entries(req.headers).filter(
+ ([key]) =>
+ ![
+ 'referer',
+ 'x-forwarded-for',
+ 'x-requested-with',
+ 'sec-ch-ua-mobile',
+ 'sec-ch-ua',
+ 'sec-ch-ua-platform'
+ ].includes(key.toLowerCase())
+ )
+ )
+ };
- options.headers.host = parsedTarget.host;
- options.headers.origin = parsedTarget.origin;
- options.headers['user-agent'] = USER_AGENT;
+ options.headers.host = parsedTarget.host;
+ options.headers.origin = parsedTarget.origin;
+ options.headers['user-agent'] = USER_AGENT;
- // For POST and PUT methods, pass the body to the outgoing request
- if (req.method === 'POST' || req.method === 'PUT') {
- options.headers['Content-Length'] = Buffer.byteLength(body);
- options.body = body;
- }
+ try {
+ const proxyRes = await fetchWithRedirects(parsedTarget, options, chunks);
- try {
- const proxyRes = await fetchWithRedirects(parsedTarget, options);
+ req.on('close', () => {
+ console.log('Request canceled by the client.');
+ proxyRes.destroy();
+ });
- req.on('close', () => {
- console.log('Request canceled by the client.');
- proxyRes.destroy();
- });
+ res.writeHead(proxyRes.statusCode, {
+ ...proxyRes.headers,
+ 'Access-Control-Allow-Origin': CORS_ORIGIN,
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
+ 'Access-Control-Allow-Headers': CORS_HEADERS,
+ 'Access-Control-Allow-Credentials': 'true'
+ });
- res.writeHead(proxyRes.statusCode, {
- ...proxyRes.headers,
- 'Access-Control-Allow-Origin': CORS_ORIGIN,
- 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
- 'Access-Control-Allow-Headers': CORS_HEADERS,
- 'Access-Control-Allow-Credentials': 'true',
- });
-
- // Pipe response data back to the client
- proxyRes.pipe(res);
-
- } catch (error) {
- console.error('Proxy error:', error);
- res.writeHead(500, { 'Content-Type': 'text/plain' });
- res.end(`Error: ${error.message}`);
- }
- });
+ // Pipe response data back to the client
+ proxyRes.pipe(res);
+ } catch (error) {
+ console.error('Proxy error:', error);
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
+ res.end(`Error: ${error.message}`);
+ }
+ });
});
server.listen(PORT, () => {
- console.log(`Server is running on http://${HOST}:${PORT}`);
+ console.log(`Server is running on http://${HOST}:${PORT}`);
});
diff --git a/update_versions.py b/update_versions.py
index 33ecba7e..599ca5ea 100644
--- a/update_versions.py
+++ b/update_versions.py
@@ -3,7 +3,7 @@ import os
import re
from datetime import datetime
-LATEST_VERSION = "1.7.22"
+LATEST_VERSION = "1.8.0"
RELEASE_DATE = datetime.now().strftime("%Y-%-m-%d") # Format: YYYY-M-D
WORKING_DIR = os.path.join(