@@ -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: |
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
Generated
+105
-6
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Generated
+455
-10
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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)
|
||||
];
|
||||
});
|
||||
@@ -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
|
||||
)
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
}
|
||||
|
||||
Generated
+827
-1345
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
declare module 'virtual:pwa-info';
|
||||
declare module 'virtual:pwa-info';
|
||||
|
||||
Vendored
+13
@@ -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)
|
||||
];
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -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,
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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', []);
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler",
|
||||
"types": [
|
||||
"vidstack/svelte"
|
||||
"shaka-player"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user