Initial commit: Plex Auto Skip Chrome extension
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
name: Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
- run: npm install
|
||||||
|
- run: npm test
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
.DS_Store
|
||||||
|
*.zip
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Justin Oros
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Plex Auto Skip
|
||||||
|
|
||||||
|
A small Chrome extension that automatically clicks "Skip Intro" and "Skip Credits" on Plex Web, so you don't have to reach for the mouse every time.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Watches the Plex Web player and clicks "Skip Intro" / "Skip Credits" the instant either button appears.
|
||||||
|
- Works on `app.plex.tv` by default, or point it at your own server address (local IP, custom domain, whatever you use).
|
||||||
|
- Toggle Skip Intro and Skip Credits independently — both on by default.
|
||||||
|
- Settings save instantly and apply live, no reload required for toggle changes.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
1. Download or clone this repo.
|
||||||
|
2. Open `chrome://extensions` in Chrome.
|
||||||
|
3. Turn on **Developer mode** (top right toggle).
|
||||||
|
4. Click **Load unpacked** and select this project's folder.
|
||||||
|
5. Click the extension's icon in the toolbar to open its settings.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
- **Plex URL** — defaults to `app.plex.tv`. Choose "Custom URL" and enter your own server address (e.g. `192.168.1.50:32400/web` or a custom domain) if you watch there instead.
|
||||||
|
- **Skip Intro** / **Skip Credits** — both on by default. Uncheck either one if you'd rather click it yourself sometimes.
|
||||||
|
|
||||||
|
Changing the Plex URL takes effect on your next page load — refresh your Plex tab after saving a new one. Toggle changes apply immediately.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
The extension's content script runs on every page but only activates once the page's hostname matches your configured Plex URL. From there it uses a `MutationObserver` to watch the page for any `<button>` or `[role="button"]` element whose visible text contains "skip intro" or "skip credit" (case-insensitive, so it still matches if Plex appends a countdown like "Skip Intro 0:05"). When a match appears and is actually visible, it clicks it — with a short cooldown per element so a single button can't be double-clicked in one burst, but can still be clicked again later if Plex reuses the same button for the next episode.
|
||||||
|
|
||||||
|
Settings (your Plex URL and the two toggles) are stored with `chrome.storage.sync`, so they follow you across machines if you're signed into Chrome.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
This project has an automated test suite that simulates the Plex Web DOM with [jsdom](https://github.com/jsdom/jsdom) and exercises the real extension code — no browser or live Plex server required.
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
The suite covers: clicking Skip Intro/Skip Credits, ignoring non-Plex pages, respecting the on/off toggles, matching a custom server URL, reacting to buttons added dynamically after page load, ignoring unrelated buttons, handling countdown suffixes in the button text, not clicking hidden buttons, correctly re-clicking a reused button after its cooldown, and the settings popup's save/validation/load behavior.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If skipping stops working after a Plex Web update, it's most likely because Plex changed the button markup. Open DevTools on the player page, inspect the Skip Intro/Skip Credits button, and check whether it's still a `<button>` or `[role="button"]` element with "Skip Intro"/"Skip Credits" somewhere in its visible text. Open an issue (or a PR) with what you find.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — see [LICENSE](LICENSE).
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
(function () {
|
||||||
|
let settings = DEFAULT_SETTINGS;
|
||||||
|
const recentClicks = new WeakMap();
|
||||||
|
const CLICK_COOLDOWN_MS = 1500;
|
||||||
|
|
||||||
|
function isSkipIntroButton(text) {
|
||||||
|
return text.indexOf("skip intro") !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSkipCreditsButton(text) {
|
||||||
|
return text.indexOf("skip credit") !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVisible(el) {
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
function canClick(el) {
|
||||||
|
const last = recentClicks.get(el);
|
||||||
|
if (last && Date.now() - last < CLICK_COOLDOWN_MS) return false;
|
||||||
|
recentClicks.set(el, Date.now());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanAndClick() {
|
||||||
|
if (!settings.skipIntro && !settings.skipCredits) return;
|
||||||
|
if (!hostMatchesConfig(window.location.hostname, settings.plexUrl)) return;
|
||||||
|
|
||||||
|
const nodes = document.querySelectorAll('button, [role="button"]');
|
||||||
|
nodes.forEach((el) => {
|
||||||
|
const text = (el.innerText || el.textContent || "").trim().toLowerCase();
|
||||||
|
if (!text) return;
|
||||||
|
if (!isVisible(el)) return;
|
||||||
|
|
||||||
|
const wantsIntroSkip = settings.skipIntro && isSkipIntroButton(text);
|
||||||
|
const wantsCreditsSkip = settings.skipCredits && isSkipCreditsButton(text);
|
||||||
|
if (!wantsIntroSkip && !wantsCreditsSkip) return;
|
||||||
|
|
||||||
|
if (canClick(el)) el.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
const observer = new MutationObserver(scanAndClick);
|
||||||
|
observer.observe(document.documentElement, { childList: true, subtree: true });
|
||||||
|
scanAndClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.storage.sync.get(DEFAULT_SETTINGS, (loaded) => {
|
||||||
|
settings = loaded;
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.storage.onChanged.addListener((changes, area) => {
|
||||||
|
if (area !== "sync") return;
|
||||||
|
Object.keys(changes).forEach((key) => {
|
||||||
|
settings[key] = changes[key].newValue;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Plex Auto Skip",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Automatically clicks Skip Intro and Skip Credits buttons on Plex Web.",
|
||||||
|
"permissions": ["storage"],
|
||||||
|
"host_permissions": ["<all_urls>"],
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup.html"
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"js": ["utils.js", "content.js"],
|
||||||
|
"run_at": "document_idle"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Generated
+846
@@ -0,0 +1,846 @@
|
|||||||
|
{
|
||||||
|
"name": "plex-auto-skip",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "plex-auto-skip",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"jsdom": "^24.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@asamuzakjp/css-color": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@csstools/css-calc": "^2.1.3",
|
||||||
|
"@csstools/css-color-parser": "^3.0.9",
|
||||||
|
"@csstools/css-parser-algorithms": "^3.0.4",
|
||||||
|
"@csstools/css-tokenizer": "^3.0.3",
|
||||||
|
"lru-cache": "^10.4.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/color-helpers": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
|
||||||
|
"dev": true,
|
||||||
|
"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.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
|
||||||
|
"dev": true,
|
||||||
|
"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.5",
|
||||||
|
"@csstools/css-tokenizer": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-color-parser": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@csstools/color-helpers": "^5.1.0",
|
||||||
|
"@csstools/css-calc": "^2.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||||
|
"@csstools/css-tokenizer": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-parser-algorithms": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
|
||||||
|
"dev": true,
|
||||||
|
"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.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-tokenizer": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/agent-base": {
|
||||||
|
"version": "7.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"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/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cssstyle": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@asamuzakjp/css-color": "^3.2.0",
|
||||||
|
"rrweb-cssom": "^0.8.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cssstyle/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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-mimetype": "^4.0.0",
|
||||||
|
"whatwg-url": "^14.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/decimal.js": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.4",
|
||||||
|
"mime-types": "^2.1.35"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-encoding": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.0",
|
||||||
|
"debug": "^4.3.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"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,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/jsdom": {
|
||||||
|
"version": "24.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.3.tgz",
|
||||||
|
"integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cssstyle": "^4.0.1",
|
||||||
|
"data-urls": "^5.0.0",
|
||||||
|
"decimal.js": "^10.4.3",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"html-encoding-sniffer": "^4.0.0",
|
||||||
|
"http-proxy-agent": "^7.0.2",
|
||||||
|
"https-proxy-agent": "^7.0.5",
|
||||||
|
"is-potential-custom-element-name": "^1.0.1",
|
||||||
|
"nwsapi": "^2.2.12",
|
||||||
|
"parse5": "^7.1.2",
|
||||||
|
"rrweb-cssom": "^0.7.1",
|
||||||
|
"saxes": "^6.0.0",
|
||||||
|
"symbol-tree": "^3.2.4",
|
||||||
|
"tough-cookie": "^4.1.4",
|
||||||
|
"w3c-xmlserializer": "^5.0.0",
|
||||||
|
"webidl-conversions": "^7.0.0",
|
||||||
|
"whatwg-encoding": "^3.1.1",
|
||||||
|
"whatwg-mimetype": "^4.0.0",
|
||||||
|
"whatwg-url": "^14.0.0",
|
||||||
|
"ws": "^8.18.0",
|
||||||
|
"xml-name-validator": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"canvas": "^2.11.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"canvas": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/nwsapi": {
|
||||||
|
"version": "2.2.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
|
||||||
|
"integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/parse5": {
|
||||||
|
"version": "7.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||||
|
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"entities": "^6.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/psl": {
|
||||||
|
"version": "1.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
|
||||||
|
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.3.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/lupomontero"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/punycode": {
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/querystringify": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/requires-port": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/rrweb-cssom": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
|
||||||
|
"integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"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,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/saxes": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"xmlchars": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v12.22.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tough-cookie": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"psl": "^1.1.33",
|
||||||
|
"punycode": "^2.1.1",
|
||||||
|
"universalify": "^0.2.0",
|
||||||
|
"url-parse": "^1.5.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/universalify": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/url-parse": {
|
||||||
|
"version": "1.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||||
|
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"querystringify": "^2.1.1",
|
||||||
|
"requires-port": "^1.0.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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"xml-name-validator": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"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==",
|
||||||
|
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
|
||||||
|
"dev": true,
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "^5.1.0",
|
||||||
|
"webidl-conversions": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.21.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
|
||||||
|
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
|
||||||
|
"dev": true,
|
||||||
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlchars": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "plex-auto-skip",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Chrome extension that automatically clicks Skip Intro and Skip Credits on Plex Web.",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"test": "node --test tests/*.test.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"jsdom": "^24.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
width: 280px;
|
||||||
|
padding: 16px;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 15px;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
select, input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
#customUrlRow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.toggle-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.toggle-row:first-of-type {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
.toggle-row span {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
button.save {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #e5a00d;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button.save:hover {
|
||||||
|
background: #cc8f0b;
|
||||||
|
}
|
||||||
|
#status {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #2a8f2a;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Plex Auto Skip</h1>
|
||||||
|
|
||||||
|
<label for="urlPreset">Plex URL</label>
|
||||||
|
<select id="urlPreset">
|
||||||
|
<option value="default">app.plex.tv (default)</option>
|
||||||
|
<option value="custom">Custom URL</option>
|
||||||
|
</select>
|
||||||
|
<div id="customUrlRow">
|
||||||
|
<input type="text" id="customUrl" placeholder="e.g. 192.168.1.50:32400/web">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toggle-row">
|
||||||
|
<span>Skip Intro</span>
|
||||||
|
<input type="checkbox" id="skipIntro">
|
||||||
|
</div>
|
||||||
|
<div class="toggle-row">
|
||||||
|
<span>Skip Credits</span>
|
||||||
|
<input type="checkbox" id="skipCredits">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="save" id="saveBtn">Save</button>
|
||||||
|
<div id="status"></div>
|
||||||
|
|
||||||
|
<script src="utils.js"></script>
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const urlPreset = document.getElementById("urlPreset");
|
||||||
|
const customUrlRow = document.getElementById("customUrlRow");
|
||||||
|
const customUrl = document.getElementById("customUrl");
|
||||||
|
const skipIntro = document.getElementById("skipIntro");
|
||||||
|
const skipCredits = document.getElementById("skipCredits");
|
||||||
|
const saveBtn = document.getElementById("saveBtn");
|
||||||
|
const status = document.getElementById("status");
|
||||||
|
|
||||||
|
function toggleCustomRow() {
|
||||||
|
customUrlRow.style.display = urlPreset.value === "custom" ? "block" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
urlPreset.addEventListener("change", toggleCustomRow);
|
||||||
|
|
||||||
|
chrome.storage.sync.get(DEFAULT_SETTINGS, (settings) => {
|
||||||
|
if (settings.plexUrl === DEFAULT_SETTINGS.plexUrl) {
|
||||||
|
urlPreset.value = "default";
|
||||||
|
} else {
|
||||||
|
urlPreset.value = "custom";
|
||||||
|
customUrl.value = settings.plexUrl.replace(/^https?:\/\//, "");
|
||||||
|
}
|
||||||
|
skipIntro.checked = settings.skipIntro;
|
||||||
|
skipCredits.checked = settings.skipCredits;
|
||||||
|
toggleCustomRow();
|
||||||
|
});
|
||||||
|
|
||||||
|
function showStatus(message, isError) {
|
||||||
|
status.textContent = message;
|
||||||
|
status.style.color = isError ? "#c0392b" : "#2a8f2a";
|
||||||
|
setTimeout(() => {
|
||||||
|
status.textContent = "";
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveBtn.addEventListener("click", () => {
|
||||||
|
const isCustom = urlPreset.value === "custom";
|
||||||
|
let plexUrl = DEFAULT_SETTINGS.plexUrl;
|
||||||
|
|
||||||
|
if (isCustom) {
|
||||||
|
const trimmed = customUrl.value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
showStatus("Enter a URL first", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plexUrl = normalizePlexUrl(trimmed);
|
||||||
|
if (!getHostFromUrl(plexUrl)) {
|
||||||
|
showStatus("That URL doesn't look valid", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.storage.sync.set(
|
||||||
|
{
|
||||||
|
plexUrl: plexUrl,
|
||||||
|
skipIntro: skipIntro.checked,
|
||||||
|
skipCredits: skipCredits.checked
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
showStatus("Saved", false);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { loadContentScript, wait } = require("./helpers");
|
||||||
|
|
||||||
|
test("clicks a Skip Intro button on app.plex.tv", async () => {
|
||||||
|
const { window } = loadContentScript({
|
||||||
|
url: "https://app.plex.tv/web/index.html",
|
||||||
|
html: `<!DOCTYPE html><html><body><button>Skip Intro</button></body></html>`
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(50);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clicks a Skip Credits button on app.plex.tv", async () => {
|
||||||
|
const { window } = loadContentScript({
|
||||||
|
url: "https://app.plex.tv/web/index.html",
|
||||||
|
html: `<!DOCTYPE html><html><body><button>Skip Credits</button></body></html>`
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(50);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does nothing on a non-matching host", async () => {
|
||||||
|
const { window } = loadContentScript({
|
||||||
|
url: "https://example.com/web/index.html",
|
||||||
|
html: `<!DOCTYPE html><html><body><button>Skip Intro</button></body></html>`
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(50);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("respects skipIntro:false toggle", async () => {
|
||||||
|
const { window } = loadContentScript({
|
||||||
|
url: "https://app.plex.tv/web/index.html",
|
||||||
|
html: `<!DOCTYPE html><html><body><button>Skip Intro</button></body></html>`,
|
||||||
|
settings: { skipIntro: false, skipCredits: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(50);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("matches a custom configured URL by hostname", async () => {
|
||||||
|
const { window } = loadContentScript({
|
||||||
|
url: "http://192.168.1.50:32400/web/index.html",
|
||||||
|
html: `<!DOCTYPE html><html><body><button>Skip Intro</button></body></html>`,
|
||||||
|
settings: { plexUrl: "http://192.168.1.50:32400/web" }
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(50);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clicks a button added dynamically after page load", async () => {
|
||||||
|
const { window } = loadContentScript({
|
||||||
|
url: "https://app.plex.tv/web/index.html",
|
||||||
|
html: `<!DOCTYPE html><html><body></body></html>`
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(20);
|
||||||
|
|
||||||
|
const btn = window.document.createElement("button");
|
||||||
|
btn.textContent = "Skip Intro";
|
||||||
|
window.document.body.appendChild(btn);
|
||||||
|
|
||||||
|
await wait(50);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not click an unrelated button", async () => {
|
||||||
|
const { window } = loadContentScript({
|
||||||
|
url: "https://app.plex.tv/web/index.html",
|
||||||
|
html: `<!DOCTYPE html><html><body><button>Watch Now</button></body></html>`
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(50);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("real Plex markup: skip button includes a countdown suffix", async () => {
|
||||||
|
const { window } = loadContentScript({
|
||||||
|
url: "https://app.plex.tv/web/index.html",
|
||||||
|
html: `<!DOCTYPE html><html><body><button><span>Skip Intro</span><span> 0:05</span></button></body></html>`
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(50);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("same DOM node reused for a later segment still gets clicked after cooldown", async () => {
|
||||||
|
const { window } = loadContentScript({
|
||||||
|
url: "https://app.plex.tv/web/index.html",
|
||||||
|
html: `<!DOCTYPE html><html><body><button id="skip">Skip Intro</button></body></html>`
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(50);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 1, "first segment's skip button should be clicked");
|
||||||
|
|
||||||
|
const btn = window.document.getElementById("skip");
|
||||||
|
btn.textContent = "";
|
||||||
|
await wait(1700);
|
||||||
|
|
||||||
|
btn.textContent = "Skip Intro";
|
||||||
|
await wait(50);
|
||||||
|
assert.strictEqual(
|
||||||
|
window.__clickedElements.length,
|
||||||
|
2,
|
||||||
|
"the same reused button should be clickable again after the cooldown"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not click a hidden skip button", async () => {
|
||||||
|
const { window } = loadContentScript({
|
||||||
|
url: "https://app.plex.tv/web/index.html",
|
||||||
|
html: `<!DOCTYPE html><html><body><button style="display:none">Skip Intro</button></body></html>`
|
||||||
|
});
|
||||||
|
|
||||||
|
await wait(50);
|
||||||
|
assert.strictEqual(window.__clickedElements.length, 0);
|
||||||
|
});
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { JSDOM } = require("jsdom");
|
||||||
|
|
||||||
|
function loadContentScript({ url, html, settings }) {
|
||||||
|
const dom = new JSDOM(html || "<!DOCTYPE html><html><body></body></html>", {
|
||||||
|
url: url,
|
||||||
|
runScripts: "dangerously"
|
||||||
|
});
|
||||||
|
|
||||||
|
const window = dom.window;
|
||||||
|
const storedSettings = Object.assign(
|
||||||
|
{ plexUrl: "https://app.plex.tv", skipIntro: true, skipCredits: true },
|
||||||
|
settings || {}
|
||||||
|
);
|
||||||
|
const changeListeners = [];
|
||||||
|
|
||||||
|
window.__clickedElements = [];
|
||||||
|
const originalClick = window.HTMLElement.prototype.click;
|
||||||
|
window.HTMLElement.prototype.click = function () {
|
||||||
|
window.__clickedElements.push(this);
|
||||||
|
return originalClick.call(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.chrome = {
|
||||||
|
storage: {
|
||||||
|
sync: {
|
||||||
|
get: (defaults, cb) => cb(Object.assign({}, defaults, storedSettings)),
|
||||||
|
set: (obj, cb) => {
|
||||||
|
Object.assign(storedSettings, obj);
|
||||||
|
if (cb) cb();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onChanged: {
|
||||||
|
addListener: (fn) => changeListeners.push(fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const utilsCode = fs.readFileSync(path.join(__dirname, "..", "utils.js"), "utf8");
|
||||||
|
const contentCode = fs.readFileSync(path.join(__dirname, "..", "content.js"), "utf8");
|
||||||
|
window.eval(utilsCode + "\n" + contentCode);
|
||||||
|
|
||||||
|
function fireStorageChange(changes) {
|
||||||
|
const wrapped = {};
|
||||||
|
Object.keys(changes).forEach((k) => {
|
||||||
|
wrapped[k] = { newValue: changes[k] };
|
||||||
|
});
|
||||||
|
changeListeners.forEach((fn) => fn(wrapped, "sync"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dom, window, fireStorageChange };
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPopupScript({ initialSettings } = {}) {
|
||||||
|
const html = fs.readFileSync(path.join(__dirname, "..", "popup.html"), "utf8");
|
||||||
|
const dom = new JSDOM(html, { url: "chrome-extension://fake/popup.html", runScripts: "dangerously" });
|
||||||
|
const window = dom.window;
|
||||||
|
const storedSettings = Object.assign(
|
||||||
|
{ plexUrl: "https://app.plex.tv", skipIntro: true, skipCredits: true },
|
||||||
|
initialSettings || {}
|
||||||
|
);
|
||||||
|
const savedCalls = [];
|
||||||
|
|
||||||
|
window.chrome = {
|
||||||
|
storage: {
|
||||||
|
sync: {
|
||||||
|
get: (defaults, cb) => cb(Object.assign({}, defaults, storedSettings)),
|
||||||
|
set: (obj, cb) => {
|
||||||
|
savedCalls.push(obj);
|
||||||
|
Object.assign(storedSettings, obj);
|
||||||
|
if (cb) cb();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const utilsCode = fs.readFileSync(path.join(__dirname, "..", "utils.js"), "utf8");
|
||||||
|
const popupCode = fs.readFileSync(path.join(__dirname, "..", "popup.js"), "utf8");
|
||||||
|
window.eval(utilsCode + "\n" + popupCode);
|
||||||
|
|
||||||
|
return { dom, window, savedCalls, storedSettings };
|
||||||
|
}
|
||||||
|
|
||||||
|
function wait(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { loadContentScript, loadPopupScript, wait };
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { loadPopupScript, wait } = require("./helpers");
|
||||||
|
|
||||||
|
test("defaults to app.plex.tv preset with both toggles on", async () => {
|
||||||
|
const { window } = loadPopupScript();
|
||||||
|
await wait(20);
|
||||||
|
|
||||||
|
assert.strictEqual(window.document.getElementById("urlPreset").value, "default");
|
||||||
|
assert.strictEqual(window.document.getElementById("skipIntro").checked, true);
|
||||||
|
assert.strictEqual(window.document.getElementById("skipCredits").checked, true);
|
||||||
|
assert.strictEqual(window.document.getElementById("customUrlRow").style.display, "none");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows custom URL field pre-filled when a custom URL is already saved", async () => {
|
||||||
|
const { window } = loadPopupScript({
|
||||||
|
initialSettings: { plexUrl: "http://192.168.1.50:32400/web" }
|
||||||
|
});
|
||||||
|
await wait(20);
|
||||||
|
|
||||||
|
assert.strictEqual(window.document.getElementById("urlPreset").value, "custom");
|
||||||
|
assert.strictEqual(window.document.getElementById("customUrl").value, "192.168.1.50:32400/web");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("saving the default preset persists the default Plex URL", async () => {
|
||||||
|
const { window, savedCalls } = loadPopupScript();
|
||||||
|
await wait(20);
|
||||||
|
|
||||||
|
window.document.getElementById("saveBtn").click();
|
||||||
|
await wait(20);
|
||||||
|
|
||||||
|
assert.strictEqual(savedCalls.length, 1);
|
||||||
|
assert.strictEqual(savedCalls[0].plexUrl, "https://app.plex.tv");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("saving a custom URL normalizes and persists it", async () => {
|
||||||
|
const { window, savedCalls } = loadPopupScript();
|
||||||
|
await wait(20);
|
||||||
|
|
||||||
|
const urlPreset = window.document.getElementById("urlPreset");
|
||||||
|
urlPreset.value = "custom";
|
||||||
|
urlPreset.dispatchEvent(new window.Event("change"));
|
||||||
|
window.document.getElementById("customUrl").value = "192.168.1.50:32400/web";
|
||||||
|
|
||||||
|
window.document.getElementById("saveBtn").click();
|
||||||
|
await wait(20);
|
||||||
|
|
||||||
|
assert.strictEqual(savedCalls.length, 1);
|
||||||
|
assert.strictEqual(savedCalls[0].plexUrl, "http://192.168.1.50:32400/web");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("saving Custom URL with a blank field does not silently fall back to the default", async () => {
|
||||||
|
const { window, savedCalls } = loadPopupScript();
|
||||||
|
await wait(20);
|
||||||
|
|
||||||
|
const urlPreset = window.document.getElementById("urlPreset");
|
||||||
|
urlPreset.value = "custom";
|
||||||
|
urlPreset.dispatchEvent(new window.Event("change"));
|
||||||
|
window.document.getElementById("customUrl").value = " ";
|
||||||
|
|
||||||
|
window.document.getElementById("saveBtn").click();
|
||||||
|
await wait(20);
|
||||||
|
|
||||||
|
assert.strictEqual(savedCalls.length, 0, "should not save when the custom URL field is blank");
|
||||||
|
assert.match(window.document.getElementById("status").textContent, /enter|url/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("saving an unparseable custom URL is rejected with an error, not saved", async () => {
|
||||||
|
const { window, savedCalls } = loadPopupScript();
|
||||||
|
await wait(20);
|
||||||
|
|
||||||
|
const urlPreset = window.document.getElementById("urlPreset");
|
||||||
|
urlPreset.value = "custom";
|
||||||
|
urlPreset.dispatchEvent(new window.Event("change"));
|
||||||
|
window.document.getElementById("customUrl").value = "http://";
|
||||||
|
|
||||||
|
window.document.getElementById("saveBtn").click();
|
||||||
|
await wait(20);
|
||||||
|
|
||||||
|
assert.strictEqual(savedCalls.length, 0, "should not save an invalid URL");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unchecking a toggle and saving persists it as false", async () => {
|
||||||
|
const { window, savedCalls } = loadPopupScript();
|
||||||
|
await wait(20);
|
||||||
|
|
||||||
|
window.document.getElementById("skipCredits").checked = false;
|
||||||
|
window.document.getElementById("saveBtn").click();
|
||||||
|
await wait(20);
|
||||||
|
|
||||||
|
assert.strictEqual(savedCalls.length, 1);
|
||||||
|
assert.strictEqual(savedCalls[0].skipCredits, false);
|
||||||
|
assert.strictEqual(savedCalls[0].skipIntro, true);
|
||||||
|
});
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
const DEFAULT_SETTINGS = {
|
||||||
|
plexUrl: "https://app.plex.tv",
|
||||||
|
skipIntro: true,
|
||||||
|
skipCredits: true
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizePlexUrl(input) {
|
||||||
|
let value = (input || "").trim();
|
||||||
|
if (!value) return DEFAULT_SETTINGS.plexUrl;
|
||||||
|
if (!/^https?:\/\//i.test(value)) value = "http://" + value;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHostFromUrl(urlString) {
|
||||||
|
try {
|
||||||
|
return new URL(urlString).hostname;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hostMatchesConfig(currentHostname, configuredUrl) {
|
||||||
|
const configuredHost = getHostFromUrl(configuredUrl);
|
||||||
|
if (!configuredHost) return false;
|
||||||
|
return currentHostname === configuredHost;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user