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(