Merge pull request #844 from Materialious/update/1.7.15

Update/1.7.15
This commit is contained in:
Ward
2025-04-02 16:18:01 +13:00
committed by GitHub
41 changed files with 2094 additions and 2340 deletions
+8
View File
@@ -39,6 +39,13 @@ jobs:
CHECKSUM=$(curl -L ${{ steps.get_release.outputs.aarch64_url }} | sha256sum | awk '{ print $1 }')
echo "aarch64_sha256=$CHECKSUM" >> "$GITHUB_OUTPUT"
- name: Generate SHA256 checksum for materialious.metainfo.xml
id: sha256_metainfo
run: |
METAINFO_URL="https://raw.githubusercontent.com/Materialious/Materialious/main/materialious/electron/materialious.metainfo.xml"
CHECKSUM=$(curl -L $METAINFO_URL | sha256sum | awk '{ print $1 }')
echo "metainfo_sha256=$CHECKSUM" >> "$GITHUB_OUTPUT"
- name: Update manifest file with new urls and checksums
run: |
MANIFEST_FILE="us.materialio.Materialious.yml"
@@ -46,6 +53,7 @@ jobs:
yq eval -i ".modules[0].sources[0].sha256 = \"${{ steps.sha256_x86.outputs.x86_64_sha256 }}\"" $MANIFEST_FILE
yq eval -i ".modules[0].sources[1].url = \"${{ steps.get_release.outputs.aarch64_url }}\"" $MANIFEST_FILE
yq eval -i ".modules[0].sources[1].sha256 = \"${{ steps.sha256_arm.outputs.aarch64_sha256 }}\"" $MANIFEST_FILE
yq eval -i ".modules[0].sources[5].sha256 = \"${{ steps.sha256_metainfo.outputs.metainfo_sha256 }}\"" $MANIFEST_FILE
- name: Set Git user identity
run: |
-2
View File
@@ -25,7 +25,6 @@
- [Invidious API extended integration!](https://github.com/Materialious/api-extended)
- Sync your watch progress between Invidious sessions.
- Watch sync parties!
- Silence skipper (Experimental.)
- [YouTube.js](https://github.com/LuanRT/YouTube.js) fallback if Invidious fails loading videos for Desktop & Android.
- Preview video on hover.
- Sponsorblock built-in.
@@ -120,7 +119,6 @@ Deploy Materialious for your Invidious instance using Docker. Follow the steps o
- [SecularSteve](https://github.com/SecularSteve) for creating Materialious' logo.
- [Invidious](https://github.com/iv-org)
- [Clipious](https://github.com/lamarios/clipious) for inspiration & a good source for learning more about undocumented Invidious routes.
- [Vidstack player](https://github.com/vidstack/player)
- [Beer CSS](https://github.com/beercss/beercss) (Especially the [YouTube template](https://github.com/beercss/beercss/tree/main/src/youtube) what was used as the base for Materialious.)
- Every dependency in [package.json](/materialious/package.json).
+105 -6
View File
@@ -14,6 +14,17 @@
<option name="screenX" value="720" />
<option name="screenY" value="1280" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OnePlus" />
<option name="codename" value="OP5552L1" />
<option name="id" value="OP5552L1" />
<option name="manufacturer" value="OnePlus" />
<option name="name" value="CPH2415" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2412" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OPPO" />
@@ -58,6 +69,17 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="a35x" />
<option name="id" value="a35x" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="A35" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
@@ -91,6 +113,17 @@
<option name="screenX" value="1080" />
<option name="screenY" value="1272" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="motorola" />
<option name="codename" value="austin" />
<option name="id" value="austin" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="moto g 5G (2022)" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
@@ -259,13 +292,24 @@
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gta7lite" />
<option name="id" value="gta7lite" />
<option name="codename" value="gta9pwifi" />
<option name="id" value="gta9pwifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab A7 Lite" />
<option name="screenDensity" value="179" />
<option name="screenX" value="800" />
<option name="screenY" value="1340" />
<option name="name" value="SM-X210" />
<option name="screenDensity" value="240" />
<option name="screenX" value="1200" />
<option name="screenY" value="1920" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts7xllite" />
<option name="id" value="gts7xllite" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-T738U" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
@@ -278,6 +322,28 @@
<option name="screenX" value="1848" />
<option name="screenY" value="2960" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8wifi" />
<option name="id" value="gts8wifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8" />
<option name="screenDensity" value="274" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts9fe" />
<option name="id" value="gts9fe" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S9 FE 5G" />
<option name="screenDensity" value="280" />
<option name="screenX" value="1440" />
<option name="screenY" value="2304" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
@@ -333,6 +399,17 @@
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="o1q" />
<option name="id" value="o1q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S21" />
<option name="screenDensity" value="421" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="google" />
@@ -422,6 +499,17 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="t2q" />
<option name="id" value="t2q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S21 Plus" />
<option name="screenDensity" value="394" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
@@ -444,6 +532,17 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="id" value="tokay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
</list>
</option>
</component>
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "us.materialio.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 96
versionName "1.7.14"
versionCode 97
versionName "1.7.15"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -16,7 +16,6 @@ dependencies {
implementation project(':capacitor-status-bar')
implementation project(':capgo-inappbrowser')
implementation project(':hugotomazi-capacitor-navigation-bar')
implementation project(':mediagrid-capacitor-native-audio')
implementation project(':capacitor-nodejs')
}
@@ -23,8 +23,5 @@ project(':capgo-inappbrowser').projectDir = new File('../node_modules/@capgo/ina
include ':hugotomazi-capacitor-navigation-bar'
project(':hugotomazi-capacitor-navigation-bar').projectDir = new File('../node_modules/@hugotomazi/capacitor-navigation-bar/android')
include ':mediagrid-capacitor-native-audio'
project(':mediagrid-capacitor-native-audio').projectDir = new File('../node_modules/@mediagrid/capacitor-native-audio/android')
include ':capacitor-nodejs'
project(':capacitor-nodejs').projectDir = new File('../node_modules/capacitor-nodejs/android')
+5 -1
View File
@@ -6,9 +6,13 @@ const config: CapacitorConfig = {
webDir: 'build',
plugins: {
CapacitorNodeJS: {
nodeDir: 'nodejs-android'
nodeDir: 'nodejs-android',
}
},
server: {
hostname: 'www.youtube.com',
androidScheme: 'https'
}
};
export default config;
+5 -1
View File
@@ -6,9 +6,13 @@ const config: CapacitorConfig = {
webDir: 'build',
plugins: {
CapacitorNodeJS: {
nodeDir: 'nodejs-android'
nodeDir: 'nodejs-android',
}
},
server: {
hostname: 'www.youtube.com',
androidScheme: 'https'
}
};
export default config;
@@ -57,7 +57,11 @@
<release version="1.7.14" date="2025-2-21">
<release version="1.7.15" date="2025-2-23">
<url>https://github.com/Materialious/Materialious/releases/tag/1.7.15</url>
</release>
<release version="1.7.14" date="2025-2-21">
<url>https://github.com/Materialious/Materialious/releases/tag/1.7.14</url>
</release>
<release version="1.7.13" date="2025-2-21">
+455 -10
View File
@@ -1,21 +1,23 @@
{
"name": "Materialious",
"version": "1.7.6",
"version": "1.7.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Materialious",
"version": "1.7.6",
"version": "1.7.15",
"license": "MIT",
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
"bgutils-js": "^3.2.0",
"chokidar": "~4.0.3",
"electron-is-dev": "~2.0.0",
"electron-serve": "~1.1.0",
"electron-unhandled": "^4.0.1",
"electron-updater": "^6.3.4",
"electron-window-state": "^5.0.3",
"jsdom": "^26.0.0",
"jsonfile": "^6.1.0"
},
"devDependencies": {
@@ -25,6 +27,25 @@
"typescript": "^5.7.3"
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.1.tgz",
"integrity": "sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA==",
"license": "MIT",
"dependencies": {
"@csstools/css-calc": "^2.1.2",
"@csstools/css-color-parser": "^3.0.8",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"lru-cache": "^10.4.3"
}
},
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
"node_modules/@capacitor-community/electron": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@capacitor-community/electron/-/electron-5.0.1.tgz",
@@ -82,6 +103,116 @@
"tslib": "^2.1.0"
}
},
"node_modules/@csstools/color-helpers": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
"integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT-0",
"engines": {
"node": ">=18"
}
},
"node_modules/@csstools/css-calc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.2.tgz",
"integrity": "sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3"
}
},
"node_modules/@csstools/css-color-parser": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.8.tgz",
"integrity": "sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"dependencies": {
"@csstools/color-helpers": "^5.0.2",
"@csstools/css-calc": "^2.1.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz",
"integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^3.0.3"
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz",
"integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@develar/schema-utils": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@@ -1317,7 +1448,6 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/at-least-node": {
@@ -1352,6 +1482,15 @@
}
]
},
"node_modules/bgutils-js": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/bgutils-js/-/bgutils-js-3.2.0.tgz",
"integrity": "sha512-CacO15JvxbclbLeCAAm9DETGlLuisRGWpPigoRvNsccSCPEC4pwYwA2g2x/pv7Om/sk79d4ib35V5HHmxPBpDg==",
"funding": [
"https://github.com/sponsors/LuanRT"
],
"license": "MIT"
},
"node_modules/big-integer": {
"version": "1.6.52",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
@@ -1911,7 +2050,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
@@ -2095,6 +2233,32 @@
"node": ">= 8"
}
},
"node_modules/cssstyle": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.0.tgz",
"integrity": "sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ==",
"license": "MIT",
"dependencies": {
"@asamuzakjp/css-color": "^3.1.1",
"rrweb-cssom": "^0.8.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/data-urls": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
"license": "MIT",
"dependencies": {
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/debug": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
@@ -2111,6 +2275,12 @@
}
}
},
"node_modules/decimal.js": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
"integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
"license": "MIT"
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@@ -2206,7 +2376,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@@ -2664,6 +2833,18 @@
"node": ">=8"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
@@ -2864,7 +3045,6 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@@ -3193,6 +3373,18 @@
"node": ">=10"
}
},
"node_modules/html-encoding-sniffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
"license": "MIT",
"dependencies": {
"whatwg-encoding": "^3.1.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@@ -3270,7 +3462,6 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
@@ -3407,6 +3598,12 @@
"integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==",
"dev": true
},
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"license": "MIT"
},
"node_modules/is-unicode-supported": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@@ -3507,6 +3704,81 @@
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
"dev": true
},
"node_modules/jsdom": {
"version": "26.0.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.0.0.tgz",
"integrity": "sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==",
"license": "MIT",
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
"decimal.js": "^10.4.3",
"form-data": "^4.0.1",
"html-encoding-sniffer": "^4.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.16",
"parse5": "^7.2.1",
"rrweb-cssom": "^0.8.0",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^5.0.0",
"w3c-xmlserializer": "^5.0.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^3.1.1",
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.1.0",
"ws": "^8.18.0",
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"canvas": "^3.0.0"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/jsdom/node_modules/agent-base": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/jsdom/node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/jsdom/node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -4133,6 +4405,12 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
"node_modules/nwsapi": {
"version": "2.2.20",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
"integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==",
"license": "MIT"
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
@@ -4251,6 +4529,18 @@
"dev": true,
"license": "BlueOak-1.0.0"
},
"node_modules/parse5": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
"integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
"license": "MIT",
"dependencies": {
"entities": "^4.5.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -4399,7 +4689,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -4639,6 +4928,12 @@
"node": ">=8.0"
}
},
"node_modules/rrweb-cssom": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
"license": "MIT"
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -4661,8 +4956,7 @@
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sanitize-filename": {
"version": "1.6.3",
@@ -4679,6 +4973,18 @@
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
"license": "ISC",
"dependencies": {
"xmlchars": "^2.2.0"
},
"engines": {
"node": ">=v12.22.7"
}
},
"node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
@@ -4954,6 +5260,12 @@
"node": ">=8"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"license": "MIT"
},
"node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
@@ -5046,6 +5358,24 @@
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="
},
"node_modules/tldts": {
"version": "6.1.85",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.85.tgz",
"integrity": "sha512-gBdZ1RjCSevRPFix/hpaUWeak2/RNUZB4/8frF1r5uYMHjFptkiT0JXIebWvgI/0ZHXvxaUDDJshiA0j6GdL3w==",
"license": "MIT",
"dependencies": {
"tldts-core": "^6.1.85"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "6.1.85",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.85.tgz",
"integrity": "sha512-DTjUVvxckL1fIoPSb3KE7ISNtkWSawZdpfxGxwiIrZoO6EbHVDXXUIlIuWympPaeS+BLGyggozX/HTMsRAdsoA==",
"license": "MIT"
},
"node_modules/tmp": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
@@ -5066,6 +5396,30 @@
"tmp": "^0.2.0"
}
},
"node_modules/tough-cookie": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
"license": "BSD-3-Clause",
"dependencies": {
"tldts": "^6.1.32"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tr46": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.0.tgz",
"integrity": "sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -5196,6 +5550,18 @@
"node": ">=0.6.0"
}
},
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
"license": "MIT",
"dependencies": {
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
@@ -5204,6 +5570,49 @@
"defaults": "^1.0.3"
}
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"license": "MIT",
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -5268,6 +5677,36 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"node_modules/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
}
},
"node_modules/xml2js": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
@@ -5296,6 +5735,12 @@
"node": ">=8.0"
}
},
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"license": "MIT"
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.7.14",
"version": "1.7.15",
"description": "Modern material design for Invidious.",
"author": {
"name": "Ward Pearce",
@@ -23,12 +23,14 @@
},
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
"bgutils-js": "^3.2.0",
"chokidar": "~4.0.3",
"electron-is-dev": "~2.0.0",
"electron-serve": "~1.1.0",
"electron-unhandled": "^4.0.1",
"electron-updater": "^6.3.4",
"electron-window-state": "^5.0.3",
"jsdom": "^26.0.0",
"jsonfile": "^6.1.0"
},
"devDependencies": {
+71 -1
View File
@@ -1,11 +1,13 @@
import type { CapacitorElectronConfig } from '@capacitor-community/electron';
import { getCapacitorElectronConfig, setupElectronDeepLinking } from '@capacitor-community/electron';
import type { MenuItemConstructorOptions } from 'electron';
import { app, MenuItem } from 'electron';
import { app, ipcMain, MenuItem } from 'electron';
import electronIsDev from 'electron-is-dev';
import unhandled from 'electron-unhandled';
import { autoUpdater } from 'electron-updater';
import { JSDOM } from 'jsdom';
import BG, { buildURL, GOOG_API_KEY, USER_AGENT, WebPoSignalOutput } from 'bgutils-js';
import { ElectronCapacitorApp, setupContentSecurityPolicy, setupReloadWatcher } from './setup';
// Graceful handling of unhandled errors.
@@ -68,3 +70,71 @@ app.on('activate', async function () {
});
// Place all ipc or other electron api calls and custom functionality under this line
ipcMain.handle('generatePoToken', async (
_,
challengeResponse: any,
requestKey: string,
visitorData: string,
videoId: string
) => {
if (!challengeResponse.bg_challenge)
throw new Error('Yt.js: Could not get challenge');
const dom = new JSDOM('<!DOCTYPE html><html lang="en"><head><title></title></head><body></body></html>', {
url: 'https://www.youtube.com/',
referrer: 'https://www.youtube.com/',
USER_AGENT
});
Object.assign(globalThis, {
window: dom.window,
document: dom.window.document,
location: dom.window.location,
origin: dom.window.origin
});
if (!Reflect.has(globalThis, 'navigator')) {
Object.defineProperty(globalThis, 'navigator', { value: dom.window.navigator });
}
const interpreterUrl = challengeResponse.bg_challenge.interpreter_url.private_do_not_access_or_else_trusted_resource_url_wrapped_value;
const bgScriptResponse = await fetch(`https:${interpreterUrl}`);
const interpreterJavascript = await bgScriptResponse.text();
if (interpreterJavascript) {
new Function(interpreterJavascript)();
} else throw new Error('Could not load VM');
const botguardClient = await BG.BotGuardClient.create({
program: challengeResponse.bg_challenge.program,
globalName: challengeResponse.bg_challenge.global_name,
globalObj: globalThis
});
const webPoSignalOutput: WebPoSignalOutput = [];
const botguardResponse = await botguardClient.snapshot({ webPoSignalOutput });
const integrityTokenResponse = await fetch(buildURL('GenerateIT', true), {
method: 'POST',
headers: {
'content-type': 'application/json+protobuf',
'x-goog-api-key': GOOG_API_KEY,
'x-user-agent': 'grpc-web-javascript/0.1',
},
body: JSON.stringify([requestKey, botguardResponse])
});
const integrityTokenResponseData = await integrityTokenResponse.json();
if (typeof integrityTokenResponseData[0] !== 'string')
throw new Error('Yt.js: Could not get integrity token');
const integrityToken = integrityTokenResponseData[0];
const integrityTokenBasedMinter = await BG.WebPoMinter.create({ integrityToken }, webPoSignalOutput);
return [
await integrityTokenBasedMinter.mintAsWebsafeString(visitorData),
await integrityTokenBasedMinter.mintAsWebsafeString(videoId)
];
});
+14
View File
@@ -1 +1,15 @@
const { contextBridge, ipcRenderer } = require('electron');
require('./rt/electron-rt');
contextBridge.exposeInMainWorld('electronAPI', {
generatePoToken: (challengeResponse: any,
requestKey: string,
visitorData: string,
videoId: string) => ipcRenderer.invoke(
'generatePoToken',
challengeResponse,
requestKey,
visitorData,
videoId
)
});
+11
View File
@@ -4,6 +4,7 @@ import {
CapElectronEventEmitter,
setupCapacitorElectronPlugins,
} from '@capacitor-community/electron';
import { USER_AGENT } from 'bgutils-js';
import chokidar from 'chokidar';
import type { MenuItemConstructorOptions } from 'electron';
import { app, BrowserWindow, Menu, MenuItem, nativeImage, session, Tray } from 'electron';
@@ -226,4 +227,14 @@ export function setupContentSecurityPolicy(customScheme: string): void {
},
});
});
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
const uri = new URL(details.url);
details.requestHeaders['User-Agent'] = USER_AGENT;
details.requestHeaders['origin'] = uri.origin;
details.requestHeaders['host'] = uri.host;
callback({ requestHeaders: details.requestHeaders });
});
}
+827 -1345
View File
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.7.14",
"version": "1.7.15",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -9,7 +9,8 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
"format": "prettier --write .",
"postinstall": "patch-package"
},
"devDependencies": {
"@capacitor/assets": "^3.0.5",
@@ -29,7 +30,7 @@
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.45.1",
"jsdom": "^25.0.1",
"patch-package": "^8.0.0",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.7",
"svelte": "^5.19.7",
@@ -51,12 +52,11 @@
"@capacitor/status-bar": "^6.0.1",
"@capgo/inappbrowser": "^6.6.8",
"@hugotomazi/capacitor-navigation-bar": "^4.0.1",
"@mediagrid/capacitor-native-audio": "^1.0.0",
"beercss": "^3.8.0",
"bgutils-js": "^3.1.3",
"bgutils-js": "^3.2.0",
"capacitor-nodejs": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz",
"dashjs": "^4.7.4",
"fuse.js": "^7.0.0",
"googlevideo": "^2.0.0",
"he": "^1.2.0",
"human-number": "^2.0.4",
"iso-3166": "^4.3.0",
@@ -66,12 +66,12 @@
"media-icons": "^0.10.0",
"mousetrap": "^1.6.5",
"peerjs": "^1.5.4",
"shaka-player": "^4.13.0",
"sponsorblock-api": "^0.2.4",
"svelte-i18n": "^4.0.1",
"svelte-infinite-loading": "^1.4.0",
"svelte-persisted-store": "^0.12.0",
"terser": "^5.34.1",
"vidstack": "^1.12.12",
"youtubei.js": "^12.2.0"
"youtubei.js": "^13.3.0"
}
}
}
@@ -0,0 +1,38 @@
diff --git a/node_modules/shaka-player/dist/shaka-player.compiled.d.ts b/node_modules/shaka-player/dist/shaka-player.compiled.d.ts
index 0971e58..9d5c515 100644
--- a/node_modules/shaka-player/dist/shaka-player.compiled.d.ts
+++ b/node_modules/shaka-player/dist/shaka-player.compiled.d.ts
@@ -5266,3 +5266,5 @@ declare namespace shaka.extern {
declare namespace shaka.extern {
type TransmuxerPlugin = ( ) => shaka.extern.Transmuxer ;
}
+
+export default shaka;
diff --git a/node_modules/shaka-player/dist/shaka-player.ui.d.ts b/node_modules/shaka-player/dist/shaka-player.ui.d.ts
index 460e009..41bab00 100644
--- a/node_modules/shaka-player/dist/shaka-player.ui.d.ts
+++ b/node_modules/shaka-player/dist/shaka-player.ui.d.ts
@@ -6011,3 +6011,5 @@ declare namespace shaka.extern {
declare namespace shaka.ui.Watermark {
type Options = { alpha : number , color : string , displayDuration : number , interval : number , position : string , size : number , skip : number , text : string , transitionDuration : number , type : string } ;
}
+
+export default shaka;
diff --git a/node_modules/shaka-player/index.d.ts b/node_modules/shaka-player/index.d.ts
new file mode 100644
index 0000000..79f4829
--- /dev/null
+++ b/node_modules/shaka-player/index.d.ts
@@ -0,0 +1,2 @@
+<reference path="./dist/shaka-player.compiled.d.ts" />
+<reference path="./dist/shaka-player.ui.d.ts" />
\ No newline at end of file
diff --git a/node_modules/shaka-player/ui.d.ts b/node_modules/shaka-player/ui.d.ts
new file mode 100644
index 0000000..84a3be0
--- /dev/null
+++ b/node_modules/shaka-player/ui.d.ts
@@ -0,0 +1,3 @@
+import shaka from 'shaka-player/dist/shaka-player.ui'
+export * from 'shaka-player/dist/shaka-player.ui'
+export default shaka;
+1 -1
View File
@@ -1 +1 @@
declare module 'virtual:pwa-info';
declare module 'virtual:pwa-info';
+13
View File
@@ -1,4 +1,7 @@
// See https://kit.svelte.dev/docs/types#app
import type { IGetChallengeResponse } from "youtubei.js";
// for information about these interfaces
declare global {
namespace App {
@@ -8,6 +11,16 @@ declare global {
// interface PageState {}
// interface Platform {}
}
interface Window {
electronAPI: {
generatePoToken: (
challengeResponse: IGetChallengeResponse,
requestKey: string,
visitorData: string,
videoId: string
) => Promise<[string, string]>;
};
}
}
export { };
@@ -4,53 +4,49 @@ import { Capacitor } from "@capacitor/core";
if (Capacitor.getPlatform() === 'android') {
const originalFetch = window.fetch;
function needsProxying(target: string): boolean {
if (!target.startsWith('http')) return false;
return true;
}
const corsProxyUrl: string = 'http://localhost:3000/';
window.fetch = async (requestInput: string | URL | Request, requestOptions?: RequestInit): Promise<Response> => {
let requestUrl: string;
const uri = requestInput instanceof Request ? requestInput.url : requestInput.toString();
if (typeof requestInput === 'string') {
requestUrl = requestInput;
} else if ('url' in requestInput) {
requestUrl = requestInput.url;
requestOptions = {
body: requestInput.body,
cache: requestInput.cache,
credentials: requestInput.credentials,
headers: requestInput.headers,
method: requestInput.method,
mode: requestInput.mode,
redirect: requestInput.redirect,
referrer: requestInput.referrer,
signal: requestInput.signal,
};
if (requestOptions.body && requestOptions.body instanceof ReadableStream) {
(requestOptions as RequestInit & { duplex?: 'half' | 'full'; }).duplex = 'half';
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;
}
} else {
requestUrl = requestInput.toString();
}
if (!requestUrl.startsWith(corsProxyUrl) && !requestUrl.startsWith('/') && !requestUrl.startsWith('blob:')) {
requestInput = corsProxyUrl + requestUrl;
}
// Use the original fetch with the proxied URL and options
return originalFetch(requestInput, requestOptions);
};
const currentOrigin: string = window.location.protocol + '//' + window.location.host;
const originalXhrOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (...args: any[]): void {
const targetOriginMatch = /^https?:\/\/([^\/]+)/i.exec(args[1]);
if (targetOriginMatch && targetOriginMatch[0].toLowerCase() !== currentOrigin) {
if (needsProxying(args[1])) {
args[1] = corsProxyUrl + args[1];
}
/* @ts-ignore */
return originalXhrOpen.apply(this, args);
};
setTimeout(() => goto('/', { replaceState: true }), 10);
setTimeout(() => goto('/', { replaceState: true }), 100);
}
@@ -1,217 +0,0 @@
import type { HttpResponse } from '@capacitor/core';
import { CapacitorHttp } from '@capacitor/core';
import type { CapFormDataEntry, WindowCapacitor } from '@capacitor/core/types/definitions-internal';
// Rip from https://github.com/ionic-team/capacitor/blob/912d532cf6c7424d180b185120e7a9ba9c1ae050/core/native-bridge.ts
// with some modifications.
const CAPACITOR_HTTP_INTERCEPTOR = '/_capacitor_http_interceptor_';
const CAPACITOR_HTTP_INTERCEPTOR_URL_PARAM = 'u';
const isRelativeOrProxyUrl = (url: string | undefined): boolean =>
!url ||
!(url.startsWith('http:') || url.startsWith('https:')) ||
url.indexOf(CAPACITOR_HTTP_INTERCEPTOR) > -1;
const createProxyUrl = (url: string, win: WindowCapacitor): string => {
if (isRelativeOrProxyUrl(url)) return url;
const bridgeUrl = new URL(win.Capacitor?.getServerUrl() ?? '');
bridgeUrl.pathname = CAPACITOR_HTTP_INTERCEPTOR;
bridgeUrl.searchParams.append(CAPACITOR_HTTP_INTERCEPTOR_URL_PARAM, url);
return bridgeUrl.toString();
};
const readFileAsBase64 = (file: File): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const data = reader.result as string;
resolve(btoa(data));
};
reader.onerror = reject;
reader.readAsBinaryString(file);
});
const convertFormData = async (formData: FormData): Promise<any> => {
const newFormData: CapFormDataEntry[] = [];
for (const pair of formData.entries()) {
const [key, value] = pair;
if (value instanceof File) {
const base64File = await readFileAsBase64(value);
newFormData.push({
key,
value: base64File,
type: 'base64File',
contentType: value.type,
fileName: value.name,
});
} else {
newFormData.push({ key, value, type: 'string' });
}
}
return newFormData;
};
const convertBody = async (
body: Document | XMLHttpRequestBodyInit | ReadableStream<any> | undefined,
contentType?: string,
): Promise<any> => {
if (body instanceof ReadableStream || body instanceof Uint8Array) {
let encodedData;
if (body instanceof ReadableStream) {
const reader = body.getReader();
const chunks: any[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const concatenated = new Uint8Array(
chunks.reduce((acc, chunk) => acc + chunk.length, 0),
);
let position = 0;
for (const chunk of chunks) {
concatenated.set(chunk, position);
position += chunk.length;
}
encodedData = concatenated;
} else {
encodedData = body;
}
let data = new TextDecoder().decode(encodedData);
let type;
if (contentType === 'application/json') {
try {
data = JSON.parse(data);
} catch (ignored) {
// ignore
}
type = 'json';
} else if (contentType === 'multipart/form-data') {
type = 'formData';
} else if (contentType?.startsWith('image')) {
type = 'image';
} else if (contentType === 'application/octet-stream') {
type = 'binary';
} else {
type = 'text';
}
return {
data,
type,
headers: { 'Content-Type': contentType || 'application/octet-stream' },
};
} else if (body instanceof URLSearchParams) {
return {
data: body.toString(),
type: 'text',
};
} else if (body instanceof FormData) {
const formData = await convertFormData(body);
return {
data: formData,
type: 'formData',
};
} else if (body instanceof File) {
const fileData = await readFileAsBase64(body);
return {
data: fileData,
type: 'file',
headers: { 'Content-Type': body.type },
};
}
return { data: body, type: 'json' };
};
export const capacitorFetch = async (
resource: RequestInfo | URL,
options?: RequestInit,
) => {
const request = new Request(resource, options);
const { method } = request;
if (
method.toLocaleUpperCase() === 'GET' ||
method.toLocaleUpperCase() === 'HEAD' ||
method.toLocaleUpperCase() === 'OPTIONS' ||
method.toLocaleUpperCase() === 'TRACE'
) {
if (typeof resource === 'string') {
return await fetch(
createProxyUrl(resource, window as WindowCapacitor),
options,
);
} else if (resource instanceof Request) {
const modifiedRequest = new Request(
createProxyUrl(resource.url, window as WindowCapacitor),
resource,
);
return await fetch(modifiedRequest, options);
}
}
const tag = `CapacitorHttp fetch ${Date.now()} ${resource}`;
console.time(tag);
try {
const { body } = request;
const optionHeaders = Object.fromEntries(request.headers.entries());
const {
data: requestData,
type,
headers,
} = await convertBody(
options?.body || body || undefined,
optionHeaders['Content-Type'] || optionHeaders['content-type'],
);
const nativeResponse: HttpResponse = await CapacitorHttp.request({
url: request.url,
method: method,
data: requestData,
dataType: type,
headers: {
...headers,
...optionHeaders,
},
});
const contentType =
nativeResponse.headers['Content-Type'] ||
nativeResponse.headers['content-type'];
let data = contentType?.startsWith('application/json')
? JSON.stringify(nativeResponse.data)
: nativeResponse.data;
// use null data for 204 No Content HTTP response
if (nativeResponse.status === 204) {
data = null;
}
// intercept & parse response before returning
const response = new Response(data, {
headers: nativeResponse.headers,
status: nativeResponse.status,
});
/*
* copy url to response, `cordova-plugin-ionic` uses this url from the response
* we need `Object.defineProperty` because url is an inherited getter on the Response
* see: https://stackoverflow.com/a/57382543
* */
Object.defineProperty(response, 'url', {
value: nativeResponse.url,
});
console.timeEnd(tag);
return response;
} catch (error) {
console.timeEnd(tag);
return Promise.reject(error);
}
};
@@ -0,0 +1,53 @@
import { BG, buildURL, GOOG_API_KEY, type WebPoSignalOutput } from 'bgutils-js';
import { type IGetChallengeResponse } from 'youtubei.js';
export async function androidPoTokenMinter(
challengeResponse: IGetChallengeResponse,
requestKey: string,
visitorData: string,
videoId: string
): Promise<[string, string]> {
if (!challengeResponse.bg_challenge)
throw new Error('Yt.js: Could not get challenge');
const interpreterUrl = challengeResponse.bg_challenge.interpreter_url.private_do_not_access_or_else_trusted_resource_url_wrapped_value;
const bgScriptResponse = await fetch(`https:${interpreterUrl}`);
const interpreterJavascript = await bgScriptResponse.text();
if (interpreterJavascript) {
new Function(interpreterJavascript)();
} else throw new Error('Could not load VM');
const botguardClient = await BG.BotGuardClient.create({
program: challengeResponse.bg_challenge.program,
globalName: challengeResponse.bg_challenge.global_name,
globalObj: globalThis
});
const webPoSignalOutput: WebPoSignalOutput = [];
const botguardResponse = await botguardClient.snapshot({ webPoSignalOutput });
const integrityTokenResponse = await fetch(buildURL('GenerateIT', true), {
method: 'POST',
headers: {
'content-type': 'application/json+protobuf',
'x-goog-api-key': GOOG_API_KEY,
'x-user-agent': 'grpc-web-javascript/0.1',
},
body: JSON.stringify([requestKey, botguardResponse])
});
const integrityTokenResponseData = await integrityTokenResponse.json();
if (typeof integrityTokenResponseData[0] !== 'string')
throw new Error('Yt.js: Could not get integrity token');
const integrityToken = integrityTokenResponseData[0];
const integrityTokenBasedMinter = await BG.WebPoMinter.create({ integrityToken }, webPoSignalOutput);
return [
await integrityTokenBasedMinter.mintAsWebsafeString(visitorData),
await integrityTokenBasedMinter.mintAsWebsafeString(videoId)
];
}
+6 -3
View File
@@ -79,10 +79,13 @@ export async function getResolveUrl(url: string): Promise<ResolvedUrl> {
}
export async function getVideo(videoId: string, local: boolean = false, fetchOptions?: RequestInit): Promise<VideoPlay> {
if (get(playerYouTubeJsAlways) && Capacitor.isNativePlatform()) {
return await patchYoutubeJs(videoId);
}
const resp = await fetch(setRegion(buildPath(`videos/${videoId}?local=${local}`)), fetchOptions);
if (
(get(playerYouTubeJsAlways) && Capacitor.isNativePlatform()) ||
(!resp.ok && get(playerYouTubeJsFallback) && Capacitor.isNativePlatform())) {
if (!resp.ok && get(playerYouTubeJsFallback) && Capacitor.isNativePlatform()) {
return await patchYoutubeJs(videoId);
} else {
await fetchErrorHandle(resp);
+4
View File
@@ -1,3 +1,6 @@
import type { YT } from 'youtubei.js';
export interface Image {
url: string;
width: number;
@@ -98,6 +101,7 @@ export interface VideoPlay extends Video {
authorThumbnails: Image[];
captions: Captions[];
storyboards?: StoryBoard[];
ytJsVideoInfo?: YT.VideoInfo,
fallbackPatch?: 'youtubejs' | 'piped';
}
+231 -365
View File
@@ -1,6 +1,4 @@
<script lang="ts">
import 'vidstack/bundle';
import { page } from '$app/stores';
import { getBestThumbnail } from '$lib/images';
import { padTime, videoLength } from '$lib/time';
@@ -8,119 +6,65 @@
import { ScreenOrientation, type ScreenOrientationResult } from '@capacitor/screen-orientation';
import { StatusBar, Style } from '@capacitor/status-bar';
import { NavigationBar } from '@hugotomazi/capacitor-navigation-bar';
import { AudioPlayer } from '@mediagrid/capacitor-native-audio';
import type { Page } from '@sveltejs/kit';
import { type Page } from '@sveltejs/kit';
import { GoogleVideo, Protos } from 'googlevideo';
import 'shaka-player/dist/controls.css';
import shaka from 'shaka-player/dist/shaka-player.ui';
import { SponsorBlock, type Category, type Segment } from 'sponsorblock-api';
import { onDestroy, onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { get } from 'svelte/store';
import type { FullscreenChangeEvent, MediaTimeUpdateEvent, PlayerSrc } from 'vidstack';
import type { MediaPlayerElement } from 'vidstack/elements';
import { deleteVideoProgress, getVideoProgress, saveVideoProgress } from '../api';
import type { VideoPlay } from '../api/model';
import { proxyVideoUrl, pullBitratePreference, type PhasedDescription } from '../misc';
import { type PhasedDescription } from '../misc';
import {
authStore,
instanceStore,
playerAlwaysLoopStore,
playerAndroidBgPlayer,
playerAndroidLockOrientation,
playerAutoPlayStore,
playerDefaultLanguage,
playerProxyVideosStore,
playerSavePlaybackPositionStore,
silenceSkipperStore,
poTokenCacheStore,
sponsorBlockCategoriesStore,
sponsorBlockDisplayToastStore,
sponsorBlockStore,
sponsorBlockTimelineStore,
sponsorBlockUrlStore,
synciousInstanceStore,
synciousStore
} from '../store';
import { getDynamicTheme, setStatusBarColor } from '../theme';
import { setStatusBarColor } from '../theme';
interface Props {
data: { video: VideoPlay; content: PhasedDescription; playlistId: string | null };
audioMode?: boolean;
player: MediaPlayerElement;
isSyncing?: boolean;
isEmbed?: boolean;
segments?: Segment[];
playerElement: HTMLMediaElement;
}
let {
data,
audioMode = false,
player = $bindable(),
isSyncing = false,
isEmbed = false,
segments = $bindable([])
segments = $bindable([]),
playerElement = $bindable()
}: Props = $props();
let src: PlayerSrc = $state([]);
let snackBarAlert = $state('');
let playerIsLive = $state(false);
let playerPosSet = false;
let userVideoSpeed = 1;
let silenceIsFastForwarding = false;
let silenceSkipperInterval: NodeJS.Timeout;
let originalOrigination: ScreenOrientationResult | undefined;
let sponsorBlockElements: Element[] = [];
let watchProgressTimeout: NodeJS.Timeout;
function setSponsorTimeline() {
if (get(sponsorBlockTimelineStore)) return;
if (segments.length === 0) return;
const timeline = document.getElementsByClassName('vds-time-slider')[0];
if (sponsorBlockElements.length > 0) {
sponsorBlockElements.forEach((barDiv) => {
if (timeline.contains(barDiv)) {
timeline.removeChild(barDiv);
}
});
}
const segmentColors = {
sponsor: '00d400',
selfpromo: 'ffff00',
interaction: 'cc00ff',
intro: '00ffff',
outro: '0202ed',
preview: '008fd6',
music_offtopic: 'ff9900',
filler: '7300FF'
};
segments.forEach((segment) => {
const startPercent = (segment.startTime / data.video.lengthSeconds) * 100;
const endPercent = (segment.endTime / data.video.lengthSeconds) * 100;
const widthPercent = endPercent - startPercent;
const barDiv = document.createElement('div');
barDiv.classList.add('sponsorskip-bar');
barDiv.style.left = `${startPercent}%`;
barDiv.style.width = `${widthPercent}%`;
barDiv.style.backgroundColor =
segment.category in segmentColors ? `#${segmentColors[segment.category]}` : 'grey';
timeline.appendChild(barDiv);
sponsorBlockElements.push(barDiv);
});
}
let player: shaka.Player;
let shakaUi: shaka.ui.Overlay;
function loadTimeFromUrl(page: Page): boolean {
if (player) {
const timeGivenUrl = page.url.searchParams.get('time');
if (timeGivenUrl && !isNaN(parseFloat(timeGivenUrl))) {
player.currentTime = Number(timeGivenUrl);
playerElement.currentTime = Number(timeGivenUrl);
return true;
}
}
@@ -130,88 +74,198 @@
page.subscribe((pageUpdate) => loadTimeFromUrl(pageUpdate));
let initStoreSilence = true;
silenceSkipperStore.subscribe((value) => {
if (initStoreSilence) {
initStoreSilence = false;
return;
}
if (typeof silenceSkipperInterval !== 'undefined') {
clearInterval(silenceSkipperInterval);
}
if (value) {
initSilenceSkipper();
}
});
const proxyVideos = get(playerProxyVideosStore);
function fastForwardToSound(
videoElement: HTMLAudioElement,
threshold: number = 0.01,
checkInterval: number = 100,
fastForwardRate: number = 4
): void {
if (!(videoElement instanceof HTMLMediaElement)) {
console.error('Provided element is not a valid HTMLMediaElement');
onMount(async () => {
shaka.polyfill.installAll();
if (!shaka.Player.isBrowserSupported()) {
return;
}
const audioContext = new AudioContext();
const source = audioContext.createMediaElementSource(videoElement);
const analyser = audioContext.createAnalyser();
player = new shaka.Player();
playerElement = document.getElementById('player') as HTMLMediaElement;
source.connect(analyser);
analyser.connect(audioContext.destination);
analyser.fftSize = 256;
await player.attach(playerElement);
shakaUi = new shaka.ui.Overlay(
player,
document.getElementById('shaka-container') as HTMLElement,
playerElement
);
const dataArray = new Uint8Array(analyser.frequencyBinCount);
shakaUi.configure({
controlPanelElements: [
'play_pause',
'spacer',
Capacitor.getPlatform() === 'electron' ? 'volume' : '',
'chapter',
'time_and_duration',
'overflow_menu'
],
overflowMenuButtons: ['cast', 'airplay', 'captions', 'quality', 'loop', 'language']
});
const checkForSound = () => {
analyser.getByteFrequencyData(dataArray);
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
},
const averageVolume = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;
const volume = averageVolume / 255;
if (volume < threshold) {
if (!silenceIsFastForwarding) {
videoElement.playbackRate = fastForwardRate;
silenceIsFastForwarding = 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
}
} else {
if (silenceIsFastForwarding) {
videoElement.playbackRate = userVideoSpeed;
silenceIsFastForwarding = false;
},
preferredDecodingAttributes: !data.video.hlsUrl ? ['smooth', 'powerEfficient'] : [],
autoShowText: shaka.config.AutoShowText.NEVER
});
if (data.video.fallbackPatch === 'youtubejs') {
const networkingEngine = player.getNetworkingEngine();
if (!networkingEngine) return;
// 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
networkingEngine.registerRequestFilter(async (type, request) => {
if (type === shaka.net.NetworkingEngine.RequestType.SEGMENT) {
const url = new URL(request.uris[0]);
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 cachedPoToken = get(poTokenCacheStore);
if (cachedPoToken) url.searchParams.set('pot', cachedPoToken);
delete request.headers.Range;
}
request.method = 'POST';
request.body = new Uint8Array([120, 0]);
}
request.uris[0] = url.toString();
}
}
};
});
silenceSkipperInterval = setInterval(checkForSound, checkInterval);
}
networkingEngine.registerResponseFilter(async (type, response) => {
let mediaData = new Uint8Array(0);
function initSilenceSkipper() {
const videoContainer = document.getElementById('video') as HTMLElement;
const videoElement = videoContainer.querySelector('video') as HTMLMediaElement;
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;
fastForwardToSound(videoElement);
}
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 googUmp = new GoogleVideo.UMP(
new GoogleVideo.ChunkedDataBuffer([new Uint8Array(response.data as ArrayBuffer)])
);
let redirect: Protos.SabrRedirect | undefined;
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;
}
}
} catch (error) {
console.error('An error occurred while processing the part:', error);
}
});
if (redirect) return handleRedirect(redirect);
if (mediaData.length) response.data = mediaData;
}
});
}
onMount(async () => {
if (!data.video.hlsUrl) {
playerIsLive = false;
let dashUrl = data.video.dashUrl;
if (!data.video.fallbackPatch && (!Capacitor.isNativePlatform() || proxyVideos)) {
dashUrl += '?local=true';
}
await player.load(dashUrl);
if (data.video.captions) {
data.video.captions.forEach(async (caption) => {
player.textTracks.add({
label: caption.label,
kind: 'captions',
language: caption.language_code,
// Need if captions are generated when youtube.js is being used.
src: caption.url.startsWith('http')
? caption.url
: `${get(instanceStore)}${caption.url}`
});
player.addTextTrackAsync(
caption.url.startsWith('http') ? caption.url : `${get(instanceStore)}${caption.url}`,
caption.language_code,
'captions',
undefined,
undefined,
caption.label
);
});
}
@@ -233,77 +287,16 @@
});
if (timestampIndex > 0) {
player.textTracks.add({
kind: 'chapters',
src: URL.createObjectURL(new Blob([chapterWebVTT])),
default: true
});
player.addChaptersTrack(
URL.createObjectURL(new Blob([chapterWebVTT])),
get(playerDefaultLanguage)
);
}
}
// Auto save watch progress every minute.
watchProgressTimeout = setInterval(() => savePlayerPos(), 60000);
player.addEventListener('pause', () => {
savePlayerPos();
if (get(silenceSkipperStore)) {
clearInterval(silenceSkipperInterval);
}
});
player.addEventListener('end', () => {
savePlayerPos();
if (get(silenceSkipperStore)) {
clearInterval(silenceSkipperInterval);
}
});
player.addEventListener('play', () => {
if (get(silenceSkipperStore)) {
initSilenceSkipper();
}
});
player.addEventListener('seeked', () => {
if (get(silenceSkipperStore)) {
clearInterval(silenceSkipperInterval);
initSilenceSkipper();
}
});
player.addEventListener('rate-change', () => {
if (silenceIsFastForwarding) return;
userVideoSpeed = player.playbackRate;
});
player.addEventListener('provider-change', (event) => {
const provider = event.detail as any;
if (provider?.type === 'dash') {
const bitrate = pullBitratePreference();
const instanceDefaultBitrate = import.meta.env.VITE_DEFAULT_DASH_BITRATE
? Number(import.meta.env.VITE_DEFAULT_DASH_BITRATE)
: -1;
provider.library = () => import('dashjs');
provider.config = {
streaming: {
abr: {
ABRStrategy: 'abrBola',
movingAverageMethod: 'ewma',
autoSwitchBitrate: {
video: false,
audio: false
},
initialBitrate: {
video: bitrate === -1 ? instanceDefaultBitrate : bitrate,
audio: bitrate === -1 ? instanceDefaultBitrate : bitrate
}
}
}
};
}
});
if (get(sponsorBlockStore) && get(sponsorBlockCategoriesStore)) {
const currentCategories = get(sponsorBlockCategoriesStore);
@@ -318,22 +311,16 @@
get(sponsorBlockCategoriesStore) as Category[]
);
setSponsorTimeline();
addEventListener('resize', () => {
setSponsorTimeline();
});
player.addEventListener('time-update', (event: MediaTimeUpdateEvent) => {
playerElement.addEventListener('timeupdate', () => {
segments.forEach((segment) => {
if (
event.detail.currentTime >= segment.startTime &&
event.detail.currentTime <= segment.endTime
playerElement.currentTime >= segment.startTime &&
playerElement.currentTime <= segment.endTime
) {
if (Math.round(player.currentTime) >= Math.round(player.duration)) {
if (Math.round(playerElement.currentTime) >= Math.round(playerElement.duration)) {
return;
}
player.currentTime = segment.endTime + 1;
playerElement.currentTime = segment.endTime + 1;
if (!get(sponsorBlockDisplayToastStore)) {
snackBarAlert = `${get(_)('skipping')} ${segment.category}`;
ui('#snackbar-alert');
@@ -345,36 +332,16 @@
}
}
src = [{ src: data.video.dashUrl, type: 'application/dash+xml' }];
await loadPlayerPos();
if (!data.video.fallbackPatch) {
if (!Capacitor.isNativePlatform() || proxyVideos) {
(src[0] as { src: string }).src += '?local=true';
const defaultLanguage = get(playerDefaultLanguage);
if (defaultLanguage) {
const audioLanguages = player.getAudioLanguages();
if (audioLanguages.includes(defaultLanguage)) {
player.selectAudioLanguage(defaultLanguage);
}
}
player.addEventListener('dash-can-play', async () => {
await loadPlayerPos();
const defaultLanguage = get(playerDefaultLanguage);
if (defaultLanguage) {
let trackIndex = 0;
for (const track of player.audioTracks) {
console.log(player.audioTracks);
if (track.label.toLowerCase().includes(defaultLanguage)) {
player.remoteControl.changeAudioTrack(trackIndex);
break;
}
trackIndex++;
}
}
if (get(playerAutoPlayStore)) {
player.play();
player.exitFullscreen();
}
});
if (Capacitor.getPlatform() === 'android' && data.video.adaptiveFormats.length > 0) {
const videoFormats = data.video.adaptiveFormats.filter((format) =>
format.type.startsWith('video/')
@@ -382,8 +349,10 @@
originalOrigination = await ScreenOrientation.orientation();
player.addEventListener('fullscreen-change', async (event: FullscreenChangeEvent) => {
if (event.detail) {
playerElement.addEventListener('fullscreenchange', async () => {
const isFullScreen = document.fullscreenElement;
if (isFullScreen) {
// Ensure bar color is black while in fullscreen
await StatusBar.setBackgroundColor({ color: '#000000' });
await NavigationBar.setColor({
@@ -396,7 +365,7 @@
if (!get(playerAndroidLockOrientation)) return;
if (event.detail && videoFormats[0].resolution) {
if (isFullScreen && videoFormats[0].resolution) {
const widthHeight = videoFormats[0].resolution.split('x');
if (widthHeight.length !== 2) return;
@@ -419,105 +388,15 @@
});
}
});
if (data.video.fallbackPatch === undefined && get(playerAndroidBgPlayer)) {
const highestBitrateAudio = data.video.adaptiveFormats
.filter((format) => format.type.startsWith('audio/'))
.reduce((prev, current) => {
return parseInt(prev.bitrate) > parseInt(current.bitrate) ? prev : current;
});
const audioId = { audioId: data.video.videoId };
let isPlayingInBackground = false;
await AudioPlayer.create({
...audioId,
audioSource: !data.video.fallbackPatch
? proxyVideoUrl(highestBitrateAudio.url)
: highestBitrateAudio.url,
friendlyTitle: data.video.title,
useForNotification: true,
loop: player.loop,
isBackgroundMusic: false
});
AudioPlayer.onAppGainsFocus(audioId, async () => {
if (!isPlayingInBackground) return;
isPlayingInBackground = false;
await AudioPlayer.pause(audioId);
const audioPlayerTime = await AudioPlayer.getCurrentTime(audioId);
const audioPlayerTimeRounded = Math.round(audioPlayerTime.currentTime);
if (audioPlayerTimeRounded > player.currentTime) {
player.currentTime = audioPlayerTimeRounded;
}
await player.play();
});
AudioPlayer.onAppLosesFocus(audioId, async () => {
if (player.paused) return;
isPlayingInBackground = true;
await AudioPlayer.play(audioId);
await AudioPlayer.seek({
...audioId,
timeInSeconds: Math.round(player.currentTime)
});
});
await AudioPlayer.initialize(audioId);
}
}
} else {
playerIsLive = true;
src = [
{
src: data.video.hlsUrl + '?local=true',
type: 'application/x-mpegurl'
}
];
await player.load(data.video.hlsUrl + '?local=true');
}
player.storage = 'video-player';
const currentTheme = await getDynamicTheme();
if (data.video.fallbackPatch === 'youtubejs') {
snackBarAlert = get(_)('player.youtubeJsFallBack');
ui('#snackbar-alert');
}
document.documentElement.style.setProperty(
'--media-slider-track-fill-bg',
currentTheme['--primary']
);
document.documentElement.style.setProperty('--media-menu-bg', currentTheme['--background']);
document.documentElement.style.setProperty(
'--media-menu-top-bar-bg',
currentTheme['--surface']
);
document.documentElement.style.setProperty(
'--media-menu-text-color',
currentTheme['--on-background']
);
document.documentElement.style.setProperty(
'--media-menu-item-info-color',
currentTheme['--on-background']
);
document.documentElement.style.setProperty(
'--media-menu-section-bg',
currentTheme['--surface']
);
document.documentElement.style.setProperty(
'--media-menu-surface-container',
currentTheme['--surface']
);
document.documentElement.style.setProperty('--audio-bg', currentTheme['--surface']);
});
async function loadPlayerPos() {
@@ -543,20 +422,23 @@
}
}
if (toSetTime > 0) player.currentTime = toSetTime;
if (toSetTime > 0) playerElement.currentTime = toSetTime;
}
function savePlayerPos() {
if (data.video.hlsUrl) return;
if (get(playerSavePlaybackPositionStore) && player && player.currentTime) {
if (player.currentTime < player.duration - 10 && player.currentTime > 10) {
if (get(playerSavePlaybackPositionStore) && player && playerElement.currentTime) {
if (
playerElement.currentTime < playerElement.duration - 10 &&
playerElement.currentTime > 10
) {
try {
localStorage.setItem(`v_${data.video.videoId}`, player.currentTime.toString());
localStorage.setItem(`v_${data.video.videoId}`, playerElement.currentTime.toString());
} catch {}
if (get(synciousStore) && get(synciousInstanceStore) && get(authStore)) {
saveVideoProgress(data.video.videoId, player.currentTime);
saveVideoProgress(data.video.videoId, playerElement.currentTime);
}
} else {
try {
@@ -572,8 +454,6 @@
onDestroy(async () => {
if (Capacitor.getPlatform() === 'android') {
await AudioPlayer.destroy({ audioId: data.video.videoId });
if (originalOrigination) {
await StatusBar.setOverlaysWebView({ overlay: false });
await StatusBar.show();
@@ -582,17 +462,15 @@
});
}
}
if (typeof silenceSkipperInterval !== 'undefined') {
clearInterval(silenceSkipperInterval);
}
if (watchProgressTimeout) {
clearTimeout(watchProgressTimeout);
}
try {
savePlayerPos();
} catch (error) {}
await player.pause();
player.destroy();
await playerElement.pause();
await player.destroy();
await shakaUi.destroy();
playerPosSet = false;
});
</script>
@@ -601,31 +479,19 @@
<div style="margin-top: 40vh;"></div>
{/if}
<media-player
bind:this={player}
id="video"
autoPlay={$playerAutoPlayStore && !isSyncing}
loop={$playerAlwaysLoopStore}
title={data.video.title}
streamType={playerIsLive ? 'live' : 'on-demand'}
viewType={audioMode ? 'audio' : 'video'}
keep-alive
{src}
<div
id="shaka-container"
style="max-height: 80vh; max-width: calc(80vh * 16 / 9); overflow: hidden; position: relative; flex: 1; background-color: black;"
data-shaka-player-container
>
<media-provider>
{#if !audioMode}
<media-poster class="vds-poster" src={getBestThumbnail(data.video.videoThumbnails, 1251, 781)}
></media-poster>
{/if}
</media-provider>
<media-audio-layout></media-audio-layout>
{#if data.video.storyboards && data.video.storyboards.length > 3}
<media-video-layout thumbnails={`${get(instanceStore)}${data.video.storyboards[3].url}`}
></media-video-layout>
{:else}
<media-video-layout></media-video-layout>
{/if}
</media-player>
<video
controls={false}
autoplay={$playerAutoPlayStore}
id="player"
style="width: 100%; height: 100%; object-fit: contain;"
poster={getBestThumbnail(data.video.videoThumbnails, 1251, 781)}
></video>
</div>
{#if !isEmbed}
<div class="snackbar" id="snackbar-alert">
@@ -100,7 +100,7 @@
</script>
<form onsubmit={preventDefault(handleSubmit)}>
<div class="field prefix round fill no-margin search">
<div class="field prefix fill no-margin search">
<i class="front">search</i>
<input
id="search-box"
@@ -151,7 +151,13 @@
{/if}
{#if !suggestionsForSearch.length && $interfaceSearchHistoryEnabled}
{#each $searchHistoryStore as history}
<a class="row" href={`/search/${encodeURIComponent(history)}`}>
<a
class="row"
onclick={() => {
search = history;
}}
href={`/search/${encodeURIComponent(history)}`}
>
<div>{history}</div>
</a>
{/each}
@@ -6,7 +6,6 @@
import { get } from 'svelte/store';
import {
playerAlwaysLoopStore,
playerAndroidBgPlayer,
playerAndroidLockOrientation,
playerAutoPlayStore,
playerAutoplayNextByDefaultStore,
@@ -16,8 +15,7 @@
playerSavePlaybackPositionStore,
playerTheatreModeByDefaultStore,
playerYouTubeJsAlways,
playerYouTubeJsFallback,
silenceSkipperStore
playerYouTubeJsFallback
} from '../../store';
let defaultLanguage = $state(get(playerDefaultLanguage));
@@ -77,22 +75,6 @@
</div>
{#if Capacitor.getPlatform() === 'android'}
<div class="field no-margin">
<nav class="no-padding">
<div class="max">
<div>{$_('layout.player.backgroundPlay')}</div>
</div>
<label class="switch">
<input
type="checkbox"
bind:checked={$playerAndroidBgPlayer}
onclick={() => playerAndroidBgPlayer.set(!$playerAndroidBgPlayer)}
/>
<span></span>
</label>
</nav>
</div>
<div class="field no-margin">
<nav class="no-padding">
<div class="max">
@@ -192,22 +174,6 @@
</nav>
</div>
<div class="field no-margin">
<nav class="no-padding">
<div class="max">
<div>{$_('layout.player.silenceSkipper')}</div>
</div>
<label class="switch">
<input
type="checkbox"
bind:checked={$silenceSkipperStore}
onclick={() => silenceSkipperStore.set(!$silenceSkipperStore)}
/>
<span></span>
</label>
</nav>
</div>
{#if Capacitor.isNativePlatform()}
<div class="field no-margin">
<nav class="no-padding">
@@ -1,7 +1,9 @@
<script lang="ts">
import { instanceStore } from '$lib/store';
import { Clipboard } from '@capacitor/clipboard';
import { Capacitor } from '@capacitor/core';
import { _ } from 'svelte-i18n';
import { get } from 'svelte/store';
import type { Notification, PlaylistPageVideo, Video, VideoBase } from '../api/model';
interface Props {
@@ -11,28 +13,28 @@
let { video }: Props = $props();
</script>
{#if !Capacitor.isNativePlatform()}
<a
class="row"
href="#copy"
onclick={async () =>
await Clipboard.write({ string: `${location.origin}/watch/${video.videoId}` })}
>
<div class="min">{$_('player.share.materialiousLink')}</div></a
>
{/if}
<a
href="#copy"
<button
class="row"
onclick={async () => {
if (Capacitor.isNativePlatform()) {
await Clipboard.write({ string: `${get(instanceStore)}/watch/${video.videoId}` });
} else {
await Clipboard.write({ string: `${location.origin}/watch/${video.videoId}` });
}
}}
>
<div class="min">{$_('player.share.materialiousLink')}</div></button
>
<button
class="row"
onclick={async () =>
await Clipboard.write({ string: `https://redirect.invidious.io/watch?v=${video.videoId}` })}
>
<div class="min">{$_('player.share.invidiousRedirect')}</div></a
><a
<div class="min">{$_('player.share.invidiousRedirect')}</div></button
><button
class="row"
href="#copy"
onclick={async () =>
await Clipboard.write({ string: `https://www.youtube.com/watch?v=${video.videoId}` })}
>
<div class="min">{$_('player.share.youtubeLink')}</div></a
<div class="min">{$_('player.share.youtubeLink')}</div></button
>
@@ -4,18 +4,16 @@
import { VTTCue, parseText, type ParsedCaptionsResult } from 'media-captions';
import { _ } from 'svelte-i18n';
import { get } from 'svelte/store';
import type { MediaTimeUpdateEvent } from 'vidstack';
import type { MediaPlayerElement } from 'vidstack/elements';
import type { VideoPlay } from '../api/model';
import { decodeHtmlCharCodes } from '../misc';
import { instanceStore } from '../store';
interface Props {
video: VideoPlay;
player: MediaPlayerElement;
playerElement: HTMLMediaElement;
}
let { video, player = $bindable() }: Props = $props();
let { video, playerElement = $bindable() }: Props = $props();
let url: string | null = $state(null);
let autoScroll: boolean = $state(true);
@@ -26,8 +24,8 @@
let currentTime = $state(0);
let search: string = $state('');
player.addEventListener('time-update', (event: MediaTimeUpdateEvent) => {
currentTime = event.detail.currentTime;
playerElement.addEventListener('timeupdate', () => {
currentTime = playerElement.currentTime;
if (autoScroll) {
const currentTranscriptLine = document.querySelector(
@@ -115,7 +113,7 @@
<div
class="transcript-line"
id={`transcript-line-${cue.startTime}`}
onclick={() => (player.currentTime = cue.startTime)}
onclick={() => (playerElement.currentTime = cue.startTime)}
class:secondary-container={currentTime >= cue.startTime && currentTime <= cue.endTime}
>
<p class="chip no-margin">{videoLength(cue.startTime)}</p>
@@ -2,48 +2,6 @@
padding: 0.1em 0.4em;
}
.vds-chapter-radio-label {
word-wrap: break-word !important;
overflow-wrap: break-word !important;
white-space: normal !important;
}
.vds-chapter-radio-content {
max-width: 200px !important;
}
.vds-kb-action {
position: absolute !important;
top: 50% !important;
left: 50% !important;
}
.vds-chapter-radio {
align-items: start !important;
}
.vds-time-divider {
font-size: 0 !important;
margin-right: 5px !important;
}
.sponsorskip-bar {
position: absolute;
margin-top: 2px;
height: 5px;
top: -1;
border-radius: 1em;
}
.vds-time-slider {
position: relative !important;
width: 100% !important;
}
.vds-poster :where(img) {
object-fit: cover !important;
}
.infinite-status-prompt {
display: none !important;
}
@@ -59,6 +17,25 @@ main.root {
overflow-y: auto;
}
article, dialog {
border-radius: .25rem !important;
}
.row {
padding: 1em !important;
}
button.row {
padding: 0 !important;
background-color: transparent !important;
border: none !important;
color: var(--on-surface) !important;
}
button.row:hover {
box-shadow: none !important;
background-color: transparent !important;
}
@media screen and (max-width: 650px) {
dialog.right {
-6
View File
@@ -25,7 +25,6 @@ import {
returnYTDislikesInstanceStore,
returnYtDislikesStore,
showWarningStore,
silenceSkipperStore,
sponsorBlockCategoriesStore,
sponsorBlockDisplayToastStore,
sponsorBlockStore,
@@ -167,11 +166,6 @@ const persistedStores: { name: string, store: Writable<any>, type: 'string' | 'b
store: sponsorBlockDisplayToastStore,
type: 'boolean'
},
{
name: 'silenceSkipper',
store: silenceSkipperStore,
type: 'boolean'
},
{
name: 'amoledTheme',
store: interfaceAmoledTheme,
-14
View File
@@ -29,20 +29,6 @@ export function proxyVideoUrl(source: string): string {
return rawSrc.toString();
}
export function pullBitratePreference(): number {
const vidstack = localStorage.getItem('video-player');
if (vidstack) {
const vidstackSettings = JSON.parse(vidstack);
if (vidstackSettings.quality && vidstackSettings.quality.bitrate) {
return vidstackSettings.quality.bitrate;
}
}
return -1;
}
export function unsafeRandomItem(array: any[]): any {
return array[Math.floor(Math.random() * array.length)];
}
+63 -113
View File
@@ -1,136 +1,87 @@
import { androidPoTokenMinter } from '$lib/android/youtube/minter';
import type { AdaptiveFormats, Captions, Image, StoryBoard, Thumbnail, VideoBase, VideoPlay } from '$lib/api/model';
import { interfaceRegionStore, poTokenCacheStore } from '$lib/store';
import { numberWithCommas } from '$lib/time';
import { Capacitor } from '@capacitor/core';
import type { WebPoSignalOutput } from 'bgutils-js';
import { BG, buildURL, GOOG_API_KEY } from 'bgutils-js';
import { USER_AGENT } from 'bgutils-js';
import { Buffer } from 'buffer';
import { get } from 'svelte/store';
import { Innertube, UniversalCache } from 'youtubei.js';
import { capacitorFetch } from '../android/http/capacitorFetch';
type WebPoMinter = {
integrityTokenBasedMinter?: BG.WebPoMinter;
botguardClient?: BG.BotGuardClient;
};
async function getWebPoMinter(): Promise<WebPoMinter> {
const requestKey = 'O43z0dpjhgX20SCx4KAo';
const challengeResponse = await fetch(buildURL('Create', true), {
method: 'POST',
headers: {
'content-type': 'application/json+protobuf',
'x-goog-api-key': GOOG_API_KEY,
'x-user-agent': 'grpc-web-javascript/0.1'
},
body: JSON.stringify([requestKey])
});
const challengeResponseData = await challengeResponse.json();
const bgChallenge = BG.Challenge.parseChallengeData(challengeResponseData);
if (!bgChallenge)
throw new Error('Could not get challenge');
const interpreterJavascript = bgChallenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
if (!document.getElementById(bgChallenge.interpreterHash)) {
const script = document.createElement('script');
script.type = 'text/javascript';
script.id = bgChallenge.interpreterHash;
script.textContent = interpreterJavascript;
document.head.appendChild(script);
}
const botguardClient = await BG.BotGuardClient.create({
globalObj: globalThis,
globalName: bgChallenge.globalName,
program: bgChallenge.program
});
if (bgChallenge) {
const webPoSignalOutput: WebPoSignalOutput = [];
const botguardResponse = await botguardClient.snapshot({ webPoSignalOutput });
const integrityTokenResponse = await fetch(buildURL('GenerateIT', true), {
method: 'POST',
headers: {
'content-type': 'application/json+protobuf',
'x-goog-api-key': GOOG_API_KEY,
'x-user-agent': 'grpc-web-javacript/0.1'
},
body: JSON.stringify([requestKey, botguardResponse])
});
const integrityTokenResponseData = await integrityTokenResponse.json();
const integrityToken = integrityTokenResponseData[0] as string | undefined;
if (!integrityToken) {
console.error('Could not get integrity token. Interpreter Hash:', bgChallenge.interpreterHash);
return {};
}
const integrityTokenBasedMinter = await BG.WebPoMinter.create({ integrityToken }, webPoSignalOutput);
return {
integrityTokenBasedMinter,
botguardClient
};
}
return {};
}
import { Innertube, UniversalCache, YT, YTNodes } from 'youtubei.js';
export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
if (!Capacitor.isNativePlatform()) {
throw new Error('Platform not supported');
throw new Error('Yt.js: Platform not supported');
}
const youtube = await Innertube.create({
fetch: Capacitor.getPlatform() === 'android' ? capacitorFetch : fetch,
generate_session_locally: true,
fetch: fetch,
cache: new UniversalCache(false),
location: get(interfaceRegionStore)
location: get(interfaceRegionStore),
user_agent: USER_AGENT,
enable_session_cache: false
});
const { integrityTokenBasedMinter } = await getWebPoMinter();
let sessionPoToken: string;
let contentPoToken: string;
let sessionWebPo: string | undefined;
const poTokensCached = get(poTokenCacheStore);
if (!poTokensCached && integrityTokenBasedMinter) {
sessionWebPo = await integrityTokenBasedMinter.mintAsWebsafeString(youtube.session.context.client.visitorData ?? '');
poTokenCacheStore.set(sessionWebPo);
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 {
sessionWebPo = poTokensCached;
[sessionPoToken, contentPoToken] = await window.electronAPI.generatePoToken(
challengeResponse,
requestKey,
youtube.session.context.client.visitorData ?? '',
videoId
);
}
const video = await youtube.getInfo(videoId);
poTokenCacheStore.set(sessionPoToken);
const extraArgs: Record<string, any> = {
playbackContext: {
contentPlaybackContext: {
vis: 0,
splay: false,
lactMilliseconds: '-1',
signatureTimestamp: youtube.session.player?.sts
}
},
serviceIntegrityDimensions: {
poToken: contentPoToken
},
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 video = new YT.VideoInfo([rawPlayerResponse, rawNextResponse], youtube!.actions, '');
console.log(video);
if (!video.primary_info || !video.secondary_info) {
throw new Error('Unable to pull video info from youtube.js');
throw new Error('Yt.js: Unable to pull video info from youtube.js');
}
let dashUri: string = '';
if (!video.basic_info.is_live) {
let manifest = await video.toDash();
let parser = new DOMParser();
let xmlDoc = parser.parseFromString(manifest, 'application/xml');
let baseURLs = xmlDoc.getElementsByTagName('BaseURL');
Array.from(baseURLs).forEach((baseURL) => {
let url = new URL(baseURL.textContent as string);
url.searchParams.set('pot', (sessionWebPo ?? BG.PoToken.generateColdStartToken(youtube.session.context.client.visitorData ?? '')));
baseURL.textContent = url.toString();
});
const serializer = new XMLSerializer();
manifest = serializer.serializeToString(xmlDoc);
dashUri = URL.createObjectURL(new Blob([manifest], { type: 'application/dash+xml;charset=utf8' }));
const manifest = await video.toDash();
dashUri = `data:application/dash+xml;charset=utf-8;base64,${Buffer.from(manifest).toString('base64')}`;
}
const descString = video.secondary_info.description?.toString() || '';
@@ -199,13 +150,11 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
});
}
console.log(video.primary_info);
return {
type: 'video',
title: video.primary_info.title?.toString() || '',
viewCount: Number(video.primary_info.view_count?.original_view_count || 0),
viewCountText: video.primary_info.view_count?.original_view_count.toString() || '0',
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,
@@ -240,6 +189,7 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
subCountText: '',
keywords: video.basic_info.keywords || [],
allowedRegions: [],
fallbackPatch: 'youtubejs'
ytJsVideoInfo: video,
fallbackPatch: 'youtubejs',
};
}
+1 -3
View File
@@ -39,7 +39,6 @@ export const playerTheatreModeByDefaultStore = persisted('theatreModeByDefault',
export const playerAutoplayNextByDefaultStore = persisted('autoplayNextByDefault', false);
export const playerYouTubeJsFallback = persisted('youTubeJsFallback', true);
export const playerYouTubeJsAlways = persisted('youTubeJsAlways', false);
export const playerAndroidBgPlayer = persisted('androidBgPlayer', true);
export const playerAndroidLockOrientation = persisted('androidLockOrientation', true);
export const playerDefaultLanguage = persisted('defaultLanguage', '');
@@ -101,8 +100,7 @@ export const syncPartyConnectionsStore: Writable<DataConnection[] | null> = writ
export const playlistSettingsStore: Writable<Record<string, { shuffle: boolean; loop: boolean; }>> =
writable({});
export const silenceSkipperStore: Writable<boolean> = persisted('silenceSkipper', false);
export const poTokenCacheStore: Writable<string> = writable();
export const poTokenCacheStore: Writable<string | undefined> = writable();
export const searchHistoryStore: Writable<string[]> = persisted('searchHistory', []);
+1 -1
View File
@@ -12,6 +12,7 @@
import Settings from '$lib/components/Settings/Settings.svelte';
import SyncParty from '$lib/components/SyncParty.svelte';
import Thumbnail from '$lib/components/Thumbnail.svelte';
import '$lib/css/global.css';
import { bookmarkletLoadFromUrl, loadSettingsFromEnv } from '$lib/externalSettings';
import { getPages } from '$lib/navPages';
import {
@@ -24,7 +25,6 @@
syncPartyPeerStore,
themeColorStore
} from '$lib/store';
import '$lib/style.css';
import { setAmoledTheme, setStatusBarColor, setTheme } from '$lib/theme';
import { App } from '@capacitor/app';
import { Browser } from '@capacitor/browser';
@@ -1,6 +1,4 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { goto } from '$app/navigation';
import {
addPlaylistVideo,
@@ -20,7 +18,7 @@
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
import { letterCase } from '$lib/letterCasing';
import { truncate, unsafeRandomItem } from '$lib/misc';
import type { PlayerEvents } from '$lib/player';
import type { PlayerEvents } from '$lib/player.js';
import {
activePageStore,
authStore,
@@ -41,11 +39,11 @@
import { onDestroy, onMount, tick } from 'svelte';
import { _ } from 'svelte-i18n';
import { get } from 'svelte/store';
import type { MediaTimeUpdateEvent } from 'vidstack';
import type { MediaPlayerElement } from 'vidstack/elements';
let { data = $bindable() } = $props();
let playerElement: HTMLMediaElement;
let comments: Comments | null = $state(null);
data.streamed.comments?.then((streamedComments) => {
comments = streamedComments;
@@ -70,7 +68,6 @@
let theatreMode = $state(get(playerTheatreModeByDefaultStore));
let audioMode = $state(get(playerListenByDefaultStore));
let player: MediaPlayerElement;
let segments: Segment[] = $state([]);
@@ -79,16 +76,6 @@
let showTranscript = $state(false);
let playerCurrentTime: number = $state(0);
// @ts-ignore
run(() => {
if (typeof player !== 'undefined') {
playerCurrentTime = player.currentTime;
player.addEventListener(
'time-update',
(event: MediaTimeUpdateEvent) => (playerCurrentTime = event.detail.currentTime)
);
}
});
playlistSettingsStore.subscribe((playlistSetting) => {
if (!data.playlistId) return;
@@ -99,9 +86,9 @@
});
function playerSyncEvents(conn: DataConnection) {
if (player) {
if (playerElement) {
conn.send({
events: [{ type: 'seek', time: player.currentTime }]
events: [{ type: 'seek', time: playerElement.currentTime }]
} as PlayerEvents);
}
@@ -120,17 +107,17 @@
const events = rawData as PlayerEvents;
events.events.forEach(async (event) => {
if (!player) return;
if (!playerElement) return;
if (event.type === 'pause') {
player.pause();
playerElement.pause();
} else if (event.type === 'play') {
player.play();
playerElement.play();
} else if (event.type === 'seek' && event.time) {
const timeDiff = player.currentTime - event.time;
const timeDiff = playerElement.currentTime - event.time;
if (timeDiff > 5 || timeDiff < -5) {
player.currentTime = event.time;
playerElement.currentTime = event.time;
}
} else if (
event.type === 'playlist' &&
@@ -144,9 +131,24 @@
});
});
if (!player) return;
if (!playerElement) return;
player.addEventListener('auto-play-fail', () => {
playerElement.addEventListener('error', () => {
if (!playerElement) return;
conn.send({
events: [
{
type: 'seek',
time: playerElement.currentTime
},
{
type: 'play'
}
]
} as PlayerEvents);
});
playerElement.addEventListener('pause', () => {
conn.send({
events: [
{
@@ -156,13 +158,13 @@
} as PlayerEvents);
});
player.addEventListener('auto-play', () => {
if (!player) return;
playerElement.addEventListener('playing', () => {
if (!playerElement) return;
conn.send({
events: [
{
type: 'seek',
time: player.currentTime
time: playerElement.currentTime
},
{
type: 'play'
@@ -171,7 +173,22 @@
} as PlayerEvents);
});
player.addEventListener('pause', () => {
playerElement.addEventListener('play', () => {
if (!playerElement) return;
conn.send({
events: [
{
type: 'seek',
time: playerElement.currentTime
},
{
type: 'play'
}
]
} as PlayerEvents);
});
playerElement.addEventListener('waiting', () => {
conn.send({
events: [
{
@@ -181,53 +198,13 @@
} as PlayerEvents);
});
player.addEventListener('playing', () => {
if (!player) return;
playerElement.addEventListener('seeked', () => {
if (!playerElement) return;
conn.send({
events: [
{
type: 'seek',
time: player.currentTime
},
{
type: 'play'
}
]
} as PlayerEvents);
});
player.addEventListener('play', () => {
if (!player) return;
conn.send({
events: [
{
type: 'seek',
time: player.currentTime
},
{
type: 'play'
}
]
} as PlayerEvents);
});
player.addEventListener('waiting', () => {
conn.send({
events: [
{
type: 'pause'
}
]
} as PlayerEvents);
});
player.addEventListener('seeked', () => {
if (!player) return;
conn.send({
events: [
{
type: 'seek',
time: player.currentTime
time: playerElement.currentTime
}
]
} as PlayerEvents);
@@ -235,7 +212,7 @@
}
syncPartyConnectionsStore.subscribe((connections) => {
if (!connections || !player) return;
if (!connections || !playerElement) return;
playerSyncEvents(connections[connections.length - 1]);
});
@@ -246,8 +223,12 @@
});
}
if (player) {
player.addEventListener('end', async () => {
if (playerElement) {
playerElement.addEventListener('timeupdate', () => {
playerCurrentTime = playerElement.currentTime;
});
playerElement.addEventListener('end', async () => {
if (playlistVideos.length === 0) {
if ($playerAutoplayNextByDefaultStore) {
goto(`/watch/${data.video.recommendedVideos[0].videoId}`);
@@ -404,7 +385,7 @@
clearTimeout(pauseTimeout);
}
pauseTimeout = setTimeout(() => {
player.pause();
// player.pause();
pauseTimerSeconds = 0;
clearTimeout(pauseTimeout);
}, pauseTimerSeconds * 1000);
@@ -426,17 +407,13 @@
<div class={`s12 m12 l${theatreMode ? '12' : '9'}`}>
<div style="display: flex;justify-content: center;">
{#key data.video.videoId}
<div
style="max-height: 80vh;max-width: calc(80vh * 16 / 9);overflow: hidden;position: relative;flex: 1;"
>
<Player
bind:segments
{data}
{audioMode}
isSyncing={$syncPartyPeerStore !== null}
bind:player
/>
</div>
<Player
bind:playerElement
bind:segments
{data}
{audioMode}
isSyncing={$syncPartyPeerStore !== null}
/>
{/key}
</div>
@@ -616,7 +593,9 @@
{#if data.content.timestamps.length > 0}
<h6 style="margin-bottom: .3em;">Chapters</h6>
{#each data.content.timestamps as timestamp}
<button onclick={() => (player.currentTime = timestamp.time)} class="timestamps"
<button
onclick={() => (playerElement.currentTime = timestamp.time)}
class="timestamps"
>{timestamp.timePretty}
{#if !timestamp.title.startsWith('-')}
-
@@ -665,7 +644,7 @@
{#if !theatreMode}
<div class="s12 m12 l3">
{#if showTranscript}
<Transcript bind:player video={data.video} />
<Transcript video={data.video} bind:playerElement />
{/if}
{#if playlist}
<article
@@ -1,10 +1,9 @@
<script lang="ts">
import Player from '$lib/components/Player.svelte';
import type { MediaPlayerElement } from 'vidstack/elements';
let { data } = $props();
let player: MediaPlayerElement;
let playerElement: HTMLMediaElement;
</script>
<Player bind:player isEmbed={true} {data} />
<Player bind:playerElement isEmbed={true} {data} />
+15 -7
View File
@@ -3,12 +3,17 @@ const https = require('https');
const HOST = 'localhost';
const PORT = 3000;
const MAX_REDIRECTS = 5;
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)'
function setCorsHeaders(res) {
res.setHeader('Access-Control-Allow-Origin', '*');
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', '*');
res.setHeader('Access-Control-Allow-Headers', CORS_HEADERS);
res.setHeader('Access-Control-Max-Age', '86400')
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
@@ -16,7 +21,6 @@ function fetchWithRedirects(targetUrl, options, 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) {
@@ -63,7 +67,7 @@ const server = http.createServer(async (req, res) => {
let parsedTarget;
try {
// Ensure protocol (http) is added if missing
if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
if (!targetUrl.startsWith('http')) {
targetUrl = 'http://' + targetUrl;
}
parsedTarget = new URL(targetUrl);
@@ -87,6 +91,10 @@ const server = http.createServer(async (req, res) => {
)
};
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);
@@ -103,9 +111,9 @@ const server = http.createServer(async (req, res) => {
res.writeHead(proxyRes.statusCode, {
...proxyRes.headers,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Origin': CORS_ORIGIN,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Headers': CORS_HEADERS,
'Access-Control-Allow-Credentials': 'true',
});
+1 -1
View File
@@ -11,7 +11,7 @@
"strict": true,
"moduleResolution": "bundler",
"types": [
"vidstack/svelte"
"shaka-player"
]
},
"include": [
-2
View File
@@ -1,6 +1,5 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { SvelteKitPWA } from '@vite-pwa/sveltekit';
import { vite as vidstack } from 'vidstack/plugins';
import { defineConfig } from 'vite';
@@ -34,7 +33,6 @@ export default defineConfig({
name: 'Materialious'
}
}),
vidstack(),
sveltekit()
],
ssr: {
+1 -1
View File
@@ -3,7 +3,7 @@ import os
import re
from datetime import datetime
LATEST_VERSION = "1.7.14"
LATEST_VERSION = "1.7.15"
RELEASE_DATE = datetime.now().strftime("%Y-%-m-%d") # Format: YYYY-M-D
WORKING_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "materialious")